##// END OF EJS Templates
pyramid: ported pyramid routing for events
marcink -
r2016:062a6c2d stable
parent child Browse files
Show More
@@ -1,102 +1,112 b''
1 1 # Copyright (C) 2016-2017 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18 import logging
19 import datetime
19 20
20 from datetime import datetime
21 from zope.cachedescriptors.property import Lazy as LazyProperty
21 22 from pyramid.threadlocal import get_current_request
23
22 24 from rhodecode.lib.utils2 import AttributeDict
23 25
24 26
25 27 # this is a user object to be used for events caused by the system (eg. shell)
26 28 SYSTEM_USER = AttributeDict(dict(
27 29 username='__SYSTEM__',
28 30 user_id='__SYSTEM_ID__'
29 31 ))
30 32
31 33 log = logging.getLogger(__name__)
32 34
33 35
34 36 class RhodecodeEvent(object):
35 37 """
36 38 Base event class for all RhodeCode events
37 39 """
38 40 name = "RhodeCodeEvent"
41 no_url_set = '<no server_url available>'
39 42
40 43 def __init__(self, request=None):
41 self.request = request or get_current_request()
42 self.utc_timestamp = datetime.utcnow()
44 self._request = request
45 self.utc_timestamp = datetime.datetime.utcnow()
46
47 def get_request(self):
48 if self._request:
49 return self._request
50 return get_current_request()
51
52 @LazyProperty
53 def request(self):
54 return self.get_request()
43 55
44 56 @property
45 57 def auth_user(self):
46 58 if not self.request:
47 59 return
48 60
49 61 user = getattr(self.request, 'user', None)
50 62 if user:
51 63 return user
52 64
53 65 api_user = getattr(self.request, 'rpc_user', None)
54 66 if api_user:
55 67 return api_user
56 68
57 69 @property
58 70 def actor(self):
59 71 auth_user = self.auth_user
60
61 72 if auth_user:
62 73 instance = auth_user.get_instance()
63 74 if not instance:
64 75 return AttributeDict(dict(
65 76 username=auth_user.username,
66 77 user_id=auth_user.user_id,
67 78 ))
68 79 return instance
69 80
70 81 return SYSTEM_USER
71 82
72 83 @property
73 84 def actor_ip(self):
74 85 auth_user = self.auth_user
75 86 if auth_user:
76 87 return auth_user.ip_addr
77 88 return '<no ip available>'
78 89
79 90 @property
80 91 def server_url(self):
81 default = '<no server_url available>'
82 92 if self.request:
83 93 try:
84 94 return self.request.route_url('home')
85 95 except Exception:
86 96 log.exception('Failed to fetch URL for server')
87 return default
97 return self.no_url_set
88 98
89 return default
99 return self.no_url_set
90 100
91 101 def as_dict(self):
92 102 data = {
93 103 'name': self.name,
94 104 'utc_timestamp': self.utc_timestamp,
95 105 'actor_ip': self.actor_ip,
96 106 'actor': {
97 107 'username': self.actor.username,
98 108 'user_id': self.actor.user_id
99 109 },
100 110 'server_url': self.server_url
101 111 }
102 112 return data
@@ -1,142 +1,144 b''
1 1 # Copyright (C) 2016-2017 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import logging
20 20
21 21 from rhodecode.translation import lazy_ugettext
22 22 from rhodecode.events.repo import (
23 23 RepoEvent, _commits_as_dict, _issues_as_dict)
24 24
25 25 log = logging.getLogger(__name__)
26 26
27 27
28 28 class PullRequestEvent(RepoEvent):
29 29 """
30 30 Base class for pull request events.
31 31
32 32 :param pullrequest: a :class:`PullRequest` instance
33 33 """
34 34
35 35 def __init__(self, pullrequest):
36 36 super(PullRequestEvent, self).__init__(pullrequest.target_repo)
37 37 self.pullrequest = pullrequest
38 38
39 39 def as_dict(self):
40 40 from rhodecode.model.pull_request import PullRequestModel
41 41 data = super(PullRequestEvent, self).as_dict()
42 42
43 43 commits = _commits_as_dict(
44 44 self,
45 45 commit_ids=self.pullrequest.revisions,
46 46 repos=[self.pullrequest.source_repo]
47 47 )
48 48 issues = _issues_as_dict(commits)
49 49
50 50 data.update({
51 51 'pullrequest': {
52 52 'title': self.pullrequest.title,
53 53 'issues': issues,
54 54 'pull_request_id': self.pullrequest.pull_request_id,
55 'url': PullRequestModel().get_url(self.pullrequest),
55 'url': PullRequestModel().get_url(
56 self.pullrequest, request=self.request),
56 57 'permalink_url': PullRequestModel().get_url(
57 self.pullrequest, permalink=True),
58 self.pullrequest, request=self.request, permalink=True),
58 59 'status': self.pullrequest.calculated_review_status(),
59 60 'commits': commits,
60 61 }
61 62 })
62 63 return data
63 64
64 65
65 66 class PullRequestCreateEvent(PullRequestEvent):
66 67 """
67 68 An instance of this class is emitted as an :term:`event` after a pull
68 69 request is created.
69 70 """
70 71 name = 'pullrequest-create'
71 72 display_name = lazy_ugettext('pullrequest created')
72 73
73 74
74 75 class PullRequestCloseEvent(PullRequestEvent):
75 76 """
76 77 An instance of this class is emitted as an :term:`event` after a pull
77 78 request is closed.
78 79 """
79 80 name = 'pullrequest-close'
80 81 display_name = lazy_ugettext('pullrequest closed')
81 82
82 83
83 84 class PullRequestUpdateEvent(PullRequestEvent):
84 85 """
85 86 An instance of this class is emitted as an :term:`event` after a pull
86 87 request's commits have been updated.
87 88 """
88 89 name = 'pullrequest-update'
89 90 display_name = lazy_ugettext('pullrequest commits updated')
90 91
91 92
92 93 class PullRequestReviewEvent(PullRequestEvent):
93 94 """
94 95 An instance of this class is emitted as an :term:`event` after a pull
95 96 request review has changed.
96 97 """
97 98 name = 'pullrequest-review'
98 99 display_name = lazy_ugettext('pullrequest review changed')
99 100
100 101
101 102 class PullRequestMergeEvent(PullRequestEvent):
102 103 """
103 104 An instance of this class is emitted as an :term:`event` after a pull
104 105 request is merged.
105 106 """
106 107 name = 'pullrequest-merge'
107 108 display_name = lazy_ugettext('pullrequest merged')
108 109
109 110
110 111 class PullRequestCommentEvent(PullRequestEvent):
111 112 """
112 113 An instance of this class is emitted as an :term:`event` after a pull
113 114 request comment is created.
114 115 """
115 116 name = 'pullrequest-comment'
116 117 display_name = lazy_ugettext('pullrequest commented')
117 118
118 119 def __init__(self, pullrequest, comment):
119 120 super(PullRequestCommentEvent, self).__init__(pullrequest)
120 121 self.comment = comment
121 122
122 123 def as_dict(self):
123 124 from rhodecode.model.comment import CommentsModel
124 125 data = super(PullRequestCommentEvent, self).as_dict()
125 126
126 127 status = None
127 128 if self.comment.status_change:
128 129 status = self.comment.status_change[0].status
129 130
130 131 data.update({
131 132 'comment': {
132 133 'status': status,
133 134 'text': self.comment.text,
134 135 'type': self.comment.comment_type,
135 136 'file': self.comment.f_path,
136 137 'line': self.comment.line_no,
137 'url': CommentsModel().get_url(self.comment),
138 'url': CommentsModel().get_url(
139 self.comment, request=self.request),
138 140 'permalink_url': CommentsModel().get_url(
139 self.comment, permalink=True),
141 self.comment, request=self.request, permalink=True),
140 142 }
141 143 })
142 144 return data
@@ -1,277 +1,281 b''
1 1 # Copyright (C) 2016-2017 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import collections
20 20 import logging
21 21
22 22 from rhodecode.translation import lazy_ugettext
23 23 from rhodecode.model.db import User, Repository, Session
24 24 from rhodecode.events.base import RhodecodeEvent
25 25 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
26 26
27 27 log = logging.getLogger(__name__)
28 28
29 29
30 30 def _commits_as_dict(event, commit_ids, repos):
31 31 """
32 32 Helper function to serialize commit_ids
33 33
34 34 :param event: class calling this method
35 35 :param commit_ids: commits to get
36 36 :param repos: list of repos to check
37 37 """
38 38 from rhodecode.lib.utils2 import extract_mentioned_users
39 39 from rhodecode.lib.helpers import (
40 40 urlify_commit_message, process_patterns, chop_at_smart)
41 41 from rhodecode.model.repo import RepoModel
42 42
43 43 if not repos:
44 44 raise Exception('no repo defined')
45 45
46 46 if not isinstance(repos, (tuple, list)):
47 47 repos = [repos]
48 48
49 49 if not commit_ids:
50 50 return []
51 51
52 52 needed_commits = list(commit_ids)
53 53
54 54 commits = []
55 55 reviewers = []
56 56 for repo in repos:
57 57 if not needed_commits:
58 58 return commits # return early if we have the commits we need
59 59
60 60 vcs_repo = repo.scm_instance(cache=False)
61 61 try:
62 62 # use copy of needed_commits since we modify it while iterating
63 63 for commit_id in list(needed_commits):
64 64 try:
65 65 cs = vcs_repo.get_changeset(commit_id)
66 66 except CommitDoesNotExistError:
67 67 continue # maybe its in next repo
68 68
69 69 cs_data = cs.__json__()
70 70 cs_data['mentions'] = extract_mentioned_users(cs_data['message'])
71 71 cs_data['reviewers'] = reviewers
72 72 cs_data['url'] = RepoModel().get_commit_url(
73 73 repo, cs_data['raw_id'], request=event.request)
74 74 cs_data['permalink_url'] = RepoModel().get_commit_url(
75 75 repo, cs_data['raw_id'], request=event.request, permalink=True)
76 76 urlified_message, issues_data = process_patterns(
77 77 cs_data['message'], repo.repo_name)
78 78 cs_data['issues'] = issues_data
79 79 cs_data['message_html'] = urlify_commit_message(
80 80 cs_data['message'], repo.repo_name)
81 81 cs_data['message_html_title'] = chop_at_smart(
82 82 cs_data['message'], '\n', suffix_if_chopped='...')
83 83 commits.append(cs_data)
84 84
85 85 needed_commits.remove(commit_id)
86 86
87 87 except Exception as e:
88 88 log.exception(e)
89 89 # we don't send any commits when crash happens, only full list
90 90 # matters we short circuit then.
91 91 return []
92 92
93 93 missing_commits = set(commit_ids) - set(c['raw_id'] for c in commits)
94 94 if missing_commits:
95 95 log.error('missing commits: %s' % ', '.join(missing_commits))
96 96
97 97 return commits
98 98
99 99
100 100 def _issues_as_dict(commits):
101 101 """ Helper function to serialize issues from commits """
102 102 issues = {}
103 103 for commit in commits:
104 104 for issue in commit['issues']:
105 105 issues[issue['id']] = issue
106 106 return issues
107 107
108 108
109 109 class RepoEvent(RhodecodeEvent):
110 110 """
111 111 Base class for events acting on a repository.
112 112
113 113 :param repo: a :class:`Repository` instance
114 114 """
115 115
116 116 def __init__(self, repo):
117 117 super(RepoEvent, self).__init__()
118 118 self.repo = repo
119 119
120 120 def as_dict(self):
121 121 from rhodecode.model.repo import RepoModel
122 122 data = super(RepoEvent, self).as_dict()
123 123 extra_fields = collections.OrderedDict()
124 124 for field in self.repo.extra_fields:
125 125 extra_fields[field.field_key] = field.field_value
126 126
127 127 data.update({
128 128 'repo': {
129 129 'repo_id': self.repo.repo_id,
130 130 'repo_name': self.repo.repo_name,
131 131 'repo_type': self.repo.repo_type,
132 132 'url': RepoModel().get_url(
133 133 self.repo, request=self.request),
134 134 'permalink_url': RepoModel().get_url(
135 135 self.repo, request=self.request, permalink=True),
136 136 'extra_fields': extra_fields
137 137 }
138 138 })
139 139 return data
140 140
141 141
142 142 class RepoPreCreateEvent(RepoEvent):
143 143 """
144 144 An instance of this class is emitted as an :term:`event` before a repo is
145 145 created.
146 146 """
147 147 name = 'repo-pre-create'
148 148 display_name = lazy_ugettext('repository pre create')
149 149
150 150
151 151 class RepoCreateEvent(RepoEvent):
152 152 """
153 153 An instance of this class is emitted as an :term:`event` whenever a repo is
154 154 created.
155 155 """
156 156 name = 'repo-create'
157 157 display_name = lazy_ugettext('repository created')
158 158
159 159
160 160 class RepoPreDeleteEvent(RepoEvent):
161 161 """
162 162 An instance of this class is emitted as an :term:`event` whenever a repo is
163 163 created.
164 164 """
165 165 name = 'repo-pre-delete'
166 166 display_name = lazy_ugettext('repository pre delete')
167 167
168 168
169 169 class RepoDeleteEvent(RepoEvent):
170 170 """
171 171 An instance of this class is emitted as an :term:`event` whenever a repo is
172 172 created.
173 173 """
174 174 name = 'repo-delete'
175 175 display_name = lazy_ugettext('repository deleted')
176 176
177 177
178 178 class RepoVCSEvent(RepoEvent):
179 179 """
180 180 Base class for events triggered by the VCS
181 181 """
182 182 def __init__(self, repo_name, extras):
183 183 self.repo = Repository.get_by_repo_name(repo_name)
184 184 if not self.repo:
185 185 raise Exception('repo by this name %s does not exist' % repo_name)
186 186 self.extras = extras
187 187 super(RepoVCSEvent, self).__init__(self.repo)
188 188
189 189 @property
190 190 def actor(self):
191 191 if self.extras.get('username'):
192 192 return User.get_by_username(self.extras['username'])
193 193
194 194 @property
195 195 def actor_ip(self):
196 196 if self.extras.get('ip'):
197 197 return self.extras['ip']
198 198
199 199 @property
200 200 def server_url(self):
201 201 if self.extras.get('server_url'):
202 202 return self.extras['server_url']
203 203
204 @property
205 def request(self):
206 return self.extras.get('request') or self.get_request()
207
204 208
205 209 class RepoPrePullEvent(RepoVCSEvent):
206 210 """
207 211 An instance of this class is emitted as an :term:`event` before commits
208 212 are pulled from a repo.
209 213 """
210 214 name = 'repo-pre-pull'
211 215 display_name = lazy_ugettext('repository pre pull')
212 216
213 217
214 218 class RepoPullEvent(RepoVCSEvent):
215 219 """
216 220 An instance of this class is emitted as an :term:`event` after commits
217 221 are pulled from a repo.
218 222 """
219 223 name = 'repo-pull'
220 224 display_name = lazy_ugettext('repository pull')
221 225
222 226
223 227 class RepoPrePushEvent(RepoVCSEvent):
224 228 """
225 229 An instance of this class is emitted as an :term:`event` before commits
226 230 are pushed to a repo.
227 231 """
228 232 name = 'repo-pre-push'
229 233 display_name = lazy_ugettext('repository pre push')
230 234
231 235
232 236 class RepoPushEvent(RepoVCSEvent):
233 237 """
234 238 An instance of this class is emitted as an :term:`event` after commits
235 239 are pushed to a repo.
236 240
237 241 :param extras: (optional) dict of data from proxied VCS actions
238 242 """
239 243 name = 'repo-push'
240 244 display_name = lazy_ugettext('repository push')
241 245
242 246 def __init__(self, repo_name, pushed_commit_ids, extras):
243 247 super(RepoPushEvent, self).__init__(repo_name, extras)
244 248 self.pushed_commit_ids = pushed_commit_ids
245 249
246 250 def as_dict(self):
247 251 data = super(RepoPushEvent, self).as_dict()
248 252
249 253 def branch_url(branch_name):
250 254 return '{}/changelog?branch={}'.format(
251 255 data['repo']['url'], branch_name)
252 256
253 257 commits = _commits_as_dict(
254 258 self, commit_ids=self.pushed_commit_ids, repos=[self.repo])
255 259
256 260 last_branch = None
257 261 for commit in reversed(commits):
258 262 commit['branch'] = commit['branch'] or last_branch
259 263 last_branch = commit['branch']
260 264 issues = _issues_as_dict(commits)
261 265
262 266 branches = set(
263 267 commit['branch'] for commit in commits if commit['branch'])
264 268 branches = [
265 269 {
266 270 'name': branch,
267 271 'url': branch_url(branch)
268 272 }
269 273 for branch in branches
270 274 ]
271 275
272 276 data['push'] = {
273 277 'commits': commits,
274 278 'issues': issues,
275 279 'branches': branches,
276 280 }
277 281 return data
@@ -1,601 +1,631 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 The base Controller API
23 23 Provides the BaseController class for subclassing. And usage in different
24 24 controllers
25 25 """
26 26
27 27 import logging
28 28 import socket
29 29
30 30 import ipaddress
31 31 import pyramid.threadlocal
32 32
33 33 from paste.auth.basic import AuthBasicAuthenticator
34 34 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
35 35 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
36 36 from pylons import config, tmpl_context as c, request, session, url
37 37 from pylons.controllers import WSGIController
38 38 from pylons.controllers.util import redirect
39 39 from pylons.i18n import translation
40 40 # marcink: don't remove this import
41 41 from pylons.templating import render_mako as render # noqa
42 42 from pylons.i18n.translation import _
43 43 from webob.exc import HTTPFound
44 44
45 45
46 46 import rhodecode
47 47 from rhodecode.authentication.base import VCS_TYPE
48 48 from rhodecode.lib import auth, utils2
49 49 from rhodecode.lib import helpers as h
50 50 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
51 51 from rhodecode.lib.exceptions import UserCreationError
52 52 from rhodecode.lib.utils import (
53 53 get_repo_slug, set_rhodecode_config, password_changed,
54 54 get_enabled_hook_classes)
55 55 from rhodecode.lib.utils2 import (
56 56 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist)
57 57 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
58 58 from rhodecode.model import meta
59 59 from rhodecode.model.db import Repository, User, ChangesetComment
60 60 from rhodecode.model.notification import NotificationModel
61 61 from rhodecode.model.scm import ScmModel
62 62 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
63 63
64 64
65 65 log = logging.getLogger(__name__)
66 66
67 67
68 68 def _filter_proxy(ip):
69 69 """
70 70 Passed in IP addresses in HEADERS can be in a special format of multiple
71 71 ips. Those comma separated IPs are passed from various proxies in the
72 72 chain of request processing. The left-most being the original client.
73 73 We only care about the first IP which came from the org. client.
74 74
75 75 :param ip: ip string from headers
76 76 """
77 77 if ',' in ip:
78 78 _ips = ip.split(',')
79 79 _first_ip = _ips[0].strip()
80 80 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
81 81 return _first_ip
82 82 return ip
83 83
84 84
85 85 def _filter_port(ip):
86 86 """
87 87 Removes a port from ip, there are 4 main cases to handle here.
88 88 - ipv4 eg. 127.0.0.1
89 89 - ipv6 eg. ::1
90 90 - ipv4+port eg. 127.0.0.1:8080
91 91 - ipv6+port eg. [::1]:8080
92 92
93 93 :param ip:
94 94 """
95 95 def is_ipv6(ip_addr):
96 96 if hasattr(socket, 'inet_pton'):
97 97 try:
98 98 socket.inet_pton(socket.AF_INET6, ip_addr)
99 99 except socket.error:
100 100 return False
101 101 else:
102 102 # fallback to ipaddress
103 103 try:
104 104 ipaddress.IPv6Address(ip_addr)
105 105 except Exception:
106 106 return False
107 107 return True
108 108
109 109 if ':' not in ip: # must be ipv4 pure ip
110 110 return ip
111 111
112 112 if '[' in ip and ']' in ip: # ipv6 with port
113 113 return ip.split(']')[0][1:].lower()
114 114
115 115 # must be ipv6 or ipv4 with port
116 116 if is_ipv6(ip):
117 117 return ip
118 118 else:
119 119 ip, _port = ip.split(':')[:2] # means ipv4+port
120 120 return ip
121 121
122 122
123 123 def get_ip_addr(environ):
124 124 proxy_key = 'HTTP_X_REAL_IP'
125 125 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
126 126 def_key = 'REMOTE_ADDR'
127 127 _filters = lambda x: _filter_port(_filter_proxy(x))
128 128
129 129 ip = environ.get(proxy_key)
130 130 if ip:
131 131 return _filters(ip)
132 132
133 133 ip = environ.get(proxy_key2)
134 134 if ip:
135 135 return _filters(ip)
136 136
137 137 ip = environ.get(def_key, '0.0.0.0')
138 138 return _filters(ip)
139 139
140 140
141 141 def get_server_ip_addr(environ, log_errors=True):
142 142 hostname = environ.get('SERVER_NAME')
143 143 try:
144 144 return socket.gethostbyname(hostname)
145 145 except Exception as e:
146 146 if log_errors:
147 147 # in some cases this lookup is not possible, and we don't want to
148 148 # make it an exception in logs
149 149 log.exception('Could not retrieve server ip address: %s', e)
150 150 return hostname
151 151
152 152
153 153 def get_server_port(environ):
154 154 return environ.get('SERVER_PORT')
155 155
156 156
157 157 def get_access_path(environ):
158 158 path = environ.get('PATH_INFO')
159 159 org_req = environ.get('pylons.original_request')
160 160 if org_req:
161 161 path = org_req.environ.get('PATH_INFO')
162 162 return path
163 163
164 164
165 165 def get_user_agent(environ):
166 166 return environ.get('HTTP_USER_AGENT')
167 167
168 168
169 169 def vcs_operation_context(
170 170 environ, repo_name, username, action, scm, check_locking=True,
171 171 is_shadow_repo=False):
172 172 """
173 173 Generate the context for a vcs operation, e.g. push or pull.
174 174
175 175 This context is passed over the layers so that hooks triggered by the
176 176 vcs operation know details like the user, the user's IP address etc.
177 177
178 178 :param check_locking: Allows to switch of the computation of the locking
179 179 data. This serves mainly the need of the simplevcs middleware to be
180 180 able to disable this for certain operations.
181 181
182 182 """
183 183 # Tri-state value: False: unlock, None: nothing, True: lock
184 184 make_lock = None
185 185 locked_by = [None, None, None]
186 186 is_anonymous = username == User.DEFAULT_USER
187 187 if not is_anonymous and check_locking:
188 188 log.debug('Checking locking on repository "%s"', repo_name)
189 189 user = User.get_by_username(username)
190 190 repo = Repository.get_by_repo_name(repo_name)
191 191 make_lock, __, locked_by = repo.get_locking_state(
192 192 action, user.user_id)
193 193
194 194 settings_model = VcsSettingsModel(repo=repo_name)
195 195 ui_settings = settings_model.get_ui_settings()
196 196
197 197 extras = {
198 198 'ip': get_ip_addr(environ),
199 199 'username': username,
200 200 'action': action,
201 201 'repository': repo_name,
202 202 'scm': scm,
203 203 'config': rhodecode.CONFIG['__file__'],
204 204 'make_lock': make_lock,
205 205 'locked_by': locked_by,
206 206 'server_url': utils2.get_server_url(environ),
207 207 'user_agent': get_user_agent(environ),
208 208 'hooks': get_enabled_hook_classes(ui_settings),
209 209 'is_shadow_repo': is_shadow_repo,
210 210 }
211 211 return extras
212 212
213 213
214 214 class BasicAuth(AuthBasicAuthenticator):
215 215
216 216 def __init__(self, realm, authfunc, registry, auth_http_code=None,
217 217 initial_call_detection=False, acl_repo_name=None):
218 218 self.realm = realm
219 219 self.initial_call = initial_call_detection
220 220 self.authfunc = authfunc
221 221 self.registry = registry
222 222 self.acl_repo_name = acl_repo_name
223 223 self._rc_auth_http_code = auth_http_code
224 224
225 225 def _get_response_from_code(self, http_code):
226 226 try:
227 227 return get_exception(safe_int(http_code))
228 228 except Exception:
229 229 log.exception('Failed to fetch response for code %s' % http_code)
230 230 return HTTPForbidden
231 231
232 232 def build_authentication(self):
233 233 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
234 234 if self._rc_auth_http_code and not self.initial_call:
235 235 # return alternative HTTP code if alternative http return code
236 236 # is specified in RhodeCode config, but ONLY if it's not the
237 237 # FIRST call
238 238 custom_response_klass = self._get_response_from_code(
239 239 self._rc_auth_http_code)
240 240 return custom_response_klass(headers=head)
241 241 return HTTPUnauthorized(headers=head)
242 242
243 243 def authenticate(self, environ):
244 244 authorization = AUTHORIZATION(environ)
245 245 if not authorization:
246 246 return self.build_authentication()
247 247 (authmeth, auth) = authorization.split(' ', 1)
248 248 if 'basic' != authmeth.lower():
249 249 return self.build_authentication()
250 250 auth = auth.strip().decode('base64')
251 251 _parts = auth.split(':', 1)
252 252 if len(_parts) == 2:
253 253 username, password = _parts
254 254 if self.authfunc(
255 255 username, password, environ, VCS_TYPE,
256 256 registry=self.registry, acl_repo_name=self.acl_repo_name):
257 257 return username
258 258 if username and password:
259 259 # we mark that we actually executed authentication once, at
260 260 # that point we can use the alternative auth code
261 261 self.initial_call = False
262 262
263 263 return self.build_authentication()
264 264
265 265 __call__ = authenticate
266 266
267 267
268 268 def attach_context_attributes(context, request, user_id, attach_to_request=False):
269 269 """
270 270 Attach variables into template context called `c`, please note that
271 271 request could be pylons or pyramid request in here.
272 272 """
273 273 rc_config = SettingsModel().get_all_settings(cache=True)
274 274
275 275 context.rhodecode_version = rhodecode.__version__
276 276 context.rhodecode_edition = config.get('rhodecode.edition')
277 277 # unique secret + version does not leak the version but keep consistency
278 278 context.rhodecode_version_hash = md5(
279 279 config.get('beaker.session.secret', '') +
280 280 rhodecode.__version__)[:8]
281 281
282 282 # Default language set for the incoming request
283 283 context.language = translation.get_lang()[0]
284 284
285 285 # Visual options
286 286 context.visual = AttributeDict({})
287 287
288 288 # DB stored Visual Items
289 289 context.visual.show_public_icon = str2bool(
290 290 rc_config.get('rhodecode_show_public_icon'))
291 291 context.visual.show_private_icon = str2bool(
292 292 rc_config.get('rhodecode_show_private_icon'))
293 293 context.visual.stylify_metatags = str2bool(
294 294 rc_config.get('rhodecode_stylify_metatags'))
295 295 context.visual.dashboard_items = safe_int(
296 296 rc_config.get('rhodecode_dashboard_items', 100))
297 297 context.visual.admin_grid_items = safe_int(
298 298 rc_config.get('rhodecode_admin_grid_items', 100))
299 299 context.visual.repository_fields = str2bool(
300 300 rc_config.get('rhodecode_repository_fields'))
301 301 context.visual.show_version = str2bool(
302 302 rc_config.get('rhodecode_show_version'))
303 303 context.visual.use_gravatar = str2bool(
304 304 rc_config.get('rhodecode_use_gravatar'))
305 305 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
306 306 context.visual.default_renderer = rc_config.get(
307 307 'rhodecode_markup_renderer', 'rst')
308 308 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
309 309 context.visual.rhodecode_support_url = \
310 310 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
311 311
312 312 context.pre_code = rc_config.get('rhodecode_pre_code')
313 313 context.post_code = rc_config.get('rhodecode_post_code')
314 314 context.rhodecode_name = rc_config.get('rhodecode_title')
315 315 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
316 316 # if we have specified default_encoding in the request, it has more
317 317 # priority
318 318 if request.GET.get('default_encoding'):
319 319 context.default_encodings.insert(0, request.GET.get('default_encoding'))
320 320 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
321 321
322 322 # INI stored
323 323 context.labs_active = str2bool(
324 324 config.get('labs_settings_active', 'false'))
325 325 context.visual.allow_repo_location_change = str2bool(
326 326 config.get('allow_repo_location_change', True))
327 327 context.visual.allow_custom_hooks_settings = str2bool(
328 328 config.get('allow_custom_hooks_settings', True))
329 329 context.debug_style = str2bool(config.get('debug_style', False))
330 330
331 331 context.rhodecode_instanceid = config.get('instance_id')
332 332
333 333 context.visual.cut_off_limit_diff = safe_int(
334 334 config.get('cut_off_limit_diff'))
335 335 context.visual.cut_off_limit_file = safe_int(
336 336 config.get('cut_off_limit_file'))
337 337
338 338 # AppEnlight
339 339 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
340 340 context.appenlight_api_public_key = config.get(
341 341 'appenlight.api_public_key', '')
342 342 context.appenlight_server_url = config.get('appenlight.server_url', '')
343 343
344 344 # JS template context
345 345 context.template_context = {
346 346 'repo_name': None,
347 347 'repo_type': None,
348 348 'repo_landing_commit': None,
349 349 'rhodecode_user': {
350 350 'username': None,
351 351 'email': None,
352 352 'notification_status': False
353 353 },
354 354 'visual': {
355 355 'default_renderer': None
356 356 },
357 357 'commit_data': {
358 358 'commit_id': None
359 359 },
360 360 'pull_request_data': {'pull_request_id': None},
361 361 'timeago': {
362 362 'refresh_time': 120 * 1000,
363 363 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
364 364 },
365 365 'pylons_dispatch': {
366 366 # 'controller': request.environ['pylons.routes_dict']['controller'],
367 367 # 'action': request.environ['pylons.routes_dict']['action'],
368 368 },
369 369 'pyramid_dispatch': {
370 370
371 371 },
372 372 'extra': {'plugins': {}}
373 373 }
374 374 # END CONFIG VARS
375 375
376 376 # TODO: This dosn't work when called from pylons compatibility tween.
377 377 # Fix this and remove it from base controller.
378 378 # context.repo_name = get_repo_slug(request) # can be empty
379 379
380 380 diffmode = 'sideside'
381 381 if request.GET.get('diffmode'):
382 382 if request.GET['diffmode'] == 'unified':
383 383 diffmode = 'unified'
384 384 elif request.session.get('diffmode'):
385 385 diffmode = request.session['diffmode']
386 386
387 387 context.diffmode = diffmode
388 388
389 389 if request.session.get('diffmode') != diffmode:
390 390 request.session['diffmode'] = diffmode
391 391
392 392 context.csrf_token = auth.get_csrf_token()
393 393 context.backends = rhodecode.BACKENDS.keys()
394 394 context.backends.sort()
395 395 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(user_id)
396 396 if attach_to_request:
397 397 request.call_context = context
398 398 else:
399 399 context.pyramid_request = pyramid.threadlocal.get_current_request()
400 400
401 401
402 402
403 403 def get_auth_user(environ):
404 404 ip_addr = get_ip_addr(environ)
405 405 # make sure that we update permissions each time we call controller
406 406 _auth_token = (request.GET.get('auth_token', '') or
407 407 request.GET.get('api_key', ''))
408 408
409 409 if _auth_token:
410 410 # when using API_KEY we assume user exists, and
411 411 # doesn't need auth based on cookies.
412 412 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
413 413 authenticated = False
414 414 else:
415 415 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
416 416 try:
417 417 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
418 418 ip_addr=ip_addr)
419 419 except UserCreationError as e:
420 420 h.flash(e, 'error')
421 421 # container auth or other auth functions that create users
422 422 # on the fly can throw this exception signaling that there's
423 423 # issue with user creation, explanation should be provided
424 424 # in Exception itself. We then create a simple blank
425 425 # AuthUser
426 426 auth_user = AuthUser(ip_addr=ip_addr)
427 427
428 428 if password_changed(auth_user, session):
429 429 session.invalidate()
430 430 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
431 431 auth_user = AuthUser(ip_addr=ip_addr)
432 432
433 433 authenticated = cookie_store.get('is_authenticated')
434 434
435 435 if not auth_user.is_authenticated and auth_user.is_user_object:
436 436 # user is not authenticated and not empty
437 437 auth_user.set_authenticated(authenticated)
438 438
439 439 return auth_user
440 440
441 441
442 442 class BaseController(WSGIController):
443 443
444 444 def __before__(self):
445 445 """
446 446 __before__ is called before controller methods and after __call__
447 447 """
448 448 # on each call propagate settings calls into global settings.
449 449 set_rhodecode_config(config)
450 450 attach_context_attributes(c, request, c.rhodecode_user.user_id)
451 451
452 452 # TODO: Remove this when fixed in attach_context_attributes()
453 453 c.repo_name = get_repo_slug(request) # can be empty
454 454
455 455 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
456 456 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
457 457 self.sa = meta.Session
458 458 self.scm_model = ScmModel(self.sa)
459 459
460 460 # set user language
461 461 user_lang = getattr(c.pyramid_request, '_LOCALE_', None)
462 462 if user_lang:
463 463 translation.set_lang(user_lang)
464 464 log.debug('set language to %s for user %s',
465 465 user_lang, self._rhodecode_user)
466 466
467 467 def _dispatch_redirect(self, with_url, environ, start_response):
468 468 resp = HTTPFound(with_url)
469 469 environ['SCRIPT_NAME'] = '' # handle prefix middleware
470 470 environ['PATH_INFO'] = with_url
471 471 return resp(environ, start_response)
472 472
473 473 def __call__(self, environ, start_response):
474 474 """Invoke the Controller"""
475 475 # WSGIController.__call__ dispatches to the Controller method
476 476 # the request is routed to. This routing information is
477 477 # available in environ['pylons.routes_dict']
478 478 from rhodecode.lib import helpers as h
479 479
480 480 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
481 481 if environ.get('debugtoolbar.wants_pylons_context', False):
482 482 environ['debugtoolbar.pylons_context'] = c._current_obj()
483 483
484 484 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
485 485 environ['pylons.routes_dict']['action']])
486 486
487 487 self.rc_config = SettingsModel().get_all_settings(cache=True)
488 488 self.ip_addr = get_ip_addr(environ)
489 489
490 490 # The rhodecode auth user is looked up and passed through the
491 491 # environ by the pylons compatibility tween in pyramid.
492 492 # So we can just grab it from there.
493 493 auth_user = environ['rc_auth_user']
494 494
495 495 # set globals for auth user
496 496 request.user = auth_user
497 497 c.rhodecode_user = self._rhodecode_user = auth_user
498 498
499 499 log.info('IP: %s User: %s accessed %s [%s]' % (
500 500 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
501 501 _route_name)
502 502 )
503 503
504 504 user_obj = auth_user.get_instance()
505 505 if user_obj and user_obj.user_data.get('force_password_change'):
506 506 h.flash('You are required to change your password', 'warning',
507 507 ignore_duplicate=True)
508 508 return self._dispatch_redirect(
509 509 url('my_account_password'), environ, start_response)
510 510
511 511 return WSGIController.__call__(self, environ, start_response)
512 512
513 513
514 def add_events_routes(config):
515 """
516 Adds routing that can be used in events. Because some events are triggered
517 outside of pyramid context, we need to bootstrap request with some
518 routing registered
519 """
520 config.add_route(name='home', pattern='/')
521
522 config.add_route(name='repo_summary', pattern='/{repo_name}')
523 config.add_route(name='repo_summary_explicit', pattern='/{repo_name}/summary')
524 config.add_route(name='repo_group_home', pattern='/{repo_group_name}')
525
526 config.add_route(name='pullrequest_show',
527 pattern='/{repo_name}/pull-request/{pull_request_id}')
528 config.add_route(name='pull_requests_global',
529 pattern='/pull-request/{pull_request_id}')
530
531 config.add_route(name='repo_commit',
532 pattern='/{repo_name}/changeset/{commit_id}')
533 config.add_route(name='repo_files',
534 pattern='/{repo_name}/files/{commit_id}/{f_path}')
535
536
537 def bootstrap_request():
538 import pyramid.testing
539 request = pyramid.testing.DummyRequest()
540 config = pyramid.testing.setUp(request=request)
541 add_events_routes(config)
542
543
514 544 class BaseRepoController(BaseController):
515 545 """
516 546 Base class for controllers responsible for loading all needed data for
517 547 repository loaded items are
518 548
519 549 c.rhodecode_repo: instance of scm repository
520 550 c.rhodecode_db_repo: instance of db
521 551 c.repository_requirements_missing: shows that repository specific data
522 552 could not be displayed due to the missing requirements
523 553 c.repository_pull_requests: show number of open pull requests
524 554 """
525 555
526 556 def __before__(self):
527 557 super(BaseRepoController, self).__before__()
528 558 if c.repo_name: # extracted from routes
529 559 db_repo = Repository.get_by_repo_name(c.repo_name)
530 560 if not db_repo:
531 561 return
532 562
533 563 log.debug(
534 564 'Found repository in database %s with state `%s`',
535 565 safe_unicode(db_repo), safe_unicode(db_repo.repo_state))
536 566 route = getattr(request.environ.get('routes.route'), 'name', '')
537 567
538 568 # allow to delete repos that are somehow damages in filesystem
539 569 if route in ['delete_repo']:
540 570 return
541 571
542 572 if db_repo.repo_state in [Repository.STATE_PENDING]:
543 573 if route in ['repo_creating_home']:
544 574 return
545 575 check_url = url('repo_creating_home', repo_name=c.repo_name)
546 576 return redirect(check_url)
547 577
548 578 self.rhodecode_db_repo = db_repo
549 579
550 580 missing_requirements = False
551 581 try:
552 582 self.rhodecode_repo = self.rhodecode_db_repo.scm_instance()
553 583 except RepositoryRequirementError as e:
554 584 missing_requirements = True
555 585 self._handle_missing_requirements(e)
556 586
557 587 if self.rhodecode_repo is None and not missing_requirements:
558 588 log.error('%s this repository is present in database but it '
559 589 'cannot be created as an scm instance', c.repo_name)
560 590
561 591 h.flash(_(
562 592 "The repository at %(repo_name)s cannot be located.") %
563 593 {'repo_name': c.repo_name},
564 594 category='error', ignore_duplicate=True)
565 595 redirect(h.route_path('home'))
566 596
567 597 # update last change according to VCS data
568 598 if not missing_requirements:
569 599 commit = db_repo.get_commit(
570 600 pre_load=["author", "date", "message", "parents"])
571 601 db_repo.update_commit_cache(commit)
572 602
573 603 # Prepare context
574 604 c.rhodecode_db_repo = db_repo
575 605 c.rhodecode_repo = self.rhodecode_repo
576 606 c.repository_requirements_missing = missing_requirements
577 607
578 608 self._update_global_counters(self.scm_model, db_repo)
579 609
580 610 def _update_global_counters(self, scm_model, db_repo):
581 611 """
582 612 Base variables that are exposed to every page of repository
583 613 """
584 614 c.repository_pull_requests = scm_model.get_pull_requests(db_repo)
585 615
586 616 def _handle_missing_requirements(self, error):
587 617 self.rhodecode_repo = None
588 618 log.error(
589 619 'Requirements are missing for repository %s: %s',
590 620 c.repo_name, error.message)
591 621
592 622 summary_url = h.route_path('repo_summary', repo_name=c.repo_name)
593 623 statistics_url = url('edit_repo_statistics', repo_name=c.repo_name)
594 624 settings_update_url = url('repo', repo_name=c.repo_name)
595 625 path = request.path
596 626 should_redirect = (
597 627 path not in (summary_url, settings_update_url)
598 628 and '/settings' not in path or path == statistics_url
599 629 )
600 630 if should_redirect:
601 631 redirect(summary_url)
@@ -1,241 +1,244 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import json
22 22 import logging
23 23 import traceback
24 24 import threading
25 25 from BaseHTTPServer import BaseHTTPRequestHandler
26 26 from SocketServer import TCPServer
27 27
28 28 import pylons
29 29 import rhodecode
30 30
31 31 from rhodecode.model import meta
32 from rhodecode.lib.base import bootstrap_request
32 33 from rhodecode.lib import hooks_base
33 34 from rhodecode.lib.utils2 import (
34 35 AttributeDict, safe_str, get_routes_generator_for_server_url)
36 from rhodecode.lib.utils2 import AttributeDict
35 37
36 38
37 39 log = logging.getLogger(__name__)
38 40
39 41
40 42 class HooksHttpHandler(BaseHTTPRequestHandler):
41 43 def do_POST(self):
42 44 method, extras = self._read_request()
43 45 try:
44 46 result = self._call_hook(method, extras)
45 47 except Exception as e:
46 48 exc_tb = traceback.format_exc()
47 49 result = {
48 50 'exception': e.__class__.__name__,
49 51 'exception_traceback': exc_tb,
50 52 'exception_args': e.args
51 53 }
52 54 self._write_response(result)
53 55
54 56 def _read_request(self):
55 57 length = int(self.headers['Content-Length'])
56 58 body = self.rfile.read(length).decode('utf-8')
57 59 data = json.loads(body)
58 60 return data['method'], data['extras']
59 61
60 62 def _write_response(self, result):
61 63 self.send_response(200)
62 64 self.send_header("Content-type", "text/json")
63 65 self.end_headers()
64 66 self.wfile.write(json.dumps(result))
65 67
66 68 def _call_hook(self, method, extras):
67 69 hooks = Hooks()
68 70 try:
69 71 result = getattr(hooks, method)(extras)
70 72 finally:
71 73 meta.Session.remove()
72 74 return result
73 75
74 76 def log_message(self, format, *args):
75 77 """
76 78 This is an overridden method of BaseHTTPRequestHandler which logs using
77 79 logging library instead of writing directly to stderr.
78 80 """
79 81
80 82 message = format % args
81 83
82 84 # TODO: mikhail: add different log levels support
83 85 log.debug(
84 86 "%s - - [%s] %s", self.client_address[0],
85 87 self.log_date_time_string(), message)
86 88
87 89
88 90 class DummyHooksCallbackDaemon(object):
89 91 def __init__(self):
90 92 self.hooks_module = Hooks.__module__
91 93
92 94 def __enter__(self):
93 95 log.debug('Running dummy hooks callback daemon')
94 96 return self
95 97
96 98 def __exit__(self, exc_type, exc_val, exc_tb):
97 99 log.debug('Exiting dummy hooks callback daemon')
98 100
99 101
100 102 class ThreadedHookCallbackDaemon(object):
101 103
102 104 _callback_thread = None
103 105 _daemon = None
104 106 _done = False
105 107
106 108 def __init__(self):
107 109 self._prepare()
108 110
109 111 def __enter__(self):
110 112 self._run()
111 113 return self
112 114
113 115 def __exit__(self, exc_type, exc_val, exc_tb):
114 116 self._stop()
115 117
116 118 def _prepare(self):
117 119 raise NotImplementedError()
118 120
119 121 def _run(self):
120 122 raise NotImplementedError()
121 123
122 124 def _stop(self):
123 125 raise NotImplementedError()
124 126
125 127
126 128 class HttpHooksCallbackDaemon(ThreadedHookCallbackDaemon):
127 129 """
128 130 Context manager which will run a callback daemon in a background thread.
129 131 """
130 132
131 133 hooks_uri = None
132 134
133 135 IP_ADDRESS = '127.0.0.1'
134 136
135 137 # From Python docs: Polling reduces our responsiveness to a shutdown
136 138 # request and wastes cpu at all other times.
137 139 POLL_INTERVAL = 0.1
138 140
139 141 def _prepare(self):
140 142 log.debug("Preparing callback daemon and registering hook object")
141 143
142 144 self._done = False
143 145 self._daemon = TCPServer((self.IP_ADDRESS, 0), HooksHttpHandler)
144 146 _, port = self._daemon.server_address
145 147 self.hooks_uri = '{}:{}'.format(self.IP_ADDRESS, port)
146 148
147 149 log.debug("Hooks uri is: %s", self.hooks_uri)
148 150
149 151 def _run(self):
150 152 log.debug("Running event loop of callback daemon in background thread")
151 153 callback_thread = threading.Thread(
152 154 target=self._daemon.serve_forever,
153 155 kwargs={'poll_interval': self.POLL_INTERVAL})
154 156 callback_thread.daemon = True
155 157 callback_thread.start()
156 158 self._callback_thread = callback_thread
157 159
158 160 def _stop(self):
159 161 log.debug("Waiting for background thread to finish.")
160 162 self._daemon.shutdown()
161 163 self._callback_thread.join()
162 164 self._daemon = None
163 165 self._callback_thread = None
164 166
165 167
166 168 def prepare_callback_daemon(extras, protocol, use_direct_calls):
167 169 callback_daemon = None
168 170
169 171 if use_direct_calls:
170 172 callback_daemon = DummyHooksCallbackDaemon()
171 173 extras['hooks_module'] = callback_daemon.hooks_module
172 174 else:
173 175 if protocol == 'http':
174 176 callback_daemon = HttpHooksCallbackDaemon()
175 177 else:
176 178 log.error('Unsupported callback daemon protocol "%s"', protocol)
177 179 raise Exception('Unsupported callback daemon protocol.')
178 180
179 181 extras['hooks_uri'] = callback_daemon.hooks_uri
180 182 extras['hooks_protocol'] = protocol
181 183
182 184 return callback_daemon, extras
183 185
184 186
185 187 class Hooks(object):
186 188 """
187 189 Exposes the hooks for remote call backs
188 190 """
189 191
190 192 def repo_size(self, extras):
191 log.debug("Called repo_size of Hooks object")
193 log.debug("Called repo_size of %s object", self)
192 194 return self._call_hook(hooks_base.repo_size, extras)
193 195
194 196 def pre_pull(self, extras):
195 log.debug("Called pre_pull of Hooks object")
197 log.debug("Called pre_pull of %s object", self)
196 198 return self._call_hook(hooks_base.pre_pull, extras)
197 199
198 200 def post_pull(self, extras):
199 log.debug("Called post_pull of Hooks object")
201 log.debug("Called post_pull of %s object", self)
200 202 return self._call_hook(hooks_base.post_pull, extras)
201 203
202 204 def pre_push(self, extras):
203 log.debug("Called pre_push of Hooks object")
205 log.debug("Called pre_push of %s object", self)
204 206 return self._call_hook(hooks_base.pre_push, extras)
205 207
206 208 def post_push(self, extras):
207 log.debug("Called post_push of Hooks object")
209 log.debug("Called post_push of %s object", self)
208 210 return self._call_hook(hooks_base.post_push, extras)
209 211
210 212 def _call_hook(self, hook, extras):
211 213 extras = AttributeDict(extras)
212 214 pylons_router = get_routes_generator_for_server_url(extras.server_url)
213 215 pylons.url._push_object(pylons_router)
216 extras.request = bootstrap_request()
214 217
215 218 try:
216 219 result = hook(extras)
217 220 except Exception as error:
218 221 exc_tb = traceback.format_exc()
219 222 log.exception('Exception when handling hook %s', hook)
220 223 error_args = error.args
221 224 return {
222 225 'status': 128,
223 226 'output': '',
224 227 'exception': type(error).__name__,
225 228 'exception_traceback': exc_tb,
226 229 'exception_args': error_args,
227 230 }
228 231 finally:
229 232 pylons.url._pop_object()
230 233 meta.Session.remove()
231 234
232 235 return {
233 236 'status': result.status,
234 237 'output': result.output,
235 238 }
236 239
237 240 def __enter__(self):
238 241 return self
239 242
240 243 def __exit__(self, exc_type, exc_val, exc_tb):
241 244 pass
General Comments 0
You need to be logged in to leave comments. Login now