WebBrowser/SafeBrowsing/SafeBrowsingAPIClient.py

branch
safe_browsing
changeset 5811
5358a3c7995f
parent 5809
5b53c17b7d93
child 5816
93c74269d59e
equal deleted inserted replaced
5809:5b53c17b7d93 5811:5358a3c7995f
13 except NameError: 13 except NameError:
14 pass 14 pass
15 15
16 import json 16 import json
17 import random 17 import random
18 import base64
18 19
19 from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject, QDateTime, QTimer, \ 20 from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject, QDateTime, QTimer, \
20 QUrl 21 QUrl, QByteArray
21 from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply 22 from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply
22 23
23 from WebBrowser.WebBrowserWindow import WebBrowserWindow 24 from WebBrowser.WebBrowserWindow import WebBrowserWindow
24 25
25 26
26 class SafeBrowsingAPIClient(QObject): 27 class SafeBrowsingAPIClient(QObject):
27 """ 28 """
28 Class implementing the low level interface for Google Safe Browsing. 29 Class implementing the low level interface for Google Safe Browsing.
30
31 @signal networkError(str) emitted to indicate a network error
32 @signal threatLists(list) emitted to publish the received threat list
33 @signal threatsUpdate(list) emitted to publish the received threats
34 update
35 @signal fullHashes(dict) emitted to publish the full hashes result
29 """ 36 """
30 ClientId = "eric6_API_client" 37 ClientId = "eric6_API_client"
31 ClientVersion = "1.0.0" 38 ClientVersion = "1.0.0"
32 39
33 GsbUrlTemplate = "https://safebrowsing.googleapis.com/v4/{0}?key={1}" 40 GsbUrlTemplate = "https://safebrowsing.googleapis.com/v4/{0}?key={1}"
34 41
35 networkError = pyqtSignal(str) 42 networkError = pyqtSignal(str)
36 threatLists = pyqtSignal(list) 43 threatLists = pyqtSignal(list)
37 44 threatsUpdate = pyqtSignal(list)
38 # threatListUpdates:fetch Content-Type: application/json POST 45 fullHashes = pyqtSignal(dict)
39 # fullHashes:find Content-Type: application/json POST
40 46
41 def __init__(self, apiKey, fairUse=True, parent=None): 47 def __init__(self, apiKey, fairUse=True, parent=None):
42 """ 48 """
43 Constructor 49 Constructor
44 50
51 """ 57 """
52 self.__apiKey = apiKey 58 self.__apiKey = apiKey
53 self.__fairUse = fairUse 59 self.__fairUse = fairUse
54 60
55 self.__nextRequestNoSoonerThan = QDateTime() 61 self.__nextRequestNoSoonerThan = QDateTime()
56 self.__replies = []
57 self.__failCount = 0 62 self.__failCount = 0
63
64 # get threat lists
65 self.__threatListsReply = None
66
67 # threats lists updates
68 self.__threatsUpdatesRequest = None
69 self.__threatsUpdateReply = None
70
71 # full hashes
72 self.__fullHashesRequest = None
73 self.__fullHashesReply = None
58 74
59 def getThreatLists(self): 75 def getThreatLists(self):
60 """ 76 """
61 Public method to retrieve all available threat lists. 77 Public method to retrieve all available threat lists.
62
63 @return threat lists
64 @rtype list of dictionaries
65 """ 78 """
66 url = QUrl(self.GsbUrlTemplate.format("threatLists", self.__apiKey)) 79 url = QUrl(self.GsbUrlTemplate.format("threatLists", self.__apiKey))
67 req = QNetworkRequest(url) 80 req = QNetworkRequest(url)
68 reply = WebBrowserWindow.networkManager().get(req) 81 reply = WebBrowserWindow.networkManager().get(req)
69 reply.finished.connect(self.__threatListsReceived) 82 reply.finished.connect(self.__threatListsReceived)
83 self.__threatListsReply = reply
70 84
71 @pyqtSlot() 85 @pyqtSlot()
72 def __threatListsReceived(self): 86 def __threatListsReceived(self):
73 """ 87 """
74 Private slot handling the threat lists. 88 Private slot handling the threat lists.
75 """ 89 """
76 reply = self.sender() 90 reply = self.sender()
77 result, hasError = self.__extractData(reply) 91 if reply is self.__threatListsReply:
78 if hasError: 92 self.__threatListsReply = None
79 # reschedule 93 result, hasError = self.__extractData(reply)
80 self.networkError.emit(reply.errorString()) 94 if hasError:
81 self.__reschedule(reply.error(), self.getThreatLists) 95 # reschedule
96 self.networkError.emit(reply.errorString())
97 self.__reschedule(reply.error(), self.getThreatLists)
98 else:
99 self.threatLists.emit(result["threatLists"])
100
101 reply.deleteLater()
102
103 def getThreatsUpdate(self, clientState=None):
104 """
105 Public method to fetch hash prefix updates for the given threat list.
106
107 @param clientState dictionary of client states with keys like
108 (threatType, platformType, threatEntryType)
109 @type dict
110 """
111 if self.__threatsUpdateReply is not None:
112 # update is in progress
113 return
114
115 if clientState is None:
116 if self.__threatsUpdatesRequest:
117 requestBody = self.__threatsUpdatesRequest
118 else:
119 return
82 else: 120 else:
83 self.__setWaitDuration(result.get("minimumWaitDuration")) 121 requestBody = {
84 self.threatLists.emit(result["threatLists"]) 122 "client": {
85 self.__failCount = 0 123 "clientId": self.ClientId,
86 124 "clientVersion": self.ClientVersion,
87 if reply in self.__replies: 125 },
88 self.__replies.remove(reply) 126 "listUpdateRequests": [],
89 reply.deleteLater() 127 }
128
129 for (threatType, platformType, threatEntryType), currentState in \
130 clientState.items():
131 requestBody["listUpdateRequests"].append(
132 {
133 "threatType": threatType,
134 "platformType": platformType,
135 "threatEntryType": threatEntryType,
136 "state": currentState,
137 "constraints": {
138 "supportedCompressions": ["RAW"],
139 }
140 }
141 )
142
143 self.__threatsUpdatesRequest = requestBody
144
145 data = QByteArray(json.dumps(requestBody).encode("utf-8"))
146 url = QUrl(self.GsbUrlTemplate.format("threatListUpdates:fetch",
147 self.__apiKey))
148 req = QNetworkRequest(url)
149 req.setHeader(QNetworkRequest.ContentTypeHeader, "application/json")
150 reply = WebBrowserWindow.networkManager().post(req, data)
151 reply.finished.connect(self.__threatsUpdateReceived)
152 self.__threatsUpdateReply = reply
153
154 @pyqtSlot()
155 def __threatsUpdateReceived(self):
156 """
157 Private slot handling the threats update.
158 """
159 reply = self.sender()
160 if reply is self.__threatsUpdateReply:
161 self.__threatsUpdateReply = None
162 result, hasError = self.__extractData(reply)
163 if hasError:
164 # reschedule
165 self.networkError.emit(reply.errorString())
166 self.__reschedule(reply.error(), self.getThreatsUpdate)
167 else:
168 self.__threatsUpdatesRequest = None
169 self.threatsUpdate.emit(result["listUpdateResponses"])
170
171 reply.deleteLater()
172
173 def getFullHashes(self, prefixes=None, clientState=None):
174 """
175 Public method to find full hashes matching hash prefixes.
176
177 @param prefixes list of hash prefixes to find
178 @type list of str (Python 2) or list of bytes (Python 3)
179 @param clientState dictionary of client states with keys like
180 (threatType, platformType, threatEntryType)
181 @type dict
182 """
183 if self.__fullHashesReply is not None:
184 # full hash request in progress
185 return
186
187 if prefixes is None or clientState is None:
188 if self.__fullHashesRequest:
189 requestBody = self.__fullHashesRequest
190 else:
191 return
192 else:
193 requestBody = {
194 "client": {
195 "clientId": self.ClientId,
196 "clientVersion": self.ClientVersion,
197 },
198 "clientStates": [],
199 "threatInfo": {
200 "threatTypes": [],
201 "platformTypes": [],
202 "threatEntryTypes": [],
203 "threatEntries": [],
204 },
205 }
206
207 for prefix in prefixes:
208 requestBody["threatInfo"]["threatEntries"].append(
209 {"hash": base64.b64encode(prefix).decode("ascii")})
210
211 for (threatType, platformType, threatEntryType), currentState in \
212 clientState.items():
213 requestBody["clientStates"].append(clientState)
214 if threatType not in requestBody["threatInfo"]["threatTypes"]:
215 requestBody["threatInfo"]["threatTypes"].append(threatType)
216 if platformType not in \
217 requestBody["threatInfo"]["platformTypes"]:
218 requestBody["threatInfo"]["platformTypes"].append(
219 platformType)
220 if threatEntryType not in \
221 requestBody["threatInfo"]["threatEntryTypes"]:
222 requestBody["threatInfo"]["threatEntryTypes"].append(
223 threatEntryType)
224
225 self.__fullHashesRequest = requestBody
226
227 data = QByteArray(json.dumps(requestBody).encode("utf-8"))
228 url = QUrl(self.GsbUrlTemplate.format("fullHashes:find",
229 self.__apiKey))
230 req = QNetworkRequest(url)
231 req.setHeader(QNetworkRequest.ContentTypeHeader, "application/json")
232 reply = WebBrowserWindow.networkManager().post(req, data)
233 reply.finished.connect(self.__fullHashesReceived)
234 self.__fullHashesReply = reply
235
236 @pyqtSlot()
237 def __fullHashesReceived(self):
238 """
239 Private slot handling the full hashes reply.
240 """
241 reply = self.sender()
242 if reply is self.__fullHashesReply:
243 self.__fullHashesReply = None
244 result, hasError = self.__extractData(reply)
245 if hasError:
246 # reschedule
247 self.networkError.emit(reply.errorString())
248 self.__reschedule(reply.error(), self.getFullHashes)
249 else:
250 self.__fullHashesRequest = None
251 self.fullHashes.emit(result)
252
253 reply.deleteLater()
90 254
91 def __extractData(self, reply): 255 def __extractData(self, reply):
92 """ 256 """
93 Private method to extract the data of a network reply. 257 Private method to extract the data of a network reply.
94 258
98 @type tuple of (list or dict, bool) 262 @type tuple of (list or dict, bool)
99 """ 263 """
100 if reply.error() != QNetworkReply.NoError: 264 if reply.error() != QNetworkReply.NoError:
101 return None, True 265 return None, True
102 266
267 self.__failCount = 0
103 result = json.loads(str(reply.readAll(), "utf-8")) 268 result = json.loads(str(reply.readAll(), "utf-8"))
269 self.__setWaitDuration(result.get("minimumWaitDuration"))
104 return result, False 270 return result, False
105 271
106 def __setWaitDuration(self, minimumWaitDuration): 272 def __setWaitDuration(self, minimumWaitDuration):
107 """ 273 """
108 Private method to set the minimum wait duration. 274 Private method to set the minimum wait duration.

eric ide

mercurial