HTTP GET packet sniffer in Scapy

You need to use the sprintf function of the packet instead of printing the packet itself. You also need to split the string returned from it and join it back together with newline characters, otherwise it spits it out all on one line:

#!/usr/bin/python
from scapy.all import *

def http_header(packet):
        http_packet=str(packet)
        if http_packet.find('GET'):
                return GET_print(packet)

def GET_print(packet1):
    ret = "***************************************GET PACKET****************************************************\n"
    ret += "\n".join(packet1.sprintf("{Raw:%Raw.load%}\n").split(r"\r\n"))
    ret += "*****************************************************************************************************\n"
    return ret

sniff(iface='eth0', prn=http_header, filter="tcp port 80")

I also added a filter for TCP port 80, but this could be removed if you need to.

Example output:

***************************************GET PACKET****************************************************
'GET /projects/scapy/doc/usage.html HTTP/1.1
Host: www.secdev.org
Connection: keep-alive
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.65 Safari/537.36
Referer: https://www.google.co.uk/
Accept-Encoding: gzip, deflate, sdch
Accept-Language: en-GB,en;q=0.8,en-US;q=0.6
If-None-Match: "28c84-48498d5654df67640-gzip"
If-Modified-Since: Mon, 19 Apr 2010 15:44:17 GMT

'
*****************************************************************************************************

Pierre points out that you can do away with the http_header function entirely by using the lfilter argument to sniff(). I took the liberty of making the code a little more succinct at the same time:

#!/usr/bin/python
from scapy.all import *

stars = lambda n: "*" * n

def GET_print(packet):
    return "\n".join((
        stars(40) + "GET PACKET" + stars(40),
        "\n".join(packet.sprintf("{Raw:%Raw.load%}").split(r"\r\n")),
        stars(90)))

sniff(
    iface='eth0',
    prn=GET_print,
    lfilter=lambda p: "GET" in str(p),
    filter="tcp port 80")

EDIT:

Please note that Scapy-http is now DEPRECATED and is included in Scapy 2.4.3+. Use import scapy.layers.http or load_layer("http") to enable it.

Answer:

There's a scapy http module that you can install by running pip install scapy-http. Once that is installed, you can import it by running import scapy_http.http. This is separate from your scapy module but adds functionality to scapy so you still need to import scapy as you usually would.

Once imported, change your filter line to

sniff(iface="eth0",
prn=GET_print,
lfilter= lambda x: x.haslayer(scapy_http.http.HTTPRequest))

I removed the filter="tcp and port 80" option because, using the http lfilter will return all HTTP Request queries regardless of port, except SSL for the obvious reason that it cannot be sniffed under the usual circumstances. You may want to keep the filter option for performance reasons.


I had commented on one way to improve it but I decided to whip together a more complete solution. This won't have the asterisk packet breaks but instead just prints the headers as pretty printed dictionary so this may work for you or may not but you can also customize it to suit your needs. Aside from the formatting, this seems like the most efficient means posted on this question so far and you can delegate to a function to add formatting and further deconstruct the dict.

#!/usr/bin/env python2

import argparse
import pprint
import sys

# Suppress scapy warning if no default route for IPv6. This needs to be done before the import from scapy.
import logging
logging.getLogger("scapy.runtime").setLevel(logging.ERROR)


# Try to import sniff from scapy.all and show error w/ install instructions if it cannot be imported.
try:
    from scapy.all import sniff
except ImportError:
    sys.stderr.write("ERROR: You must have scapy installed.\n")
    sys.stderr.write("You can install it by running: sudo pip install -U 'scapy>=2.3,<2.4'")
    exit(1)

# Try to import scapy_http.http and show error w/ install instructions if it cannot be imported.
try:
    import scapy_http.http
except ImportError:
    sys.stderr.write("ERROR: You must have scapy-http installed.\n")
    sys.stderr.write("You can install it by running: sudo pip install -U 'scapy>=1.8'")
    exit(1)


if __name__ == "__main__":
    # Parser command line arguments and make them available.
    parser = argparse.ArgumentParser(
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
        description="Print HTTP Request headers (must be run as root or with capabilities to sniff).",
    )
    parser.add_argument("--interface", "-i", help="Which interface to sniff on.", default="eth0")
    parser.add_argument("--filter", "-f", help='BPF formatted packet filter.', default="tcp and port 80")
    parser.add_argument("--count", "-c", help="Number of packets to capture. 0 is unlimited.", type=int, default=0)
    args = parser.parse_args()

    # Sniff for the data and print it using lambda instead of writing a function to pretty print.
    # There is no reason not to use a function you write for this but I just wanted to keep the example simply while
    # demoing how to only match HTTP requests and to access the HTTP headers as pre-created dict's instead of
    # parsing the data as a string.
    sniff(iface=args.interface,
          promisc=False,
          filter=args.filter,
          lfilter=lambda x: x.haslayer(scapy_http.http.HTTPRequest),
          prn=lambda pkt: pprint.pprint(pkt.getlayer(scapy_http.http.HTTPRequest).fields, indent=4),
          count=args.count
    )