~launchpad-pqm/launchpad/devel

13811.2.1 by Jeroen Vermeulen
Fix some of the lint people left in the past few days.
1
# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
9590.1.135 by Michael Hudson
add files from launchpad-loggerhead tree to launchpad tree
2
# GNU Affero General Public License version 3 (see the file LICENSE).
3
4
import logging
5
import os
6
import threading
7
import urllib
14158.2.1 by j.c.sackett
Checking for private status implemented.
8
import urllib2
9590.1.135 by Michael Hudson
add files from launchpad-loggerhead tree to launchpad tree
9
import urlparse
10
import xmlrpclib
11
13811.2.1 by Jeroen Vermeulen
Fix some of the lint people left in the past few days.
12
from bzrlib import (
13
    errors,
14
    lru_cache,
15
    urlutils,
16
    )
7675.668.1 by Michael Hudson
only call get_transport(internal_branch_by_id_root) once per thread
17
from bzrlib.transport import get_transport
13811.2.1 by Jeroen Vermeulen
Fix some of the lint people left in the past few days.
18
from loggerhead.apps import (
19
    favicon_app,
20
    static_app,
21
    )
9590.1.135 by Michael Hudson
add files from launchpad-loggerhead tree to launchpad tree
22
from loggerhead.apps.branch import BranchWSGIApp
13686.2.20 by Robert Collins
Migrate the launchpad_loggerhead implementation of oops_middleware to that within oops_wsgi.
23
import oops_wsgi
13811.2.1 by Jeroen Vermeulen
Fix some of the lint people left in the past few days.
24
from openid.consumer.consumer import (
25
    CANCEL,
26
    Consumer,
27
    FAILURE,
28
    SUCCESS,
29
    )
30
from openid.extensions.sreg import (
31
    SRegRequest,
32
    SRegResponse,
33
    )
9590.1.135 by Michael Hudson
add files from launchpad-loggerhead tree to launchpad tree
34
from paste.fileapp import DataApp
13686.2.20 by Robert Collins
Migrate the launchpad_loggerhead implementation of oops_middleware to that within oops_wsgi.
35
from paste.httpexceptions import (
36
    HTTPMovedPermanently,
37
    HTTPNotFound,
38
    HTTPUnauthorized,
39
    )
13811.2.1 by Jeroen Vermeulen
Fix some of the lint people left in the past few days.
40
from paste.request import (
41
    construct_url,
42
    parse_querystring,
43
    path_info_pop,
44
    )
9590.1.135 by Michael Hudson
add files from launchpad-loggerhead tree to launchpad tree
45
46
from canonical.config import config
13811.2.1 by Jeroen Vermeulen
Fix some of the lint people left in the past few days.
47
from canonical.launchpad.webapp.errorlog import ErrorReportingUtility
48
from canonical.launchpad.webapp.vhosts import allvhosts
9590.1.135 by Michael Hudson
add files from launchpad-loggerhead tree to launchpad tree
49
from canonical.launchpad.xmlrpc import faults
50
from lp.code.interfaces.codehosting import (
13811.2.1 by Jeroen Vermeulen
Fix some of the lint people left in the past few days.
51
    BRANCH_TRANSPORT,
52
    LAUNCHPAD_ANONYMOUS,
53
    )
54
from lp.codehosting.safe_open import safe_open
9590.1.136 by Michael Hudson
reapply changes to less exotically constructed tree
55
from lp.codehosting.vfs import get_lp_server
13811.2.1 by Jeroen Vermeulen
Fix some of the lint people left in the past few days.
56
9590.1.135 by Michael Hudson
add files from launchpad-loggerhead tree to launchpad tree
57
58
robots_txt = '''\
59
User-agent: *
60
Disallow: /
61
'''
62
63
robots_app = DataApp(robots_txt, content_type='text/plain')
64
65
66
thread_transports = threading.local()
67
9590.1.136 by Michael Hudson
reapply changes to less exotically constructed tree
68
69
def check_fault(fault, *fault_classes):
70
    """Check if 'fault's faultCode matches any of 'fault_classes'.
71
72
    :param fault: An instance of `xmlrpclib.Fault`.
73
    :param fault_classes: Any number of `LaunchpadFault` subclasses.
74
    """
