#!/usr/bin/env python2
"""
Handles the main Pappy session .
. . data : : session
The : class : ` pappyproxy . pappy . PappySession ` object for the current session . Mainly
used for accessing the session ' s config information.
"""
import argparse
import crochet
import datetime
import os
import schema . update
import shutil
import sys
import tempfile
import signal
from . import comm
from . import config
from . import compress
from . import context
from . import crypto
from . import http
from . console import ProxyCmd
from twisted . enterprise import adbapi
from twisted . internet import reactor , defer
from twisted . internet . error import CannotListenError
from twisted . internet . protocol import ServerFactory
from twisted . internet . threads import deferToThread
crochet . no_setup ( )
main_context = context . Context ( )
all_contexts = [ main_context ]
session = None
quit_confirm_time = None
try :
from guppy import hpy
heapstats = hpy ( )
heapstats . setref ( )
except ImportError :
heapstats = None
class PappySession ( object ) :
"""
An object representing a pappy session . Mainly you ' ll only use this to get to
the session config .
: ivar config : The configuration settings for the session
: vartype config : : class : ` pappyproxy . config . PappyConfig `
"""
def __init__ ( self , sessconfig ) :
self . config = sessconfig
self . complete_defer = defer . Deferred ( )
self . server_factories = [ ]
self . plugin_loader = None
self . cons = None
self . dbpool = None
self . delete_data_on_quit = False
self . ports = None
self . crypto = Crypto ( sessconfig )
self . password = None
@defer . inlineCallbacks
def start ( self ) :
from . import proxy , plugin
# If the data file doesn't exist, create it with restricted permissions
if not os . path . isfile ( self . config . datafile ) :
with os . fdopen ( os . open ( self . config . datafile , os . O_CREAT , 0o0600 ) , ' r ' ) :
pass
self . dbpool = adbapi . ConnectionPool ( " sqlite3 " , self . config . datafile ,
check_same_thread = False ,
cp_openfun = set_text_factory ,
cp_max = 1 )
try :
yield schema . update . update_schema ( self . dbpool , self . config . datafile )
except Exception as e :
print ' Error updating schema: %s ' % e
print ' Exiting... '
self . complete_defer . callback ( None )
return
http . init ( self . dbpool )
yield http . Request . cache . load_ids ( )
context . reset_context_caches ( )
# Run the proxy
if self . config . debug_dir and os . path . exists ( self . config . debug_dir ) :
shutil . rmtree ( self . config . debug_dir )
print ' Removing old debugging output '
listen_strs = [ ]
self . ports = [ ]
for listener in self . config . listeners :
server_factory = proxy . ProxyServerFactory ( save_all = True )
try :
if ' forward_host_ssl ' in listener and listener [ ' forward_host_ssl ' ] :
server_factory . force_ssl = True
server_factory . forward_host = listener [ ' forward_host_ssl ' ]
elif ' forward_host ' in listener and listener [ ' forward_host ' ] :
server_factory . force_ssl = False
server_factory . forward_host = listener [ ' forward_host ' ]
port = reactor . listenTCP ( listener [ ' port ' ] , server_factory , interface = listener [ ' interface ' ] )
listener_str = ' port %d ' % listener [ ' port ' ]
if listener [ ' interface ' ] not in ( ' 127.0.0.1 ' , ' localhost ' ) :
listener_str + = ' (bound to %s ) ' % listener [ ' interface ' ]
listen_strs . append ( listener_str )
self . ports . append ( port )
self . server_factories . append ( server_factory )
except CannotListenError as e :
print repr ( e )
if listen_strs :
print ' Proxy is listening on %s ' % ( ' , ' . join ( listen_strs ) )
else :
print ' No listeners opened '
com_factory = ServerFactory ( )
com_factory . protocol = comm . CommServer
# Make the port different for every instance of pappy, then pass it to
# anything we run. Otherwise we can only have it running once on a machine
self . comm_port = reactor . listenTCP ( 0 , com_factory , interface = ' 127.0.0.1 ' )
self . comm_port = self . comm_port . getHost ( ) . port
# Load the scope
yield context . load_scope ( self . dbpool )
context . reset_to_scope ( main_context )
sys . argv = [ sys . argv [ 0 ] ] # cmd2 tries to parse args
self . cons = ProxyCmd ( session = session )
self . plugin_loader = plugin . PluginLoader ( self . cons )
for d in self . config . plugin_dirs :
if not os . path . exists ( d ) :
os . makedirs ( d )
self . plugin_loader . load_directory ( d )
# Add cleanup to defer
self . complete_defer = deferToThread ( self . cons . cmdloop )
self . complete_defer . addCallback ( self . cleanup )
@defer . inlineCallbacks
def encrypt ( self ) :
if self . password :
self . crypto . encrypt_project ( self . password )
else :
self . password = self . crypto . get_password ( )
self . crypto . encrypt_project ( self . password )
@defer . inlineCallbacks
def decrypt ( self ) :
self . password = self . crypto . get_password ( )
self . crypto . decrypt_project ( self . password )
@defer . inlineCallbacks
def cleanup ( self , ignored = None ) :
for port in self . ports :
yield port . stopListening ( )
if self . delete_data_on_quit :
print ' Deleting temporary datafile '
os . remove ( self . config . datafile )
def parse_args ( ) :
# parses sys.argv and returns a settings dictionary
parser = argparse . ArgumentParser ( description = ' An intercepting proxy for testing web applications. ' )
parser . add_argument ( ' -l ' , ' --lite ' , help = ' Run the proxy in " lite " mode ' , action = ' store_true ' )
parser . add_argument ( ' -c ' , ' --crypt ' , type = str , nargs = ' ? ' , help = ' Start pappy in " crypto " mode, optionally supply a name for the encrypted project archive [CRYPT] ' )
args = parser . parse_args ( sys . argv [ 1 : ] )
settings = { }
if args . lite :
settings [ ' lite ' ] = True
else :
settings [ ' lite ' ] = False
if args . crypt :
settings [ ' crypt ' ] = args . crypt
elif args . crypt == " " :
settings [ ' crypt ' ] = ' project.crypt '
else :
settings [ ' crypt ' ] = None
return settings
def set_text_factory ( conn ) :
conn . text_factory = str
def custom_int_handler ( signum , frame ) :
# sorry
print " Sorry, we can ' t kill things partway through otherwise the data file might be left in a corrupt state "
@defer . inlineCallbacks
def main ( ) :
global session
try :
settings = parse_args ( )
except SystemExit :
print ' Did you mean to just start the console? If so, just run `pappy` without any arguments then enter commands into the prompt that appears. '
reactor . stop ( )
defer . returnValue ( None )
pappy_config = config . PappyConfig ( )
if not os . path . exists ( pappy_config . data_dir ) :
os . makedirs ( pappy_config . data_dir )
session = PappySession ( pappy_config )
signal . signal ( signal . SIGINT , inturrupt_handler )
if settings [ ' crypt ' ] :
session . decrypt ( )
conf_settings = pappy_config . load_from_file ( ' ./config.json ' )
pappy_config . global_load_from_file ( )
session . delete_data_on_quit = False
elif settings [ ' lite ' ] :
conf_settings = pappy_config . get_default_config ( )
conf_settings [ ' debug_dir ' ] = None
conf_settings [ ' debug_to_file ' ] = False
conf_settings [ ' history_size ' ] = 0
with tempfile . NamedTemporaryFile ( delete = False ) as tf :
conf_settings [ ' data_file ' ] = tf . name
print ' Temporary datafile is %s ' % tf . name
session . delete_data_on_quit = True
pappy_config . load_settings ( conf_settings )
else :
# Initialize config
pappy_config . load_from_file ( ' ./config.json ' )
pappy_config . global_load_from_file ( )
session . delete_data_on_quit = False
yield session . start ( )
session . complete_defer . addCallback ( lambda ignored : reactor . stop ( ) )
def start ( ) :
reactor . callWhenRunning ( main )
reactor . run ( )
def inturrupt_handler ( signal , frame ) :
global session
global quit_confirm_time
if not quit_confirm_time or datetime . datetime . now ( ) > quit_confirm_time :
print ' '
print ( ' Inturrupting will cause Pappy to quit completely. This will '
' cause any in-memory only requests to be lost, but all other '
' data will be saved. ' )
print ( ' Inturrupt a second time to confirm. ' )
print ' '
quit_confirm_time = datetime . datetime . now ( ) + datetime . timedelta ( 0 , 10 )
else :
d = session . cleanup ( )
d . addCallback ( lambda _ : reactor . stop ( ) )
d . addCallback ( lambda _ : os . _exit ( 1 ) ) # Sorry blocking threads :(
if __name__ == ' __main__ ' :
start ( )