--- a/WebBrowser/SafeBrowsing/SafeBrowsingAPIClient.py Tue Apr 10 19:43:45 2018 +0200 +++ b/WebBrowser/SafeBrowsing/SafeBrowsingAPIClient.py Wed Apr 11 19:57:23 2018 +0200 @@ -22,6 +22,8 @@ from WebBrowser.WebBrowserWindow import WebBrowserWindow +from .SafeBrowsingThreatList import ThreatList + class SafeBrowsingAPIClient(QObject): """ @@ -30,7 +32,7 @@ @signal networkError(str) emitted to indicate a network error """ ClientId = "eric6_API_client" - ClientVersion = "1.0.0" + ClientVersion = "2.0.0" GsbUrlTemplate = "https://safebrowsing.googleapis.com/v4/{0}?key={1}" @@ -47,11 +49,20 @@ @param parent reference to the parent object @type QObject """ + super(SafeBrowsingAPIClient, self).__init__(parent) + self.__apiKey = apiKey self.__fairUse = fairUse self.__nextRequestNoSoonerThan = QDateTime() self.__failCount = 0 + + self.__lookupApiCache = {} + # Temporary cache used by the lookup API (v4) + # key: URL as string + # value: dictionary with these entries: + # "validUntil": (QDateTime) + # "threatInfo": (ThreatList) def setApiKey(self, apiKey): """ @@ -262,7 +273,89 @@ ## Methods below implement the 'Lookup API (v4)' ####################################################################### - # TODO: implement the Lookup API (including temporary caching) + def lookupUrl(self, url, platforms): + """ + Public method to send an URL to Google for checking. + + @param url URL to be checked + @type QUrl + @param platforms list of platform types to check against + @type list of str + @return list of threat list info objects + @rtype list of ThreatList + """ + # sanitize the URL by removing user info and query data + url = url.adjusted( + QUrl.RemoveUserInfo | QUrl.RemoveQuery | QUrl.RemoveFragment + ) + urlStr = url.toString() + if urlStr in self.__lookupApiCache: + if self.__lookupApiCache[urlStr]["validUntil"] > \ + QDateTime.currentDateTime(): + # cached entry is still valid + return self.__lookupApiCache[urlStr]["threatInfo"] + else: + del self.__lookupApiCache[urlStr] + + requestBody = { + "client": { + "clientId": self.ClientId, + "clientVersion": self.ClientVersion, + }, + "threatInfo": { + "threatTypes": [ + "MALWARE", "SOCIAL_ENGINEERING", "UNWANTED_SOFTWARE", + "POTENTIALLY_HARMFUL_APPLICATION", + ], + "platformTypes": platforms, + "threatEntryTypes": ["URL", "EXECUTABLE"], + "threatEntries": [ + {"url": urlStr}, + ], + }, + } + + data = QByteArray(json.dumps(requestBody).encode("utf-8")) + url = QUrl(self.GsbUrlTemplate.format("threatMatches:find", + self.__apiKey)) + req = QNetworkRequest(url) + req.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") + reply = WebBrowserWindow.networkManager().post(req, data) + + while reply.isRunning(): + QCoreApplication.processEvents(QEventLoop.AllEvents, 200) + # max. 200 ms processing + + threats = [] + if reply.error() != QNetworkReply.NoError: + self.networkError.emit(reply.errorString()) + else: + res = json.loads(str(reply.readAll(), "utf-8")) + if res and "matches" in res: + cacheDuration = 0 + for match in res["matches"]: + threatInfo = ThreatList( + match["threatType"], + match["platformType"], + match["threatEntryType"], + ) + threats.append(threatInfo) + if "cacheDuration" in match: + cacheDurationSec = int( + match["cacheDuration"].strip().rstrip("s") + .split(".")[0]) + if cacheDurationSec > cacheDuration: + cacheDuration = cacheDurationSec + if cacheDuration > 0 and bool(threats): + validUntil = QDateTime.currentDateTime().addSecs( + cacheDuration) + self.__lookupApiCache[urlStr] = { + "validUntil": validUntil, + "threatInfo": threats + } + + reply.deleteLater() + return threats ####################################################################### ## Methods below implement global (class wide) functionality