1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3"""
4This is the core script for running plugins.
5
6It works by grabbing individual packets from a file or interface and feeding
7them into a chain of plugins (plugin_chain). Each plugin in the chain
8decides if the packet will continue on to the next plugin or just fade away.
9
10In practice, users generally only use one plugin, so the "chain" will only
11have one plugin, which is perfectly fine. The chain exists to allow plugins
12to alter or filter packets before passing them to more general plugins. For
13example, --plugin=country+netflow would pass packets through the country
14plugin, and then the netflow plugin. This would allow filtering traffic by
15country code before viewing flow data.
16
17Many things go into making this chain run smoothly, however. This includes
18reading in user arguments, setting filters, opening files/interfaces, etc. All
19of this largely takes place in the main() function.
20"""
21
22# standard Python library imports
23import bz2
24import faulthandler
25import gzip
26import multiprocessing
27import logging
28import operator
29import os
30import queue
31import sys
32import tempfile
33import zipfile
34from collections import OrderedDict
35from getpass import getpass
36from glob import glob
37from importlib import import_module
38from typing import Iterable
39
40import pcapy
41from pypacker.layer12 import ethernet, ppp, pppoe, ieee80211, linuxcc, radiotap, can
42from pypacker.layer3 import ip, ip6
43
44import dshell.core
45from dshell.api import get_plugin_information
46from dshell.core import Packet
47from dshell.dshelllist import get_plugins, get_output_modules
48from dshell.dshellargparse import DshellArgumentParser
49from dshell.output.output import QueueOutputWrapper
50from dshell.util import get_output_path
51from tabulate import tabulate
52
53logger = logging.getLogger(__name__)
54
55
56# plugin_chain will eventually hold the user-selected plugins that packets
57# will trickle through.
58plugin_chain = []
59
60
61def feed_plugin_chain(plugin_index: int, packet: Packet):
62    """
63    Every packet fed into Dshell goes through this function.
64    Its goal is to pass each packet down the chain of selected plugins.
65    Each plugin decides whether the packet(s) will proceed to the next
66    plugin, i.e. act as a filter.
67    """
68    if plugin_index >= len(plugin_chain):
69        # We are at the end of the chain.
70        return
71
72    current_plugin = plugin_chain[plugin_index]
73
74    # Pass packet into plugin for processing.
75    current_plugin.consume_packet(packet)
76
77    # Process produced packets.
78    for _packet in current_plugin.produce_packets():
79        feed_plugin_chain(plugin_index + 1, _packet)
80
81
82def clean_plugin_chain(plugin_index):
83    """
84    This is called at the end of packet capture.
85    It will go through the plugins and attempt to cleanup any connections
86    that were not yet closed.
87    """
88    if plugin_index >= len(plugin_chain):
89        # We are at the end of the chain
90        return
91
92    current_plugin = plugin_chain[plugin_index]
93
94    # need to flush even if there are no more plugins in the chain to ensure all packets are processed.
95    current_plugin.flush()
96
97    # Feed plugin chain with lingering packets released by flush.
98    for _packet in current_plugin.produce_packets():
99        feed_plugin_chain(plugin_index + 1, _packet)
100
101    clean_plugin_chain(plugin_index + 1)
102
103
104def decompress_file(filepath, extension, unzipdir):
105    """
106    Attempts to decompress a provided file and write the data to a temporary
107    file. The list of created temporary files is returned.
108    """
109    filename = os.path.split(filepath)[-1]
110    openfiles = []
111    logger.debug("Attempting to decompress {!r}".format(filepath))
112    if extension == '.gz':
113        f = gzip.open(filepath, 'rb')
114        openfiles.append(f)
115    elif extension == '.bz2':
116        f = bz2.open(filepath, 'rb')
117        openfiles.append(f)
118    elif extension == '.zip':
119        pswd = getpass("Enter password for .zip file {!r} [default: none]: ".format(filepath))
120        pswd = pswd.encode() # TODO I'm not sure encoding to utf-8 will work in all cases
121        try:
122            z = zipfile.ZipFile(filepath)
123            for z2 in z.namelist():
124                f = z.open(z2, 'r', pswd)
125                openfiles.append(f)
126        except (RuntimeError, zipfile.BadZipFile) as e:
127            logger.error("Could not process .zip file {!r}. {!s}".format(filepath, e))
128            return []
129
130    tempfiles = []
131    for openfile in openfiles:
132        with openfile:
133            try:
134                # check if this file is actually something decompressable
135                openfile.peek(1)
136            except OSError as e:
137                logger.error("Could not process compressed file {!r}. {!s}".format(filepath, e))
138                continue
139            with tempfile.NamedTemporaryFile(dir=unzipdir, delete=False, prefix=filename) as tfile:
140                for piece in openfile:
141                    tfile.write(piece)
142                tempfiles.append(tfile.name)
143    return tempfiles
144
145
146def print_plugins(plugins):
147    """
148    Print list of plugins with additional info.
149    """
150    headers = ['module', 'name', 'title', 'type', 'author', 'description']
151    rows = []
152    for name, module in sorted(plugins.items()):
153        rows.append([
154            module.__module__,
155            name,
156            module.name,
157            module.__class__.__bases__[0].__name__,
158            module.author,
159            module.description,
160        ])
161
162    print(tabulate(rows, headers=headers))
163
164
165def main(plugin_args=None, **kwargs):
166    global plugin_chain
167
168    if not plugin_args:
169        plugin_args = {}
170
171    # dictionary of all available plugins: {name: module path}
172    plugin_map = get_plugins()
173
174    # Attempt to catch segfaults caused when certain linktypes (e.g. 204) are
175    # given to pcapy
176    faulthandler.enable()
177
178    if not plugin_chain:
179        logger.error("No plugin selected")
180        sys.exit(1)
181
182    plugin_chain[0].defrag_ip = kwargs.get("defrag", False)
183
184    # Setup logging
185    log_format = "%(levelname)s (%(name)s) - %(message)s"
186    if kwargs.get("verbose", False):
187        log_level = logging.INFO
188    elif kwargs.get("debug", False):
189        log_level = logging.DEBUG
190    elif kwargs.get("quiet", False):
191        log_level = logging.CRITICAL
192    else:
193        log_level = logging.WARNING
194    logging.basicConfig(format=log_format, level=log_level)
195
196    # since pypacker handles its own exceptions (loudly), this attempts to keep
197    # it quiet
198    logging.getLogger("pypacker").setLevel(logging.CRITICAL)
199
200    if kwargs.get("allcc", False):
201        # Activate all country code (allcc) mode to display all 3 GeoIP2 country
202        # codes
203        dshell.core.geoip.acc = True
204
205    dshell.core.geoip.check_file_dates()
206
207    # If alternate output module is selected, tell each plugin to use that
208    # instead
209    if kwargs.get("omodule", None):
210        try:
211            # TODO: Create a factory classmethod in the base Output class (e.g. "from_name()") instead.
212            omodule = import_module("dshell.output."+kwargs["omodule"])
213            omodule = omodule.obj
214            for plugin in plugin_chain:
215                # TODO: Should we have a single instance of the Output module used by all plugins?
216                oomodule = omodule()
217                plugin.out = oomodule
218        except ImportError as e:
219            logger.error("Could not import module named '{}'. Use --list-output flag to see available modules".format(kwargs["omodule"]))
220            sys.exit(1)
221
222    # Check if any user-defined output arguments are provided
223    if kwargs.get("oargs", None):
224        oargs = {}
225        for oarg in kwargs["oargs"]:
226            if '=' in oarg:
227                key, val = oarg.split('=', 1)
228                oargs[key] = val
229            else:
230                oargs[oarg] = True
231        logger.debug("oargs: %s" % oargs)
232        for plugin in plugin_chain:
233            plugin.out.set_oargs(**oargs)
234
235    # If writing to a file, set for each output module here
236    if kwargs.get("outfile", None):
237        for plugin in plugin_chain:
238            plugin.out.reset_fh(filename=kwargs["outfile"])
239
240    # Set nobuffer mode if that's what the user wants
241    if kwargs.get("nobuffer", False):
242        for plugin in plugin_chain:
243            plugin.out.nobuffer = True
244
245    # Set the extra flag for all output modules
246    if kwargs.get("extra", False):
247        for plugin in plugin_chain:
248            plugin.out.extra = True
249            plugin.out.set_format(plugin.out.format)
250
251    # Set the BPF filters
252    # Each plugin has its own default BPF that will be extended or replaced
253    # based on --no-vlan, --ebpf, or --bpf arguments.
254    for plugin in plugin_chain:
255        if kwargs.get("bpf", None):
256            plugin.bpf = kwargs.get("bpf", "")
257            continue
258        if plugin.bpf:
259            if kwargs.get("ebpf", None):
260                plugin.bpf = "({}) and ({})".format(plugin.bpf, kwargs.get("ebpf", ""))
261        else:
262            if kwargs.get("ebpf", None):
263                plugin.bpf = kwargs.get("ebpf", "")
264        if kwargs.get("novlan", False):
265            plugin.vlan_bpf = False
266
267    # Decide on the inputs to use for pcap
268    # If --interface is set, ignore all files and listen live on the wire
269    # Otherwise, use all of the files and globs to open offline pcap.
270    # Recurse through any directories if the command-line flag is set.
271    if kwargs.get("interface", None):
272        inputs = [kwargs.get("interface")]
273    else:
274        inputs = []
275        inglobs = kwargs.get("files", [])
276        infiles = []
277        for inglob in inglobs:
278            outglob = glob(inglob)
279            if not outglob:
280                logger.warning("Could not find file(s) matching {!r}".format(inglob))
281                continue
282            infiles.extend(outglob)
283        while len(infiles) > 0:
284            infile = infiles.pop(0)
285            if kwargs.get("recursive", False) and os.path.isdir(infile):
286                morefiles = os.listdir(infile)
287                for morefile in morefiles:
288                    infiles.append(os.path.join(infile, morefile))
289            elif os.path.isfile(infile):
290                inputs.append(infile)
291
292    # Process plugin-specific options
293    for plugin in plugin_chain:
294        for option, args in plugin.optiondict.items():
295            if option in plugin_args.get(plugin, {}):
296                setattr(plugin, option, plugin_args[plugin][option])
297            else:
298                setattr(plugin, option, args.get("default", None))
299        plugin.handle_plugin_options()
300
301
302    #### Dshell is ready to read pcap! ####
303    for plugin in plugin_chain:
304        plugin._premodule()
305
306    # If we are not multiprocessing, simply pass the files for processing
307    if not kwargs.get("multiprocessing", False):
308        process_files(inputs, **kwargs)
309    # If we are multiprocessing, things get more complicated.
310    else:
311        # Create an output queue, and wrap the 'write' function of each
312        # plugins's output module to send calls to the multiprocessing queue
313        output_queue = multiprocessing.Queue()
314        output_wrappers = {}
315        for plugin in plugin_chain:
316            qo = QueueOutputWrapper(plugin.out, output_queue)
317            output_wrappers[qo.id] = qo
318            plugin.out.write = qo.write
319
320        # Create processes to handle each separate input file
321        processes = []
322        for i in inputs:
323            processes.append(
324                multiprocessing.Process(target=process_files, args=([i],), kwargs=kwargs)
325            )
326
327        # Spawn processes, and keep track of which ones are running
328        running = []
329        max_writes_per_batch = 50
330        while processes or running:
331            if processes and len(running) < kwargs.get("process_max", 4):
332                # Start a process and move it to the 'running' list
333                proc = processes.pop(0)
334                proc.start()
335                logger.debug("Started process {}".format(proc.pid))
336                running.append(proc)
337            for proc in running:
338                if not proc.is_alive():
339                    # Remove finished processes from 'running' list
340                    logger.debug("Ended process {} (exit code: {})".format(proc.pid, proc.exitcode))
341                    running.remove(proc)
342            try:
343                # Process write commands in the output queue.
344                # Since some plugins write copiously and may block other
345                # processes from launching, only write up to a maximum number
346                # before breaking and rechecking the processes.
347                writes = 0
348                while writes < max_writes_per_batch:
349                    wrapper_id, args, kwargs = output_queue.get(True, 1)
350                    owrapper = output_wrappers[wrapper_id]
351                    owrapper.true_write(*args, **kwargs)
352                    writes += 1
353            except queue.Empty:
354                pass
355
356        output_queue.close()
357
358    for plugin in plugin_chain:
359        plugin._postmodule()
360
361
362# Maps datalink type reported by pcapy to a pypacker packet class.
363datalink_map = {
364    1: ethernet.Ethernet,
365    9: ppp.PPP,
366    51: pppoe.PPPoE,
367    105: ieee80211.IEEE80211,
368    113: linuxcc.LinuxCC,
369    127: radiotap.Radiotap,
370    204: ppp.PPP,
371    227: can.CAN,
372    228: ip.IP,
373    229: ip6.IP6,
374}
375
376
377def read_packets(input: str, interface=False, bpf=None, count=None) -> Iterable[dshell.Packet]:
378    """
379    Yields packets from input pcap file or device.
380
381    :param str input: device or pcap file path
382    :param bool interface: Whether input is a device.
383    :param str bpf: Optional bpf filter.
384    :param int count: Optional max count of packets to read before exiting.
385
386    :yields: packets defined by pypacker.
387        NOTE: Timestamp and frame id are added to packet for convenience.
388    """
389
390    if interface:
391        # Listen on an interface if the option is set
392        try:
393            capture = pcapy.open_live(input, 65536, True, 1)
394        except pcapy.PcapError as e:
395            # User probably doesn't have permission to listen on interface
396            # In any case, print just the error without traceback
397            logger.error(str(e))
398            return
399    else:
400        # Otherwise, read from pcap file(s)
401        try:
402            capture = pcapy.open_offline(input)
403        except pcapy.PcapError as e:
404            logger.error("Could not open '{}': {!s}".format(input, e))
405            return
406
407    # TODO: We may want to allow all packets to go through and then allow the plugin to filter
408    #   them out in feed_plugin_chain().
409    #   That way our frame_id won't be out of sync from skipped packets.
410    # Try and use the first plugin's BPF as the initial filter
411    # The BPFs for other plugins will be applied along the chain as needed
412    try:
413        if bpf:
414            capture.setfilter(bpf)
415    except pcapy.PcapError as e:
416        if str(e).startswith("no VLAN support for data link type"):
417            logger.error("Cannot use VLAN filters for {!r}. Recommend running with --no-vlan argument.".format(input))
418            return
419        elif "syntax error" in str(e) or "link layer applied in wrong context" == str(e):
420            logger.error("Could not compile BPF: {!s} ({!r})".format(e, bpf))
421            return
422        elif "802.11 link-layer types supported only on 802.11" == str(e):
423            logger.error("BPF incompatible with pcap file: {!s}".format(e))
424            return
425        else:
426            raise e
427
428    # Set the datalink layer for each plugin, based on the pcapy capture.
429    # Also compile a pcapy BPF object for each.
430    datalink = capture.datalink()
431    for plugin in plugin_chain:
432        # TODO Find way around libpcap bug that segfaults when certain BPFs
433        #      are used with certain datalink types
434        #      (e.g. datalink=204, bpf="ip")
435        plugin.link_layer_type = datalink
436        plugin.recompile_bpf()
437
438    # Get correct pypacker class based on datalink layer.
439    packet_class = datalink_map.get(datalink, ethernet.Ethernet)
440
441    logger.info(f"Datalink: {datalink} - {packet_class.__name__}")
442
443    # Iterate over the file/interface and yield Packet objects.
444    frame = 1  # Start with 1 because Wireshark starts with 1.
445    while True:
446        try:
447            header, packet_data = capture.next()
448            if header is None and not packet_data:
449                # probably the end of the capture
450                break
451            if count and frame - 1 >= count:
452                # we've reached the maximum number of packets to process
453                break
454
455            # Add timestamp and frame id to packet object for convenience.
456            pktlen = header.getlen()
457            s, us = header.getts()
458            ts = s + us / 1000000.0
459
460            # Wrap packet in dshell's Packet class.
461            packet = dshell.Packet(pktlen, packet_class(packet_data), ts, frame=frame)
462            frame += 1
463
464            yield packet
465
466        except pcapy.PcapError as e:
467            estr = str(e)
468            eformat = "Error processing '{i}' - {e}"
469            if estr.startswith("truncated dump file"):
470                logger.error(eformat.format(i=input, e=estr))
471                logger.debug(e, exc_info=True)
472            elif estr.startswith("bogus savefile header"):
473                logger.error(eformat.format(i=input, e=estr))
474                logger.debug(e, exc_info=True)
475            else:
476                raise
477            break
478
479
480# TODO: The use of kwargs makes it difficult to understand what arguments the function accept
481#   and difficult to follow the code flow.
482def process_files(inputs, **kwargs):
483    # Iterate over each of the input files
484    # For live capture, the "input" would just be the name of the interface
485    global plugin_chain
486    interface = kwargs.get("interface", False)
487    count = kwargs.get("count", None)
488    # Try and use the first plugin's BPF as the initial filter
489    # The BPFs for other plugins will be applied along the chain as needed
490    bpf = plugin_chain[0].bpf
491
492    while len(inputs) > 0:
493        input0 = inputs.pop(0)
494
495        # Check if file needs to be decompressed by its file extension
496        extension = os.path.splitext(input0)[-1]
497        if extension in (".gz", ".bz2", ".zip") and "interface" not in kwargs:
498            tempfiles = decompress_file(input0, extension, kwargs.get("unzipdir", tempfile.gettempdir()))
499            inputs = tempfiles + inputs
500            continue
501
502        for plugin in plugin_chain:
503            plugin._prefile(input0)
504
505        for packet in read_packets(input0, interface=interface, bpf=bpf, count=count):
506            feed_plugin_chain(0, packet)
507
508        clean_plugin_chain(0)
509        for plugin in plugin_chain:
510            plugin.purge()
511            plugin._postfile()
512
513
514# TODO: Separate some of this logic outside of this function so we can call
515#   dshell as a library.
516def main_command_line():
517    # Since plugin_chain contains the actual plugin instances we have to make sure
518    # we reset the global plugin_chain so multiple runs don't affect each other.
519    # (This was necessary to call this function through a python script.)
520    # TODO: Should plugin_chain be a list of plugin classes instead of instances?
521    global plugin_chain
522    plugin_chain = []
523
524    # dictionary of all available plugins: {name: module path}
525    plugin_map = get_plugins()
526    # dictionary of plugins that the user wants to use: {name: object}
527    active_plugins = OrderedDict()
528
529    # The main argument parser. It will have every command line option
530    # available and should be used when actually parsing
531    parser = DshellArgumentParser(
532        usage="%(prog)s [options] [plugin options] file1 file2 ... fileN",
533        add_help=False)
534    parser.add_argument('-c', '--count', type=int, default=0,
535                      help='Number of packets to process')
536    parser.add_argument('--debug', action="store_true",
537                      help="Show debug messages")
538    parser.add_argument('-v', '--verbose', action="store_true",
539                      help="Show informational messages")
540    parser.add_argument('-acc', '--allcc', action="store_true",
541                      help="Show all 3 GeoIP2 country code types (represented_country/registered_country/country)")
542    parser.add_argument('-d', '-p', '--plugin', dest='plugin', type=str,
543                      action='append', metavar="PLUGIN",
544                      help="Use a specific plugin module. Can be chained with '+'.")
545    parser.add_argument('--defragment', dest='defrag', action='store_true',
546                      help='Reconnect fragmented IP packets')
547    parser.add_argument('-h', '-?', '--help', dest='help',
548                      help="Print common command-line flags and exit", action='store_true',
549                      default=False)
550    parser.add_argument('-i', '--interface', default=None, type=str,
551                        help="Listen live on INTERFACE instead of reading pcap")
552    parser.add_argument('-l', '--ls', '--list', action="store_true",
553                      help='List all available plugins', dest='list')
554    parser.add_argument('-r', '--recursive', dest='recursive', action='store_true',
555                      help='Recursively process all PCAP files under input directory')
556    parser.add_argument('--unzipdir', type=str, metavar="DIRECTORY",
557                      default=tempfile.gettempdir(),
558                      help='Directory to use when decompressing input files (.gz, .bz2, and .zip only)')
559
560    multiprocess_group = parser.add_argument_group("multiprocessing arguments")
561    multiprocess_group.add_argument('-P', '--parallel', dest='multiprocessing', action='store_true',
562                      help='Handle each file in separate parallel processes')
563    multiprocess_group.add_argument('-n', '--nprocs', type=int, default=4,
564                      metavar='NUMPROCS', dest='process_max',
565                      help='Define max number of parallel processes (default: 4)')
566
567    filter_group = parser.add_argument_group("filter arguments")
568    filter_group.add_argument('--bpf', default='', type=str,
569                        help="Overwrite all BPFs and use provided input. Use carefully!")
570    filter_group.add_argument('--ebpf', default='', type=str, metavar="BPF",
571                        help="Extend existing BPFs with provided input for additional filtering. It will transform input into \"(<original bpf>) and (<ebpf>)\"")
572    filter_group.add_argument("--no-vlan", action="store_true", dest="novlan",
573                        help="Ignore packets with VLAN headers")
574
575    output_group = parser.add_argument_group("output arguments")
576    output_group.add_argument("--lo", "--list-output", action="store_true",
577                            help="List available output modules",
578                            dest="listoutput")
579    output_group.add_argument("--no-buffer", action="store_true",
580                            help="Do not buffer plugin output",
581                            dest="nobuffer")
582    output_group.add_argument("-x", "--extra", action="store_true",
583                            help="Appends extra data to all plugin output.")
584    # TODO Figure out how to make --extra flag play nicely with user-only
585    #      output modules, like jsonout and csvout
586    output_group.add_argument("-O", "--omodule", type=str, dest="omodule",
587                            metavar="MODULE",
588                            help="Use specified output module for plugins instead of defaults. For example, --omodule=jsonout for JSON output.")
589    output_group.add_argument("--oarg", type=str, metavar="ARG=VALUE",
590                            dest="oargs", action="append",
591                            help="Supply a specific keyword argument to plugins' output modules. Can be used multiple times for multiple arguments. Not using an equal sign will treat it as a flag and set the value to True. Example: --oarg \"delimiter=:\" --oarg \"timeformat=%%H %%M %%S\"")
592    output_group.add_argument("-q", "--quiet", action="store_true",
593                            help="Disable logging")
594    output_group.add_argument("-W", metavar="OUTFILE", dest="outfile",
595                            help="Write to OUTFILE instead of stdout")
596
597    parser.add_argument('files', nargs='*',
598                        help="pcap files or globs to process")
599
600    # A short argument parser, meant to only hold the simplified list of
601    # arguments for when a plugin is called without a pcap file.
602    # DO NOT USE for any serious argument parsing.
603    parser_short = DshellArgumentParser(
604        usage="%(prog)s [options] [plugin options] file1 file2 ... fileN",
605        add_help=False)
606    parser_short.add_argument('-h', '-?', '--help', dest='help',
607                            help="Print common command-line flags and exit", action='store_true',
608                            default=False)
609    parser.add_argument('--version', action='version',
610                        version="Dshell " + str(dshell.core.__version__))
611    parser_short.add_argument('-d', '-p', '--plugin', dest='plugin', type=str,
612                      action='append', metavar="PLUGIN",
613                      help="Use a specific plugin module")
614    parser_short.add_argument('--ebpf', default='', type=str, metavar="BPF",
615                        help="Extend existing BPFs with provided input for additional filtering. It will transform input into \"(<original bpf>) and (<ebpf>)\"")
616    parser_short.add_argument('-i', '--interface',
617                        help="Listen live on INTERFACE instead of reading pcap")
618    parser_short.add_argument('-l', '--ls', '--list', action="store_true",
619                      help='List all available plugins', dest='list')
620    parser_short.add_argument("--lo", "--list-output", action="store_true",
621                            help="List available output modules")
622    # FIXME: Should this duplicate option be removed?
623    parser_short.add_argument("-o", "--omodule", type=str, metavar="MODULE",
624                            help="Use specified output module for plugins instead of defaults. For example, --omodule=jsonout for JSON output.")
625    parser_short.add_argument('files', nargs='*',
626                              help="pcap files or globs to process")
627
628    # Start parsing the arguments
629    # Specifically, we want to grab the desired plugin list
630    # This will let us add the plugin-specific arguments and reprocess the args
631    opts, xopts = parser.parse_known_args()
632    if opts.plugin:
633        # Multiple plugins can be chained using either multiple instances
634        # of -d/-p/--plugin or joining them together with + signs.
635        plugins = '+'.join(opts.plugin)
636        plugins = plugins.split('+')
637        # check for invalid plugins
638        for plugin in plugins:
639            plugin = plugin.strip()
640            if not plugin:
641                # User probably mistyped '++' instead of '+' somewhere.
642                # Be nice and ignore this minor infraction.
643                continue
644            if plugin not in plugin_map:
645                parser_short.epilog = "ERROR! Invalid plugin provided: '{}'".format(plugin)
646                parser_short.print_help()
647                sys.exit(1)
648            # While we're at it, go ahead and import the plugin modules now
649            # This can probably be done further down the line, but here is
650            # just convenient
651            plugin_module = import_module(plugin_map[plugin])
652            # Handle multiple instances of same plugin by appending number to
653            # end of plugin name. This is used mostly to separate
654            # plugin-specific arguments from each other
655            if plugin in active_plugins:
656                i = 1
657                plugin = plugin + str(i)
658                while plugin in active_plugins:
659                    i += 1
660                    plugin = plugin[:-(len(str(i-1)))] + str(i)
661            # Add copy of plugin object to chain and add to argument parsers
662            # TODO: Use class attributes for class related things like name, description, optionsdict
663            #   This way we don't have to initialize the plugin at this point and fixes a lot of the
664            #   issues that arise that come from dealing with a singleton.
665            active_plugins[plugin] = plugin_module.DshellPlugin()
666            plugin_chain.append(active_plugins[plugin])
667            parser.add_plugin_arguments(plugin, active_plugins[plugin])
668            parser_short.add_plugin_arguments(plugin, active_plugins[plugin])
669        opts, xopts = parser.parse_known_args()
670
671    if xopts:
672        for xopt in xopts:
673            logger.warning('Could not understand argument {!r}'.format(xopt))
674
675    if opts.help:
676        # Just print the full help message and exit
677        parser.print_help()
678        print("\n")
679        for plugin in plugin_chain:
680            print("############### {}".format(plugin.name))
681            print(plugin.longdescription)
682            print("\n")
683            print('Default BPF: "{}"'.format(plugin.bpf))
684        print("\n")
685        sys.exit()
686
687    if opts.list:
688        try:
689            print_plugins(get_plugin_information())
690        except ImportError as e:
691            logger.error(e, exc_info=opts.debug)
692        sys.exit()
693
694    if opts.listoutput:
695        # List available output modules and a brief description
696        output_map = get_output_modules(get_output_path())
697        for modulename in sorted(output_map):
698            try:
699                module = import_module("dshell.output."+modulename)
700                module = module.obj
701            except Exception as e:
702                etype = e.__class__.__name__
703                logger.debug("Could not load {} module. ({}: {!s})".format(modulename, etype, e))
704            else:
705                print("\t{:<25} {}".format(modulename, module._DESCRIPTION))
706        sys.exit()
707
708    if not opts.plugin:
709        # If a plugin isn't provided, print the short help message
710        parser_short.epilog = "Select a plugin to use with -d or --plugin"
711        parser_short.print_help()
712        sys.exit()
713
714    if not opts.files and not opts.interface:
715        # If no files are provided, print the short help message
716        parser_short.epilog = "Include a pcap file to get started. Use --help for more information."
717        parser_short.print_help()
718        sys.exit()
719
720    # Process the plugin-specific args and set the attributes within them
721    plugin_args = {}
722    for plugin_name, plugin in active_plugins.items():
723        plugin_args[plugin] = {}
724        args_and_attrs = parser.get_plugin_arguments(plugin_name, plugin)
725        for darg, dattr in args_and_attrs:
726            value = getattr(opts, darg)
727            plugin_args[plugin][dattr] = value
728
729    main(plugin_args=plugin_args, **vars(opts))
730
731
732if __name__ == "__main__":
733    main_command_line()
734