~launchpad-pqm/launchpad-loggerhead/devel

53.1.1 by Karl Fogel
License under the AGPLv3.
1
# Copyright 2009 Canonical Ltd.  This software is licensed under the
2
# GNU Affero General Public License version 3 (see the file LICENSE).
3
38.3.1 by Tim Penhey
More logging.
4
import logging
29.2.9 by Michael Hudson
code layout pendantry
5
import re
29.2.2 by Michael Hudson
add the actual file (oops) with some fixes
6
import os
31.1.7 by Michael Hudson
one transport per thread
7
import threading
35.3.7 by Michael Hudson
maybe this works??
8
import urllib
29.2.2 by Michael Hudson
add the actual file (oops) with some fixes
9
import urlparse
35.2.3 by Michael Hudson
it works it works ship it!
10
import xmlrpclib
29.2.2 by Michael Hudson
add the actual file (oops) with some fixes
11
48.1.1 by Michael Hudson
escape path before handing it to translatePath
12
from bzrlib import branch, errors, lru_cache, urlutils
35.3.7 by Michael Hudson
maybe this works??
13
14
from loggerhead.apps import favicon_app, static_app
15
from loggerhead.apps.branch import BranchWSGIApp
16
17
from openid.extensions.sreg import SRegRequest, SRegResponse
35.3.13 by Michael Hudson
do something if response is a failure!!
18
from openid.consumer.consumer import CANCEL, Consumer, FAILURE, SUCCESS
35.3.7 by Michael Hudson
maybe this works??
19
from openid.store.memstore import MemoryStore
20
29.2.2 by Michael Hudson
add the actual file (oops) with some fixes
21
from paste.fileapp import DataApp
35.3.7 by Michael Hudson
maybe this works??
22
from paste.request import construct_url, parse_querystring, path_info_pop
35.3.9 by Michael Hudson
more workingness
23
from paste.httpexceptions import (
24
    HTTPMovedPermanently, HTTPNotFound, HTTPUnauthorized)
29.2.2 by Michael Hudson
add the actual file (oops) with some fixes
25
35.1.2 by Michael Hudson
stop using our own config
26
from canonical.config import config
49.1.1 by Jonathan Lange
Update import locations, since codehosting is now under lp.
27
from canonical.launchpad.xmlrpc import faults
46.1.1 by Jonathan Lange
Fix the import
28
from lp.code.interfaces.codehosting import (
38.1.1 by Michael Hudson
is this it?
29
    BRANCH_TRANSPORT, LAUNCHPAD_ANONYMOUS, LAUNCHPAD_SERVICES)
49.1.1 by Jonathan Lange
Update import locations, since codehosting is now under lp.
30
from lp.codehosting.vfs import branch_id_to_path
29.2.9 by Michael Hudson
code layout pendantry
31
29.2.2 by Michael Hudson
add the actual file (oops) with some fixes
32
robots_txt = '''\
33
User-agent: *
34
Disallow: /
35
'''
36
37
robots_app = DataApp(robots_txt, content_type='text/plain')
38
39
31.1.7 by Michael Hudson
one transport per thread
40
thread_transports = threading.local()
41
29.2.2 by Michael Hudson
add the actual file (oops) with some fixes
42
def valid_launchpad_name(s):
43
    return re.match('^[a-z0-9][a-z0-9\+\.\-]*$', s) is not None
44
29.2.9 by Michael Hudson
code layout pendantry
45
29.2.2 by Michael Hudson
add the actual file (oops) with some fixes
46
def valid_launchpad_user_name(s):
47
    return re.match('^~[a-z0-9][a-z0-9\+\.\-]*$', s) is not None
48
29.2.9 by Michael Hudson
code layout pendantry
49
29.2.2 by Michael Hudson
add the actual file (oops) with some fixes
50
def valid_launchpad_branch_name(s):
51
    return re.match(r'^(?i)[a-z0-9][a-z0-9+\.\-@_]*\Z', s) is not None
52
29.2.9 by Michael Hudson
code layout pendantry
53
29.2.2 by Michael Hudson
add the actual file (oops) with some fixes
54
class RootApp:
55
35.3.1 by Michael Hudson
outline
56
    def __init__(self, session_var):
44.3.1 by Michael Hudson
cache vastly less whole-history data
57
        self.graph_cache = lru_cache.LRUCache(10)
