|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2017 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing the interface for Google Safe Browsing. |
|
8 """ |
|
9 |
|
10 # |
|
11 # Some part of this code were ported from gglsbl.client and adapted |
|
12 # to Qt. |
|
13 # |
|
14 # https://github.com/afilipovich/gglsbl |
|
15 # |
|
16 |
|
17 from __future__ import unicode_literals |
|
18 |
|
19 import os |
|
20 import base64 |
|
21 |
|
22 from PyQt5.QtCore import QObject |
|
23 |
|
24 import Preferences |
|
25 import Utilities |
|
26 |
|
27 from .SafeBrowsingAPIClient import SafeBrowsingAPIClient |
|
28 from .SafeBrowsingCache import SafeBrowsingCache, ThreatList, HashPrefixList |
|
29 |
|
30 |
|
31 class SafeBrowsingManager(QObject): |
|
32 """ |
|
33 Class implementing the interface for Google Safe Browsing. |
|
34 """ |
|
35 def __init__(self): |
|
36 """ |
|
37 Constructor |
|
38 """ |
|
39 super(SafeBrowsingManager, self).__init__() |
|
40 |
|
41 self.__apiKey = Preferences.getWebBrowser("SafeBrowsingApiKey") |
|
42 if self.__apiKey: |
|
43 ## self.__apiClient = SafeBrowsingAPIClient(self.__apiKey, |
|
44 ## parent=self) |
|
45 # TODO: switch these after debugging is finished |
|
46 self.__apiClient = SafeBrowsingAPIClient(self.__apiKey, |
|
47 parent=self, |
|
48 fairUse=False) |
|
49 else: |
|
50 self.__apiClient = None |
|
51 |
|
52 self.__enabled = ( |
|
53 Preferences.getWebBrowser("SafeBrowsingEnabled") and |
|
54 bool(self.__apiKey)) |
|
55 |
|
56 gsbCachePath = os.path.join( |
|
57 Utilities.getConfigDir(), "web_browser", "safe_browsing") |
|
58 self.__cache = SafeBrowsingCache(gsbCachePath, self) |
|
59 |
|
60 self.__gsbDialog = None |
|
61 self.__platforms = None # TODO: delete if not needed |
|
62 |
|
63 def configurationChanged(self): |
|
64 """ |
|
65 Public method to handle changes of the settings. |
|
66 """ |
|
67 apiKey = Preferences.getWebBrowser("SafeBrowsingApiKey") |
|
68 if apiKey != self.__apiKey: |
|
69 self.__apiKey = apiKey |
|
70 if self.__apiKey: |
|
71 if self.__apiClient: |
|
72 self.__apiClient.setApiKey(self.__apiKey) |
|
73 else: |
|
74 ## self.__apiClient = SafeBrowsingAPIClient(self.__apiKey, |
|
75 ## parent=self) |
|
76 # TODO: switch these after debugging is finished |
|
77 self.__apiClient = SafeBrowsingAPIClient(self.__apiKey, |
|
78 parent=self, |
|
79 fairUse=False) |
|
80 |
|
81 self.__enabled = ( |
|
82 Preferences.getWebBrowser("SafeBrowsingEnabled") and |
|
83 bool(self.__apiKey)) |
|
84 |
|
85 def isEnabled(self): |
|
86 """ |
|
87 Public method to check, if safe browsing is enabled. |
|
88 |
|
89 @return flag indicating the enabled state |
|
90 @rtype bool |
|
91 """ |
|
92 return self.__enabled |
|
93 |
|
94 def close(self): |
|
95 """ |
|
96 Public method to close the safe browsing interface. |
|
97 """ |
|
98 self.__cache.close() |
|
99 |
|
100 def fairUseDelayExpired(self): |
|
101 """ |
|
102 Public method to check, if the fair use wait period has expired. |
|
103 |
|
104 @return flag indicating expiration |
|
105 @rtype bool |
|
106 """ |
|
107 return self.__enabled and self.__apiClient.fairUseDelayExpired() |
|
108 |
|
109 def updateHashPrefixCache(self): |
|
110 """ |
|
111 Public method to load or update the locally cached threat lists. |
|
112 |
|
113 @return flag indicating success and an error message |
|
114 @rtype tuple of (bool, str) |
|
115 """ |
|
116 if not self.__enabled: |
|
117 return False, self.tr("Safe Browsing is disabled.") |
|
118 |
|
119 if not self.__apiClient.fairUseDelayExpired(): |
|
120 return False, \ |
|
121 self.tr("The fair use wait period has not expired yet.") |
|
122 |
|
123 # step 1: remove expired hashes |
|
124 self.__cache.cleanupFullHashes() |
|
125 |
|
126 # step 2: update threat lists |
|
127 threatListsForRemove = {} |
|
128 for threatList, clientState in self.__cache.getThreatLists(): |
|
129 threatListsForRemove[repr(threatList)] = threatList |
|
130 threatLists = self.__apiClient.getThreatLists() |
|
131 for entry in threatLists: |
|
132 threatList = ThreatList.fromApiEntry(entry) |
|
133 if self.__platforms is None or \ |
|
134 threatList.platformType in self.__platforms: |
|
135 self.__cache.addThreatList(threatList) |
|
136 key = repr(threatList) |
|
137 if key in threatListsForRemove: |
|
138 del threatListsForRemove[key] |
|
139 for threatList in threatListsForRemove.values(): |
|
140 self.__cache.deleteHashPrefixList(threatList) |
|
141 self.__cache.deleteThreatList(threatList) |
|
142 del threatListsForRemove |
|
143 |
|
144 # step 3: update threats |
|
145 threatLists = self.__cache.getThreatLists() |
|
146 clientStates = {} |
|
147 for threatList, clientState in threatLists: |
|
148 clientStates[threatList.asTuple()] = clientState |
|
149 threatsUpdateResponses = \ |
|
150 self.__apiClient.getThreatsUpdate(clientStates) |
|
151 for response in threatsUpdateResponses: |
|
152 responseThreatList = ThreatList.fromApiEntry(response) |
|
153 if response["responseType"] == "FULL_UPDATE": |
|
154 self.__cache.deleteHashPrefixList(responseThreatList) |
|
155 for removal in response.get("removals", []): |
|
156 self.__cache.removeHashPrefixIndices( |
|
157 responseThreatList, removal["rawIndices"]["indices"]) |
|
158 for addition in response.get("additions", []): |
|
159 hashPrefixList = HashPrefixList( |
|
160 addition["rawHashes"]["prefixSize"], |
|
161 base64.b64decode(addition["rawHashes"]["rawHashes"])) |
|
162 self.__cache.populateHashPrefixList(responseThreatList, |
|
163 hashPrefixList) |
|
164 expectedChecksum = base64.b64decode(response["checksum"]["sha256"]) |
|
165 if self.__verifyThreatListChecksum(responseThreatList, |
|
166 expectedChecksum): |
|
167 self.__cache.updateThreatListClientState( |
|
168 responseThreatList, response["newClientState"]) |
|
169 else: |
|
170 return False, \ |
|
171 self.tr("Local cache checksum does not match the server." |
|
172 " Consider cleaning the cache. Threat update has" |
|
173 " been aborted.") |
|
174 |
|
175 return True, "" |
|
176 |
|
177 def __verifyThreatListChecksum(self, threatList, remoteChecksum): |
|
178 """ |
|
179 Private method to verify the local checksum of a threat list with the |
|
180 checksum of the safe browsing server. |
|
181 """ |
|
182 localChecksum = self.__cache.hashPrefixListChecksum(threatList) |
|
183 return remoteChecksum == localChecksum |
|
184 |
|
185 def fullCacheCleanup(self): |
|
186 """ |
|
187 Public method to clean up the cache completely. |
|
188 """ |
|
189 self.__cache.prepareCacheDb() |
|
190 |
|
191 def showSafeBrowsingDialog(self): |
|
192 """ |
|
193 Public slot to show the safe browsing management dialog. |
|
194 """ |
|
195 if self.__gsbDialog is None: |
|
196 from WebBrowser.WebBrowserWindow import WebBrowserWindow |
|
197 from .SafeBrowsingDialog import SafeBrowsingDialog |
|
198 self.__gsbDialog = SafeBrowsingDialog( |
|
199 self, parent=WebBrowserWindow.mainWindow()) |
|
200 |
|
201 self.__gsbDialog.show() |