import HydrusConstants as HC import HydrusExceptions import HydrusFileHandling import HydrusImageHandling import HydrusNetwork import HydrusPaths import HydrusSerialisable import os import time import traceback from twisted.internet import reactor, defer from twisted.internet.threads import deferToThread from twisted.web.server import NOT_DONE_YET from twisted.web.resource import Resource from twisted.web.static import File as FileResource, NoRangeStaticProducer import HydrusData import HydrusGlobals as HG def GenerateEris( service, domain ): name = service.GetName() service_type = service.GetServiceType() welcome_text_1 = 'This is ' + name + ',' welcome_text_2 = 'a ' + HC.service_string_lookup[ service_type ] + '.' welcome_text_3 = 'Software version ' + str( HC.SOFTWARE_VERSION ) welcome_text_4 = 'Network version ' + str( HC.NETWORK_VERSION ) if domain.IsLocal(): welcome_text_5 = 'It only responds to requests from localhost.' else: welcome_text_5 = 'It responds to requests from any host.' return '''''' + name + '''
                         8888  8888888
                  888888888888888888888888
               8888:::8888888888888888888888888
             8888::::::8888888888888888888888888888
            88::::::::888:::8888888888888888888888888
          88888888::::8:::::::::::88888888888888888888
        888 8::888888::::::::::::::::::88888888888   888
           88::::88888888::::m::::::::::88888888888    8
         888888888888888888:M:::::::::::8888888888888
        88888888888888888888::::::::::::M88888888888888
        8888888888888888888888:::::::::M8888888888888888
         8888888888888888888888:::::::M888888888888888888
        8888888888888888::88888::::::M88888888888888888888
      88888888888888888:::88888:::::M888888888888888   8888
     88888888888888888:::88888::::M::;o*M*o;888888888    88
    88888888888888888:::8888:::::M:::::::::::88888888    8
   88888888888888888::::88::::::M:;:::::::::::888888888
  8888888888888888888:::8::::::M::aAa::::::::M8888888888       8
  88   8888888888::88::::8::::M:::::::::::::888888888888888 8888
 88  88888888888:::8:::::::::M::::::::::;::88:88888888888888888
 8  8888888888888:::::::::::M::"@@@@@@@"::::8w8888888888888888
  88888888888:888::::::::::M:::::"@a@":::::M8i888888888888888
 8888888888::::88:::::::::M88:::::::::::::M88z88888888888888888
8888888888:::::8:::::::::M88888:::::::::MM888!888888888888888888
888888888:::::8:::::::::M8888888MAmmmAMVMM888*88888888   88888888
888888 M:::::::::::::::M888888888:::::::MM88888888888888   8888888
8888   M::::::::::::::M88888888888::::::MM888888888888888    88888
 888   M:::::::::::::M8888888888888M:::::mM888888888888888    8888
  888  M::::::::::::M8888:888888888888::::m::Mm88888 888888   8888
   88  M::::::::::::8888:88888888888888888::::::Mm8   88888   888
   88  M::::::::::8888M::88888::888888888888:::::::Mm88888    88
   8   MM::::::::8888M:::8888:::::888888888888::::::::Mm8     4              ''' + welcome_text_1 + '''
       8M:::::::8888M:::::888:::::::88:::8888888::::::::Mm    2              ''' + welcome_text_2 + '''
      88MM:::::8888M:::::::88::::::::8:::::888888:::M:::::M
     8888M:::::888MM::::::::8:::::::::::M::::8888::::M::::M                  ''' + welcome_text_3 + '''
    88888M:::::88:M::::::::::8:::::::::::M:::8888::::::M::M                  ''' + welcome_text_4 + '''
   88 888MM:::888:M:::::::::::::::::::::::M:8888:::::::::M:
   8 88888M:::88::M:::::::::::::::::::::::MM:88::::::::::::M                 ''' + welcome_text_5 + '''
     88888M:::88::M::::::::::*88*::::::::::M:88::::::::::::::M
    888888M:::88::M:::::::::88@@88:::::::::M::88::::::::::::::M
    888888MM::88::MM::::::::88@@88:::::::::M:::8::::::::::::::*8
    88888  M:::8::MM:::::::::*88*::::::::::M:::::::::::::::::88@@
    8888   MM::::::MM:::::::::::::::::::::MM:::::::::::::::::88@@
     888    M:::::::MM:::::::::::::::::::MM::M::::::::::::::::*8
     888    MM:::::::MMM::::::::::::::::MM:::MM:::::::::::::::M
      88     M::::::::MMMM:::::::::::MMMM:::::MM::::::::::::MM
       88    MM:::::::::MMMMMMMMMMMMMMM::::::::MMM::::::::MMM
        88    MM::::::::::::MMMMMMM::::::::::::::MMMMMMMMMM
         88   8MM::::::::::::::::::::::::::::::::::MMMMMM
          8   88MM::::::::::::::::::::::M:::M::::::::MM
              888MM::::::::::::::::::MM::::::MM::::::MM
             88888MM:::::::::::::::MMM:::::::mM:::::MM
             888888MM:::::::::::::MMM:::::::::MMM:::M
            88888888MM:::::::::::MMM:::::::::::MM:::M
           88 8888888M:::::::::MMM::::::::::::::M:::M
           8  888888 M:::::::MM:::::::::::::::::M:::M:
              888888 M::::::M:::::::::::::::::::M:::MM
             888888  M:::::M::::::::::::::::::::::::M:M
             888888  M:::::M:::::::::@::::::::::::::M::M
             88888   M::::::::::::::@@:::::::::::::::M::M
            88888   M::::::::::::::@@@::::::::::::::::M::M
           88888   M:::::::::::::::@@::::::::::::::::::M::M
          88888   M:::::m::::::::::@::::::::::Mm:::::::M:::M
          8888   M:::::M:::::::::::::::::::::::MM:::::::M:::M
         8888   M:::::M:::::::::::::::::::::::MMM::::::::M:::M
        888    M:::::Mm::::::::::::::::::::::MMM:::::::::M::::M
      8888    MM::::Mm:::::::::::::::::::::MMMM:::::::::m::m:::M
     888      M:::::M::::::::::::::::::::MMM::::::::::::M::mm:::M
  8888       MM:::::::::::::::::::::::::MM:::::::::::::mM::MM:::M:
             M:::::::::::::::::::::::::M:::::::::::::::mM::MM:::Mm
            MM::::::m:::::::::::::::::::::::::::::::::::M::MM:::MM
            M::::::::M:::::::::::::::::::::::::::::::::::M::M:::MM
           MM:::::::::M:::::::::::::M:::::::::::::::::::::M:M:::MM
           M:::::::::::M88:::::::::M:::::::::::::::::::::::MM::MMM 
           M::::::::::::8888888888M::::::::::::::::::::::::MM::MM 
           M:::::::::::::88888888M:::::::::::::::::::::::::M::MM
           M::::::::::::::888888M:::::::::::::::::::::::::M::MM
           M:::::::::::::::88888M:::::::::::::::::::::::::M:MM
           M:::::::::::::::::88M::::::::::::::::::::::::::MMM
           M:::::::::::::::::::M::::::::::::::::::::::::::MMM
           MM:::::::::::::::::M::::::::::::::::::::::::::MMM
            M:::::::::::::::::M::::::::::::::::::::::::::MMM
            MM:::::::::::::::M::::::::::::::::::::::::::MMM
             M:::::::::::::::M:::::::::::::::::::::::::MMM
             MM:::::::::::::M:::::::::::::::::::::::::MMM
              M:::::::::::::M::::::::::::::::::::::::MMM
              MM:::::::::::M::::::::::::::::::::::::MMM
               M:::::::::::M:::::::::::::::::::::::MMM
               MM:::::::::M:::::::::::::::::::::::MMM
                M:::::::::M::::::::::::::::::::::MMM
                MM:::::::M::::::::::::::::::::::MMM
                 MM::::::M:::::::::::::::::::::MMM
                 MM:::::M:::::::::::::::::::::MMM
                  MM::::M::::::::::::::::::::MMM 
                  MM:::M::::::::::::::::::::MMM
                   MM::M:::::::::::::::::::MMM
                   MM:M:::::::::::::::::::MMM
                    MMM::::::::::::::::::MMM
                    MM::::::::::::::::::MMM
                     M:::::::::::::::::MMM
                    MM::::::::::::::::MMM
                    MM:::::::::::::::MMM
                    MM::::M:::::::::MMM:
                    mMM::::MM:::::::MMMM
                     MMM:::::::::::MMM:M
                     mMM:::M:::::::M:M:M
                      MM::MMMM:::::::M:M
                      MM::MMM::::::::M:M
                      mMM::MM::::::::M:M
                       MM::MM:::::::::M:M
                       MM::MM::::::::::M:m
                       MM:::M:::::::::::MM
                       MMM:::::::::::::::M:
                       MMM:::::::::::::::M:
                       MMM::::::::::::::::M
                       MMM::::::::::::::::M
                       MMM::::::::::::::::Mm
                        MM::::::::::::::::MM
                        MMM:::::::::::::::MM
                        MMM:::::::::::::::MM
                        MMM:::::::::::::::MM
                        MMM:::::::::::::::MM
                         MM::::::::::::::MMM
                         MMM:::::::::::::MM
                         MMM:::::::::::::MM
                         MMM::::::::::::MM
                          MM::::::::::::MM
                          MM::::::::::::MM
                          MM:::::::::::MM
                          MMM::::::::::MM
                          MMM::::::::::MM
                           MM:::::::::MM
                           MMM::::::::MM
                           MMM::::::::MM
                            MM::::::::MM
                            MMM::::::MM
                            MMM::::::MM
                             MM::::::MM
                             MM::::::MM
                              MM:::::MM
                              MM:::::MM:
                              MM:::::M:M
                              MM:::::M:M
                              :M::::::M:
                             M:M:::::::M
                            M:::M::::::M
                           M::::M::::::M
                          M:::::M:::::::M
                         M::::::MM:::::::M
                         M:::::::M::::::::M
                         M;:;::::M:::::::::M
                         M:m:;:::M::::::::::M
                         MM:m:m::M::::::::;:M
                          MM:m::MM:::::::;:;M
                           MM::MMM::::::;:m:M
                            MMMM MM::::m:m:MM
                                  MM::::m:MM
                                   MM::::MM
                                    MM::MM
                                     MMMM
''' def ParseFileArguments( path ): HydrusImageHandling.ConvertToPngIfBmp( path ) hash = HydrusFileHandling.GetHashFromPath( path ) try: ( size, mime, width, height, duration, num_frames, num_words ) = HydrusFileHandling.GetFileInfo( path ) except HydrusExceptions.SizeException: raise HydrusExceptions.ForbiddenException( 'File is of zero length!' ) except HydrusExceptions.MimeException: raise HydrusExceptions.ForbiddenException( 'Filetype is not permitted!' ) except Exception as e: raise HydrusExceptions.ForbiddenException( HydrusData.ToUnicode( e ) ) args = {} args[ 'path' ] = path args[ 'hash' ] = hash args[ 'size' ] = size args[ 'mime' ] = mime if width is not None: args[ 'width' ] = width if height is not None: args[ 'height' ] = height if duration is not None: args[ 'duration' ] = duration if num_frames is not None: args[ 'num_frames' ] = num_frames if num_words is not None: args[ 'num_words' ] = num_words if mime in HC.MIMES_WITH_THUMBNAILS: try: thumbnail = HydrusFileHandling.GenerateThumbnail( path ) except Exception as e: tb = traceback.format_exc() raise HydrusExceptions.ForbiddenException( 'Could not generate thumbnail from that file:' + os.linesep + tb ) args[ 'thumbnail' ] = thumbnail return args hydrus_favicon = FileResource( os.path.join( HC.STATIC_DIR, 'hydrus.ico' ), defaultType = 'image/x-icon' ) class HydrusDomain( object ): def __init__( self, local_only ): self._local_only = local_only def CheckValid( self, client_ip ): if self._local_only and client_ip != '127.0.0.1': raise HydrusExceptions.ForbiddenException( 'Only local access allowed!' ) def IsLocal( self ): return self._local_only class HydrusResource( Resource ): def __init__( self, service, domain ): Resource.__init__( self ) self._service = service self._service_key = self._service.GetServiceKey() self._domain = domain service_type = self._service.GetServiceType() self._server_version_string = HC.service_string_lookup[ service_type ] + '/' + str( HC.NETWORK_VERSION ) def _callbackCheckRestrictions( self, request ): self._domain.CheckValid( request.getClientIP() ) self._checkService( request ) self._checkUserAgent( request ) return request def _checkService( self, request ): if HG.server_busy: raise HydrusExceptions.ServerBusyException( 'This server is busy, please try again later.' ) return request def _checkUserAgent( self, request ): request.is_hydrus_user_agent = False if request.requestHeaders.hasHeader( 'User-Agent' ): user_agent_texts = request.requestHeaders.getRawHeaders( 'User-Agent' ) user_agent_text = user_agent_texts[0] try: user_agents = user_agent_text.split( ' ' ) except: return # crazy user agent string, so just assume not a hydrus client for user_agent in user_agents: if '/' in user_agent: ( client, network_version ) = user_agent.split( '/', 1 ) if client == 'hydrus': request.is_hydrus_user_agent = True network_version = int( network_version ) if network_version == HC.NETWORK_VERSION: return else: if network_version < HC.NETWORK_VERSION: message = 'Your client is out of date; please download the latest release.' else: message = 'This server is out of date; please ask its admin to update to the latest release.' raise HydrusExceptions.NetworkVersionException( 'Network version mismatch! This server\'s network version is ' + str( HC.NETWORK_VERSION ) + ', whereas your client\'s is ' + str( network_version ) + '! ' + message ) def _callbackParseGETArgs( self, request ): hydrus_args = HydrusNetwork.ParseGETArgs( request.args ) request.hydrus_args = hydrus_args return request def _callbackParsePOSTArgs( self, request ): request.content.seek( 0 ) if not request.requestHeaders.hasHeader( 'Content-Type' ): hydrus_args = {} else: content_types = request.requestHeaders.getRawHeaders( 'Content-Type' ) content_type = content_types[0] try: mime = HC.mime_enum_lookup[ content_type ] except: raise HydrusExceptions.ForbiddenException( 'Did not recognise Content-Type header!' ) total_bytes_read = 0 if mime == HC.APPLICATION_JSON: json_string = request.content.read() total_bytes_read += len( json_string ) hydrus_args = HydrusNetwork.ParseBodyString( json_string ) else: ( os_file_handle, temp_path ) = HydrusPaths.GetTempPath() request.temp_file_info = ( os_file_handle, temp_path ) with open( temp_path, 'wb' ) as f: for block in HydrusPaths.ReadFileLikeAsBlocks( request.content ): f.write( block ) total_bytes_read += len( block ) hydrus_args = ParseFileArguments( temp_path ) self._reportDataUsed( request, total_bytes_read ) request.hydrus_args = hydrus_args return request def _callbackRenderResponseContext( self, request ): self._CleanUpTempFile( request ) response_context = request.hydrus_response_context status_code = response_context.GetStatusCode() request.setResponseCode( status_code ) for ( k, v, kwargs ) in response_context.GetCookies(): request.addCookie( k, v, **kwargs ) do_finish = True if response_context.HasPath(): path = response_context.GetPath() size = os.path.getsize( path ) mime = response_context.GetMime() if mime == HC.APPLICATION_UNKNOWN: mime = HydrusFileHandling.GetMime( path ) content_type = HC.mime_string_lookup[ mime ] content_length = size ( base, filename ) = os.path.split( path ) content_disposition = 'inline; filename="' + filename + '"' # can't be unicode! request.setHeader( 'Content-Type', str( content_type ) ) request.setHeader( 'Content-Length', str( content_length ) ) request.setHeader( 'Content-Disposition', str( content_disposition ) ) request.setHeader( 'Expires', time.strftime( '%a, %d %b %Y %H:%M:%S GMT', time.gmtime( time.time() + 86400 * 365 ) ) ) request.setHeader( 'Cache-Control', str( 86400 * 365 ) ) fileObject = open( path, 'rb' ) producer = NoRangeStaticProducer( request, fileObject ) producer.start() do_finish = False elif response_context.HasBody(): mime = response_context.GetMime() body = response_context.GetBody() content_type = HC.mime_string_lookup[ mime ] content_length = len( body ) content_disposition = 'inline' request.setHeader( 'Content-Type', content_type ) request.setHeader( 'Content-Length', str( content_length ) ) request.setHeader( 'Content-Disposition', content_disposition ) request.write( HydrusData.ToByteString( body ) ) else: content_length = 0 request.setHeader( 'Content-Length', str( content_length ) ) self._reportDataUsed( request, content_length ) self._reportRequestUsed( request ) if do_finish: request.finish() def _callbackDoGETJob( self, request ): def wrap_thread_result( response_context ): request.hydrus_response_context = response_context return request d = deferToThread( self._threadDoGETJob, request ) d.addCallback( wrap_thread_result ) return d def _callbackDoPOSTJob( self, request ): def wrap_thread_result( response_context ): request.hydrus_response_context = response_context return request d = deferToThread( self._threadDoPOSTJob, request ) d.addCallback( wrap_thread_result ) return d def _errbackDisconnected( self, failure, request_deferred ): request_deferred.cancel() def _errbackHandleEmergencyError( self, failure, request ): try: self._CleanUpTempFile( request ) except: pass try: HydrusData.DebugPrint( failure.getTraceback() ) except: pass try: request.write( failure.getTraceback() ) except: pass try: request.finish() except: pass def _errbackHandleProcessingError( self, failure, request ): self._CleanUpTempFile( request ) default_mime = HC.TEXT_HTML default_encoding = HydrusData.ToByteString if failure.type == KeyError: response_context = ResponseContext( 400, mime = default_mime, body = default_encoding( 'It appears one or more parameters required for that request were missing:' + os.linesep + failure.getTraceback() ) ) elif failure.type == HydrusExceptions.BandwidthException: response_context = ResponseContext( 509, mime = default_mime, body = default_encoding( failure.value ) ) elif failure.type == HydrusExceptions.PermissionException: response_context = ResponseContext( 401, mime = default_mime, body = default_encoding( failure.value ) ) elif failure.type == HydrusExceptions.ForbiddenException: response_context = ResponseContext( 403, mime = default_mime, body = default_encoding( failure.value ) ) elif failure.type in ( HydrusExceptions.NotFoundException, HydrusExceptions.DataMissing ): response_context = ResponseContext( 404, mime = default_mime, body = default_encoding( failure.value ) ) elif failure.type == HydrusExceptions.NetworkVersionException: response_context = ResponseContext( 426, mime = default_mime, body = default_encoding( failure.value ) ) elif failure.type == HydrusExceptions.ServerBusyException: response_context = ResponseContext( 503, mime = default_mime, body = default_encoding( failure.value ) ) elif failure.type == HydrusExceptions.SessionException: response_context = ResponseContext( 419, mime = default_mime, body = default_encoding( failure.value ) ) else: HydrusData.DebugPrint( failure.getTraceback() ) response_context = ResponseContext( 500, mime = default_mime, body = default_encoding( 'The repository encountered an error it could not handle! Here is a dump of what happened, which will also be written to your client.log file. If it persists, please forward it to hydrus.admin@gmail.com:' + os.linesep * 2 + failure.getTraceback() ) ) request.hydrus_response_context = response_context return request def _parseAccessKey( self, request ): if not request.requestHeaders.hasHeader( 'Hydrus-Key' ): raise HydrusExceptions.PermissionException( 'No hydrus key header found!' ) hex_keys = request.requestHeaders.getRawHeaders( 'Hydrus-Key' ) hex_key = hex_keys[0] try: access_key = hex_key.decode( 'hex' ) except: raise HydrusExceptions.ForbiddenException( 'Could not parse the hydrus key!' ) return access_key def _reportDataUsed( self, request, num_bytes ): self._service.ReportDataUsed( num_bytes ) HG.controller.ReportDataUsed( num_bytes ) def _reportRequestUsed( self, request ): self._service.ReportRequestUsed() HG.controller.ReportRequestUsed() def _threadDoGETJob( self, request ): raise HydrusExceptions.NotFoundException( 'This service does not support that request!' ) def _threadDoPOSTJob( self, request ): raise HydrusExceptions.NotFoundException( 'This service does not support that request!' ) def _CleanUpTempFile( self, request ): if hasattr( request, 'temp_file_info' ): ( os_file_handle, temp_path ) = request.temp_file_info HydrusPaths.CleanUpTempPath( os_file_handle, temp_path ) del request.temp_file_info def render_GET( self, request ): request.setHeader( 'Server', self._server_version_string ) d = defer.Deferred() d.addCallback( self._callbackCheckRestrictions ) d.addCallback( self._callbackParseGETArgs ) d.addCallback( self._callbackDoGETJob ) d.addErrback( self._errbackHandleProcessingError, request ) d.addCallback( self._callbackRenderResponseContext ) d.addErrback( self._errbackHandleEmergencyError, request ) reactor.callLater( 0, d.callback, request ) request.notifyFinish().addErrback( self._errbackDisconnected, d ) return NOT_DONE_YET def render_POST( self, request ): request.setHeader( 'Server', self._server_version_string ) d = defer.Deferred() d.addCallback( self._callbackCheckRestrictions ) d.addCallback( self._callbackParsePOSTArgs ) d.addCallback( self._callbackDoPOSTJob ) d.addErrback( self._errbackHandleProcessingError, request ) d.addCallback( self._callbackRenderResponseContext ) d.addErrback( self._errbackHandleEmergencyError, request ) reactor.callLater( 0, d.callback, request ) request.notifyFinish().addErrback( self._errbackDisconnected, d ) return NOT_DONE_YET class HydrusResourceRobotsTXT( HydrusResource ): def _threadDoGETJob( self, request ): body = '''User-agent: * Disallow: /''' response_context = ResponseContext( 200, mime = HC.TEXT_PLAIN, body = body ) return response_context class HydrusResourceWelcome( HydrusResource ): def _threadDoGETJob( self, request ): body = GenerateEris( self._service, self._domain ) response_context = ResponseContext( 200, mime = HC.TEXT_HTML, body = body ) return response_context class ResponseContext( object ): def __init__( self, status_code, mime = HC.APPLICATION_JSON, body = None, path = None, cookies = None ): if isinstance( body, HydrusSerialisable.SerialisableBase ): body = body.DumpToNetworkString() if cookies is None: cookies = [] self._status_code = status_code self._mime = mime self._body = body self._path = path self._cookies = cookies def GetBody( self ): return self._body def GetCookies( self ): return self._cookies def GetLength( self ): return len( self._body ) def GetMime( self ): return self._mime def GetPath( self ): return self._path def GetStatusCode( self ): return self._status_code def HasBody( self ): return self._body is not None def HasPath( self ): return self._path is not None