75
    for cls in fault_classes:
76
        if fault.faultCode == cls.error_code:
77
            return True
78
    return False
9590.1.135 by Michael Hudson
add files from launchpad-loggerhead tree to launchpad tree
79
80
81
class RootApp:
82
83
    def __init__(self, session_var):
84
        self.graph_cache = lru_cache.LRUCache(10)
85
        self.branchfs = xmlrpclib.ServerProxy(
9590.1.136 by Michael Hudson
reapply changes to less exotically constructed tree
86
            config.codehosting.codehosting_endpoint)
9590.1.135 by Michael Hudson
add files from launchpad-loggerhead tree to launchpad tree
87
        self.session_var = session_var
88
        self.log = logging.getLogger('lp-loggerhead')
89
7675.668.1 by Michael Hudson
only call get_transport(internal_branch_by_id_root) once per thread
90
    def get_transport(self):
91
        t = getattr(thread_transports, 'transport', None)
9590.1.135 by Michael Hudson
add files from launchpad-loggerhead tree to launchpad tree
92
        if t is None:
7675.668.1 by Michael Hudson
only call get_transport(internal_branch_by_id_root) once per thread
93
            thread_transports.transport = get_transport(
94
                config.codehosting.internal_branch_by_id_root)
95
        return thread_transports.transport
9590.1.135 by Michael Hudson
add files from launchpad-loggerhead tree to launchpad tree
96
97
    def _make_consumer(self, environ):
98
        """Build an OpenID `Consumer` object with standard arguments."""
12289.1.1 by William Grant
Stop loggerhead from using an OpenID store. If a store is used it must be shared between instances, and it's easier to just not use one.
99
        # Multiple instances need to share a store or not use one at all (in
100
        # which case they will use check_authentication). Using no store is
101
        # easier, and check_authentication is cheap.
102
        return Consumer(environ[self.session_var], None)
9590.1.135 by Michael Hudson
add files from launchpad-loggerhead tree to launchpad tree
103
104
    def _begin_login(self, environ, start_response):
105
        """Start the process of authenticating with OpenID.
106
107
        We redirect the user to Launchpad to identify themselves, asking to be
108
        sent their nickname.  Launchpad will then redirect them to our +login
109
        page with enough information that we can then redirect them again to
110
        the page they were looking at, with a cookie that gives us the
111
        username.
112
        """
9590.1.136 by Michael Hudson
reapply changes to less exotically constructed tree
113
        openid_vhost = config.launchpad.openid_provider_vhost
9590.1.135 by Michael Hudson
add files from launchpad-loggerhead tree to launchpad tree
114
        openid_request = self._make_consumer(environ).begin(
9590.1.136 by Michael Hudson
reapply changes to less exotically constructed tree
115
            allvhosts.configs[openid_vhost].rooturl)
9590.1.135 by Michael Hudson
add files from launchpad-loggerhead tree to launchpad tree
116
        openid_request.addExtension(
117
            SRegRequest(required=['nickname']))
118
        back_to = construct_url(environ)
119
        raise HTTPMovedPermanently(openid_request.redirectURL(
120
            config.codehosting.secure_codebrowse_root,
121
            config.codehosting.secure_codebrowse_root + '+login/?'
13811.2.1 by Jeroen Vermeulen
Fix some of the lint people left in the past few days.
122
            + urllib.urlencode({'back_to': back_to})))
9590.1.135 by Michael Hudson
add files from launchpad-loggerhead tree to launchpad tree
123
124
    def _complete_login(self, environ, start_response):
125
        """Complete the OpenID authentication process.
126
127
        Here we handle the result of the OpenID process.  If the process
128
        succeeded, we record the username in the session and redirect the user
129
        to the page they were trying to view that triggered the login attempt.
130
        In the various failures cases we return a 401 Unauthorized response
131
        with a brief explanation of what went wrong.
132
        """
133
        query = dict(parse_querystring(environ))
134
        # Passing query['openid.return_to'] here is massive cheating, but
135
        # given we control the endpoint who cares.
136
        response = self._make_consumer(environ).complete(
137
            query, query['openid.return_to'])
138
        if response.status == SUCCESS:
