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 |