1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 import time
19 import socket
20 import re
21 from urlparse import urlparse, urlunparse
22
23
24 from rhn.UserDictCase import UserDictCase
25 from spacewalk.common.rhnLib import parseUrl
26 from spacewalk.common.rhnConfig import CFG
27 from spacewalk.common.rhnLog import log_debug, log_error
28 from spacewalk.common.rhnException import rhnFault
29 from spacewalk.common import rhnFlags, apache
30 from spacewalk.common.rhnTranslate import _
31
32
33 from proxy.rhnShared import SharedHandler
34 from proxy.rhnConstants import URI_PREFIX_KS_CHECKSUM
35 import rhnRepository
36 import proxy.rhnProxyAuth
37
38
39
40 _PROXY_VERSION = '5.5.0'
47
48 """ Spacewalk Proxy broker specific handler code called by rhnApache.
49
50 Workflow is:
51 Client -> Apache:Broker -> Squid -> Apache:Redirect -> Satellite
52
53 Broker handler get request from clients from outside. Some request
54 (POST and HEAD) bypass cache so, it is passed directly to parent.
55 For everything else we transform destination to localhost:80 (which
56 is handled by Redirect handler) and set proxy as local squid.
57 This way we got all request cached localy by squid.
58 """
59
60
62 SharedHandler.__init__(self, req)
63
64
65 self.componentType = 'proxy.broker'
66 self.cachedClientInfo = None
67 self.authChannels = None
68 self.clientServerId = None
69 self.rhnParentXMLRPC = None
70 hostname = ''
71
72 my_ip_addr = req.headers_in['SERVER_ADDR']
73 if req.headers_in.has_key('Host'):
74
75 try:
76
77
78
79
80
81 host_header = req.headers_in['Host'].split(':')[0]
82 if socket.gethostbyname(host_header) == my_ip_addr:
83
84
85
86 hostname = host_header
87 except (socket.gaierror, socket.error,
88 socket.herror, socket.timeout):
89
90 pass
91 if not hostname:
92
93
94 try:
95 hostname = socket.gethostbyaddr(my_ip_addr)[0]
96 except (socket.gaierror, socket.error,
97 socket.herror, socket.timeout):
98
99 pass
100 if not hostname:
101
102
103 hostname = socket.gethostname()
104 log_debug(-1, 'WARNING: no hostname in the incoming headers; '
105 'punting: %s' % hostname)
106 hostname = parseUrl(hostname)[1].split(':')[0]
107 self.proxyAuth = proxy.rhnProxyAuth.get_proxy_auth(hostname)
108
109 self._initConnectionVariables(req)
110
112 """ set connection variables
113 NOTE: self.{caChain,rhnParent,httpProxy*} are initialized
114 in SharedHandler
115
116 rules:
117 - GET requests:
118 . are non-SSLed (potentially SSLed by the redirect)
119 . use the local cache
120 . CFG.HTTP_PROXY or CFG.USE_SSL:
121 . use the SSL Redirect
122 (i.e., parent is now 127.0.0.1)
123 . NOTE: the reason we use the SSL Redirect if we
124 are going through an outside HTTP_PROXY:
125 o CFG.HTTP_PROXY is ONLY used by an SSL
126 redirect - maybe should rethink that.
127 . not CFG.USE_SSL and not CFG.HTTP_PROXY:
128 . bypass the SSL Redirect (performance)
129 - POST and HEAD requests (not GET) bypass both the local cache
130 and SSL redirect (we SSL it directly)
131 """
132
133 scheme = 'http'
134
135 effectiveURI = self._getEffectiveURI()
136 effectiveURI_parts = urlparse(effectiveURI)
137
138
139
140 if not effectiveURI_parts.scheme and effectiveURI_parts.netloc and effectiveURI_parts.netloc == 'XMLRPC':
141 effectiveURI_parts = urlparse(urlunparse([
142 '',
143 '',
144 '/' + effectiveURI_parts.netloc + effectiveURI_parts.path,
145 effectiveURI_parts.params,
146 effectiveURI_parts.query,
147 effectiveURI_parts.fragment]))
148
149 if req.method == 'GET':
150 scheme = 'http'
151 self.httpProxy = CFG.SQUID
152 self.caChain = self.httpProxyUsername = self.httpProxyPassword = ''
153 if CFG.HTTP_PROXY or CFG.USE_SSL or re.search('^' + URI_PREFIX_KS_CHECKSUM, effectiveURI_parts[2]):
154
155
156
157
158
159 self.rhnParent = self.proxyAuth.hostname
160 else:
161
162 if CFG.USE_SSL:
163 scheme = 'https'
164 else:
165 scheme = 'http'
166 self.caChain = ''
167
168 self.rhnParentXMLRPC = urlunparse((scheme, self.rhnParent, '/XMLRPC', '', '', ''))
169 self.rhnParent = urlunparse((scheme, self.rhnParent) + effectiveURI_parts[2:])
170
171 log_debug(2, 'set self.rhnParent: %s' % self.rhnParent)
172 log_debug(2, 'set self.rhnParentXMLRPC: %s' % self.rhnParentXMLRPC)
173 if self.httpProxy:
174 if self.httpProxyUsername and self.httpProxyPassword:
175 log_debug(2, 'using self.httpProxy: %s (authenticating)' % self.httpProxy)
176 else:
177 log_debug(2, 'using self.httpProxy: %s (non-authenticating)' % self.httpProxy)
178 else:
179 log_debug(2, '*not* using an http proxy')
180
182 """ Main handler to handle all requests pumped through this server. """
183
184
185 log_debug(1)
186 self._prepHandler()
187
188 _oto = rhnFlags.get('outputTransportOptions')
189
190
191 _oto['X-RHN-Transport-Capability'] = "follow-redirects=3"
192
193
194
195
196
197 ip_path = None
198 if 'X-RHN-IP-Path' in _oto:
199 ip_path = _oto['X-RHN-IP-Path']
200 log_debug(4, "X-RHN-IP-Path is: %s" % repr(ip_path))
201 client_ip = self.req.connection.remote_ip
202 if ip_path is None:
203 ip_path = client_ip
204 else:
205 ip_path += ',' + client_ip
206 _oto['X-RHN-IP-Path'] = ip_path
207
208
209 if 'X-RHN-Proxy-Auth' in _oto:
210 log_debug(5, 'X-RHN-Proxy-Auth currently set to: %s' % repr(_oto['X-RHN-Proxy-Auth']))
211 else:
212 log_debug(5, 'X-RHN-Proxy-Auth is not set')
213
214 if self.req.headers_in.has_key('X-RHN-Proxy-Auth'):
215 tokens = []
216 if 'X-RHN-Proxy-Auth' in _oto:
217 tokens = _oto['X-RHN-Proxy-Auth'].split(',')
218 log_debug(5, 'Tokens: %s' % tokens)
219
220
221 getResult = self.__local_GET_handler(self.req)
222 if getResult is not None:
223
224 return getResult
225
226
227
228
229
230
231 authToken = self.proxyAuth.check_cached_token()
232 log_debug(5, 'Auth token for this machine only! %s' % authToken)
233 tokens = []
234
235 _oto = rhnFlags.get('outputTransportOptions')
236 if _oto.has_key('X-RHN-Proxy-Auth'):
237 log_debug(5, ' (auth token prior): %s' % repr(_oto['X-RHN-Proxy-Auth']))
238 tokens = _oto['X-RHN-Proxy-Auth'].split(',')
239
240
241 tokens.append(authToken)
242 tokens = [t for t in tokens if t]
243
244 _oto['X-RHN-Proxy-Auth'] = ','.join(tokens)
245 log_debug(5, ' (auth token after): %s'
246 % repr(_oto['X-RHN-Proxy-Auth']))
247
248 log_debug(3, 'Trying to connect to parent')
249
250
251
252
253
254 for _i in range(2):
255 self._connectToParent()
256
257 log_debug(4, 'after _connectToParent')
258
259 rhnFlags.get('outputTransportOptions')['X-RHN-Proxy-Version'] = str(_PROXY_VERSION)
260
261 status = self._serverCommo()
262
263
264 respHeaders = self.responseContext.getHeaders()
265 if not respHeaders or \
266 not respHeaders.has_key('X-RHN-Proxy-Auth-Error'):
267
268
269
270 break
271
272 error = str(respHeaders['X-RHN-Proxy-Auth-Error']).split(':')[0]
273
274
275
276 if (respHeaders.has_key('X-RHN-Proxy-Auth-Origin') and
277 respHeaders['X-RHN-Proxy-Auth-Origin'] != self.proxyAuth.hostname):
278 break
279
280
281 if error == '1003':
282 msg = "Spacewalk Proxy Session Token INVALID -- bad!"
283 log_error(msg)
284 log_debug(0, msg)
285 elif error == '1004':
286 log_debug(1,
287 "Spacewalk Proxy Session Token expired, acquiring new one.")
288 else:
289 msg = "Spacewalk Proxy login failed, error code is %s" % error
290 log_error(msg)
291 log_debug(0, msg)
292 raise rhnFault(1000,
293 _("Spacewalk Proxy error (issues with proxy login). "
294 "Please contact your system administrator."))
295
296
297 rhnFlags.get('outputTransportOptions')['X-RHN-Proxy-Auth'] = self.proxyAuth.check_cached_token(1)
298 else:
299
300 log_debug(0, "Unable to acquire proxy authentication token")
301 raise rhnFault(1000,
302 _("Spacewalk Proxy error (unable to acquire proxy auth token). "
303 "Please contact your system administrator."))
304
305
306 if (status != apache.OK) and (status != apache.HTTP_PARTIAL_CONTENT):
307 log_debug(1, "Leaving handler with status code %s" % status)
308 return status
309
310 self.__handleAction(self.responseContext.getHeaders())
311
312 return self._clientCommo()
313
317
318 @staticmethod
320 """ read kickstart options from incoming url
321 URIs we care about look something like:
322 /ks/dist/session/2xfe7113bc89f359001280dee1f4a020bc/
323 ks-rhel-x86_64-server-6-6.5/Packages/rhnsd-4.9.3-2.el6.x86_64.rpm
324 /ks/dist/ks-rhel-x86_64-server-6-6.5/Packages/
325 rhnsd-4.9.3-2.el6.x86_64.rpm
326 /ks/dist/org/1/ks-rhel-x86_64-server-6-6.5/Packages/
327 rhnsd-4.9.3-2.el6.x86_64.rpm
328 /ks/dist/ks-rhel-x86_64-server-6-6.5/child/sherr-child-1/Packages/
329 rhnsd-4.9.3-2.el6.x86_64.rpm
330 """
331 args = req.path_info.split('/')
332 params = {'child': None, 'session': None, 'orgId': None,
333 'file': args[-1]}
334 action = None
335 if args[2] == 'org':
336 params['orgId'] = args[3]
337 kickstart = args[4]
338 if args[5] == 'Packages':
339 action = 'getPackage'
340 elif args[2] == 'session':
341 params['session'] = args[3]
342 kickstart = args[4]
343 if args[5] == 'Packages':
344 action = 'getPackage'
345 elif args[3] == 'child':
346 params['child'] = args[4]
347 kickstart = args[2]
348 if args[5] == 'Packages':
349 action = 'getPackage'
350 else:
351 kickstart = args[2]
352 if args[3] == 'Packages':
353 action = 'getPackage'
354 return kickstart, action, params
355
356 @staticmethod
358 """ read url from incoming url and return (req_type, channel, action, params)
359 URI should look something like:
360 /GET-REQ/rhel-i386-server-5/getPackage/autofs-5.0.1-0.rc2.143.el5_5.6.i386.rpm
361 """
362 args = req.path_info.split('/')
363 if len(args) < 5:
364 return (None, None, None, None)
365
366 return (args[1], args[2], args[3], args[4:])
367
368
369
371 log_debug(1)
372
373
374 if not headers.has_key('X-RHN-Action'):
375
376 return
377
378 log_debug(2, "Action is %s" % headers['X-RHN-Action'])
379
380 if headers['X-RHN-Action'] != 'login':
381
382 return
383
384
385 self.__cacheClientSessionToken(headers)
386
388 """ GETs: authenticate user, and service local GETs.
389 if not a local fetch, return None
390 """
391
392 log_debug(2, 'request method: %s' % req.method)
393
394
395 if req.method != "GET" or not CFG.PKG_DIR:
396
397 return None
398
399
400
401
402
403
404
405
406
407 args = req.path_info.split('/')
408
409 if re.search('^' + URI_PREFIX_KS_CHECKSUM, urlparse(self.rhnParent)[2]):
410
411 if len(args) < 3 or args[2] != 'Packages':
412 return None
413 req_type = 'tinyurl'
414 reqident = args[1]
415 reqaction = 'getPackage'
416 reqparams = [args[-1]]
417 self.cachedClientInfo = UserDictCase()
418 elif (len(args) > 3 and args[1] == 'dist'):
419
420 req_type = 'ks-dist'
421 reqident, reqaction, reqparams = self._split_ks_url(req)
422 self.cachedClientInfo = UserDictCase()
423 else:
424
425 (req_type, reqident, reqaction, reqparams) = self._split_url(req)
426
427 if req_type is None or (req_type not in
428 ['$RHN', 'GET-REQ', 'tinyurl', 'ks-dist']):
429
430
431 return None
432
433
434 if req_type in ['$RHN', 'GET-REQ']:
435
436
437
438 token = self.__getSessionToken()
439 self.__checkAuthSessionTokenCache(token, reqident)
440
441
442 for ch in self.authChannels:
443 channel, _version, _isBaseChannel, isLocalChannel = ch[:4]
444 if channel == reqident and str(isLocalChannel) == '1':
445
446 break
447 else:
448
449 return None
450
451
452 localFlist = CFG.PROXY_LOCAL_FLIST or []
453
454 if reqaction not in localFlist:
455
456 return None
457
458
459
460 log_debug(3, "Retrieve from local repository.")
461 log_debug(3, req_type, reqident, reqaction, reqparams)
462 result = self.__callLocalRepository(req_type, reqident, reqaction,
463 reqparams)
464 if result is None:
465 log_debug(3, "Not available locally; will try higher up the chain.")
466 else:
467
468 rhnFlags.set("NeedEncoding", 1)
469
470 return result
471
472 @staticmethod
474 """ Get/test-for session token in headers (rhnFlags) """
475 log_debug(1)
476 if not rhnFlags.test("AUTH_SESSION_TOKEN"):
477 raise rhnFault(33, "Missing session token")
478 return rhnFlags.get("AUTH_SESSION_TOKEN")
479
481 """pull session token from headers and push to caching daemon. """
482
483 log_debug(1)
484
485 if not headers.has_key('X-RHN-Server-ID'):
486 log_debug(3, "Client server ID not found in headers")
487
488
489 return None
490 serverId = 'X-RHN-Server-ID'
491
492 self.clientServerId = headers[serverId]
493 token = UserDictCase()
494
495
496
497 prefix = "x-rhn-auth"
498 l = len(prefix)
499 tokenKeys = [x for x in headers.keys() if x[:l].lower() == prefix]
500 for k in tokenKeys:
501 if k.lower() == 'x-rhn-auth-channels':
502
503
504 values = self._get_header(k)
505 token[k] = [x.split(':') for x in values]
506 else:
507
508 token[k] = headers[k]
509
510
511 serverTime = float(token['X-RHN-Auth-Server-Time'])
512 token["X-RHN-Auth-Proxy-Clock-Skew"] = time.time() - serverTime
513
514
515 self.proxyAuth.set_client_token(self.clientServerId, token)
516 return token
517
519 """ Contacts the local repository and retrieves files"""
520
521 log_debug(2, req_type, identifier, funct, params)
522
523
524 if rhnFlags.get('outputTransportOptions').has_key('X-RHN-Proxy-Auth'):
525 self.cachedClientInfo['X-RHN-Proxy-Auth'] = rhnFlags.get('outputTransportOptions')['X-RHN-Proxy-Auth']
526 if rhnFlags.get('outputTransportOptions').has_key('Host'):
527 self.cachedClientInfo['Host'] = rhnFlags.get('outputTransportOptions')['Host']
528
529 if req_type == 'tinyurl':
530 try:
531 rep = rhnRepository.TinyUrlRepository(identifier,
532 self.cachedClientInfo, rhnParent=self.rhnParent,
533 rhnParentXMLRPC=self.rhnParentXMLRPC,
534 httpProxy=self.httpProxy,
535 httpProxyUsername=self.httpProxyUsername,
536 httpProxyPassword=self.httpProxyPassword,
537 caChain=self.caChain,
538 systemId=self.proxyAuth.get_system_id())
539 except rhnRepository.NotLocalError:
540 return None
541 elif req_type == 'ks-dist':
542 try:
543 rep = rhnRepository.KickstartRepository(identifier,
544 self.cachedClientInfo, rhnParent=self.rhnParent,
545 rhnParentXMLRPC=self.rhnParentXMLRPC,
546 httpProxy=self.httpProxy,
547 httpProxyUsername=self.httpProxyUsername,
548 httpProxyPassword=self.httpProxyPassword,
549 caChain=self.caChain, orgId=params['orgId'],
550 child=params['child'], session=params['session'],
551 systemId=self.proxyAuth.get_system_id())
552 except rhnRepository.NotLocalError:
553 return None
554 params = [params['file']]
555 else:
556
557 version = None
558 for c in self.authChannels:
559 ch, ver = c[:2]
560 if ch == identifier:
561 version = ver
562 break
563
564
565
566 rep = rhnRepository.Repository(identifier, version,
567 self.cachedClientInfo, rhnParent=self.rhnParent,
568 rhnParentXMLRPC=self.rhnParentXMLRPC,
569 httpProxy=self.httpProxy,
570 httpProxyUsername=self.httpProxyUsername,
571 httpProxyPassword=self.httpProxyPassword,
572 caChain=self.caChain)
573
574 f = rep.get_function(funct)
575 if not f:
576 raise rhnFault(1000,
577 _("Spacewalk Proxy configuration error: invalid function %s") % funct)
578
579 log_debug(3, "Calling %s(%s)" % (funct, params))
580 if params is None:
581 params = ()
582 try:
583 ret = f(*params)
584 except rhnRepository.NotLocalError:
585
586 return None
587
588 return ret
589
591 """ Authentication / authorize the channel """
592
593 log_debug(2, token, channel)
594
595 self.clientServerId = token['X-RHN-Server-ID'].split("/")[-1]
596
597 cachedToken = self.proxyAuth.get_client_token(self.clientServerId)
598 if not cachedToken:
599
600
601 cachedToken = self.proxyAuth.update_client_token_if_valid(
602 self.clientServerId, token)
603
604 if not cachedToken:
605 msg = _("Invalid session key - server ID not found in cache: %s") \
606 % self.clientServerId
607 log_error(msg)
608 raise rhnFault(33, msg)
609
610 self.cachedClientInfo = UserDictCase(cachedToken)
611
612 clockSkew = self.cachedClientInfo["X-RHN-Auth-Proxy-Clock-Skew"]
613 del self.cachedClientInfo["X-RHN-Auth-Proxy-Clock-Skew"]
614
615
616 self.authChannels = self.cachedClientInfo['X-RHN-Auth-Channels']
617 del self.cachedClientInfo['X-RHN-Auth-Channels']
618 self.cachedClientInfo['X-RHN-Server-ID'] = self.clientServerId
619 log_debug(4, 'Retrieved token from cache: %s' % self.cachedClientInfo)
620
621
622 if not _dictEquals(token, self.cachedClientInfo,
623 ['X-RHN-Auth-Channels']):
624
625
626 updatedToken = self.proxyAuth.update_client_token_if_valid(
627 self.clientServerId, token)
628
629 if updatedToken:
630 self.cachedClientInfo = UserDictCase(updatedToken)
631 clockSkew = self.cachedClientInfo[
632 "X-RHN-Auth-Proxy-Clock-Skew"]
633 del self.cachedClientInfo["X-RHN-Auth-Proxy-Clock-Skew"]
634 self.authChannels = self.cachedClientInfo[
635 'X-RHN-Auth-Channels']
636 del self.cachedClientInfo['X-RHN-Auth-Channels']
637 self.cachedClientInfo['X-RHN-Server-ID'] = \
638 self.clientServerId
639 log_debug(4, 'Retrieved token from cache: %s' %
640 self.cachedClientInfo)
641
642 if not updatedToken or not _dictEquals(
643 token, self.cachedClientInfo, ['X-RHN-Auth-Channels']):
644 log_debug(3, "Session tokens different")
645 raise rhnFault(33)
646
647
648 serverTime = float(token['X-RHN-Auth-Server-Time'])
649 offset = float(token['X-RHN-Auth-Expire-Offset'])
650 if time.time() > serverTime + offset + clockSkew:
651 log_debug(3, "Session token has expired")
652 raise rhnFault(34)
653
654
655 authChannels = [x[0] for x in self.authChannels]
656 log_debug(4, "Auth channels: '%s'" % authChannels)
657
658 if channel not in authChannels:
659 log_debug(4, "Not subscribed to channel %s; unauthorized" %
660 channel)
661 raise rhnFault(35, _('Unauthorized channel access requested.'))
662
665 """ Function that compare two dictionaries, ignoring certain keys """
666 exceptions = [x.lower() for x in (exceptions or [])]
667 for k, v in d1.items():
668 if k.lower() in exceptions:
669 continue
670 if not d2.has_key(k) or d2[k] != v:
671 return 0
672 for k, v in d2.items():
673 if k.lower() in exceptions:
674 continue
675 if not d1.has_key(k) or d1[k] != v:
676 return 0
677 return 1
678
679
680
681