|
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() |