35.2.3 by Michael Hudson
it works it works ship it!
58
        self.branchfs = xmlrpclib.ServerProxy(
35.2.5 by Michael Hudson
merge run-codebrowse-in-tree
59
            config.codehosting.branchfs_endpoint)
35.3.1 by Michael Hudson
outline
60
        self.session_var = session_var
35.3.7 by Michael Hudson
maybe this works??
61
        self.store = MemoryStore()
38.3.1 by Tim Penhey
More logging.
62
        self.log = logging.getLogger('lp-loggerhead')
38.1.1 by Michael Hudson
is this it?
63
        branch.Branch.hooks.install_named_hook(
64
            'transform_fallback_location',
65
            self._transform_fallback_location_hook,
66
            'RootApp._transform_fallback_location_hook')
67
38.1.2 by Michael Hudson
workingness++
68
    def _transform_fallback_location_hook(self, branch, url):
38.1.3 by Michael Hudson
docs
69
        """Transform a human-readable fallback URL into and id-based one.
70
71
        Branches on Launchpad record their stacked-on URLs in the form
72
        '/~user/product/branch', but we need to access branches based on
73
        database ID to gain access to private branches.  So we use this hook
74
        into Bazaar's branch-opening process to translate the former to the
75
        latter.
76
        """
77
        # It might seem that using the LAUNCHPAD_SERVICES 'user', which allows
78
        # access to all branches, here would be a security risk.  But in fact
79
        # it isn't, because a user will only have launchpad.View on the
80
        # stacked branch if they have it for all the stacked-on branches.
81
        # (It would be nice to use the user from the request, but that's far
82
        # from simple because branch hooks are global per-process and we
83
        # handle different requests in different threads).
38.1.1 by Michael Hudson
is this it?
84
        transport_type, info, trail = self.branchfs.translatePath(
85
            LAUNCHPAD_SERVICES, url)
86
        return urlparse.urljoin(
87
            config.codehosting.internal_branch_by_id_root,
88
            branch_id_to_path(info['id']))
29.2.2 by Michael Hudson
add the actual file (oops) with some fixes
89
31.1.7 by Michael Hudson
one transport per thread
90
    def get_transports(self):
91
        t = getattr(thread_transports, 'transports', None)
92
        if t is None:
93
            thread_transports.transports = []
94
        return thread_transports.transports
95
35.3.16 by Michael Hudson
docstrings!
96
    def _make_consumer(self, environ):
97
        """Build an OpenID `Consumer` object with standard arguments."""
98
        return Consumer(environ[self.session_var], self.store)
99
35.3.7 by Michael Hudson
maybe this works??
100
    def _begin_login(self, environ, start_response):
35.3.16 by Michael Hudson
docstrings!
101
        """Start the process of authenticating with OpenID.
102
103
        We redirect the user to Launchpad to identify themselves, asking to be
35.3.18 by Michael Hudson
typos
104
        sent their nickname.  Launchpad will then redirect them to our +login
35.3.16 by Michael Hudson
docstrings!
105
        page with enough information that we can then redirect them again to
35.3.18 by Michael Hudson
typos
106
        the page they were looking at, with a cookie that gives us the
107
        username.
35.3.16 by Michael Hudson
docstrings!
108
        """
35.3.8 by Michael Hudson
this now works in simple cases
109
        openid_request = self._make_consumer(environ).begin(
52.1.1 by Michael Hudson
argh that config section went away!
110
            'https://' + config.vhost.openid.hostname)
35.3.7 by Michael Hudson
maybe this works??
111
        openid_request.addExtension(
112
            SRegRequest(required=['nickname']))
113
        back_to = construct_url(environ)
114
        raise HTTPMovedPermanently(openid_request.redirectURL(
35.3.15 by Michael Hudson
less roundtrippy
115
            config.codehosting.secure_codebrowse_root,
116
            config.codehosting.secure_codebrowse_root + '+login/?'
35.3.7 by Michael Hudson
maybe this works??
117
            + urllib.urlencode({'back_to':back_to})))
118
119
    def _complete_login(self, environ, start_response):
35.3.16 by Michael Hudson
docstrings!
120
        """Complete the OpenID authentication process.
121
122
        Here we handle the result of the OpenID process.  If the process
123
        succeeded, we record the username in the session and redirect the user
124
        to the page they were trying to view that triggered the login attempt.
125
        In the various failures cases we return a 401 Unauthorized response
126
        with a brief explanation of what went wrong.
127
        """
