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