~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
4183.7.1 by Sabin Iacob
update FSF mailing address
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
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
3245.4.16 by Andrew Bennetts
Remove duplication of request version identification logic in wsgi.py
25
from bzrlib.smart import protocol, medium
2018.5.21 by Andrew Bennetts
Move bzrlib.transport.smart to bzrlib.smart
26
from bzrlib.transport import chroot, get_transport
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
27
from bzrlib.urlutils import local_path_to_url
3943.8.1 by Marius Kruger
remove all trailing whitespace from bzr source
28
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
29
3708.1.1 by Marius Kruger
wsgi.make_app can now optionally load plugins (used by bzr+http://)
30
def make_app(root, prefix, path_var='REQUEST_URI', readonly=True,
3708.1.2 by Marius Kruger
wsgi.make_app can now optionally do normal logging (used by bzr+http://)
31
    load_plugins=True, enable_logging=True):
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
32
    """Convenience function to construct a WSGI bzr smart server.
3943.8.1 by Marius Kruger
remove all trailing whitespace from bzr source
33
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
34
    :param root: a local path that requests will be relative to.
35
    :param prefix: See RelpathSetter.
36
    :param path_var: See RelpathSetter.
37
    """
2190.1.4 by John Arbash Meinel
Add ability to enable writeable bzr+http access.
38
    local_url = local_path_to_url(root)
39
    if readonly:
40
        base_transport = get_transport('readonly+' + local_url)
41
    else:
42
        base_transport = get_transport(local_url)
3708.1.1 by Marius Kruger
wsgi.make_app can now optionally load plugins (used by bzr+http://)
43
    if load_plugins:
44
        from bzrlib.plugin import load_plugins
45
        load_plugins()
3708.1.2 by Marius Kruger
wsgi.make_app can now optionally do normal logging (used by bzr+http://)
46
    if enable_logging:
47
        import bzrlib.trace
48
        bzrlib.trace.enable_default_logging()
2692.1.14 by Andrew Bennetts
All WSGI tests passing, and manual testing works too.
49
    app = SmartWSGIApp(base_transport, prefix)
50
    app = RelpathSetter(app, '', path_var)
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
51
    return app
52
53
54
class RelpathSetter(object):
55
    """WSGI middleware to set 'bzrlib.relpath' in the environ.
3943.8.1 by Marius Kruger
remove all trailing whitespace from bzr source
56
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
57
    Different servers can invoke a SmartWSGIApp in different ways.  This
58
    middleware allows an adminstrator to configure how to the SmartWSGIApp will
59
    determine what path it should be serving for a given request for many common
60
    situations.
61
62
    For example, a request for "/some/prefix/repo/branch/.bzr/smart" received by
63
    a typical Apache and mod_fastcgi configuration will set `REQUEST_URI` to
64
    "/some/prefix/repo/branch/.bzr/smart".  A RelpathSetter with
65
    prefix="/some/prefix/" and path_var="REQUEST_URI" will set that request's
66
    'bzrlib.relpath' variable to "repo/branch".
67
    """
3943.8.1 by Marius Kruger
remove all trailing whitespace from bzr source
68
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
69
    def __init__(self, app, prefix='', path_var='REQUEST_URI'):
70
        """Constructor.
71
72
        :param app: WSGI app to wrap, e.g. a SmartWSGIApp instance.
73
        :param path_var: the variable in the WSGI environ to calculate the
74
            'bzrlib.relpath' variable from.
75
        :param prefix: a prefix to strip from the variable specified in
76
            path_var before setting 'bzrlib.relpath'.
77
        """
78
        self.app = app
79
        self.prefix = prefix
80
        self.path_var = path_var
81
82
    def __call__(self, environ, start_response):
83
        path = environ[self.path_var]
84
        suffix = '/.bzr/smart'
85
        if not (path.startswith(self.prefix) and path.endswith(suffix)):
2705.2.1 by Matt Nordhoff
wsgi.RelpathSetter sent an empty dict of headers instead of an empty list in 404 error responses.
86
            start_response('404 Not Found', [])
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
87
            return []
88
        environ['bzrlib.relpath'] = path[len(self.prefix):-len(suffix)]
89
        return self.app(environ, start_response)
90
91
92
class SmartWSGIApp(object):
93
    """A WSGI application for the bzr smart server."""
94
2692.1.1 by Andrew Bennetts
Add translate_client_path method to SmartServerRequest.
95
    def __init__(self, backing_transport, root_client_path='/'):
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
96
        """Constructor.
97
98
        :param backing_transport: a transport.  Requests will be processed
99
            relative to this transport.
2692.1.1 by Andrew Bennetts
Add translate_client_path method to SmartServerRequest.
100
        :param root_client_path: the client path that maps to the root of
101
            backing_transport.  This is used to interpret relpaths received from
102
            the client.
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
103
        """
2018.4.11 by Andrew Bennetts
Use ChrootTransportDecorator so that the WSGI server won't let you access the entire filesystem.
104
        # Use a ChrootTransportDecorator so that this web application won't
105
        # accidentally let people access locations they shouldn't.
106
        # e.g. consider a smart server request for "get /etc/passwd" or
107
        # something.
2018.5.104 by Andrew Bennetts
Completely rework chrooted transports.
108
        self.chroot_server = chroot.ChrootServer(backing_transport)
4934.3.3 by Martin Pool
Rename Server.setUp to Server.start_server
109
        self.chroot_server.start_server()
2018.5.104 by Andrew Bennetts
Completely rework chrooted transports.
110
        self.backing_transport = get_transport(self.chroot_server.get_url())
2692.1.1 by Andrew Bennetts
Add translate_client_path method to SmartServerRequest.
111
        self.root_client_path = root_client_path
2379.2.1 by Robert Collins
Rewritten chroot transport that prevents accidental chroot escapes when
112
        # While the chroot server can technically be torn down at this point,
3943.8.1 by Marius Kruger
remove all trailing whitespace from bzr source
113
        # as all it does is remove the scheme registration from transport's
114
        # protocol dictionary, we don't *just in case* there are parts of
2379.2.1 by Robert Collins
Rewritten chroot transport that prevents accidental chroot escapes when
115
        # bzrlib that will invoke 'get_transport' on urls rather than cloning
116
        # around the existing transport.
4934.3.1 by Martin Pool
Rename Server.tearDown to .stop_server
117
        #self.chroot_server.stop_server()
2018.5.104 by Andrew Bennetts
Completely rework chrooted transports.
118
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
119
    def __call__(self, environ, start_response):
120
        """WSGI application callable."""
121
        if environ['REQUEST_METHOD'] != 'POST':
122
            start_response('405 Method not allowed', [('Allow', 'POST')])
123
            return []
124
125
        relpath = environ['bzrlib.relpath']
2692.1.14 by Andrew Bennetts
All WSGI tests passing, and manual testing works too.
126
127
        if not relpath.startswith('/'):
128
            relpath = '/' + relpath
129
        if not relpath.endswith('/'):
130
            relpath += '/'
131
2692.1.16 by Andrew Bennetts
Improve comments.
132
        # Compare the HTTP path (relpath) and root_client_path, and calculate
133
        # new relpath and root_client_path accordingly, to be used to build the
134
        # request.
2692.1.14 by Andrew Bennetts
All WSGI tests passing, and manual testing works too.
135
        if relpath.startswith(self.root_client_path):
2692.1.16 by Andrew Bennetts
Improve comments.
136
            # The relpath traverses all of the mandatory root client path.
137
            # Remove the root_client_path from the relpath, and set
138
            # adjusted_tcp to None to tell the request handler that no further
139
            # path translation is required.
2692.1.14 by Andrew Bennetts
All WSGI tests passing, and manual testing works too.
140
            adjusted_rcp = None
141
            adjusted_relpath = relpath[len(self.root_client_path):]
142
        elif self.root_client_path.startswith(relpath):
143
            # The relpath traverses some of the mandatory root client path.
2692.1.16 by Andrew Bennetts
Improve comments.
144
            # Subtract the relpath from the root_client_path, and set the
145
            # relpath to '.'.
2692.1.14 by Andrew Bennetts
All WSGI tests passing, and manual testing works too.
146
            adjusted_rcp = '/' + self.root_client_path[len(relpath):]
147
            adjusted_relpath = '.'
148
        else:
149
            adjusted_rcp = self.root_client_path
150
            adjusted_relpath = relpath
151
152
        if adjusted_relpath.startswith('/'):
153
            adjusted_relpath = adjusted_relpath[1:]
3376.2.4 by Martin Pool
Remove every assert statement from bzrlib!
154
        if adjusted_relpath.startswith('/'):
155
            raise AssertionError(adjusted_relpath)
2692.1.14 by Andrew Bennetts
All WSGI tests passing, and manual testing works too.
156
157
        transport = self.backing_transport.clone(adjusted_relpath)
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
158
        out_buffer = StringIO()
159
        request_data_length = int(environ['CONTENT_LENGTH'])
160
        request_data_bytes = environ['wsgi.input'].read(request_data_length)
2432.2.2 by Andrew Bennetts
Smart server mediums now detect which protocol version a request is and dispatch accordingly.
161
        smart_protocol_request = self.make_request(
2692.1.14 by Andrew Bennetts
All WSGI tests passing, and manual testing works too.
162
            transport, out_buffer.write, request_data_bytes, adjusted_rcp)
2018.4.5 by Andrew Bennetts
Improvement thanks to John's review.
163
        if smart_protocol_request.next_read_size() != 0:
164
            # The request appears to be incomplete, or perhaps it's just a
165
            # newer version we don't understand.  Regardless, all we can do
166
            # is return an error response in the format of our version of the
167
            # protocol.
168
            response_data = 'error\x01incomplete request\n'
169
        else:
170
            response_data = out_buffer.getvalue()
2018.4.1 by Andrew Bennetts
Add WSGI smart server.
171
        headers = [('Content-type', 'application/octet-stream')]
172
        headers.append(("Content-Length", str(len(response_data))))
173
        start_response('200 OK', headers)
174
        return [response_data]
175
2692.1.14 by Andrew Bennetts
All WSGI tests passing, and manual testing works too.
176
    def make_request(self, transport, write_func, request_bytes, rcp):
3245.4.16 by Andrew Bennetts
Remove duplication of request version identification logic in wsgi.py
177
        protocol_factory, unused_bytes = medium._get_protocol_factory_for_bytes(
178
            request_bytes)
4760.1.1 by Andrew Bennetts
Add optional jail_root argument to SmartServerRequest and friends, and use it in the WSGI glue. Allows opening branches in shared repos via bzr+http (assuming the repo should be accessible).
179
        server_protocol = protocol_factory(
180
            transport, write_func, rcp, self.backing_transport)
3245.4.16 by Andrew Bennetts
Remove duplication of request version identification logic in wsgi.py
181
        server_protocol.accept_bytes(unused_bytes)
2432.2.2 by Andrew Bennetts
Smart server mediums now detect which protocol version a request is and dispatch accordingly.
182
        return server_protocol