139
            self.log.error('open id response: SUCCESS')
140
            sreg_info = SRegResponse.fromSuccessResponse(response)
9590.1.136 by Michael Hudson
reapply changes to less exotically constructed tree
141
            print sreg_info
9590.1.135 by Michael Hudson
add files from launchpad-loggerhead tree to launchpad tree
142
            environ[self.session_var]['user'] = sreg_info['nickname']
143
            raise HTTPMovedPermanently(query['back_to'])
144
        elif response.status == FAILURE:
145
            self.log.error('open id response: FAILURE: %s', response.message)
146
            exc = HTTPUnauthorized()
147
            exc.explanation = response.message
148
            raise exc
149
        elif response.status == CANCEL:
150
            self.log.error('open id response: CANCEL')
151
            exc = HTTPUnauthorized()
7675.671.1 by Gary Poster
log out from bzr and openid after logging out from Launchpad.
152
            exc.explanation = "Authentication cancelled."
9590.1.135 by Michael Hudson
add files from launchpad-loggerhead tree to launchpad tree
153
            raise exc
154
        else:
155
            self.log.error('open id response: UNKNOWN')
156
            exc = HTTPUnauthorized()
157
            exc.explanation = "Unknown OpenID response."
158
            raise exc
159
7675.671.1 by Gary Poster
log out from bzr and openid after logging out from Launchpad.
160
    def _logout(self, environ, start_response):
7675.672.2 by Brad Crittenden
Add documentation per review
161
        """Logout of loggerhead.
162
163
        Clear the cookie and redirect to `next_to`.
164
        """
7675.671.1 by Gary Poster
log out from bzr and openid after logging out from Launchpad.
165
        environ[self.session_var].clear()
166
        query = dict(parse_querystring(environ))
167
        next_url = query.get('next_to')
168
        if next_url is None:
169
            next_url = allvhosts.configs['mainsite'].rooturl
170
        raise HTTPMovedPermanently(next_url)
171
9590.1.135 by Michael Hudson
add files from launchpad-loggerhead tree to launchpad tree
172
    def __call__(self, environ, start_response):
173
        environ['loggerhead.static.url'] = environ['SCRIPT_NAME']
174
        if environ['PATH_INFO'].startswith('/static/'):
175
            path_info_pop(environ)
176
            return static_app(environ, start_response)
177
        elif environ['PATH_INFO'] == '/favicon.ico':
178
            return favicon_app(environ, start_response)
179
        elif environ['PATH_INFO'] == '/robots.txt':
180
            return robots_app(environ, start_response)
181
        elif environ['PATH_INFO'].startswith('/+login'):
182
            return self._complete_login(environ, start_response)
7675.671.1 by Gary Poster
log out from bzr and openid after logging out from Launchpad.
183
        elif environ['PATH_INFO'].startswith('/+logout'):
184
            return self._logout(environ, start_response)
9590.1.135 by Michael Hudson
add files from launchpad-loggerhead tree to launchpad tree
185
        path = environ['PATH_INFO']
186
        trailingSlashCount = len(path) - len(path.rstrip('/'))
187
        user = environ[self.session_var].get('user', LAUNCHPAD_ANONYMOUS)
7675.668.1 by Michael Hudson
only call get_transport(internal_branch_by_id_root) once per thread
188
        lp_server = get_lp_server(user, branch_transport=self.get_transport())
9590.1.136 by Michael Hudson
reapply changes to less exotically constructed tree
189
        lp_server.start_server()
9590.1.135 by Michael Hudson
add files from launchpad-loggerhead tree to launchpad tree
190
        try:
14158.2.1 by j.c.sackett
Checking for private status implemented.
191
9590.1.136 by Michael Hudson
reapply changes to less exotically constructed tree
192
            try:
193
                transport_type, info, trail = self.branchfs.translatePath(
194
                    user, urlutils.escape(path))
195
            except xmlrpclib.Fault, f:
196
                if check_fault(f, faults.PathTranslationError):
197
                    raise HTTPNotFound()
198
                elif check_fault(f, faults.PermissionDenied):
199
                    # If we're not allowed to see the branch...
200
                    if environ['wsgi.url_scheme'] != 'https':
