##// END OF EJS Templates
fixed multiple IP addresses in each of extracted IP....
marcink -
r3669:b66fd6de beta
parent child Browse files
Show More
@@ -1,337 +1,345 b''
1 """The base Controller API
1 """The base Controller API
2
2
3 Provides the BaseController class for subclassing.
3 Provides the BaseController class for subclassing.
4 """
4 """
5 import logging
5 import logging
6 import time
6 import time
7 import traceback
7 import traceback
8
8
9 from paste.auth.basic import AuthBasicAuthenticator
9 from paste.auth.basic import AuthBasicAuthenticator
10 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden
10 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden
11 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
11 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
12
12
13 from pylons import config, tmpl_context as c, request, session, url
13 from pylons import config, tmpl_context as c, request, session, url
14 from pylons.controllers import WSGIController
14 from pylons.controllers import WSGIController
15 from pylons.controllers.util import redirect
15 from pylons.controllers.util import redirect
16 from pylons.templating import render_mako as render
16 from pylons.templating import render_mako as render
17
17
18 from rhodecode import __version__, BACKENDS
18 from rhodecode import __version__, BACKENDS
19
19
20 from rhodecode.lib.utils2 import str2bool, safe_unicode, AttributeDict,\
20 from rhodecode.lib.utils2 import str2bool, safe_unicode, AttributeDict,\
21 safe_str, safe_int
21 safe_str, safe_int
22 from rhodecode.lib.auth import AuthUser, get_container_username, authfunc,\
22 from rhodecode.lib.auth import AuthUser, get_container_username, authfunc,\
23 HasPermissionAnyMiddleware, CookieStoreWrapper
23 HasPermissionAnyMiddleware, CookieStoreWrapper
24 from rhodecode.lib.utils import get_repo_slug, invalidate_cache
24 from rhodecode.lib.utils import get_repo_slug, invalidate_cache
25 from rhodecode.model import meta
25 from rhodecode.model import meta
26
26
27 from rhodecode.model.db import Repository, RhodeCodeUi, User, RhodeCodeSetting
27 from rhodecode.model.db import Repository, RhodeCodeUi, User, RhodeCodeSetting
28 from rhodecode.model.notification import NotificationModel
28 from rhodecode.model.notification import NotificationModel
29 from rhodecode.model.scm import ScmModel
29 from rhodecode.model.scm import ScmModel
30 from rhodecode.model.meta import Session
30 from rhodecode.model.meta import Session
31
31
32 log = logging.getLogger(__name__)
32 log = logging.getLogger(__name__)
33
33
34
34
35 def _filter_proxy(ip):
36 """
37 HEADERS can have mutliple ips inside the left-most being the original
38 client, and each successive proxy that passed the request adding the IP
39 address where it received the request from.
40
41 :param ip:
42 """
43 if ',' in ip:
44 _ips = ip.split(',')
45 _first_ip = _ips[0].strip()
46 log.debug('Got multiple IPs %s, using %s' % (','.join(_ips), _first_ip))
47 return _first_ip
48 return ip
49
50
35 def _get_ip_addr(environ):
51 def _get_ip_addr(environ):
36 proxy_key = 'HTTP_X_REAL_IP'
52 proxy_key = 'HTTP_X_REAL_IP'
37 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
53 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
38 def_key = 'REMOTE_ADDR'
54 def_key = 'REMOTE_ADDR'
39
55
40 ip = environ.get(proxy_key)
56 ip = environ.get(proxy_key)
41 if ip:
57 if ip:
42 return ip
58 return _filter_proxy(ip)
43
59
44 ip = environ.get(proxy_key2)
60 ip = environ.get(proxy_key2)
45 if ip:
61 if ip:
46 return ip
62 return _filter_proxy(ip)
47
63
48 ip = environ.get(def_key, '0.0.0.0')
64 ip = environ.get(def_key, '0.0.0.0')
49
65 return _filter_proxy(ip)
50 # HEADERS can have mutliple ips inside
51 # the left-most being the original client, and each successive proxy
52 # that passed the request adding the IP address where it received the
53 # request from.
54 if ',' in ip:
55 ip = ip.split(',')[0].strip()
56
57 return ip
58
66
59
67
60 def _get_access_path(environ):
68 def _get_access_path(environ):
61 path = environ.get('PATH_INFO')
69 path = environ.get('PATH_INFO')
62 org_req = environ.get('pylons.original_request')
70 org_req = environ.get('pylons.original_request')
63 if org_req:
71 if org_req:
64 path = org_req.environ.get('PATH_INFO')
72 path = org_req.environ.get('PATH_INFO')
65 return path
73 return path
66
74
67
75
68 class BasicAuth(AuthBasicAuthenticator):
76 class BasicAuth(AuthBasicAuthenticator):
69
77
70 def __init__(self, realm, authfunc, auth_http_code=None):
78 def __init__(self, realm, authfunc, auth_http_code=None):
71 self.realm = realm
79 self.realm = realm
72 self.authfunc = authfunc
80 self.authfunc = authfunc
73 self._rc_auth_http_code = auth_http_code
81 self._rc_auth_http_code = auth_http_code
74
82
75 def build_authentication(self):
83 def build_authentication(self):
76 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
84 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
77 if self._rc_auth_http_code and self._rc_auth_http_code == '403':
85 if self._rc_auth_http_code and self._rc_auth_http_code == '403':
78 # return 403 if alternative http return code is specified in
86 # return 403 if alternative http return code is specified in
79 # RhodeCode config
87 # RhodeCode config
80 return HTTPForbidden(headers=head)
88 return HTTPForbidden(headers=head)
81 return HTTPUnauthorized(headers=head)
89 return HTTPUnauthorized(headers=head)
82
90
83 def authenticate(self, environ):
91 def authenticate(self, environ):
84 authorization = AUTHORIZATION(environ)
92 authorization = AUTHORIZATION(environ)
85 if not authorization:
93 if not authorization:
86 return self.build_authentication()
94 return self.build_authentication()
87 (authmeth, auth) = authorization.split(' ', 1)
95 (authmeth, auth) = authorization.split(' ', 1)
88 if 'basic' != authmeth.lower():
96 if 'basic' != authmeth.lower():
89 return self.build_authentication()
97 return self.build_authentication()
90 auth = auth.strip().decode('base64')
98 auth = auth.strip().decode('base64')
91 _parts = auth.split(':', 1)
99 _parts = auth.split(':', 1)
92 if len(_parts) == 2:
100 if len(_parts) == 2:
93 username, password = _parts
101 username, password = _parts
94 if self.authfunc(environ, username, password):
102 if self.authfunc(environ, username, password):
95 return username
103 return username
96 return self.build_authentication()
104 return self.build_authentication()
97
105
98 __call__ = authenticate
106 __call__ = authenticate
99
107
100
108
101 class BaseVCSController(object):
109 class BaseVCSController(object):
102
110
103 def __init__(self, application, config):
111 def __init__(self, application, config):
104 self.application = application
112 self.application = application
105 self.config = config
113 self.config = config
106 # base path of repo locations
114 # base path of repo locations
107 self.basepath = self.config['base_path']
115 self.basepath = self.config['base_path']
108 #authenticate this mercurial request using authfunc
116 #authenticate this mercurial request using authfunc
109 self.authenticate = BasicAuth('', authfunc,
117 self.authenticate = BasicAuth('', authfunc,
110 config.get('auth_ret_code'))
118 config.get('auth_ret_code'))
111 self.ip_addr = '0.0.0.0'
119 self.ip_addr = '0.0.0.0'
112
120
113 def _handle_request(self, environ, start_response):
121 def _handle_request(self, environ, start_response):
114 raise NotImplementedError()
122 raise NotImplementedError()
115
123
116 def _get_by_id(self, repo_name):
124 def _get_by_id(self, repo_name):
117 """
125 """
118 Get's a special pattern _<ID> from clone url and tries to replace it
126 Get's a special pattern _<ID> from clone url and tries to replace it
119 with a repository_name for support of _<ID> non changable urls
127 with a repository_name for support of _<ID> non changable urls
120
128
121 :param repo_name:
129 :param repo_name:
122 """
130 """
123 try:
131 try:
124 data = repo_name.split('/')
132 data = repo_name.split('/')
125 if len(data) >= 2:
133 if len(data) >= 2:
126 by_id = data[1].split('_')
134 by_id = data[1].split('_')
127 if len(by_id) == 2 and by_id[1].isdigit():
135 if len(by_id) == 2 and by_id[1].isdigit():
128 _repo_name = Repository.get(by_id[1]).repo_name
136 _repo_name = Repository.get(by_id[1]).repo_name
129 data[1] = _repo_name
137 data[1] = _repo_name
130 except Exception:
138 except Exception:
131 log.debug('Failed to extract repo_name from id %s' % (
139 log.debug('Failed to extract repo_name from id %s' % (
132 traceback.format_exc()
140 traceback.format_exc()
133 )
141 )
134 )
142 )
135
143
136 return '/'.join(data)
144 return '/'.join(data)
137
145
138 def _invalidate_cache(self, repo_name):
146 def _invalidate_cache(self, repo_name):
139 """
147 """
140 Set's cache for this repository for invalidation on next access
148 Set's cache for this repository for invalidation on next access
141
149
142 :param repo_name: full repo name, also a cache key
150 :param repo_name: full repo name, also a cache key
143 """
151 """
144 invalidate_cache('get_repo_cached_%s' % repo_name)
152 invalidate_cache('get_repo_cached_%s' % repo_name)
145
153
146 def _check_permission(self, action, user, repo_name, ip_addr=None):
154 def _check_permission(self, action, user, repo_name, ip_addr=None):
147 """
155 """
148 Checks permissions using action (push/pull) user and repository
156 Checks permissions using action (push/pull) user and repository
149 name
157 name
150
158
151 :param action: push or pull action
159 :param action: push or pull action
152 :param user: user instance
160 :param user: user instance
153 :param repo_name: repository name
161 :param repo_name: repository name
154 """
162 """
155 #check IP
163 #check IP
156 authuser = AuthUser(user_id=user.user_id, ip_addr=ip_addr)
164 authuser = AuthUser(user_id=user.user_id, ip_addr=ip_addr)
157 if not authuser.ip_allowed:
165 if not authuser.ip_allowed:
158 return False
166 return False
159 else:
167 else:
160 log.info('Access for IP:%s allowed' % (ip_addr))
168 log.info('Access for IP:%s allowed' % (ip_addr))
161 if action == 'push':
169 if action == 'push':
162 if not HasPermissionAnyMiddleware('repository.write',
170 if not HasPermissionAnyMiddleware('repository.write',
163 'repository.admin')(user,
171 'repository.admin')(user,
164 repo_name):
172 repo_name):
165 return False
173 return False
166
174
167 else:
175 else:
168 #any other action need at least read permission
176 #any other action need at least read permission
169 if not HasPermissionAnyMiddleware('repository.read',
177 if not HasPermissionAnyMiddleware('repository.read',
170 'repository.write',
178 'repository.write',
171 'repository.admin')(user,
179 'repository.admin')(user,
172 repo_name):
180 repo_name):
173 return False
181 return False
174
182
175 return True
183 return True
176
184
177 def _get_ip_addr(self, environ):
185 def _get_ip_addr(self, environ):
178 return _get_ip_addr(environ)
186 return _get_ip_addr(environ)
179
187
180 def _check_ssl(self, environ, start_response):
188 def _check_ssl(self, environ, start_response):
181 """
189 """
182 Checks the SSL check flag and returns False if SSL is not present
190 Checks the SSL check flag and returns False if SSL is not present
183 and required True otherwise
191 and required True otherwise
184 """
192 """
185 org_proto = environ['wsgi._org_proto']
193 org_proto = environ['wsgi._org_proto']
186 #check if we have SSL required ! if not it's a bad request !
194 #check if we have SSL required ! if not it's a bad request !
187 require_ssl = str2bool(RhodeCodeUi.get_by_key('push_ssl').ui_value)
195 require_ssl = str2bool(RhodeCodeUi.get_by_key('push_ssl').ui_value)
188 if require_ssl and org_proto == 'http':
196 if require_ssl and org_proto == 'http':
189 log.debug('proto is %s and SSL is required BAD REQUEST !'
197 log.debug('proto is %s and SSL is required BAD REQUEST !'
190 % org_proto)
198 % org_proto)
191 return False
199 return False
192 return True
200 return True
193
201
194 def _check_locking_state(self, environ, action, repo, user_id):
202 def _check_locking_state(self, environ, action, repo, user_id):
195 """
203 """
196 Checks locking on this repository, if locking is enabled and lock is
204 Checks locking on this repository, if locking is enabled and lock is
197 present returns a tuple of make_lock, locked, locked_by.
205 present returns a tuple of make_lock, locked, locked_by.
198 make_lock can have 3 states None (do nothing) True, make lock
206 make_lock can have 3 states None (do nothing) True, make lock
199 False release lock, This value is later propagated to hooks, which
207 False release lock, This value is later propagated to hooks, which
200 do the locking. Think about this as signals passed to hooks what to do.
208 do the locking. Think about this as signals passed to hooks what to do.
201
209
202 """
210 """
203 locked = False # defines that locked error should be thrown to user
211 locked = False # defines that locked error should be thrown to user
204 make_lock = None
212 make_lock = None
205 repo = Repository.get_by_repo_name(repo)
213 repo = Repository.get_by_repo_name(repo)
206 user = User.get(user_id)
214 user = User.get(user_id)
207
215
208 # this is kind of hacky, but due to how mercurial handles client-server
216 # this is kind of hacky, but due to how mercurial handles client-server
209 # server see all operation on changeset; bookmarks, phases and
217 # server see all operation on changeset; bookmarks, phases and
210 # obsolescence marker in different transaction, we don't want to check
218 # obsolescence marker in different transaction, we don't want to check
211 # locking on those
219 # locking on those
212 obsolete_call = environ['QUERY_STRING'] in ['cmd=listkeys',]
220 obsolete_call = environ['QUERY_STRING'] in ['cmd=listkeys',]
213 locked_by = repo.locked
221 locked_by = repo.locked
214 if repo and repo.enable_locking and not obsolete_call:
222 if repo and repo.enable_locking and not obsolete_call:
215 if action == 'push':
223 if action == 'push':
216 #check if it's already locked !, if it is compare users
224 #check if it's already locked !, if it is compare users
217 user_id, _date = repo.locked
225 user_id, _date = repo.locked
218 if user.user_id == user_id:
226 if user.user_id == user_id:
219 log.debug('Got push from user %s, now unlocking' % (user))
227 log.debug('Got push from user %s, now unlocking' % (user))
220 # unlock if we have push from user who locked
228 # unlock if we have push from user who locked
221 make_lock = False
229 make_lock = False
222 else:
230 else:
223 # we're not the same user who locked, ban with 423 !
231 # we're not the same user who locked, ban with 423 !
224 locked = True
232 locked = True
225 if action == 'pull':
233 if action == 'pull':
226 if repo.locked[0] and repo.locked[1]:
234 if repo.locked[0] and repo.locked[1]:
227 locked = True
235 locked = True
228 else:
236 else:
229 log.debug('Setting lock on repo %s by %s' % (repo, user))
237 log.debug('Setting lock on repo %s by %s' % (repo, user))
230 make_lock = True
238 make_lock = True
231
239
232 else:
240 else:
233 log.debug('Repository %s do not have locking enabled' % (repo))
241 log.debug('Repository %s do not have locking enabled' % (repo))
234 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s'
242 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s'
235 % (make_lock, locked, locked_by))
243 % (make_lock, locked, locked_by))
236 return make_lock, locked, locked_by
244 return make_lock, locked, locked_by
237
245
238 def __call__(self, environ, start_response):
246 def __call__(self, environ, start_response):
239 start = time.time()
247 start = time.time()
240 try:
248 try:
241 return self._handle_request(environ, start_response)
249 return self._handle_request(environ, start_response)
242 finally:
250 finally:
243 log = logging.getLogger('rhodecode.' + self.__class__.__name__)
251 log = logging.getLogger('rhodecode.' + self.__class__.__name__)
244 log.debug('Request time: %.3fs' % (time.time() - start))
252 log.debug('Request time: %.3fs' % (time.time() - start))
245 meta.Session.remove()
253 meta.Session.remove()
246
254
247
255
248 class BaseController(WSGIController):
256 class BaseController(WSGIController):
249
257
250 def __before__(self):
258 def __before__(self):
251 """
259 """
252 __before__ is called before controller methods and after __call__
260 __before__ is called before controller methods and after __call__
253 """
261 """
254 c.rhodecode_version = __version__
262 c.rhodecode_version = __version__
255 c.rhodecode_instanceid = config.get('instance_id')
263 c.rhodecode_instanceid = config.get('instance_id')
256 c.rhodecode_name = config.get('rhodecode_title')
264 c.rhodecode_name = config.get('rhodecode_title')
257 c.use_gravatar = str2bool(config.get('use_gravatar'))
265 c.use_gravatar = str2bool(config.get('use_gravatar'))
258 c.ga_code = config.get('rhodecode_ga_code')
266 c.ga_code = config.get('rhodecode_ga_code')
259 # Visual options
267 # Visual options
260 c.visual = AttributeDict({})
268 c.visual = AttributeDict({})
261 rc_config = RhodeCodeSetting.get_app_settings()
269 rc_config = RhodeCodeSetting.get_app_settings()
262
270
263 c.visual.show_public_icon = str2bool(rc_config.get('rhodecode_show_public_icon'))
271 c.visual.show_public_icon = str2bool(rc_config.get('rhodecode_show_public_icon'))
264 c.visual.show_private_icon = str2bool(rc_config.get('rhodecode_show_private_icon'))
272 c.visual.show_private_icon = str2bool(rc_config.get('rhodecode_show_private_icon'))
265 c.visual.stylify_metatags = str2bool(rc_config.get('rhodecode_stylify_metatags'))
273 c.visual.stylify_metatags = str2bool(rc_config.get('rhodecode_stylify_metatags'))
266 c.visual.lightweight_dashboard = str2bool(rc_config.get('rhodecode_lightweight_dashboard'))
274 c.visual.lightweight_dashboard = str2bool(rc_config.get('rhodecode_lightweight_dashboard'))
267 c.visual.lightweight_dashboard_items = safe_int(config.get('dashboard_items', 100))
275 c.visual.lightweight_dashboard_items = safe_int(config.get('dashboard_items', 100))
268 c.visual.repository_fields = str2bool(rc_config.get('rhodecode_repository_fields'))
276 c.visual.repository_fields = str2bool(rc_config.get('rhodecode_repository_fields'))
269 c.repo_name = get_repo_slug(request) # can be empty
277 c.repo_name = get_repo_slug(request) # can be empty
270 c.backends = BACKENDS.keys()
278 c.backends = BACKENDS.keys()
271 c.unread_notifications = NotificationModel()\
279 c.unread_notifications = NotificationModel()\
272 .get_unread_cnt_for_user(c.rhodecode_user.user_id)
280 .get_unread_cnt_for_user(c.rhodecode_user.user_id)
273 self.cut_off_limit = int(config.get('cut_off_limit'))
281 self.cut_off_limit = int(config.get('cut_off_limit'))
274
282
275 self.sa = meta.Session
283 self.sa = meta.Session
276 self.scm_model = ScmModel(self.sa)
284 self.scm_model = ScmModel(self.sa)
277
285
278 def __call__(self, environ, start_response):
286 def __call__(self, environ, start_response):
279 """Invoke the Controller"""
287 """Invoke the Controller"""
280 # WSGIController.__call__ dispatches to the Controller method
288 # WSGIController.__call__ dispatches to the Controller method
281 # the request is routed to. This routing information is
289 # the request is routed to. This routing information is
282 # available in environ['pylons.routes_dict']
290 # available in environ['pylons.routes_dict']
283 try:
291 try:
284 self.ip_addr = _get_ip_addr(environ)
292 self.ip_addr = _get_ip_addr(environ)
285 # make sure that we update permissions each time we call controller
293 # make sure that we update permissions each time we call controller
286 api_key = request.GET.get('api_key')
294 api_key = request.GET.get('api_key')
287 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
295 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
288 user_id = cookie_store.get('user_id', None)
296 user_id = cookie_store.get('user_id', None)
289 username = get_container_username(environ, config)
297 username = get_container_username(environ, config)
290 auth_user = AuthUser(user_id, api_key, username, self.ip_addr)
298 auth_user = AuthUser(user_id, api_key, username, self.ip_addr)
291 request.user = auth_user
299 request.user = auth_user
292 self.rhodecode_user = c.rhodecode_user = auth_user
300 self.rhodecode_user = c.rhodecode_user = auth_user
293 if not self.rhodecode_user.is_authenticated and \
301 if not self.rhodecode_user.is_authenticated and \
294 self.rhodecode_user.user_id is not None:
302 self.rhodecode_user.user_id is not None:
295 self.rhodecode_user.set_authenticated(
303 self.rhodecode_user.set_authenticated(
296 cookie_store.get('is_authenticated')
304 cookie_store.get('is_authenticated')
297 )
305 )
298 log.info('IP: %s User: %s accessed %s' % (
306 log.info('IP: %s User: %s accessed %s' % (
299 self.ip_addr, auth_user, safe_unicode(_get_access_path(environ)))
307 self.ip_addr, auth_user, safe_unicode(_get_access_path(environ)))
300 )
308 )
301 return WSGIController.__call__(self, environ, start_response)
309 return WSGIController.__call__(self, environ, start_response)
302 finally:
310 finally:
303 meta.Session.remove()
311 meta.Session.remove()
304
312
305
313
306 class BaseRepoController(BaseController):
314 class BaseRepoController(BaseController):
307 """
315 """
308 Base class for controllers responsible for loading all needed data for
316 Base class for controllers responsible for loading all needed data for
309 repository loaded items are
317 repository loaded items are
310
318
311 c.rhodecode_repo: instance of scm repository
319 c.rhodecode_repo: instance of scm repository
312 c.rhodecode_db_repo: instance of db
320 c.rhodecode_db_repo: instance of db
313 c.repository_followers: number of followers
321 c.repository_followers: number of followers
314 c.repository_forks: number of forks
322 c.repository_forks: number of forks
315 c.repository_following: weather the current user is following the current repo
323 c.repository_following: weather the current user is following the current repo
316 """
324 """
317
325
318 def __before__(self):
326 def __before__(self):
319 super(BaseRepoController, self).__before__()
327 super(BaseRepoController, self).__before__()
320 if c.repo_name:
328 if c.repo_name:
321
329
322 dbr = c.rhodecode_db_repo = Repository.get_by_repo_name(c.repo_name)
330 dbr = c.rhodecode_db_repo = Repository.get_by_repo_name(c.repo_name)
323 c.rhodecode_repo = c.rhodecode_db_repo.scm_instance
331 c.rhodecode_repo = c.rhodecode_db_repo.scm_instance
324 # update last change according to VCS data
332 # update last change according to VCS data
325 dbr.update_changeset_cache(dbr.get_changeset())
333 dbr.update_changeset_cache(dbr.get_changeset())
326 if c.rhodecode_repo is None:
334 if c.rhodecode_repo is None:
327 log.error('%s this repository is present in database but it '
335 log.error('%s this repository is present in database but it '
328 'cannot be created as an scm instance', c.repo_name)
336 'cannot be created as an scm instance', c.repo_name)
329
337
330 redirect(url('home'))
338 redirect(url('home'))
331
339
332 # some globals counter for menu
340 # some globals counter for menu
333 c.repository_followers = self.scm_model.get_followers(dbr)
341 c.repository_followers = self.scm_model.get_followers(dbr)
334 c.repository_forks = self.scm_model.get_forks(dbr)
342 c.repository_forks = self.scm_model.get_forks(dbr)
335 c.repository_pull_requests = self.scm_model.get_pull_requests(dbr)
343 c.repository_pull_requests = self.scm_model.get_pull_requests(dbr)
336 c.repository_following = self.scm_model.is_following_repo(c.repo_name,
344 c.repository_following = self.scm_model.is_following_repo(c.repo_name,
337 self.rhodecode_user.user_id)
345 self.rhodecode_user.user_id)
General Comments 0
You need to be logged in to leave comments. Login now