Search code examples
pythonpyqtpyqt5interceptorpacket

Convert PyQt5 WebEngine request into IP packet and vice-versa


With PyQt5, I have successfully set up a QWebEngineUrlRequestInterceptor subclass, and I can modify the headers and data of the web request. I am building this for a VPN-like app, where the request gets converted into a packet (sent with scapy's IP() object or a similar module), encrypted and sent to another address, who will decrypt and convert the packet data back into a QWebEngine request. My question is how can one convert a web request intercepted with PyQt5 into an IP packet format, and vice-versa?

Here is the interceptor code:

#Custom url interceptor for modifying headers
#and the url before sending it on
class UrlRequestInterceptor(QWebEngineUrlRequestInterceptor):
  def __init__(self,parent=None):
    super(QWebEngineUrlRequestInterceptor,self).__init__(parent)

  def interceptRequest(self,info):
    #Modify the actual request here - todo: block the
    #request here, packet simulate etc...
    info.setHttpHeader(b"Accept-Language",InterClassVariables.languageCode.encode())

Solution

  • The problem is that QWebEngineUrlRequestInterceptor interacts with HTTP (layer 7) when you want IP info (layer 3). QWebEngineUrlRequestInfo does not have any IP properties (again it's at layer 7).

    While it's possible to use Qt's QHostInfo, it's simpler to use vanilla Python. Here, we are using netifaces. While it's not in the standard library, alternatives are more complex, platform-dependent, or require an internet connection.

    import socket
    
    from PyQt5.Qt import QUrl
    from PyQt5.Qt import QWebEngineUrlRequestInterceptor
    from PyQt5.Qt import QWebEngineUrlRequestInfo
    import netifaces
    from scapy.all import IP
    
    #Custom url interceptor for modifying headers
    #and the url before sending it on
    class UrlRequestInterceptor(QWebEngineUrlRequestInterceptor):
      def __init__(self,parent=None):
        super(QWebEngineUrlRequestInterceptor,self).__init__(parent)
        self.info = None
    
      def interceptRequest(self, info: QWebEngineUrlRequestInfo):
        #Modify the actual request here - todo: block the
        #request here, packet simulate etc...
        self.info = info
        self.info.setHttpHeader(b"Accept-Language",
          InterClassVariables.languageCode.encode())
        self.sendPacket()
    
      def getDestIp(self):
        qUrl = self.info.requestURL()
        url = qUrl.toString()
        dest_ip = socket.gethostbyname(url)
        return dest_ip
    
      def sendPacket()
        src_ip = netifaces.gateways()['default'][netifaces.AF_INET][0]
        dest_ip = get_dest_ip(self.info)
        ip_layer = IP(src=src_ip, dst=dest_ip)
        other_layers = OTHER_PROTOCOLS() # FIXME
        pkt = ip_layer/other_layers
        send(pkt) # Choose whichever send function is most appropriate here
    

    If we print the packet (without other layers), we should get something similar to this:

    $ python project.py -show-packet # Pseudo cli
    ###[ IP ]###
      version   = 4
      ihl       = None
      tos       = 0x0
      len       = None
      id        = 1
      flags     =
      frag      = 0
      ttl       = 64
      proto     = ip
      chksum    = None
      src       = 172.31.98.1
      dst       = 151.101.65.69
      \options   \