class UPHandler(WebHandler): __tc = ThreadConfig() def prepare(self): if not self.isRegisteredUser(): raise WErr(401, "Not a registered user") self.set_header("Pragma", "no-cache") self.set_header("Cache-Control", "max-age=0, no-store, no-cache, must-revalidate") @asyncGen def web_saveAppState(self): self.__tc.setSetup(False) try: app = self.request.arguments['app'][-1] name = self.request.arguments['name'][-1] state = self.request.arguments['state'][-1] except KeyError as excp: raise WErr(400, "Missing %s" % excp) data = base64.b64encode(zlib.compress(DEncode.encode(state), 9)) up = UserProfileClient("Web/App/%s" % app) result = yield self.threadTask(up.storeVar, name, data) if not result['OK']: raise WErr.fromSERROR(result) self.set_status(200) self.finish() @asyncGen def web_loadAppState(self): self.__tc.setSetup(False) try: app = self.request.arguments['app'][-1] name = self.request.arguments['name'][-1] except KeyError as excp: raise WErr(400, "Missing %s" % excp) up = UserProfileClient("Web/App/%s" % app) result = yield self.threadTask(up.retrieveVar, name) if not result['OK']: raise WErr.fromSERROR(result) data = result['Value'] data, count = DEncode.decode(zlib.decompress(base64.b64decode(data))) self.set_header("Content-Type", "application/json") self.finish(data) @asyncGen def web_listAppState(self): self.__tc.setSetup(False) try: app = self.request.arguments['app'][-1] except KeyError as excp: raise WErr(400, "Missing %s" % excp) up = UserProfileClient("Web/App/%s" % app) result = yield self.threadTask(up.listAvailableVars, {'UserName': [self.getUserName()]}) if not result['OK']: raise WErr.fromSERROR(result) data = result['Value'] self.finish({'app': [e[-1] for e in data]})
class WebHandler(tornado.web.RequestHandler): __threadPool = getGlobalThreadPool() __disetConfig = ThreadConfig() __log = False #Auth requirements AUTH_PROPS = None #Location of the handler in the URL LOCATION = "" #URL Schema with holders to generate handler urls URLSCHEMA = "" #RE to extract group and setup PATH_RE = "" #Helper function to create threaded gen.Tasks with automatic callback and execption handling @classmethod def threadTask(cls, method, *args, **kwargs): """ Helper method to generate a gen.Task and automatically call the callback when the real method ends. THIS IS SPARTAAAAAAAAAA """ #Save the task to access the runner genTask = False #This runs in the separate thread, calls the callback on finish and takes into account exceptions def cbMethod(*cargs, **ckwargs): cb = ckwargs.pop('callback') method = cargs[0] disetConf = cargs[1] cargs = cargs[2] cls.__disetConfig.load(disetConf) ioloop = tornado.ioloop.IOLoop.instance() try: result = method(*cargs, **ckwargs) ioloop.add_callback(functools.partial(cb, result)) except Exception, excp: exc_info = sys.exc_info() ioloop.add_callback( lambda: genTask.runner.handle_exception(*exc_info)) #Put the task in the thread :) def threadJob(tmethod, *targs, **tkwargs): tkwargs['callback'] = tornado.stack_context.wrap( tkwargs['callback']) targs = (tmethod, cls.__disetConfig.dump(), targs) cls.__threadPool.generateJobAndQueueIt(cbMethod, args=targs, kwargs=tkwargs) #Return a YieldPoint genTask = tornado.gen.Task(threadJob, method, *args, **kwargs) return genTask
class BaseClient: VAL_EXTRA_CREDENTIALS_HOST = "hosts" KW_USE_CERTIFICATES = "useCertificates" KW_EXTRA_CREDENTIALS = "extraCredentials" KW_TIMEOUT = "timeout" KW_SETUP = "setup" KW_VO = "VO" KW_DELEGATED_DN = "delegatedDN" KW_DELEGATED_GROUP = "delegatedGroup" KW_IGNORE_GATEWAYS = "ignoreGateways" KW_PROXY_LOCATION = "proxyLocation" KW_PROXY_STRING = "proxyString" KW_PROXY_CHAIN = "proxyChain" KW_SKIP_CA_CHECK = "skipCACheck" KW_KEEP_ALIVE_LAPSE = "keepAliveLapse" __threadConfig = ThreadConfig() def __init__( self, serviceName, **kwargs ): if type( serviceName ) not in types.StringTypes: raise TypeError( "Service name expected to be a string. Received %s type %s" % ( str( serviceName ), type( serviceName ) ) ) self._destinationSrv = serviceName self._serviceName = serviceName self.kwargs = kwargs self.__initStatus = S_OK() self.__idDict = {} self.__extraCredentials = "" self.__enableThreadCheck = False self.__retry = 0 self.__retryDelay = 0 self.__nbOfUrls = 1 #by default we always have 1 url for example: RPCClient('dips://volhcb38.cern.ch:9162/Framework/SystemAdministrator') self.__nbOfRetry = 3 # by default we try try times self.__bannedUrls = [] for initFunc in ( self.__discoverSetup, self.__discoverVO, self.__discoverTimeout, self.__discoverURL, self.__discoverCredentialsToUse, self.__checkTransportSanity, self.__setKeepAliveLapse ): result = initFunc() if not result[ 'OK' ] and self.__initStatus[ 'OK' ]: self.__initStatus = result self._initialize() #HACK for thread-safety: self.__allowedThreadID = False def _initialize( self ): pass def getDestinationService( self ): return self._destinationSrv def getServiceName( self ): return self._serviceName def __discoverSetup( self ): #Which setup to use? if self.KW_SETUP in self.kwargs and self.kwargs[ self.KW_SETUP ]: self.setup = str( self.kwargs[ self.KW_SETUP ] ) else: self.setup = self.__threadConfig.getSetup() if not self.setup: self.setup = gConfig.getValue( "/DIRAC/Setup", "Test" ) return S_OK() def __discoverVO( self ): #Which setup to use? if self.KW_VO in self.kwargs and self.kwargs[ self.KW_VO ]: self.vo = str( self.kwargs[ self.KW_VO ] ) else: self.vo = gConfig.getValue( "/DIRAC/VirtualOrganization", "unknown" ) return S_OK() def __discoverURL( self ): #Calculate final URL try: result = self.__findServiceURL() except Exception, e: return S_ERROR( str( e ) ) if not result[ 'OK' ]: return result self.serviceURL = result[ 'Value' ] retVal = Network.splitURL( self.serviceURL ) if not retVal[ 'OK' ]: return S_ERROR( "URL is malformed: %s" % retVal[ 'Message' ] ) self.__URLTuple = retVal[ 'Value' ] self._serviceName = self.__URLTuple[-1] res = gConfig.getOptionsDict( "/DIRAC/ConnConf/%s:%s" % self.__URLTuple[1:3] ) if res[ 'OK' ]: opts = res[ 'Value' ] for k in opts: if k not in self.kwargs: self.kwargs[k] = opts[k] return S_OK()
class TornadoBaseClient(object): """ This class contain initialization method and all utilities method used for RPC """ __threadConfig = ThreadConfig() VAL_EXTRA_CREDENTIALS_HOST = "hosts" KW_USE_CERTIFICATES = "useCertificates" KW_EXTRA_CREDENTIALS = "extraCredentials" KW_TIMEOUT = "timeout" KW_SETUP = "setup" KW_VO = "VO" KW_DELEGATED_DN = "delegatedDN" KW_DELEGATED_GROUP = "delegatedGroup" KW_IGNORE_GATEWAYS = "ignoreGateways" KW_PROXY_LOCATION = "proxyLocation" KW_PROXY_STRING = "proxyString" KW_PROXY_CHAIN = "proxyChain" KW_SKIP_CA_CHECK = "skipCACheck" KW_KEEP_ALIVE_LAPSE = "keepAliveLapse" def __init__(self, serviceName, **kwargs): """ :param serviceName: URL of the service (proper uri or just System/Component) :param useCertificates: If set to True, use the server certificate :param extraCredentials: :param timeout: Timeout of the call (default 600 s) :param setup: Specify the Setup :param VO: Specify the VO :param delegatedDN: Not clear what it can be used for. :param delegatedGroup: Not clear what it can be used for. :param ignoreGateways: Ignore the DIRAC Gatways settings :param proxyLocation: Specify the location of the proxy :param proxyString: Specify the proxy string :param proxyChain: Specify the proxy chain :param skipCACheck: Do not check the CA :param keepAliveLapse: Duration for keepAliveLapse (heartbeat like) (now managed by requests) """ if not isinstance(serviceName, six.string_types): raise TypeError( "Service name expected to be a string. Received %s type %s" % (str(serviceName), type(serviceName)) ) self._destinationSrv = serviceName self._serviceName = serviceName self.__ca_location = False self.kwargs = kwargs self.__useCertificates = None # The CS useServerCertificate option can be overridden by explicit argument self.__forceUseCertificates = self.kwargs.get(self.KW_USE_CERTIFICATES) self.__initStatus = S_OK() self.__idDict = {} self.__extraCredentials = "" # by default we always have 1 url for example: # RPCClient('dips://volhcb38.cern.ch:9162/Framework/SystemAdministrator') self.__nbOfUrls = 1 self.__bannedUrls = [] # For pylint... self.setup = None self.vo = None self.serviceURL = None for initFunc in ( self.__discoverTimeout, self.__discoverSetup, self.__discoverVO, self.__discoverCredentialsToUse, self.__discoverExtraCredentials, self.__discoverURL, ): result = initFunc() if not result["OK"] and self.__initStatus["OK"]: self.__initStatus = result def __discoverSetup(self): """Discover which setup to use and stores it in self.setup The setup is looked for: * kwargs of the constructor (see KW_SETUP) * in the CS /DIRAC/Setup * default to 'Test' """ if self.KW_SETUP in self.kwargs and self.kwargs[self.KW_SETUP]: self.setup = str(self.kwargs[self.KW_SETUP]) else: self.setup = self.__threadConfig.getSetup() if not self.setup: self.setup = gConfig.getValue("/DIRAC/Setup", "Test") return S_OK() def __discoverURL(self): """Calculate the final URL. It is called at initialization and in connect in case of issue It sets: * self.serviceURL: the url (dips) selected as target using __findServiceURL * self.__URLTuple: a split of serviceURL obtained by Network.splitURL * self._serviceName: the last part of URLTuple (typically System/Component) WARNING: COPY PASTE FROM BaseClient """ # Calculate final URL try: result = self.__findServiceURL() except Exception as e: return S_ERROR(repr(e)) if not result["OK"]: return result self.serviceURL = result["Value"] retVal = Network.splitURL(self.serviceURL) if not retVal["OK"]: return retVal self.__URLTuple = retVal["Value"] self._serviceName = self.__URLTuple[-1] res = gConfig.getOptionsDict("/DIRAC/ConnConf/%s:%s" % self.__URLTuple[1:3]) if res["OK"]: opts = res["Value"] for k in opts: if k not in self.kwargs: self.kwargs[k] = opts[k] return S_OK() def __discoverVO(self): """Discover which VO to use and stores it in self.vo The VO is looked for: * kwargs of the constructor (see KW_VO) * in the CS /DIRAC/VirtualOrganization * default to 'unknown' WARNING: COPY/PASTE FROM Core/Diset/private/BaseClient FOR NOW """ if self.KW_VO in self.kwargs and self.kwargs[self.KW_VO]: self.vo = str(self.kwargs[self.KW_VO]) else: self.vo = gConfig.getValue("/DIRAC/VirtualOrganization", "unknown") return S_OK() def __discoverCredentialsToUse(self): """Discovers which credentials to use for connection. * Server certificate: -> If KW_USE_CERTIFICATES in kwargs, sets it in self.__useCertificates -> If not, check gConfig.useServerCertificate(), and sets it in self.__useCertificates and kwargs[KW_USE_CERTIFICATES] * Certification Authorities check: -> if KW_SKIP_CA_CHECK is not in kwargs and we are using the certificates, set KW_SKIP_CA_CHECK to false in kwargs -> if KW_SKIP_CA_CHECK is not in kwargs and we are not using the certificate, check the skipCACheck * Proxy Chain WARNING: MOSTLY COPY/PASTE FROM Core/Diset/private/BaseClient """ # Use certificates? if self.KW_USE_CERTIFICATES in self.kwargs: self.__useCertificates = self.kwargs[self.KW_USE_CERTIFICATES] else: self.__useCertificates = gConfig.useServerCertificate() self.kwargs[self.KW_USE_CERTIFICATES] = self.__useCertificates if self.KW_SKIP_CA_CHECK not in self.kwargs: if self.__useCertificates: self.kwargs[self.KW_SKIP_CA_CHECK] = False else: self.kwargs[self.KW_SKIP_CA_CHECK] = skipCACheck() # Rewrite a little bit from here: don't need the proxy string, we use the file if self.KW_PROXY_CHAIN in self.kwargs: try: self.kwargs[self.KW_PROXY_STRING] = self.kwargs[self.KW_PROXY_CHAIN].dumpAllToString()["Value"] del self.kwargs[self.KW_PROXY_CHAIN] except Exception: return S_ERROR("Invalid proxy chain specified on instantiation") # ==== REWRITED FROM HERE ==== # For certs always check CA's. For clients skipServerIdentityCheck return S_OK() def __discoverExtraCredentials(self): """Add extra credentials informations. * self.__extraCredentials -> if KW_EXTRA_CREDENTIALS in kwargs, we set it -> Otherwise, if we use the server certificate, we set it to VAL_EXTRA_CREDENTIALS_HOST -> If we have a delegation (see bellow), we set it to (delegatedDN, delegatedGroup) -> otherwise it is an empty string * delegation: -> if KW_DELEGATED_DN in kwargs, or delegatedDN in threadConfig, put in in self.kwargs -> If we have a delegated DN but not group, we find the corresponding group in the CS WARNING: COPY/PASTE FROM Core/Diset/private/BaseClient """ # which extra credentials to use? if self.__useCertificates: self.__extraCredentials = self.VAL_EXTRA_CREDENTIALS_HOST else: self.__extraCredentials = "" if self.KW_EXTRA_CREDENTIALS in self.kwargs: self.__extraCredentials = self.kwargs[self.KW_EXTRA_CREDENTIALS] # Are we delegating something? delegatedDN, delegatedGroup = self.__threadConfig.getID() if self.KW_DELEGATED_DN in self.kwargs and self.kwargs[self.KW_DELEGATED_DN]: delegatedDN = self.kwargs[self.KW_DELEGATED_DN] elif delegatedDN: self.kwargs[self.KW_DELEGATED_DN] = delegatedDN if self.KW_DELEGATED_GROUP in self.kwargs and self.kwargs[self.KW_DELEGATED_GROUP]: delegatedGroup = self.kwargs[self.KW_DELEGATED_GROUP] elif delegatedGroup: self.kwargs[self.KW_DELEGATED_GROUP] = delegatedGroup if delegatedDN: if not delegatedGroup: result = findDefaultGroupForDN(self.kwargs[self.KW_DELEGATED_DN]) if not result["OK"]: return result self.__extraCredentials = (delegatedDN, delegatedGroup) return S_OK() def __discoverTimeout(self): """Discover which timeout to use and stores it in self.timeout The timeout can be specified kwargs of the constructor (see KW_TIMEOUT), with a minimum of 120 seconds. If unspecified, the timeout will be 600 seconds. The value is set in self.timeout, as well as in self.kwargs[KW_TIMEOUT] WARNING: COPY/PASTE FROM Core/Diset/private/BaseClient """ if self.KW_TIMEOUT in self.kwargs: self.timeout = self.kwargs[self.KW_TIMEOUT] else: self.timeout = False if self.timeout: self.timeout = max(120, self.timeout) else: self.timeout = 600 self.kwargs[self.KW_TIMEOUT] = self.timeout return S_OK() def __findServiceURL(self): """ Discovers the URL of a service, taking into account gateways, multiple URLs, banned URLs If the site on which we run is configured to use gateways (/DIRAC/Gateways/<siteName>), these URLs will be used. To ignore the gateway, it is possible to set KW_IGNORE_GATEWAYS to False in kwargs. If self._destinationSrv (given as constructor attribute) is a properly formed URL, we just return this one. If we have to use a gateway, we just replace the server name in the url. The list of URLs defined in the CS (<System>/URLs/<Component>) is randomized This method also sets some attributes: * self.__nbOfUrls = number of URLs * self.__nbOfRetry removed in HTTPS (Managed by requests) * self.__bannedUrls is reinitialized if all the URLs are banned :return: the selected URL WARNING (Mostly) COPY PASTE FROM BaseClient (protocols list is changed to https) """ if not self.__initStatus["OK"]: return self.__initStatus # Load the Gateways URLs for the current site Name gatewayURL = False if not self.kwargs.get(self.KW_IGNORE_GATEWAYS): gatewayURLs = getGatewayURLs() if gatewayURLs: gatewayURL = "/".join(gatewayURLs[0].split("/")[:3]) # If what was given as constructor attribute is a properly formed URL, # we just return this one. # If we have to use a gateway, we just replace the server name in it if self._destinationSrv.startswith("https://"): gLogger.debug("Already given a valid url", self._destinationSrv) if not gatewayURL: return S_OK(self._destinationSrv) gLogger.debug("Reconstructing given URL to pass through gateway") path = "/".join(self._destinationSrv.split("/")[3:]) finalURL = "%s/%s" % (gatewayURL, path) gLogger.debug("Gateway URL conversion:\n %s -> %s" % (self._destinationSrv, finalURL)) return S_OK(finalURL) if gatewayURL: gLogger.debug("Using gateway", gatewayURL) return S_OK("%s/%s" % (gatewayURL, self._destinationSrv)) # If nor url is given as constructor, we extract the list of URLs from the CS (System/URLs/Component) try: # We randomize the list, and add at the end the failover URLs (System/FailoverURLs/Component) urlsList = getServiceURLs(self._destinationSrv, setup=self.setup, failover=True) except Exception as e: return S_ERROR("Cannot get URL for %s in setup %s: %s" % (self._destinationSrv, self.setup, repr(e))) if not urlsList: return S_ERROR("URL for service %s not found" % self._destinationSrv) self.__nbOfUrls = len(urlsList) # __nbOfRetry removed in HTTPS (managed by requests) if self.__nbOfUrls == len(self.__bannedUrls): self.__bannedUrls = [] # retry all urls gLogger.debug("Retrying again all URLs") if self.__bannedUrls and len(urlsList) > 1: # we have host which is not accessible. We remove that host from the list. # We only remove if we have more than one instance for i in self.__bannedUrls: gLogger.debug("Removing banned URL", "%s" % i) urlsList.remove(i) sURL = urlsList[0] # If we have banned URLs, and several URLs at disposals, we make sure that the selected sURL # is not on a host which is banned. If it is, we take the next one in the list using __selectUrl if self.__bannedUrls and self.__nbOfUrls > 2: # when we have multiple services then we can # have a situation when two services are running on the same machine with different ports... retVal = Network.splitURL(sURL) nexturl = None if retVal["OK"]: nexturl = retVal["Value"] found = False for i in self.__bannedUrls: retVal = Network.splitURL(i) if retVal["OK"]: bannedurl = retVal["Value"] else: break # We found a banned URL on the same host as the one we are running on if nexturl[1] == bannedurl[1]: found = True break if found: nexturl = self.__selectUrl(nexturl, urlsList[1:]) if nexturl: # an url found which is in different host sURL = nexturl gLogger.debug("Discovering URL for service", "%s -> %s" % (self._destinationSrv, sURL)) return S_OK(sURL) def __selectUrl(self, notselect, urls): """In case when multiple services are running in the same host, a new url has to be in a different host Note: If we do not have different host we will use the selected url... :param notselect: URL that should NOT be selected :param urls: list of potential URLs :return: selected URL WARNING: COPY/PASTE FROM Core/Diset/private/BaseClient """ url = None for i in urls: retVal = Network.splitURL(i) if retVal["OK"]: if retVal["Value"][1] != notselect[1]: # the hots are different url = i break else: gLogger.error(retVal["Message"]) return url def getServiceName(self): """ Returns the name of the service, if you had given a url at init, returns the URL. """ return self._serviceName def getDestinationService(self): """ Returns the url the service. """ urls = getServiceURLs(self._serviceName) return urls[0] if urls else "" def _getBaseStub(self): """Returns a tuple with (self._destinationSrv, newKwargs) self._destinationSrv is what was given as first parameter of the init serviceName newKwargs is an updated copy of kwargs: * if set, we remove the useCertificates (KW_USE_CERTIFICATES) in newKwargs This method is just used to return information in case of error in the InnerRPCClient WARNING: COPY/PASTE FROM Core/Diset/private/BaseClient """ newKwargs = dict(self.kwargs) # Remove useCertificates as the forwarder of the call will have to # independently decide whether to use their cert or not anyway. if "useCertificates" in newKwargs: del newKwargs["useCertificates"] return (self._destinationSrv, newKwargs) def _request(self, retry=0, outputFile=None, **kwargs): """ Sends the request to server :param retry: internal parameters for recursive call. TODO: remove ? :param outputFile: (default None) path to a file where to store the received data. If set, the server response will be streamed for optimization purposes, and the response data will not go through the JDecode process :param **kwargs: Any argument there is used as a post parameter. They are detailed bellow. :param method: (mandatory) name of the distant method :param args: (mandatory) json serialized list of argument for the procedure :returns: The received data. If outputFile is set, return always S_OK """ # Adding some informations to send if self.__extraCredentials: kwargs[self.KW_EXTRA_CREDENTIALS] = encode(self.__extraCredentials) kwargs["clientVO"] = self.vo kwargs["clientSetup"] = self.setup # Getting URL url = self.__findServiceURL() if not url["OK"]: return url url = url["Value"] # Getting CA file (or skip verification) verify = not self.kwargs.get(self.KW_SKIP_CA_CHECK) if verify: if not self.__ca_location: self.__ca_location = Locations.getCAsLocation() if not self.__ca_location: gLogger.error("No CAs found!") return S_ERROR("No CAs found!") verify = self.__ca_location # getting certificate # Do we use the server certificate ? if self.kwargs[self.KW_USE_CERTIFICATES]: cert = Locations.getHostCertificateAndKeyLocation() elif self.kwargs.get(self.KW_PROXY_STRING): tmpHandle, cert = tempfile.mkstemp() fp = os.fdopen(tmpHandle, "wb") fp.write(self.kwargs[self.KW_PROXY_STRING]) fp.close() # CHRIS 04.02.21 # TODO: add proxyLocation check ? else: cert = Locations.getProxyLocation() if not cert: gLogger.error("No proxy found") return S_ERROR("No proxy found") # We have a try/except for all the exceptions # whose default behavior is to try again, # maybe to different server try: # And we have a second block to handle specific exceptions # which makes it not worth retrying try: rawText = None # Default case, just return the result if not outputFile: call = requests.post(url, data=kwargs, timeout=self.timeout, verify=verify, cert=cert) # raising the exception for status here # means essentialy that we are losing here the information of what is returned by the server # as error message, since it is not passed to the exception # However, we can store the text and return it raw as an error, # since there is no guarantee that it is any JEncoded text # Note that we would get an exception only if there is an exception on the server side which # is not handled. # Any standard S_ERROR will be transfered as an S_ERROR with a correct code. rawText = call.text call.raise_for_status() return decode(rawText)[0] else: # Instruct the server not to encode the response kwargs["rawContent"] = True rawText = None # Stream download # https://requests.readthedocs.io/en/latest/user/advanced/#body-content-workflow with requests.post( url, data=kwargs, timeout=self.timeout, verify=verify, cert=cert, stream=True ) as r: rawText = r.text r.raise_for_status() with open(outputFile, "wb") as f: for chunk in r.iter_content(4096): # if chunk: # filter out keep-alive new chuncks f.write(chunk) return S_OK() # Some HTTPError are not worth retrying except requests.exceptions.HTTPError as e: status_code = e.response.status_code if status_code == http_client.NOT_IMPLEMENTED: return S_ERROR(errno.ENOSYS, "%s is not implemented" % kwargs.get("method")) elif status_code in (http_client.FORBIDDEN, http_client.UNAUTHORIZED): return S_ERROR(errno.EACCES, "No access to %s" % url) # if it is something else, retry raise # Whatever exception we have here, we deem worth retrying except Exception as e: # CHRIS TODO review this part: retry logic is fishy # self.__bannedUrls is emptied in findServiceURLs if url not in self.__bannedUrls: self.__bannedUrls += [url] if retry < self.__nbOfUrls - 1: self._request(retry=retry + 1, outputFile=outputFile, **kwargs) errStr = "%s: %s" % (str(e), rawText) return S_ERROR(errStr)
class BaseClient(object): """ Glues together stubs with threading, credentials, and URLs discovery (by DIRAC vo and setup). Basically what needs to be done to enable RPC calls, and transfer, to find a URL. """ VAL_EXTRA_CREDENTIALS_HOST = "hosts" KW_USE_CERTIFICATES = "useCertificates" KW_EXTRA_CREDENTIALS = "extraCredentials" KW_TIMEOUT = "timeout" KW_SETUP = "setup" KW_VO = "VO" KW_DELEGATED_DN = "delegatedDN" KW_DELEGATED_GROUP = "delegatedGroup" KW_IGNORE_GATEWAYS = "ignoreGateways" KW_PROXY_LOCATION = "proxyLocation" KW_PROXY_STRING = "proxyString" KW_PROXY_CHAIN = "proxyChain" KW_SKIP_CA_CHECK = "skipCACheck" KW_KEEP_ALIVE_LAPSE = "keepAliveLapse" __threadConfig = ThreadConfig() def __init__(self, serviceName, **kwargs): """ Constructor :param serviceName: URL of the service (proper uri or just System/Component) :param useCertificates: If set to True, use the server certificate :param extraCredentials: :param timeout: Timeout of the call (default 600 s) :param setup: Specify the Setup :param VO: Specify the VO :param delegatedDN: Not clear what it can be used for. :param delegatedGroup: Not clear what it can be used for. :param ignoreGateways: Ignore the DIRAC Gatways settings :param proxyLocation: Specify the location of the proxy :param proxyString: Specify the proxy string :param proxyChain: Specify the proxy chain :param skipCACheck: Do not check the CA :param keepAliveLapse: Duration for keepAliveLapse (heartbeat like) """ if not isinstance(serviceName, six.string_types): raise TypeError("Service name expected to be a string. Received %s type %s" % (str(serviceName), type(serviceName))) # Explicitly convert to a str to avoid Python 2 M2Crypto issues with unicode objects self._destinationSrv = str(serviceName) self._serviceName = str(serviceName) self.kwargs = kwargs self.__useCertificates = None # The CS useServerCertificate option can be overridden by explicit argument self.__forceUseCertificates = self.kwargs.get(self.KW_USE_CERTIFICATES) self.__initStatus = S_OK() self.__idDict = {} self.__extraCredentials = "" self.__enableThreadCheck = False self.__retry = 0 self.__retryDelay = 0 # by default we always have 1 url for example: # RPCClient('dips://volhcb38.cern.ch:9162/Framework/SystemAdministrator') self.__nbOfUrls = 1 self.__nbOfRetry = 3 # by default we try try times self.__retryCounter = 1 self.__bannedUrls = [] for initFunc in (self.__discoverSetup, self.__discoverVO, self.__discoverTimeout, self.__discoverURL, self.__discoverCredentialsToUse, self.__checkTransportSanity, self.__setKeepAliveLapse): result = initFunc() if not result['OK'] and self.__initStatus['OK']: self.__initStatus = result self.numberOfURLs = 0 self._initialize() # HACK for thread-safety: self.__allowedThreadID = False def _initialize(self): pass def getDestinationService(self): """ Return service destination :return: str """ return self._destinationSrv def getServiceName(self): """ Return service name :return: str """ return self._serviceName def __discoverSetup(self): """ Discover which setup to use and stores it in self.setup The setup is looked for: * kwargs of the constructor (see KW_SETUP) * the ThreadConfig * in the CS /DIRAC/Setup * default to 'Test' :return: S_OK()/S_ERROR() """ if self.KW_SETUP in self.kwargs and self.kwargs[self.KW_SETUP]: self.setup = str(self.kwargs[self.KW_SETUP]) else: self.setup = self.__threadConfig.getSetup() if not self.setup: self.setup = gConfig.getValue("/DIRAC/Setup", "Test") return S_OK() def __discoverVO(self): """ Discover which VO to use and stores it in self.vo The VO is looked for: * kwargs of the constructor (see KW_VO) * in the CS /DIRAC/VirtualOrganization * default to 'unknown' :return: S_OK()/S_ERROR() """ if self.KW_VO in self.kwargs and self.kwargs[self.KW_VO]: self.vo = str(self.kwargs[self.KW_VO]) else: self.vo = gConfig.getValue("/DIRAC/VirtualOrganization", "unknown") return S_OK() def __discoverURL(self): """ Calculate the final URL. It is called at initialization and in connect in case of issue It sets: * self.serviceURL: the url (dips) selected as target using __findServiceURL * self.__URLTuple: a split of serviceURL obtained by Network.splitURL * self._serviceName: the last part of URLTuple (typically System/Component) :return: S_OK()/S_ERROR() """ # Calculate final URL try: result = self.__findServiceURL() except Exception as e: return S_ERROR(repr(e)) if not result['OK']: return result self.serviceURL = result['Value'] retVal = Network.splitURL(self.serviceURL) if not retVal['OK']: return retVal self.__URLTuple = retVal['Value'] self._serviceName = self.__URLTuple[-1] res = gConfig.getOptionsDict("/DIRAC/ConnConf/%s:%s" % self.__URLTuple[1:3]) if res['OK']: opts = res['Value'] for k in opts: if k not in self.kwargs: self.kwargs[k] = opts[k] return S_OK() def __discoverTimeout(self): """ Discover which timeout to use and stores it in self.timeout The timeout can be specified kwargs of the constructor (see KW_TIMEOUT), with a minimum of 120 seconds. If unspecified, the timeout will be 600 seconds. The value is set in self.timeout, as well as in self.kwargs[KW_TIMEOUT] :return: S_OK()/S_ERROR() """ if self.KW_TIMEOUT in self.kwargs: self.timeout = self.kwargs[self.KW_TIMEOUT] else: self.timeout = False if self.timeout: self.timeout = max(120, self.timeout) else: self.timeout = 600 self.kwargs[self.KW_TIMEOUT] = self.timeout return S_OK() def __discoverCredentialsToUse(self): """ Discovers which credentials to use for connection. * Server certificate: -> If KW_USE_CERTIFICATES in kwargs, sets it in self.__useCertificates -> If not, check gConfig.useServerCertificate(), and sets it in self.__useCertificates and kwargs[KW_USE_CERTIFICATES] * Certification Authorities check: -> if KW_SKIP_CA_CHECK is not in kwargs and we are using the certificates, set KW_SKIP_CA_CHECK to false in kwargs -> if KW_SKIP_CA_CHECK is not in kwargs and we are not using the certificate, check the CS.skipCACheck * Proxy Chain -> if KW_PROXY_CHAIN in kwargs, we remove it and dump its string form into kwargs[KW_PROXY_STRING] :return: S_OK()/S_ERROR() """ # Use certificates? if self.KW_USE_CERTIFICATES in self.kwargs: self.__useCertificates = self.kwargs[self.KW_USE_CERTIFICATES] else: self.__useCertificates = gConfig.useServerCertificate() self.kwargs[self.KW_USE_CERTIFICATES] = self.__useCertificates if self.KW_SKIP_CA_CHECK not in self.kwargs: if self.__useCertificates: self.kwargs[self.KW_SKIP_CA_CHECK] = False else: self.kwargs[self.KW_SKIP_CA_CHECK] = skipCACheck() if self.KW_PROXY_CHAIN in self.kwargs: try: self.kwargs[self.KW_PROXY_STRING] = self.kwargs[self.KW_PROXY_CHAIN].dumpAllToString()['Value'] del self.kwargs[self.KW_PROXY_CHAIN] except BaseException: return S_ERROR("Invalid proxy chain specified on instantiation") return S_OK() def __discoverExtraCredentials(self): """ Add extra credentials informations. * self.__extraCredentials -> if KW_EXTRA_CREDENTIALS in kwargs, we set it -> Otherwise, if we use the server certificate, we set it to VAL_EXTRA_CREDENTIALS_HOST -> If we have a delegation (see bellow), we set it to (delegatedDN, delegatedGroup) -> otherwise it is an empty string * delegation: -> if KW_DELEGATED_DN in kwargs, or delegatedDN in threadConfig, put in in self.kwargs -> if KW_DELEGATED_GROUP in kwargs or delegatedGroup in threadConfig, put it in self.kwargs -> If we have a delegated DN but not group, we find the corresponding group in the CS :return: S_OK()/S_ERROR() """ # which extra credentials to use? self.__extraCredentials = self.VAL_EXTRA_CREDENTIALS_HOST if self.__useCertificates else "" if self.KW_EXTRA_CREDENTIALS in self.kwargs: self.__extraCredentials = self.kwargs[self.KW_EXTRA_CREDENTIALS] # Are we delegating something? delegatedDN = self.kwargs.get(self.KW_DELEGATED_DN) or self.__threadConfig.getDN() delegatedGroup = self.kwargs.get(self.KW_DELEGATED_GROUP) or self.__threadConfig.getGroup() if delegatedDN: self.kwargs[self.KW_DELEGATED_DN] = delegatedDN if not delegatedGroup: result = Registry.findDefaultGroupForDN(delegatedDN) if not result['OK']: return result delegatedGroup = result['Value'] self.kwargs[self.KW_DELEGATED_GROUP] = delegatedGroup self.__extraCredentials = (delegatedDN, delegatedGroup) return S_OK() def __findServiceURL(self): """ Discovers the URL of a service, taking into account gateways, multiple URLs, banned URLs If the site on which we run is configured to use gateways (/DIRAC/Gateways/<siteName>), these URLs will be used. To ignore the gateway, it is possible to set KW_IGNORE_GATEWAYS to False in kwargs. If self._destinationSrv (given as constructor attribute) is a properly formed URL, we just return this one. If we have to use a gateway, we just replace the server name in the url. The list of URLs defined in the CS (<System>/URLs/<Component>) is randomized This method also sets some attributes: * self.__nbOfUrls = number of URLs * self.__nbOfRetry = 2 if we have more than 2 urls, otherwise 3 * self.__bannedUrls is reinitialized if all the URLs are banned :return: S_OK(str)/S_ERROR() -- the selected URL """ if not self.__initStatus['OK']: return self.__initStatus # Load the Gateways URLs for the current site Name gatewayURL = False if not self.kwargs.get(self.KW_IGNORE_GATEWAYS): dRetVal = gConfig.getOption("/DIRAC/Gateways/%s" % DIRAC.siteName()) if dRetVal['OK']: rawGatewayURL = List.randomize(List.fromChar(dRetVal['Value'], ","))[0] gatewayURL = "/".join(rawGatewayURL.split("/")[:3]) # If what was given as constructor attribute is a properly formed URL, # we just return this one. # If we have to use a gateway, we just replace the server name in it for protocol in gProtocolDict: if self._destinationSrv.find("%s://" % protocol) == 0: gLogger.debug("Already given a valid url", self._destinationSrv) if not gatewayURL: return S_OK(self._destinationSrv) gLogger.debug("Reconstructing given URL to pass through gateway") path = "/".join(self._destinationSrv.split("/")[3:]) finalURL = "%s/%s" % (gatewayURL, path) gLogger.debug("Gateway URL conversion:\n %s -> %s" % (self._destinationSrv, finalURL)) return S_OK(finalURL) if gatewayURL: gLogger.debug("Using gateway", gatewayURL) return S_OK("%s/%s" % (gatewayURL, self._destinationSrv)) # We extract the list of URLs from the CS (System/URLs/Component) try: urls = getServiceURL(self._destinationSrv, setup=self.setup) except Exception as e: return S_ERROR("Cannot get URL for %s in setup %s: %s" % (self._destinationSrv, self.setup, repr(e))) if not urls: return S_ERROR("URL for service %s not found" % self._destinationSrv) failoverUrls = [] # Try if there are some failover URLs to use as last resort try: failoverUrlsStr = getServiceFailoverURL(self._destinationSrv, setup=self.setup) if failoverUrlsStr: failoverUrls = failoverUrlsStr.split(',') except Exception as e: pass # We randomize the list, and add at the end the failover URLs (System/FailoverURLs/Component) urlsList = List.randomize(List.fromChar(urls, ",")) + failoverUrls self.__nbOfUrls = len(urlsList) self.__nbOfRetry = 2 if self.__nbOfUrls > 2 else 3 # we retry 2 times all services, if we run more than 2 services if self.__nbOfUrls == len(self.__bannedUrls): self.__bannedUrls = [] # retry all urls gLogger.debug("Retrying again all URLs") if len(self.__bannedUrls) > 0 and len(urlsList) > 1: # we have host which is not accessible. We remove that host from the list. # We only remove if we have more than one instance for i in self.__bannedUrls: gLogger.debug("Removing banned URL", "%s" % i) urlsList.remove(i) # Take the first URL from the list # randUrls = List.randomize( urlsList ) + failoverUrls sURL = urlsList[0] # If we have banned URLs, and several URLs at disposals, we make sure that the selected sURL # is not on a host which is banned. If it is, we take the next one in the list using __selectUrl # If we have banned URLs, and several URLs at disposals, we make sure that the selected sURL # is not on a host which is banned. If it is, we take the next one in the list using __selectUrl if len(self.__bannedUrls) > 0 and self.__nbOfUrls > 2: # when we have multiple services then we can # have a situation when two services are running on the same machine with different ports... retVal = Network.splitURL(sURL) nexturl = None if retVal['OK']: nexturl = retVal['Value'] found = False for i in self.__bannedUrls: retVal = Network.splitURL(i) if retVal['OK']: bannedurl = retVal['Value'] else: break # We found a banned URL on the same host as the one we are running on if nexturl[1] == bannedurl[1]: found = True break if found: nexturl = self.__selectUrl(nexturl, urlsList[1:]) if nexturl: # an url found which is in different host sURL = nexturl gLogger.debug("Discovering URL for service", "%s -> %s" % (self._destinationSrv, sURL)) return S_OK(sURL) def __selectUrl(self, notselect, urls): """ In case when multiple services are running in the same host, a new url has to be in a different host Note: If we do not have different host we will use the selected url... :param notselect: URL that should NOT be selected :param list urls: list of potential URLs :return: str -- selected URL """ url = None for i in urls: retVal = Network.splitURL(i) if retVal['OK']: if retVal['Value'][1] != notselect[1]: # the hosts are different url = i break else: gLogger.error(retVal['Message']) return url def __checkThreadID(self): """ ..warning:: just guessing.... This seems to check that we are not creating a client and then using it in a multithreaded environment. However, it is triggered only if self.__enableThreadCheck is to True, but it is hardcoded to False, and does not seem to be modified anywhere in the code. """ if not self.__initStatus['OK']: return self.__initStatus cThID = thread.get_ident() if not self.__allowedThreadID: self.__allowedThreadID = cThID elif cThID != self.__allowedThreadID: msgTxt = """ =======DISET client thread safety error======================== Client %s can only run on thread %s and this is thread %s ===============================================================""" % (str(self), self.__allowedThreadID, cThID) gLogger.error("DISET client thread safety error", msgTxt) # raise Exception( msgTxt ) def _connect(self): """ Establish the connection. It uses the URL discovered in __discoverURL. In case the connection cannot be established, __discoverURL is called again, and _connect calls itself. We stop after trying self.__nbOfRetry * self.__nbOfUrls :return: S_OK()/S_ERROR() """ # Check if the useServerCertificate configuration changed # Note: I am not really sure that all this block makes # any sense at all since all these variables are # evaluated in __discoverCredentialsToUse if gConfig.useServerCertificate() != self.__useCertificates: if self.__forceUseCertificates is None: self.__useCertificates = gConfig.useServerCertificate() self.kwargs[self.KW_USE_CERTIFICATES] = self.__useCertificates # The server certificate use context changed, rechecking the transport sanity result = self.__checkTransportSanity() if not result['OK']: return result # Take all the extra credentials self.__discoverExtraCredentials() if not self.__initStatus['OK']: return self.__initStatus if self.__enableThreadCheck: self.__checkThreadID() gLogger.debug("Trying to connect to: %s" % self.serviceURL) try: # Calls the transport method of the apropriate protocol. # self.__URLTuple[1:3] = [server name, port, System/Component] transport = gProtocolDict[self.__URLTuple[0]]['transport'](self.__URLTuple[1:3], **self.kwargs) # the socket timeout is the default value which is 1. # later we increase to 5 retVal = transport.initAsClient() # We try at most __nbOfRetry each URLs if not retVal['OK']: gLogger.warn("Issue getting socket:", "%s : %s : %s" % (transport, self.__URLTuple, retVal['Message'])) # We try at most __nbOfRetry each URLs if self.__retry < self.__nbOfRetry * self.__nbOfUrls - 1: # Recompose the URL (why not using self.serviceURL ? ) url = "%s://%s:%d/%s" % (self.__URLTuple[0], self.__URLTuple[1], int(self.__URLTuple[2]), self.__URLTuple[3]) # Add the url to the list of banned URLs if it is not already there. (Can it happen ? I don't think so) if url not in self.__bannedUrls: gLogger.warn("Non-responding URL temporarily banned", "%s" % url) self.__bannedUrls += [url] # Increment the retry counter self.__retry += 1 # 16.07.20 CHRIS: I guess this setSocketTimeout does not behave as expected. # If the initasClient did not work, we anyway re-enter the whole method, # so a new transport object is created. # However, it migh be that this timeout value was propagated down to the # SocketInfoFactory singleton, and thus used, but that means that the timeout # specified in parameter was then void. # If it is our last attempt for each URL, we increase the timeout if self.__retryCounter == self.__nbOfRetry - 1: transport.setSocketTimeout(5) # we increase the socket timeout in case the network is not good gLogger.info("Retry connection", ": %d to %s" % (self.__retry, self.serviceURL)) # If we tried all the URL, we increase the global counter (__retryCounter), and sleep if len(self.__bannedUrls) == self.__nbOfUrls: self.__retryCounter += 1 # we run only one service! In that case we increase the retry delay. self.__retryDelay = 3. / self.__nbOfUrls if self.__nbOfUrls > 1 else 2 gLogger.info("Waiting %f seconds before retry all service(s)" % self.__retryDelay) time.sleep(self.__retryDelay) # rediscover the URL self.__discoverURL() # try to reconnect return self._connect() else: return retVal except Exception as e: gLogger.exception(lException=True, lExcInfo=True) return S_ERROR("Can't connect to %s: %s" % (self.serviceURL, repr(e))) # We add the connection to the transport pool gLogger.debug("Connected to: %s" % self.serviceURL) trid = getGlobalTransportPool().add(transport) return S_OK((trid, transport)) def _disconnect(self, trid): """ Disconnect the connection. :param str trid: Transport ID in the transportPool """ getGlobalTransportPool().close(trid) @staticmethod def _serializeStConnectionInfo(stConnectionInfo): """ We want to send tuple but we need to convert into a list """ serializedTuple = [list(x) if isinstance(x, tuple) else x for x in stConnectionInfo] return serializedTuple def _proposeAction(self, transport, action): """ Proposes an action by sending a tuple containing * System/Component * Setup * VO * action * extraCredentials It is kind of a handshake. The server might ask for a delegation, in which case it is done here. The result of the delegation is then returned. :param transport: the Transport object returned by _connect :param action: tuple (<action type>, <action name>). It depends on the subclasses of BaseClient. <action type> can be for example 'RPC' or 'FileTransfer' :return: whatever the server sent back """ if not self.__initStatus['OK']: return self.__initStatus stConnectionInfo = ((self.__URLTuple[3], self.setup, self.vo), action, self.__extraCredentials, DIRAC.version) # Send the connection info and get the answer back retVal = transport.sendData(S_OK(BaseClient._serializeStConnectionInfo(stConnectionInfo))) if not retVal['OK']: return retVal serverReturn = transport.receiveData() # TODO: Check if delegation is required. This seems to be used only for the GatewayService if serverReturn['OK'] and 'Value' in serverReturn and isinstance(serverReturn['Value'], dict): gLogger.debug("There is a server requirement") serverRequirements = serverReturn['Value'] if 'delegate' in serverRequirements: gLogger.debug("A delegation is requested") serverReturn = self.__delegateCredentials(transport, serverRequirements['delegate']) return serverReturn def __delegateCredentials(self, transport, delegationRequest): """ Perform a credential delegation. This seems to be used only for the GatewayService. It calls the delegation mechanism of the Transport class. Note that it is not used when delegating credentials to the ProxyDB :param transport: the Transport object returned by _connect :param delegationRequest: delegation request :return: S_OK()/S_ERROR() """ retVal = gProtocolDict[self.__URLTuple[0]]['delegation'](delegationRequest, self.kwargs) if not retVal['OK']: return retVal retVal = transport.sendData(retVal['Value']) if not retVal['OK']: return retVal return transport.receiveData() def __checkTransportSanity(self): """ Calls the sanity check of the underlying Transport object and stores the result in self.__idDict. It is checked at the creation of the BaseClient, and when connecting if the use of the certificate has changed. :return: S_OK()/S_ERROR() """ if not self.__initStatus['OK']: return self.__initStatus retVal = gProtocolDict[self.__URLTuple[0]]['sanity'](self.__URLTuple[1:3], self.kwargs) if not retVal['OK']: return retVal idDict = retVal['Value'] for key in idDict: self.__idDict[key] = idDict[key] return S_OK() def __setKeepAliveLapse(self): """ Select the maximum Keep alive lapse between 150 seconds and what is specifind in kwargs[KW_KEEP_ALIVE_LAPSE], and sets it in kwargs[KW_KEEP_ALIVE_LAPSE] :return: S_OK()/S_ERROR() """ kaa = 1 if self.KW_KEEP_ALIVE_LAPSE in self.kwargs: try: kaa = max(0, int(self.kwargs[self.KW_KEEP_ALIVE_LAPSE])) except BaseException: pass if kaa: kaa = max(150, kaa) self.kwargs[self.KW_KEEP_ALIVE_LAPSE] = kaa return S_OK() def _getBaseStub(self): """ Returns a list with [self._destinationSrv, newKwargs] self._destinationSrv is what was given as first parameter of the init serviceName newKwargs is an updated copy of kwargs: * if set, we remove the useCertificates (KW_USE_CERTIFICATES) in newKwargs This method is just used to return information in case of error in the InnerRPCClient :return: tuple """ newKwargs = dict(self.kwargs) # Remove useCertificates as the forwarder of the call will have to # independently decide whether to use their cert or not anyway. if 'useCertificates' in newKwargs: del newKwargs['useCertificates'] return [self._destinationSrv, newKwargs] def __bool__(self): return True # For Python 2 compatibility __nonzero__ = __bool__ def __str__(self): return "<DISET Client %s %s>" % (self.serviceURL, self.__extraCredentials)
class BaseClient: VAL_EXTRA_CREDENTIALS_HOST = "hosts" KW_USE_CERTIFICATES = "useCertificates" KW_EXTRA_CREDENTIALS = "extraCredentials" KW_TIMEOUT = "timeout" KW_SETUP = "setup" KW_VO = "VO" KW_DELEGATED_DN = "delegatedDN" KW_DELEGATED_GROUP = "delegatedGroup" KW_IGNORE_GATEWAYS = "ignoreGateways" KW_PROXY_LOCATION = "proxyLocation" KW_PROXY_STRING = "proxyString" KW_PROXY_CHAIN = "proxyChain" KW_SKIP_CA_CHECK = "skipCACheck" KW_KEEP_ALIVE_LAPSE = "keepAliveLapse" __threadConfig = ThreadConfig() def __init__(self, serviceName, **kwargs): if type(serviceName) not in types.StringTypes: raise TypeError( "Service name expected to be a string. Received %s type %s" % (str(serviceName), type(serviceName))) self._destinationSrv = serviceName self._serviceName = serviceName self.kwargs = kwargs self.__initStatus = S_OK() self.__idDict = {} self.__extraCredentials = "" self.__enableThreadCheck = False self.__retry = 0 self.__retryDelay = 0 self.__nbOfUrls = 1 #by default we always have 1 url for example: RPCClient('dips://volhcb38.cern.ch:9162/Framework/SystemAdministrator') self.__nbOfRetry = 3 # by default we try try times self.__retryCounter = 1 self.__bannedUrls = [] for initFunc in (self.__discoverSetup, self.__discoverVO, self.__discoverTimeout, self.__discoverURL, self.__discoverCredentialsToUse, self.__checkTransportSanity, self.__setKeepAliveLapse): result = initFunc() if not result['OK'] and self.__initStatus['OK']: self.__initStatus = result self.numberOfURLs = 0 self._initialize() #HACK for thread-safety: self.__allowedThreadID = False def _initialize(self): pass def getDestinationService(self): return self._destinationSrv def getServiceName(self): return self._serviceName def __discoverSetup(self): #Which setup to use? if self.KW_SETUP in self.kwargs and self.kwargs[self.KW_SETUP]: self.setup = str(self.kwargs[self.KW_SETUP]) else: self.setup = self.__threadConfig.getSetup() if not self.setup: self.setup = gConfig.getValue("/DIRAC/Setup", "Test") return S_OK() def __discoverVO(self): #Which setup to use? if self.KW_VO in self.kwargs and self.kwargs[self.KW_VO]: self.vo = str(self.kwargs[self.KW_VO]) else: self.vo = gConfig.getValue("/DIRAC/VirtualOrganization", "unknown") return S_OK() def __discoverURL(self): #Calculate final URL try: result = self.__findServiceURL() except Exception as e: return S_ERROR(repr(e)) if not result['OK']: return result self.serviceURL = result['Value'] retVal = Network.splitURL(self.serviceURL) if not retVal['OK']: return retVal self.__URLTuple = retVal['Value'] self._serviceName = self.__URLTuple[-1] res = gConfig.getOptionsDict("/DIRAC/ConnConf/%s:%s" % self.__URLTuple[1:3]) if res['OK']: opts = res['Value'] for k in opts: if k not in self.kwargs: self.kwargs[k] = opts[k] return S_OK() def __discoverTimeout(self): if self.KW_TIMEOUT in self.kwargs: self.timeout = self.kwargs[self.KW_TIMEOUT] else: self.timeout = False if self.timeout: self.timeout = max(120, self.timeout) else: self.timeout = 600 self.kwargs[self.KW_TIMEOUT] = self.timeout return S_OK() def __discoverCredentialsToUse(self): #Use certificates? if self.KW_USE_CERTIFICATES in self.kwargs: self.useCertificates = self.kwargs[self.KW_USE_CERTIFICATES] else: self.useCertificates = gConfig.useServerCertificate() self.kwargs[self.KW_USE_CERTIFICATES] = self.useCertificates if self.KW_SKIP_CA_CHECK not in self.kwargs: if self.useCertificates: self.kwargs[self.KW_SKIP_CA_CHECK] = False else: self.kwargs[self.KW_SKIP_CA_CHECK] = CS.skipCACheck() if self.KW_PROXY_CHAIN in self.kwargs: try: self.kwargs[self.KW_PROXY_STRING] = self.kwargs[ self.KW_PROXY_CHAIN].dumpAllToString()['Value'] del self.kwargs[self.KW_PROXY_CHAIN] except: return S_ERROR( "Invalid proxy chain specified on instantiation") return S_OK() def __discoverExtraCredentials(self): #Wich extra credentials to use? if self.useCertificates: self.__extraCredentials = self.VAL_EXTRA_CREDENTIALS_HOST else: self.__extraCredentials = "" if self.KW_EXTRA_CREDENTIALS in self.kwargs: self.__extraCredentials = self.kwargs[self.KW_EXTRA_CREDENTIALS] #Are we delegating something? delegatedDN, delegatedGroup = self.__threadConfig.getID() if self.KW_DELEGATED_DN in self.kwargs and self.kwargs[ self.KW_DELEGATED_DN]: delegatedDN = self.kwargs[self.KW_DELEGATED_DN] elif delegatedDN: self.kwargs[self.KW_DELEGATED_DN] = delegatedDN if self.KW_DELEGATED_GROUP in self.kwargs and self.kwargs[ self.KW_DELEGATED_GROUP]: delegatedGroup = self.kwargs[self.KW_DELEGATED_GROUP] elif delegatedGroup: self.kwargs[self.KW_DELEGATED_GROUP] = delegatedGroup if delegatedDN: if not delegatedGroup: result = CS.findDefaultGroupForDN( self.kwargs[self.KW_DELEGATED_DN]) if not result['OK']: return result self.__extraCredentials = (delegatedDN, delegatedGroup) return S_OK() def __findServiceURL(self): if not self.__initStatus['OK']: return self.__initStatus gatewayURL = False if self.KW_IGNORE_GATEWAYS not in self.kwargs or not self.kwargs[ self.KW_IGNORE_GATEWAYS]: dRetVal = gConfig.getOption("/DIRAC/Gateways/%s" % DIRAC.siteName()) if dRetVal['OK']: rawGatewayURL = List.randomize( List.fromChar(dRetVal['Value'], ","))[0] gatewayURL = "/".join(rawGatewayURL.split("/")[:3]) for protocol in gProtocolDict.keys(): if self._destinationSrv.find("%s://" % protocol) == 0: gLogger.debug("Already given a valid url", self._destinationSrv) if not gatewayURL: return S_OK(self._destinationSrv) gLogger.debug( "Reconstructing given URL to pass through gateway") path = "/".join(self._destinationSrv.split("/")[3:]) finalURL = "%s/%s" % (gatewayURL, path) gLogger.debug("Gateway URL conversion:\n %s -> %s" % (self._destinationSrv, finalURL)) return S_OK(finalURL) if gatewayURL: gLogger.debug("Using gateway", gatewayURL) return S_OK("%s/%s" % (gatewayURL, self._destinationSrv)) try: urls = getServiceURL(self._destinationSrv, setup=self.setup) except Exception as e: return S_ERROR("Cannot get URL for %s in setup %s: %s" % (self._destinationSrv, self.setup, repr(e))) if not urls: return S_ERROR("URL for service %s not found" % self._destinationSrv) urlsList = List.fromChar(urls, ",") self.__nbOfUrls = len(urlsList) self.__nbOfRetry = 2 if self.__nbOfUrls > 2 else 3 # we retry 2 times all services, if we run more than 2 services if len(urlsList) == len(self.__bannedUrls): self.__bannedUrls = [] # retry all urls gLogger.debug("Retrying again all URLs") if len(self.__bannedUrls) > 0 and len(urlsList) > 1: # we have host which is not accessible. We remove that host from the list. # We only remove if we have more than one instance for i in self.__bannedUrls: gLogger.debug("Removing banned URL", "%s" % i) urlsList.remove(i) randUrls = List.randomize(urlsList) sURL = randUrls[0] if len( self.__bannedUrls ) > 0 and self.__nbOfUrls > 2: # when we have multiple services then we can have a situation # when two service are running on the same machine with different port... retVal = Network.splitURL(sURL) nexturl = None if retVal['OK']: nexturl = retVal['Value'] found = False for i in self.__bannedUrls: retVal = Network.splitURL(i) if retVal['OK']: bannedurl = retVal['Value'] else: break if nexturl[1] == bannedurl[1]: found = True break if found: nexturl = self.__selectUrl(nexturl, randUrls[1:]) if nexturl: # an url found which is in different host sURL = nexturl gLogger.debug("Discovering URL for service", "%s -> %s" % (self._destinationSrv, sURL)) return S_OK(sURL) def __selectUrl(self, notselect, urls): """In case when multiple services are running in the same host, a new url has to be in a different host Note: If we do not have different host we will use the selected url... """ url = None for i in urls: retVal = Network.splitURL(i) if retVal['OK']: if retVal['Value'][1] != notselect[1]: # the hots are different url = i break else: gLogger.error(retVal['Message']) return url def __checkThreadID(self): if not self.__initStatus['OK']: return self.__initStatus cThID = thread.get_ident() if not self.__allowedThreadID: self.__allowedThreadID = cThID elif cThID != self.__allowedThreadID: msgTxt = """ =======DISET client thread safety error======================== Client %s can only run on thread %s and this is thread %s ===============================================================""" % ( str(self), self.__allowedThreadID, cThID) gLogger.error("DISET client thread safety error", msgTxt) #raise Exception( msgTxt ) def _connect(self): self.__discoverExtraCredentials() if not self.__initStatus['OK']: return self.__initStatus if self.__enableThreadCheck: self.__checkThreadID() gLogger.debug("Connecting to: %s" % self.serviceURL) try: transport = gProtocolDict[self.__URLTuple[0]]['transport']( self.__URLTuple[1:3], **self.kwargs) #the socket timeout is the default value which is 1. #later we increase to 5 retVal = transport.initAsClient() if not retVal['OK']: if self.__retry < self.__nbOfRetry * self.__nbOfUrls - 1: url = "%s://%s:%d/%s" % ( self.__URLTuple[0], self.__URLTuple[1], int(self.__URLTuple[2]), self.__URLTuple[3]) if url not in self.__bannedUrls: self.__bannedUrls += [url] if len(self.__bannedUrls) < self.__nbOfUrls: gLogger.notice( "Non-responding URL temporarily banned", "%s" % url) self.__retry += 1 if self.__retryCounter == self.__nbOfRetry - 1: transport.setSocketTimeout( 5 ) # we increase the socket timeout in case the network is not good gLogger.info("Retry connection: ", "%d" % self.__retry) if len(self.__bannedUrls) == self.__nbOfUrls: self.__retryCounter += 1 self.__retryDelay = 3. / self.__nbOfUrls if self.__nbOfUrls > 1 else 2 # we run only one service! In that case we increase the retry delay. gLogger.info( "Waiting %f second before retry all service(s)" % self.__retryDelay) time.sleep(self.__retryDelay) self.__discoverURL() return self._connect() else: return retVal except Exception as e: return S_ERROR("Can't connect to %s: %s" % (self.serviceURL, repr(e))) trid = getGlobalTransportPool().add(transport) return S_OK((trid, transport)) def _disconnect(self, trid): getGlobalTransportPool().close(trid) def _proposeAction(self, transport, action): if not self.__initStatus['OK']: return self.__initStatus stConnectionInfo = ((self.__URLTuple[3], self.setup, self.vo), action, self.__extraCredentials) retVal = transport.sendData(S_OK(stConnectionInfo)) if not retVal['OK']: return retVal serverReturn = transport.receiveData() #TODO: Check if delegation is required if serverReturn['OK'] and 'Value' in serverReturn and type( serverReturn['Value']) == types.DictType: gLogger.debug("There is a server requirement") serverRequirements = serverReturn['Value'] if 'delegate' in serverRequirements: gLogger.debug("A delegation is requested") serverReturn = self.__delegateCredentials( transport, serverRequirements['delegate']) return serverReturn def __delegateCredentials(self, transport, delegationRequest): retVal = gProtocolDict[self.__URLTuple[0]]['delegation']( delegationRequest, self.kwargs) if not retVal['OK']: return retVal retVal = transport.sendData(retVal['Value']) if not retVal['OK']: return retVal return transport.receiveData() def __checkTransportSanity(self): if not self.__initStatus['OK']: return self.__initStatus retVal = gProtocolDict[self.__URLTuple[0]]['sanity']( self.__URLTuple[1:3], self.kwargs) if not retVal['OK']: return retVal idDict = retVal['Value'] for key in idDict: self.__idDict[key] = idDict[key] return S_OK() def __setKeepAliveLapse(self): kaa = 1 if self.KW_KEEP_ALIVE_LAPSE in self.kwargs: try: kaa = max(0, int(self.kwargs)) except: pass if kaa: kaa = max(150, kaa) self.kwargs[self.KW_KEEP_ALIVE_LAPSE] = kaa return S_OK() def _getBaseStub(self): newKwargs = dict(self.kwargs) #Set DN tDN, tGroup = self.__threadConfig.getID() if not self.KW_DELEGATED_DN in newKwargs: if tDN: newKwargs[self.KW_DELEGATED_DN] = tDN elif 'DN' in self.__idDict: newKwargs[self.KW_DELEGATED_DN] = self.__idDict['DN'] #Discover group if not self.KW_DELEGATED_GROUP in newKwargs: if 'group' in self.__idDict: newKwargs[self.KW_DELEGATED_GROUP] = self.__idDict['group'] elif tGroup: newKwargs[self.KW_DELEGATED_GROUP] = tGroup else: if self.KW_DELEGATED_DN in newKwargs: if CS.getUsernameForDN( newKwargs[self.KW_DELEGATED_DN])['OK']: result = CS.findDefaultGroupForDN( newKwargs[self.KW_DELEGATED_DN]) if result['OK']: newKwargs[ self.KW_DELEGATED_GROUP] = result['Value'] if CS.getHostnameForDN( newKwargs[self.KW_DELEGATED_DN])['OK']: newKwargs[ self. KW_DELEGATED_GROUP] = self.VAL_EXTRA_CREDENTIALS_HOST if 'useCertificates' in newKwargs: del (newKwargs['useCertificates']) return (self._destinationSrv, newKwargs) def __nonzero__(self): return True def __str__(self): return "<DISET Client %s %s>" % (self.serviceURL, self.__extraCredentials)
class SessionData(object): __disetConfig = ThreadConfig() __handlers = {} __groupMenu = {} __extensions = [] __extVersion = False @classmethod def setHandlers(cls, handlers): cls.__handlers = {} for k in handlers: handler = handlers[k] cls.__handlers[handler.LOCATION.strip("/")] = handler #Calculate extensions cls.__extensions = [] for ext in CSGlobals.getInstalledExtensions(): if ext in ("EiscatWebDIRAC", "WebAppDIRAC", "DIRAC"): continue cls.__extensions.append(ext) cls.__extensions.append("DIRAC") cls.__extensions.append("EiscatWebDIRAC") print "cls.__extensions in SessionData Lib" print cls.__extensions def __init__(self, credDict, setup): self.__credDict = credDict self.__setup = setup def __isGroupAuthApp(self, appLoc): handlerLoc = "/".join(List.fromChar(appLoc, ".")[1:]) if not handlerLoc: return False if handlerLoc not in self.__handlers: gLogger.error("Handler %s required by %s does not exist!" % (handlerLoc, appLoc)) return False handler = self.__handlers[handlerLoc] auth = AuthManager(Conf.getAuthSectionForHandler(handlerLoc)) return auth.authQuery("", dict(self.__credDict), handler.AUTH_PROPS) def __generateSchema(self, base, path): """ Generate a menu schema based on the user credentials """ #Calculate schema schema = [] fullName = "%s/%s" % (base, path) result = gConfig.getSections(fullName) if not result['OK']: return schema sectionsList = result['Value'] for sName in sectionsList: subSchema = self.__generateSchema(base, "%s/%s" % (path, sName)) if subSchema: schema.append((sName, subSchema)) result = gConfig.getOptions(fullName) if not result['OK']: return schema optionsList = result['Value'] for opName in optionsList: opVal = gConfig.getValue("%s/%s" % (fullName, opName)) if opVal.find("link|") == 0: schema.append(("link", opName, opVal[5:])) continue if self.__isGroupAuthApp(opVal): schema.append(("app", opName, opVal)) return schema def __getGroupMenu(self): """ Load the schema from the CS and filter based on the group """ #Somebody coming from HTTPS and not with a valid group group = self.__credDict.get("group", "") #Cache time! if group not in self.__groupMenu: base = "%s/Schema" % (Conf.BASECS) self.__groupMenu[group] = self.__generateSchema(base, "") return self.__groupMenu[group] @classmethod def getWebAppPath(cls): return os.path.join( os.path.dirname(os.path.dirname(os.path.realpath(__file__))), "WebApp") @classmethod def getExtJSVersion(cls): if not cls.__extVersion: extPath = os.path.join(cls.getWebAppPath(), "static", "extjs") extVersionPath = [] for entryName in os.listdir(extPath): if entryName.find("ext-") == 0: extVersionPath.append(entryName) cls.__extVersion = sorted(extVersionPath)[-1] return cls.__extVersion def getData(self): data = { 'menu': self.__getGroupMenu(), 'user': self.__credDict, 'validGroups': [], 'setup': self.__setup, 'validSetups': gConfig.getSections("/DIRAC/Setups")['Value'], 'extensions': self.__extensions, 'extVersion': self.getExtJSVersion() } #Add valid groups if known DN = self.__credDict.get("DN", "") if DN: result = Registry.getGroupsForDN(DN) if result['OK']: data['validGroups'] = result['Value'] #Calculate baseURL baseURL = [ Conf.rootURL().strip("/"), "s:%s" % data['setup'], "g:%s" % self.__credDict.get('group', '') ] data['baseURL'] = "/%s" % "/".join(baseURL) return data
class WebHandler(tornado.web.RequestHandler): __disetConfig = ThreadConfig() __log = False #Auth requirements AUTH_PROPS = None #Location of the handler in the URL LOCATION = "" #URL Schema with holders to generate handler urls URLSCHEMA = "" #RE to extract group and setup PATH_RE = "" #Helper function to create threaded gen.Tasks with automatic callback and execption handling def threadTask(self, method, *args, **kwargs): """ Helper method to generate a gen.Task and automatically call the callback when the real method ends. THIS IS SPARTAAAAAAAAAA. SPARTA has improved using futures ;) """ #Save the task to access the runner genTask = False #This runs in the separate thread, calls the callback on finish and takes into account exceptions def cbMethod(*cargs, **ckwargs): cb = ckwargs.pop('callback') method = cargs[0] disetConf = cargs[1] cargs = cargs[2] self.__disetConfig.reset() self.__disetConfig.load(disetConf) ioloop = tornado.ioloop.IOLoop.instance() try: result = method(*cargs, **ckwargs) ioloop.add_callback(functools.partial(cb, result)) except Exception as excp: gLogger.error("Following exception occured %s" % excp) exc_info = sys.exc_info() genTask.set_exc_info(exc_info) ioloop.add_callback(lambda: genTask.exception()) #Put the task in the thread :) def threadJob(tmethod, *targs, **tkwargs): tkwargs['callback'] = tornado.stack_context.wrap( tkwargs['callback']) targs = (tmethod, self.__disetDump, targs) gThreadPool.submit(cbMethod, *targs, **tkwargs) #Return a YieldPoint genTask = tornado.gen.Task(threadJob, method, *args, **kwargs) return genTask def __disetBlockDecor(self, func): def wrapper(*args, **kwargs): raise RuntimeError( "All DISET calls must be made from inside a Threaded Task! Bad boy!" ) return wrapper def __init__(self, *args, **kwargs): """ Initialize the handler """ super(WebHandler, self).__init__(*args, **kwargs) if not WebHandler.__log: WebHandler.__log = gLogger.getSubLogger(self.__class__.__name__) self.__credDict = {} self.__setup = Conf.setup() self.__processCredentials() self.__disetConfig.reset() self.__disetConfig.setDecorator(self.__disetBlockDecor) self.__disetDump = self.__disetConfig.dump() match = self.PATH_RE.match(self.request.path) self._pathResult = self.__checkPath(*match.groups()) self.__sessionData = SessionData(self.__credDict, self.__setup) def __processCredentials(self): """ Extract the user credentials based on the certificate or what comes from the balancer """ #NGINX if Conf.balancer() == "nginx": headers = self.request.headers if headers['X-Scheme'] == "https" and headers[ 'X-Ssl_client_verify'] == 'SUCCESS': DN = headers['X-Ssl_client_s_dn'] self.__credDict['DN'] = DN self.__credDict['issuer'] = headers['X-Ssl_client_i_dn'] result = Registry.getUsernameForDN(DN) if not result['OK']: self.__credDict['validDN'] = False else: self.__credDict['validDN'] = True self.__credDict['username'] = result['Value'] return #TORNADO if not self.request.protocol == "https": return derCert = self.request.get_ssl_certificate(binary_form=True) if not derCert: return pemCert = ssl.DER_cert_to_PEM_cert(derCert) chain = X509Chain() chain.loadChainFromString(pemCert) result = chain.getCredentials() if not result['OK']: self.log.error("Could not get client credentials %s" % result['Message']) return self.__credDict = result['Value'] #Hack. Data coming from OSSL directly and DISET difer in DN/subject try: self.__credDict['DN'] = self.__credDict['subject'] except KeyError: pass def _request_summary(self): """ Return a string returning the summary of the request """ summ = super(WebHandler, self)._request_summary() cl = [] if self.__credDict.get('validDN', False): cl.append(self.__credDict['username']) if self.__credDict.get('validGroup', False): cl.append("@%s" % self.__credDict['group']) cl.append(" (%s)" % self.__credDict['DN']) summ = "%s %s" % (summ, "".join(cl)) return summ @property def log(self): return self.__log @classmethod def getLog(cls): return cls.__log def getUserDN(self): return self.__credDict.get('DN', '') def getUserName(self): return self.__credDict.get('username', '') def getUserGroup(self): return self.__credDict.get('group', '') def getUserSetup(self): return self.__setup def getUserProperties(self): return self.__sessionData.getData().properties def isRegisteredUser(self): return self.__credDict.get('validDN', "") and self.__credDict.get( 'validGroup', "") def getSessionData(self): return self.__sessionData.getData() def actionURL(self, action=""): """ Given an action name for the handler, return the URL """ if action == "index": action = "" group = self.getUserGroup() if group: group = "/g:%s" % group setup = self.getUserSetup() if setup: setup = "/s:%s" % setup location = self.LOCATION if location: location = "/%s" % location ats = dict(action=action, group=group, setup=setup, location=location) return self.URLSCHEMA % ats def __auth(self, handlerRoute, group): """ Authenticate request """ userDN = self.getUserDN() if group: self.__credDict['group'] = group else: if userDN: result = Registry.findDefaultGroupForDN(userDN) if result['OK']: self.__credDict['group'] = result['Value'] self.__credDict['validGroup'] = False if type(self.AUTH_PROPS) not in (types.ListType, types.TupleType): self.AUTH_PROPS = [ p.strip() for p in self.AUTH_PROPS.split(",") if p.strip() ] allAllowed = False for p in self.AUTH_PROPS: if p.lower() in ('all', 'any'): allAllowed = True auth = AuthManager(Conf.getAuthSectionForHandler(handlerRoute)) ok = auth.authQuery("", self.__credDict, self.AUTH_PROPS) if ok: if userDN: self.__credDict['validGroup'] = True self.log.info("AUTH OK: %s by %s@%s (%s)" % (handlerRoute, self.__credDict['username'], self.__credDict['group'], userDN)) else: self.__credDict['validDN'] = False self.log.info("AUTH OK: %s by visitor" % (handlerRoute)) elif allAllowed: self.log.info("AUTH ALL: %s by %s" % (handlerRoute, userDN)) ok = True else: self.log.info("AUTH KO: %s by %s@%s" % (handlerRoute, userDN, group)) return ok def __checkPath(self, setup, group, route): """ Check the request, auth, credentials and DISET config """ if route[-1] == "/": methodName = "index" handlerRoute = route else: iP = route.rfind("/") methodName = route[iP + 1:] handlerRoute = route[:iP] if setup: self.__setup = setup if not self.__auth(handlerRoute, group): return WErr(401, "Unauthorized, bad boy!") DN = self.getUserDN() if DN: self.__disetConfig.setDN(DN) group = self.getUserGroup() if group: self.__disetConfig.setGroup(group) self.__disetConfig.setSetup(setup) self.__disetDump = self.__disetConfig.dump() return WOK(methodName) def get(self, setup, group, route): if not self._pathResult.ok: raise self._pathResult methodName = "web_%s" % self._pathResult.data try: mObj = getattr(self, methodName) except AttributeError as e: self.log.fatal("This should not happen!! %s" % e) raise tornado.web.HTTPError(404) return mObj() def post(self, *args, **kwargs): return self.get(*args, **kwargs) def write_error(self, status_code, **kwargs): self.set_status(status_code) cType = "text/plain" data = self._reason if 'exc_info' in kwargs: ex = kwargs['exc_info'][1] trace = traceback.format_exception(*kwargs["exc_info"]) if not isinstance(ex, WErr): data += "\n".join(trace) else: if self.settings.get("debug"): self.log.error("Request ended in error:\n %s" % "\n ".join(trace)) data = ex.msg if type(data) == types.DictType: cType = "application/json" data = json.dumps(data) self.set_header('Content-Type', cType) self.finish(data)
class S3GatewayHandler(RequestHandler): """ .. class:: S3GatewayHandler """ # FC instance to check whether access is permitted or not _fc = None # Mapping between the S3 methods and the DFC methods _s3ToFC_methods = { "head_object": "getFileMetadata", "get_object": "getFileMetadata", # consider that if we are allowed to see the file metadata # we can also download it "put_object": "addFile", "delete_object": "removeFile", } _S3Storages = {} # This allows us to perform the DFC queries on behalf of a user # without having to recreate a DFC object every time and # pass it the "delegatedDN" and "delegatedGroup" values _tc = ThreadConfig() @classmethod def initializeHandler(cls, serviceInfoDict): """initialize handler""" log = LOG.getSubLogger("initializeHandler") for seName in DMSHelpers().getStorageElements(): se = StorageElement(seName) # TODO: once we finally merge _allProtocolParameters with the # standard paramaters in the StorageBase, this will be much neater for storagePlugin in se.storages: storageParam = storagePlugin._allProtocolParameters # pylint: disable=protected-access if (storageParam.get("Protocol") == "s3" and "Aws_access_key_id" in storageParam and "Aws_secret_access_key" in storageParam): cls._S3Storages[seName] = storagePlugin log.debug("Add %s to the list of usable S3 storages" % seName) break log.info("S3Gateway initialized storages", "%s" % list(cls._S3Storages)) cls._fc = FileCatalog() return S_OK() def _hasAccess(self, lfn, s3_method): """Check if we have permission to execute given operation on the given file (if exists) or its directory""" opType = self._s3ToFC_methods.get(s3_method) if not opType: return S_ERROR(errno.EINVAL, "Unknown S3 method %s" % s3_method) return returnSingleResult(self._fc.hasAccess(lfn, opType)) types_createPresignedUrl = [ six.string_types, six.string_types, (dict, list), six.integer_types ] def export_createPresignedUrl(self, storageName, s3_method, urls, expiration): """Generate a presigned URL for a given object, given method, and given storage Permissions are checked against the DFC :param storageName: SE name :param s3_method: name of the S3 client method we want to perform. :param urls: Iterable of urls. If s3_method is put_object, it must be a dict <url:fields> where fields is a dictionary (see ~DIRAC.Resources.Storage.S3Storage.S3Storage.createPresignedUrl) :param expiration: duration of the token """ log = LOG.getSubLogger("createPresignedUrl") if s3_method == "put_object" and not isinstance(urls, dict): return S_ERROR(errno.EINVAL, "urls has to be a dict <url:fields>") # Fetch the remote credentials, and set them in the ThreadConfig # This allows to perform the FC operations on behalf of the user credDict = self.getRemoteCredentials() if not credDict: # If we can't obtain remote credentials, consider it permission denied return S_ERROR(errno.EACCES, "Could not obtain remote credentials") self._tc.setDN(credDict["DN"]) self._tc.setGroup(credDict["group"]) successful = {} failed = {} s3Plugin = self._S3Storages[storageName] for url in urls: try: log.verbose( "Creating presigned URL", "SE: %s Method: %s URL: %s Expiration: %s" % (storageName, s3_method, url, expiration), ) # Finding the LFN to query the FC # I absolutely hate doing such path mangling but well.... res = s3Plugin._getKeyFromURL(url) # pylint: disable=protected-access if not res["OK"]: failed[url] = res["Message"] log.debug("Could not parse the url %s %s" % (url, res)) continue lfn = "/" + res["Value"] log.debug("URL: %s -> LFN %s" % (url, lfn)) # Checking whether access is permitted res = self._hasAccess(lfn, s3_method) if not res["OK"]: failed[url] = res["Message"] continue if not res["Value"]: failed[url] = "Permission denied" continue res = returnSingleResult( s3Plugin.createPresignedUrl({url: urls.get("Fields")}, s3_method, expiration=expiration)) log.debug("Presigned URL for %s: %s" % (url, res)) if res["OK"]: successful[url] = res["Value"] else: failed["url"] = res["Message"] except Exception as e: log.exception("Exception presigning URL") failed[url] = repr(e) return S_OK({"Successful": successful, "Failed": failed})
class WebHandler(tornado.web.RequestHandler): __disetConfig = ThreadConfig() __log = False # Auth requirements AUTH_PROPS = None # Location of the handler in the URL LOCATION = "" # URL Schema with holders to generate handler urls URLSCHEMA = "" # RE to extract group and setup PATH_RE = "" def threadTask(self, method, *args, **kwargs): if tornado.version < '5.0.0': return self.threadTaskOld(method, *args, **kwargs) else: return self.threadTaskExecutor(method, *args, **kwargs) # Helper function to create threaded gen.Tasks with automatic callback and execption handling @deprecated("Only for Tornado 4.x.x and DIRAC v6r20") def threadTaskOld(self, method, *args, **kwargs): """ Helper method to generate a gen.Task and automatically call the callback when the real method ends. THIS IS SPARTAAAAAAAAAA. SPARTA has improved using futures ;) """ # Save the task to access the runner genTask = False # This runs in the separate thread, calls the callback on finish and takes into account exceptions def cbMethod(*cargs, **ckwargs): cb = ckwargs.pop('callback') method = cargs[0] disetConf = cargs[1] cargs = cargs[2] self.__disetConfig.reset() self.__disetConfig.load(disetConf) ioloop = tornado.ioloop.IOLoop.instance() try: result = method(*cargs, **ckwargs) ioloop.add_callback(functools.partial(cb, result)) except Exception as excp: gLogger.error("Following exception occured %s" % excp) exc_info = sys.exc_info() genTask.set_exc_info(exc_info) ioloop.add_callback(lambda: genTask.exception()) # Put the task in the thread :) def threadJob(tmethod, *targs, **tkwargs): tkwargs['callback'] = tornado.stack_context.wrap( tkwargs['callback']) targs = (tmethod, self.__disetDump, targs) gThreadPool.submit(cbMethod, *targs, **tkwargs) # Return a YieldPoint genTask = tornado.gen.Task(threadJob, method, *args, **kwargs) return genTask def threadTaskExecutor(self, method, *args, **kwargs): def threadJob(*targs, **tkwargs): args = targs[0] disetConf = targs[1] self.__disetConfig.reset() self.__disetConfig.load(disetConf) return method(*args, **tkwargs) targs = (args, self.__disetDump) return tornado.ioloop.IOLoop.current().run_in_executor( gThreadPool, functools.partial(threadJob, *targs, **kwargs)) def __disetBlockDecor(self, func): def wrapper(*args, **kwargs): raise RuntimeError( "All DISET calls must be made from inside a Threaded Task!") return wrapper def __init__(self, *args, **kwargs): """ Initialize the handler """ super(WebHandler, self).__init__(*args, **kwargs) if not WebHandler.__log: WebHandler.__log = gLogger.getSubLogger(self.__class__.__name__) self.__credDict = {} self.__setup = Conf.setup() self.__processCredentials() self.__disetConfig.reset() self.__disetConfig.setDecorator(self.__disetBlockDecor) self.__disetDump = self.__disetConfig.dump() match = self.PATH_RE.match(self.request.path) self._pathResult = self.__checkPath(*match.groups()) self.__sessionData = SessionData(self.__credDict, self.__setup) def __processCredentials(self): """ Extract the user credentials based on the certificate or what comes from the balancer """ if not self.request.protocol == "https": return # OIDC auth method def oAuth2(): if self.get_secure_cookie("AccessToken"): access_token = self.get_secure_cookie("AccessToken") url = Conf.getCSValue( "TypeAuths/%s/authority" % typeAuth) + '/userinfo' heads = { 'Authorization': 'Bearer ' + access_token, 'Content-Type': 'application/json' } if 'error' in requests.get(url, headers=heads, verify=False).json(): self.log.error('OIDC request error: %s' % requests.get( url, headers=heads, verify=False).json()['error']) return ID = requests.get(url, headers=heads, verify=False).json()['sub'] result = getUsernameForID(ID) if result['OK']: self.__credDict['username'] = result['Value'] result = getDNForUsername(self.__credDict['username']) if result['OK']: self.__credDict['validDN'] = True self.__credDict['DN'] = result['Value'][0] result = getCAForUsername(self.__credDict['username']) if result['OK']: self.__credDict['issuer'] = result['Value'][0] return # Type of Auth if not self.get_secure_cookie("TypeAuth"): self.set_secure_cookie("TypeAuth", 'Certificate') typeAuth = self.get_secure_cookie("TypeAuth") self.log.info("Type authentication: %s" % str(typeAuth)) if typeAuth == "Visitor": return retVal = Conf.getCSSections("TypeAuths") if retVal['OK']: if typeAuth in retVal.get("Value"): method = Conf.getCSValue("TypeAuths/%s/method" % typeAuth, 'default') if method == "oAuth2": oAuth2() # NGINX if Conf.balancer() == "nginx": headers = self.request.headers if headers['X-Scheme'] == "https" and headers[ 'X-Ssl_client_verify'] == 'SUCCESS': DN = headers['X-Ssl_client_s_dn'] if not DN.startswith('/'): items = DN.split(',') items.reverse() DN = '/' + '/'.join(items) self.__credDict['DN'] = DN self.__credDict['issuer'] = headers['X-Ssl_client_i_dn'] result = Registry.getUsernameForDN(DN) if not result['OK']: self.__credDict['validDN'] = False else: self.__credDict['validDN'] = True self.__credDict['username'] = result['Value'] return # TORNADO derCert = self.request.get_ssl_certificate(binary_form=True) if not derCert: return pemCert = ssl.DER_cert_to_PEM_cert(derCert) chain = X509Chain() chain.loadChainFromString(pemCert) result = chain.getCredentials() if not result['OK']: self.log.error("Could not get client credentials %s" % result['Message']) return self.__credDict = result['Value'] # Hack. Data coming from OSSL directly and DISET difer in DN/subject try: self.__credDict['DN'] = self.__credDict['subject'] except KeyError: pass def _request_summary(self): """ Return a string returning the summary of the request """ summ = super(WebHandler, self)._request_summary() cl = [] if self.__credDict.get('validDN', False): cl.append(self.__credDict['username']) if self.__credDict.get('validGroup', False): cl.append("@%s" % self.__credDict['group']) cl.append(" (%s)" % self.__credDict['DN']) summ = "%s %s" % (summ, "".join(cl)) return summ @property def log(self): return self.__log @classmethod def getLog(cls): return cls.__log def getUserDN(self): return self.__credDict.get('DN', '') def getUserName(self): return self.__credDict.get('username', '') def getUserGroup(self): return self.__credDict.get('group', '') def getUserSetup(self): return self.__setup def getUserProperties(self): return self.__sessionData.getData().properties def isRegisteredUser(self): return self.__credDict.get('validDN', "") and self.__credDict.get( 'validGroup', "") def getSessionData(self): return self.__sessionData.getData() def actionURL(self, action=""): """ Given an action name for the handler, return the URL """ if action == "index": action = "" group = self.getUserGroup() if group: group = "/g:%s" % group setup = self.getUserSetup() if setup: setup = "/s:%s" % setup location = self.LOCATION if location: location = "/%s" % location ats = dict(action=action, group=group, setup=setup, location=location) return self.URLSCHEMA % ats def __auth(self, handlerRoute, group, method): """ Authenticate request :param str handlerRoute: the name of the handler :param str group: DIRAC group :param str method: the name of the method :return: bool """ userDN = self.getUserDN() if group: self.__credDict['group'] = group else: if userDN: result = Registry.findDefaultGroupForDN(userDN) if result['OK']: self.__credDict['group'] = result['Value'] self.__credDict['validGroup'] = False if type(self.AUTH_PROPS) not in (types.ListType, types.TupleType): self.AUTH_PROPS = [ p.strip() for p in self.AUTH_PROPS.split(",") if p.strip() ] auth = AuthManager(Conf.getAuthSectionForHandler(handlerRoute)) ok = auth.authQuery(method, self.__credDict, self.AUTH_PROPS) if ok: if userDN: self.__credDict['validGroup'] = True self.log.info("AUTH OK: %s by %s@%s (%s)" % (handlerRoute, self.__credDict['username'], self.__credDict['group'], userDN)) else: self.__credDict['validDN'] = False self.log.info("AUTH OK: %s by visitor" % (handlerRoute)) elif self.isTrustedHost(self.__credDict.get('DN')): self.log.info("Request is coming from Trusted host") return True else: self.log.info("AUTH KO: %s by %s@%s" % (handlerRoute, userDN, group)) return ok def isTrustedHost(self, dn): """ Check if the request coming from a TrustedHost :param str dn: certificate DN :return: bool if the host is Trusrted it return true otherwise false """ retVal = CS.getHostnameForDN(dn) if retVal['OK']: hostname = retVal['Value'] if Properties.TRUSTED_HOST in CS.getPropertiesForHost( hostname, []): return True return False def __checkPath(self, setup, group, route): """ Check the request, auth, credentials and DISET config """ if route[-1] == "/": methodName = "index" handlerRoute = route else: iP = route.rfind("/") methodName = route[iP + 1:] handlerRoute = route[:iP] if setup: self.__setup = setup if not self.__auth(handlerRoute, group, methodName): return WErr(401, "Unauthorized.") DN = self.getUserDN() if DN: self.__disetConfig.setDN(DN) group = self.getUserGroup() if group: self.__disetConfig.setGroup(group) self.__disetConfig.setSetup(setup) self.__disetDump = self.__disetConfig.dump() return WOK(methodName) def get(self, setup, group, route): if not self._pathResult.ok: raise self._pathResult methodName = "web_%s" % self._pathResult.data try: mObj = getattr(self, methodName) except AttributeError as e: self.log.fatal("This should not happen!! %s" % e) raise tornado.web.HTTPError(404) return mObj() def post(self, *args, **kwargs): return self.get(*args, **kwargs) def write_error(self, status_code, **kwargs): self.set_status(status_code) cType = "text/plain" data = self._reason if 'exc_info' in kwargs: ex = kwargs['exc_info'][1] trace = traceback.format_exception(*kwargs["exc_info"]) if not isinstance(ex, WErr): data += "\n".join(trace) else: if self.settings.get("debug"): self.log.error("Request ended in error:\n %s" % "\n ".join(trace)) data = ex.msg if isinstance(data, dict): cType = "application/json" data = json.dumps(data) self.set_header('Content-Type', cType) self.finish(data)
class UPHandler(WebHandler): AUTH_PROPS = "authenticated" __tc = ThreadConfig() def prepare(self): if not self.isRegisteredUser(): raise WErr(401, "Not a registered user") self.set_header("Pragma", "no-cache") self.set_header("Cache-Control", "max-age=0, no-store, no-cache, must-revalidate") #Do not use the defined user setup. Use the web one to show the same profile independenly of # user setup self.__tc.setSetup(False) def __getUP(self): try: obj = self.request.arguments['obj'][-1] app = self.request.arguments['app'][-1] except KeyError as excp: raise WErr(400, "Missing %s" % excp) return UserProfileClient("Web/%s/%s" % (obj, app)) @asyncGen def web_saveAppState(self): up = self.__getUP() try: name = self.request.arguments['name'][-1] state = self.request.arguments['state'][-1] except KeyError as excp: raise WErr(400, "Missing %s" % excp) data = base64.b64encode(zlib.compress(DEncode.encode(state), 9)) result = yield self.threadTask(up.storeVar, name, data) if not result['OK']: raise WErr.fromSERROR(result) self.set_status(200) self.finish() @asyncGen def web_loadAppState(self): up = self.__getUP() try: name = self.request.arguments['name'][-1] except KeyError as excp: raise WErr(400, "Missing %s" % excp) result = yield self.threadTask(up.retrieveVar, name) if not result['OK']: raise WErr.fromSERROR(result) data = result['Value'] data, count = DEncode.decode(zlib.decompress(base64.b64decode(data))) self.finish(data) @asyncGen def web_listAppState(self): up = self.__getUP() result = yield self.threadTask(up.retrieveAllVars) if not result['OK']: raise WErr.fromSERROR(result) data = result['Value'] for k in data: #Unpack data data[k] = json.loads( DEncode.decode(zlib.decompress(base64.b64decode(data[k])))[0]) self.finish(data) @asyncGen def web_delAppState(self): up = self.__getUP() try: name = self.request.arguments['name'][-1] except KeyError as excp: raise WErr(400, "Missing %s" % excp) result = yield self.threadTask(up.deleteVar, name) if not result['OK']: raise WErr.fromSERROR(result) self.finish()
class UPHandler(WebHandler): AUTH_PROPS = "authenticated" __tc = ThreadConfig() def prepare(self): if not self.isRegisteredUser(): raise WErr(401, "Not a registered user") self.set_header("Pragma", "no-cache") self.set_header("Cache-Control", "max-age=0, no-store, no-cache, must-revalidate") # Do not use the defined user setup. Use the web one to show the same profile independenly of # user setup self.__tc.setSetup(False) def __getUP(self): try: obj = self.request.arguments['obj'][-1] app = self.request.arguments['app'][-1] except KeyError as excp: raise WErr(400, "Missing %s" % excp) return UserProfileClient("Web/%s/%s" % (obj, app)) @asyncGen def web_saveAppState(self): up = self.__getUP() try: name = self.request.arguments['name'][-1] state = self.request.arguments['state'][-1] except KeyError as excp: raise WErr(400, "Missing %s" % excp) data = base64.b64encode(zlib.compress(DEncode.encode(state), 9)) # before we save the state (modify the state) we have to remeber the actual access: ReadAccess and PublishAccess result = yield self.threadTask(up.getVarPermissions, name) if result['OK']: access = result['Value'] else: access = { 'ReadAccess': 'USER', 'PublishAccess': 'USER' } # this is when the application/desktop does not exists. result = yield self.threadTask(up.storeVar, name, data) if not result['OK']: raise WErr.fromSERROR(result) # change the access to the application/desktop result = yield self.threadTask(up.setVarPermissions, name, access) if not result['OK']: raise WErr.fromSERROR(result) self.set_status(200) self.finish() @asyncGen def web_makePublicAppState(self): up = self.__getUP() try: name = self.request.arguments['name'][-1] except KeyError as excp: raise WErr(400, "Missing %s" % excp) try: access = self.request.arguments['access'][-1].upper() except KeyError as excp: access = 'ALL' if access not in ('ALL', 'VO', 'GROUP', 'USER'): raise WErr(400, "Invalid access") revokeAccess = {'ReadAccess': access} if access == 'USER': # if we make private a state, # we have to revoke from the public as well revokeAccess['PublishAccess'] = 'USER' # TODO: Check access is in either 'ALL', 'VO' or 'GROUP' result = yield self.threadTask(up.setVarPermissions, name, revokeAccess) if not result['OK']: raise WErr.fromSERROR(result) self.set_status(200) self.finish() @asyncGen def web_loadAppState(self): up = self.__getUP() try: name = self.request.arguments['name'][-1] except KeyError as excp: raise WErr(400, "Missing %s" % excp) result = yield self.threadTask(up.retrieveVar, name) if not result['OK']: raise WErr.fromSERROR(result) data = result['Value'] data, count = DEncode.decode(zlib.decompress(base64.b64decode(data))) self.finish(data) @asyncGen def web_loadUserAppState(self): up = self.__getUP() try: user = self.request.arguments['user'][-1] group = self.request.arguments['group'][-1] name = self.request.arguments['name'][-1] except KeyError as excp: raise WErr(400, "Missing %s" % excp) result = yield self.threadTask(up.retrieveVarFromUser, user, group, name) if not result['OK']: raise WErr.fromSERROR(result) data = result['Value'] data, count = DEncode.decode(zlib.decompress(base64.b64decode(data))) self.finish(data) @asyncGen def web_listAppState(self): up = self.__getUP() result = yield self.threadTask(up.retrieveAllVars) if not result['OK']: raise WErr.fromSERROR(result) data = result['Value'] for k in data: # Unpack data data[k] = json.loads( DEncode.decode(zlib.decompress(base64.b64decode(data[k])))[0]) self.finish(data) @asyncGen def web_delAppState(self): up = self.__getUP() try: name = self.request.arguments['name'][-1] except KeyError as excp: raise WErr(400, "Missing %s" % excp) result = yield self.threadTask(up.deleteVar, name) if not result['OK']: raise WErr.fromSERROR(result) self.finish() @asyncGen def web_listPublicDesktopStates(self): up = self.__getUP() result = yield self.threadTask(up.listAvailableVars) if not result['OK']: raise WErr.fromSERROR(result) data = result['Value'] paramNames = ['UserName', 'Group', 'VO', 'desktop'] records = [] for i in data: records += [dict(zip(paramNames, i))] sharedDesktops = {} for i in records: result = yield self.threadTask(up.getVarPermissions, i['desktop']) if not result['OK']: raise WErr.fromSERROR(result) if result['Value']['ReadAccess'] == 'ALL': print i['UserName'], i['Group'], i result = yield self.threadTask(up.retrieveVarFromUser, i['UserName'], i['Group'], i['desktop']) if not result['OK']: raise WErr.fromSERROR(result) if i['UserName'] not in sharedDesktops: sharedDesktops[i['UserName']] = {} sharedDesktops[i['UserName']][i['desktop']] = json.loads( DEncode.decode( zlib.decompress(base64.b64decode( result['Value'])))[0]) sharedDesktops[i['UserName']]['Metadata'] = i else: sharedDesktops[i['UserName']][i['desktop']] = json.loads( DEncode.decode( zlib.decompress(base64.b64decode( result['Value'])))[0]) sharedDesktops[i['UserName']]['Metadata'] = i self.finish(sharedDesktops) @asyncGen def web_makePublicDesktopState(self): up = UserProfileClient("Web/application/desktop") try: name = self.request.arguments['name'][-1] except KeyError as excp: raise WErr(400, "Missing %s" % excp) try: access = self.request.arguments['access'][-1].upper() except KeyError as excp: access = 'ALL' if access not in ('ALL', 'VO', 'GROUP', 'USER'): raise WErr(400, "Invalid access") # TODO: Check access is in either 'ALL', 'VO' or 'GROUP' result = yield self.threadTask(up.setVarPermissions, name, {'ReadAccess': access}) if not result['OK']: raise WErr.fromSERROR(result) self.set_status(200) self.finish() @asyncGen def web_changeView(self): up = self.__getUP() try: desktopName = self.request.arguments['desktop'][-1] view = self.request.arguments['view'][-1] except KeyError as excp: raise WErr(400, "Missing %s" % excp) result = yield self.threadTask(up.retrieveVar, desktopName) if not result['OK']: raise WErr.fromSERROR(result) data = result['Value'] oDesktop = json.loads( DEncode.decode(zlib.decompress(base64.b64decode(data)))[0]) oDesktop[unicode('view')] = unicode(view) oDesktop = json.dumps(oDesktop) data = base64.b64encode(zlib.compress(DEncode.encode(oDesktop), 9)) result = yield self.threadTask(up.storeVar, desktopName, data) if not result['OK']: raise WErr.fromSERROR(result) self.set_status(200) self.finish() @asyncGen def web_listPublicStates(self): session = self.getSessionData() user = session["user"]["username"] up = self.__getUP() retVal = yield self.threadTask(up.getUserProfileNames, {'PublishAccess': 'ALL'}) if not retVal['OK']: raise WErr.fromSERROR(retVal) data = retVal['Value'] if data == None: raise WErr(404, "There are no public states!") paramNames = ['user', 'group', 'vo', 'name'] mydesktops = { 'name': 'My Desktops', 'group': '', 'vo': '', 'user': '', 'iconCls': 'my-desktop', 'children': [] } shareddesktops = { 'name': 'Shared Desktops', 'group': '', 'vo': '', 'user': '', 'expanded': 'true', 'iconCls': 'shared-desktop', 'children': [] } myapplications = { 'name': 'My Applications', 'group': '', 'vo': '', 'user': '', 'children': [] } sharedapplications = { 'name': 'Shared Applications', 'group': '', 'vo': '', 'user': '', 'expanded': 'true', 'iconCls': 'shared-desktop', 'children': [] } desktopsApplications = { 'text': '.', 'children': [{ 'name': 'Desktops', 'group': '', 'vo': '', 'user': '', 'children': [mydesktops, shareddesktops] }, { 'name': 'Applications', 'group': '', 'vo': '', 'user': '', 'children': [myapplications, sharedapplications] }] } type = '' for i in data: application = i.replace('Web/application/', '') up = UserProfileClient(i) retVal = up.listAvailableVars() if not retVal['OK']: raise WErr.fromSERROR(retVal) else: states = retVal['Value'] for state in states: record = dict(zip(paramNames, state)) record['app'] = application retVal = yield self.threadTask(up.getVarPermissions, record['name']) if not retVal['OK']: raise WErr.fromSERROR(retVal) else: permissions = retVal['Value'] if permissions['PublishAccess'] == 'ALL': if application == 'desktop': record['type'] = 'desktop' record['leaf'] = 'true' record['iconCls'] = 'core-desktop-icon', if record['user'] == user: mydesktops['children'].append(record) else: shareddesktops['children'].append(record) else: record['type'] = 'application' record['leaf'] = 'true' record['iconCls'] = 'core-application-icon' if record['user'] == user: myapplications['children'].append(record) else: sharedapplications['children'].append( record) self.finish(desktopsApplications) @asyncGen def web_publishAppState(self): up = self.__getUP() try: name = self.request.arguments['name'][-1] except KeyError as excp: raise WErr(400, "Missing %s" % excp) try: access = self.request.arguments['access'][-1].upper() except KeyError as excp: access = 'ALL' if access not in ('ALL', 'VO', 'GROUP', 'USER'): raise WErr(400, "Invalid access") result = yield self.threadTask(up.setVarPermissions, name, { 'PublishAccess': access, 'ReadAccess': access }) if not result['OK']: raise WErr.fromSERROR(result) self.set_status(200) self.finish()