Example #1
0
    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))
Example #2
0
    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))
Example #3
0
    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()
Example #4
0
    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
Example #5
0
    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()