Package: riposte
riposteRiposteallows you to easily wrap your application inside a tailored
interactive shell. Common chores regarding building REPLs was factored out and
being taken care of so you can really focus on specific domain logic of your
application.The motivation for buildingRipostecoming from many sleepless nights of
handling numerous tricky cases regarding REPLs duringroutersploitdevelopment. Like
every other project it began very innocently but after a while, when the project
got some real traction and code base was rapidly growing, shell logic started
to intertwine with domain logic making things less and less readable and
contributor friendly.Moreover, to our surprise, people started to forkroutersploitnot because they were
interested in the security of embedded devices but simply because they want to
leverage our interactive shell logic and build their own tools using similar
concept. All these years they must have said:"There must be a better way!"and they were completely right, the better way is calledRiposte.Table of contentsGetting startedInstallingExample usageManualCommandCompleterGuidesPrintingHistoryPromptBannerInline command executionCLIInput streamsProject statusContributingVersioningLicenseAcknowledgmentsGetting StartedInstallingThe package is available on PyPI so please usepipto install it:pipinstallriposteRiposte supports Python 3.6 and newer.Example usagefromriposteimportRipostecalculator=Riposte(prompt="calc:~$ ")MEMORY=[]@calculator.command("add")defadd(x:int,y:int):result=f"{x}+{y}={x+y}"MEMORY.append(result)calculator.success(result)@calculator.command("multiply")defmultiply(x:int,y:int):result=f"{x}*{y}={x*y}"MEMORY.append(result)calculator.success(result)@calculator.command("memory")defmemory():forentryinMEMORY:calculator.print(entry)calculator.run()calc:~$add22[+]2+2=4calc:~$multiply33[+]3*3=9calc:~$memory2+2=43*3=9calc:~$ManualCommandFirst and foremost you want to register some commands to make your REPL
actionable. Adding command and bounding it with handling function is possible
throughRiposte.commanddecorator.fromriposteimportRiposterepl=Riposte()@repl.command("hello")defhello():repl.success("Is it me you looking for?")repl.run()riposte:~$hello[+]Isitmeyoulookingfor?AdditionallyRiposte.commandaccepts few optional parameters:descriptionfew words describing command which you can later use to build
meaningful helpguidesdefinition of how to interpret passed argumentsCompleterRipostecomes with support for tab-completion for commands. You can register
completer function in a similar way you registering commands, just useRiposte.completedecorator and point it to a specific command.fromriposteimportRiposterepl=Riposte()START_SUBCOMMANDS=["foo","bar"]@repl.command("start")defstart(subcommand:str):ifsubcommandinSTART_SUBCOMMANDS:repl.status(f"{subcommand}started")else:repl.error("Unknown subcommand.")@repl.complete("start")defstart_completer(text,line,start_index,end_index):return[subcommandforsubcommandinSTART_SUBCOMMANDSifsubcommand.startswith(text)]repl.run()Completer function is triggered by the TAB key. Every completer function should
return list of valid options and should accept the following parameters:textlast word in the linelinecontent of the whole linestart_indexstarting index of the last word in the lineend_indexending index of the last word in the lineSo in the case of our example:riposte:~ $ start ba<TAB>text -> "ba"
line -> "start ba"
start_index -> 6
end_index -> 8Equipped with this information you can build your custom completer functions for
every command.GuidesGuides is a way of saying howcommandshould interpret arguments
passed by the user via prompt.Riposterely ontype-hintsin order to do that.fromriposteimportRiposterepl=Riposte()@repl.command("guideme")defguideme(x:int,y:str):repl.print("x:",x,type(x))repl.print("y:",y,type(y))repl.run()riposte:~$guideme11x:1<class'int'>
y:1<class'str'>In both cases we've passed value1asxandy. Based on
parameter's type-hint passed arguments was interpreted asintin case ofxand asstrin case ofy. You can also use this technique for different types.fromriposteimportRiposterepl=Riposte()@repl.command("guideme")defguideme(x:dict,y:list):x["foo"]="bar"repl.print("x:",x,type(x))y.append("foobar")repl.print("y:",y,type(y))repl.run()riposte:~$guideme"{'bar': 'baz'}""['barbaz']"x:{'bar':'baz','foo':'bar'}<class'dict'>
y:['barbaz','foobar']<class'list'>Another more powerful way of defining guides for handling function parameters
is defining it straight fromRiposte.commanddecorator. In this case guide
defined this way take precedence over the type hints.fromriposteimportRiposterepl=Riposte()@repl.command("guideme",guides={"x":[int]})defguideme(x):repl.print("x:",x,type(x))repl.run()riposte:~$guideme1x:1<class'int'>Why it is more powerful? Because this way you can chain different guides,
where output of one guide is input for another, creating validation or cast
input into more complex types.fromcollectionsimportnamedtuplefromriposteimportRipostefromriposte.exceptionsimportRiposteExceptionfromriposte.guidesimportliteralrepl=Riposte()defnon_negative(value:int):ifvalue<0:raiseRiposteException("Value can't be negative")returnvaluePoint=namedtuple("Point",("x","y"))defget_point(value:dict):returnPoint(**value)@repl.command("guideme",guides={"x":[int,non_negative],"y":[literal,get_point]})defguideme(x,y):repl.print("x:",x,type(x))repl.print("y:",y,type(y))repl.run()riposte:~$guideme-1'{"x": 1, "y": 2}'[-]Valuecan't be negativeriposte:~ $ guideme 1 '{"x":1,"y":2}'x: 1 <class 'int'>y: Point(x=1, y=2) <class '__main__.Point'>
riposte:~$Under the hood, it is a simple function call where the input string is passed
to first guide function in the chain. In this case, the call looks like this:non_negative(int("-1"))# guide chain for parameter `x`get_point(literal('{"x": 1, "y": 2}'))# guide chain for parameter `y`PrintingRipostecomes with built-in thread safe printing methods:printinfoerrorstatussuccessEvery method follows the signature of Python's built-inprint()function.
Besidesprintall of them provide informative coloring corresponding to its name.We strongly encourage to stick to our thread safe printing API but if you are
feeling frisky, know what you are doing and you are 100% sure, that threaded
execution is something that will never come up at some point in the lifecycle of
you application feel free to use Python's built-inprint()function.ExtendingPrinterMixinIf you want to change the styling of existing methods or add custom one, please
extendPrinterMixinclass.fromriposteimportRipostefromriposte.printer.mixinsimportPrinterMixinclassExtendedPrinterMixin(PrinterMixin):defsuccess(self,*args,**kwargs):# overwriting existing methodself.print(*args,**kwargs)defshout(self,*args,**kwargs):# adding new oneself.print((*args,"!!!"),**kwargs)classCustomRiposte(Riposte,ExtendedPrinterMixin):passrepl=CustomRiposte()@repl.command("foobar")deffoobar(message:str):repl.shout(message)CustomizingPrinterMixinNot happy about existing printing API? No problem, you can also build your own
from scratch usingPrinterBaseMixinand its thread safe_printmethod.fromriposteimportRipostefromriposte.printer.mixinsimportPrinterBaseMixinclassCustomPrinterMixin(PrinterBaseMixin):defask(self,*args,**kwargs):# adding new oneself._print((*args,"???"),**kwargs)defshout(self,*args,**kwargs):# adding new oneself._print((*args,"!!!"),**kwargs)classCustomRiposte(Riposte,CustomPrinterMixin):passrepl=CustomRiposte()@repl.command("foobar")deffoobar(message:str):repl.shout(message)repl.ask(message)repl.success(message)# It'll raise exception as it's no longer availableColoring output withPalleteIf you feel like adding a few colors to the output you can always usePallete.fromriposteimportRipostefromriposte.printerimportPaletterepl=Riposte()@repl.command("foo")deffoo(msg:str):repl.print(Palette.GREEN.format(msg))# It will be greenPalletegoes with the following output formattings:GREYREDGREENYELLOWBLUEMAGENTACYANWHITEBOLDHistoryCommand history is stored in your HOME directory in.ripostefile.
The default length is 100 lines. Both settings can be changed usinghistory_fileandhistory_lengthparameters.frompathlibimportPathfromriposteimportRiposterepl=Riposte(history_file=Path.home()/".custom_history_file",history_length=500,)PromptThe default prompt isriposte:~ $but you can easily customize it:fromriposteimportRiposterepl=Riposte(prompt="custom-prompt >>> ")repl.run()You can also dynamically resolve prompt layout based on the state of some object
simply by overwritingRiposte.promptproperty. In the following example, we'll
determine prompt based onMODULEvalue:fromriposteimportRiposteclassApplication:def__init__(self):self.module=NoneclassCustomRiposte(Riposte):@propertydefprompt(self):ifapp.module:returnf"foo:{app.module}> "else:returnself._prompt# reference to `prompt` parameter.app=Application()repl=CustomRiposte(prompt="foo > ")@repl.command("set")defset_module(module_name:str):app.module=module_namerepl.success("Module has been set.")@repl.command("unset")defunset_module():app.module=Nonerepl.success("Module has been unset.")repl.run()foo>setbar[+]Modulehasbeenset.
foo:bar>unset[+]Modulehasbeenunset.
foo>Banner# banner.pyfromriposteimportRiposteBANNER=""" _   _      _ _         _    _            _     _ _| | | |    | | |       | |  | |          | |   | | || |_| | ___| | | ___   | |  | | ___  _ __| | __| | ||  _  |/ _ \ | |/ _ \  | |/\| |/ _ \| '__| |/ _` | || | | |  __/ | | (_) | \  /\  / (_) | |  | | (_| |_|\_| |_/\___|_|_|\___/   \/  \/ \___/|_|  |_|\__,_(_)Welcome User Hello World v1.2.3"""repl=Riposte(banner=BANNER)@repl.command("hello")defhello():repl.print("Hello World!")repl.run()$pythonbanner.py_________||||||||||||||||||_||___|||___||||______||__||||_|/_\||/_\||/\||/_\|'__||/_`||||||__/||(_)|\/\/(_)||||(_||_|\_||_/\___|_|_|\___/\/\/\___/|_||_|\__,_(_)WelcomeUserHelloWorldv1.2.3

