##// END OF EJS Templates
pep8ify middlewares
marcink -
r1275:27232762 beta
parent child Browse files
Show More
@@ -1,52 +1,54 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.middleware.https_fixup
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 middleware to handle https correctly
7 7
8 8 :created_on: May 23, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 from rhodecode.lib import str2bool
27 27
28
28 29 class HttpsFixup(object):
30
29 31 def __init__(self, app, config):
30 32 self.application = app
31 33 self.config = config
32 34
33 35 def __call__(self, environ, start_response):
34 36 self.__fixup(environ)
35 37 return self.application(environ, start_response)
36 38
37
38 39 def __fixup(self, environ):
39 """Function to fixup the environ as needed. In order to use this
40 """
41 Function to fixup the environ as needed. In order to use this
40 42 middleware you should set this header inside your
41 43 proxy ie. nginx, apache etc.
42 44 """
43 45 proto = environ.get('HTTP_X_URL_SCHEME')
44 46
45 47 if str2bool(self.config.get('force_https')):
46 48 proto = 'https'
47 49
48 50 if proto == 'https':
49 51 environ['wsgi.url_scheme'] = proto
50 52 else:
51 53 environ['wsgi.url_scheme'] = 'http'
52 54 return None
@@ -1,273 +1,279 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.middleware.simplegit
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 SimpleGit middleware for handling git protocol request (push/clone etc.)
7 7 It's implemented with basic auth function
8 8
9 9 :created_on: Apr 28, 2010
10 10 :author: marcink
11 11 :copyright: (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26
27 27 import os
28 28 import logging
29 29 import traceback
30 30
31 31 from dulwich import server as dulserver
32 32
33
33 34 class SimpleGitUploadPackHandler(dulserver.UploadPackHandler):
34 35
35 36 def handle(self):
36 37 write = lambda x: self.proto.write_sideband(1, x)
37 38
38 graph_walker = dulserver.ProtocolGraphWalker(self, self.repo.object_store,
39 self.repo.get_peeled)
39 graph_walker = dulserver.ProtocolGraphWalker(self,
40 self.repo.object_store,
41 self.repo.get_peeled)
40 42 objects_iter = self.repo.fetch_objects(
41 43 graph_walker.determine_wants, graph_walker, self.progress,
42 44 get_tagged=self.get_tagged)
43 45
44 46 # Do they want any objects?
45 47 if len(objects_iter) == 0:
46 48 return
47 49
48 50 self.progress("counting objects: %d, done.\n" % len(objects_iter))
49 dulserver.write_pack_data(dulserver.ProtocolFile(None, write), objects_iter,
50 len(objects_iter))
51 dulserver.write_pack_data(dulserver.ProtocolFile(None, write),
52 objects_iter, len(objects_iter))
51 53 messages = []
52 54 messages.append('thank you for using rhodecode')
53 55
54 56 for msg in messages:
55 57 self.progress(msg + "\n")
56 58 # we are done
57 59 self.proto.write("0000")
58 60
59 61 dulserver.DEFAULT_HANDLERS = {
60 62 'git-upload-pack': SimpleGitUploadPackHandler,
61 63 'git-receive-pack': dulserver.ReceivePackHandler,
62 64 }
63 65
64 66 from dulwich.repo import Repo
65 67 from dulwich.web import HTTPGitApplication
66 68
67 69 from paste.auth.basic import AuthBasicAuthenticator
68 70 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
69 71
70 72 from rhodecode.lib.auth import authfunc, HasPermissionAnyMiddleware
71 73 from rhodecode.lib.utils import invalidate_cache, check_repo_fast
72 74 from rhodecode.model.user import UserModel
73 75
74 76 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError
75 77
76 78 log = logging.getLogger(__name__)
77 79
80
78 81 def is_git(environ):
79 82 """Returns True if request's target is git server.
80 83 ``HTTP_USER_AGENT`` would then have git client version given.
81 84
82 85 :param environ:
83 86 """
84 87 http_user_agent = environ.get('HTTP_USER_AGENT')
85 88 if http_user_agent and http_user_agent.startswith('git'):
86 89 return True
87 90 return False
88 91
92
89 93 class SimpleGit(object):
90 94
91 95 def __init__(self, application, config):
92 96 self.application = application
93 97 self.config = config
94 98 #authenticate this git request using
95 99 self.authenticate = AuthBasicAuthenticator('', authfunc)
96 100 self.ipaddr = '0.0.0.0'
97 101 self.repository = None
98 102 self.username = None
99 103 self.action = None
100 104
101 105 def __call__(self, environ, start_response):
102 106 if not is_git(environ):
103 107 return self.application(environ, start_response)
104 108
105 109 proxy_key = 'HTTP_X_REAL_IP'
106 110 def_key = 'REMOTE_ADDR'
107 111 self.ipaddr = environ.get(proxy_key, environ.get(def_key, '0.0.0.0'))
108 112 # skip passing error to error controller
109 113 environ['pylons.status_code_redirect'] = True
110 114
111 115 #======================================================================
112 116 # GET ACTION PULL or PUSH
113 117 #======================================================================
114 118 self.action = self.__get_action(environ)
115 119 try:
116 120 #==================================================================
117 121 # GET REPOSITORY NAME
118 122 #==================================================================
119 123 self.repo_name = self.__get_repository(environ)
120 124 except:
121 125 return HTTPInternalServerError()(environ, start_response)
122 126
123 127 #======================================================================
124 128 # CHECK ANONYMOUS PERMISSION
125 129 #======================================================================
126 130 if self.action in ['pull', 'push'] or self.action:
127 131 anonymous_user = self.__get_user('default')
128 132 self.username = anonymous_user.username
129 anonymous_perm = self.__check_permission(self.action, anonymous_user ,
130 self.repo_name)
133 anonymous_perm = self.__check_permission(self.action,
134 anonymous_user,
135 self.repo_name)
131 136
132 137 if anonymous_perm is not True or anonymous_user.active is False:
133 138 if anonymous_perm is not True:
134 log.debug('Not enough credentials to access this repository'
135 'as anonymous user')
139 log.debug('Not enough credentials to access this '
140 'repository as anonymous user')
136 141 if anonymous_user.active is False:
137 142 log.debug('Anonymous access is disabled, running '
138 143 'authentication')
139 144 #==============================================================
140 145 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
141 146 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
142 147 #==============================================================
143 148
144 149 if not REMOTE_USER(environ):
145 self.authenticate.realm = str(self.config['rhodecode_realm'])
150 self.authenticate.realm = str(
151 self.config['rhodecode_realm'])
146 152 result = self.authenticate(environ)
147 153 if isinstance(result, str):
148 154 AUTH_TYPE.update(environ, 'basic')
149 155 REMOTE_USER.update(environ, result)
150 156 else:
151 157 return result.wsgi_application(environ, start_response)
152 158
153
154 159 #==============================================================
155 160 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME FROM
156 161 # BASIC AUTH
157 162 #==============================================================
158 163
159 164 if self.action in ['pull', 'push'] or self.action:
160 165 username = REMOTE_USER(environ)
161 166 try:
162 167 user = self.__get_user(username)
163 168 self.username = user.username
164 169 except:
165 170 log.error(traceback.format_exc())
166 return HTTPInternalServerError()(environ, start_response)
171 return HTTPInternalServerError()(environ,
172 start_response)
167 173
168 174 #check permissions for this repository
169 perm = self.__check_permission(self.action, user, self.repo_name)
175 perm = self.__check_permission(self.action, user,
176 self.repo_name)
170 177 if perm is not True:
171 178 print 'not allowed'
172 179 return HTTPForbidden()(environ, start_response)
173 180
174 self.extras = {'ip':self.ipaddr,
175 'username':self.username,
176 'action':self.action,
177 'repository':self.repo_name}
181 self.extras = {'ip': self.ipaddr,
182 'username': self.username,
183 'action': self.action,
184 'repository': self.repo_name}
178 185
179 186 #===================================================================
180 187 # GIT REQUEST HANDLING
181 188 #===================================================================
182 189 self.basepath = self.config['base_path']
183 190 self.repo_path = os.path.join(self.basepath, self.repo_name)
184 191 #quick check if that dir exists...
185 192 if check_repo_fast(self.repo_name, self.basepath):
186 193 return HTTPNotFound()(environ, start_response)
187 194 try:
188 195 app = self.__make_app()
189 196 except:
190 197 log.error(traceback.format_exc())
191 198 return HTTPInternalServerError()(environ, start_response)
192 199
193 200 #invalidate cache on push
194 201 if self.action == 'push':
195 202 self.__invalidate_cache(self.repo_name)
196 203 messages = []
197 204 messages.append('thank you for using rhodecode')
198 205 return app(environ, start_response)
199 206 else:
200 207 return app(environ, start_response)
201 208
202
203 209 def __make_app(self):
204 backend = dulserver.DictBackend({'/' + self.repo_name: Repo(self.repo_path)})
210 _d = {'/' + self.repo_name: Repo(self.repo_path)}
211 backend = dulserver.DictBackend(_d)
205 212 gitserve = HTTPGitApplication(backend)
206 213
207 214 return gitserve
208 215
209 216 def __check_permission(self, action, user, repo_name):
210 217 """Checks permissions using action (push/pull) user and repository
211 218 name
212 219
213 220 :param action: push or pull action
214 221 :param user: user instance
215 222 :param repo_name: repository name
216 223 """
217 224 if action == 'push':
218 225 if not HasPermissionAnyMiddleware('repository.write',
219 'repository.admin')\
220 (user, repo_name):
226 'repository.admin')(user,
227 repo_name):
221 228 return False
222 229
223 230 else:
224 231 #any other action need at least read permission
225 232 if not HasPermissionAnyMiddleware('repository.read',
226 233 'repository.write',
227 'repository.admin')\
228 (user, repo_name):
234 'repository.admin')(user,
235 repo_name):
229 236 return False
230 237
231 238 return True
232 239
233
234 240 def __get_repository(self, environ):
235 241 """Get's repository name out of PATH_INFO header
236 242
237 243 :param environ: environ where PATH_INFO is stored
238 244 """
239 245 try:
240 246 repo_name = '/'.join(environ['PATH_INFO'].split('/')[1:])
241 247 if repo_name.endswith('/'):
242 248 repo_name = repo_name.rstrip('/')
243 249 except:
244 250 log.error(traceback.format_exc())
245 251 raise
246 252 repo_name = repo_name.split('/')[0]
247 253 return repo_name
248 254
249
250 255 def __get_user(self, username):
251 256 return UserModel().get_by_username(username, cache=True)
252 257
253 258 def __get_action(self, environ):
254 259 """Maps git request commands into a pull or push command.
255 260
256 261 :param environ:
257 262 """
258 263 service = environ['QUERY_STRING'].split('=')
259 264 if len(service) > 1:
260 265 service_cmd = service[1]
261 266 mapping = {'git-receive-pack': 'push',
262 267 'git-upload-pack': 'pull',
263 268 }
264 269
265 return mapping.get(service_cmd, service_cmd if service_cmd else 'other')
270 return mapping.get(service_cmd,
271 service_cmd if service_cmd else 'other')
266 272 else:
267 273 return 'other'
268 274
269 275 def __invalidate_cache(self, repo_name):
270 276 """we know that some change was made to repositories and we should
271 277 invalidate the cache to see the changes right away but only for
272 278 push requests"""
273 279 invalidate_cache('get_repo_cached_%s' % repo_name)
@@ -1,270 +1,271 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.middleware.simplehg
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 SimpleHG middleware for handling mercurial protocol request
7 7 (push/clone etc.). It's implemented with basic auth function
8 8
9 9 :created_on: Apr 28, 2010
10 10 :author: marcink
11 11 :copyright: (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26
27 27 import os
28 28 import logging
29 29 import traceback
30 30
31 31 from mercurial.error import RepoError
32 32 from mercurial.hgweb import hgweb
33 33 from mercurial.hgweb.request import wsgiapplication
34 34
35 35 from paste.auth.basic import AuthBasicAuthenticator
36 36 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
37 37
38 38 from rhodecode.lib.auth import authfunc, HasPermissionAnyMiddleware
39 39 from rhodecode.lib.utils import make_ui, invalidate_cache, \
40 40 check_repo_fast, ui_sections
41 41 from rhodecode.model.user import UserModel
42 42
43 43 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError
44 44
45 45 log = logging.getLogger(__name__)
46 46
47
47 48 def is_mercurial(environ):
48 49 """Returns True if request's target is mercurial server - header
49 50 ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``.
50 51 """
51 52 http_accept = environ.get('HTTP_ACCEPT')
52 53 if http_accept and http_accept.startswith('application/mercurial'):
53 54 return True
54 55 return False
55 56
57
56 58 class SimpleHg(object):
57 59
58 60 def __init__(self, application, config):
59 61 self.application = application
60 62 self.config = config
61 63 #authenticate this mercurial request using authfunc
62 64 self.authenticate = AuthBasicAuthenticator('', authfunc)
63 65 self.ipaddr = '0.0.0.0'
64 66 self.repo_name = None
65 67 self.username = None
66 68 self.action = None
67 69
68 70 def __call__(self, environ, start_response):
69 71 if not is_mercurial(environ):
70 72 return self.application(environ, start_response)
71 73
72 74 proxy_key = 'HTTP_X_REAL_IP'
73 75 def_key = 'REMOTE_ADDR'
74 76 self.ipaddr = environ.get(proxy_key, environ.get(def_key, '0.0.0.0'))
75 77 # skip passing error to error controller
76 78 environ['pylons.status_code_redirect'] = True
77 79
78 80 #======================================================================
79 81 # GET ACTION PULL or PUSH
80 82 #======================================================================
81 83 self.action = self.__get_action(environ)
82 84 try:
83 85 #==================================================================
84 86 # GET REPOSITORY NAME
85 87 #==================================================================
86 88 self.repo_name = self.__get_repository(environ)
87 89 except:
88 90 return HTTPInternalServerError()(environ, start_response)
89 91
90 92 #======================================================================
91 93 # CHECK ANONYMOUS PERMISSION
92 94 #======================================================================
93 95 if self.action in ['pull', 'push']:
94 96 anonymous_user = self.__get_user('default')
95 97 self.username = anonymous_user.username
96 anonymous_perm = self.__check_permission(self.action, anonymous_user ,
97 self.repo_name)
98 anonymous_perm = self.__check_permission(self.action,
99 anonymous_user,
100 self.repo_name)
98 101
99 102 if anonymous_perm is not True or anonymous_user.active is False:
100 103 if anonymous_perm is not True:
101 log.debug('Not enough credentials to access this repository'
102 'as anonymous user')
104 log.debug('Not enough credentials to access this '
105 'repository as anonymous user')
103 106 if anonymous_user.active is False:
104 107 log.debug('Anonymous access is disabled, running '
105 108 'authentication')
106 109 #==============================================================
107 110 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
108 111 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
109 112 #==============================================================
110 113
111 114 if not REMOTE_USER(environ):
112 self.authenticate.realm = str(self.config['rhodecode_realm'])
115 self.authenticate.realm = str(
116 self.config['rhodecode_realm'])
113 117 result = self.authenticate(environ)
114 118 if isinstance(result, str):
115 119 AUTH_TYPE.update(environ, 'basic')
116 120 REMOTE_USER.update(environ, result)
117 121 else:
118 122 return result.wsgi_application(environ, start_response)
119 123
120
121 124 #==============================================================
122 125 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME FROM
123 126 # BASIC AUTH
124 127 #==============================================================
125 128
126 129 if self.action in ['pull', 'push']:
127 130 username = REMOTE_USER(environ)
128 131 try:
129 132 user = self.__get_user(username)
130 133 self.username = user.username
131 134 except:
132 135 log.error(traceback.format_exc())
133 return HTTPInternalServerError()(environ, start_response)
136 return HTTPInternalServerError()(environ,
137 start_response)
134 138
135 139 #check permissions for this repository
136 perm = self.__check_permission(self.action, user, self.repo_name)
140 perm = self.__check_permission(self.action, user,
141 self.repo_name)
137 142 if perm is not True:
138 143 return HTTPForbidden()(environ, start_response)
139 144
140 self.extras = {'ip':self.ipaddr,
141 'username':self.username,
142 'action':self.action,
143 'repository':self.repo_name}
145 self.extras = {'ip': self.ipaddr,
146 'username': self.username,
147 'action': self.action,
148 'repository': self.repo_name}
144 149
145 #===================================================================
150 #======================================================================
146 151 # MERCURIAL REQUEST HANDLING
147 #===================================================================
148 environ['PATH_INFO'] = '/'#since we wrap into hgweb, reset the path
152 #======================================================================
153 environ['PATH_INFO'] = '/' # since we wrap into hgweb, reset the path
149 154 self.baseui = make_ui('db')
150 155 self.basepath = self.config['base_path']
151 156 self.repo_path = os.path.join(self.basepath, self.repo_name)
152 157
153 158 #quick check if that dir exists...
154 159 if check_repo_fast(self.repo_name, self.basepath):
155 160 return HTTPNotFound()(environ, start_response)
156 161 try:
157 162 app = wsgiapplication(self.__make_app)
158 163 except RepoError, e:
159 164 if str(e).find('not found') != -1:
160 165 return HTTPNotFound()(environ, start_response)
161 166 except Exception:
162 167 log.error(traceback.format_exc())
163 168 return HTTPInternalServerError()(environ, start_response)
164 169
165 170 #invalidate cache on push
166 171 if self.action == 'push':
167 172 self.__invalidate_cache(self.repo_name)
168 173
169 174 return app(environ, start_response)
170 175
171
172 176 def __make_app(self):
173 177 """Make an wsgi application using hgweb, and my generated baseui
174 178 instance
175 179 """
176 180
177 181 hgserve = hgweb(str(self.repo_path), baseui=self.baseui)
178 182 return self.__load_web_settings(hgserve, self.extras)
179 183
180
181 184 def __check_permission(self, action, user, repo_name):
182 185 """Checks permissions using action (push/pull) user and repository
183 186 name
184 187
185 188 :param action: push or pull action
186 189 :param user: user instance
187 190 :param repo_name: repository name
188 191 """
189 192 if action == 'push':
190 193 if not HasPermissionAnyMiddleware('repository.write',
191 'repository.admin')\
192 (user, repo_name):
194 'repository.admin')(user,
195 repo_name):
193 196 return False
194 197
195 198 else:
196 199 #any other action need at least read permission
197 200 if not HasPermissionAnyMiddleware('repository.read',
198 201 'repository.write',
199 'repository.admin')\
200 (user, repo_name):
202 'repository.admin')(user,
203 repo_name):
201 204 return False
202 205
203 206 return True
204 207
205
206 208 def __get_repository(self, environ):
207 209 """Get's repository name out of PATH_INFO header
208 210
209 211 :param environ: environ where PATH_INFO is stored
210 212 """
211 213 try:
212 214 repo_name = '/'.join(environ['PATH_INFO'].split('/')[1:])
213 215 if repo_name.endswith('/'):
214 216 repo_name = repo_name.rstrip('/')
215 217 except:
216 218 log.error(traceback.format_exc())
217 219 raise
218 220
219 221 return repo_name
220 222
221 223 def __get_user(self, username):
222 224 return UserModel().get_by_username(username, cache=True)
223 225
224 226 def __get_action(self, environ):
225 227 """Maps mercurial request commands into a clone,pull or push command.
226 228 This should always return a valid command string
227 229
228 230 :param environ:
229 231 """
230 232 mapping = {'changegroup': 'pull',
231 233 'changegroupsubset': 'pull',
232 234 'stream_out': 'pull',
233 235 'listkeys': 'pull',
234 236 'unbundle': 'push',
235 237 'pushkey': 'push', }
236 238 for qry in environ['QUERY_STRING'].split('&'):
237 239 if qry.startswith('cmd'):
238 240 cmd = qry.split('=')[-1]
239 if mapping.has_key(cmd):
241 if cmd in mapping:
240 242 return mapping[cmd]
241 243 else:
242 244 return 'pull'
243 245
244 246 def __invalidate_cache(self, repo_name):
245 247 """we know that some change was made to repositories and we should
246 248 invalidate the cache to see the changes right away but only for
247 249 push requests"""
248 250 invalidate_cache('get_repo_cached_%s' % repo_name)
249 251
250
251 252 def __load_web_settings(self, hgserve, extras={}):
252 253 #set the global ui for hgserve instance passed
253 254 hgserve.repo.ui = self.baseui
254 255
255 256 hgrc = os.path.join(self.repo_path, '.hg', 'hgrc')
256 257
257 258 #inject some additional parameters that will be available in ui
258 259 #for hooks
259 260 for k, v in extras.items():
260 261 hgserve.repo.ui.setconfig('rhodecode_extras', k, v)
261 262
262 263 repoui = make_ui('file', hgrc, False)
263 264
264 265 if repoui:
265 266 #overwrite our ui instance with the section from hgrc file
266 267 for section in ui_sections:
267 268 for k, v in repoui.configitems(section):
268 269 hgserve.repo.ui.setconfig(section, k, v)
269 270
270 271 return hgserve
General Comments 0
You need to be logged in to leave comments. Login now