From 0f476d49f8d4aa84210392bf13b59afc67b32b31 Mon Sep 17 00:00:00 2001 From: Senthil Kumaran Date: Thu, 30 Sep 2010 06:09:18 +0000 Subject: Issue1491 - BaseHTTPServer incorrectly implements response code 100 --- Doc/library/http.server.rst | 20 +++++++++ Lib/http/server.py | 33 +++++++++++++-- Lib/test/test_httpservers.py | 98 ++++++++++++++++++++++++++++++++++++++++++++ Misc/NEWS | 3 ++ 4 files changed, 151 insertions(+), 3 deletions(-) diff --git a/Doc/library/http.server.rst b/Doc/library/http.server.rst index 1ca1620..2f28012 100644 --- a/Doc/library/http.server.rst +++ b/Doc/library/http.server.rst @@ -155,6 +155,17 @@ of which this module provides three different variants: This method will parse and dispatch the request to the appropriate :meth:`do_\*` method. You should never need to override it. + .. method:: handle_expect_100() + + When a HTTP/1.1 compliant server receives a ``Expect: 100-continue`` + request header it responds back with a ``100 Continue`` followed by ``200 + OK`` headers. + This method can be overridden to raise an error if the server does not + want the client to continue. For e.g. server can chose to send ``417 + Expectation Failed`` as a response header and ``return False``. + + .. versionadded:: 3.2 + .. method:: send_error(code, message=None) Sends and logs a complete error reply to the client. The numeric *code* @@ -174,6 +185,15 @@ of which this module provides three different variants: Writes a specific HTTP header to the output stream. *keyword* should specify the header keyword, with *value* specifying its value. + .. method:: send_response_only(code, message=None) + + Sends the reponse header only, used for the purposes when ``100 + Continue`` response is sent by the server to the client. If the *message* + is not specified, the HTTP message corresponding the response *code* is + sent. + + .. versionadded:: 3.2 + .. method:: end_headers() Sends a blank line, indicating the end of the HTTP headers in the diff --git a/Lib/http/server.py b/Lib/http/server.py index 098ad25..4fa58a2 100644 --- a/Lib/http/server.py +++ b/Lib/http/server.py @@ -322,6 +322,30 @@ class BaseHTTPRequestHandler(socketserver.StreamRequestHandler): elif (conntype.lower() == 'keep-alive' and self.protocol_version >= "HTTP/1.1"): self.close_connection = 0 + # Examine the headers and look for an Expect directive + expect = self.headers.get('Expect', "") + if (expect.lower() == "100-continue" and + self.protocol_version >= "HTTP/1.1" and + self.request_version >= "HTTP/1.1"): + if not self.handle_expect_100(): + return False + return True + + def handle_expect_100(self): + """Decide what to do with an "Expect: 100-continue" header. + + If the client is expecting a 100 Continue response, we must + respond with either a 100 Continue or a final response before + waiting for the request body. The default is to always respond + with a 100 Continue. You can behave differently (for example, + reject unauthorized requests) by overriding this method. + + This method should either return True (possibly after sending + a 100 Continue response) or send an error response and return + False. + + """ + self.send_response_only(100) return True def handle_one_request(self): @@ -400,6 +424,12 @@ class BaseHTTPRequestHandler(socketserver.StreamRequestHandler): """ self.log_request(code) + self.send_response_only(code, message) + self.send_header('Server', self.version_string()) + self.send_header('Date', self.date_time_string()) + + def send_response_only(self, code, message=None): + """Send the response header only.""" if message is None: if code in self.responses: message = self.responses[code][0] @@ -408,9 +438,6 @@ class BaseHTTPRequestHandler(socketserver.StreamRequestHandler): if self.request_version != 'HTTP/0.9': self.wfile.write(("%s %d %s\r\n" % (self.protocol_version, code, message)).encode('ASCII', 'strict')) - # print (self.protocol_version, code, message) - self.send_header('Server', self.version_string()) - self.send_header('Date', self.date_time_string()) def send_header(self, keyword, value): """Send a MIME header.""" diff --git a/Lib/test/test_httpservers.py b/Lib/test/test_httpservers.py index b276c73..23fc7e4 100644 --- a/Lib/test/test_httpservers.py +++ b/Lib/test/test_httpservers.py @@ -10,11 +10,13 @@ from http import server import os import sys +import re import base64 import shutil import urllib.parse import http.client import tempfile +from io import BytesIO import unittest from test import support @@ -403,8 +405,103 @@ class CGIHTTPServerTestCase(BaseTestCase): class SocketlessRequestHandler(SimpleHTTPRequestHandler): def __init__(self): + self.get_called = False + self.protocol_version = "HTTP/1.1" + + def do_GET(self): + self.get_called = True + self.send_response(200) + self.send_header('Content-Type', 'text/html') + self.end_headers() + self.wfile.write(b'Data\r\n') + + def log_message(self, format, *args): pass +class RejectingSocketlessRequestHandler(SocketlessRequestHandler): + def handle_expect_100(self): + self.send_error(417) + return False + +class BaseHTTPRequestHandlerTestCase(unittest.TestCase): + """Test the functionaility of the BaseHTTPServer. + + Test the support for the Expect 100-continue header. + """ + + HTTPResponseMatch = re.compile(b'HTTP/1.[0-9]+ 200 OK') + + def setUp (self): + self.handler = SocketlessRequestHandler() + + def send_typical_request(self, message): + input = BytesIO(message) + output = BytesIO() + self.handler.rfile = input + self.handler.wfile = output + self.handler.handle_one_request() + output.seek(0) + return output.readlines() + + def verify_get_called(self): + self.assertTrue(self.handler.get_called) + + def verify_expected_headers(self, headers): + for fieldName in b'Server: ', b'Date: ', b'Content-Type: ': + self.assertEqual(sum(h.startswith(fieldName) for h in headers), 1) + + def verify_http_server_response(self, response): + match = self.HTTPResponseMatch.search(response) + self.assertTrue(match is not None) + + def test_http_1_1(self): + result = self.send_typical_request(b'GET / HTTP/1.1\r\n\r\n') + self.verify_http_server_response(result[0]) + self.verify_expected_headers(result[1:-1]) + self.verify_get_called() + self.assertEqual(result[-1], b'Data\r\n') + + def test_http_1_0(self): + result = self.send_typical_request(b'GET / HTTP/1.0\r\n\r\n') + self.verify_http_server_response(result[0]) + self.verify_expected_headers(result[1:-1]) + self.verify_get_called() + self.assertEqual(result[-1], b'Data\r\n') + + def test_http_0_9(self): + result = self.send_typical_request(b'GET / HTTP/0.9\r\n\r\n') + self.assertEqual(len(result), 1) + self.assertEqual(result[0], b'Data\r\n') + self.verify_get_called() + + def test_with_continue_1_0(self): + result = self.send_typical_request(b'GET / HTTP/1.0\r\nExpect: 100-continue\r\n\r\n') + self.verify_http_server_response(result[0]) + self.verify_expected_headers(result[1:-1]) + self.verify_get_called() + self.assertEqual(result[-1], b'Data\r\n') + + def test_with_continue_1_1(self): + result = self.send_typical_request(b'GET / HTTP/1.1\r\nExpect: 100-continue\r\n\r\n') + self.assertEqual(result[0], b'HTTP/1.1 100 Continue\r\n') + self.assertEqual(result[1], b'HTTP/1.1 200 OK\r\n') + self.verify_expected_headers(result[2:-1]) + self.verify_get_called() + self.assertEqual(result[-1], b'Data\r\n') + + def test_with_continue_rejected(self): + usual_handler = self.handler # Save to avoid breaking any subsequent tests. + self.handler = RejectingSocketlessRequestHandler() + result = self.send_typical_request(b'GET / HTTP/1.1\r\nExpect: 100-continue\r\n\r\n') + self.assertEqual(result[0], b'HTTP/1.1 417 Expectation Failed\r\n') + self.verify_expected_headers(result[1:-1]) + # The expect handler should short circuit the usual get method by + # returning false here, so get_called should be false + self.assertFalse(self.handler.get_called) + self.assertEqual(sum(r == b'Connection: close\r\n' for r in result[1:-1]), 1) + self.handler = usual_handler # Restore to avoid breaking any subsequent tests. + + class SimpleHTTPRequestHandlerTestCase(unittest.TestCase): """ Test url parsing """ def setUp(self): @@ -431,6 +528,7 @@ def test_main(verbose=None): cwd = os.getcwd() try: support.run_unittest( + BaseHTTPRequestHandlerTestCase, BaseHTTPServerTestCase, SimpleHTTPServerTestCase, CGIHTTPServerTestCase, diff --git a/Misc/NEWS b/Misc/NEWS index 79c710c..9144e25 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -76,6 +76,9 @@ Core and Builtins Library ------- +- Issue #1491: BaseHTTPServer nows send a 100 Continue response before sending + a 200 OK for the Expect: 100-continue request header. + - Issue #9360: Cleanup and improvements to the nntplib module. The API now conforms to the philosophy of bytes and unicode separation in Python 3. A test suite has also been added. -- cgit v0.12