Handle content:// URLs in View Source. https://redmine.stoutner.com/issues/361
[PrivacyBrowser.git] / app / src / main / java / com / stoutner / privacybrowser / backgroundtasks / GetSourceBackgroundTask.java
1 /*
2  * Copyright © 2017-2021 Soren Stoutner <soren@stoutner.com>.
3  *
4  * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
5  *
6  * Privacy Browser is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * Privacy Browser is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with Privacy Browser.  If not, see <http://www.gnu.org/licenses/>.
18  */
19
20 package com.stoutner.privacybrowser.backgroundtasks;
21
22 import android.content.ContentResolver;
23 import android.database.Cursor;
24 import android.graphics.Typeface;
25 import android.net.Uri;
26 import android.os.Build;
27 import android.text.SpannableStringBuilder;
28 import android.text.Spanned;
29 import android.text.style.StyleSpan;
30 import android.webkit.CookieManager;
31
32 import com.stoutner.privacybrowser.viewmodels.WebViewSource;
33
34 import java.io.BufferedInputStream;
35 import java.io.BufferedReader;
36 import java.io.ByteArrayOutputStream;
37 import java.io.IOException;
38 import java.io.InputStream;
39 import java.io.InputStreamReader;
40 import java.net.HttpURLConnection;
41 import java.net.Proxy;
42 import java.net.URL;
43
44 public class GetSourceBackgroundTask {
45     public SpannableStringBuilder[] acquire(String urlString, String userAgent, String localeString, Proxy proxy, ContentResolver contentResolver, WebViewSource webViewSource) {
46         // Initialize the spannable string builders.
47         SpannableStringBuilder requestHeadersBuilder = new SpannableStringBuilder();
48         SpannableStringBuilder responseMessageBuilder = new SpannableStringBuilder();
49         SpannableStringBuilder responseHeadersBuilder = new SpannableStringBuilder();
50         SpannableStringBuilder responseBodyBuilder = new SpannableStringBuilder();
51
52         if (urlString.startsWith("content://")) {  // This is a content URL.
53             // Attempt to read the content data.  Return an error if this fails.
54             try {
55                 // Get a URI for the content URL.
56                 Uri contentUri = Uri.parse(urlString);
57
58                 // Define the variables necessary to build the response headers.
59                 int oldResponseHeadersBuilderLength;
60                 int newResponseHeadersBuilderLength;
61
62                 // Get a cursor with metadata about the content URL.
63                 Cursor contentCursor = contentResolver.query(contentUri, null, null, null, null);
64
65                 // Move the content cursor to the first row.
66                 contentCursor.moveToFirst();
67
68                 for (int i = 0; i < contentCursor.getColumnCount(); i++) {
69                     // Add a new line if this is not the first entry.
70                     if (i > 0) {
71                         responseHeadersBuilder.append(System.getProperty("line.separator"));
72                     }
73
74                     // Add each header to the string builder.
75                     if (Build.VERSION.SDK_INT >= 21) {  // Newer versions of Android are so smart.
76                         responseHeadersBuilder.append(contentCursor.getColumnName(i), new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
77                     } else {  // Older versions are not so much.
78                         oldResponseHeadersBuilderLength = responseHeadersBuilder.length();
79                         responseHeadersBuilder.append(contentCursor.getColumnName(i));
80                         newResponseHeadersBuilderLength = responseHeadersBuilder.length();
81                         responseHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldResponseHeadersBuilderLength, newResponseHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
82                     }
83                     responseHeadersBuilder.append(":  ");
84                     responseHeadersBuilder.append(contentCursor.getString(i));
85                 }
86
87                 // Close the content cursor.
88                 contentCursor.close();
89
90                 // Create a buffered string reader for the content data.
91                 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(contentResolver.openInputStream(contentUri)));
92
93                 // Get the data from the buffered reader one line at a time.
94                 for (String contentLineString; ((contentLineString = bufferedReader.readLine()) != null);) {
95                     // Add the line to the response body builder.
96                     responseBodyBuilder.append(contentLineString);
97
98                     // Append a new line.
99                     responseBodyBuilder.append("\n");
100                 }
101             } catch (Exception exception) {
102                 // Return the error message.
103                 webViewSource.returnError(exception.toString());
104             }
105         } else {  // This is not a content URL.
106             // Because everything relating to requesting data from a webserver can throw errors, the entire section must catch `IOExceptions`.
107             try {
108                 // Get the current URL from the main activity.
109                 URL url = new URL(urlString);
110
111                 // Open a connection to the URL.  No data is actually sent at this point.
112                 HttpURLConnection httpUrlConnection = (HttpURLConnection) url.openConnection(proxy);
113
114                 // Define the variables necessary to build the request headers.
115                 int oldRequestHeadersBuilderLength;
116                 int newRequestHeadersBuilderLength;
117
118
119                 // Set the `Host` header property.
120                 httpUrlConnection.setRequestProperty("Host", url.getHost());
121
122                 // Add the `Host` header to the string builder and format the text.
123                 if (Build.VERSION.SDK_INT >= 21) {  // Newer versions of Android are so smart.
124                     requestHeadersBuilder.append("Host", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
125                 } else {  // Older versions not so much.
126                     oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
127                     requestHeadersBuilder.append("Host");
128                     newRequestHeadersBuilderLength = requestHeadersBuilder.length();
129                     requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
130                 }
131                 requestHeadersBuilder.append(":  ");
132                 requestHeadersBuilder.append(url.getHost());
133
134
135                 // Set the `Connection` header property.
136                 httpUrlConnection.setRequestProperty("Connection", "keep-alive");
137
138                 // Add the `Connection` header to the string builder and format the text.
139                 requestHeadersBuilder.append(System.getProperty("line.separator"));
140                 if (Build.VERSION.SDK_INT >= 21) {  // Newer versions of Android are so smart.
141                     requestHeadersBuilder.append("Connection", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
142                 } else {  // Older versions not so much.
143                     oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
144                     requestHeadersBuilder.append("Connection");
145                     newRequestHeadersBuilderLength = requestHeadersBuilder.length();
146                     requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
147                 }
148                 requestHeadersBuilder.append(":  keep-alive");
149
150
151                 // Set the `Upgrade-Insecure-Requests` header property.
152                 httpUrlConnection.setRequestProperty("Upgrade-Insecure-Requests", "1");
153
154                 // Add the `Upgrade-Insecure-Requests` header to the string builder and format the text.
155                 requestHeadersBuilder.append(System.getProperty("line.separator"));
156                 if (Build.VERSION.SDK_INT >= 21) {  // Newer versions of Android are so smart.
157                     requestHeadersBuilder.append("Upgrade-Insecure-Requests", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
158                 } else {  // Older versions not so much.
159                     oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
160                     requestHeadersBuilder.append("Upgrade-Insecure_Requests");
161                     newRequestHeadersBuilderLength = requestHeadersBuilder.length();
162                     requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
163                 }
164                 requestHeadersBuilder.append(":  1");
165
166
167                 // Set the `User-Agent` header property.
168                 httpUrlConnection.setRequestProperty("User-Agent", userAgent);
169
170                 // Add the `User-Agent` header to the string builder and format the text.
171                 requestHeadersBuilder.append(System.getProperty("line.separator"));
172                 if (Build.VERSION.SDK_INT >= 21) {  // Newer versions of Android are so smart.
173                     requestHeadersBuilder.append("User-Agent", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
174                 } else {  // Older versions not so much.
175                     oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
176                     requestHeadersBuilder.append("User-Agent");
177                     newRequestHeadersBuilderLength = requestHeadersBuilder.length();
178                     requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
179                 }
180                 requestHeadersBuilder.append(":  ");
181                 requestHeadersBuilder.append(userAgent);
182
183
184                 // Set the `x-requested-with` header property.
185                 httpUrlConnection.setRequestProperty("x-requested-with", "");
186
187                 // Add the `x-requested-with` header to the string builder and format the text.
188                 requestHeadersBuilder.append(System.getProperty("line.separator"));
189                 if (Build.VERSION.SDK_INT >= 21) {  // Newer versions of Android are so smart.
190                     requestHeadersBuilder.append("x-requested-with", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
191                 } else {  // Older versions not so much.
192                     oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
193                     requestHeadersBuilder.append("x-requested-with");
194                     newRequestHeadersBuilderLength = requestHeadersBuilder.length();
195                     requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
196                 }
197                 requestHeadersBuilder.append(":  ");
198
199
200                 // Set the `Sec-Fetch-Site` header property.
201                 httpUrlConnection.setRequestProperty("Sec-Fetch-Site", "none");
202
203                 // Add the `Sec-Fetch-Site` header to the string builder and format the text.
204                 requestHeadersBuilder.append(System.getProperty("line.separator"));
205                 if (Build.VERSION.SDK_INT >= 21) {  // Newer versions of Android are so smart.
206                     requestHeadersBuilder.append("Sec-Fetch-Site", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
207                 } else {  // Older versions not so much.
208                     oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
209                     requestHeadersBuilder.append("Sec-Fetch-Site");
210                     newRequestHeadersBuilderLength = requestHeadersBuilder.length();
211                     requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
212                 }
213                 requestHeadersBuilder.append(":  none");
214
215
216                 // Set the `Sec-Fetch-Mode` header property.
217                 httpUrlConnection.setRequestProperty("Sec-Fetch-Mode", "navigate");
218
219                 // Add the `Sec-Fetch-Mode` header to the string builder and format the text.
220                 requestHeadersBuilder.append(System.getProperty("line.separator"));
221                 if (Build.VERSION.SDK_INT >= 21) {  // Newer versions of Android are so smart.
222                     requestHeadersBuilder.append("Sec-Fetch-Mode", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
223                 } else {  // Older versions not so much.
224                     oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
225                     requestHeadersBuilder.append("Sec-Fetch-Mode");
226                     newRequestHeadersBuilderLength = requestHeadersBuilder.length();
227                     requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
228                 }
229                 requestHeadersBuilder.append(":  navigate");
230
231
232                 // Set the `Sec-Fetch-User` header property.
233                 httpUrlConnection.setRequestProperty("Sec-Fetch-User", "?1");
234
235                 // Add the `Sec-Fetch-User` header to the string builder and format the text.
236                 requestHeadersBuilder.append(System.getProperty("line.separator"));
237                 if (Build.VERSION.SDK_INT >= 21) {  // Newer versions of Android are so smart.
238                     requestHeadersBuilder.append("Sec-Fetch-User", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
239                 } else {  // Older versions not so much.
240                     oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
241                     requestHeadersBuilder.append("Sec-Fetch-User");
242                     newRequestHeadersBuilderLength = requestHeadersBuilder.length();
243                     requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
244                 }
245                 requestHeadersBuilder.append(":  ?1");
246
247
248                 // Set the `Accept` header property.
249                 httpUrlConnection.setRequestProperty("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3");
250
251                 // Add the `Accept` header to the string builder and format the text.
252                 requestHeadersBuilder.append(System.getProperty("line.separator"));
253                 if (Build.VERSION.SDK_INT >= 21) {  // Newer versions of Android are so smart.
254                     requestHeadersBuilder.append("Accept", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
255                 } else {  // Older versions not so much.
256                     oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
257                     requestHeadersBuilder.append("Accept");
258                     newRequestHeadersBuilderLength = requestHeadersBuilder.length();
259                     requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
260                 }
261                 requestHeadersBuilder.append(":  ");
262                 requestHeadersBuilder.append("text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3");
263
264
265                 // Set the `Accept-Language` header property.
266                 httpUrlConnection.setRequestProperty("Accept-Language", localeString);
267
268                 // Add the `Accept-Language` header to the string builder and format the text.
269                 requestHeadersBuilder.append(System.getProperty("line.separator"));
270                 if (Build.VERSION.SDK_INT >= 21) {  // Newer versions of Android are so smart.
271                     requestHeadersBuilder.append("Accept-Language", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
272                 } else {  // Older versions not so much.
273                     oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
274                     requestHeadersBuilder.append("Accept-Language");
275                     newRequestHeadersBuilderLength = requestHeadersBuilder.length();
276                     requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
277                 }
278                 requestHeadersBuilder.append(":  ");
279                 requestHeadersBuilder.append(localeString);
280
281
282                 // Get the cookies for the current domain.
283                 String cookiesString = CookieManager.getInstance().getCookie(url.toString());
284
285                 // Only process the cookies if they are not null.
286                 if (cookiesString != null) {
287                     // Add the cookies to the header property.
288                     httpUrlConnection.setRequestProperty("Cookie", cookiesString);
289
290                     // Add the cookie header to the string builder and format the text.
291                     requestHeadersBuilder.append(System.getProperty("line.separator"));
292                     if (Build.VERSION.SDK_INT >= 21) {  // Newer versions of Android are so smart.
293                         requestHeadersBuilder.append("Cookie", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
294                     } else {  // Older versions not so much.
295                         oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
296                         requestHeadersBuilder.append("Cookie");
297                         newRequestHeadersBuilderLength = requestHeadersBuilder.length();
298                         requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
299                     }
300                     requestHeadersBuilder.append(":  ");
301                     requestHeadersBuilder.append(cookiesString);
302                 }
303
304
305                 // `HttpUrlConnection` sets `Accept-Encoding` to be `gzip` by default.  If the property is manually set, than `HttpUrlConnection` does not process the decoding.
306                 // Add the `Accept-Encoding` header to the string builder and format the text.
307                 requestHeadersBuilder.append(System.getProperty("line.separator"));
308                 if (Build.VERSION.SDK_INT >= 21) {  // Newer versions of Android are so smart.
309                     requestHeadersBuilder.append("Accept-Encoding", new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
310                 } else {  // Older versions not so much.
311                     oldRequestHeadersBuilderLength = requestHeadersBuilder.length();
312                     requestHeadersBuilder.append("Accept-Encoding");
313                     newRequestHeadersBuilderLength = requestHeadersBuilder.length();
314                     requestHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldRequestHeadersBuilderLength, newRequestHeadersBuilderLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
315                 }
316                 requestHeadersBuilder.append(":  gzip");
317
318
319                 // The actual network request is in a `try` bracket so that `disconnect()` is run in the `finally` section even if an error is encountered in the main block.
320                 try {
321                     // Initialize the string builders.
322                     responseMessageBuilder = new SpannableStringBuilder();
323                     responseHeadersBuilder = new SpannableStringBuilder();
324
325                     // Get the response code, which causes the connection to the server to be made.
326                     int responseCode = httpUrlConnection.getResponseCode();
327
328                     // Populate the response message string builder.
329                     if (Build.VERSION.SDK_INT >= 21) {  // Newer versions of Android are so smart.
330                         responseMessageBuilder.append(String.valueOf(responseCode), new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
331                     } else {  // Older versions not so much.
332                         responseMessageBuilder.append(String.valueOf(responseCode));
333                         int newLength = responseMessageBuilder.length();
334                         responseMessageBuilder.setSpan(new StyleSpan(Typeface.BOLD), 0, newLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
335                     }
336                     responseMessageBuilder.append(":  ");
337                     responseMessageBuilder.append(httpUrlConnection.getResponseMessage());
338
339                     // Initialize the iteration variable.
340                     int i = 0;
341
342                     // Iterate through the received header fields.
343                     while (httpUrlConnection.getHeaderField(i) != null) {
344                         // Add a new line if there is already information in the string builder.
345                         if (i > 0) {
346                             responseHeadersBuilder.append(System.getProperty("line.separator"));
347                         }
348
349                         // Add the header to the string builder and format the text.
350                         if (Build.VERSION.SDK_INT >= 21) {  // Newer versions of Android are so smart.
351                             responseHeadersBuilder.append(httpUrlConnection.getHeaderFieldKey(i), new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
352                         } else {  // Older versions not so much.
353                             int oldLength = responseHeadersBuilder.length();
354                             responseHeadersBuilder.append(httpUrlConnection.getHeaderFieldKey(i));
355                             int newLength = responseHeadersBuilder.length();
356                             responseHeadersBuilder.setSpan(new StyleSpan(Typeface.BOLD), oldLength, newLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
357                         }
358                         responseHeadersBuilder.append(":  ");
359                         responseHeadersBuilder.append(httpUrlConnection.getHeaderField(i));
360
361                         // Increment the iteration variable.
362                         i++;
363                     }
364
365                     // Instantiate an input stream for the response body.
366                     InputStream inputStream;
367
368                     // Get the correct input stream based on the response code.
369                     if (responseCode == 404) {  // Get the error stream.
370                         inputStream = new BufferedInputStream(httpUrlConnection.getErrorStream());
371                     } else {  // Get the response body stream.
372                         inputStream = new BufferedInputStream(httpUrlConnection.getInputStream());
373                     }
374
375                     // Initialize the byte array output stream and the conversion buffer byte array.
376                     ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
377                     byte[] conversionBufferByteArray = new byte[1024];
378
379                     // Define the buffer length variable.
380                     int bufferLength;
381
382                     try {
383                         // Attempt to read data from the input stream and store it in the conversion buffer byte array.  Also store the amount of data read in the buffer length variable.
384                         while ((bufferLength = inputStream.read(conversionBufferByteArray)) > 0) {  // Proceed while the amount of data stored in the buffer is > 0.
385                             // Write the contents of the conversion buffer to the byte array output stream.
386                             byteArrayOutputStream.write(conversionBufferByteArray, 0, bufferLength);
387                         }
388                     } catch (IOException exception) {
389                         // Return the error message.
390                         webViewSource.returnError(exception.toString());
391                     }
392
393                     // Close the input stream.
394                     inputStream.close();
395
396                     // Populate the response body string with the contents of the byte array output stream.
397                     responseBodyBuilder.append(byteArrayOutputStream.toString());
398                 } finally {
399                     // Disconnect HTTP URL connection.
400                     httpUrlConnection.disconnect();
401                 }
402             } catch (Exception exception) {
403                 // Return the error message.
404                 webViewSource.returnError(exception.toString());
405             }
406         }
407
408         // Return the response body string as the result.
409         return new SpannableStringBuilder[] {requestHeadersBuilder, responseMessageBuilder, responseHeadersBuilder, responseBodyBuilder};
410     }
411 }