~bzr-pqm/bzr/bzr.dev

2018.4.1 by Andrew Bennetts
Add WSGI smart server.
1
# Copyright (C) 2006 Canonical Ltd
2
#
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
7
#
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
# GNU General Public License for more details.
12
#
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
17
"""WSGI application for bzr HTTP smart server.
18
19
For more information about WSGI, see PEP 333:
20
    http://www.python.org/dev/peps/pep-0333/
21
"""
22
23
from cStringIO import StringIO
24
2018.4.11 by Andrew Bennetts
Use ChrootTransportDecorator so that the WSGI server won't let you access the entire filesystem.
25
from bzrlib.transport import chroot, get_transport, smart
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
26
from bzrlib.urlutils import local_path_to_url
27
    
28
2190.1.4 by John Arbash Meinel
Add ability to enable writeable bzr+http access.
29
def make_app(root, prefix, path_var, readonly=True):
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
30
    """Convenience function to construct a WSGI bzr smart server.
31
    
32
    :param root: a local path that requests will be relative to.
33
    :param prefix: See RelpathSetter.
34
    :param path_var: See RelpathSetter.
35
    """
2190.1.4 by John Arbash Meinel
Add ability to enable writeable bzr+http access.
36
    local_url = local_path_to_url(root)
37
    if readonly:
38
        base_transport = get_transport('readonly+' + local_url)
39
    else:
40
        base_transport = get_transport(local_url)
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
41
    app = SmartWSGIApp(base_transport)
42
    app = RelpathSetter(app, prefix, path_var)
43
    return app
44
45
46
class RelpathSetter(object):
47
    """WSGI middleware to set 'bzrlib.relpath' in the environ.
48
    
49
    Different servers can invoke a SmartWSGIApp in different ways.  This
50
    middleware allows an adminstrator to configure how to the SmartWSGIApp will
51
    determine what path it should be serving for a given request for many common
52
    situations.
53
54
    For example, a request for "/some/prefix/repo/branch/.bzr/smart" received by
55
    a typical Apache and mod_fastcgi configuration will set `REQUEST_URI` to
56
    "/some/prefix/repo/branch/.bzr/smart".  A RelpathSetter with
57
    prefix="/some/prefix/" and path_var="REQUEST_URI" will set that request's
58
    'bzrlib.relpath' variable to "repo/branch".
59
    """
60
    
61
    def __init__(self, app, prefix='', path_var='REQUEST_URI'):
62
        """Constructor.
63
64
        :param app: WSGI app to wrap, e.g. a SmartWSGIApp instance.
65
        :param path_var: the variable in the WSGI environ to calculate the
66
            'bzrlib.relpath' variable from.
67
        :param prefix: a prefix to strip from the variable specified in
68
            path_var before setting 'bzrlib.relpath'.
69
        """
70
        self.app = app
71
        self.prefix = prefix
72
        self.path_var = path_var
73
74
    def __call__(self, environ, start_response):
75
        path = environ[self.path_var]
76
        suffix = '/.bzr/smart'
77
        if not (path.startswith(self.prefix) and path.endswith(suffix)):
78
            start_response('404 Not Found', {})
79
            return []
80
        environ['bzrlib.relpath'] = path[len(self.prefix):-len(suffix)]
81
        return self.app(environ, start_response)
82
83
84
class SmartWSGIApp(object):
85
    """A WSGI application for the bzr smart server."""
86
87
    def __init__(self, backing_transport):
88
        """Constructor.
89
90
        :param backing_transport: a transport.  Requests will be processed
91
            relative to this transport.
92
        """
2018.4.11 by Andrew Bennetts
Use ChrootTransportDecorator so that the WSGI server won't let you access the entire filesystem.
93
        # Use a ChrootTransportDecorator so that this web application won't
94
        # accidentally let people access locations they shouldn't.
95
        # e.g. consider a smart server request for "get /etc/passwd" or
96
        # something.
97
        self.backing_transport = chroot.ChrootTransportDecorator(
98
            'chroot+' + backing_transport.base, _decorated=backing_transport)
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
99
100
    def __call__(self, environ, start_response):
101
        """WSGI application callable."""
102
        if environ['REQUEST_METHOD'] != 'POST':
103
            start_response('405 Method not allowed', [('Allow', 'POST')])
104
            return []
105
106
        relpath = environ['bzrlib.relpath']
107
        transport = self.backing_transport.clone(relpath)
108
        out_buffer = StringIO()
109
        smart_protocol_request = self.make_request(transport, out_buffer.write)
110
        request_data_length = int(environ['CONTENT_LENGTH'])
111
        request_data_bytes = environ['wsgi.input'].read(request_data_length)
112
        smart_protocol_request.accept_bytes(request_data_bytes)
2018.4.5 by Andrew Bennetts
Improvement thanks to John's review.
113
        if smart_protocol_request.next_read_size() != 0:
114
            # The request appears to be incomplete, or perhaps it's just a
115
            # newer version we don't understand.  Regardless, all we can do
116
            # is return an error response in the format of our version of the
117
            # protocol.
118
            response_data = 'error\x01incomplete request\n'
119
        else:
120
            response_data = out_buffer.getvalue()
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
121
        headers = [('Content-type', 'application/octet-stream')]
122
        headers.append(("Content-Length", str(len(response_data))))
123
        start_response('200 OK', headers)
124
        return [response_data]
125
126
    def make_request(self, transport, write_func):
127
        return smart.SmartServerRequestProtocolOne(transport, write_func)