1## This file is part of Scapy
2## See http://www.secdev.org/projects/scapy for more informations
3## Copyright (C) Philippe Biondi <phil@secdev.org>
4## This program is published under a GPLv2 license
5
6"""
7Main module for interactive startup.
8"""
9
10from __future__ import absolute_import
11from __future__ import print_function
12
13import sys, os, getopt, re, code
14import gzip, glob
15import importlib
16import logging
17from random import choice
18import types
19import io
20
21# Never add any global import, in main.py, that would trigger a warning messsage
22# before the console handlers gets added in interact()
23from scapy.error import log_interactive, log_loading, log_scapy, warning
24import scapy.modules.six as six
25from scapy.themes import DefaultTheme, apply_ipython_style
26
27IGNORED = list(six.moves.builtins.__dict__)
28
29GLOBKEYS = []
30
31LAYER_ALIASES = {
32    "tls": "tls.all"
33}
34
35QUOTES = [
36    ("Craft packets like it is your last day on earth.", "Lao-Tze"),
37    ("Craft packets like I craft my beer.", "Jean De Clerck"),
38    ("Craft packets before they craft you.", "Socrate"),
39    ("Craft me if you can.", "IPv6 layer"),
40    ("To craft a packet, you have to be a packet, and learn how to swim in the "
41     "wires and in the waves.", "Jean-Claude Van Damme"),
42]
43
44def _probe_config_file(cf):
45    cf_path = os.path.join(os.path.expanduser("~"), cf)
46    try:
47        os.stat(cf_path)
48    except OSError:
49        return None
50    else:
51        return cf_path
52
53def _read_config_file(cf, _globals=globals(), _locals=locals(), interactive=True):
54    """Read a config file: execute a python file while loading scapy, that may contain
55    some pre-configured values.
56
57    If _globals or _locals are specified, they will be updated with the loaded vars.
58    This allows an external program to use the function. Otherwise, vars are only available
59    from inside the scapy console.
60
61    params:
62    - _globals: the globals() vars
63    - _locals: the locals() vars
64    - interactive: specified whether or not errors should be printed using the scapy console or
65    raised.
66
67    ex, content of a config.py file:
68        'conf.verb = 42\n'
69    Manual loading:
70        >>> _read_config_file("./config.py"))
71        >>> conf.verb
72        42
73    """
74    log_loading.debug("Loading config file [%s]", cf)
75    try:
76        exec(compile(open(cf).read(), cf, 'exec'), _globals, _locals)
77    except IOError as e:
78        if interactive:
79            raise
80        log_loading.warning("Cannot read config file [%s] [%s]", cf, e)
81    except Exception as e:
82        if interactive:
83            raise
84        log_loading.exception("Error during evaluation of config file [%s]", cf)
85
86def _validate_local(x):
87    """Returns whether or not a variable should be imported.
88    Will return False for any default modules (sys), or if
89    they are detected as private vars (starting with a _)"""
90    global IGNORED
91    return x[0] != "_" and not x in IGNORED
92
93DEFAULT_PRESTART_FILE = _probe_config_file(".scapy_prestart.py")
94DEFAULT_STARTUP_FILE = _probe_config_file(".scapy_startup.py")
95SESSION = None
96
97def _usage():
98    print("""Usage: scapy.py [-s sessionfile] [-c new_startup_file] [-p new_prestart_file] [-C] [-P]
99    -C: do not read startup file
100    -P: do not read pre-startup file""")
101    sys.exit(0)
102
103
104######################
105## Extension system ##
106######################
107
108
109def _load(module, globals_dict=None, symb_list=None):
110    """Loads a Python module to make variables, objects and functions
111available globally.
112
113    The idea is to load the module using importlib, then copy the
114symbols to the global symbol table.
115
116    """
117    if globals_dict is None:
118        globals_dict = six.moves.builtins.__dict__
119    try:
120        mod = importlib.import_module(module)
121        if '__all__' in mod.__dict__:
122            # import listed symbols
123            for name in mod.__dict__['__all__']:
124                if symb_list is not None:
125                    symb_list.append(name)
126                globals_dict[name] = mod.__dict__[name]
127        else:
128            # only import non-private symbols
129            for name, sym in six.iteritems(mod.__dict__):
130                if _validate_local(name):
131                    if symb_list is not None:
132                        symb_list.append(name)
133                    globals_dict[name] = sym
134    except Exception:
135        log_interactive.error("Loading module %s", module, exc_info=True)
136
137def load_module(name):
138    """Loads a Scapy module to make variables, objects and functions
139    available globally.
140
141    """
142    _load("scapy.modules."+name)
143
144def load_layer(name, globals_dict=None, symb_list=None):
145    """Loads a Scapy layer module to make variables, objects and functions
146    available globally.
147
148    """
149    _load("scapy.layers." + LAYER_ALIASES.get(name, name),
150          globals_dict=globals_dict, symb_list=symb_list)
151
152def load_contrib(name):
153    """Loads a Scapy contrib module to make variables, objects and
154    functions available globally.
155
156    If no contrib module can be found with the given name, try to find
157    a layer module, since a contrib module may become a layer module.
158
159    """
160    try:
161        importlib.import_module("scapy.contrib." + name)
162        _load("scapy.contrib." + name)
163    except ImportError:
164        # if layer not found in contrib, try in layers
165        load_layer(name)
166
167def list_contrib(name=None):
168    if name is None:
169        name="*.py"
170    elif "*" not in name and "?" not in name and not name.endswith(".py"):
171        name += ".py"
172    name = os.path.join(os.path.dirname(__file__), "contrib", name)
173    for f in sorted(glob.glob(name)):
174        mod = os.path.basename(f)
175        if mod.startswith("__"):
176            continue
177        if mod.endswith(".py"):
178            mod = mod[:-3]
179        desc = { "description":"-", "status":"?", "name":mod }
180        for l in io.open(f, errors="replace"):
181            p = l.find("scapy.contrib.")
182            if p >= 0:
183                p += 14
184                q = l.find("=", p)
185                key = l[p:q].strip()
186                value = l[q+1:].strip()
187                desc[key] = value
188        print("%(name)-20s: %(description)-40s status=%(status)s" % desc)
189
190
191
192
193
194
195##############################
196## Session saving/restoring ##
197##############################
198
199def update_ipython_session(session):
200    """Updates IPython session with a custom one"""
201    try:
202        get_ipython().user_ns.update(session)
203    except:
204        pass
205
206def save_session(fname=None, session=None, pickleProto=-1):
207    """Save current Scapy session to the file specified in the fname arg.
208
209    params:
210     - fname: file to save the scapy session in
211     - session: scapy session to use. If None, the console one will be used
212     - pickleProto: pickle proto version (default: -1 = latest)"""
213    from scapy import utils
214    if fname is None:
215        fname = conf.session
216        if not fname:
217            conf.session = fname = utils.get_temp_file(keep=True)
218    log_interactive.info("Use [%s] as session file" % fname)
219
220    if session is None:
221        try:
222            session = get_ipython().user_ns
223        except:
224            session = six.moves.builtins.__dict__["scapy_session"]
225
226    to_be_saved = session.copy()
227    if "__builtins__" in to_be_saved:
228        del(to_be_saved["__builtins__"])
229
230    for k in list(to_be_saved):
231        i = to_be_saved[k]
232        if hasattr(i, "__module__") and (k[0] == "_" or i.__module__.startswith("IPython")):
233            del(to_be_saved[k])
234        if isinstance(i, ConfClass):
235            del(to_be_saved[k])
236        elif isinstance(i, (type, type, types.ModuleType)):
237            if k[0] != "_":
238                log_interactive.error("[%s] (%s) can't be saved.", k, type(to_be_saved[k]))
239            del(to_be_saved[k])
240
241    try:
242         os.rename(fname, fname+".bak")
243    except OSError:
244         pass
245
246    f=gzip.open(fname,"wb")
247    six.moves.cPickle.dump(to_be_saved, f, pickleProto)
248    f.close()
249    del f
250
251def load_session(fname=None):
252    """Load current Scapy session from the file specified in the fname arg.
253    This will erase any existing session.
254
255    params:
256     - fname: file to load the scapy session from"""
257    if fname is None:
258        fname = conf.session
259    try:
260        s = six.moves.cPickle.load(gzip.open(fname,"rb"))
261    except IOError:
262        try:
263            s = six.moves.cPickle.load(open(fname,"rb"))
264        except IOError:
265            # Raise "No such file exception"
266            raise
267
268    scapy_session = six.moves.builtins.__dict__["scapy_session"]
269    scapy_session.clear()
270    scapy_session.update(s)
271    update_ipython_session(scapy_session)
272
273    log_loading.info("Loaded session [%s]" % fname)
274
275def update_session(fname=None):
276    """Update current Scapy session from the file specified in the fname arg.
277
278    params:
279     - fname: file to load the scapy session from"""
280    if fname is None:
281        fname = conf.session
282    try:
283        s = six.moves.cPickle.load(gzip.open(fname,"rb"))
284    except IOError:
285        s = six.moves.cPickle.load(open(fname,"rb"))
286    scapy_session = six.moves.builtins.__dict__["scapy_session"]
287    scapy_session.update(s)
288    update_ipython_session(scapy_session)
289
290def init_session(session_name, mydict=None):
291    global SESSION
292    global GLOBKEYS
293
294    scapy_builtins = {k: v for k, v in six.iteritems(importlib.import_module(".all", "scapy").__dict__) if _validate_local(k)}
295    six.moves.builtins.__dict__.update(scapy_builtins)
296    GLOBKEYS.extend(scapy_builtins)
297    GLOBKEYS.append("scapy_session")
298    scapy_builtins=None # XXX replace with "with" statement
299
300    if session_name:
301        try:
302            os.stat(session_name)
303        except OSError:
304            log_loading.info("New session [%s]" % session_name)
305        else:
306            try:
307                try:
308                    SESSION = six.moves.cPickle.load(gzip.open(session_name,"rb"))
309                except IOError:
310                    SESSION = six.moves.cPickle.load(open(session_name,"rb"))
311                log_loading.info("Using session [%s]" % session_name)
312            except EOFError:
313                log_loading.error("Error opening session [%s]" % session_name)
314            except AttributeError:
315                log_loading.error("Error opening session [%s]. Attribute missing" %  session_name)
316
317        if SESSION:
318            if "conf" in SESSION:
319                conf.configure(SESSION["conf"])
320                SESSION["conf"] = conf
321        else:
322            conf.session = session_name
323            SESSION = {"conf":conf}
324    else:
325        SESSION = {"conf": conf}
326
327    six.moves.builtins.__dict__["scapy_session"] = SESSION
328
329    if mydict is not None:
330        six.moves.builtins.__dict__["scapy_session"].update(mydict)
331        update_ipython_session(mydict)
332        GLOBKEYS.extend(mydict)
333
334################
335##### Main #####
336################
337
338def scapy_delete_temp_files():
339    for f in conf.temp_files:
340        try:
341            os.unlink(f)
342        except:
343            pass
344    del(conf.temp_files[:])
345
346def _prepare_quote(quote, author, max_len=78):
347    """This function processes a quote and returns a string that is ready
348to be used in the fancy prompt.
349
350    """
351    quote = quote.split(' ')
352    max_len -= 6
353    lines = []
354    cur_line = []
355    def _len(line):
356        return sum(len(elt) for elt in line) + len(line) - 1
357    while quote:
358        if not cur_line or (_len(cur_line) + len(quote[0]) - 1 <= max_len):
359            cur_line.append(quote.pop(0))
360            continue
361        lines.append('   | %s' % ' '.join(cur_line))
362        cur_line = []
363    if cur_line:
364        lines.append('   | %s' % ' '.join(cur_line))
365        cur_line = []
366    lines.append('   | %s-- %s' % (" " * (max_len - len(author) - 5), author))
367    return lines
368
369def interact(mydict=None,argv=None,mybanner=None,loglevel=20):
370    global SESSION
371    global GLOBKEYS
372
373    console_handler = logging.StreamHandler()
374    console_handler.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
375    log_scapy.addHandler(console_handler)
376
377    from scapy.config import conf
378    conf.color_theme = DefaultTheme()
379    conf.interactive = True
380    if loglevel is not None:
381        conf.logLevel = loglevel
382
383    STARTUP_FILE = DEFAULT_STARTUP_FILE
384    PRESTART_FILE = DEFAULT_PRESTART_FILE
385
386    session_name = None
387
388    if argv is None:
389        argv = sys.argv
390
391    try:
392        opts = getopt.getopt(argv[1:], "hs:Cc:Pp:d")
393        for opt, parm in opts[0]:
394            if opt == "-h":
395                _usage()
396            elif opt == "-s":
397                session_name = parm
398            elif opt == "-c":
399                STARTUP_FILE = parm
400            elif opt == "-C":
401                STARTUP_FILE = None
402            elif opt == "-p":
403                PRESTART_FILE = parm
404            elif opt == "-P":
405                PRESTART_FILE = None
406            elif opt == "-d":
407                conf.logLevel = max(1, conf.logLevel-10)
408
409        if len(opts[1]) > 0:
410            raise getopt.GetoptError("Too many parameters : [%s]" % " ".join(opts[1]))
411
412
413    except getopt.GetoptError as msg:
414        log_loading.error(msg)
415        sys.exit(1)
416
417    init_session(session_name, mydict)
418
419    if STARTUP_FILE:
420        _read_config_file(STARTUP_FILE, interactive=True)
421    if PRESTART_FILE:
422        _read_config_file(PRESTART_FILE, interactive=True)
423
424    if conf.fancy_prompt:
425
426        the_logo = [
427            "                                      ",
428            "                     aSPY//YASa       ",
429            "             apyyyyCY//////////YCa    ",
430            "            sY//////YSpcs  scpCY//Pp  ",
431            " ayp ayyyyyyySCP//Pp           syY//C ",
432            " AYAsAYYYYYYYY///Ps              cY//S",
433            "         pCCCCY//p          cSSps y//Y",
434            "         SPPPP///a          pP///AC//Y",
435            "              A//A            cyP////C",
436            "              p///Ac            sC///a",
437            "              P////YCpc           A//A",
438            "       scccccp///pSP///p          p//Y",
439            "      sY/////////y  caa           S//P",
440            "       cayCyayP//Ya              pY/Ya",
441            "        sY/PsY////YCc          aC//Yp ",
442            "         sc  sccaCY//PCypaapyCP//YSs  ",
443            "                  spCPY//////YPSps    ",
444            "                       ccaacs         ",
445            "                                      ",
446        ]
447
448        the_banner = [
449            "",
450            "",
451            "   |",
452            "   | Welcome to Scapy",
453            "   | Version %s" % conf.version,
454            "   |",
455            "   | https://github.com/secdev/scapy",
456            "   |",
457            "   | Have fun!",
458            "   |",
459        ]
460
461        quote, author = choice(QUOTES)
462        the_banner.extend(_prepare_quote(quote, author, max_len=39))
463        the_banner.append("   |")
464        the_banner = "\n".join(
465            logo + banner for logo, banner in six.moves.zip_longest(
466                (conf.color_theme.logo(line) for line in the_logo),
467                (conf.color_theme.success(line) for line in the_banner),
468                fillvalue=""
469            )
470        )
471    else:
472        the_banner = "Welcome to Scapy (%s)" % conf.version
473    if mybanner is not None:
474        the_banner += "\n"
475        the_banner += mybanner
476
477    if not conf.interactive_shell or conf.interactive_shell.lower() in [
478            "ipython", "auto"
479    ]:
480        try:
481            import IPython
482            from IPython.terminal.embed import InteractiveShellEmbed
483        except ImportError:
484            log_loading.warning(
485                "IPython not available. Using standard Python shell "
486                "instead.\nAutoCompletion, History are disabled."
487            )
488            IPYTHON = False
489        else:
490            IPYTHON = True
491    else:
492        IPYTHON = False
493
494    init_session(session_name, mydict)
495
496    if IPYTHON:
497        banner = the_banner + " using IPython %s\n" % IPython.__version__
498        try:
499            from traitlets.config.loader import Config
500        except ImportError:
501            log_loading.warning(
502                "traitlets not available. Some Scapy shell features won't be "
503                "available."
504            )
505            try:
506                ipshell = InteractiveShellEmbed(
507                    banner1=banner,
508                    user_ns=SESSION,
509                )
510            except:
511                code.interact(banner = the_banner, local=SESSION)
512        else:
513            cfg = Config()
514            try:
515                get_ipython
516            except NameError:
517                # Set "classic" prompt style when launched from run_scapy(.bat) files
518                # Register and apply scapy color+prompt style
519                apply_ipython_style(shell=cfg.TerminalInteractiveShell)
520                cfg.TerminalInteractiveShell.confirm_exit = False
521                cfg.TerminalInteractiveShell.separate_in = u''
522            cfg.TerminalInteractiveShell.hist_file = conf.histfile
523            # configuration can thus be specified here.
524            try:
525                ipshell = InteractiveShellEmbed(config=cfg,
526                                                banner1=banner,
527                                                hist_file=conf.histfile if conf.histfile else None,
528                                                user_ns=SESSION)
529            except (AttributeError, TypeError):
530                log_loading.warning("IPython too old. Won't support history and color style.")
531                try:
532                    ipshell = InteractiveShellEmbed(
533                        banner1=banner,
534                        user_ns=SESSION,
535                    )
536                except:
537                    code.interact(banner = the_banner, local=SESSION)
538        ipshell(local_ns=SESSION)
539    else:
540        code.interact(banner = the_banner, local=SESSION)
541
542    if conf.session:
543        save_session(conf.session, SESSION)
544
545    for k in GLOBKEYS:
546        try:
547            del(six.moves.builtins.__dict__[k])
548        except:
549            pass
550
551if __name__ == "__main__":
552    interact()
553