WebBrowser/SafeBrowsing/SafeBrowsingAPIClient.py

Tue, 18 Jul 2017 19:33:46 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Tue, 18 Jul 2017 19:33:46 +0200
branch
safe_browsing
changeset 5811
5358a3c7995f
parent 5809
5b53c17b7d93
child 5816
93c74269d59e
permissions
-rw-r--r--

Done implementing the SafeBrowsingAPIClient class.

# -*- coding: utf-8 -*-

# Copyright (c) 2017 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing the low level interface for Google Safe Browsing.
"""

from __future__ import unicode_literals
try:
    str = unicode       # __IGNORE_EXCEPTION__
except NameError:
    pass

import json
import random
import base64

from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject, QDateTime, QTimer, \
    QUrl, QByteArray
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply

from WebBrowser.WebBrowserWindow import WebBrowserWindow


class SafeBrowsingAPIClient(QObject):
    """
    Class implementing the low level interface for Google Safe Browsing.
    
    @signal networkError(str) emitted to indicate a network error
    @signal threatLists(list) emitted to publish the received threat list
    @signal threatsUpdate(list) emitted to publish the received threats
        update
    @signal fullHashes(dict) emitted to publish the full hashes result
    """
    ClientId = "eric6_API_client"
    ClientVersion = "1.0.0"
    
    GsbUrlTemplate = "https://safebrowsing.googleapis.com/v4/{0}?key={1}"
    
    networkError = pyqtSignal(str)
    threatLists = pyqtSignal(list)
    threatsUpdate = pyqtSignal(list)
    fullHashes = pyqtSignal(dict)
    
    def __init__(self, apiKey, fairUse=True, parent=None):
        """
        Constructor
        
        @param apiKey API key to be used
        @type str
        @param fairUse flag indicating to follow the fair use policy
        @type bool
        @param parent reference to the parent object
        @type QObject
        """
        self.__apiKey = apiKey
        self.__fairUse = fairUse
        
        self.__nextRequestNoSoonerThan = QDateTime()
        self.__failCount = 0
        
        # get threat lists
        self.__threatListsReply = None
        
        # threats lists updates
        self.__threatsUpdatesRequest = None
        self.__threatsUpdateReply = None
        
        # full hashes
        self.__fullHashesRequest = None
        self.__fullHashesReply = None
    
    def getThreatLists(self):
        """
        Public method to retrieve all available threat lists.
        """
        url = QUrl(self.GsbUrlTemplate.format("threatLists", self.__apiKey))
        req = QNetworkRequest(url)
        reply = WebBrowserWindow.networkManager().get(req)
        reply.finished.connect(self.__threatListsReceived)
        self.__threatListsReply = reply
    
    @pyqtSlot()
    def __threatListsReceived(self):
        """
        Private slot handling the threat lists.
        """
        reply = self.sender()
        if reply is self.__threatListsReply:
            self.__threatListsReply = None
            result, hasError = self.__extractData(reply)
            if hasError:
                # reschedule
                self.networkError.emit(reply.errorString())
                self.__reschedule(reply.error(), self.getThreatLists)
            else:
                self.threatLists.emit(result["threatLists"])
            
            reply.deleteLater()
    
    def getThreatsUpdate(self, clientState=None):
        """
        Public method to fetch hash prefix updates for the given threat list.
        
        @param clientState dictionary of client states with keys like
            (threatType, platformType, threatEntryType)
        @type dict
        """
        if self.__threatsUpdateReply is not None:
            # update is in progress
            return
        
        if clientState is None:
            if self.__threatsUpdatesRequest:
                requestBody = self.__threatsUpdatesRequest
            else:
                return
        else:
            requestBody = {
                "client": {
                    "clientId": self.ClientId,
                    "clientVersion": self.ClientVersion,
                },
                "listUpdateRequests": [],
            }
            
            for (threatType, platformType, threatEntryType), currentState in \
                    clientState.items():
                requestBody["listUpdateRequests"].append(
                    {
                        "threatType": threatType,
                        "platformType": platformType,
                        "threatEntryType": threatEntryType,
                        "state": currentState,
                        "constraints": {
                            "supportedCompressions": ["RAW"],
                        }
                    }
                )
            
            self.__threatsUpdatesRequest = requestBody
        
        data = QByteArray(json.dumps(requestBody).encode("utf-8"))
        url = QUrl(self.GsbUrlTemplate.format("threatListUpdates:fetch",
                                              self.__apiKey))
        req = QNetworkRequest(url)
        req.setHeader(QNetworkRequest.ContentTypeHeader, "application/json")
        reply = WebBrowserWindow.networkManager().post(req, data)
        reply.finished.connect(self.__threatsUpdateReceived)
        self.__threatsUpdateReply = reply
    
    @pyqtSlot()
    def __threatsUpdateReceived(self):
        """
        Private slot handling the threats update.
        """
        reply = self.sender()
        if reply is self.__threatsUpdateReply:
            self.__threatsUpdateReply = None
            result, hasError = self.__extractData(reply)
            if hasError:
                # reschedule
                self.networkError.emit(reply.errorString())
                self.__reschedule(reply.error(), self.getThreatsUpdate)
            else:
                self.__threatsUpdatesRequest = None
                self.threatsUpdate.emit(result["listUpdateResponses"])
            
            reply.deleteLater()
    
    def getFullHashes(self, prefixes=None, clientState=None):
        """
        Public method to find full hashes matching hash prefixes.
        
        @param prefixes list of hash prefixes to find
        @type list of str (Python 2) or list of bytes (Python 3)
        @param clientState dictionary of client states with keys like
            (threatType, platformType, threatEntryType)
        @type dict
        """
        if self.__fullHashesReply is not None:
            # full hash request in progress
            return
        
        if prefixes is None or clientState is None:
            if self.__fullHashesRequest:
                requestBody = self.__fullHashesRequest
            else:
                return
        else:
            requestBody = {
                "client": {
                    "clientId": self.ClientId,
                    "clientVersion": self.ClientVersion,
                },
                "clientStates": [],
                "threatInfo": {
                    "threatTypes": [],
                    "platformTypes": [],
                    "threatEntryTypes": [],
                    "threatEntries": [],
                },
            }
            
            for prefix in prefixes:
                requestBody["threatInfo"]["threatEntries"].append(
                    {"hash": base64.b64encode(prefix).decode("ascii")})
            
            for (threatType, platformType, threatEntryType), currentState in \
                    clientState.items():
                requestBody["clientStates"].append(clientState)
                if threatType not in requestBody["threatInfo"]["threatTypes"]:
                    requestBody["threatInfo"]["threatTypes"].append(threatType)
                if platformType not in \
                        requestBody["threatInfo"]["platformTypes"]:
                    requestBody["threatInfo"]["platformTypes"].append(
                        platformType)
                if threatEntryType not in \
                        requestBody["threatInfo"]["threatEntryTypes"]:
                    requestBody["threatInfo"]["threatEntryTypes"].append(
                        threatEntryType)
            
            self.__fullHashesRequest = requestBody
        
        data = QByteArray(json.dumps(requestBody).encode("utf-8"))
        url = QUrl(self.GsbUrlTemplate.format("fullHashes:find",
                                              self.__apiKey))
        req = QNetworkRequest(url)
        req.setHeader(QNetworkRequest.ContentTypeHeader, "application/json")
        reply = WebBrowserWindow.networkManager().post(req, data)
        reply.finished.connect(self.__fullHashesReceived)
        self.__fullHashesReply = reply
    
    @pyqtSlot()
    def __fullHashesReceived(self):
        """
        Private slot handling the full hashes reply.
        """
        reply = self.sender()
        if reply is self.__fullHashesReply:
            self.__fullHashesReply = None
            result, hasError = self.__extractData(reply)
            if hasError:
                # reschedule
                self.networkError.emit(reply.errorString())
                self.__reschedule(reply.error(), self.getFullHashes)
            else:
                self.__fullHashesRequest = None
                self.fullHashes.emit(result)
            
            reply.deleteLater()
    
    def __extractData(self, reply):
        """
        Private method to extract the data of a network reply.
        
        @param reply reference to the network reply object
        @type QNetworkReply
        @return tuple containing the extracted data and an error flag
        @type tuple of (list or dict, bool)
        """
        if reply.error() != QNetworkReply.NoError:
            return None, True
        
        self.__failCount = 0
        result = json.loads(str(reply.readAll(), "utf-8"))
        self.__setWaitDuration(result.get("minimumWaitDuration"))
        return result, False
    
    def __setWaitDuration(self, minimumWaitDuration):
        """
        Private method to set the minimum wait duration.
        
        @param minimumWaitDuration duration to be set
        @type str
        """
        if not self.__fairUse or minimumWaitDuration is None:
            self.__nextRequestNoSoonerThan = QDateTime()
        else:
            waitDuration = int(minimumWaitDuration.rstrip("s"))
            self.__nextRequestNoSoonerThan = \
                QDateTime.currentDateTime().addSecs(waitDuration)
    
    def __reschedule(self, errorCode, func):
        """
        Private method to reschedule an API access.
        
        @param errorCode error code returned by the function to be rescheduled
        @type int
        @param func function to be rescheduled
        @type func
        """
        if errorCode >= 500:
            return
        
        self.__failCount += 1
        waitDuration = min(
            int(2 ** (self.__failCount - 1) * 15 * 60 * (1 + random.random())),
            24 * 60 * 60)
        QTimer.singleShot(waitDuration * 1000, func)

eric ide

mercurial