Source code for paradrop.backend.pdfcd.server

####################################################################
# Copyright 2013-2015 All Rights Reserved
# Authors: The Paradrop Team
###################################################################

'''
pdfcd.server.
Contains the classes required to establish a RESTful API server using Twisted.
'''

from twisted.web.server import Site
from twisted.internet import reactor

from pdtools.lib.output import out
from pdtools.lib import store, riffle, names

from pdtools.lib.pdutils import timeflt, str2json, json2str
from paradrop.lib.api import pdapi
from paradrop.lib.api import pdrest
from paradrop.lib import settings
from paradrop.lib.utils import dockerapi

from paradrop.backend import fc


# Import local refs to pdfcd utilities
from . import apiutils
from . import apichute

# temp
from paradrop.backend.pdfcd.apiinternal import Base
from paradrop.backend.pdfcd import apiinternal


[docs]class ParadropAPIServer(pdrest.APIResource): """ The main API server module. This sets up all of the submodules which should contain different types of RESTful API calls. """ def __init__(self, lclreactor): pdrest.APIResource.__init__(self) self.reactor = lclreactor # Establish the configurer which is the launch point for all chute related endeavors self.configurer = fc.configurer.PDConfigurer(None, lclreactor) # Allow each module to register their API calls apichute.ChuteAPI(self) def _complete(self, update): """ THREADED:call from event thread All API operations require some work to happen outside of the API module, therefore when a request comes in we tell it to wait a minute (a NOT_DONE_YET return) and then later on we use the reference we have to the request from @update to write the response back to the user and close the connection. :param name: update :param type: UpdateObject """ # Wrap in a try-catch, if the connection is closed by the client before # we have a chance to write out our results then this below will result # in an exception being raised try: update.pkg.request.write(json2str(update.result)) update.pkg.request.finish() # TODO don't catch all exceptions here except Exception as e: pass
[docs] def complete(self, update): """ Kicks off the properly threaded call to complete the API call that was passed to PDConfigurer. Since the PDConfigurer module has its own thread that runs outside of the main event loop, we have to call back into the event system properly in order to keep any issues of concurrency at bay. """ self.reactor.callFromThread(self._complete, update)
[docs] def preprocess(self, request, checkThresh, tictoc): """ Check if failure attempts for the user name has met the threshold. Arguments: request : checkThresh : If None, no checking. Tuple of arguments for checking thresholds ip: IP of client in string format token: sessionToken if found, or None username: username if signin, or None failureDict: the dict for failure history ticktoc: If none, do not track the usage. The start time of the API call Return: str: if threshold is met None: if ok """ if(checkThresh): ip, token, key, failureDict = checkThresh # Check if IP is in whitelist ipaddr = apiutils.unpackIPAddr(ip) for ipnet in self.WHITELIST_IP: if(apiutils.addressInNetwork(ipaddr, ipnet)): out.verbose('Request from white list: %s\n' % (ip)) return None if(key in failureDict): if(failureDict[key].attempts >= settings.DBAPI_FAILURE_THRESH): out.err('Threshold met: %s %s!\n' % (ip, key)) if(tictoc is None): usage = None else: usage = (tictoc, None) self.failprocess(ip, request, (ip, self.clientFailures), None, usage, pdapi.getResponse(pdapi.ERR_THRESHOLD)) duration = 0 # self.perf.toc(tictoc) # Log the info of this call # TODO self.usageTracker.addTrackInfo(ip, 'Null', request.path, self.usageTracker.FAIL_THRESH, duration, request.content.getvalue()) return "Threshold Met!" # Otherwise everything is ok return None
[docs] def postprocess(self, request, key, failureDict, logUsage): """ If the client is successful in their request, we should: * reset their failure attempts if failureDict is not none. * set success response code * If usage is not none, add usage track info of the api call """ request.setResponseCode(*pdapi.getResponse(pdapi.OK)) if(logUsage): tictoc, ip, devid = logUsage duration = 0 # self.perf.toc(tictoc) # when devid is none, we log "Null" into the database if(devid is None): devid = "Null" # Log the info of this call # TODO self.usageTracker.addTrackInfo(ip, devid, request.path, self.usageTracker.SUCCESS, duration, request.content.getvalue()) if(failureDict is not None): if(key in failureDict): out.info('Clearing %s from failure list\n' % (key)) del failureDict[key]
[docs] def failprocess(self, ip, request, logFailure, errorStmt, logUsage, errType): """ If logFailure is not None, Update the failureDict when the request does something wrong If logUsage is not None, log the usage track info. Arguments: ip : IP of client request : the request we received logFailure : If none, we do not log this failure to failure dict. Otherwise it is a tuple of key : the key to use in the failureDict failureDict : the dict record the failure attempt history errorStmt : A string to return to the user, looks like 'Malformed Body: %s' so that we can add things like "Number of attempts remaining: 2" to the response logUsage : A tuple of (tictoc and devid) used for usage tracker errorResponse: The error code to set response code Returns: String to respond to the client """ time = timeflt() # Set the response error code if(errType == pdapi.ERR_BADPARAM): request.setResponseCode(*pdapi.getResponse(errType, "")) else: request.setResponseCode(*pdapi.getResponse(errType)) headers = request.received_headers if(logUsage is not None): tictoc, devid = logUsage if(devid is None): devid = "Null" duration = 0 # self.perf.toc(tictoc) # Log the info of this call # TODO self.usageTracker.addTrackInfo(ip, devid , request.path, self.usageTracker.FAIL_AUTH, duration, request.content.getvalue()) if(logFailure is not None): key, failureDict = logFailure # update the accessInfo if(key in failureDict): failureDict[key].update(ip, headers, time) else: failureDict[key] = AccessInfo(ip, headers, time) attempts = failureDict[key].attempts out.warn('Failure access recorded: %s Attempts: %d\n' % (key, attempts)) remaining = str(max(0, settings.DBAPI_FAILURE_THRESH - attempts)) if(errorStmt): return errorStmt % ("%s attempts remaining" % remaining) if(errorStmt): return errorStmt % ("Null")
@pdrest.GET('^/v1/test')
[docs] def GET_test(self, request): """ A Simple test method to ping if the API server is working properly. """ request.setHeader('Access-Control-Allow-Origin', settings.PDFCD_HEADER_VALUE) ip = apiutils.getIP(request) out.info('Test called (%s)\n' % (ip)) request.setResponseCode(*pdapi.getResponse(pdapi.OK)) return "SUCCESS\n"
@pdrest.ALL('^/')
[docs] def default(self, request): """ A dummy catch for all API calls that are not caught by any other module. """ ip = apiutils.getIP(request) uri = request.uri method = request.method # Get data about who done it out.err("Default caught API call (%s => %s:%s)\n" % (ip, method, uri)) # Someone might be trying something bad, track their IP res = self.preprocess(request, (ip, None, ip, self.defaultFailures), tictoc) if(res): return res self.failprocess(ip, request, (ip, self.defaultFailures), None, (tictoc, None), pdapi.ERR_BADMETHOD) return ""
############################################################################### # Initialization ###############################################################################
[docs]def initializeSystem(): """ Perform some initialization steps such as writing important configuration. """ dockerapi.writeDockerConfig()
############################################################################### # Main function ###############################################################################
[docs]def setup(args=None): """ This is the main setup function to establish the TCP listening logic for the API server. This code also takes into account development or unit test mode. """ # Setup API server api = ParadropAPIServer(reactor) api.putChild('internal', Base(apiinternal, allowNone=True)) site = Site(api, timeout=None) # Development mode if(args and args.development): thePort = settings.PDFCD_PORT + 10000 out.info('Using DEVELOPMENT variables') # Disable sending the error traceback to the client site.displayTracebacks = True elif(args and args.unittest): thePort = settings.DBAPI_PORT + 20000 out.info('Running under unittest mode') site.displayTracebacks = True else: thePort = settings.PDFCD_PORT site.displayTracebacks = False initializeSystem() # Setup the port we listen on reactor.listenTCP(thePort, site) # Never return from here reactor.run()