diff -r 7bf90dcae4e1 -r 5b53c17b7d93 WebBrowser/SafeBrowsing/SafeBrowsingAPIClient.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebBrowser/SafeBrowsing/SafeBrowsingAPIClient.py Mon Jul 17 19:58:37 2017 +0200 @@ -0,0 +1,136 @@ +# -*- 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 + +from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject, QDateTime, QTimer, \ + QUrl +from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply + +from WebBrowser.WebBrowserWindow import WebBrowserWindow + + +class SafeBrowsingAPIClient(QObject): + """ + Class implementing the low level interface for Google Safe Browsing. + """ + ClientId = "eric6_API_client" + ClientVersion = "1.0.0" + + GsbUrlTemplate = "https://safebrowsing.googleapis.com/v4/{0}?key={1}" + + networkError = pyqtSignal(str) + threatLists = pyqtSignal(list) + + # threatListUpdates:fetch Content-Type: application/json POST + # fullHashes:find Content-Type: application/json POST + + 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.__replies = [] + self.__failCount = 0 + + def getThreatLists(self): + """ + Public method to retrieve all available threat lists. + + @return threat lists + @rtype list of dictionaries + """ + url = QUrl(self.GsbUrlTemplate.format("threatLists", self.__apiKey)) + req = QNetworkRequest(url) + reply = WebBrowserWindow.networkManager().get(req) + reply.finished.connect(self.__threatListsReceived) + + @pyqtSlot() + def __threatListsReceived(self): + """ + Private slot handling the threat lists. + """ + reply = self.sender() + result, hasError = self.__extractData(reply) + if hasError: + # reschedule + self.networkError.emit(reply.errorString()) + self.__reschedule(reply.error(), self.getThreatLists) + else: + self.__setWaitDuration(result.get("minimumWaitDuration")) + self.threatLists.emit(result["threatLists"]) + self.__failCount = 0 + + if reply in self.__replies: + self.__replies.remove(reply) + 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 + + result = json.loads(str(reply.readAll(), "utf-8")) + 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)