201
                        # ... the request shouldn't have come in over http, as
202
                        # requests for private branches over http should be
203
                        # redirected to https by the dynamic rewrite script we
204
                        # use (which runs before this code is reached), but
205
                        # just in case...
206
                        env_copy = environ.copy()
207
                        env_copy['wsgi.url_scheme'] = 'https'
208
                        raise HTTPMovedPermanently(construct_url(env_copy))
209
                    elif user != LAUNCHPAD_ANONYMOUS:
210
                        # ... if the user is already logged in and still can't
211
                        # see the branch, they lose.
212
                        exc = HTTPUnauthorized()
213
                        exc.explanation = "You are logged in as %s." % user
214
                        raise exc
215
                    else:
216
                        # ... otherwise, lets give them a chance to log in
217
                        # with OpenID.
218
                        return self._begin_login(environ, start_response)
9590.1.135 by Michael Hudson
add files from launchpad-loggerhead tree to launchpad tree
219
                else:
9590.1.136 by Michael Hudson
reapply changes to less exotically constructed tree
220
                    raise
221
            if transport_type != BRANCH_TRANSPORT:
222
                raise HTTPNotFound()
223
            trail = urlutils.unescape(trail).encode('utf-8')
224
            trail += trailingSlashCount * '/'
225
            amount_consumed = len(path) - len(trail)
226
            consumed = path[:amount_consumed]
227
            branch_name = consumed.strip('/')
228
            self.log.info('Using branch: %s', branch_name)
229
            if trail and not trail.startswith('/'):
230
                trail = '/' + trail
231
            environ['PATH_INFO'] = trail
232
            environ['SCRIPT_NAME'] += consumed.rstrip('/')
233
            branch_url = lp_server.get_url() + branch_name
234
            branch_link = urlparse.urljoin(
235
                config.codebrowse.launchpad_root, branch_name)
236
            cachepath = os.path.join(
237
                config.codebrowse.cachepath, branch_name[1:])
238
            if not os.path.isdir(cachepath):
239
                os.makedirs(cachepath)
240
            self.log.info('branch_url: %s', branch_url)
14301.2.1 by j.c.sackett
Fixed creation of api_url to look up branch status.
241
            base_api_url = config.appserver_root_url('api')
242
            branch_api_url = '%s/%s/%s' % (
243
                base_api_url,
244
                'devel',
245
                branch_name,
246
                )
14158.2.3 by j.c.sackett
Updated try/except to base on error codes.
247
            self.log.info('branch_api_url: %s', branch_api_url)
14158.2.1 by j.c.sackett
Checking for private status implemented.
248
            req = urllib2.Request(branch_api_url)
14158.2.3 by j.c.sackett
Updated try/except to base on error codes.
249
            private = False
14158.2.1 by j.c.sackett
Checking for private status implemented.
250
            try:
251
                # We need to determine if the branch is private
252
                response = urllib2.urlopen(req)
14158.2.3 by j.c.sackett
Updated try/except to base on error codes.
253
            except urllib2.HTTPError as response:
14301.2.1 by j.c.sackett
Fixed creation of api_url to look up branch status.
254
                code = response.getcode()
14399.2.1 by j.c.sackett
Added 404 to privacy implying error codes, since we mark many private things as not there rather than as forbidden.
255
                if code in (400, 401, 403, 404):
256
                    # There are several error codes that imply private data.
257
                    # 400 (bad request) is a default error code from the API
258
                    # 401 (unauthorized) should never be returned as the
259
                    # requests are always from anon. If it is returned
260
                    # however, the data is certainly private.
261
                    # 403 (forbidden) is obviously private.
262
                    # 404 (not found) implies privacy from a private team or
263
                    # similar situation, which we hide as not existing rather
264
                    # than mark as forbidden.
14158.2.3 by j.c.sackett
Updated try/except to base on error codes.
265
                    self.log.info("Branch is private")
266
                    private = True
267
                self.log.info(
268
                    "Branch state not determined; api error, return code: %s",
14301.2.1 by j.c.sackett
Fixed creation of api_url to look up branch status.
269
                    code)
270
                response.close()
14158.2.1 by j.c.sackett
Checking for private status implemented.
271
            else:
