hydrus/hydrus/core/HydrusNATPunch.py

446 lines
15 KiB
Python

import os
import socket
import subprocess
import threading
import traceback
from hydrus.core import HydrusConstants as HC
from hydrus.core import HydrusData
from hydrus.core import HydrusExceptions
from hydrus.core import HydrusText
from hydrus.core import HydrusThreading
# the _win32, _linux, _osx stuff here is legacy, from when I used to bundle these exes. this cause anti-virus false positive wew
if HC.PLATFORM_WINDOWS:
possible_bin_filenames = [ 'upnpc-static.exe', 'upnpc-static.exe', 'miniupnpc.exe', 'upnpc_win32.exe' ]
else:
possible_bin_filenames = [ 'upnpc-static', 'upnpc-shared', 'miniupnpc' ]
if HC.PLATFORM_LINUX:
possible_bin_filenames.append( 'upnpc_linux' )
elif HC.PLATFORM_MACOS:
possible_bin_filenames.append( 'upnpc_osx' )
UPNPC_PATH = 'miniupnpc' # no exe, we'll assume installed to system
UPNPC_IS_MISSING = False
UPNPC_MANAGER_ERROR_PRINTED = False
for filename in possible_bin_filenames:
possible_path = os.path.join( HC.BIN_DIR, filename )
if os.path.exists( possible_path ):
UPNPC_PATH = possible_path
EXTERNAL_IP = {}
EXTERNAL_IP[ 'ip' ] = None
EXTERNAL_IP[ 'time' ] = 0
def RaiseMissingUPnPcError( operation ):
message = 'Unfortunately, the operation "{}" requires miniupnpc, which does not seem to be available for your system. You can install it yourself easily, please check install_dir/bin/upnpc_readme.txt for more information!'.format( operation )
global UPNPC_IS_MISSING
if not UPNPC_IS_MISSING:
HydrusData.ShowText( message )
UPNPC_IS_MISSING = True
raise FileNotFoundError( message )
def GetExternalIP():
if HydrusData.TimeHasPassed( EXTERNAL_IP[ 'time' ] + ( 3600 * 24 ) ):
cmd = [ UPNPC_PATH, '-l' ]
sbp_kwargs = HydrusData.GetSubprocessKWArgs( text = True )
HydrusData.CheckProgramIsNotShuttingDown()
try:
p = subprocess.Popen( cmd, stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = subprocess.PIPE, **sbp_kwargs )
except FileNotFoundError:
RaiseMissingUPnPcError( 'fetch external IP' )
HydrusData.WaitForProcessToFinish( p, 30 )
( stdout, stderr ) = HydrusThreading.SubprocessCommunicate( p )
if stderr is not None and len( stderr ) > 0:
raise Exception( 'Problem while trying to fetch External IP (if it says No IGD UPnP Device, you are either on a VPN or your router does not seem to support UPnP):' + os.linesep * 2 + str( stderr ) )
else:
try:
lines = HydrusText.DeserialiseNewlinedTexts( stdout )
i = lines.index( 'i protocol exPort->inAddr:inPort description remoteHost leaseTime' )
# ExternalIPAddress = ip
( gumpf, external_ip_address ) = lines[ i - 1 ].split( ' = ' )
except ValueError:
raise Exception( 'Could not parse external IP!' )
if external_ip_address == '0.0.0.0':
raise Exception( 'Your UPnP device returned your external IP as 0.0.0.0! Try rebooting it, or overwrite it in options!' )
EXTERNAL_IP[ 'ip' ] = external_ip_address
EXTERNAL_IP[ 'time' ] = HydrusData.GetNow()
return EXTERNAL_IP[ 'ip' ]
def GetLocalIP():
return socket.gethostbyname( socket.gethostname() )
def AddUPnPMapping( internal_client, internal_port, external_port, protocol, description, duration = 3600 ):
cmd = [ UPNPC_PATH, '-e', description, '-a', internal_client, str( internal_port ), str( external_port ), protocol, str( duration ) ]
sbp_kwargs = HydrusData.GetSubprocessKWArgs( text = True )
HydrusData.CheckProgramIsNotShuttingDown()
try:
p = subprocess.Popen( cmd, stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = subprocess.PIPE, **sbp_kwargs )
except FileNotFoundError:
RaiseMissingUPnPcError( 'add UPnP port forward' )
HydrusData.WaitForProcessToFinish( p, 30 )
( stdout, stderr ) = HydrusThreading.SubprocessCommunicate( p )
AddUPnPMappingCheckResponse( internal_client, internal_port, external_port, protocol, stdout, stderr )
def AddUPnPMappingCheckResponse( internal_client, internal_port, external_port, protocol, stdout, stderr ):
if stdout is not None and 'failed with code' in stdout:
already_exists_str = '{} TCP is redirected to internal {}:{}'.format( external_port, internal_client, internal_port )
wrong_port_str = '{} TCP is redirected to internal {}'.format( external_port, internal_client )
points_elsewhere_str = '{} TCP is redirected to internal '.format( external_port )
if already_exists_str in stdout:
raise HydrusExceptions.RouterException( 'The UPnP mapping of {}:{}->external:{}({}) already exists, and your router did not like it being re-added! It is probably a good idea to set this manually through your router interface with an indefinite lease.'.format( internal_client, internal_port, external_port, protocol ) )
elif wrong_port_str in stdout:
raise HydrusExceptions.RouterException( 'The UPnP mapping of {}:{}->external:{}({}) could not be added because that external port is already forwarded to another port on this computer! You will have to remove it, either through hydrus or the router\'s direct interface (probably a web server hosted at its address).'.format( internal_client, internal_port, external_port, protocol ) )
elif points_elsewhere_str in stdout:
raise HydrusExceptions.RouterException( 'The UPnP mapping of {}:{}->external:{}({}) could not be added because that external port is already mapped to another computer on this network! You will have to remove it, either through hydrus or the router\'s direct interface (probably a web server hosted at its address).'.format( internal_client, internal_port, external_port, protocol ) )
if 'UnknownError' in stdout:
raise HydrusExceptions.RouterException( 'Problem while trying to add UPnP mapping:' + os.linesep * 2 + stdout )
else:
raise Exception( 'Problem while trying to add UPnP mapping:' + os.linesep * 2 + stdout )
if stderr is not None and len( stderr ) > 0:
raise Exception( 'Problem while trying to add UPnP mapping:' + os.linesep * 2 + stderr )
def GetUPnPMappings():
cmd = [ UPNPC_PATH, '-l' ]
sbp_kwargs = HydrusData.GetSubprocessKWArgs( text = True )
HydrusData.CheckProgramIsNotShuttingDown()
try:
p = subprocess.Popen( cmd, stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = subprocess.PIPE, **sbp_kwargs )
except FileNotFoundError:
RaiseMissingUPnPcError( 'get current UPnP port forward mappings' )
HydrusData.WaitForProcessToFinish( p, 30 )
( stdout, stderr ) = HydrusThreading.SubprocessCommunicate( p )
if stderr is not None and len( stderr ) > 0:
raise Exception( 'Problem while trying to fetch UPnP mappings (if it says No IGD UPnP Device, you are either on a VPN or your router does not seem to support UPnP):' + os.linesep * 2 + stderr )
else:
return GetUPnPMappingsParseResponse( stdout )
def GetUPnPMappingsParseResponse( stdout ):
try:
lines = HydrusText.DeserialiseNewlinedTexts( stdout )
i = lines.index( 'i protocol exPort->inAddr:inPort description remoteHost leaseTime' )
data_lines = []
i += 1
while i < len( lines ):
if not lines[ i ][0] in ( ' ', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' ): break
data_lines.append( lines[ i ] )
i += 1
processed_data = []
for line in data_lines:
# 0 UDP 65533->192.168.0.197:65533 'Skype UDP at 192.168.0.197:65533 (2665)' '' 0
while ' ' in line:
line = line.replace( ' ', ' ' )
if line.startswith( ' ' ):
( empty, number, protocol, mapping_data, rest_of_line ) = line.split( ' ', 4 )
else:
( number, protocol, mapping_data, rest_of_line ) = line.split( ' ', 3 )
( external_port, rest_of_mapping_data ) = mapping_data.split( '->' )
external_port = int( external_port )
if rest_of_mapping_data.count( ':' ) == 1:
( internal_client, internal_port ) = rest_of_mapping_data.split( ':' )
else:
parts = rest_of_mapping_data.split( ':' )
internal_port = parts.pop( -1 )
internal_client = ':'.join( parts )
internal_port = int( internal_port )
( empty, description, space, remote_host, rest_of_line ) = rest_of_line.split( '\'', 4 )
lease_time = int( rest_of_line[1:] )
processed_data.append( ( description, internal_client, internal_port, external_port, protocol, lease_time ) )
return processed_data
except Exception as e:
HydrusData.Print( 'UPnP problem:' )
HydrusData.Print( traceback.format_exc() )
HydrusData.Print( 'Full response follows:' )
HydrusData.Print( stdout )
raise Exception( 'Problem while trying to parse UPnP mappings:' + os.linesep * 2 + str( e ) )
def RemoveUPnPMapping( external_port, protocol ):
cmd = [ UPNPC_PATH, '-d', str( external_port ), protocol ]
sbp_kwargs = HydrusData.GetSubprocessKWArgs( text = True )
HydrusData.CheckProgramIsNotShuttingDown()
try:
p = subprocess.Popen( cmd, stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = subprocess.PIPE, **sbp_kwargs )
except FileNotFoundError:
RaiseMissingUPnPcError( 'remove UPnP port forward' )
HydrusData.WaitForProcessToFinish( p, 30 )
( stdout, stderr ) = HydrusThreading.SubprocessCommunicate( p )
if stderr is not None and len( stderr ) > 0:
raise Exception( 'Problem while trying to remove UPnP mapping:' + os.linesep * 2 + stderr )
class ServicesUPnPManager( object ):
def __init__( self, services ):
self._lock = threading.Lock()
self._services = services
def _RefreshUPnP( self, force_wipe = False ):
running_service_with_upnp = True in ( service.GetPort() is not None and service.GetUPnPPort() is not None for service in self._services )
if not force_wipe:
if not running_service_with_upnp:
return
if running_service_with_upnp and UPNPC_IS_MISSING:
return # welp
try:
local_ip = GetLocalIP()
except:
return # can't get local IP, we are wewlad atm, probably some complicated multiple network situation we'll have to deal with later
try:
current_mappings = GetUPnPMappings()
except FileNotFoundError:
if not force_wipe:
global UPNPC_MANAGER_ERROR_PRINTED
if not UPNPC_MANAGER_ERROR_PRINTED:
HydrusData.ShowText( 'Hydrus was set up to manage your services\' port forwards with UPnP, but the miniupnpc executable is not available. Please check install_dir/bin/upnpc_readme.txt for more details.' )
UPNPC_MANAGER_ERROR_PRINTED = True
return # in this case, most likely miniupnpc could not be found, so skip for now
except:
return # This IGD probably doesn't support UPnP, so don't spam the user with errors they can't fix!
our_mappings = { ( internal_client, internal_port ) : external_port for ( description, internal_client, internal_port, external_port, protocol, enabled ) in current_mappings }
for service in self._services:
internal_port = service.GetPort()
upnp_port = service.GetUPnPPort()
if ( local_ip, internal_port ) in our_mappings:
current_external_port = our_mappings[ ( local_ip, internal_port ) ]
port_is_incorrect = upnp_port is None or upnp_port != current_external_port
if port_is_incorrect or force_wipe:
RemoveUPnPMapping( current_external_port, 'TCP' )
for service in self._services:
internal_port = service.GetPort()
upnp_port = service.GetUPnPPort()
if upnp_port is not None:
service_type = service.GetServiceType()
protocol = 'TCP'
description = HC.service_string_lookup[ service_type ] + ' at ' + local_ip + ':' + str( internal_port )
duration = 86400
try:
AddUPnPMapping( local_ip, internal_port, upnp_port, protocol, description, duration = duration )
except HydrusExceptions.RouterException:
HydrusData.Print( 'The UPnP Daemon tried to add {}:{}->external:{} but it failed. Please try it manually to get a full log of what happened.'.format( local_ip, internal_port, upnp_port ) )
return
def SetServices( self, services ):
with self._lock:
self._services = services
self._RefreshUPnP()
def RefreshUPnP( self ):
with self._lock:
self._RefreshUPnP()