riposte:~$If for some reason you don't want to display banner (Maybe you have custominput stream?) you can setRiposte.print_bannerattribute
toFalse.Inline command executionSimilarly to thebashif you delimit commands with semicolon you can trigger
execution of multiple commands in one line.riposte:~$hello;hello;hello[+]Isitmeyoulookingfor?[+]Isitmeyoulookingfor?[+]Isitmeyoulookingfor?Ripostealso exposes CLI for your applications which gives you the ability to
pass commands using-cswitch:$pythonapp.py-c"hello; hello; hello;"[+]Isitmeyoulookingfor?[+]Isitmeyoulookingfor?[+]Isitmeyoulookingfor?
$Given all of this, you can also start to treat your application as something
that could be turned into automated scripts.CLIIf you application needs custom CLI argumentsRipostegives you way to
implement it by overwritingRiposte.setup_cli()method. Let's say you want to
introduce--verboseflag into your application:# custom_cli_args.pyfromriposteimportRiposteclassCustomArgsRiposte(Riposte):defsetup_cli(self):super().setup_cli()# preserve default Riposte CLIself.parser.add_argument("-v","--verbose",action="store_true",help="Verbose mode")repl=CustomArgsRiposte()@repl.command("foo")deffoo(bar:str):repl.success("Foobar executed.")ifrepl.arguments.verbose:repl.success("Argument passed as bar: ",bar)repl.run()$pythoncustom_cli_args.py-v
riposte:~$foo123[+]Foobarexecuted.[+]Argumentpassedasbar:123riposte:~$Riposte.parseris an instance of Python's builtinargparse.ArgumentParserso for all further instructions regarding adding CLI arguments please followargparsedocumentation.Passed arguments are being parsed inRiposte.run()and stored inRiposte.argumentsso you can access it within your application. If you need
to access them before entering the main evaluation loop you can overwriteRiposte.parse_cli_arguments()fromriposteimportRiposteclassCustomArgsRiposte(Riposte):defsetup_cli(self):super().setup_cli()# preserve default Riposte CLIself.parser.add_argument("-v","--verbose",action="store_true",help="Verbose mode")defparse_cli_arguments(self):super().parse_cli_arguments()# preserve default Riposte CLIifself.arguments.verbose:do_something_specific()Input streamsThe input stream is an abstraction telling how you feedRipostewith
commands. Right now you can use following ones out of the box.PromptDefault one which allows you input commands using the traditional prompt.CLIRipostealso exposes CLI for your applications which gives you the ability
to pass commands using-cswitch:$pythonapp.py-c"hello; hello; hello;"[+]Isitmeyoulookingfor?[+]Isitmeyoulookingfor?[+]Isitmeyoulookingfor?FileYou can also pass text file containing commands as an argument to your
application:# demo.pyfromriposteimportRiposterepl=Riposte()@repl.command("hello")defhello():repl.print("Is it me you looking for?")repl.run()commands.rpsttext file containing commands to be executed:hello
hello
hello$pythondemo.pycommands.rpst[+]Isitmeyoulookingfor?[+]Isitmeyoulookingfor?[+]Isitmeyoulookingfor?Adding custom input streamIf for some reason you need a custom way of feedingRipostewith commands
you can always add your custom input stream. The input stream is a generator
that yields function which after calling it returns a string (the command)Generator[Callable[[], str], None, None]. Let's say you are an evil genius
and want to make your interactive shell application less interactive by
feeding it with some kind of messaging system.importitertoolsfromtypingimportCallable,GeneratorfromriposteimportRipostefromsome.messaging.systemimportSubscriberdefsome_messaging_system_input_stream(subscriber:Subscriber# you can parametrize your input streams)->Generator[Callable,None,None]:# itertools.repeat() make sure that your input stream runs foreveryield fromitertools.repeat(subscriber.poll)# calling poll() will return commandclassCustomInputStreamRiposte(Riposte):defsetup_cli(self):super().setup_cli()# preserve default Riposte CLIself.parser.add_argument("-h","--host",help="Some messaging system address")defparse_cli_arguments(self)->None:super().parse_cli_arguments()# preserve default Riposte CLIifself.arguments.host:subscriber=Subscriber(self.arguments.host)self.input_stream=some_messaging_system_input_stream(subscriber)self.print_banner=False# I guess you don't want to print bannerProject statusRiposteis under development. It might be considered to be in beta phase.
There might be some breaking changes in the future although a lot of concepts
present here was already battle-tested duringroutersploitdevelopment.ContributingPlease readCONTRIBUTING.mdfor details on our code of conduct, and the
process for submitting pull requests to us.VersioningProject usesSemVerversioning. For the versions
available, see thereleases.LicenseRiposteis licensed under the MIT License - see theLICENSEfile for detailsAcknowledgmentsroutersploitclick