35.3.7 by Michael Hudson
maybe this works??
128
        query = dict(parse_querystring(environ))
35.3.16 by Michael Hudson
docstrings!
129
        # Passing query['openid.return_to'] here is massive cheating, but
130
        # given we control the endpoint who cares.
35.3.13 by Michael Hudson
do something if response is a failure!!
131
        response = self._make_consumer(environ).complete(
132
            query, query['openid.return_to'])
133
        if response.status == SUCCESS:
38.3.1 by Tim Penhey
More logging.
134
            self.log.error('open id response: SUCCESS')
35.3.13 by Michael Hudson
do something if response is a failure!!
135
            sreg_info = SRegResponse.fromSuccessResponse(response)
136
            environ[self.session_var]['user'] = sreg_info['nickname']
137
            raise HTTPMovedPermanently(query['back_to'])
138
        elif response.status == FAILURE:
38.3.1 by Tim Penhey
More logging.
139
            self.log.error('open id response: FAILURE: %s', response.message)
35.3.13 by Michael Hudson
do something if response is a failure!!
140
            exc = HTTPUnauthorized()
141
            exc.explanation = response.message
142
            raise exc
143
        elif response.status == CANCEL:
38.3.1 by Tim Penhey
More logging.
144
            self.log.error('open id response: CANCEL')
35.3.13 by Michael Hudson
do something if response is a failure!!
145
            exc = HTTPUnauthorized()
146
            exc.explanation = "Authetication cancelled."
147
            raise exc
148
        else:
38.3.1 by Tim Penhey
More logging.
149
            self.log.error('open id response: UNKNOWN')
35.3.13 by Michael Hudson
do something if response is a failure!!
150
            exc = HTTPUnauthorized()
151
            exc.explanation = "Unknown OpenID response."
152
            raise exc
35.3.7 by Michael Hudson
maybe this works??
153
29.2.2 by Michael Hudson
add the actual file (oops) with some fixes
154
    def __call__(self, environ, start_response):
35.3.7 by Michael Hudson
maybe this works??
155
        environ['loggerhead.static.url'] = environ['SCRIPT_NAME']
29.2.2 by Michael Hudson
add the actual file (oops) with some fixes
156
        if environ['PATH_INFO'].startswith('/static/'):
35.3.7 by Michael Hudson
maybe this works??
157
            path_info_pop(environ)
29.2.2 by Michael Hudson
add the actual file (oops) with some fixes
158
            return static_app(environ, start_response)
159
        elif environ['PATH_INFO'] == '/favicon.ico':
160
            return favicon_app(environ, start_response)
161
        elif environ['PATH_INFO'] == '/robots.txt':
162
            return robots_app(environ, start_response)
35.3.7 by Michael Hudson
maybe this works??
163
        elif environ['PATH_INFO'].startswith('/+login'):
164
            return self._complete_login(environ, start_response)
35.2.3 by Michael Hudson
it works it works ship it!
165
        path = environ['PATH_INFO']
166
        trailingSlashCount = len(path) - len(path.rstrip('/'))
35.3.7 by Michael Hudson
maybe this works??
167
        user = environ[self.session_var].get('user', LAUNCHPAD_ANONYMOUS)
35.2.4 by Michael Hudson
cleanups
168
        try:
35.2.6 by Michael Hudson
review comments
169
            transport_type, info, trail = self.branchfs.translatePath(
48.1.1 by Michael Hudson
escape path before handing it to translatePath
170
                user, urlutils.escape(path))
35.2.4 by Michael Hudson
cleanups
171
        except xmlrpclib.Fault, f:
35.3.7 by Michael Hudson
maybe this works??
172
            if faults.check_fault(f, faults.PathTranslationError):
51.1.1 by Michael Hudson
we should really 404 here!
173
                raise HTTPNotFound()
35.3.7 by Michael Hudson
maybe this works??
174
            elif faults.check_fault(f, faults.PermissionDenied):
35.3.16 by Michael Hudson
docstrings!
175
                # If we're not allowed to see the branch...
35.3.9 by Michael Hudson
more workingness
176
                if environ['wsgi.url_scheme'] != 'https':
35.3.16 by Michael Hudson
docstrings!
177
                    # ... the request shouldn't have come in over http, as
178
                    # requests for private branches over http should be
35.3.19 by Michael Hudson
docstring
179
                    # redirected to https by the dynamic rewrite script we use
