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