Source code for coapy.httputil
# -*- coding: utf-8 -*-
# Copyright 2013, Peter A. Bigot
# Parts Copyright © 2001-2013 Python Software Foundation; All Rights Reserved
#
# Excepting the small part of RequestHandler.handle_one_request that
# is copied from Python 2.7.3 BaseHTTPServer.py and is subject to the
# PSF License:
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain a
# copy of the License at:
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Utility classes and functions used within CoAPy.
:copyright: Copyright 2013, Peter A. Bigot
:license: Apache-2.0
"""
from __future__ import unicode_literals
from __future__ import print_function
from __future__ import absolute_import
from __future__ import division
import logging
_log = logging.getLogger(__name__)
import sys
import urllib
import BaseHTTPServer
import socket
import urlparse
[docs]class HTTPServer(BaseHTTPServer.HTTPServer):
"""Modifications required to provide the server development
interface we want.
"""
[docs] def make_uri(self, path, query='', fragment=''):
"""Create an absolute URI for something hosted on this server.
Needed for things like a `Location
<http://tools.ietf.org/html/rfc2616#section-14.30>`_ header.
.. note::
This function will not return a usable URI if
:attr:`python:BaseHTTPServer.HTTPServer.socket` is bound to
a wildcard address.
"""
(sa, port) = self.socket.getsockname()[:2]
if 80 != port:
netloc = '{0}:{1}'.format(sa, port)
else:
netloc = sa
return urlparse.urlunsplit(('http', netloc, path, query, fragment))
[docs]class HTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
"""Modifications required to provide the server development
interface we want.
This version inverts control relative to the Python standard web
server hierarchy: Rather than have the supported methods defined
by the generic handler, we follow REST principles and delegate the
selection of supported methods to the resources. A
:class:`HTTPRequestHandler` class retains in :attr:`_Resources` a
map from path prefixes to instances of :class:`HTTPResource` which
in turn provide the methods that are appropriate to that resource.
Processing is delegated to the registered resource whose
:attr:`HTTPResource.path` is the longest segment prefix of the
request URI ``path``.
"""
split_uri = None
"""The results of invoking :func:`python:urlparse.urlsplit` on
:attr:`path<python:BaseHTTPServer.BaseHTTPRequestHandler.path>`.
"""
_Resources = None
"""A map from absolute path segment prefixes to instances of
:class:`HTTPResource`.
"""
@classmethod
[docs] def add_resource(cls, resource):
"""Add *resource* to the registry for this class at
:attr:`resource.path<HTTPResource.path>`. This function is
automatically invoked in the constructor for
:class:`HTTPResource`.
"""
if cls._Resources is None:
cls._Resources = {}
if not isinstance(resource, HTTPResource):
raise TypeError(resource)
if resource.path in cls._Resources:
raise ValueError(resource)
cls._Resources[resource.path] = resource
@classmethod
[docs] def remove_resource(cls, resource):
"""Remove *resource* from the registry."""
del cls._Resources[resource.path]
@classmethod
[docs] def lookup_resource(cls, path):
"""Find the best registered resource at *path*.
*path* is a slash-separated hierarchy of path segments. The
registered resource for which :attr:`HTTPResource.path`
matches a segmented prefix of *path* is returned. If no
prefix matches, ``None`` is returned.
"""
segments = path.split('/')
while segments:
path = '/'.join(segments)
resource = cls._Resources.get(path)
if resource is not None:
return resource
segments.pop()
return None
# NOTE: The implementation of this method is from Python 2.7
# BaseHTTPServer.py, modified as marked in the code.
[docs] def handle_one_request(self):
"""Handle a single HTTP request.
Replace parts of
:meth:`python:BaseHTTPServer.BaseHTTPRequestHandler.handle_one_request`
to identify a :class:`HTTPResource` instance using the ``path``
part of the URI, and delegate all processing (including
checking for supported methods) to that resource. If no
segment prefix of ``path`` is associated with a registered
resource a 404 error is returned to the client.
This function assigns :attr:`split_uri`.
.. note::
Resource lookup uses the the `path part of the URI
<http://tools.ietf.org/html/rfc3986.html#section-3.3>`_,
exclusive of any query or fragment components that may also
be present in
:attr:`path<python:BaseHTTPServer.BaseHTTPRequestHandler.path>`.
"""
try:
self.raw_requestline = self.rfile.readline(65537)
if len(self.raw_requestline) > 65536:
self.requestline = ''
self.request_version = ''
self.command = ''
self.send_error(414)
return
if not self.raw_requestline:
self.close_connection = 1
return
if not self.parse_request():
# An error code has been sent, just exit
return
# >>> Modifications begin here:
self.split_uri = urlparse.urlsplit(self.path)
resource = self.lookup_resource(self.split_uri.path)
if resource is None:
self.send_error(404)
return
resource.handle_request(self)
# <<< Modifications end here
self.wfile.flush() #actually send the response if not already done.
except socket.timeout, e:
#a read or a write timed out. Discard this connection
self.log_error("Request timed out: %r", e)
self.close_connection = 1
return
[docs]class HTTPResource(object):
"""Class supporting a resource with specific methods.
Instances of this class are delegates from
:class:`HTTPRequestHandler` (or a subclass). The appropriate
resource is identified by a segment prefix of the
:attr:`path<HTTPRequestHandler.path>`. ``do_FOO`` methods are
implemented in the resource rather than in the request handler.
*path* is the absolute path to the resource. *handler_class* is
(the subclass of) :class:`HTTPRequestHandler` into which the
created resource will be registered at *path*.
Note that the architectural model supports multiple instances of a
resource class at different paths.
"""
@property
[docs] def path(self):
"""A read-only property specifying the prefix under which
this resource is registered within :attr:`handler_class`.
"""
return self.__path
@property
[docs] def handler_class(self):
"""A read-only property providing the subclass of
:class:`HTTPRequestHandler` within which this resource is
registered.
"""
return self.__handler_class
def __init__(self, path, handler_class=HTTPRequestHandler):
if not issubclass(handler_class, HTTPRequestHandler):
raise TypeError(handler_class)
self.__path = path
self.__handler_class = handler_class
handler_class.add_resource(self)
[docs] def handle_request(self, request):
"""Process a single request.
The :attr:`command<python:BaseHTTPRequestHandler.command>`
from the request is append to ``do_`` and the resulting method
is invoked to execute the operation. If the command is not
implemented, a 501 Unsupported Method error is returned to the
client.
"""
self.request = request
mname = 'do_' + request.command
meth = getattr(self, mname, None)
if meth is None:
request.send_error(501, "Unsupported method (%r)" % request.command)
return
try:
meth(request)
except NotImplementedError:
request.send_error(501, "Unsupported method (%r)" % request.command)
[docs] def do_HEAD(self, request):
"""Default implementation delegates to :meth:`do_GET` with
``head_only=True``.
"""
self.do_GET(request, head_only=True)
[docs] def do_GET(self, request, head_only=False):
"""Stub for a `GET
<http://tools.ietf.org/html/rfc2616#section-9.3>`_ method.
*request* is the instance of :class:`HTTPRequestHandler` that
has the request-specific data and ability to communicate
results to the client. *head_only* is ``False`` normally, but
may be set to ``True`` to allow this function to also
implement the `HEAD
<http://tools.ietf.org/html/rfc2616#section-9.4>`_ method: the
implementation is responsible for eliding the body of the
response in that case.
"""
raise NotImplementedError