def default_parser( self, parents=[], # input_pcaps: List[Tuple[str, PreprocessingActions]], **kwargs ) -> argparse_completer.ACArgumentParser: """ Generates a parser with common options. This parser can be completed or overridden by its children. Args: mptcpstream: to accept an mptcp.stream id available_dataframe: True if a pcap was preloaded at start direction: Enable filtering the stream depending if the packets were sent towards the MPTCP client or the MPTCP server skip_subflows: Allow to hide some subflows from the plot Return: An argparse.ArgumentParser """ parser = argparse_completer.ACArgumentParser( parents=parents, add_help=(parents == []), ) parser.add_argument('-o', '--out', action="store", default=None, help='Name of the output plot') parser.add_argument( '--display', action="store_true", help='will display the generated plot (use xdg-open by default)') parser.add_argument('--title', action="store", type=str, help='Overrides the default plot title') parser.add_argument( '--primary', action="store_true", help="Copy to X clipboard, requires `xsel` to be installed") return parser
def default_parser(self, *args, **kwargs): parser = argparse_completer.ACArgumentParser( description="Plot One Way Delays") subparsers = parser.add_subparsers( dest="protocol", title="Subparsers", help='sub-command help', ) subparsers.required = True # type: ignore actions = { "tcp": PreprocessingActions.MergeTcp | PreprocessingActions.FilterDestination, "mptcp": PreprocessingActions.MergeMpTcp | PreprocessingActions.FilterDestination, } for protocol, actions in actions.items(): expected_pcaps = {"pcap": actions} temp = gen_pcap_parser(input_pcaps=expected_pcaps, parents=[super().default_parser()]) subparser = subparsers.add_parser(protocol, parents=[ temp, ], add_help=False) parser.description = ''' Helps plotting One Way Delays between tcp connections ''' parser.epilog = ''' plot owd tcp examples/client_2_filtered.pcapng 0 examples/server_2_filtered.pcapng 0 --display ''' return parser
def __init__(self): super().__init__() video_types_subparsers = TabCompleteExample.video_parser.add_subparsers( title='Media Types', dest='type') vid_movies_parser = argparse_completer.ACArgumentParser(prog='movies') vid_movies_parser.set_defaults( func=TabCompleteExample._do_vid_media_movies) vid_movies_commands_subparsers = vid_movies_parser.add_subparsers( title='Commands', dest='command') vid_movies_list_parser = vid_movies_commands_subparsers.add_parser( 'list') vid_movies_list_parser.add_argument('-t', '--title', help='Title Filter') vid_movies_list_parser.add_argument( '-r', '--rating', help='Rating Filter', nargs='+', choices=TabCompleteExample.ratings_types) # save a reference to the action object director_action = vid_movies_list_parser.add_argument( '-d', '--director', help='Director Filter') actor_action = vid_movies_list_parser.add_argument('-a', '--actor', help='Actor Filter', action='append') # tag the action objects with completion providers. This can be a collection or a callable setattr(director_action, argparse_completer.ACTION_ARG_CHOICES, TabCompleteExample.static_list_directors) setattr(actor_action, argparse_completer.ACTION_ARG_CHOICES, query_actors) vid_movies_add_parser = vid_movies_commands_subparsers.add_parser( 'add') vid_movies_add_parser.add_argument('title', help='Movie Title') vid_movies_add_parser.add_argument( 'rating', help='Movie Rating', choices=TabCompleteExample.ratings_types) # save a reference to the action object director_action = vid_movies_add_parser.add_argument('-d', '--director', help='Director', nargs=(1, 2), required=True) actor_action = vid_movies_add_parser.add_argument('actor', help='Actors', nargs='*') vid_movies_load_parser = vid_movies_commands_subparsers.add_parser( 'load') vid_movie_file_action = vid_movies_load_parser.add_argument( 'movie_file', help='Movie database') vid_movies_read_parser = vid_movies_commands_subparsers.add_parser( 'read') vid_movie_fread_action = vid_movies_read_parser.add_argument( 'movie_file', help='Movie database') # tag the action objects with completion providers. This can be a collection or a callable setattr(director_action, argparse_completer.ACTION_ARG_CHOICES, TabCompleteExample.static_list_directors) setattr(actor_action, argparse_completer.ACTION_ARG_CHOICES, 'instance_query_actors') # tag the file property with a custom completion function 'delimeter_complete' provided by cmd2. setattr(vid_movie_file_action, argparse_completer.ACTION_ARG_CHOICES, ('delimiter_complete', { 'delimiter': '/', 'match_against': TabCompleteExample.file_list })) setattr(vid_movie_fread_action, argparse_completer.ACTION_ARG_CHOICES, ('path_complete', [False, False])) vid_movies_delete_parser = vid_movies_commands_subparsers.add_parser( 'delete') vid_delete_movie_id = vid_movies_delete_parser.add_argument( 'movie_id', help='Movie ID') setattr(vid_delete_movie_id, argparse_completer.ACTION_ARG_CHOICES, TabCompleteExample.instance_query_movie_ids) setattr(vid_delete_movie_id, argparse_completer.ACTION_DESCRIPTIVE_COMPLETION_HEADER, 'Title') # Add the 'movies' parser as a parent of sub-parser video_types_subparsers.add_parser('movies', parents=[vid_movies_parser], add_help=False) vid_shows_parser = argparse_completer.ACArgumentParser(prog='shows') vid_shows_parser.set_defaults( func=TabCompleteExample._do_vid_media_shows) vid_shows_commands_subparsers = vid_shows_parser.add_subparsers( title='Commands', dest='command') vid_shows_list_parser = vid_shows_commands_subparsers.add_parser( 'list') video_types_subparsers.add_parser('shows', parents=[vid_shows_parser], add_help=False)
class TabCompleteExample(cmd2.Cmd): """ Example cmd2 application where we a base command which has a couple subcommands.""" CAT_AUTOCOMPLETE = 'AutoComplete Examples' def __init__(self): super().__init__() video_types_subparsers = TabCompleteExample.video_parser.add_subparsers( title='Media Types', dest='type') vid_movies_parser = argparse_completer.ACArgumentParser(prog='movies') vid_movies_parser.set_defaults( func=TabCompleteExample._do_vid_media_movies) vid_movies_commands_subparsers = vid_movies_parser.add_subparsers( title='Commands', dest='command') vid_movies_list_parser = vid_movies_commands_subparsers.add_parser( 'list') vid_movies_list_parser.add_argument('-t', '--title', help='Title Filter') vid_movies_list_parser.add_argument( '-r', '--rating', help='Rating Filter', nargs='+', choices=TabCompleteExample.ratings_types) # save a reference to the action object director_action = vid_movies_list_parser.add_argument( '-d', '--director', help='Director Filter') actor_action = vid_movies_list_parser.add_argument('-a', '--actor', help='Actor Filter', action='append') # tag the action objects with completion providers. This can be a collection or a callable setattr(director_action, argparse_completer.ACTION_ARG_CHOICES, TabCompleteExample.static_list_directors) setattr(actor_action, argparse_completer.ACTION_ARG_CHOICES, query_actors) vid_movies_add_parser = vid_movies_commands_subparsers.add_parser( 'add') vid_movies_add_parser.add_argument('title', help='Movie Title') vid_movies_add_parser.add_argument( 'rating', help='Movie Rating', choices=TabCompleteExample.ratings_types) # save a reference to the action object director_action = vid_movies_add_parser.add_argument('-d', '--director', help='Director', nargs=(1, 2), required=True) actor_action = vid_movies_add_parser.add_argument('actor', help='Actors', nargs='*') vid_movies_load_parser = vid_movies_commands_subparsers.add_parser( 'load') vid_movie_file_action = vid_movies_load_parser.add_argument( 'movie_file', help='Movie database') vid_movies_read_parser = vid_movies_commands_subparsers.add_parser( 'read') vid_movie_fread_action = vid_movies_read_parser.add_argument( 'movie_file', help='Movie database') # tag the action objects with completion providers. This can be a collection or a callable setattr(director_action, argparse_completer.ACTION_ARG_CHOICES, TabCompleteExample.static_list_directors) setattr(actor_action, argparse_completer.ACTION_ARG_CHOICES, 'instance_query_actors') # tag the file property with a custom completion function 'delimeter_complete' provided by cmd2. setattr(vid_movie_file_action, argparse_completer.ACTION_ARG_CHOICES, ('delimiter_complete', { 'delimiter': '/', 'match_against': TabCompleteExample.file_list })) setattr(vid_movie_fread_action, argparse_completer.ACTION_ARG_CHOICES, ('path_complete', [False, False])) vid_movies_delete_parser = vid_movies_commands_subparsers.add_parser( 'delete') vid_delete_movie_id = vid_movies_delete_parser.add_argument( 'movie_id', help='Movie ID') setattr(vid_delete_movie_id, argparse_completer.ACTION_ARG_CHOICES, TabCompleteExample.instance_query_movie_ids) setattr(vid_delete_movie_id, argparse_completer.ACTION_DESCRIPTIVE_COMPLETION_HEADER, 'Title') # Add the 'movies' parser as a parent of sub-parser video_types_subparsers.add_parser('movies', parents=[vid_movies_parser], add_help=False) vid_shows_parser = argparse_completer.ACArgumentParser(prog='shows') vid_shows_parser.set_defaults( func=TabCompleteExample._do_vid_media_shows) vid_shows_commands_subparsers = vid_shows_parser.add_subparsers( title='Commands', dest='command') vid_shows_list_parser = vid_shows_commands_subparsers.add_parser( 'list') video_types_subparsers.add_parser('shows', parents=[vid_shows_parser], add_help=False) # For mocking a data source for the example commands ratings_types = ['G', 'PG', 'PG-13', 'R', 'NC-17'] show_ratings = ['TV-Y', 'TV-Y7', 'TV-G', 'TV-PG', 'TV-14', 'TV-MA'] static_list_directors = [ 'J. J. Abrams', 'Irvin Kershner', 'George Lucas', 'Richard Marquand', 'Rian Johnson', 'Gareth Edwards' ] USER_MOVIE_LIBRARY = ['ROGUE1', 'SW_EP04', 'SW_EP05'] MOVIE_DATABASE_IDS = [ 'SW_EP1', 'SW_EP02', 'SW_EP03', 'ROGUE1', 'SW_EP04', 'SW_EP05', 'SW_EP06', 'SW_EP07', 'SW_EP08', 'SW_EP09' ] MOVIE_DATABASE = { 'SW_EP04': { 'title': 'Star Wars: Episode IV - A New Hope', 'rating': 'PG', 'director': ['George Lucas'], 'actor': [ 'Mark Hamill', 'Harrison Ford', 'Carrie Fisher', 'Alec Guinness', 'Peter Mayhew', 'Anthony Daniels' ] }, 'SW_EP05': { 'title': 'Star Wars: Episode V - The Empire Strikes Back', 'rating': 'PG', 'director': ['Irvin Kershner'], 'actor': [ 'Mark Hamill', 'Harrison Ford', 'Carrie Fisher', 'Alec Guinness', 'Peter Mayhew', 'Anthony Daniels' ] }, 'SW_EP06': { 'title': 'Star Wars: Episode VI - Return of the Jedi', 'rating': 'PG', 'director': ['Richard Marquand'], 'actor': [ 'Mark Hamill', 'Harrison Ford', 'Carrie Fisher', 'Alec Guinness', 'Peter Mayhew', 'Anthony Daniels' ] }, 'SW_EP1': { 'title': 'Star Wars: Episode I - The Phantom Menace', 'rating': 'PG', 'director': ['George Lucas'], 'actor': ['Liam Neeson', 'Ewan McGregor', 'Natalie Portman', 'Jake Lloyd'] }, 'SW_EP02': { 'title': 'Star Wars: Episode II - Attack of the Clones', 'rating': 'PG', 'director': ['George Lucas'], 'actor': [ 'Liam Neeson', 'Ewan McGregor', 'Natalie Portman', 'Hayden Christensen', 'Christopher Lee' ] }, 'SW_EP03': { 'title': 'Star Wars: Episode III - Revenge of the Sith', 'rating': 'PG-13', 'director': ['George Lucas'], 'actor': [ 'Liam Neeson', 'Ewan McGregor', 'Natalie Portman', 'Hayden Christensen' ] }, } USER_SHOW_LIBRARY = {'SW_REB': ['S01E01', 'S02E02']} SHOW_DATABASE_IDS = ['SW_CW', 'SW_TCW', 'SW_REB'] SHOW_DATABASE = { 'SW_CW': { 'title': 'Star Wars: Clone Wars', 'rating': 'TV-Y7', 'seasons': { 1: ['S01E01', 'S01E02', 'S01E03'], 2: ['S02E01', 'S02E02', 'S02E03'] } }, 'SW_TCW': { 'title': 'Star Wars: The Clone Wars', 'rating': 'TV-PG', 'seasons': { 1: ['S01E01', 'S01E02', 'S01E03'], 2: ['S02E01', 'S02E02', 'S02E03'] } }, 'SW_REB': { 'title': 'Star Wars: Rebels', 'rating': 'TV-Y7', 'seasons': { 1: ['S01E01', 'S01E02', 'S01E03'], 2: ['S02E01', 'S02E02', 'S02E03'] } }, } file_list = \ [ '/home/user/file.db', '/home/user/file space.db', '/home/user/another.db', '/home/other user/maps.db', '/home/other user/tests.db' ] def instance_query_actors(self) -> List[str]: """Simulating a function that queries and returns a completion values""" return actors def instance_query_movie_ids(self) -> List[str]: """Demonstrates showing tabular hinting of tab completion information""" completions_with_desc = [] for movie_id in self.MOVIE_DATABASE_IDS: if movie_id in self.MOVIE_DATABASE: movie_entry = self.MOVIE_DATABASE[movie_id] completions_with_desc.append( argparse_completer.CompletionItem(movie_id, movie_entry['title'])) return completions_with_desc ################################################################################### # The media command demonstrates a completer with multiple layers of subcommands # - This example demonstrates how to tag a completion attribute on each action, enabling argument # completion without implementing a complete_COMMAND function def _do_vid_media_movies(self, args) -> None: if not args.command: self.do_help('video movies') elif args.command == 'list': for movie_id in TabCompleteExample.MOVIE_DATABASE: movie = TabCompleteExample.MOVIE_DATABASE[movie_id] print( '{}\n-----------------------------\n{} ID: {}\nDirector: {}\nCast:\n {}\n\n' .format(movie['title'], movie['rating'], movie_id, ', '.join(movie['director']), '\n '.join(movie['actor']))) def _do_vid_media_shows(self, args) -> None: if not args.command: self.do_help('video shows') elif args.command == 'list': for show_id in TabCompleteExample.SHOW_DATABASE: show = TabCompleteExample.SHOW_DATABASE[show_id] print('{}\n-----------------------------\n{} ID: {}'.format( show['title'], show['rating'], show_id)) for season in show['seasons']: ep_list = show['seasons'][season] print(' Season {}:\n {}'.format( season, '\n '.join(ep_list))) print() video_parser = argparse_completer.ACArgumentParser(prog='video') @cmd2.with_category(CAT_AUTOCOMPLETE) @cmd2.with_argparser(video_parser) def do_video(self, args): """Video management command demonstrates multiple layers of subcommands being handled by AutoCompleter""" func = getattr(args, 'func', None) if func is not None: # Call whatever subcommand function was selected func(self, args) else: # No subcommand was provided, so call help self.do_help('video')
class PyscriptExample(Cmd): ratings_types = ['G', 'PG', 'PG-13', 'R', 'NC-17'] def _do_media_movies(self, args) -> None: if not args.command: self.do_help('media movies') else: self.poutput('media movies ' + str(args.__dict__)) def _do_media_shows(self, args) -> None: if not args.command: self.do_help('media shows') if not args.command: self.do_help('media shows') else: self.poutput('media shows ' + str(args.__dict__)) media_parser = argparse_completer.ACArgumentParser(prog='media') media_types_subparsers = media_parser.add_subparsers(title='Media Types', dest='type') movies_parser = media_types_subparsers.add_parser('movies') movies_parser.set_defaults(func=_do_media_movies) movies_commands_subparsers = movies_parser.add_subparsers(title='Commands', dest='command') movies_list_parser = movies_commands_subparsers.add_parser('list') movies_list_parser.add_argument('-t', '--title', help='Title Filter') movies_list_parser.add_argument('-r', '--rating', help='Rating Filter', nargs='+', choices=ratings_types) movies_list_parser.add_argument('-d', '--director', help='Director Filter') movies_list_parser.add_argument('-a', '--actor', help='Actor Filter', action='append') movies_add_parser = movies_commands_subparsers.add_parser('add') movies_add_parser.add_argument('title', help='Movie Title') movies_add_parser.add_argument('rating', help='Movie Rating', choices=ratings_types) movies_add_parser.add_argument('-d', '--director', help='Director', nargs=(1, 2), required=True) movies_add_parser.add_argument('actor', help='Actors', nargs='*') movies_delete_parser = movies_commands_subparsers.add_parser('delete') shows_parser = media_types_subparsers.add_parser('shows') shows_parser.set_defaults(func=_do_media_shows) shows_commands_subparsers = shows_parser.add_subparsers(title='Commands', dest='command') shows_list_parser = shows_commands_subparsers.add_parser('list') @with_argparser(media_parser) def do_media(self, args): """Media management command demonstrates multiple layers of sub-commands being handled by AutoCompleter""" func = getattr(args, 'func', None) if func is not None: # Call whatever subcommand function was selected func(self, args) else: # No subcommand was provided, so call help self.do_help('media') foo_parser = argparse_completer.ACArgumentParser(prog='foo') foo_parser.add_argument('-c', dest='counter', action='count') foo_parser.add_argument('-t', dest='trueval', action='store_true') foo_parser.add_argument('-n', dest='constval', action='store_const', const=42) foo_parser.add_argument('variable', nargs=(2, 3)) foo_parser.add_argument('optional', nargs='?') foo_parser.add_argument('zeroormore', nargs='*') @with_argparser(foo_parser) def do_foo(self, args): self.poutput('foo ' + str(sorted(args.__dict__))) if self._in_py: FooResult = namedtuple_with_defaults('FooResult', [ 'counter', 'trueval', 'constval', 'variable', 'optional', 'zeroormore' ]) self._last_result = FooResult( **{ 'counter': args.counter, 'trueval': args.trueval, 'constval': args.constval, 'variable': args.variable, 'optional': args.optional, 'zeroormore': args.zeroormore }) bar_parser = argparse_completer.ACArgumentParser(prog='bar') bar_parser.add_argument('first') bar_parser.add_argument('oneormore', nargs='+') bar_parser.add_argument('-a', dest='aaa') @with_argparser(bar_parser) def do_bar(self, args): out = 'bar ' arg_dict = args.__dict__ keys = list(arg_dict.keys()) keys.sort() out += '{' for key in keys: out += "'{}':'{}'".format(key, arg_dict[key]) self.poutput(out)
class TabCompleteExample(cmd2.Cmd): """ Example cmd2 application where we a base command which has a couple sub-commands.""" CAT_AUTOCOMPLETE = 'AutoComplete Examples' def __init__(self): super().__init__() # For mocking a data source for the example commands ratings_types = ['G', 'PG', 'PG-13', 'R', 'NC-17'] show_ratings = ['TV-Y', 'TV-Y7', 'TV-G', 'TV-PG', 'TV-14', 'TV-MA'] static_list_directors = [ 'J. J. Abrams', 'Irvin Kershner', 'George Lucas', 'Richard Marquand', 'Rian Johnson', 'Gareth Edwards' ] USER_MOVIE_LIBRARY = ['ROGUE1', 'SW_EP04', 'SW_EP05'] MOVIE_DATABASE_IDS = [ 'SW_EP1', 'SW_EP02', 'SW_EP03', 'ROGUE1', 'SW_EP04', 'SW_EP05', 'SW_EP06', 'SW_EP07', 'SW_EP08', 'SW_EP09' ] MOVIE_DATABASE = { 'SW_EP04': { 'title': 'Star Wars: Episode IV - A New Hope', 'rating': 'PG', 'director': ['George Lucas'], 'actor': [ 'Mark Hamill', 'Harrison Ford', 'Carrie Fisher', 'Alec Guinness', 'Peter Mayhew', 'Anthony Daniels' ] }, 'SW_EP05': { 'title': 'Star Wars: Episode V - The Empire Strikes Back', 'rating': 'PG', 'director': ['Irvin Kershner'], 'actor': [ 'Mark Hamill', 'Harrison Ford', 'Carrie Fisher', 'Alec Guinness', 'Peter Mayhew', 'Anthony Daniels' ] }, 'SW_EP06': { 'title': 'Star Wars: Episode VI - Return of the Jedi', 'rating': 'PG', 'director': ['Richard Marquand'], 'actor': [ 'Mark Hamill', 'Harrison Ford', 'Carrie Fisher', 'Alec Guinness', 'Peter Mayhew', 'Anthony Daniels' ] }, 'SW_EP1': { 'title': 'Star Wars: Episode I - The Phantom Menace', 'rating': 'PG', 'director': ['George Lucas'], 'actor': ['Liam Neeson', 'Ewan McGregor', 'Natalie Portman', 'Jake Lloyd'] }, 'SW_EP02': { 'title': 'Star Wars: Episode II - Attack of the Clones', 'rating': 'PG', 'director': ['George Lucas'], 'actor': [ 'Liam Neeson', 'Ewan McGregor', 'Natalie Portman', 'Hayden Christensen', 'Christopher Lee' ] }, 'SW_EP03': { 'title': 'Star Wars: Episode III - Revenge of the Sith', 'rating': 'PG-13', 'director': ['George Lucas'], 'actor': [ 'Liam Neeson', 'Ewan McGregor', 'Natalie Portman', 'Hayden Christensen' ] }, } USER_SHOW_LIBRARY = {'SW_REB': ['S01E01', 'S02E02']} SHOW_DATABASE_IDS = ['SW_CW', 'SW_TCW', 'SW_REB'] SHOW_DATABASE = { 'SW_CW': { 'title': 'Star Wars: Clone Wars', 'rating': 'TV-Y7', 'seasons': { 1: ['S01E01', 'S01E02', 'S01E03'], 2: ['S02E01', 'S02E02', 'S02E03'] } }, 'SW_TCW': { 'title': 'Star Wars: The Clone Wars', 'rating': 'TV-PG', 'seasons': { 1: ['S01E01', 'S01E02', 'S01E03'], 2: ['S02E01', 'S02E02', 'S02E03'] } }, 'SW_REB': { 'title': 'Star Wars: Rebels', 'rating': 'TV-Y7', 'seasons': { 1: ['S01E01', 'S01E02', 'S01E03'], 2: ['S02E01', 'S02E02', 'S02E03'] } }, } file_list = \ [ '/home/user/file.db', '/home/user/file space.db', '/home/user/another.db', '/home/other user/maps.db', '/home/other user/tests.db' ] def instance_query_actors(self) -> List[str]: """Simulating a function that queries and returns a completion values""" return actors def instance_query_movie_ids(self) -> List[str]: """Demonstrates showing tabular hinting of tab completion information""" completions_with_desc = [] # Sort the movie id strings with a natural sort since they contain numbers for movie_id in utils.natural_sort(self.MOVIE_DATABASE_IDS): if movie_id in self.MOVIE_DATABASE: movie_entry = self.MOVIE_DATABASE[movie_id] completions_with_desc.append( argparse_completer.CompletionItem(movie_id, movie_entry['title'])) # Mark that we already sorted the matches self.matches_sorted = True return completions_with_desc # This demonstrates a number of customizations of the AutoCompleter version of ArgumentParser # - The help output will separately group required vs optional flags # - The help output for arguments with multiple flags or with append=True is more concise # - ACArgumentParser adds the ability to specify ranges of argument counts in 'nargs' suggest_description = "Suggest command demonstrates argparse customizations.\n" suggest_description += "See hybrid_suggest and orig_suggest to compare the help output." suggest_parser = argparse_completer.ACArgumentParser( description=suggest_description) suggest_parser.add_argument('-t', '--type', choices=['movie', 'show'], required=True) suggest_parser.add_argument('-d', '--duration', nargs=(1, 2), action='append', help='Duration constraint in minutes.\n' '\tsingle value - maximum duration\n' '\t[a, b] - duration range') @cmd2.with_category(CAT_AUTOCOMPLETE) @cmd2.with_argparser(suggest_parser) def do_suggest(self, args) -> None: """Suggest command demonstrates argparse customizations""" if not args.type: self.do_help('suggest') # If you prefer the original argparse help output but would like narg ranges, it's possible # to enable narg ranges without the help changes using this method suggest_parser_hybrid = argparse.ArgumentParser() # This registers the custom narg range handling argparse_completer.register_custom_actions(suggest_parser_hybrid) suggest_parser_hybrid.add_argument('-t', '--type', choices=['movie', 'show'], required=True) suggest_parser_hybrid.add_argument('-d', '--duration', nargs=(1, 2), action='append', help='Duration constraint in minutes.\n' '\tsingle value - maximum duration\n' '\t[a, b] - duration range') @cmd2.with_category(CAT_AUTOCOMPLETE) @cmd2.with_argparser(suggest_parser_hybrid) def do_hybrid_suggest(self, args): if not args.type: self.do_help('orig_suggest') # This variant demonstrates the AutoCompleter working with the orginial argparse. # Base argparse is unable to specify narg ranges. Autocompleter will keep expecting additional arguments # for the -d/--duration flag until you specify a new flag or end processing of flags with '--' suggest_parser_orig = argparse.ArgumentParser() suggest_parser_orig.add_argument('-t', '--type', choices=['movie', 'show'], required=True) suggest_parser_orig.add_argument('-d', '--duration', nargs='+', action='append', help='Duration constraint in minutes.\n' '\tsingle value - maximum duration\n' '\t[a, b] - duration range') @cmd2.with_argparser(suggest_parser_orig) @cmd2.with_category(CAT_AUTOCOMPLETE) def do_orig_suggest(self, args) -> None: if not args.type: self.do_help('orig_suggest') ################################################################################### # The media command demonstrates a completer with multiple layers of subcommands # - This example demonstrates how to tag a completion attribute on each action, enabling argument # completion without implementing a complete_COMMAND function def _do_vid_media_movies(self, args) -> None: if not args.command: self.do_help('media movies') elif args.command == 'list': for movie_id in TabCompleteExample.MOVIE_DATABASE: movie = TabCompleteExample.MOVIE_DATABASE[movie_id] print( '{}\n-----------------------------\n{} ID: {}\nDirector: {}\nCast:\n {}\n\n' .format(movie['title'], movie['rating'], movie_id, ', '.join(movie['director']), '\n '.join(movie['actor']))) def _do_vid_media_shows(self, args) -> None: if not args.command: self.do_help('media shows') elif args.command == 'list': for show_id in TabCompleteExample.SHOW_DATABASE: show = TabCompleteExample.SHOW_DATABASE[show_id] print('{}\n-----------------------------\n{} ID: {}'.format( show['title'], show['rating'], show_id)) for season in show['seasons']: ep_list = show['seasons'][season] print(' Season {}:\n {}'.format( season, '\n '.join(ep_list))) print() video_parser = argparse_completer.ACArgumentParser(prog='media') video_types_subparsers = video_parser.add_subparsers(title='Media Types', dest='type') vid_movies_parser = video_types_subparsers.add_parser('movies') vid_movies_parser.set_defaults(func=_do_vid_media_movies) vid_movies_commands_subparsers = vid_movies_parser.add_subparsers( title='Commands', dest='command') vid_movies_list_parser = vid_movies_commands_subparsers.add_parser('list') vid_movies_list_parser.add_argument('-t', '--title', help='Title Filter') vid_movies_list_parser.add_argument('-r', '--rating', help='Rating Filter', nargs='+', choices=ratings_types) # save a reference to the action object director_action = vid_movies_list_parser.add_argument( '-d', '--director', help='Director Filter') actor_action = vid_movies_list_parser.add_argument('-a', '--actor', help='Actor Filter', action='append') # tag the action objects with completion providers. This can be a collection or a callable setattr(director_action, argparse_completer.ACTION_ARG_CHOICES, static_list_directors) setattr(actor_action, argparse_completer.ACTION_ARG_CHOICES, query_actors) vid_movies_add_parser = vid_movies_commands_subparsers.add_parser('add') vid_movies_add_parser.add_argument('title', help='Movie Title') vid_movies_add_parser.add_argument('rating', help='Movie Rating', choices=ratings_types) # save a reference to the action object director_action = vid_movies_add_parser.add_argument('-d', '--director', help='Director', nargs=(1, 2), required=True) actor_action = vid_movies_add_parser.add_argument('actor', help='Actors', nargs='*') vid_movies_load_parser = vid_movies_commands_subparsers.add_parser('load') vid_movie_file_action = vid_movies_load_parser.add_argument( 'movie_file', help='Movie database') vid_movies_read_parser = vid_movies_commands_subparsers.add_parser('read') vid_movie_fread_action = vid_movies_read_parser.add_argument( 'movie_file', help='Movie database') # tag the action objects with completion providers. This can be a collection or a callable setattr(director_action, argparse_completer.ACTION_ARG_CHOICES, static_list_directors) setattr(actor_action, argparse_completer.ACTION_ARG_CHOICES, 'instance_query_actors') # tag the file property with a custom completion function 'delimiter_complete' provided by cmd2. setattr(vid_movie_file_action, argparse_completer.ACTION_ARG_CHOICES, ('delimiter_complete', { 'delimiter': '/', 'match_against': file_list })) setattr(vid_movie_fread_action, argparse_completer.ACTION_ARG_CHOICES, ('path_complete', )) vid_movies_delete_parser = vid_movies_commands_subparsers.add_parser( 'delete') vid_delete_movie_id = vid_movies_delete_parser.add_argument( 'movie_id', help='Movie ID') setattr(vid_delete_movie_id, argparse_completer.ACTION_ARG_CHOICES, instance_query_movie_ids) setattr(vid_delete_movie_id, argparse_completer.ACTION_DESCRIPTIVE_COMPLETION_HEADER, 'Title') vid_shows_parser = video_types_subparsers.add_parser('shows') vid_shows_parser.set_defaults(func=_do_vid_media_shows) vid_shows_commands_subparsers = vid_shows_parser.add_subparsers( title='Commands', dest='command') vid_shows_list_parser = vid_shows_commands_subparsers.add_parser('list') @cmd2.with_category(CAT_AUTOCOMPLETE) @cmd2.with_argparser(video_parser) def do_video(self, args): """Video management command demonstrates multiple layers of sub-commands being handled by AutoCompleter""" func = getattr(args, 'func', None) if func is not None: # Call whatever subcommand function was selected func(self, args) else: # No subcommand was provided, so call help self.do_help('video') ################################################################################### # The media command demonstrates a completer with multiple layers of subcommands # - This example uses a flat completion lookup dictionary def _do_media_movies(self, args) -> None: if not args.command: self.do_help('media movies') elif args.command == 'list': for movie_id in TabCompleteExample.MOVIE_DATABASE: movie = TabCompleteExample.MOVIE_DATABASE[movie_id] print( '{}\n-----------------------------\n{} ID: {}\nDirector: {}\nCast:\n {}\n\n' .format(movie['title'], movie['rating'], movie_id, ', '.join(movie['director']), '\n '.join(movie['actor']))) elif args.command == 'add': print( 'Adding Movie\n----------------\nTitle: {}\nRating: {}\nDirectors: {}\nActors: {}\n\n' .format(args.title, args.rating, ', '.join(args.director), ', '.join(args.actor))) def _do_media_shows(self, args) -> None: if not args.command: self.do_help('media shows') elif args.command == 'list': for show_id in TabCompleteExample.SHOW_DATABASE: show = TabCompleteExample.SHOW_DATABASE[show_id] print('{}\n-----------------------------\n{} ID: {}'.format( show['title'], show['rating'], show_id)) for season in show['seasons']: ep_list = show['seasons'][season] print(' Season {}:\n {}'.format( season, '\n '.join(ep_list))) print() media_parser = argparse_completer.ACArgumentParser(prog='media') media_types_subparsers = media_parser.add_subparsers(title='Media Types', dest='type') movies_parser = media_types_subparsers.add_parser('movies') movies_parser.set_defaults(func=_do_media_movies) movies_commands_subparsers = movies_parser.add_subparsers(title='Commands', dest='command') movies_list_parser = movies_commands_subparsers.add_parser('list') movies_list_parser.add_argument('-t', '--title', help='Title Filter') movies_list_parser.add_argument('-r', '--rating', help='Rating Filter', nargs='+', choices=ratings_types) movies_list_parser.add_argument('-d', '--director', help='Director Filter') movies_list_parser.add_argument('-a', '--actor', help='Actor Filter', action='append') movies_add_parser = movies_commands_subparsers.add_parser('add') movies_add_parser.add_argument('title', help='Movie Title') movies_add_parser.add_argument('rating', help='Movie Rating', choices=ratings_types) movies_add_parser.add_argument('-d', '--director', help='Director', nargs=(1, 2), required=True) movies_add_parser.add_argument('actor', help='Actors', nargs=argparse.REMAINDER) movies_delete_parser = movies_commands_subparsers.add_parser('delete') movies_delete_movie_id = movies_delete_parser.add_argument('movie_id', help='Movie ID') setattr(movies_delete_movie_id, argparse_completer.ACTION_ARG_CHOICES, 'instance_query_movie_ids') setattr(movies_delete_movie_id, argparse_completer.ACTION_DESCRIPTIVE_COMPLETION_HEADER, 'Title') movies_load_parser = movies_commands_subparsers.add_parser('load') movie_file_action = movies_load_parser.add_argument('movie_file', help='Movie database') shows_parser = media_types_subparsers.add_parser('shows') shows_parser.set_defaults(func=_do_media_shows) shows_commands_subparsers = shows_parser.add_subparsers(title='Commands', dest='command') shows_list_parser = shows_commands_subparsers.add_parser('list') @cmd2.with_category(CAT_AUTOCOMPLETE) @cmd2.with_argparser(media_parser) def do_media(self, args): """Media management command demonstrates multiple layers of sub-commands being handled by AutoCompleter""" func = getattr(args, 'func', None) if func is not None: # Call whatever subcommand function was selected func(self, args) else: # No subcommand was provided, so call help self.do_help('media') # This completer is implemented using a single dictionary to look up completion lists for all layers of # subcommands. For each argument, AutoCompleter will search for completion values from the provided # arg_choices dict. This requires careful naming of argparse arguments so that there are no unintentional # name collisions. def complete_media(self, text, line, begidx, endidx): """ Adds tab completion to media""" choices = { 'actor': query_actors, # function 'director': TabCompleteExample.static_list_directors, # static list 'movie_file': (self.path_complete, ) } completer = argparse_completer.AutoCompleter( TabCompleteExample.media_parser, self, arg_choices=choices) tokens, _ = self.tokens_for_completion(line, begidx, endidx) results = completer.complete_command(tokens, text, line, begidx, endidx) return results ################################################################################### # The library command demonstrates a completer with multiple layers of subcommands # with different completion results per sub-command # - This demonstrates how to build a tree of completion lookups to pass down # # Only use this method if you absolutely need to as it dramatically # increases the complexity and decreases readability. def _do_library_movie(self, args): if not args.type or not args.command: self.do_help('library movie') def _do_library_show(self, args): if not args.type: self.do_help('library show') def _query_movie_database(self): return list( set(TabCompleteExample.MOVIE_DATABASE_IDS).difference( set(TabCompleteExample.USER_MOVIE_LIBRARY))) def _query_movie_user_library(self): return TabCompleteExample.USER_MOVIE_LIBRARY def _filter_library(self, text, line, begidx, endidx, full, exclude=()): candidates = list(set(full).difference(set(exclude))) return [entry for entry in candidates if entry.startswith(text)] library_parser = argparse_completer.ACArgumentParser(prog='library') library_subcommands = library_parser.add_subparsers(title='Media Types', dest='type') library_movie_parser = library_subcommands.add_parser('movie') library_movie_parser.set_defaults(func=_do_library_movie) library_movie_subcommands = library_movie_parser.add_subparsers( title='Command', dest='command') library_movie_add_parser = library_movie_subcommands.add_parser('add') library_movie_add_parser.add_argument('movie_id', help='ID of movie to add', action='append') library_movie_add_parser.add_argument('-b', '--borrowed', action='store_true') library_movie_remove_parser = library_movie_subcommands.add_parser( 'remove') library_movie_remove_parser.add_argument('movie_id', help='ID of movie to remove', action='append') library_show_parser = library_subcommands.add_parser('show') library_show_parser.set_defaults(func=_do_library_show) library_show_subcommands = library_show_parser.add_subparsers( title='Command', dest='command') library_show_add_parser = library_show_subcommands.add_parser('add') library_show_add_parser.add_argument('show_id', help='Show IDs to add') library_show_add_parser.add_argument('episode_id', nargs='*', help='Show IDs to add') library_show_rmv_parser = library_show_subcommands.add_parser('remove') # Demonstrates a custom completion function that does more with the command line than is # allowed by the standard completion functions def _filter_episodes(self, text, line, begidx, endidx, show_db, user_lib): tokens, _ = self.tokens_for_completion(line, begidx, endidx) show_id = tokens[3] if show_id: if show_id in show_db: show = show_db[show_id] all_episodes = itertools.chain(*(show['seasons'].values())) if show_id in user_lib: user_eps = user_lib[show_id] else: user_eps = [] return self._filter_library(text, line, begidx, endidx, all_episodes, user_eps) return [] @cmd2.with_category(CAT_AUTOCOMPLETE) @cmd2.with_argparser(library_parser) def do_library(self, args): """Media management command demonstrates multiple layers of sub-commands being handled by AutoCompleter""" func = getattr(args, 'func', None) if func is not None: # Call whatever subcommand function was selected func(self, args) else: # No subcommand was provided, so call help self.do_help('library') def complete_library(self, text, line, begidx, endidx): # this demonstrates the much more complicated scenario of having # unique completion parameters per sub-command that use the same # argument name. To do this we build a multi-layer nested tree # of lookups far AutoCompleter to traverse. This nested tree must # match the structure of the argparse parser # movie_add_choices = {'movie_id': self._query_movie_database} movie_remove_choices = {'movie_id': self._query_movie_user_library} # This demonstrates the ability to mix custom completion functions with argparse completion. # By specifying a tuple for a completer, AutoCompleter expects a custom completion function # with optional index-based as well as keyword based arguments. This is an alternative to using # a partial function. show_add_choices = { 'show_id': ( self._filter_library, # This is a custom completion function # This tuple represents index-based args to append to the function call ( list(TabCompleteExample.SHOW_DATABASE.keys()), )), 'episode_id': ( self._filter_episodes, # this is a custom completion function # this list represents index-based args to append to the function call [TabCompleteExample.SHOW_DATABASE], # this dict contains keyword-based args to append to the function call { 'user_lib': TabCompleteExample.USER_SHOW_LIBRARY }) } show_remove_choices = {} # The library movie sub-parser group 'command' has 2 sub-parsers: # 'add' and 'remove' library_movie_command_params = \ {'add': (movie_add_choices, None), 'remove': (movie_remove_choices, None)} library_show_command_params = \ {'add': (show_add_choices, None), 'remove': (show_remove_choices, None)} # The 'library movie' command has a sub-parser group called 'command' library_movie_subcommand_groups = { 'command': library_movie_command_params } library_show_subcommand_groups = { 'command': library_show_command_params } # Mapping of a specific sub-parser of the 'type' group to a tuple. Each # tuple has 2 values corresponding what's passed to the constructor # parameters (arg_choices,subcmd_args_lookup) of the nested # instance of AutoCompleter library_type_params = { 'movie': (None, library_movie_subcommand_groups), 'show': (None, library_show_subcommand_groups) } # maps the a subcommand group to a dictionary mapping a specific # sub-command to a tuple of (arg_choices, subcmd_args_lookup) # # In this example, 'library_parser' has a sub-parser group called 'type' # under the type sub-parser group, there are 2 sub-parsers: 'movie', 'show' library_subcommand_groups = {'type': library_type_params} completer = argparse_completer.AutoCompleter( TabCompleteExample.library_parser, self, subcmd_args_lookup=library_subcommand_groups) tokens, _ = self.tokens_for_completion(line, begidx, endidx) results = completer.complete_command(tokens, text, line, begidx, endidx) return results
class MpTcpAnalyzerCmdApp(cmd2.Cmd): """ mptcpanalyzer can run into 3 modes: #. interactive mode (default): an interpreter with some basic completion will accept your commands. There is also some help embedded. #. if a filename is passed as argument, it will load commands from this file otherwise, it will consider the unknow arguments as one command, the same that could be used interactively """ intro = textwrap.dedent(""" Press ? to list the available commands and `help <command>` or `<command> -h` for a detailed help of the command """.format(__version__)) def stevedore_error_handler(manager, entrypoint, exception): print("Error while loading entrypoint [%s]" % entrypoint) def __init__(self, cfg: MpTcpAnalyzerConfig, stdin=sys.stdin, **kwargs) -> None: """ Args: cfg (MpTcpAnalyzerConfig): A valid configuration Attributes: prompt (str): Prompt seen by the user, displays currently loaded pcpa config: configution to get user parameters data: dataframe currently in use """ self.shortcuts.update({ 'lc': 'list_connections', 'ls': 'list_subflows', 'lr': 'list_reinjections' }) super().__init__(completekey='tab', stdin=stdin) self.prompt = FG_COLORS['blue'] + "Ready>" + color_off self.data = None # type: pd.DataFrame self.config = cfg self.tshark_config = TsharkConfig( delimiter=cfg["mptcpanalyzer"]["delimiter"], profile=cfg["mptcpanalyzer"]["wireshark_profile"], ) # cmd2 specific initialization self.abbrev = True # when no ambiguities, run the command self.allow_cli_args = True # disable autoload of transcripts self.allow_redirection = True # allow pipes in commands self.default_to_shell = False self.debug = True # for now self.set_posix_shlex = True # need cmd2 >= 0.8 # Load Plots ###################### # you can list available plots under the namespace # https://pypi.python.org/pypi/entry_point_inspector # https://docs.openstack.org/stevedore/latest/reference/index.html#stevedore.extension.ExtensionManager # mgr = driver.DriverManager( self.plot_mgr = extension.ExtensionManager( namespace='mptcpanalyzer.plots', invoke_on_load=True, verify_requirements=True, invoke_args=(self.tshark_config,), # invoke_kwds propagate_map_exceptions=True, on_load_failure_callback=self.stevedore_error_handler ) self.cmd_mgr = extension.ExtensionManager( namespace='mptcpanalyzer.cmds', invoke_on_load=True, verify_requirements=True, invoke_args=(), propagate_map_exceptions=False, on_load_failure_callback=self.stevedore_error_handler ) # do_plot parser ###################### # not my first choice but to accomodate cmd2 constraints # see https://github.com/python-cmd2/cmd2/issues/498 subparsers = MpTcpAnalyzerCmdApp.plot_parser.add_subparsers(dest="plot_type", title="Subparsers", help='sub-command help',) subparsers.required = True # type: ignore def register_plots(ext, subparsers): """Adds a parser per plot""" # check if dat is loaded parser = ext.obj.default_parser() assert parser, "Forgot to return parser" subparsers.add_parser(ext.name, parents=[parser], add_help=False) self.plot_mgr.map(register_plots, subparsers) # # will raise NoMatches when no plot available # if loading commands from a file, we disable prompt not to pollute output if stdin != sys.stdin: log.info("Disabling prompt because reading from stdin") self.use_rawinput = False self.prompt = "" self.intro = "" """ The optional arguments stdin and stdout specify the input and output file objects that the Cmd instance or subclass instance will use for input and output. If not specified, they will default to sys.stdin and sys.stdout. """ print("WARNING: mptcpanalyzer may require a custom wireshark. " "Check github for mptcp patches streaming.") @property def plot_manager(self): return self.plot_mgr @plot_manager.setter def plot_manager(self, mgr): """ Override the default plot manager, only used for testing :param mgr: a stevedore plugin manager """ self.plot_mgr = mgr def load_plugins(self, mgr=None): """ This function monkey patches the class to inject Command plugins Attrs: mgr: override the default plugin manager when set. Useful to run tests """ mgr = mgr if mgr is not None else self.cmd_mgr def _inject_cmd(ext, data): log.debug("Injecting plugin %s" % ext.name) for prefix in ["do", "help", "complete"]: method_name = prefix + "_" + ext.name try: obj = getattr(ext.obj, prefix) if obj: setattr(MpTcpAnalyzerCmdApp, method_name, obj) except AttributeError: log.debug("Plugin does not provide %s" % method_name) # there is also map_method available try: mgr.map(_inject_cmd, self) except stevedore.exception.NoMatches as e: log.error("stevedore: No matches (%s)" % e) def precmd(self, line): """ Here we can preprocess line, with for instance shlex.split() ? Note: This is only called when using cmdloop, not with onecmd ! """ # default behavior print(">>> %s" % line) return line def cmdloop(self, intro=None): """ overrides baseclass just to be able to catch exceptions """ try: super().cmdloop() except KeyboardInterrupt as e: pass # Exception raised by sys.exit(), which is called by argparse # we don't want the program to finish just when there is an input error except SystemExit as e: self.cmdloop() except mp.MpTcpException as e: print(e) self.cmdloop() except Exception as e: log.critical("Unknown error, aborting...") log.critical("%s" % e) print("Displaying backtrace:\n") traceback.print_exc() def postcmd(self, stop, line): """ Override baseclass returning true will stop the program """ log.debug("postcmd result for line [%s] => %r", line, stop) return True if stop is True else False parser = MpTcpAnalyzerParser(description="List subflows of an MPTCP connection") filter_stream = parser.add_argument("mptcpstream", action="store", type=int, help="Equivalent to wireshark mptcp.stream id") # TODO for tests only, fix setattr(filter_stream, argparse_completer.ACTION_ARG_CHOICES, [0, 1, 2]) @with_argparser(parser) @with_category(CAT_MPTCP) @is_loaded def do_list_subflows(self, args): """ list mptcp subflows [mptcp.stream id] Example: ls 0 """ self.list_subflows(args.mptcpstream) @is_loaded def list_subflows(self, mptcpstreamid: int): try: con = MpTcpConnection.build_from_dataframe(self.data, mptcpstreamid) self.poutput("mptcp.stream %d has %d subflow(s) (client/server): " % (mptcpstreamid, len(con.subflows()))) for sf in con.subflows(): self.poutput("\t%s" % sf) except mp.MpTcpException as e: self.perror(e) # def help_list_subflows(self): # print("Use parser -h") # def complete_list_subflows(self, text, line, begidx, endidx): # """ help to complete the args """ # # conversion to set removes duplicate keys # l = list(set(self.data["mptcpstream"])) # # convert items to str else it won't be used for completion # l = [str(x) for x in l] # return l # parser = gen_pcap_parser({"pcap": PreprocessingActions.FilterStream | PreprocessingActions.Merge }, protocol="tcp") parser = argparse_completer.ACArgumentParser( description=''' This function tries to map a tcp.stream id from one pcap to one in another pcap in another dataframe. ''' ) # TODO could use LoadSinglePcap load_pcap1 = parser.add_argument("pcap1", action="store", help="first to load") load_pcap2 = parser.add_argument("pcap2", action="store", help="second pcap") # cmd2.Cmd.path_complete ? # setattr(action_stream, argparse_completer.ACTION_ARG_CHOICES, range(0, 10)) # use path_filter setattr(load_pcap1, argparse_completer.ACTION_ARG_CHOICES, ('path_complete', )) setattr(load_pcap2, argparse_completer.ACTION_ARG_CHOICES, ('path_complete', )) parser.add_argument("tcpstreamid", action="store", type=int, help="tcp.stream id visible in wireshark for pcap1") parser.add_argument("--json", action="store_true", default=False, help="Machine readable summary.") parser.add_argument( '-v', '--verbose', dest="verbose", default=False, action="store_true", help="how to display each connection") parser.epilog = ''' Examples: map_tcp_connection examples/client_1_tcp_only.pcap examples/server_1_tcp_only.pcap 0 ''' @with_argparser(parser) @with_category(CAT_TCP) def do_map_tcp_connection(self, args): df1 = load_into_pandas(args.pcap1, self.tshark_config) df2 = load_into_pandas(args.pcap2, self.tshark_config) main_connection = TcpConnection.build_from_dataframe(df1, args.tcpstreamid) mappings = map_tcp_stream(df2, main_connection) self.poutput("Trying to map %s" % (main_connection,)) self.poutput("%d mapping(s) found" % len(mappings)) for match in mappings: # formatted_output = main.format_mapping(match) # output = "{c1.tcpstreamid} <-> {c2.tcpstreamid} with score={score}" # formatted_output = output.format( # c1=main_connection, # c2=match, # score=score # ) # print(formatted_output) self.poutput("%s" % str(match)) parser = MpTcpAnalyzerParser( description="This function tries to map a mptcp.stream from a dataframe" "(aka pcap) to mptcp.stream" "in another dataframe. " ) load_pcap1 = parser.add_argument("pcap1", action="store", type=str, help="first to load") load_pcap2 = parser.add_argument("pcap2", action="store", type=str, help="second pcap") setattr(load_pcap1, argparse_completer.ACTION_ARG_CHOICES, ('path_complete', )) setattr(load_pcap2, argparse_completer.ACTION_ARG_CHOICES, ('path_complete', )) parser.add_argument("mptcpstreamid", action="store", type=int, help="to filter") parser.add_argument("--trim", action="store", type=float, default=0, help="Remove mappings with a score below this threshold") parser.add_argument("--limit", action="store", type=int, default=2, help="Limit display to the --limit best mappings") parser.add_argument( '-v', '--verbose', dest="verbose", default=False, action="store_true", help="display all candidates") @with_argparser(parser) @with_category(CAT_MPTCP) @experimental def do_map_mptcp_connection(self, args): """ Tries to map mptcp.streams from different pcaps. Score based mechanism Todo: - Limit number of displayed matches """ df1 = load_into_pandas(args.pcap1, self.tshark_config) df2 = load_into_pandas(args.pcap2, self.tshark_config) main_connection = MpTcpConnection.build_from_dataframe(df1, args.mptcpstreamid) mappings = map_mptcp_connection(df2, main_connection) self.poutput("%d mapping(s) found" % len(mappings)) mappings.sort(key=lambda x: x.score, reverse=True) for rank, match in enumerate(mappings): if rank >= args.limit: self.pfeedback("ignoring mappings left") break winner_like = match.score == float('inf') output = "{c1.mptcpstreamid} <-> {c2.mptcpstreamid} with score={score} {extra}" formatted_output = output.format( c1=main_connection, c2=match.mapped, score=FG_COLORS['red'] + str(match.score) + color_off, extra= " <-- should be a correct match" if winner_like else "" ) if match.score < args.trim: continue # match = MpTcpMapping(match.mapped, match.score, mapped_subflows) def _print_subflow(x): return "\n-" + x[0].format_mapping(x[1]) formatted_output += ''.join( [ _print_subflow(x) for x in match.subflow_mappings]) self.poutput(formatted_output) # def parser_summary(): # """ """ # pass summary_parser = MpTcpAnalyzerParser(description="Prints a summary of the mptcp connection") action_stream = summary_parser.add_argument( "mptcpstream", type=MpTcpStreamId, action=mp.parser.retain_stream("pcap"), help="mptcp.stream id") # TODO update the stream id autcompletion dynamically ? # setattr(action_stream, argparse_completer.ACTION_ARG_CHOICES, range(0, 10)) summary_parser.add_argument( 'destination', # mp.DestinationChoice, action="store", choices=mp.DestinationChoice, type=lambda x: mp.ConnectionRoles[x], help='Filter flows according to their direction' '(towards the client or the server)' 'Depends on mptcpstream' ) summary_parser.add_argument("--json", action="store_true", default=False, help="Machine readable summary.") @with_argparser_test(summary_parser, preload_pcap=True) @is_loaded def do_summary(self, args, unknown): """ Naive summary contributions of the mptcp connection See summary_extended for more details """ df = self.data # myNs = Namespace() # myNs._dataframes = { "pcap": self.data } # args = parser.parse_args(args, myNs) mptcpstream = args.mptcpstream success, ret = stats.mptcp_compute_throughput( self.data, args.mptcpstream, args.destination ) if success is not True: self.perror("Throughput computation failed:") self.perror(ret) return if args.json: import json # TODO use self.poutput # or use a stream, it must just be testable val = json.dumps(ret, ensure_ascii=False) self.poutput(val) return mptcp_transferred = ret["mptcp_throughput_bytes"] self.poutput("mptcpstream %d transferred %d bytes." % (ret["mptcpstreamid"], mptcp_transferred)) for tcpstream, sf_bytes in map(lambda sf: (sf["tcpstreamid"], sf["throughput_bytes"]), ret["subflow_stats"]): subflow_load = sf_bytes/mptcp_transferred self.poutput("tcpstream {} transferred {sf_tput} bytes out of {mptcp_tput}, " "accounting for {tput_ratio:.2f}%".format( tcpstream, sf_tput=sf_bytes, mptcp_tput=mptcp_transferred, tput_ratio=subflow_load*100 )) parser = gen_pcap_parser({"pcap": PreprocessingActions.Preload}) parser.description = "Export connection(s) to CSV" parser.epilog = ''' ''' # faut qu'il prenne le pcap ici sinon je ne peux pas autofiltrer : parser.add_argument("output", action="store", help="Output filename") group = parser.add_mutually_exclusive_group(required=False) group.add_argument('--tcpstream', action=functools.partial(FilterStream, "pcap", False), type=TcpStreamId) group.add_argument('--mptcpstream', action=functools.partial(FilterStream, "pcap", True), type=MpTcpStreamId) # parser.add_argument("protocol", action="store", choices=["mptcp", "tcp"], help="tcp.stream id visible in wireshark") # TODO check ? parser.add_argument("--destination", action="store", choices=mp.DestinationChoice, help="tcp.stream id visible in wireshark") parser.add_argument("--drop-syn", action="store_true", default=False, help="Helper just for my very own specific usecase") @is_loaded @with_argparser(parser) def do_tocsv(self, args): """ Selects tcp/mptcp/udp connection and exports it to csv """ df = self.data # TODO let the parser do it # if args.tcpstream: # # df = df[ df.tcpstream == args.tcpstream] # self.poutput("Filtering tcpstream") # con = TcpConnection.build_from_dataframe(df, args.tcpstream) # if args.destination: # self.poutput("Filtering destination") # q = con.generate_direction_query(args.destination) # df = df.query(q) # elif args.mptcpstream: # self.poutput("Unsupported yet") # df = df[ df.mptcpstream == args.mptcpstream] # need to compute the destinations before dropping syn from the dataframe # df['tcpdest'] = np.nan; for streamid, subdf in df.groupby("tcpstream"): con = TcpConnection.build_from_dataframe(df, streamid) df = mpdata.tcpdest_from_connections(df, con) if args.drop_syn: # use subdf ? self.poutput("drop-syn Unsupported yet") df.drop(subdf.head(3).index, inplace=True) # drop 3 first packets of each connection ? # this should be a filter syns = df[df.tcpflags == mp.TcpFlags.SYN] # df = df[ df.flags ] # if args.destination: # if args.tcpstream: # TODO we should filter destination self.poutput("Writing to %s" % args.output) pandas_to_csv(df, args.output) parser = gen_bicap_parser("mptcp", True) parser.add_argument("--json", action="store_true", default=False, help="Machine readable summary.") parser.description = """ Look into more details of an mptcp connection """ parser.epilog = """ summary_extended examples/client_2_redundant.pcapng 0 examples/server_2_redundant.pcapng 0 """ @with_argparser(parser) def do_summary_extended(self, args): """ Summarize contributions of each subflow For now it is naive, does not look at retransmissions ? """ print("%r" % args) df_pcap1 = load_into_pandas(args.pcap1, self.tshark_config) destinations = args.destinations # or list(mp.ConnectionRoles) for destination in destinations: success, basic_stats = stats.mptcp_compute_throughput( # TODO here we should load the pcap before hand ! df_pcap1, args.pcap1stream, args.destinations ) if success is not True: self.perror("Error %s" % basic_stats) # TODO already be done # TODO we should have the parser do it df = load_merged_streams_into_pandas( args.pcap1, args.pcap2, args.pcap1stream, args.pcap2stream, True, self.tshark_config ) success, ret = stats.mptcp_compute_throughput_extended( df, stats=basic_stats, destination=destination ) if success is not True: self.perror("Throughput computation failed:") self.perror(ret) return if args.json: import json # TODO use self.poutput # or use a stream, it must just be testable val = json.dumps(ret, ensure_ascii=False) self.poutput(val) return # TODO display goodput/ratio total_transferred = ret["mptcp_throughput_bytes"] # (ret["mptcpstreamid"], ret["mptcp_bytes"])) msg = "mptcpstream {mptcpstreamid} throughput/goodput {mptcp_throughput_bytes}/{mptcp_goodput_bytes}" self.poutput(msg.format(**ret)) for sf in ret["subflow_stats"]: subflow_load = sf_bytes/ret["mptcp_bytes"] msg = """ tcpstream {tcpstreamid} analysis: - throughput: transferred {} out of {mptcp_throughput_bytes}, accounting for {.2f:throughput_contribution}% - goodput: transferred {mptcp_goodput} out of {mptcp_goodput_bytes}, accounting for {.2f:goodput_contribution}% """ self.poutput( msg.format( mptcp_tput=ret["mptcp_throughput_bytes"], **ret, **sf )) # @is_loaded @with_category(CAT_TCP) def do_list_tcp_connections(self, *args): """ List tcp connections via their ids (tcp.stream) """ streams = self.data.groupby("tcpstream") self.poutput('%d tcp connection(s)' % len(streams)) for tcpstream, group in streams: # self.list_subflows(mptcpstream) self.data.tcp.connection(tcpstream) con = TcpConnection.build_from_dataframe(self.data, tcpstream) self.poutput(con) self.poutput("\n") @is_loaded @with_category(CAT_MPTCP) def do_list_mptcp_connections(self, *args): """ List mptcp connections via their ids (mptcp.stream) """ streams = self.data.groupby("mptcpstream") self.poutput('%d mptcp connection(s)' % len(streams)) for mptcpstream, group in streams: self.list_subflows(mptcpstream) self.poutput("\n") # def generate_namespace(self) -> argparse.Namespace: # myNamespace = Namespace() # myNamespace.toto = self.data # parser = argparse_completer.ACArgumentParser( # description=""" # Mptcpanalyzer filters pcaps to keep only tcp packets. # This may explain why printed packet ids dont map # """ # ) load_pcap1 = parser.add_argument("imported_pcap", type=str, help="Capture file to cleanup.") setattr(load_pcap1, argparse_completer.ACTION_ARG_CHOICES, ('path_complete', )) parser.add_argument("exported_pcap", type=str, help="Cleaned up file") @with_argparser(parser) def do_clean_pcap(self, args): """ toto """ self.poutput("Exporting a clean version of {} in {}".format( args.imported_pcap, args.exported_pcap)) self.tshark_config.filter_pcap(args.imported_pcap, args.exported_pcap) # TODO it should be able to print for both parser = gen_bicap_parser("tcp", True) parser.description = """This function tries merges a tcp stream from 2 pcaps in an attempt to print owds. See map_tcp_connection first maybe.""" # TODO add a limit of packets or use ppaged() # parser.add_argument("protocol", action="store", choices=["mptcp", "tcp"], # help="tcp.stream id visible in wireshark") # give a choice "hash" / "stochastic" parser.add_argument( '-v', '--verbose', dest="verbose", default=False, action="store_true", help="how to display each connection" ) parser.add_argument("--csv", action="store", default=None, help="Machine readable summary.") parser.epilog = ''' You can run for example: map_tcp_connection examples/client_1_tcp_only.pcap examples/server_1_tcp_only.pcap 0 ''' @with_argparser(parser) @experimental def do_print_owds(self, args): """ TODO options to diagnose errors: - print unmapped packets - print abnormal OWDs (negative etc) """ self.poutput("Loading merged streams") df = args._dataframes["pcap"] result = df print(result.head(10)) # print("%r" % result) # print(result[mpdata.TCP_DEBUG_FIELDS].head(20)) # for key, subdf in df.groupby(_sender("tcpdest")) # todo sort by chronological order ? # for row in df.itertuples(); # self.ppaged() if args.csv: self.pfeedback("Exporting to csv") with open(args.csv, "w") as fd: df.to_csv( fd, sep="|", index=False, header=True, ) # print unmapped packets print("print_owds finished") # print("TODO display before doing plots") # TODO display errors print(result[["owd"]].head(20)) # print(result.columns) mpdata.print_weird_owds(result) # print(result[["owd"]].head(20)) def do_check_tshark(self, line): """ Check your tshark/wireshark version """ self.poutput("TODO implement automated check") self.poutput("you need a wireshark > 19 June 2018 with commit dac91db65e756a3198616da8cca11d66a5db6db7...") parser = gen_bicap_parser("mptcp", dest=True) parser.description = """ Qualify reinjections of the connection. You might want to run map_mptcp_connection first to find out what map to which """ parser.add_argument("--failed", action="store_true", default=False, help="List failed reinjections too.") parser.add_argument("--csv", action="store_true", default=False, help="Machine readable summary.") parser.add_argument("--debug", action="store_true", default=False, help="Explain decision for every reinjection.") @with_argparser_and_unknown_args(parser) @with_category(CAT_MPTCP) @experimental def do_qualify_reinjections(self, args, unknown): """ test with: mp qualify_reinjections 0 TODO move the code into a proper function """ # TODO this should be done automatically right ? df_all = load_merged_streams_into_pandas( args.pcap1, args.pcap2, args.pcap1stream, args.pcap2stream, mptcp=True, tshark_config=self.tshark_config ) # adds a redundant column df = classify_reinjections(df_all) # print(df_all[ pd.notnull(df_all[_sender("reinjection_of")])] [ # _sender(["reinjection_of", "reinjected_in", "packetid", "reltime"]) + # _receiver(["packetid", "reltime"]) # ]) # to help debug # df.to_excel("temp.xls") def _print_reinjection_comparison(original_packet, reinj, ): """ Expects tuples of original and reinjection packets """ # original_packet = sender_df.loc[ sender_df.packetid == initial_packetid, ].iloc[0] row = reinj reinjection_packetid = getattr(row, _sender("packetid")), reinjection_start = getattr(row, _sender("abstime")), reinjection_arrival = getattr(row, _receiver("abstime")), original_start = original_packet[_sender("abstime")], original_arrival = original_packet[_receiver("abstime")] if reinj.redundant == False: # print(original_packet["packetid"]) msg = ("packet {pktid} is a successful reinjection of {initial_packetid}." " It arrived at {reinjection_arrival} to compare with {original_arrival}" " while being transmitted at {reinjection_start} to compare with " "{original_start}, i.e., {reinj_delta} before") # TODO use assert instead if getattr(row, _receiver("abstime")) > original_packet[ _receiver("abstime") ]: print("BUG: this is not a valid reinjection after all ?") elif args.failed: # only de msg = "packet {pktid} is a failed reinjection of {initial_packetid}." else: return msg = msg.format( pktid = reinjection_packetid, initial_packetid = initial_packetid, reinjection_start = reinjection_start, reinjection_arrival = reinjection_arrival, original_start = original_start, original_arrival = original_arrival, reinj_delta = reinj.reinj_delta, ) self.poutput(msg) # with pd.option_context('display.max_rows', None, 'display.max_columns', 300): # print(reinjected_packets[["packetid", "packetid_receiver", *_receiver(["reinjected_in", "reinjection_of"])]].head()) # TODO filter depending on --failed and --destinations if args.csv: self.pfeedback("Exporting to csv") # keep redundant # only export a subset ? # for # df1 = df[['a','d']] # smalldf = df.drop() columns = _sender(["abstime", "reinjection_of", "reinjected_in", "packetid", "tcpstream", "mptcpstream", "tcpdest", "mptcpdest"]) columns += _receiver(["abstime", "packetid"]) columns += ["redundant", "owd", "reinj_delta"] df[columns].to_csv( self.stdout, sep="|", index=False, header=True, ) return for destination in ConnectionRoles: if args.destinations and destination not in args.destinations: log.debug("ignoring destination %s " % destination) continue self.poutput("looking for reinjections towards mptcp %s" % destination) sender_df = df[df.mptcpdest == destination] log.debug("%d reinjections in that direction" % (len(sender_df), )) # TODO we now need to display successful reinjections reinjections = sender_df[pd.notnull(sender_df[_sender("reinjection_of")])] successful_reinjections = reinjections[reinjections.redundant == False] self.poutput("%d successful reinjections" % len(successful_reinjections)) # print(successful_reinjections[ _sender(["packetid", "reinjection_of"]) + _receiver(["packetid"]) ]) for row in reinjections.itertuples(index=False): # loc ? this is an array, sort it and take the first one ? initial_packetid = row.reinjection_of[0] # print("initial_packetid = %r %s" % (initial_packetid, type(initial_packetid))) original_packet = df_all.loc[df_all.packetid == initial_packetid].iloc[0] # print("original packet = %r %s" % (original_packet, type(original_packet))) # if row.redundant == True and args.failed: # _print_failed_reinjection(original_packet, row, debug=args.debug) _print_reinjection_comparison(original_packet, row, ) parser = MpTcpAnalyzerParser( description="Listing reinjections of the connection" ) parser.add_argument("mptcpstream", type=MpTcpStreamId, help="mptcp.stream id") parser.add_argument("--summary", action="store_true", default=False, help="Just count reinjections") @is_loaded @with_category(CAT_MPTCP) @with_argparser_test(parser) def do_list_reinjections(self, args): """ List reinjections We want to be able to distinguish between good and bad reinjections (like good and bad RTOs). A good reinjection is a reinjection for which either: - the segment arrives first at the receiver - the cumulative DACK arrives at the sender sooner thanks to that reinjection To do that, we need to take into account latencies """ df = self.data df = self.data[df.mptcpstream == args.mptcpstream] if df.empty: self.poutput("No packet with mptcp.stream == %d" % args.mptcpstream) return # known : Set[int] = set() # print(df.columns) # TODO move to outer function ? # TODO use ppaged reinjections = df.dropna(axis=0, subset=["reinjection_of"] ) total_nb_reinjections = 0 output = "" for row in reinjections.itertuples(): # if row.packetid not in known: # ','.join(map(str,row.reinjection_of) output += ("packetid=%d (tcp.stream %d) is a reinjection of %d packet(s): " % (row.packetid, row.tcpstream, len(row.reinjection_of))) # print("reinjOf=", row.reinjection_of) # assuming packetid is the index for pktId in row.reinjection_of: # print("packetId %d" % pktId) # entry = self.data.iloc[ pktId - 1] entry = self.data.loc[ pktId ] # entry = df.loc[ df.packetid == pktId] # print("packetId %r" % entry) output += ("- packet %d (tcp.stream %d)" % (entry.packetid, entry.tcpstream)) # known.update([row.packetid] + row.reinjection) self.ppaged(output) # reinjections = df["reinjection_of"].dropna(axis=0, ) # print("number of reinjections of ") parser = MpTcpAnalyzerParser( description="Loads a pcap to analyze" ) parser.add_argument("input_file", action=LoadSinglePcap, help="Either a pcap or a csv file." "When a pcap is passed, mptcpanalyzer looks for a cached csv" "else it generates a " "csv from the pcap with the external tshark program.") @with_argparser(parser) def do_load_pcap(self, args): """ Load the file as the current one """ print(args) # args = shlex.split(args) # print(args) # parser = self.do_load_pcap.argparser # print(parser) # args = parser.parse_args(args) self.poutput("Loading %s" % args.input_file) self.data = args._dataframes["input_file"] self.prompt = "%s> " % os.path.basename(args.input_file) def do_list_available_plots(self, args): """ Print available plots. Mostly for debug, you should use 'plot'. """ plot_names = self.list_available_plots() print(plot_names) def list_available_plots(self): return self.plot_mgr.names() def pcap_loaded(self): return isinstance(self.data, pd.DataFrame) plot_parser = MpTcpAnalyzerParser(prog='plot', description='Generate plots') # TODO complete the help # plot throughput tcp examples/client_2_redundant.pcapng 0 examples/server_2_redundant.pcapng 0 3" "quit" plot_parser.epilog = ''' You can run for example: plot owd tcp examples/client_2_filtered.pcapng 0 examples/server_2_filtered.pcapng 0 --display ''' @with_argparser_and_unknown_args(plot_parser) def do_plot(self, args, unknown): """ global member used by others do_plot members * Loads required dataframes when necessary """ # Allocate plot object plotter = self.plot_mgr[args.plot_type].obj # TODO reparse with the definitive parser ? # 'converts' the namespace to for the syntax define a dict dargs = vars(args) print("%s" % dargs) dataframes = dargs.pop("_dataframes") # workaround argparse limitations to set as default both directions # TODO replace that with an action ? # destinations=dargs.get("destinations", list(mp.ConnectionRoles)) # dargs.update(destinations=destinations) # log.debug("Selecting destinations %s" % (destinations,)) # dataframes = plotter.preprocess(**dargs) print("%s" % args) # dataframes = args._dataframes.values() assert dataframes is not None, "Preprocess must return a list" # pass unknown_args too ? result = plotter.run(**dataframes, **dargs) # to save to file for instance plotter.postprocess(result, **dargs) @with_category(CAT_GENERAL) def do_clean_cache(self, line): """ mptcpanalyzer saves pcap to csv converted files in a cache folder, (most likely $XDG_CACHE_HOME/mptcpanalyzer). This commands clears the cache. """ cache = mp.get_cache() self.poutput("Cleaning cache [%s]" % cache.folder) cache.clean() def do_dump(self, args): """ Dumps content of the csv file, with columns selected by the user. Mostly used for debug """ parser = argparse.ArgumentParser(description="dumps csv content") parser.add_argument('columns', default=[ "ipsrc", "ipdst"], choices=self.data.columns, nargs="*") parser.add_argument('-n', default=10, action="store", help="Number of results to display") args = parser.parse_args(shlex.split(args)) print(self.data[args.columns]) def complete_dump(self, text, line, begidx, endidx): """ Should return a list of possibilities """ l = [x for x in self.data.columns if x.startswith(text)] return l # not needed in cmd2 def do_quit(self, *args): """ Quit/exit program """ print("Thanks for flying with mptcpanalyzer.") return True def do_EOF(self, line): """ Keep it to be able to exit with CTRL+D """ return True def preloop(self): """ Executed once when cmdloop is called """ histfile = self.config["mptcpanalyzer"]['history'] if readline and os.path.exists(histfile): log.debug("Loading history from %s" % histfile) readline.read_history_file(histfile) def postloop(self): histfile = self.config["mptcpanalyzer"]['history'] if readline: log.debug("Saving history to %s" % histfile) readline.set_history_length(histfile_size) readline.write_history_file(histfile)
class DeadSFSShell(cmd2.Cmd): """Main shell for deadSFS""" intro = "Welcome to deadSFS shell. Type help or ? to list commands" prompt = "deadSFS>" CAT_CONNECTION = "Connection" CAT_ENCRYPTED_FTP_COMMANDS = "Encrypted FTP commands" CAT_RAW_FTP_COMMANDS = "Raw (non-decrypted) FTP commands" def _instance_pwd_file_names(self, _) -> List[str]: decrypted_files, failed_files = self.enc_ftp.shared_nlst() return decrypted_files + list(map(lambda x: "WARNING: NOT DECRYPTED: {}".format(x), failed_files)) filename_parser = argparse_completer.ACArgumentParser() filename = filename_parser.add_argument( "filename", nargs="?", help="decrypted filename/path") setattr(filename, argparse_completer.ACTION_ARG_CHOICES, '_instance_pwd_file_names') def _instance_pwd_raw_file_names(self) -> List[str]: return self.enc_ftp.non_decrypted_ftp.nlst() raw_filename_parser = argparse_completer.ACArgumentParser() raw_filename = raw_filename_parser.add_argument( "raw_filename", nargs="?", help="raw (non-decrypted) filename/path") setattr(raw_filename, argparse_completer.ACTION_ARG_CHOICES, '_instance_pwd_raw_file_names') def __init__(self, enc_key: bytes, tls: bool = False, certfile=None, keyfile=None, cafile=None): """Initialize the deadSFS shell :param enc_key: The private key used for filename and file encryption :param tls: Enable using a FTP TLS connection The below parameters are only relevant if ``tls`` is equal to :obj:`True`: :param certfile: Path to the clients *.pem self-certificate :param keyfile: Path to the clients *.pem self-certificate key :param cafile: Path the *.pem certificate authority to validate the FTP server's connection """ self.allow_cli_args = False super().__init__() if tls: context = ssl.SSLContext(ssl.PROTOCOL_TLS) # use TLS_V1_2 only instead of TLS_V1_1, TLS_V1, SSLv2, or SSLv3 context.options |= ssl.OP_NO_TLSv1 context.options |= ssl.OP_NO_TLSv1_1 context.options |= ssl.OP_NO_SSLv2 context.options |= ssl.OP_NO_SSLv3 # use a custom self certificate,otherwise, use default certificates if certfile and keyfile: context.load_cert_chain(certfile=certfile, keyfile=keyfile) else: context.load_default_certs() # enable validating the FTP servers connection with a ca if cafile: context.load_verify_locations(cafile=cafile) self.enc_ftp = EncryptedFTPTLS(enc_key, context=context) else: self.enc_ftp = EncryptedFTP(enc_key) def do_quit(self, _): """Exit out of the deadSFS shell Close connections for the FTP server if they exist. """ self.enc_ftp.close() return super().do_quit(_) connect_argparser = argparse.ArgumentParser() connect_argparser.add_argument('host', type=str, help="hostname to connect to") connect_argparser.add_argument('port', type=int, help="port to connect to") @with_category(CAT_CONNECTION) @with_argparser(connect_argparser) def do_connect(self, args): """Connect and login into the remote FTP server""" print(self.enc_ftp.connect(args.host, args.port)) username = input("username: "******".") @ftp_connected @with_category(CAT_CONNECTION) def do_disconnect(self, _): """Disconnect from the remote FTP server""" print(self.enc_ftp.quit()) @staticmethod def _fix_filename_arg(filename_arg: str) -> str: if filename_arg is None or \ isinstance(filename_arg, str) and not filename_arg.strip(): return "" return filename_arg @ftp_connected @with_category(CAT_ENCRYPTED_FTP_COMMANDS) @with_argparser(filename_parser) def do_nlst(self, args): """List the contents of the current working directory of the remote filesystem""" filename = DeadSFSShell._fix_filename_arg(args.filename) print(self.enc_ftp.nlst(filename)) @ftp_connected @with_category(CAT_RAW_FTP_COMMANDS) @with_argparser(raw_filename_parser) def do_raw_nlst(self, args): """List the contents of the directory specified by its encrypted filename on the remote filesystem""" filename = DeadSFSShell._fix_filename_arg(args.raw_filename) print(self.enc_ftp.non_decrypted_ftp.nlst(filename)) @ftp_connected @with_category(CAT_ENCRYPTED_FTP_COMMANDS) @with_argparser(filename_parser) def do_mkd(self, args): """Make a sub-directory within the current working directory of the remote filesystem""" print(self.enc_ftp.mkd(args.filename)) @ftp_connected @with_category(CAT_ENCRYPTED_FTP_COMMANDS) @with_argparser(filename_parser) def do_rmd(self, args): """Remove a directory from the remote filesystem""" print(self.enc_ftp.rmd(args.filename)) @ftp_connected @with_category(CAT_RAW_FTP_COMMANDS) @with_argparser(raw_filename_parser) def do_raw_rmd(self, args): """Remove a directory specified by its encrypted filename from the remote filesystem""" print(self.enc_ftp.non_decrypted_ftp.rmd(args.raw_filename)) @ftp_connected @with_category(CAT_ENCRYPTED_FTP_COMMANDS) def do_pwd(self, _): """Print the pathname of the current directory on the server""" print(self.enc_ftp.pwd()) @ftp_connected @with_category(CAT_RAW_FTP_COMMANDS) def do_raw_pwd(self, _): """Print the non-decrypted pathname of the current directory on the server""" print(self.enc_ftp.non_decrypted_ftp.pwd()) @ftp_connected @with_category(CAT_ENCRYPTED_FTP_COMMANDS) @with_argparser(filename_parser) def do_cwd(self, args): """Change the current working directory of the remote filesystem""" filename = DeadSFSShell._fix_filename_arg(args.filename) print(self.enc_ftp.cwd(filename)) @ftp_connected @with_category(CAT_RAW_FTP_COMMANDS) @with_argparser(raw_filename_parser) def do_raw_cwd(self, args): """Change the current working directory of the remote filesystem to the one specified by its encrypted path""" filename = DeadSFSShell._fix_filename_arg(args.raw_filename) print(self.enc_ftp.non_decrypted_ftp.cwd(filename)) @ftp_connected @with_category(CAT_ENCRYPTED_FTP_COMMANDS) def do_wf(self, arg): """Encrypt and write a file into the remote filesystem""" with open(arg, "r") as f: print(self.enc_ftp.storefile(arg, f.read())) @ftp_connected @with_category(CAT_ENCRYPTED_FTP_COMMANDS) @with_argparser(filename_parser) def do_rf(self, args): """Decrypt, Read, and save in the current working directory a file from the remote filesystem""" content = self.enc_ftp.readfile(args.filename) print("obtained {}'s content:\n{}".format(args.filename, content)) with open(args.filename, "w") as f: f.write(content) @ftp_connected @with_category(CAT_RAW_FTP_COMMANDS) @with_argparser(raw_filename_parser) def do_raw_rf(self, args): """Read, and save in the current working directory a file from the remote filesystem""" cmd = "RETR {}".format(args.raw_filename) buf = BytesIO() def callback(data: bytes): buf.write(data) self.enc_ftp.non_decrypted_ftp.retrbinary(cmd, callback) buf.seek(0) content = buf.read().decode("utf-8") print("obtained {}'s content:\n{}".format(args.raw_filename, content)) with open(args.raw_filename, "w") as f: f.write(content) @ftp_connected @with_category(CAT_ENCRYPTED_FTP_COMMANDS) @with_argparser(filename_parser) def do_rmf(self, args): """Delete a file from the remote filesystem""" print(self.enc_ftp.delete(args.filename)) @ftp_connected @with_category(CAT_RAW_FTP_COMMANDS) @with_argparser(raw_filename_parser) def do_raw_rmf(self, args): """Delete a file specified by its encrypted filename from the remote filesystem without""" print(self.enc_ftp.non_decrypted_ftp.delete(args.raw_filename)) # we have to re-build this manually as we cannot copy argparsers # this is a direct limitation of python currently rename_filename_parser = argparse_completer.ACArgumentParser() rename_filename = rename_filename_parser.add_argument( "filename", nargs="?", help="decrypted filename to change") setattr(rename_filename, argparse_completer.ACTION_ARG_CHOICES, '_instance_pwd_file_names') rename_filename_parser.add_argument("new_filename", help="new decrypted filename") @ftp_connected @with_category(CAT_ENCRYPTED_FTP_COMMANDS) @with_argparser_and_unknown_args(rename_filename_parser) def do_rename(self, args): """Rename an encrypted file""" print(self.enc_ftp.rename(args.filename, args.new_filename)) @ftp_connected @with_category(CAT_ENCRYPTED_FTP_COMMANDS) @with_argparser(raw_filename_parser) def do_vald(self, args): """Validate that all files (including files within nested directories) within a specified directory are properly encrypted and not wrongly modified on the remote FTP server""" filename = DeadSFSShell._fix_filename_arg(args.raw_filename) self.enc_ftp.validate_dir(filename)
class CmdLineApp(Cmd): sdcard = '/storage/sdcard0/' maps = None def __init__(self, file_path): Cmd.__init__(self) self.prompt = 'saam > ' self.shortcuts.remove(('@', 'load')) self.shortcuts.append(('@', 'adb_cmd')) self.shortcuts.remove(('@@', '_relative_load')) self.shortcuts.append(('$', 'adb_shell_cmd')) self.shortcuts.append(('qc', 'quit_and_clean')) self.apk_path = file_path self.apk = APK(self.apk_path) self.apk_out = os.path.basename(file_path) + ".out" self.smali_files = None self.smali_dir = None self.adb = None self.smali_method_descs = [] self.java_files = [] vsmali_action = CmdLineApp.vsmali_parser.add_argument( 'sfile', help='smali file path') setattr(vsmali_action, argparse_completer.ACTION_ARG_CHOICES, ('delimiter_complete', { 'delimiter': '/', 'match_against': self.smali_method_descs })) vjava_action = CmdLineApp.vjava_parser.add_argument( 'jfile', help='java file path') setattr(vjava_action, argparse_completer.ACTION_ARG_CHOICES, ('delimiter_complete', { 'delimiter': '/', 'match_against': self.java_files })) def do_quit_and_clean(self, arg): import shutil shutil.rmtree(self.apk_out) self._should_quit = True return self._STOP_AND_EXIT # ------------------- Hardware And System --------------------- def do_devinfos(self, arg): ''' 显示设备硬件信息 ''' cmd = 'getprop ro.product.brand' print('Brand :', self.adb.run_shell_cmd(cmd)[:-1].decode()) cmd = 'getprop ro.product.model' print('Model :', self.adb.run_shell_cmd(cmd)[:-1].decode()) cmd = 'getprop ro.product.device' print('Device :', self.adb.run_shell_cmd(cmd)[:-1].decode()) cmd = 'getprop ro.product.cpu.abi' print('CPU :', self.adb.run_shell_cmd(cmd)[:-1].decode()) cmd = 'getprop persist.radio.imei' print('IMEI :', self.adb.run_shell_cmd(cmd).decode()) def do_osinfos(self): '''显示设备系统信息''' cmd = 'getprop ro.build.description' print('Build Desc :', self.adb.run_shell_cmd(cmd)[:-1].decode()) cmd = 'getprop ro.build.date' print('Build Data :', self.adb.run_shell_cmd(cmd)[:-1].decode()) cmd = 'getprop ro.build.version.release' print('Build Version :', self.adb.run_shell_cmd(cmd)[:-1].decode()) cmd = 'getprop ro.build.version.sdk' print('SDK Version :', self.adb.run_shell_cmd(cmd)[:-1].decode()) # ---------------------- Manifest ------------------------- @staticmethod def serialize_xml(org_xml): import xmlformatter import xml # org_xml = re.sub(r'>[^<]+<', '><', org_xml) try: formatter = xmlformatter.Formatter() return formatter.format_string(org_xml).decode('utf-8') except xml.parsers.expat.ExpatError: return org_xml def do_manifest(self, arg): '''显示清单信息''' # TODO 清单的显示形式更改 # 1. 分类显示,简单,默认 # 2. 原始XML的方式,有可能显示超过页面,需要使用more的方式。 org_xml = self.apk.get_org_manifest() if org_xml: print(self.serialize_xml(org_xml)) def get_package(self): return self.apk.get_manifest()['@package'] def get_main_activity(self): try: activities = self.apk.get_manifest()['application']['activity'] except KeyError: return None if not isinstance(activities, list): activities = [activities] for item in activities: content = json.dumps(item) if 'android.intent.action.MAIN' in content\ and 'android.intent.category.LAUNCHER' in content: return item['@android:name'] return None def do_receiver(self, arg): try: receivers = self.apk.get_manifest()['application']['receiver'] except KeyError: return None print(json.dumps(receivers, indent=4, sort_keys=True)) def do_activity(self, arg): try: receivers = self.apk.get_manifest()['application']['activity'] except KeyError: return None print(json.dumps(receivers, indent=4, sort_keys=True)) def do_service(self, arg): try: receivers = self.apk.get_manifest()['application']['service'] except KeyError: return None print(json.dumps(receivers, indent=4, sort_keys=True)) def show_risk_children(self, flag=False): result = '' pflag = True self.apk.get_files().sort(key=lambda k: (k.get('type'), k.get('name'))) for item in self.apk.get_files(): if flag: print(item.get('type'), item.get('name')) continue if item.get('type') not in ['dex', 'apk', 'elf']: continue if pflag: result = Color.red('\n===== Risk Files =====\n') pflag = False result += item.get('type') + ' ' + item.get('name') + '\n' return result children_parser = argparse.ArgumentParser() children_parser.add_argument('-a', '--all', action='store_true') @with_argparser(children_parser) def do_children(self, args): ''' 列出APK中的特殊文件 ''' self.apk.get_files().sort(key=lambda k: (k.get('type'), k.get('name'))) for item in self.apk.get_files(): if args.all: print(item.get('type'), item.get('name')) continue if item.get('type') in ['dex', 'apk', 'elf']: print(item.get('type'), item.get('name')) # ------------------- Static Analysis ------------------------- # TODO 默认使用了apktool反编译 # 转化为jar文件/默认解压为.class文件,cfr反编译工具会自行处理。 def do_decompile(self, arg): ''' 使用apktool反编译apk, 默认初始化清单相关的包。 ''' pkgs = self.find_pkg_refx_manifest(self.apk.get_org_manifest()) apktool.decode(None, self.apk_out, self.apk_path, True) for item in os.listdir(self.apk_out): if not item.startswith('smali'): continue self.smali_dir = SmaliDir(os.path.join(self.apk_out, item), include=pkgs) @staticmethod def find_pkg_refx_manifest(manifest): ''' 找出与清单相关的包 ''' if not manifest: return pkgs = set() ptn_s = r'android:name="([^\.]*?\.[^\.]*?)\..*?"' ptn = re.compile(ptn_s) for item in ptn.finditer(manifest): pkgs.add(item.groups()[0]) if "android.intent" in pkgs: pkgs.remove("android.intent") if "android.permission" in pkgs: pkgs.remove("android.permission") return pkgs init_smali_argparser = argparse.ArgumentParser() init_smali_argparser.add_argument('-m', '--manifest', action='store_true', help='根据manifest相关包初始化') init_smali_argparser.add_argument('-f', '--filter', action='store_true', help='根据过滤列表初始化') @with_argparser(init_smali_argparser) def do_init_smali(self, args): ''' 初始化smali,默认初始化所有 ''' pkgs = None if args.manifest: pkgs = self.find_pkg_refx_manifest(self.apk.get_org_manifest()) self.smali_dir = None for item in os.listdir(self.apk_out): if not item.startswith('smali'): continue sd = SmaliDir(os.path.join(self.apk_out, item), include=pkgs) if self.smali_dir: self.smali_dir[len(self.smali_dir):] = sd else: self.smali_dir = sd # print('初始化{0}个smali文件'.format(len(self.smali_dir))) for items in self.smali_dir: for mtd in items.get_methods(): self.smali_method_descs.append(mtd.get_desc()) print('初始化{0}个smali文件'.format(len(self.smali_dir))) def do_build(self, arg): ''' 使用apktool回编译apk ''' apktool.build(self.apk_out, force=True) def __init__smali_dir(self): pass def show_risk_perm(self): result = '' if not self.apk.get_manifest(): return result risk_perms = ['_SMS', '_CALL', '_DEVICE_ADMIN', '_AUDIO', '_CONTACTS'] ps = set() pflag = True def process_perm_item(item, pflagx=pflag): for perm in risk_perms: if perm not in item.get('@android:name'): continue if pflagx: result += Color.red('===== Risk Permissions =====\n') pflagx = False name = item.get('@android:name') if name in ps: continue result += name + '\n' ps.add(name) perms = self.apk.get_manifest().get('uses-permission', []) if isinstance(perms, dict): process_perm_item(perms) else: for item in self.apk.get_manifest().get('uses-permission', []): process_perm_item(item) app = self.apk.get_manifest().get('application') if app is None: return result recs = app.get('receiver', None) def process_item(item): text = '' perm = item.get('@android:permission', '') if '_DEVICE' not in perm: return text if pflag: text += Color.red('===== Risk Permissions =====\n') text += perm + item.get('@android:name') + '\n' if isinstance(recs, dict): text = process_item(item) if text: result += text pflag = False elif isinstance(recs, list): for item in recs: text = process_item(item) if text: result += text pflag = False break if not pflag: result += '\n' return result def show_risk_code(self, level): result = Color.red('===== Risk Codes =====\n') for k, v in RISKS.items(): if v['lvl'] < level: continue kflag = True for sf in self.smali_dir: for mtd in sf.get_methods(): mflag = True lines = re.split(r'\n\s*', mtd.get_body()) for idx, line in enumerate(lines): for ptn in v['code']: if re.search(ptn, line): if kflag: result += Color.magenta('---' + k + '---\n') kflag = False if mflag: result += Color.green(' ' + str(mtd)) + '\n' mflag = False result += ' ' * 3 + str(idx) + line + '\n' break return result risk_parser = argparse.ArgumentParser() risk_parser.add_argument('-l', '--level', help='指定风险级别,0/1/2/3,0为最低级别,3为最高级别') risk_parser.add_argument('-f', '--force', action='store_true', help='强制覆盖已有文件') @with_argparser(risk_parser) def do_risk(self, args): if os.path.exists(self.apk_path + '.risk.txt'): if not args.force: # TODO read file return text = '' text += self.show_risk_perm() level = 3 if args.level: level = int(args.level) text += self.show_risk_code(level) text += self.show_risk_children() # TODO save to file self.ppaged(text) # so # 文本 # 二进制文件 # 图片 def ref(self, desc): global notes global classes global recursion_times recursion_times += 1 if recursion_times > recursion_limit: return body = None for smali_file in self.smali_dir: for mtd in smali_file.get_methods(): if desc in str(mtd): body = mtd.get_body() break print(desc) if body: for smali_file in self.smali_dir: for mtd in smali_file.get_methods(): if desc in mtd.get_body(): if desc.startswith('L'): tmp = desc[:desc.index(';')] classes.add(tmp[:tmp.rindex('/')]) tmp = str(mtd)[:str(mtd).index(';')] classes.add(tmp[:tmp.rindex('/')]) notes.append([desc, str(mtd)]) self.ref(str(mtd)) def refs(self, desc): self.ref(desc) def do_xrefs(self, arg): pass ref_parser = argparse.ArgumentParser() ref_parser.add_argument('-l', '--limit', help='递归次数') @with_argparser(ref_parser) def do_ref(self, args): if len(args) != 1: return if args.limit: recursion_limit = args.l mtd = args[0] self.refs(mtd) dot = Digraph() if notes: dot.edges(notes) dot.render('refs.gv', view=True) def main_ref(self, arg): ''' 生成入口函数调用数,调用层数默认100。 可以传入一个数值修改递归次数。 ''' args = arg.split() if len(args) == 1: times = int(args[0]) global recursion_limit recursion_limit = times else: recursion_limit = 100 dot = Digraph() dot.attr('node', style='filled', fillcolor='red') # global recursion_times # recursion_times = 0 # if not self.smali_files: # self.smali_files = parse_smali(self.apk_out + os.sep + 'smali') main_acitivity = self.get_main_activity() main_acitivity = main_acitivity.replace('.', '/') + ';->onCreate' self.refs(main_acitivity) dot.node(main_acitivity) # recs = self.apk.get_receivers() # for item in recs: # dot.node(item) # self.refs(item) # dot.attr('node', shape='box', style='filled', # fillcolor='white', color='black') # if notes: # dot.edges(notes) # dot.render('refs.gv', view=True) # for item in sorted(list(classes)): # print(item) search_parser = argparse.ArgumentParser() search_parser.add_argument('txt', help='默认在Dex中搜索字符串') # <public type="string" name="failed_text" id="0x7f050002" /> search_parser.add_argument('-r', '--res', action='store_true', help='查找资源(id/name/图片名)') search_parser.add_argument('-a', '--all', action='store_true', help='在所有文本文件中查找(不包含Dex)') @with_argparser(search_parser) def do_search(self, args): def find_in_the_layout_xml(txt): results = [] layout_path = os.path.join(self.apk_out, 'res', 'layout') for root, _, names in os.walk(layout_path): for name in names: xml_path = os.path.join(root, name) with open(xml_path, mode='r') as f: lines = f.readlines() for line in lines: if txt in line: results.append(os.path.splitext(name)[0]) break return results def find_in_the_smali_dir(txt): for smali_file in self.smali_dir: if txt in smali_file.get_content(): print(smali_file) def find_in_the_txt(content): self.apk.get_files().sort( key=lambda k: (k.get('type'), k.get('name'))) for item in self.apk.get_files(): if item.get('type') != 'txt': continue if 'META-INF' in item.get('name'): continue txt_path = os.path.join(self.apk_out, item.get('name')) with open(txt_path, mode='r') as f: if 'txt_path' in f.read(): print(item.get('name')) if args.res: public_xml = os.path.join(self.apk_out, 'res', 'values', 'public.xml') rtype = None rname = None with open(public_xml, mode='r') as f: lines = f.readlines() flag = False for line in lines: if args.txt in line: match = line.strip() print(match) g = re.search(r'type="(.*?)" name="(.*?)"', match).groups() if g: rtype = g[0] rname = g[1] break else: return if rtype in {'string', 'layout'}: find_in_the_smali_dir(rname) elif rtype in {'id', 'drawable'}: txts = find_in_the_layout_xml(rname) for txt in txts: print(os.path.join('res', 'layout', txt + '.xml')) find_in_the_smali_dir(txt) print() elif args.all: find_in_the_txt(args.txt) else: find_in_the_smali_dir(args.txt) vjava_parser = argparse_completer.ACArgumentParser(prog='vjava') def do_dex2java(self, args): """将APK转化为Java代码,使用vjava可以查看。 """ from .decompiler.enjarify import dex2jar output = os.path.join(self.apk_out, 'classes.jar') dex2jar(self.apk_path, output=output) from .decompiler.cfr import class2java java_output = os.path.join(self.apk_out, 'java') print(java_output) class2java(output, java_output) def do_init_java(self, args): java_output = os.path.join(self.apk_out, 'java') for root, dirs, files in os.walk(java_output): for d in dirs: for f in files: self.java_files.append(os.path.join(root, d, f)) vjava_parser = argparse_completer.ACArgumentParser(prog='vjava') @cmd2.with_argparser(vjava_parser) def do_vjava(self, args): """查看java代码,可以指定类、方法 Args: args (TYPE): 类、方法 """ # TODO 封装cfr # TODO 修改apktutils的反编译接口 if not self.java_files: print('please dex2jar and init java') return try: with open(args.jfile) as f: print(f.read()) except Exception as e: pass vsmali_parser = argparse_completer.ACArgumentParser(prog='vsmali') # @cmd2.with_category('CAT_AUTOCOMPLETE') @cmd2.with_argparser(vsmali_parser) def do_vsmali(self, args): """查看smali代码,可以指定类、方法 Args: args (TYPE): 类、方法(自动生成) """ # TODO 判断smali目录是否为空 # 如果为空,直接提示,请反编译 if not self.smali_dir: print('please decompile and init smali') return try: print(self.smali_dir.get_method_from_desc(args.sfile).get_body()) except Exception as e: pass # for item in self.smali_dir: # print(dir(item)) # print(item.get_file_path(), item.get_class(), item.get_methods()) # for item in self.smali_files: # print(item) # # ------------------- ADB ------------------------- adb_parser = argparse.ArgumentParser() adb_parser.add_argument( '-s', '--serial', help='use device with given serial (overrides $ANDROID_SERIAL)') @with_argparser(adb_parser) def do_adb_ready(self, args): ''' 连接设备/模拟器,准备adb命令。 ''' serial = None if args.serial: serial = args.serial self.adb = pyadb3.ADB(device=serial) if len(self.adb.get_output().decode()) > 10: print('ADB ready.') else: print("unable to connect to device.") def do_adb(self, arg): ''' 执行adb命令 ''' if not self.adb: self.adb = pyadb3.ADB() self.adb.run_cmd(arg) print(self.adb.get_output().decode('utf-8', errors='ignore')) def do_adb_shell(self, arg): ''' 执行adb shell命令 ''' if not self.adb: self.adb = pyadb3.ADB() self.adb.run_shell_cmd(arg) print(self.adb.get_output().decode('utf-8', errors='ignore')) def do_topactivity(self, args): if not self.adb: self.adb = pyadb3.ADB() self.adb.run_shell_cmd( "dumpsys activity activities | grep mFocusedActivity") print(self.adb.get_output().decode('utf-8', errors='ignore').split()[-2]) def do_details(self, args): if not self.adb: self.adb = pyadb3.ADB() self.adb.run_shell_cmd("dumpsys package {}".format(self.get_package())) print(self.adb.get_output().decode('utf-8', errors='ignore')) # ------------------- 备份还原 ------------------------- # ------------------- 应用管理 ------------------------- lspkgs_parser = argparse.ArgumentParser() lspkgs_parser.add_argument('-f', '--file', action='store_true', help='显示应用关联的 apk 文件') lspkgs_parser.add_argument('-d', '--disabled', action='store_true', help='只显示 disabled 的应用') lspkgs_parser.add_argument('-e', '--enabled', action='store_true', help='只显示 enabled 的应用') lspkgs_parser.add_argument('-s', '--system', action='store_true', help='只显示系统应用') lspkgs_parser.add_argument('-3', '--three', action='store_true', help='只显示第三方应用') lspkgs_parser.add_argument('-i', '--installer', action='store_true', help='显示应用的 installer') lspkgs_parser.add_argument('-u', '--uninstall', action='store_true', help='包含已卸载应用') lspkgs_parser.add_argument('filter', type=str, nargs="?", help='包名包含 < FILTER > 字符串') @with_argparser(lspkgs_parser) def do_lspkgs(self, args): ''' 查看应用列表,默认所有应用 ''' cmd = 'pm list packages' if args.file: cmd += ' -f' if args.disabled: cmd += ' -d' elif args.enabled: cmd += ' -e' if args.system: cmd += ' -s' elif args.three: cmd += ' -3' if args.installer: cmd += ' -i' elif args.uninstall: cmd += ' -u' if args.filter: cmd += ' ' + args.filter self.adb.run_shell_cmd(cmd) print(self.adb.get_output().decode()) def do_install(self, arg): ''' 安装应用到手机或模拟器 ''' self.adb.run_cmd('install -r -f {}'.format(self.apk_path)) output = self.adb.get_output().decode() if self.adb.get_error(): print(self.adb.get_error().decode(errors='ignore')) elif 'Failure' in output: print(output) else: # TODO if the sdcard path doesn't exist. cmd = 'touch %s.now' % self.sdcard self.adb.run_shell_cmd(cmd) def do_uninstall(self, arg): ''' 卸载应用 ''' self.adb.run_cmd('uninstall %s' % self.get_package()) self.clearsd() def clearsd(self): ''' pull the newer files from sdcard. ''' cmd = 'find %s -path "%slost+found" -prune -o -type d -print -newer %s.now -delete' % ( self.sdcard, self.sdcard, self.sdcard) self.adb.run_shell_cmd(cmd) startapp_parser = argparse.ArgumentParser() startapp_parser.add_argument('-d', '--debug', action='store_true') def do_strace(self, args): ''' 使用 strace 跟踪应用 setenforce 0 # In Android 4.3 and later, if SELinux is enabled, strace will fail with "strace: wait: Permission denied" 有两种方式: 1. 使用调试的方式启动应用,strace -p $pid 2. trace -p $zygote_pid,启动应用 ''' # cmd = "strace -f -p `ps | grep zygote | awk '{print $2}'`" print(self.sdcard) cmd = "set `ps | grep zygote`; strace -p $2 -f -tt -T -s 500 -o {}strace.txt".format( self.sdcard) self.adb.run_shell_cmd(cmd) @with_argparser(startapp_parser) def do_startapp(self, args): '''启动应用''' main_acitivity = self.get_main_activity() if not main_acitivity: print("It does not have main activity.") return cmd = 'am start -n %s/%s' % (self.get_package(), main_acitivity) if args.debug: cmd = 'am start -D -n %s/%s' % (self.get_package(), main_acitivity) self.adb.run_shell_cmd(cmd) def do_stopapp(self, arg): '''停止应用''' cmd = 'am force-stop %s' % self.get_package() self.adb.run_shell_cmd(cmd) kill_parser = argparse.ArgumentParser() kill_parser.add_argument('-a', '--all', action='store_true') @with_argparser(kill_parser) def do_kill(self, args): '''杀死应用''' cmd = 'am kill %s' % self.get_package() if args.all: cmd = 'am kill-all' self.adb.run_shell_cmd(cmd) def do_clear(self, args): cmd = 'pm clear {}'.format(self.get_package()) self.adb.run_shell_cmd(cmd) def do_screencap(self, args): import time cmd = 'screencap -p /sdcard/{}.png'.format(time.time()) if not self.adb: self.adb = pyadb3.ADB() self.adb.run_shell_cmd(cmd) monkey_parser = argparse.ArgumentParser() monkey_parser.add_argument('-v', '--verbose', action='store_true') monkey_parser.add_argument('count', type=int, help="Count") @with_argparser(monkey_parser) def do_monkey(self, args): if not self.adb: self.adb = pyadb3.ADB() cmd = "monkey -p {} ".format(self.get_package()) if args.verbose: cmd += '-v ' cmd += str(args.count) self.adb.run_shell_cmd(cmd) print(self.adb.get_output().decode()) # -------------------------------------------------------- def do_set_sdcard(self, arg): ''' 设置sdcard位置 ''' if len(arg.split()) != 1: print('Please give one argument to set sdcard path.') return self.sdcard = arg # @options([make_option('-d', '--debug', action='store_true'), # make_option('-e', '--edit', action='store_true')]) def do_test(self, args): ''' 测试应用(未支持) ''' print(args) print(''.join(args)) if args.debug: print('debug') # TODO 增加自动化测试 # 获取Receiver, 启动 # 获取Service,启动 # 获取Acitivity,启动 def do_pids(self, arg): ''' 显示应用进程 ''' print(self.adb.run_shell_cmd('ps | grep %s' % self.get_package())) def lsof(self): axml = self.apk.get_manifest() if axml: lines = self.adb.run_shell_cmd('ps | grep %s' % axml.getPackageName()).decode() if not lines: return pids = [] for line in lines.strip().split('\r\r\n'): pids.append(line.split()[1]) for pid in pids: self.adb.run_shell_cmd('lsof | grep %s' % pid) lines = self.adb.get_output().decode().split('\r\r\n').decode() for line in lines: # print(line) if not line.endswith('(deleted)'): continue tmps = line.split() fdno = tmps[3] if not fdno.isdecimal(): continue print(line) filename = tmps[8].replace('/', '_') print('%s %s' % (fdno, filename)) self.adb.run_shell_cmd( "cat /proc/%s/fd/%s > /storage/sdcard0/%s" % (pid, fdno, filename)) self.adb.run_cmd('pull -a -p /storage/sdcard0/%s' % filename) def do_lssd(self, arg): '''列出SDCARD新增的文件''' command = ('find /storage/sdcard0 -path "/storage/sdcard0/lost+found"' ' -prune -o -type f -print -newer /storage/sdcard0/.now') self.adb.run_shell_cmd(command) print(self.adb.get_output().decode()) def pulldata(self): ''' pull /data/data/pkg ''' pkg = self.get_package() self.adb.run_shell_cmd('cp -r /data/data/%s /storage/sdcard0' % pkg) # self.adb.run_cmd('pull -a -p /storage/sdcard0/%s %s' % (pkg, pkg)) # self.adb.run_shell_cmd('rm -r /storage/sdcard0/%s' % pkg) def pullsd(self): ''' pull the newer files from sdcard. ''' command = ('find /storage/sdcard0 -path "/storage/sdcard0/lost+found"' ' -prune -o -type f -print -newer /storage/sdcard0/.now') ret = self.adb.run_shell_cmd(command).decode() dir_set = set([]) import os for line in ret.split('\r\r\n'): if line == '/storage/sdcard0/.now': continue if line: print('->', line) path = os.path.dirname(line) flag = 0 skip_path = None for item in dir_set: if item == path: flag == 2 break if item in path: flag = 2 break if path in item: flag = 1 skip_path = item break if flag == 1: print(path, skip_path) dir_set.add(path) dir_set.remove(skip_path) elif flag == 0: dir_set.add(path) for line in dir_set: print(line, ) local_path = os.path.dirname(line)[1:] if not os.path.exists(local_path): os.makedirs(local_path) self.adb.run_cmd('pull -a %s %s' % (line, local_path)) def do_pull(self, arg): '''导出样本的所有的运行生成的文件''' self.pulldata() self.pullsd() # ----------------------------- 内存操作 ----------------------------- # 内存字符串查看?内存字符串修改? def do_memview(self, arg): '''查看内存分布''' if not self.maps: self.get_maps() print(self.maps) def get_maps(self): pkg = self.get_package() lines = self.adb.run_shell_cmd('ps | grep %s' % pkg).decode() pids = [] for line in lines.strip().split('\r\r\n'): pids.append(line.split()[1]) for pid in pids: lines = self.adb.run_shell_cmd('ls /proc/%s/task/' % pid) clone = lines.decode().split()[-1] cmd = 'cat /proc/%s/maps' % clone self.adb.run_shell_cmd(cmd).decode() self.maps = self.adb.get_output().decode() def memdump(self, arg): ''' 内存Dump(仍未支持,需要gdb) 用法: memdump 内存起始地址 内存结束地址 ''' pass