def send_headers(self, headers, end_headers=True, cookies=True): """ Write a dictionary of HTTP headers to the client. ============ ======== ============ Argument Default Description ============ ======== ============ headers A dictionary of HTTP headers. end_headers True *Optional.* If this is set to True, a double CRLF sequence will be written at the end of the cookie headers, signifying the end of the HTTP headers segment and the beginning of the response. cookies True *Optional.* If this is set to True, HTTP cookies will be sent along with the headers. ============ ======== ============ """ self._started = True out = [] append = out.append if isinstance(headers, (tuple, list)): hv = headers headers = [] for key, val in hv: headers.append(key.lower()) append('%s: %s' % (key, val)) else: hv = headers headers = [] for key in hv: headers.append(key.lower()) val = hv[key] if type(val) is list: for v in val: append('%s: %s' % (key, v)) else: append('%s: %s' % (key, val)) if not 'date' in headers and self.protocol == 'HTTP/1.1': append('Date: %s' % date(datetime.utcnow())) if not 'server' in headers: append('Server: %s' % SERVER) if cookies and hasattr(self, '_cookies_out'): self.send_cookies() if end_headers: append(CRLF) else: append('') self.connection.write(CRLF.join(out))
def send_headers(self, headers, end_headers=True, cookies=True): """ Write a dictionary of HTTP headers to the client. ============ ======== ============ Argument Default Description ============ ======== ============ headers A dictionary of HTTP headers. end_headers True *Optional.* If this is set to True, a double CRLF sequence will be written at the end of the cookie headers, signifying the end of the HTTP headers segment and the beginning of the response. cookies True *Optional.* If this is set to True, HTTP cookies will be sent along with the headers. ============ ======== ============ """ self._started = True out = [] append = out.append if isinstance(headers, (tuple,list)): hv = headers headers = [] for key, val in hv: headers.append(key.lower()) append('%s: %s' % (key, val)) else: hv = headers headers = [] for key in hv: headers.append(key.lower()) val = hv[key] if type(val) is list: for v in val: append('%s: %s' % (key, v)) else: append('%s: %s' % (key, val)) if not 'date' in headers and self.protocol == 'HTTP/1.1': append('Date: %s' % date(datetime.utcnow())) if not 'server' in headers: append('Server: %s' % SERVER) if cookies and hasattr(self, '_cookies_out'): self.send_cookies() if end_headers: append(CRLF) else: append('') self.connection.write(CRLF.join(out))
def send_file(self, path, filename=None, guess_mime=True, headers=None): """ Send a file to the client, given the path to that file. This method makes use of ``X-Sendfile``, if the :class:`~pants.http.server.HTTPServer` instance is configured to send X-Sendfile headers. If ``X-Sendfile`` is not available, Pants will make full use of caching headers, Ranges, and the `sendfile <http://www.kernel.org/doc/man-pages/online/pages/man2/sendfile.2.html>`_ system call to improve file transfer performance. Additionally, if the client had made a ``HEAD`` request, the contents of the file will not be transferred. .. note:: The request is finished automatically by this method. =========== ======== ============ Argument Default Description =========== ======== ============ path The path to the file to send. If this is a relative path, and the :class:`~pants.http.server.HTTPServer` instance has no root path for Sendfile set, the path will be assumed relative to the current working directory. filename None *Optional.* If this is set, the file will be sent as a download with the given filename as the default name to save it with. guess_mime True *Optional.* If this is set to True, Pants will attempt to set the ``Content-Type`` header based on the file extension. headers None *Optional.* A dictionary of HTTP headers to send with the file. =========== ======== ============ .. note:: If you set a ``Content-Type`` header with the ``headers`` parameter, the mime type will not be used, even if ``guess_mime`` is True. The ``headers`` will also override any ``Content-Disposition`` header generated by the ``filename`` parameter. """ self._started = True # The base path base = self.connection.server.file_root if not base: base = os.getcwd() # Now, the headers. if not headers: headers = {} # The Content-Disposition headers. if filename and not 'Content-Disposition' in headers: if not isinstance(filename, basestring): filename = str(filename) elif isinstance(filename, unicode): filename = filename.encode('utf8') headers[ 'Content-Disposition'] = 'attachment; filename="%s"' % filename # The Content-Type header. if not 'Content-Type' in headers: if guess_mime: if not mimetypes.inited: mimetypes.init() content_type = mimetypes.guess_type(path)[0] if not content_type: content_type = 'application/octet-stream' headers['Content-Type'] = content_type else: headers['Content-Type'] = 'application/octet-stream' # If X-Sendfile is enabled, this becomes much easier. if self.connection.server.sendfile: # We don't want absolute paths, if we can help it. if os.path.isabs(path): rel = os.path.relpath(path, base) if not rel.startswith('..'): path = rel # If we don't have an absolute path, append the prefix. if self.connection.server.sendfile_prefix and not os.path.isabs( path): path = os.path.join(self.connection.server.sendfile_prefix, path) if isinstance(self.connection.server.sendfile, basestring): headers[self.connection.server.sendfile] = path else: headers['X-Sendfile'] = path headers['Content-Length'] = 0 # Now, pass it through and be done. self.send_status() self.send_headers(headers) self.finish() return # If we get here, then we have to handle sending the file ourself. This # gets a bit trickier. First, let's find the proper path. if not os.path.isabs(path): path = os.path.join(base, path) # Let's start with some information on the file. stat = os.stat(path) modified = datetime.fromtimestamp(stat.st_mtime) expires = datetime.utcnow() + timedelta(days=7) etag = '"%x-%x"' % (stat.st_size, int(stat.st_mtime)) if not 'Last-Modified' in headers: headers['Last-Modified'] = date(modified) if not 'Expires' in headers: headers['Expires'] = date(expires) if not 'Cache-Control' in headers: headers['Cache-Control'] = 'max-age=604800' if not 'Accept-Ranges' in headers: headers['Accept-Ranges'] = 'bytes' if not 'ETag' in headers: headers['ETag'] = etag # Check request headers. not_modified = False if 'If-Modified-Since' in self.headers: try: since = parse_date(self.headers['If-Modified-Since']) except ValueError: since = None if since and since >= modified: not_modified = True if 'If-None-Match' in self.headers: values = self.headers['If-None-Match'].split(',') for val in values: val = val.strip() if val == '*' or etag == val: not_modified = True break # Send a 304 Not Modified, if possible. if not_modified: self.send_status(304) if 'Content-Length' in headers: del headers['Content-Length'] if 'Content-Type' in headers: del headers['Content-Type'] self.send_headers(headers) self.finish() return # Check for an If-Range header. if 'If-Range' in self.headers and 'Range' in self.headers: head = self.headers['If-Range'] if head != etag: try: match = parse_date(head) == modified except ValueError: match = False if not match: del self.headers['Range'] # Open the file. if not os.access(path, os.R_OK): self.send_response( 'You do not have permission to access that file.', 403) return try: f = open(path, 'rb') except IOError: self.send_response( 'You do not have permission to access that file.', 403) return # If we have no Range header, just do things the easy way. if not 'Range' in self.headers: headers['Content-Length'] = stat.st_size self.send_status() self.send_headers(headers) if self.method != 'HEAD': self.connection.write_file(f) self.finish() return # Start parsing the Range header. length = stat.st_size start = length - 1 end = 0 try: if not self.headers['Range'].startswith('bytes='): raise ValueError for pair in self.headers['Range'][6:].split(','): pair = pair.strip() if pair.startswith('-'): # Final x bytes. val = int(pair[1:]) if val > length: raise ValueError end = length - 1 s = length - val if s < start: start = s elif pair.endswith('-'): # Everything past x. val = int(pair[:-1]) if val > length - 1: raise ValueError end = length - 1 if val < start: start = val else: s, e = map(int, pair.split('-')) if start < 0 or start > end or end > length - 1: raise ValueError if s < start: start = s if e > end: end = e except ValueError: # Any ValueErrors need to send a 416 error response. self.send_response('416 Requested Range Not Satisfiable', 416) return # Set the Content-Range header, and the Content-Length. total = 1 + (end - start) headers['Content-Range'] = 'bytes %d-%d/%d' % (start, end, length) headers['Content-Length'] = total # Now, send the response. self.send_status(206) self.send_headers(headers) if self.method != 'HEAD': if end == length - 1: total = 0 self.connection.write_file(f, nbytes=total, offset=start) self.finish()
def __call__(self, request): """ Serve a request. """ try: path = request.match.group(1) if path is None: path = urllib.unquote_plus(request.path) except (AttributeError, IndexError): path = urllib.unquote_plus(request.path) # Convert the path to unicode. path = decode(path) # Strip off a starting quote. if path.startswith('/') or path.startswith('\\'): path = path[1:] # Normalize the path. full_path = os.path.normpath(os.path.join(self.path, path)) # Validate the request. if not full_path.startswith(self.path): abort(403) elif not os.path.exists(full_path): abort() elif not os.access(full_path, os.R_OK): abort(403) # Is this a directory? if os.path.isdir(full_path): # Check defaults. for f in self.defaults: full = os.path.join(full_path, f) if os.path.exists(full): request.path = urllib.quote(full.encode('utf8')) if hasattr(request, 'match'): del request.match return self.__call__(request) # Guess not. List it. if hasattr(request, 'match'): return self.list_directory(request, path) else: body, status, headers = self.list_directory(request, path) if isinstance(body, unicode): body = body.encode('utf-8') headers['Content-Length'] = len(body) request.send_status(status) request.send_headers(headers) request.send(body) request.finish() return # Blacklist Checking. self.check_blacklist(full_path) # Try rendering the content. ext = os.path.basename(full_path).rpartition('.')[-1] if ext in self.renderers: f, mtime, size, type = self.renderers[ext](request, full_path) else: # Get the information for the actual file. f = None stat = os.stat(full_path) mtime = stat.st_mtime size = stat.st_size type = mimetypes.guess_type(full_path)[0] # If we don't have a type, text/plain it. if type is None: type = 'text/plain' # Generate a bunch of data for headers. modified = datetime.fromtimestamp(mtime) expires = datetime.utcnow() + timedelta(days=7) etag = '"%x-%x"' % (size, int(mtime)) headers = { 'Last-Modified' : date(modified), 'Expires' : date(expires), 'Cache-Control' : 'max-age=604800', 'Content-Type' : type, 'Date' : date(datetime.utcnow()), 'Server' : SERVER, 'Accept-Ranges' : 'bytes', 'ETag' : etag } do304 = False if 'If-Modified-Since' in request.headers: try: since = _parse_date(request.headers['If-Modified-Since']) except ValueError: since = None if since and since >= modified: do304 = True if 'If-None-Match' in request.headers: if etag == request.headers['If-None-Match']: do304 = True if do304: if f: f.close() request.auto_finish = False request.send_status(304) request.send_headers(headers) request.finish() return if 'If-Range' in request.headers: if etag != request.headers['If-Range'] and \ 'Range' in request.headers: del request.headers['Range'] last = size - 1 range = 0, last status = 200 if 'Range' in request.headers: if request.headers['Range'].startswith('bytes='): try: val = request.headers['Range'][6:].split(',')[0] start, end = val.split('-') except ValueError: if f: f.close() abort(416) try: if end and not start: end = last start = last - int(end) else: start = int(start or 0) end = int(end or last) if start < 0 or start > end or end > last: if f: f.close() abort(416) range = start, end except ValueError: pass if range[0] != 0 or range[1] != last: status = 206 headers['Content-Range'] = 'bytes %d-%d/%d' % ( range[0], range[1], size) # Set the content length header. if range[0] == range[1]: headers['Content-Length'] = 0 else: headers['Content-Length'] = 1 + (range[1] - range[0]) # Send the headers and status line. request.auto_finish = False request.send_status(status) request.send_headers(headers) # Don't send the body if this is head. if request.method == 'HEAD': if f: f.close() request.finish() return # Open the file and send it. if range[0] == range[1]: if f: f.close() request.finish() return if f is None: f = open(full_path, 'rb') if range[1] != last: length = 1 + (range[1] - range[0]) else: length = 0 request.connection.write_file(f, nbytes=length, offset=range[0]) request.connection._finished = True
def send_file(self, path, filename=None, guess_mime=True, headers=None): """ Send a file to the client, given the path to that file. This method makes use of ``X-Sendfile``, if the :class:`~pants.http.server.HTTPServer` instance is configured to send X-Sendfile headers. If ``X-Sendfile`` is not available, Pants will make full use of caching headers, Ranges, and the `sendfile <http://www.kernel.org/doc/man-pages/online/pages/man2/sendfile.2.html>`_ system call to improve file transfer performance. Additionally, if the client had made a ``HEAD`` request, the contents of the file will not be transferred. .. note:: The request is finished automatically by this method. =========== ======== ============ Argument Default Description =========== ======== ============ path The path to the file to send. If this is a relative path, and the :class:`~pants.http.server.HTTPServer` instance has no root path for Sendfile set, the path will be assumed relative to the current working directory. filename None *Optional.* If this is set, the file will be sent as a download with the given filename as the default name to save it with. guess_mime True *Optional.* If this is set to True, Pants will attempt to set the ``Content-Type`` header based on the file extension. headers None *Optional.* A dictionary of HTTP headers to send with the file. =========== ======== ============ .. note:: If you set a ``Content-Type`` header with the ``headers`` parameter, the mime type will not be used, even if ``guess_mime`` is True. The ``headers`` will also override any ``Content-Disposition`` header generated by the ``filename`` parameter. """ self._started = True # The base path base = self.connection.server.file_root if not base: base = os.getcwd() # Now, the headers. if not headers: headers = {} # The Content-Disposition headers. if filename and not 'Content-Disposition' in headers: if not isinstance(filename, basestring): filename = str(filename) elif isinstance(filename, unicode): filename = filename.encode('utf8') headers['Content-Disposition'] = 'attachment; filename="%s"' % filename # The Content-Type header. if not 'Content-Type' in headers: if guess_mime: if not mimetypes.inited: mimetypes.init() content_type = mimetypes.guess_type(path)[0] if not content_type: content_type = 'application/octet-stream' headers['Content-Type'] = content_type else: headers['Content-Type'] = 'application/octet-stream' # If X-Sendfile is enabled, this becomes much easier. if self.connection.server.sendfile: # We don't want absolute paths, if we can help it. if os.path.isabs(path): rel = os.path.relpath(path, base) if not rel.startswith('..'): path = rel # If we don't have an absolute path, append the prefix. if self.connection.server.sendfile_prefix and not os.path.isabs(path): path = os.path.join(self.connection.server.sendfile_prefix, path) if isinstance(self.connection.server.sendfile, basestring): headers[self.connection.server.sendfile] = path else: headers['X-Sendfile'] = path headers['Content-Length'] = 0 # Now, pass it through and be done. self.send_status() self.send_headers(headers) self.finish() return # If we get here, then we have to handle sending the file ourself. This # gets a bit trickier. First, let's find the proper path. if not os.path.isabs(path): path = os.path.join(base, path) # Let's start with some information on the file. stat = os.stat(path) modified = datetime.fromtimestamp(stat.st_mtime) expires = datetime.utcnow() + timedelta(days=7) etag = '"%x-%x"' % (stat.st_size, int(stat.st_mtime)) if not 'Last-Modified' in headers: headers['Last-Modified'] = date(modified) if not 'Expires' in headers: headers['Expires'] = date(expires) if not 'Cache-Control' in headers: headers['Cache-Control'] = 'max-age=604800' if not 'Accept-Ranges' in headers: headers['Accept-Ranges'] = 'bytes' if not 'ETag' in headers: headers['ETag'] = etag # Check request headers. not_modified = False if 'If-Modified-Since' in self.headers: try: since = parse_date(self.headers['If-Modified-Since']) except ValueError: since = None if since and since >= modified: not_modified = True if 'If-None-Match' in self.headers: values = self.headers['If-None-Match'].split(',') for val in values: val = val.strip() if val == '*' or etag == val: not_modified = True break # Send a 304 Not Modified, if possible. if not_modified: self.send_status(304) if 'Content-Length' in headers: del headers['Content-Length'] if 'Content-Type' in headers: del headers['Content-Type'] self.send_headers(headers) self.finish() return # Check for an If-Range header. if 'If-Range' in self.headers and 'Range' in self.headers: head = self.headers['If-Range'] if head != etag: try: match = parse_date(head) == modified except ValueError: match = False if not match: del self.headers['Range'] # Open the file. if not os.access(path, os.R_OK): self.send_response('You do not have permission to access that file.', 403) return try: f = open(path, 'rb') except IOError: self.send_response('You do not have permission to access that file.', 403) return # If we have no Range header, just do things the easy way. if not 'Range' in self.headers: headers['Content-Length'] = stat.st_size self.send_status() self.send_headers(headers) if self.method != 'HEAD': self.connection.write_file(f) self.finish() return # Start parsing the Range header. length = stat.st_size start = length - 1 end = 0 try: if not self.headers['Range'].startswith('bytes='): raise ValueError for pair in self.headers['Range'][6:].split(','): pair = pair.strip() if pair.startswith('-'): # Final x bytes. val = int(pair[1:]) if val > length: raise ValueError end = length - 1 s = length - val if s < start: start = s elif pair.endswith('-'): # Everything past x. val = int(pair[:-1]) if val > length - 1: raise ValueError end = length - 1 if val < start: start = val else: s, e = map(int, pair.split('-')) if start < 0 or start > end or end > length - 1: raise ValueError if s < start: start = s if e > end: end = e except ValueError: # Any ValueErrors need to send a 416 error response. self.send_response('416 Requested Range Not Satisfiable', 416) return # Set the Content-Range header, and the Content-Length. total = 1 + (end - start) headers['Content-Range'] = 'bytes %d-%d/%d' % (start, end, length) headers['Content-Length'] = total # Now, send the response. self.send_status(206) self.send_headers(headers) if self.method != 'HEAD': if end == length - 1: total = 0 self.connection.write_file(f, nbytes=total, offset=start) self.finish()