180
                    # (which runs before this code is reached), but just in
181
                    # case...
35.3.9 by Michael Hudson
more workingness
182
                    env_copy = environ.copy()
183
                    env_copy['wsgi.url_scheme'] = 'https'
35.3.10 by Michael Hudson
serve on a different port for https connections :/
184
                    raise HTTPMovedPermanently(construct_url(env_copy))
35.3.9 by Michael Hudson
more workingness
185
                elif user != LAUNCHPAD_ANONYMOUS:
35.3.16 by Michael Hudson
docstrings!
186
                    # ... if the user is already logged in and still can't see
187
                    # the branch, they lose.
35.3.9 by Michael Hudson
more workingness
188
                    exc = HTTPUnauthorized()
189
                    exc.explanation = "You are logged in as %s." % user
190
                    raise exc
35.3.7 by Michael Hudson
maybe this works??
191
                else:
35.3.16 by Michael Hudson
docstrings!
192
                    # ... otherwise, lets give them a chance to log in with
193
                    # OpenID.
35.3.7 by Michael Hudson
maybe this works??
194
                    return self._begin_login(environ, start_response)
35.2.4 by Michael Hudson
cleanups
195
            else:
196
                raise
35.2.6 by Michael Hudson
review comments
197
        if transport_type != BRANCH_TRANSPORT:
51.1.1 by Michael Hudson
we should really 404 here!
198
            raise HTTPNotFound()
48.1.1 by Michael Hudson
escape path before handing it to translatePath
199
        trail = urlutils.unescape(trail).encode('utf-8')
35.2.3 by Michael Hudson
it works it works ship it!
200
        trail += trailingSlashCount * '/'
201
        amount_consumed = len(path) - len(trail)
202
        consumed = path[:amount_consumed]
35.2.4 by Michael Hudson
cleanups
203
        branch_name = consumed.strip('/')
38.3.1 by Tim Penhey
More logging.
204
        self.log.info('Using branch: %s', branch_name)
35.2.3 by Michael Hudson
it works it works ship it!
205
        if trail and not trail.startswith('/'):
206
            trail = '/' + trail
207
        environ['PATH_INFO'] = trail
208
        environ['SCRIPT_NAME'] += consumed.rstrip('/')
29.2.2 by Michael Hudson
add the actual file (oops) with some fixes
209
        branch_url = urlparse.urljoin(
35.2.5 by Michael Hudson
merge run-codebrowse-in-tree
210
            config.codehosting.internal_branch_by_id_root,
35.2.3 by Michael Hudson
it works it works ship it!
211
            branch_id_to_path(info['id']))
34.1.1 by Tim Penhey
Add back links to launchpad.
212
        branch_link = urlparse.urljoin(
35.1.2 by Michael Hudson
stop using our own config
213
            config.codebrowse.launchpad_root, branch_name)
29.2.2 by Michael Hudson
add the actual file (oops) with some fixes
214
        cachepath = os.path.join(
35.1.2 by Michael Hudson
stop using our own config
215
            config.codebrowse.cachepath, branch_name[1:])
29.2.2 by Michael Hudson
add the actual file (oops) with some fixes
216
        if not os.path.isdir(cachepath):
217
            os.makedirs(cachepath)
38.3.1 by Tim Penhey
More logging.
218
        self.log.info('branch_url: %s', branch_url)
29.2.2 by Michael Hudson
add the actual file (oops) with some fixes
219
        try:
31.1.7 by Michael Hudson
one transport per thread
220
            bzr_branch = branch.Branch.open(
221
                branch_url, possible_transports=self.get_transports())
38.3.1 by Tim Penhey
More logging.
222
        except errors.NotBranchError, err:
223
            self.log.warning('Not a branch: %s', err)
29.2.2 by Michael Hudson
add the actual file (oops) with some fixes
224
            raise HTTPNotFound()
31.1.3 by Michael Hudson
keep up with changes in loggerhead
225
        bzr_branch.lock_read()
226
        try:
227
            view = BranchWSGIApp(
35.2.4 by Michael Hudson
cleanups
228
                bzr_branch, branch_name, {'cachepath': cachepath},
44.2.1 by Michael Hudson
hide the box
229
                self.graph_cache, branch_link=branch_link, served_url=None)
31.1.3 by Michael Hudson
keep up with changes in loggerhead
230
            return view.app(environ, start_response)
231
        finally:
232
            bzr_branch.unlock()