272
                self.log.info("Branch is public")
14301.2.1 by j.c.sackett
Fixed creation of api_url to look up branch status.
273
                response.close()
14158.2.1 by j.c.sackett
Checking for private status implemented.
274
9590.1.136 by Michael Hudson
reapply changes to less exotically constructed tree
275
            try:
276
                bzr_branch = safe_open(
7675.668.1 by Michael Hudson
only call get_transport(internal_branch_by_id_root) once per thread
277
                    lp_server.get_url().strip(':/'), branch_url)
9590.1.136 by Michael Hudson
reapply changes to less exotically constructed tree
278
            except errors.NotBranchError, err:
279
                self.log.warning('Not a branch: %s', err)
280
                raise HTTPNotFound()
281
            bzr_branch.lock_read()
282
            try:
283
                view = BranchWSGIApp(
284
                    bzr_branch, branch_name, {'cachepath': cachepath},
13811.2.1 by Jeroen Vermeulen
Fix some of the lint people left in the past few days.
285
                    self.graph_cache, branch_link=branch_link,
14158.2.4 by j.c.sackett
Vital step: passing in private.
286
                    served_url=None, private=private)
9590.1.136 by Michael Hudson
reapply changes to less exotically constructed tree
287
                return view.app(environ, start_response)
288
            finally:
289
                bzr_branch.unlock()
9590.1.135 by Michael Hudson
add files from launchpad-loggerhead tree to launchpad tree
290
        finally:
9590.1.136 by Michael Hudson
reapply changes to less exotically constructed tree
291
            lp_server.stop_server()
11225.1.1 by Andrew Bennetts
Add OOPS logging to codebrowse.
292
293
294
def make_error_utility():
295
    """Make an error utility for logging errors from codebrowse."""
296
    error_utility = ErrorReportingUtility()
297
    error_utility.configure('codebrowse')
298
    return error_utility
299
300
13811.2.1 by Jeroen Vermeulen
Fix some of the lint people left in the past few days.
301
# XXX AndrewBennets 2010-07-27: This HTML template should be replaced
302
# with the same one that lpnet uses for reporting OOPSes to users, or at
303
# least something that looks similar.  But even this is better than the
304
# "Internal Server Error" you'd get otherwise.
11225.1.1 by Andrew Bennetts
Add OOPS logging to codebrowse.
305
_oops_html_template = '''\
306
<html>
13757.1.1 by William Grant
Fix launchpad_loggerhead OOPS page template to use 'id' instead of 'oopsid' as a dict key.
307
<head><title>Oops! %(id)s</title></head>
11225.1.1 by Andrew Bennetts
Add OOPS logging to codebrowse.
308
<body>
309
<h1>Oops!</h1>
310
<p>Something broke while generating the page.
311
Please try again in a few minutes, and if the problem persists file a bug at
12094.1.1 by Ian Booth
Removed old launchpad project references in bug urls
312
<a href="https://bugs.launchpad.net/launchpad"
313
>https://bugs.launchpad.net/launchpad</a>
13757.1.1 by William Grant
Fix launchpad_loggerhead OOPS page template to use 'id' instead of 'oopsid' as a dict key.
314
and quote OOPS-ID <strong>%(id)s</strong>
11225.1.1 by Andrew Bennetts
Add OOPS logging to codebrowse.
315
</p></body></html>'''
316
317
318
def oops_middleware(app):
319
    """Middleware to log an OOPS if the request fails.
320
13811.2.1 by Jeroen Vermeulen
Fix some of the lint people left in the past few days.
321
    If the request fails before the response body has started then this
322
    returns a basic HTML error page with the OOPS ID to the user (and status
323
    code 500).
11225.1.1 by Andrew Bennetts
Add OOPS logging to codebrowse.
324
    """
325
    error_utility = make_error_utility()
13686.2.20 by Robert Collins
Migrate the launchpad_loggerhead implementation of oops_middleware to that within oops_wsgi.
326
    return oops_wsgi.make_app(app, error_utility._oops_config,
14452.4.2 by Robert Collins
Generate OOPS from bazaar.launchpad.net on slow responses (7 seconds or more).
327
            template=_oops_html_template, soft_start_timeout=7000)