|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2016 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a class to handle URL requests before they get processed |
|
8 by QtWebEngine. |
|
9 """ |
|
10 |
|
11 from PyQt5.QtCore import QMutex, QUrl |
|
12 from PyQt5.QtWebEngineCore import ( |
|
13 QWebEngineUrlRequestInterceptor, QWebEngineUrlRequestInfo |
|
14 ) |
|
15 |
|
16 from E5Utilities.E5MutexLocker import E5MutexLocker |
|
17 |
|
18 from ..WebBrowserPage import WebBrowserPage |
|
19 |
|
20 import Preferences |
|
21 |
|
22 |
|
23 class NetworkUrlInterceptor(QWebEngineUrlRequestInterceptor): |
|
24 """ |
|
25 Class implementing an URL request handler. |
|
26 """ |
|
27 def __init__(self, parent=None): |
|
28 """ |
|
29 Constructor |
|
30 |
|
31 @param parent reference to the parent object |
|
32 @type QObject |
|
33 """ |
|
34 super().__init__(parent) |
|
35 |
|
36 self.__interceptors = [] |
|
37 self.__mutex = QMutex() |
|
38 |
|
39 self.__loadSettings() |
|
40 |
|
41 def interceptRequest(self, info): |
|
42 """ |
|
43 Public method handling an URL request. |
|
44 |
|
45 @param info URL request information |
|
46 @type QWebEngineUrlRequestInfo |
|
47 """ |
|
48 with E5MutexLocker(self.__mutex): |
|
49 # Do Not Track feature |
|
50 if self.__doNotTrack: |
|
51 info.setHttpHeader(b"DNT", b"1") |
|
52 info.setHttpHeader(b"X-Do-Not-Track", b"1") |
|
53 |
|
54 # Send referrer header? |
|
55 if info.requestUrl().host() not in Preferences.getWebBrowser( |
|
56 "SendRefererWhitelist"): |
|
57 self.__setRefererHeader(info) |
|
58 |
|
59 # User Agents header |
|
60 userAgent = WebBrowserPage.userAgentForUrl(info.requestUrl()) |
|
61 info.setHttpHeader(b"User-Agent", userAgent.encode()) |
|
62 |
|
63 for interceptor in self.__interceptors: |
|
64 interceptor.interceptRequest(info) |
|
65 |
|
66 def installUrlInterceptor(self, interceptor): |
|
67 """ |
|
68 Public method to install an URL interceptor. |
|
69 |
|
70 @param interceptor URL interceptor to be installed |
|
71 @type UrlInterceptor |
|
72 """ |
|
73 with E5MutexLocker(self.__mutex): |
|
74 if interceptor not in self.__interceptors: |
|
75 self.__interceptors.append(interceptor) |
|
76 |
|
77 def removeUrlInterceptor(self, interceptor): |
|
78 """ |
|
79 Public method to remove an URL interceptor. |
|
80 |
|
81 @param interceptor URL interceptor to be removed |
|
82 @type UrlInterceptor |
|
83 """ |
|
84 with E5MutexLocker(self.__mutex): |
|
85 if interceptor in self.__interceptors: |
|
86 self.__interceptors.remove(interceptor) |
|
87 |
|
88 def __loadSettings(self): |
|
89 """ |
|
90 Private method to load the Network Manager settings. |
|
91 """ |
|
92 with E5MutexLocker(self.__mutex): |
|
93 self.__doNotTrack = Preferences.getWebBrowser( |
|
94 "DoNotTrack") |
|
95 self.__sendReferer = Preferences.getWebBrowser( |
|
96 "RefererSendReferer") |
|
97 self.__refererDefaultPolicy = Preferences.getWebBrowser( |
|
98 "RefererDefaultPolicy") |
|
99 self.__refererTrimmingPolicy = Preferences.getWebBrowser( |
|
100 "RefererTrimmingPolicy") |
|
101 |
|
102 def preferencesChanged(self): |
|
103 """ |
|
104 Public slot to handle a change of preferences. |
|
105 """ |
|
106 self.__loadSettings() |
|
107 |
|
108 def __setRefererHeader(self, info): |
|
109 """ |
|
110 Private method to set the 'Referer' header depending on the configured |
|
111 rule set. |
|
112 |
|
113 @param info URL request information |
|
114 @type QWebEngineUrlRequestInfo |
|
115 @see <a href="https://wiki.mozilla.org/Security/Referrer"> |
|
116 Mozilla Referrer</a> |
|
117 @see <a href="https://www.w3.org/TR/referrer-policy/"> |
|
118 W3C Referrer Policy</a> |
|
119 """ |
|
120 # 1. SendReferer: |
|
121 # 0 = never |
|
122 # 1 = only on click (NavigationTypeLink) |
|
123 # 2 = always (default) |
|
124 # 2. RefererTrimmingPolicy: |
|
125 # 0 = send full URL (no trimming) (default) |
|
126 # 1 = send the URL without its query string |
|
127 # 2 = only send the origin (ensure trailing /) |
|
128 # 3. RefererDefaultPolicy: |
|
129 # set the default referrer policy (which can be overriden by |
|
130 # the site) |
|
131 # 0 = no-referrer |
|
132 # 1 = same-origin |
|
133 # 2 = strict-origin-when-cross-origin |
|
134 # 3 = no-referrer-when-downgrade (default) |
|
135 # see: https://wiki.mozilla.org/Security/Referrer |
|
136 # see: https://www.w3.org/TR/referrer-policy/ |
|
137 |
|
138 if ( |
|
139 self.__sendReferer == 0 or |
|
140 # never send referer header |
|
141 |
|
142 (self.__sendReferer == 1 and |
|
143 (info.navigationType() != |
|
144 QWebEngineUrlRequestInfo.NavigationType.NavigationTypeLink)) |
|
145 # send referer header only on click |
|
146 ): |
|
147 info.setHttpHeader(b"Referer", b"") |
|
148 else: |
|
149 # send referer header always applying further policies |
|
150 url = info.firstPartyUrl() |
|
151 reqUrl = info.requestUrl() |
|
152 if self.__refererDefaultPolicy == 0: |
|
153 # no-referrer |
|
154 refererUrl = b"" |
|
155 elif self.__refererDefaultPolicy == 1: |
|
156 # same-origin |
|
157 if self.__sameOrigin(url, reqUrl): |
|
158 refererUrl = self.__trimmedReferer(url) |
|
159 else: |
|
160 refererUrl = b"" |
|
161 elif self.__refererDefaultPolicy == 2: |
|
162 # strict-origin-when-cross-origin |
|
163 if self.__sameOrigin(url, reqUrl): |
|
164 refererUrl = self.__trimmedReferer(url) |
|
165 elif url.scheme() in ("https", "wss"): |
|
166 if self.__potentiallyTrustworthy(url): |
|
167 refererUrl = self.__refererOrigin(url) |
|
168 else: |
|
169 refererUrl = b"" |
|
170 else: |
|
171 refererUrl = self.__refererOrigin(url) |
|
172 else: |
|
173 # no-referrer-when-downgrade |
|
174 if ( |
|
175 url.scheme() in ("https", "wss") and |
|
176 not self.__potentiallyTrustworthy(url) |
|
177 ): |
|
178 refererUrl = b"" |
|
179 else: |
|
180 refererUrl = self.__trimmedReferer(url) |
|
181 |
|
182 info.setHttpHeader(b"Referer", refererUrl) |
|
183 |
|
184 def __sameOrigin(self, url1, url2): |
|
185 """ |
|
186 Private method to test the "same origin" policy. |
|
187 |
|
188 @param url1 first URL for the test |
|
189 @type QUrl |
|
190 @param url2 second URL for the test |
|
191 @type QUrl |
|
192 @return flag indicating that both URLs have the same origin |
|
193 @rtype bool |
|
194 """ |
|
195 origin1 = url1.url(QUrl.UrlFormattingOption.RemoveUserInfo | |
|
196 QUrl.UrlFormattingOption.RemovePath) |
|
197 origin2 = url2.url(QUrl.UrlFormattingOption.RemoveUserInfo | |
|
198 QUrl.UrlFormattingOption.RemovePath) |
|
199 |
|
200 return origin1 == origin2 |
|
201 |
|
202 def __potentiallyTrustworthy(self, url): |
|
203 """ |
|
204 Private method to check, if the given URL is potentially trustworthy. |
|
205 |
|
206 @param url URL to be checked |
|
207 @type QUrl |
|
208 @return flag indicating a potentially trustworthy URL |
|
209 @rtype bool |
|
210 """ |
|
211 if url.scheme() == "data": |
|
212 return False |
|
213 |
|
214 if url.toString() in ("about:blank", "about:srcdoc"): |
|
215 return True |
|
216 |
|
217 origin = url.adjusted(QUrl.UrlFormattingOption.RemoveUserInfo | |
|
218 QUrl.UrlFormattingOption.RemovePath) |
|
219 |
|
220 if origin.isEmpty() or origin.scheme() == "": |
|
221 return False |
|
222 if origin.scheme() in ("https", "wss"): |
|
223 return True |
|
224 if origin.host().startswith("127.") or origin.host().endswith(":1"): |
|
225 return True |
|
226 if ( |
|
227 origin.host() == "localhost" or |
|
228 origin.host().endswith(".localhost") |
|
229 ): |
|
230 return True |
|
231 if origin.scheme() == "file": |
|
232 return True |
|
233 if origin.scheme() in ("qrc", "qthelp", "eric"): |
|
234 return True |
|
235 |
|
236 return False |
|
237 |
|
238 def __trimmedReferer(self, url): |
|
239 """ |
|
240 Private method to generate the trimmed referer header URL. |
|
241 |
|
242 @param url URL to be trimmed as a referer header |
|
243 @type QUrl |
|
244 @return trimmed referer header URL |
|
245 @rtype QByteArray or bytes |
|
246 """ |
|
247 if self.__refererTrimmingPolicy == 0: |
|
248 # send full URL (no trimming) (default) |
|
249 refererUrl = url.toEncoded( |
|
250 QUrl.UrlFormattingOption.RemoveUserInfo | |
|
251 QUrl.UrlFormattingOption.RemoveFragment |
|
252 ) |
|
253 elif self.__refererTrimmingPolicy == 1: |
|
254 # send the URL without its query string |
|
255 refererUrl = url.toEncoded( |
|
256 QUrl.UrlFormattingOption.RemoveUserInfo | |
|
257 QUrl.UrlFormattingOption.RemoveFragment | |
|
258 QUrl.UrlFormattingOption.RemoveQuery |
|
259 ) |
|
260 else: |
|
261 # only send the origin (ensure trailing /) |
|
262 refererUrl = self.__refererOrigin(url) |
|
263 |
|
264 return refererUrl |
|
265 |
|
266 def __refererOrigin(self, url): |
|
267 """ |
|
268 Private method to generate an origin referer header URL. |
|
269 |
|
270 @param url URL to generate the header from |
|
271 @type QUrl |
|
272 @return origin referer header URL |
|
273 @rtype QByteArray or bytes |
|
274 """ |
|
275 referer = url.toEncoded( |
|
276 QUrl.UrlFormattingOption.RemoveUserInfo | |
|
277 QUrl.UrlFormattingOption.RemovePath |
|
278 ) |
|
279 if not referer.endsWith(b"/"): |
|
280 referer += b"/" |
|
281 |
|
282 return referer |