def login(self,env,start_response): cl = vanilla.getContentLength(env) if cl == None: return vanilla.http_error(411,env,start_response,'missing Content-Length header') content = env['wsgi.input'].read(cl) query = urlparse.parse_qs(content) if 'username' not in query: return vanilla.http_error(400,env,start_response,msg='missing username') #Use first occurence from query string username = query['username'][0] if 'password' not in query: return vanilla.http_error(400,env,start_response,msg='missing password') #Use first occurence from query string password = query['password'][0] userId = self.authenticateUser(username,password) if userId == None: self.logger.info('Failed authorization for user:%s' , username) return vanilla.sendJsonWsgiResponse(env,start_response,{'error':'bad username or password'}) session = self.sm.startSession(username,userId) return vanilla.sendJsonWsgiResponse(env,start_response,self.getResponseForSession(session),additionalHeaders=[session.getCookie()])
def claimInvite(self,env,start_response,secret,username,password): secret = base64.urlsafe_b64decode(secret + '=') try: newuser = self.users.claimInvite(secret,username,password) except users.UserAlreadyExists: return vanilla.http_error(409,env,start_response,msg='User with that name already exists') except ValueError as e: return vanilla.http_error(404,env,start_response,msg=e.message) return vanilla.sendJsonWsgiResponse(env,start_response,{'href' : newuser})
def changePassword(self,env,start_response,session,uid,password): uid = int(uid,16) if None == self.authmgr.changePassword(uid,password): return vanilla.http_error(400,env,start_response) return vanilla.sendJsonWsgiResponse(env,start_response,{})
def authenticateUser(username,password): #Password comes across as 64 bytes of base64 encoded data #with trailing ='s lopped off. password += '==' if len(password) != 88: #64 bytes in base64 is length 88 return None return vanilla.http_error(400,env,start_response,msg='password too short') try: password = base64.urlsafe_b64decode(password) except TypeError: return None return vanilla.http_error(400,env,start_response,msg='password poorly formed') return authmgr.authenticateUser(username,password)
def changeRolesOfUser(self,env,start_response,session,uid,roles): uid = int(uid,16) try: self.users.setUserRoles(roles,uid) except ValueError as e: return vanilla.http_error(400,env,start_response,msg=e.message) return vanilla.sendJsonWsgiResponse(env,start_response,{'roles':self.users.getUserRoles(uid)})
def __call__(self, env, start_response): #Extract and normalize the path #Posix path may not be the best approach here but #no alternate has been found pathInfo = posixpath.normpath(env['PATH_INFO']) #Split the path into components. Drop the first #since it should always be the empty string pathComponents = pathInfo.split('/')[1 + self.pathDepth:] env['fairywren.pathComponents'] = pathComponents requestMethod = env['REQUEST_METHOD'] #The default is request not found errorCode = 404 #Find a resource with a patch matching the requested one for resource in self.resources: kwargs = resource.wants(pathComponents) if kwargs == None: continue #If the method does not agree with the resource, the #code is method not supported if requestMethod != resource.method: errorCode = 405 continue self.logger.debug('%s:%s handled by %s', requestMethod, pathInfo, resource.getName()) if resource.requireAuthentication: session = self.sm.getSession(env) if session == None: return vanilla.sendJsonWsgiResponse( env, start_response, restInterface.NOT_AUTHENTICATED) #Check to see if the resource requires authorization if resource.requireAuthorization: authorized = resource.allowSelf and resource.getOwnerId( *pathComponents) == session.getId() authorized |= self.authorizeUser(session, resource.allowedRoles) if not authorized: self.logger.debug('%s:%s not authorized for %s', requestMethod, pathInfo, session.getUsername()) return vanilla.sendJsonWsgiResponse( env, start_response, restInterface.NOT_AUTHORIZED) return resource(env, start_response, session, **kwargs) else: return resource(env, start_response, **kwargs) self.logger.info('%s:%s not handled, %d', requestMethod, pathInfo, errorCode) return vanilla.http_error(errorCode, env, start_response)
def inviteStatus(self,env,start_response,secret): secret = base64.urlsafe_b64decode(secret + '=') try: claimed =self.users.getInviteState(secret) except ValueError as e: return vanilla.http_error(404,env,start_response,msg=e.message) return vanilla.sendJsonWsgiResponse(env,start_response,{'claimed':claimed})
def updateTorrent(self,env,start_response,session,uid,extended,title): uid = int(uid,16) try: self.torrents.updateTorrent(uid,title,extended) except ValueError as e: return vanilla.http_error(404,env,start_response,msg=e.message) return vanilla.sendJsonWsgiResponse(env,start_response,{})
def deleteTorrent(self,env,start_response,session,uid): uid = int(uid,16) try: torrent = self.torrents.deleteTorrent(uid) except ValueError as e: return vanilla.http_error(404,env,start_response,msg=e.message) return vanilla.sendJsonWsgiResponse(env,start_response,{})
def addUser(self,env,start_response,session,password,username): try: resourceForNewUser,_ = self.users.addUser(username,password) except users.UserAlreadyExists: return vanilla.http_error(409,env,start_response,'user already exists') response = { 'href' : resourceForNewUser } return vanilla.sendJsonWsgiResponse(env,start_response,response)
def searchTorrents(self,env,start_response,session,query): tokens = query.get('token') if tokens == None: return vanilla.http_error(400,env,start_response,'search must have at least one instance of token parameter') if len(tokens) > 5: return vanilla.http_error(400,env,start_response,'search may not have more than 5 tokens') listOfTorrents = [] for torrent in self.torrents.searchTorrents(tokens): torrentInfoHash = torrent.pop('infoHash') torrent.pop('id') seeds, leeches = self.peers.getNumberOfPeers(torrentInfoHash) torrent['seeds'] = seeds torrent['leeches'] = leeches listOfTorrents.append(torrent) return vanilla.sendJsonWsgiResponse(env,start_response, {'torrents': listOfTorrents})
def userInfo(self,env,start_response,session,uid): uid = int(uid,16) response = self.users.getInfo(uid) if response == None: return vanilla.http_error(404,env,start_response) if session.getId() == uid: response['announce'] = { 'href': self.torrents.getAnnounceUrlForUser(uid) } return vanilla.sendJsonWsgiResponse(env,start_response,response)
def torrentInfo(self,env,start_response,session,uid): uid = int(uid,16) response = self.torrents.getInfo(uid) if response == None: return vanilla.http_error(404,env,start_response,msg='no such torrent') torrentInfoHash = response.pop('infoHash') numSeeds,numLeeches = self.peers.getNumberOfPeers(torrentInfoHash) response['extended'] = self.torrents.getExtendedInfo(uid) response['seeds'] = numSeeds response['leeches'] = numLeeches return vanilla.sendJsonWsgiResponse(env,start_response,response)
def listTorrents(self,env,start_response,session): if 'QUERY_STRING' not in env: query = {} else: query = urlparse.parse_qs(env['QUERY_STRING']) if 'search' in query: return self.searchTorrents(env,start_response,session,query) #Use the first occurence of the supplied parameter #With a default resultSize = query.get('resultSize',[self.MAX_TORRENTS_PER_RESULT])[0] try: resultSize = int(resultSize) except ValueError: return vanilla.http_error(400,env,start_response,'resultSize must be integer') #Use the first occurence of the supplied parameter #With a default of zero subset = query.get('subset',[0])[0] try: subset = int(subset) except ValueError: return vanilla.http_error(400,env,start_response,'subset must be integer') listOfTorrents = [] for torrent in self.torrents.getTorrents(resultSize,subset): torrent.pop('id') torrentInfoHash = torrent.pop('infoHash') seeds, leeches = self.peers.getNumberOfPeers(torrentInfoHash) torrent['seeds'] = seeds torrent['leeches'] = leeches listOfTorrents.append(torrent) return vanilla.sendJsonWsgiResponse(env,start_response, {'torrents' : listOfTorrents ,'numSubsets' : int(math.ceil(self.torrents.getNumTorrents() / float(resultSize)))} )
def login(self, env, start_response): cl = vanilla.getContentLength(env) if cl == None: return vanilla.http_error(411, env, start_response, 'missing Content-Length header') content = env['wsgi.input'].read(cl) query = urlparse.parse_qs(content) if 'username' not in query: return vanilla.http_error(400, env, start_response, msg='missing username') #Use first occurence from query string username = query['username'][0] if 'password' not in query: return vanilla.http_error(400, env, start_response, msg='missing password') #Use first occurence from query string password = query['password'][0] userId = self.authenticateUser(username, password) if userId == None: self.logger.info('Failed authorization for user:%s', username) return vanilla.sendJsonWsgiResponse( env, start_response, {'error': 'bad username or password'}) session = self.sm.startSession(username, userId) return vanilla.sendJsonWsgiResponse( env, start_response, self.getResponseForSession(session), additionalHeaders=[session.getCookie()])
def __call__(self,env,start_response): #Extract and normalize the path #Posix path may not be the best approach here but #no alternate has been found pathInfo = posixpath.normpath(env['PATH_INFO']) #Split the path into components. Drop the first #since it should always be the empty string pathComponents = pathInfo.split('/')[1+self.pathDepth:] env['fairywren.pathComponents'] = pathComponents requestMethod = env['REQUEST_METHOD'] #The default is request not found errorCode = 404 #Find a resource with a patch matching the requested one for resource in self.resources: kwargs = resource.wants(pathComponents) if kwargs == None: continue #If the method does not agree with the resource, the #code is method not supported if requestMethod != resource.method: errorCode = 405 continue self.logger.debug('%s:%s handled by %s',requestMethod,pathInfo,resource.getName()) if resource.requireAuthentication: session = self.sm.getSession(env) if session == None: return vanilla.sendJsonWsgiResponse(env,start_response,restInterface.NOT_AUTHENTICATED) #Check to see if the resource requires authorization if resource.requireAuthorization: authorized = resource.allowSelf and resource.getOwnerId(*pathComponents)==session.getId() authorized |= self.authorizeUser(session,resource.allowedRoles) if not authorized: self.logger.debug('%s:%s not authorized for %s',requestMethod,pathInfo,session.getUsername()) return vanilla.sendJsonWsgiResponse(env,start_response,restInterface.NOT_AUTHORIZED) return resource(env,start_response,session,**kwargs) else: return resource(env,start_response,**kwargs) self.logger.info('%s:%s not handled, %d', requestMethod,pathInfo,errorCode) return vanilla.http_error(errorCode,env,start_response)
def downloadTorrent(self,env,start_response,session,uid): uid = int(uid,16) try: torrent = self.torrents.getTorrentForDownload(uid,session.getId()) except ValueError as e: return vanilla.http_error(404,env,start_response,msg=e.message) headers = [('Content-Type','application/x-bittorrent')] headers.append(('Content-Disposition','attachment; filename="%s.torrent"' % vanilla.sanitizeForContentDispositionHeaderFilename(torrent.getTitle()) )) headers.append(('Cache-Control','no-cache')) start_response('200 OK',headers) return [torrent.raw()]
def createTorrent(self,env,start_response,session): if not 'CONTENT_TYPE' in env: return vanilla.http_error(411,env,start_response,'missing Content-Type header') contentType = env['CONTENT_TYPE'] if 'multipart/form-data' not in contentType: return vanilla.http_error(415,env,start_response,'must be form upload') forms,files = multipart.parse_form_data(env) if 'torrent' not in files or 'title' not in forms: return vanilla.http_error(400,env,start_response,'missing torrent or title') try: extended = json.loads(forms.get('extended','{}')) except ValueError: return vanilla.http_error(400,env,start_response,'bad extended info') if not isinstance(extended,dict): return vanilla.http_error(400,env,start_response,'extended info must be dict') data = files['torrent'].raw try: newTorrent = torrents.Torrent.fromBencodedData(data) except ValueError as e: return vanilla.http_error(400,env,start_response,str(e)) response = {} response['redownload'] = newTorrent.scrub() response['redownload'] |= self.torrents.getAnnounceUrlForUser(session.getId())!=newTorrent.getAnnounceUrl() try: url,infoUrl = self.torrents.addTorrent(newTorrent,forms['title'],session.getId(),extended) except ValueError as e: #Thrown when a torrent already exists with this info hash return vanilla.http_error(400,env,start_response,e.message) response['metainfo'] = { 'href' : url } response['info'] = { 'href' : infoUrl } return vanilla.sendJsonWsgiResponse(env,start_response,response)
def announce(self,env,start_response): #Extract and normalize the path #Posix path may not be the best approach here but #no alternate has been found pathInfo = posixpath.normpath(env['PATH_INFO']) #Split the path into components. Drop the first #since it should always be the empty string pathComponents = pathInfo.split('/')[1+self.pathDepth:] #A SHA512 encoded in base64 is 88 characters #but the last two are always '==' so #86 is used here if len(pathComponents) !=2 or len(pathComponents[0]) != 86 or pathComponents[1] != 'announce': return vanilla.http_error(404,env,start_response) #Only GET requests are valid if env['REQUEST_METHOD'] != 'GET': return vanilla.http_error(405,env,start_response) #Add the omitted equals signs back in secretKey = pathComponents[0] + '==' #base64 decode the secret key try: secretKey = base64.urlsafe_b64decode(secretKey) except TypeError: return vanilla.http_error(404,env,start_response) #Extract the IP of the peer peerIp = getClientAddress(env) peerIpAsString = peerIp try: peerIp = dottedQuadToInt(peerIp) except ValueError: return vanilla.http_error(500,env,start_response) #Parse the query string. Absence indicates error if 'QUERY_STRING' not in env: return vanilla.http_error(400,env,start_response) query = urlparse.parse_qs(env['QUERY_STRING']) #List of tuples. Each tuple is # #Parameter name #default value (if any) #type conversion, side-effect free callable params = [] def validateInfoHash(info_hash): #Info hashes are a SHA1 hash, and are always 20 bytes if len(info_hash) != 20: raise ValueError("Length " + str(len(info_hash)) + ' not acceptable') return info_hash params.append(('info_hash',None,validateInfoHash)) def validatePeerId(peer_id): #Peer IDs are a string chosen by the peer to identify itself #and are always 20 bytes if len(peer_id) != 20: raise ValueError("Improper Length") return peer_id params.append(('peer_id',None,validatePeerId)) def validatePort(port): port = int(port) #Ipv4 ports should not be higher than this value if port > 2 ** 16 - 1 or port <= 0: raise ValueError("Port outside of range") return port def validateByteCount(byteCount): byteCount = int(byteCount) if byteCount < 0: raise ValueError('byte count cannot be negative') return byteCount params.append(('port',None,validatePort)) params.append(('uploaded',None,validateByteCount)) params.append(('downloaded',None,validateByteCount)) params.append(('left',None,validateByteCount)) #If the client doesn't specify the compact parameter, it is #safe to assume that compact responses are understood. So a #default value of 1 is used. Additionally, any non zero #value provided assumes the client wants a compact response params.append(('compact',1,int)) def validateEvent(event): event = event.lower() if event not in ['started','stopped','completed']: raise ValueError("Unknown event") return event params.append(('event','update',validateEvent)) maxNumWant = 35 def limitNumWant(numwant): numwant = int(numwant) if numwant < 0: raise ValueError('numwant cannot be negative') numwant = min(numwant,maxNumWant) return numwant params.append(('numwant',maxNumWant,limitNumWant)) #Dictionary holding parameters to query p = dict() #Use the params to generate the parameters for param,defaultValue,typeConversion in params: #If the parameter is in the query, extract the first #occurence and type convert if requested if param in query: p[param] = query[param][0] if typeConversion: try: p[param] = typeConversion(p[param]) except ValueError as e: return vanilla.http_error(400,env,start_response,msg='bad value for ' + param) #If the parameter is not in the query, then #use a default value is present. Otherwise this is an error else: if defaultValue == None: return vanilla.http_error(400,env,start_response,msg='missing ' + param) p[param] = defaultValue #Make sure the secret key is valid userId = self.auth.authenticateSecretKey(secretKey) if userId == None: response = {} response['failure reason'] = 'failed to authenticate secret key' return sendBencodedWsgiResponse(env,start_response,response) #Make sure the info hash is allowed torrentId = self.auth.authorizeInfoHash(p['info_hash']) if torrentId == None: response = {} response['failure reason'] = 'unauthorized info hash' return sendBencodedWsgiResponse(env,start_response,response) #Construct the peers entry peer = peers.Peer(peerIp,p['port'],p['left']) #This is the basic response format response = {} response['interval'] = 5*60 response['complete'] = 0 response['incomplete'] = 0 response['peers'] = [] #This value is set to True if the number of seeds or leeches #changes in the course of processing this result change = False #This value is set to true if the peer is added, false if removed addPeer = False #For all 3 cases here just return peers if p['event'] in ['started','completed','update']: response['complete'] = self.peers.getNumberOfLeeches(p['info_hash']) response['incomplete'] = self.peers.getNumberOfSeeds(p['info_hash']) change = self.peers.updatePeer(p['info_hash'],peer) if change: addPeer = True peersForResponse = self.peers.getPeers(p['info_hash']) #Return a compact response or a traditional response #based on what is requested if p['compact'] != 0: peerStruct = struct.Struct('!IH') maxSize = p['numwant'] * peerStruct.size peersBuffer = array.array('c') for peer in itertools.islice(peersForResponse,0,p['numwant']): peersBuffer.fromstring(peerStruct.pack(peer.ip,peer.port)) response['peers'] = peersBuffer.tostring() else: for peer in itertools.islice(peersForResponse,0,p['numwant']): #For non-compact responses, use a bogus peerId. Hardly any client #uses this type of response anyways. There is no real meaning to the #peer ID except informal agreements. response['peers'].append({'peer id':'0'*20,'ip':socket.inet_ntoa(struct.pack('!I',peer.ip)),'port':peer.port}) #For stop event, just remove the peer. Don't return anything elif p['event'] == 'stopped': change = self.peers.removePeer(p['info_hash'],peer) addPeer = False #Log the successful announce self.announceLog.info('%s:%d %s,%s,%d',peerIpAsString,p['port'],p['info_hash'].encode('hex').upper(),p['event'],p['left']) for callback in self.afterAnnounce: callback(userId,p['info_hash'],peerIpAsString,p['port'],p['peer_id']) return sendBencodedWsgiResponse(env,start_response,response)
def __call__(self, env, start_response, *args, **kwargs): try: kwargs.update(self._extractParams(env)) except ValueError as e: return vanilla.http_error(400, env, start_response, msg=e.message) return self.wrap(self.instance, env, start_response, *args, **kwargs)
def __call__(self,env,start_response,*args,**kwargs): try: kwargs.update(self._extractParams(env)) except ValueError as e: return vanilla.http_error(400,env,start_response,msg=e.message) return self.wrap(self.instance,env,start_response,*args,**kwargs)