OLD | NEW |
| (Empty) |
1 # -*- test-case-name: twisted.web.test.test_http -*- | |
2 | |
3 # Copyright (c) 2001-2007 Twisted Matrix Laboratories. | |
4 # See LICENSE for details. | |
5 | |
6 | |
7 """ | |
8 HyperText Transfer Protocol implementation. | |
9 | |
10 This is used by twisted.web. | |
11 | |
12 Future Plans: | |
13 - HTTP client support will at some point be refactored to support HTTP/1.1. | |
14 - Accept chunked data from clients in server. | |
15 - Other missing HTTP features from the RFC. | |
16 | |
17 Maintainer: U{Itamar Shtull-Trauring<mailto:twisted@itamarst.org>} | |
18 """ | |
19 | |
20 # system imports | |
21 from cStringIO import StringIO | |
22 import tempfile | |
23 import base64, binascii | |
24 import cgi | |
25 import socket | |
26 import math | |
27 import time | |
28 import calendar | |
29 import warnings | |
30 import os | |
31 from urlparse import urlparse as _urlparse | |
32 | |
33 from zope.interface import implements | |
34 | |
35 # twisted imports | |
36 from twisted.internet import interfaces, reactor, protocol, address | |
37 from twisted.protocols import policies, basic | |
38 from twisted.python import log | |
39 try: # try importing the fast, C version | |
40 from twisted.protocols._c_urlarg import unquote | |
41 except ImportError: | |
42 from urllib import unquote | |
43 | |
44 | |
45 protocol_version = "HTTP/1.1" | |
46 | |
47 _CONTINUE = 100 | |
48 SWITCHING = 101 | |
49 | |
50 OK = 200 | |
51 CREATED = 201 | |
52 ACCEPTED = 202 | |
53 NON_AUTHORITATIVE_INFORMATION = 203 | |
54 NO_CONTENT = 204 | |
55 RESET_CONTENT = 205 | |
56 PARTIAL_CONTENT = 206 | |
57 MULTI_STATUS = 207 | |
58 | |
59 MULTIPLE_CHOICE = 300 | |
60 MOVED_PERMANENTLY = 301 | |
61 FOUND = 302 | |
62 SEE_OTHER = 303 | |
63 NOT_MODIFIED = 304 | |
64 USE_PROXY = 305 | |
65 TEMPORARY_REDIRECT = 307 | |
66 | |
67 BAD_REQUEST = 400 | |
68 UNAUTHORIZED = 401 | |
69 PAYMENT_REQUIRED = 402 | |
70 FORBIDDEN = 403 | |
71 NOT_FOUND = 404 | |
72 NOT_ALLOWED = 405 | |
73 NOT_ACCEPTABLE = 406 | |
74 PROXY_AUTH_REQUIRED = 407 | |
75 REQUEST_TIMEOUT = 408 | |
76 CONFLICT = 409 | |
77 GONE = 410 | |
78 LENGTH_REQUIRED = 411 | |
79 PRECONDITION_FAILED = 412 | |
80 REQUEST_ENTITY_TOO_LARGE = 413 | |
81 REQUEST_URI_TOO_LONG = 414 | |
82 UNSUPPORTED_MEDIA_TYPE = 415 | |
83 REQUESTED_RANGE_NOT_SATISFIABLE = 416 | |
84 EXPECTATION_FAILED = 417 | |
85 | |
86 INTERNAL_SERVER_ERROR = 500 | |
87 NOT_IMPLEMENTED = 501 | |
88 BAD_GATEWAY = 502 | |
89 SERVICE_UNAVAILABLE = 503 | |
90 GATEWAY_TIMEOUT = 504 | |
91 HTTP_VERSION_NOT_SUPPORTED = 505 | |
92 INSUFFICIENT_STORAGE_SPACE = 507 | |
93 NOT_EXTENDED = 510 | |
94 | |
95 RESPONSES = { | |
96 # 100 | |
97 _CONTINUE: "Continue", | |
98 SWITCHING: "Switching Protocols", | |
99 | |
100 # 200 | |
101 OK: "OK", | |
102 CREATED: "Created", | |
103 ACCEPTED: "Accepted", | |
104 NON_AUTHORITATIVE_INFORMATION: "Non-Authoritative Information", | |
105 NO_CONTENT: "No Content", | |
106 RESET_CONTENT: "Reset Content.", | |
107 PARTIAL_CONTENT: "Partial Content", | |
108 MULTI_STATUS: "Multi-Status", | |
109 | |
110 # 300 | |
111 MULTIPLE_CHOICE: "Multiple Choices", | |
112 MOVED_PERMANENTLY: "Moved Permanently", | |
113 FOUND: "Found", | |
114 SEE_OTHER: "See Other", | |
115 NOT_MODIFIED: "Not Modified", | |
116 USE_PROXY: "Use Proxy", | |
117 # 306 not defined?? | |
118 TEMPORARY_REDIRECT: "Temporary Redirect", | |
119 | |
120 # 400 | |
121 BAD_REQUEST: "Bad Request", | |
122 UNAUTHORIZED: "Unauthorized", | |
123 PAYMENT_REQUIRED: "Payment Required", | |
124 FORBIDDEN: "Forbidden", | |
125 NOT_FOUND: "Not Found", | |
126 NOT_ALLOWED: "Method Not Allowed", | |
127 NOT_ACCEPTABLE: "Not Acceptable", | |
128 PROXY_AUTH_REQUIRED: "Proxy Authentication Required", | |
129 REQUEST_TIMEOUT: "Request Time-out", | |
130 CONFLICT: "Conflict", | |
131 GONE: "Gone", | |
132 LENGTH_REQUIRED: "Length Required", | |
133 PRECONDITION_FAILED: "Precondition Failed", | |
134 REQUEST_ENTITY_TOO_LARGE: "Request Entity Too Large", | |
135 REQUEST_URI_TOO_LONG: "Request-URI Too Long", | |
136 UNSUPPORTED_MEDIA_TYPE: "Unsupported Media Type", | |
137 REQUESTED_RANGE_NOT_SATISFIABLE: "Requested Range not satisfiable", | |
138 EXPECTATION_FAILED: "Expectation Failed", | |
139 | |
140 # 500 | |
141 INTERNAL_SERVER_ERROR: "Internal Server Error", | |
142 NOT_IMPLEMENTED: "Not Implemented", | |
143 BAD_GATEWAY: "Bad Gateway", | |
144 SERVICE_UNAVAILABLE: "Service Unavailable", | |
145 GATEWAY_TIMEOUT: "Gateway Time-out", | |
146 HTTP_VERSION_NOT_SUPPORTED: "HTTP Version not supported", | |
147 INSUFFICIENT_STORAGE_SPACE: "Insufficient Storage Space", | |
148 NOT_EXTENDED: "Not Extended" | |
149 } | |
150 | |
151 CACHED = """Magic constant returned by http.Request methods to set cache | |
152 validation headers when the request is conditional and the value fails | |
153 the condition.""" | |
154 | |
155 # backwards compatability | |
156 responses = RESPONSES | |
157 | |
158 | |
159 # datetime parsing and formatting | |
160 weekdayname = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] | |
161 monthname = [None, | |
162 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', | |
163 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] | |
164 weekdayname_lower = [name.lower() for name in weekdayname] | |
165 monthname_lower = [name and name.lower() for name in monthname] | |
166 | |
167 def urlparse(url): | |
168 """ | |
169 Parse an URL into six components. | |
170 | |
171 This is similar to L{urlparse.urlparse}, but rejects C{unicode} input | |
172 and always produces C{str} output. | |
173 | |
174 @type url: C{str} | |
175 | |
176 @raise TypeError: The given url was a C{unicode} string instead of a | |
177 C{str}. | |
178 | |
179 @rtype: six-tuple of str | |
180 @return: The scheme, net location, path, params, query string, and fragment | |
181 of the URL. | |
182 """ | |
183 if isinstance(url, unicode): | |
184 raise TypeError("url must be str, not unicode") | |
185 scheme, netloc, path, params, query, fragment = _urlparse(url) | |
186 if isinstance(scheme, unicode): | |
187 scheme = scheme.encode('ascii') | |
188 netloc = netloc.encode('ascii') | |
189 path = path.encode('ascii') | |
190 query = query.encode('ascii') | |
191 fragment = fragment.encode('ascii') | |
192 return scheme, netloc, path, params, query, fragment | |
193 | |
194 | |
195 def parse_qs(qs, keep_blank_values=0, strict_parsing=0, unquote=unquote): | |
196 """like cgi.parse_qs, only with custom unquote function""" | |
197 d = {} | |
198 items = [s2 for s1 in qs.split("&") for s2 in s1.split(";")] | |
199 for item in items: | |
200 try: | |
201 k, v = item.split("=", 1) | |
202 except ValueError: | |
203 if strict_parsing: | |
204 raise | |
205 continue | |
206 if v or keep_blank_values: | |
207 k = unquote(k.replace("+", " ")) | |
208 v = unquote(v.replace("+", " ")) | |
209 if k in d: | |
210 d[k].append(v) | |
211 else: | |
212 d[k] = [v] | |
213 return d | |
214 | |
215 def datetimeToString(msSinceEpoch=None): | |
216 """Convert seconds since epoch to HTTP datetime string.""" | |
217 if msSinceEpoch == None: | |
218 msSinceEpoch = time.time() | |
219 year, month, day, hh, mm, ss, wd, y, z = time.gmtime(msSinceEpoch) | |
220 s = "%s, %02d %3s %4d %02d:%02d:%02d GMT" % ( | |
221 weekdayname[wd], | |
222 day, monthname[month], year, | |
223 hh, mm, ss) | |
224 return s | |
225 | |
226 def datetimeToLogString(msSinceEpoch=None): | |
227 """Convert seconds since epoch to log datetime string.""" | |
228 if msSinceEpoch == None: | |
229 msSinceEpoch = time.time() | |
230 year, month, day, hh, mm, ss, wd, y, z = time.gmtime(msSinceEpoch) | |
231 s = "[%02d/%3s/%4d:%02d:%02d:%02d +0000]" % ( | |
232 day, monthname[month], year, | |
233 hh, mm, ss) | |
234 return s | |
235 | |
236 | |
237 # a hack so we don't need to recalculate log datetime every hit, | |
238 # at the price of a small, unimportant, inaccuracy. | |
239 _logDateTime = None | |
240 _logDateTimeUsers = 0 | |
241 _resetLogDateTimeID = None | |
242 | |
243 def _resetLogDateTime(): | |
244 global _logDateTime | |
245 global _resetLogDateTime | |
246 global _resetLogDateTimeID | |
247 _logDateTime = datetimeToLogString() | |
248 _resetLogDateTimeID = reactor.callLater(1, _resetLogDateTime) | |
249 | |
250 def _logDateTimeStart(): | |
251 global _logDateTimeUsers | |
252 if not _logDateTimeUsers: | |
253 _resetLogDateTime() | |
254 _logDateTimeUsers += 1 | |
255 | |
256 def _logDateTimeStop(): | |
257 global _logDateTimeUsers | |
258 _logDateTimeUsers -= 1; | |
259 if (not _logDateTimeUsers and _resetLogDateTimeID | |
260 and _resetLogDateTimeID.active()): | |
261 _resetLogDateTimeID.cancel() | |
262 | |
263 def timegm(year, month, day, hour, minute, second): | |
264 """Convert time tuple in GMT to seconds since epoch, GMT""" | |
265 EPOCH = 1970 | |
266 assert year >= EPOCH | |
267 assert 1 <= month <= 12 | |
268 days = 365*(year-EPOCH) + calendar.leapdays(EPOCH, year) | |
269 for i in range(1, month): | |
270 days = days + calendar.mdays[i] | |
271 if month > 2 and calendar.isleap(year): | |
272 days = days + 1 | |
273 days = days + day - 1 | |
274 hours = days*24 + hour | |
275 minutes = hours*60 + minute | |
276 seconds = minutes*60 + second | |
277 return seconds | |
278 | |
279 def stringToDatetime(dateString): | |
280 """Convert an HTTP date string (one of three formats) to seconds since epoch
.""" | |
281 parts = dateString.split() | |
282 | |
283 if not parts[0][0:3].lower() in weekdayname_lower: | |
284 # Weekday is stupid. Might have been omitted. | |
285 try: | |
286 return stringToDatetime("Sun, "+dateString) | |
287 except ValueError: | |
288 # Guess not. | |
289 pass | |
290 | |
291 partlen = len(parts) | |
292 if (partlen == 5 or partlen == 6) and parts[1].isdigit(): | |
293 # 1st date format: Sun, 06 Nov 1994 08:49:37 GMT | |
294 # (Note: "GMT" is literal, not a variable timezone) | |
295 # (also handles without "GMT") | |
296 # This is the normal format | |
297 day = parts[1] | |
298 month = parts[2] | |
299 year = parts[3] | |
300 time = parts[4] | |
301 elif (partlen == 3 or partlen == 4) and parts[1].find('-') != -1: | |
302 # 2nd date format: Sunday, 06-Nov-94 08:49:37 GMT | |
303 # (Note: "GMT" is literal, not a variable timezone) | |
304 # (also handles without without "GMT") | |
305 # Two digit year, yucko. | |
306 day, month, year = parts[1].split('-') | |
307 time = parts[2] | |
308 year=int(year) | |
309 if year < 69: | |
310 year = year + 2000 | |
311 elif year < 100: | |
312 year = year + 1900 | |
313 elif len(parts) == 5: | |
314 # 3rd date format: Sun Nov 6 08:49:37 1994 | |
315 # ANSI C asctime() format. | |
316 day = parts[2] | |
317 month = parts[1] | |
318 year = parts[4] | |
319 time = parts[3] | |
320 else: | |
321 raise ValueError("Unknown datetime format %r" % dateString) | |
322 | |
323 day = int(day) | |
324 month = int(monthname_lower.index(month.lower())) | |
325 year = int(year) | |
326 hour, min, sec = map(int, time.split(':')) | |
327 return int(timegm(year, month, day, hour, min, sec)) | |
328 | |
329 def toChunk(data): | |
330 """Convert string to a chunk. | |
331 | |
332 @returns: a tuple of strings representing the chunked encoding of data""" | |
333 return ("%x\r\n" % len(data), data, "\r\n") | |
334 | |
335 def fromChunk(data): | |
336 """Convert chunk to string. | |
337 | |
338 @returns: tuple (result, remaining), may raise ValueError. | |
339 """ | |
340 prefix, rest = data.split('\r\n', 1) | |
341 length = int(prefix, 16) | |
342 if length < 0: | |
343 raise ValueError("Chunk length must be >= 0, not %d" % (length,)) | |
344 if not rest[length:length + 2] == '\r\n': | |
345 raise ValueError, "chunk must end with CRLF" | |
346 return rest[:length], rest[length + 2:] | |
347 | |
348 | |
349 def parseContentRange(header): | |
350 """Parse a content-range header into (start, end, realLength). | |
351 | |
352 realLength might be None if real length is not known ('*'). | |
353 """ | |
354 kind, other = header.strip().split() | |
355 if kind.lower() != "bytes": | |
356 raise ValueError, "a range of type %r is not supported" | |
357 startend, realLength = other.split("/") | |
358 start, end = map(int, startend.split("-")) | |
359 if realLength == "*": | |
360 realLength = None | |
361 else: | |
362 realLength = int(realLength) | |
363 return (start, end, realLength) | |
364 | |
365 | |
366 class StringTransport: | |
367 """ | |
368 I am a StringIO wrapper that conforms for the transport API. I support | |
369 the `writeSequence' method. | |
370 """ | |
371 def __init__(self): | |
372 self.s = StringIO() | |
373 def writeSequence(self, seq): | |
374 self.s.write(''.join(seq)) | |
375 def __getattr__(self, attr): | |
376 return getattr(self.__dict__['s'], attr) | |
377 | |
378 | |
379 class HTTPClient(basic.LineReceiver): | |
380 """A client for HTTP 1.0 | |
381 | |
382 Notes: | |
383 You probably want to send a 'Host' header with the name of | |
384 the site you're connecting to, in order to not break name | |
385 based virtual hosting. | |
386 """ | |
387 length = None | |
388 firstLine = 1 | |
389 __buffer = None | |
390 | |
391 def sendCommand(self, command, path): | |
392 self.transport.write('%s %s HTTP/1.0\r\n' % (command, path)) | |
393 | |
394 def sendHeader(self, name, value): | |
395 self.transport.write('%s: %s\r\n' % (name, value)) | |
396 | |
397 def endHeaders(self): | |
398 self.transport.write('\r\n') | |
399 | |
400 def lineReceived(self, line): | |
401 if self.firstLine: | |
402 self.firstLine = 0 | |
403 l = line.split(None, 2) | |
404 version = l[0] | |
405 status = l[1] | |
406 try: | |
407 message = l[2] | |
408 except IndexError: | |
409 # sometimes there is no message | |
410 message = "" | |
411 self.handleStatus(version, status, message) | |
412 return | |
413 if line: | |
414 key, val = line.split(':', 1) | |
415 val = val.lstrip() | |
416 self.handleHeader(key, val) | |
417 if key.lower() == 'content-length': | |
418 self.length = int(val) | |
419 else: | |
420 self.__buffer = StringIO() | |
421 self.handleEndHeaders() | |
422 self.setRawMode() | |
423 | |
424 def connectionLost(self, reason): | |
425 self.handleResponseEnd() | |
426 | |
427 def handleResponseEnd(self): | |
428 if self.__buffer is not None: | |
429 b = self.__buffer.getvalue() | |
430 self.__buffer = None | |
431 self.handleResponse(b) | |
432 | |
433 def handleResponsePart(self, data): | |
434 self.__buffer.write(data) | |
435 | |
436 def connectionMade(self): | |
437 pass | |
438 | |
439 handleStatus = handleHeader = handleEndHeaders = lambda *args: None | |
440 | |
441 def rawDataReceived(self, data): | |
442 if self.length is not None: | |
443 data, rest = data[:self.length], data[self.length:] | |
444 self.length -= len(data) | |
445 else: | |
446 rest = '' | |
447 self.handleResponsePart(data) | |
448 if self.length == 0: | |
449 self.handleResponseEnd() | |
450 self.setLineMode(rest) | |
451 | |
452 | |
453 # response codes that must have empty bodies | |
454 NO_BODY_CODES = (204, 304) | |
455 | |
456 class Request: | |
457 """A HTTP request. | |
458 | |
459 Subclasses should override the process() method to determine how | |
460 the request will be processed. | |
461 | |
462 @ivar method: The HTTP method that was used. | |
463 @ivar uri: The full URI that was requested (includes arguments). | |
464 @ivar path: The path only (arguments not included). | |
465 @ivar args: All of the arguments, including URL and POST arguments. | |
466 @type args: A mapping of strings (the argument names) to lists of values. | |
467 i.e., ?foo=bar&foo=baz&quux=spam results in | |
468 {'foo': ['bar', 'baz'], 'quux': ['spam']}. | |
469 @ivar received_headers: All received headers | |
470 """ | |
471 | |
472 implements(interfaces.IConsumer) | |
473 | |
474 producer = None | |
475 finished = 0 | |
476 code = OK | |
477 code_message = RESPONSES[OK] | |
478 method = "(no method yet)" | |
479 clientproto = "(no clientproto yet)" | |
480 uri = "(no uri yet)" | |
481 startedWriting = 0 | |
482 chunked = 0 | |
483 sentLength = 0 # content-length of response, or total bytes sent via chunkin
g | |
484 etag = None | |
485 lastModified = None | |
486 _forceSSL = 0 | |
487 | |
488 def __init__(self, channel, queued): | |
489 """ | |
490 @param channel: the channel we're connected to. | |
491 @param queued: are we in the request queue, or can we start writing to | |
492 the transport? | |
493 """ | |
494 self.channel = channel | |
495 self.queued = queued | |
496 self.received_headers = {} | |
497 self.received_cookies = {} | |
498 self.headers = {} # outgoing headers | |
499 self.cookies = [] # outgoing cookies | |
500 | |
501 if queued: | |
502 self.transport = StringTransport() | |
503 else: | |
504 self.transport = self.channel.transport | |
505 | |
506 def _cleanup(self): | |
507 """Called when have finished responding and are no longer queued.""" | |
508 if self.producer: | |
509 log.err(RuntimeError("Producer was not unregistered for %s" % self.u
ri)) | |
510 self.unregisterProducer() | |
511 self.channel.requestDone(self) | |
512 del self.channel | |
513 try: | |
514 self.content.close() | |
515 except OSError: | |
516 # win32 suckiness, no idea why it does this | |
517 pass | |
518 del self.content | |
519 | |
520 # methods for channel - end users should not use these | |
521 | |
522 def noLongerQueued(self): | |
523 """Notify the object that it is no longer queued. | |
524 | |
525 We start writing whatever data we have to the transport, etc. | |
526 | |
527 This method is not intended for users. | |
528 """ | |
529 if not self.queued: | |
530 raise RuntimeError, "noLongerQueued() got called unnecessarily." | |
531 | |
532 self.queued = 0 | |
533 | |
534 # set transport to real one and send any buffer data | |
535 data = self.transport.getvalue() | |
536 self.transport = self.channel.transport | |
537 if data: | |
538 self.transport.write(data) | |
539 | |
540 # if we have producer, register it with transport | |
541 if (self.producer is not None) and not self.finished: | |
542 self.transport.registerProducer(self.producer, self.streamingProduce
r) | |
543 | |
544 # if we're finished, clean up | |
545 if self.finished: | |
546 self._cleanup() | |
547 | |
548 def gotLength(self, length): | |
549 """Called when HTTP channel got length of content in this request. | |
550 | |
551 This method is not intended for users. | |
552 """ | |
553 if length < 100000: | |
554 self.content = StringIO() | |
555 else: | |
556 self.content = tempfile.TemporaryFile() | |
557 | |
558 def parseCookies(self): | |
559 """Parse cookie headers. | |
560 | |
561 This method is not intended for users.""" | |
562 cookietxt = self.getHeader("cookie") | |
563 if cookietxt: | |
564 for cook in cookietxt.split(';'): | |
565 cook = cook.lstrip() | |
566 try: | |
567 k, v = cook.split('=', 1) | |
568 self.received_cookies[k] = v | |
569 except ValueError: | |
570 pass | |
571 | |
572 def handleContentChunk(self, data): | |
573 """Write a chunk of data. | |
574 | |
575 This method is not intended for users. | |
576 """ | |
577 self.content.write(data) | |
578 | |
579 def requestReceived(self, command, path, version): | |
580 """Called by channel when all data has been received. | |
581 | |
582 This method is not intended for users. | |
583 """ | |
584 self.content.seek(0,0) | |
585 self.args = {} | |
586 self.stack = [] | |
587 | |
588 self.method, self.uri = command, path | |
589 self.clientproto = version | |
590 x = self.uri.split('?', 1) | |
591 | |
592 if len(x) == 1: | |
593 self.path = self.uri | |
594 else: | |
595 self.path, argstring = x | |
596 self.args = parse_qs(argstring, 1) | |
597 | |
598 # cache the client and server information, we'll need this later to be | |
599 # serialized and sent with the request so CGIs will work remotely | |
600 self.client = self.channel.transport.getPeer() | |
601 self.host = self.channel.transport.getHost() | |
602 | |
603 # Argument processing | |
604 args = self.args | |
605 ctype = self.getHeader('content-type') | |
606 if self.method == "POST" and ctype: | |
607 mfd = 'multipart/form-data' | |
608 key, pdict = cgi.parse_header(ctype) | |
609 if key == 'application/x-www-form-urlencoded': | |
610 args.update(parse_qs(self.content.read(), 1)) | |
611 elif key == mfd: | |
612 try: | |
613 args.update(cgi.parse_multipart(self.content, pdict)) | |
614 except KeyError, e: | |
615 if e.args[0] == 'content-disposition': | |
616 # Parse_multipart can't cope with missing | |
617 # content-dispostion headers in multipart/form-data | |
618 # parts, so we catch the exception and tell the client | |
619 # it was a bad request. | |
620 self.channel.transport.write( | |
621 "HTTP/1.1 400 Bad Request\r\n\r\n") | |
622 self.channel.transport.loseConnection() | |
623 return | |
624 raise | |
625 | |
626 self.process() | |
627 | |
628 def __repr__(self): | |
629 return '<%s %s %s>'% (self.method, self.uri, self.clientproto) | |
630 | |
631 def process(self): | |
632 """Override in subclasses. | |
633 | |
634 This method is not intended for users. | |
635 """ | |
636 pass | |
637 | |
638 | |
639 # consumer interface | |
640 | |
641 def registerProducer(self, producer, streaming): | |
642 """Register a producer.""" | |
643 if self.producer: | |
644 raise ValueError, "registering producer %s before previous one (%s)
was unregistered" % (producer, self.producer) | |
645 | |
646 self.streamingProducer = streaming | |
647 self.producer = producer | |
648 | |
649 if self.queued: | |
650 producer.pauseProducing() | |
651 else: | |
652 self.transport.registerProducer(producer, streaming) | |
653 | |
654 def unregisterProducer(self): | |
655 """Unregister the producer.""" | |
656 if not self.queued: | |
657 self.transport.unregisterProducer() | |
658 self.producer = None | |
659 | |
660 # private http response methods | |
661 | |
662 def _sendError(self, code, resp=''): | |
663 self.transport.write('%s %s %s\r\n\r\n' % (self.clientproto, code, resp)
) | |
664 | |
665 | |
666 # The following is the public interface that people should be | |
667 # writing to. | |
668 | |
669 def getHeader(self, key): | |
670 """Get a header that was sent from the network. | |
671 """ | |
672 return self.received_headers.get(key.lower()) | |
673 | |
674 def getCookie(self, key): | |
675 """Get a cookie that was sent from the network. | |
676 """ | |
677 return self.received_cookies.get(key) | |
678 | |
679 def finish(self): | |
680 """We are finished writing data.""" | |
681 if self.finished: | |
682 warnings.warn("Warning! request.finish called twice.", stacklevel=2) | |
683 return | |
684 | |
685 if not self.startedWriting: | |
686 # write headers | |
687 self.write('') | |
688 | |
689 if self.chunked: | |
690 # write last chunk and closing CRLF | |
691 self.transport.write("0\r\n\r\n") | |
692 | |
693 # log request | |
694 if hasattr(self.channel, "factory"): | |
695 self.channel.factory.log(self) | |
696 | |
697 self.finished = 1 | |
698 if not self.queued: | |
699 self._cleanup() | |
700 | |
701 def write(self, data): | |
702 """ | |
703 Write some data as a result of an HTTP request. The first | |
704 time this is called, it writes out response data. | |
705 """ | |
706 if not self.startedWriting: | |
707 self.startedWriting = 1 | |
708 version = self.clientproto | |
709 l = [] | |
710 l.append('%s %s %s\r\n' % (version, self.code, | |
711 self.code_message)) | |
712 # if we don't have a content length, we send data in | |
713 # chunked mode, so that we can support pipelining in | |
714 # persistent connections. | |
715 if ((version == "HTTP/1.1") and | |
716 (self.headers.get('content-length', None) is None) and | |
717 self.method != "HEAD" and self.code not in NO_BODY_CODES): | |
718 l.append("%s: %s\r\n" % ('Transfer-encoding', 'chunked')) | |
719 self.chunked = 1 | |
720 if self.lastModified is not None: | |
721 if self.headers.has_key('last-modified'): | |
722 log.msg("Warning: last-modified specified both in" | |
723 " header list and lastModified attribute.") | |
724 else: | |
725 self.setHeader('last-modified', | |
726 datetimeToString(self.lastModified)) | |
727 if self.etag is not None: | |
728 self.setHeader('ETag', self.etag) | |
729 for name, value in self.headers.items(): | |
730 l.append("%s: %s\r\n" % (name.capitalize(), value)) | |
731 for cookie in self.cookies: | |
732 l.append('%s: %s\r\n' % ("Set-Cookie", cookie)) | |
733 l.append("\r\n") | |
734 | |
735 self.transport.writeSequence(l) | |
736 | |
737 # if this is a "HEAD" request, we shouldn't return any data | |
738 if self.method == "HEAD": | |
739 self.write = lambda data: None | |
740 return | |
741 | |
742 # for certain result codes, we should never return any data | |
743 if self.code in NO_BODY_CODES: | |
744 self.write = lambda data: None | |
745 return | |
746 | |
747 self.sentLength = self.sentLength + len(data) | |
748 if data: | |
749 if self.chunked: | |
750 self.transport.writeSequence(toChunk(data)) | |
751 else: | |
752 self.transport.write(data) | |
753 | |
754 def addCookie(self, k, v, expires=None, domain=None, path=None, max_age=None
, comment=None, secure=None): | |
755 """Set an outgoing HTTP cookie. | |
756 | |
757 In general, you should consider using sessions instead of cookies, see | |
758 twisted.web.server.Request.getSession and the | |
759 twisted.web.server.Session class for details. | |
760 """ | |
761 cookie = '%s=%s' % (k, v) | |
762 if expires is not None: | |
763 cookie = cookie +"; Expires=%s" % expires | |
764 if domain is not None: | |
765 cookie = cookie +"; Domain=%s" % domain | |
766 if path is not None: | |
767 cookie = cookie +"; Path=%s" % path | |
768 if max_age is not None: | |
769 cookie = cookie +"; Max-Age=%s" % max_age | |
770 if comment is not None: | |
771 cookie = cookie +"; Comment=%s" % comment | |
772 if secure: | |
773 cookie = cookie +"; Secure" | |
774 self.cookies.append(cookie) | |
775 | |
776 def setResponseCode(self, code, message=None): | |
777 """Set the HTTP response code. | |
778 """ | |
779 self.code = code | |
780 if message: | |
781 self.code_message = message | |
782 else: | |
783 self.code_message = RESPONSES.get(code, "Unknown Status") | |
784 | |
785 def setHeader(self, k, v): | |
786 """Set an outgoing HTTP header. | |
787 """ | |
788 self.headers[k.lower()] = v | |
789 | |
790 def redirect(self, url): | |
791 """Utility function that does a redirect. | |
792 | |
793 The request should have finish() called after this. | |
794 """ | |
795 self.setResponseCode(FOUND) | |
796 self.setHeader("location", url) | |
797 | |
798 def setLastModified(self, when): | |
799 """Set the X{Last-Modified} time for the response to this request. | |
800 | |
801 If I am called more than once, I ignore attempts to set | |
802 Last-Modified earlier, only replacing the Last-Modified time | |
803 if it is to a later value. | |
804 | |
805 If I am a conditional request, I may modify my response code | |
806 to L{NOT_MODIFIED} if appropriate for the time given. | |
807 | |
808 @param when: The last time the resource being returned was | |
809 modified, in seconds since the epoch. | |
810 @type when: number | |
811 @return: If I am a X{If-Modified-Since} conditional request and | |
812 the time given is not newer than the condition, I return | |
813 L{http.CACHED<CACHED>} to indicate that you should write no | |
814 body. Otherwise, I return a false value. | |
815 """ | |
816 # time.time() may be a float, but the HTTP-date strings are | |
817 # only good for whole seconds. | |
818 when = long(math.ceil(when)) | |
819 if (not self.lastModified) or (self.lastModified < when): | |
820 self.lastModified = when | |
821 | |
822 modified_since = self.getHeader('if-modified-since') | |
823 if modified_since: | |
824 modified_since = stringToDatetime(modified_since.split(';', 1)[0]) | |
825 if modified_since >= when: | |
826 self.setResponseCode(NOT_MODIFIED) | |
827 return CACHED | |
828 return None | |
829 | |
830 def setETag(self, etag): | |
831 """Set an X{entity tag} for the outgoing response. | |
832 | |
833 That's \"entity tag\" as in the HTTP/1.1 X{ETag} header, \"used | |
834 for comparing two or more entities from the same requested | |
835 resource.\" | |
836 | |
837 If I am a conditional request, I may modify my response code | |
838 to L{NOT_MODIFIED} or L{PRECONDITION_FAILED}, if appropriate | |
839 for the tag given. | |
840 | |
841 @param etag: The entity tag for the resource being returned. | |
842 @type etag: string | |
843 @return: If I am a X{If-None-Match} conditional request and | |
844 the tag matches one in the request, I return | |
845 L{http.CACHED<CACHED>} to indicate that you should write | |
846 no body. Otherwise, I return a false value. | |
847 """ | |
848 if etag: | |
849 self.etag = etag | |
850 | |
851 tags = self.getHeader("if-none-match") | |
852 if tags: | |
853 tags = tags.split() | |
854 if (etag in tags) or ('*' in tags): | |
855 self.setResponseCode(((self.method in ("HEAD", "GET")) | |
856 and NOT_MODIFIED) | |
857 or PRECONDITION_FAILED) | |
858 return CACHED | |
859 return None | |
860 | |
861 def getAllHeaders(self): | |
862 """Return dictionary of all headers the request received.""" | |
863 return self.received_headers | |
864 | |
865 def getRequestHostname(self): | |
866 """ | |
867 Get the hostname that the user passed in to the request. | |
868 | |
869 This will either use the Host: header (if it is available) or the | |
870 host we are listening on if the header is unavailable. | |
871 | |
872 @returns: the requested hostname | |
873 @rtype: C{str} | |
874 """ | |
875 return (self.getHeader('host') or | |
876 socket.gethostbyaddr(self.getHost()[1])[0] | |
877 ).split(':')[0] | |
878 | |
879 def getHost(self): | |
880 """Get my originally requesting transport's host. | |
881 | |
882 Don't rely on the 'transport' attribute, since Request objects may be | |
883 copied remotely. For information on this method's return value, see | |
884 twisted.internet.tcp.Port. | |
885 """ | |
886 return self.host | |
887 | |
888 def setHost(self, host, port, ssl=0): | |
889 """Change the host and port the request thinks it's using. | |
890 | |
891 This method is useful for working with reverse HTTP proxies (e.g. | |
892 both Squid and Apache's mod_proxy can do this), when the address | |
893 the HTTP client is using is different than the one we're listening on. | |
894 | |
895 For example, Apache may be listening on https://www.example.com, and the
n | |
896 forwarding requests to http://localhost:8080, but we don't want HTML pro
duced | |
897 by Twisted to say 'http://localhost:8080', they should say 'https://www.
example.com', | |
898 so we do:: | |
899 | |
900 request.setHost('www.example.com', 443, ssl=1) | |
901 | |
902 This method is experimental. | |
903 """ | |
904 self._forceSSL = ssl | |
905 self.received_headers["host"] = host | |
906 self.host = address.IPv4Address("TCP", host, port) | |
907 | |
908 def getClientIP(self): | |
909 """ | |
910 Return the IP address of the client who submitted this request. | |
911 | |
912 @returns: the client IP address | |
913 @rtype: C{str} | |
914 """ | |
915 if isinstance(self.client, address.IPv4Address): | |
916 return self.client.host | |
917 else: | |
918 return None | |
919 | |
920 def isSecure(self): | |
921 """ | |
922 Return True if this request is using a secure transport. | |
923 | |
924 Normally this method returns True if this request's HTTPChannel | |
925 instance is using a transport that implements ISSLTransport. | |
926 | |
927 This will also return True if setHost() has been called | |
928 with ssl=True. | |
929 | |
930 @returns: True if this request is secure | |
931 @rtype: C{bool} | |
932 """ | |
933 if self._forceSSL: | |
934 return True | |
935 transport = getattr(getattr(self, 'channel', None), 'transport', None) | |
936 if interfaces.ISSLTransport(transport, None) is not None: | |
937 return True | |
938 return False | |
939 | |
940 def _authorize(self): | |
941 # Authorization, (mostly) per the RFC | |
942 try: | |
943 authh = self.getHeader("Authorization") | |
944 if not authh: | |
945 self.user = self.password = '' | |
946 return | |
947 bas, upw = authh.split() | |
948 if bas.lower() != "basic": | |
949 raise ValueError | |
950 upw = base64.decodestring(upw) | |
951 self.user, self.password = upw.split(':', 1) | |
952 except (binascii.Error, ValueError): | |
953 self.user = self.password = "" | |
954 except: | |
955 log.err() | |
956 self.user = self.password = "" | |
957 | |
958 def getUser(self): | |
959 """ | |
960 Return the HTTP user sent with this request, if any. | |
961 | |
962 If no user was supplied, return the empty string. | |
963 | |
964 @returns: the HTTP user, if any | |
965 @rtype: C{str} | |
966 """ | |
967 try: | |
968 return self.user | |
969 except: | |
970 pass | |
971 self._authorize() | |
972 return self.user | |
973 | |
974 def getPassword(self): | |
975 """ | |
976 Return the HTTP password sent with this request, if any. | |
977 | |
978 If no password was supplied, return the empty string. | |
979 | |
980 @returns: the HTTP password, if any | |
981 @rtype: C{str} | |
982 """ | |
983 try: | |
984 return self.password | |
985 except: | |
986 pass | |
987 self._authorize() | |
988 return self.password | |
989 | |
990 def getClient(self): | |
991 if self.client.type != 'TCP': | |
992 return None | |
993 host = self.client.host | |
994 try: | |
995 name, names, addresses = socket.gethostbyaddr(host) | |
996 except socket.error: | |
997 return host | |
998 names.insert(0, name) | |
999 for name in names: | |
1000 if '.' in name: | |
1001 return name | |
1002 return names[0] | |
1003 | |
1004 def connectionLost(self, reason): | |
1005 """connection was lost""" | |
1006 pass | |
1007 | |
1008 class HTTPChannel(basic.LineReceiver, policies.TimeoutMixin): | |
1009 """A receiver for HTTP requests.""" | |
1010 | |
1011 maxHeaders = 500 # max number of headers allowed per request | |
1012 | |
1013 length = 0 | |
1014 persistent = 1 | |
1015 __header = '' | |
1016 __first_line = 1 | |
1017 __content = None | |
1018 | |
1019 # set in instances or subclasses | |
1020 requestFactory = Request | |
1021 | |
1022 _savedTimeOut = None | |
1023 | |
1024 def __init__(self): | |
1025 # the request queue | |
1026 self.requests = [] | |
1027 | |
1028 def connectionMade(self): | |
1029 self.setTimeout(self.timeOut) | |
1030 | |
1031 def lineReceived(self, line): | |
1032 self.resetTimeout() | |
1033 | |
1034 if self.__first_line: | |
1035 # if this connection is not persistent, drop any data which | |
1036 # the client (illegally) sent after the last request. | |
1037 if not self.persistent: | |
1038 self.dataReceived = self.lineReceived = lambda *args: None | |
1039 return | |
1040 | |
1041 # IE sends an extraneous empty line (\r\n) after a POST request; | |
1042 # eat up such a line, but only ONCE | |
1043 if not line and self.__first_line == 1: | |
1044 self.__first_line = 2 | |
1045 return | |
1046 | |
1047 # create a new Request object | |
1048 request = self.requestFactory(self, len(self.requests)) | |
1049 self.requests.append(request) | |
1050 | |
1051 self.__first_line = 0 | |
1052 parts = line.split() | |
1053 if len(parts) != 3: | |
1054 self.transport.write("HTTP/1.1 400 Bad Request\r\n\r\n") | |
1055 self.transport.loseConnection() | |
1056 return | |
1057 command, request, version = parts | |
1058 self._command = command | |
1059 self._path = request | |
1060 self._version = version | |
1061 elif line == '': | |
1062 if self.__header: | |
1063 self.headerReceived(self.__header) | |
1064 self.__header = '' | |
1065 self.allHeadersReceived() | |
1066 if self.length == 0: | |
1067 self.allContentReceived() | |
1068 else: | |
1069 self.setRawMode() | |
1070 elif line[0] in ' \t': | |
1071 self.__header = self.__header+'\n'+line | |
1072 else: | |
1073 if self.__header: | |
1074 self.headerReceived(self.__header) | |
1075 self.__header = line | |
1076 | |
1077 def headerReceived(self, line): | |
1078 """Do pre-processing (for content-length) and store this header away. | |
1079 """ | |
1080 header, data = line.split(':', 1) | |
1081 header = header.lower() | |
1082 data = data.strip() | |
1083 if header == 'content-length': | |
1084 self.length = int(data) | |
1085 reqHeaders = self.requests[-1].received_headers | |
1086 reqHeaders[header] = data | |
1087 if len(reqHeaders) > self.maxHeaders: | |
1088 self.transport.write("HTTP/1.1 400 Bad Request\r\n\r\n") | |
1089 self.transport.loseConnection() | |
1090 | |
1091 def allContentReceived(self): | |
1092 command = self._command | |
1093 path = self._path | |
1094 version = self._version | |
1095 | |
1096 # reset ALL state variables, so we don't interfere with next request | |
1097 self.length = 0 | |
1098 self._header = '' | |
1099 self.__first_line = 1 | |
1100 del self._command, self._path, self._version | |
1101 | |
1102 # Disable the idle timeout, in case this request takes a long | |
1103 # time to finish generating output. | |
1104 if self.timeOut: | |
1105 self._savedTimeOut = self.setTimeout(None) | |
1106 | |
1107 req = self.requests[-1] | |
1108 req.requestReceived(command, path, version) | |
1109 | |
1110 def rawDataReceived(self, data): | |
1111 if len(data) < self.length: | |
1112 self.requests[-1].handleContentChunk(data) | |
1113 self.length = self.length - len(data) | |
1114 else: | |
1115 self.requests[-1].handleContentChunk(data[:self.length]) | |
1116 extraneous = data[self.length:] | |
1117 self.allContentReceived() | |
1118 self.setLineMode(extraneous) | |
1119 | |
1120 def allHeadersReceived(self): | |
1121 req = self.requests[-1] | |
1122 req.parseCookies() | |
1123 self.persistent = self.checkPersistence(req, self._version) | |
1124 req.gotLength(self.length) | |
1125 | |
1126 def checkPersistence(self, request, version): | |
1127 """Check if the channel should close or not.""" | |
1128 connection = request.getHeader('connection') | |
1129 if connection: | |
1130 tokens = map(str.lower, connection.split(' ')) | |
1131 else: | |
1132 tokens = [] | |
1133 | |
1134 # HTTP 1.0 persistent connection support is currently disabled, | |
1135 # since we need a way to disable pipelining. HTTP 1.0 can't do | |
1136 # pipelining since we can't know in advance if we'll have a | |
1137 # content-length header, if we don't have the header we need to close th
e | |
1138 # connection. In HTTP 1.1 this is not an issue since we use chunked | |
1139 # encoding if content-length is not available. | |
1140 | |
1141 #if version == "HTTP/1.0": | |
1142 # if 'keep-alive' in tokens: | |
1143 # request.setHeader('connection', 'Keep-Alive') | |
1144 # return 1 | |
1145 # else: | |
1146 # return 0 | |
1147 if version == "HTTP/1.1": | |
1148 if 'close' in tokens: | |
1149 request.setHeader('connection', 'close') | |
1150 return 0 | |
1151 else: | |
1152 return 1 | |
1153 else: | |
1154 return 0 | |
1155 | |
1156 def requestDone(self, request): | |
1157 """Called by first request in queue when it is done.""" | |
1158 if request != self.requests[0]: raise TypeError | |
1159 del self.requests[0] | |
1160 | |
1161 if self.persistent: | |
1162 # notify next request it can start writing | |
1163 if self.requests: | |
1164 self.requests[0].noLongerQueued() | |
1165 else: | |
1166 if self._savedTimeOut: | |
1167 self.setTimeout(self._savedTimeOut) | |
1168 else: | |
1169 self.transport.loseConnection() | |
1170 | |
1171 def timeoutConnection(self): | |
1172 log.msg("Timing out client: %s" % str(self.transport.getPeer())) | |
1173 policies.TimeoutMixin.timeoutConnection(self) | |
1174 | |
1175 def connectionLost(self, reason): | |
1176 self.setTimeout(None) | |
1177 for request in self.requests: | |
1178 request.connectionLost(reason) | |
1179 | |
1180 | |
1181 class HTTPFactory(protocol.ServerFactory): | |
1182 """Factory for HTTP server.""" | |
1183 | |
1184 protocol = HTTPChannel | |
1185 | |
1186 logPath = None | |
1187 | |
1188 timeOut = 60 * 60 * 12 | |
1189 | |
1190 def __init__(self, logPath=None, timeout=60*60*12): | |
1191 if logPath is not None: | |
1192 logPath = os.path.abspath(logPath) | |
1193 self.logPath = logPath | |
1194 self.timeOut = timeout | |
1195 | |
1196 def buildProtocol(self, addr): | |
1197 p = protocol.ServerFactory.buildProtocol(self, addr) | |
1198 # timeOut needs to be on the Protocol instance cause | |
1199 # TimeoutMixin expects it there | |
1200 p.timeOut = self.timeOut | |
1201 return p | |
1202 | |
1203 def startFactory(self): | |
1204 _logDateTimeStart() | |
1205 if self.logPath: | |
1206 self.logFile = self._openLogFile(self.logPath) | |
1207 else: | |
1208 self.logFile = log.logfile | |
1209 | |
1210 def stopFactory(self): | |
1211 if hasattr(self, "logFile"): | |
1212 if self.logFile != log.logfile: | |
1213 self.logFile.close() | |
1214 del self.logFile | |
1215 _logDateTimeStop() | |
1216 | |
1217 def _openLogFile(self, path): | |
1218 """Override in subclasses, e.g. to use twisted.python.logfile.""" | |
1219 f = open(path, "a", 1) | |
1220 return f | |
1221 | |
1222 def _escape(self, s): | |
1223 # pain in the ass. Return a string like python repr, but always | |
1224 # escaped as if surrounding quotes were "". | |
1225 r = repr(s) | |
1226 if r[0] == "'": | |
1227 return r[1:-1].replace('"', '\\"').replace("\\'", "'") | |
1228 return r[1:-1] | |
1229 | |
1230 def log(self, request): | |
1231 """Log a request's result to the logfile, by default in combined log for
mat.""" | |
1232 if hasattr(self, "logFile"): | |
1233 line = '%s - - %s "%s" %d %s "%s" "%s"\n' % ( | |
1234 request.getClientIP(), | |
1235 # request.getUser() or "-", # the remote user is almost never im
portant | |
1236 _logDateTime, | |
1237 '%s %s %s' % (self._escape(request.method), | |
1238 self._escape(request.uri), | |
1239 self._escape(request.clientproto)), | |
1240 request.code, | |
1241 request.sentLength or "-", | |
1242 self._escape(request.getHeader("referer") or "-"), | |
1243 self._escape(request.getHeader("user-agent") or "-")) | |
1244 self.logFile.write(line) | |
OLD | NEW |