WebBrowser/SafeBrowsing/SafeBrowsingAPIClient.py

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

eric ide

mercurial