2 * Copyright 2017-2023 Soren Stoutner <soren@stoutner.com>.
4 * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
6 * Privacy Browser Android 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.
11 * Privacy Browser Android 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.
16 * You should have received a copy of the GNU General Public License
17 * along with Privacy Browser Android. If not, see <http://www.gnu.org/licenses/>.
20 package com.stoutner.privacybrowser.backgroundtasks
22 import android.annotation.SuppressLint
23 import android.content.ContentResolver
24 import android.graphics.Typeface
25 import android.net.Uri
26 import android.text.SpannableStringBuilder
27 import android.text.Spanned
28 import android.text.style.StyleSpan
29 import android.webkit.CookieManager
31 import com.stoutner.privacybrowser.viewmodels.HeadersViewModel
33 import java.io.BufferedInputStream
34 import java.io.BufferedReader
35 import java.io.ByteArrayOutputStream
36 import java.io.IOException
37 import java.io.InputStream
38 import java.io.InputStreamReader
40 import java.net.HttpURLConnection
44 import java.security.SecureRandom
45 import java.security.cert.X509Certificate
47 import javax.net.ssl.HostnameVerifier
48 import javax.net.ssl.HttpsURLConnection
49 import javax.net.ssl.SSLContext
50 import javax.net.ssl.SSLSession
51 import javax.net.ssl.TrustManager
52 import javax.net.ssl.X509TrustManager
55 class GetHeadersBackgroundTask {
56 fun acquire(urlString: String, userAgent: String, localeString: String, proxy: Proxy, contentResolver: ContentResolver, headersViewModel: HeadersViewModel, ignoreSslErrors: Boolean):
57 Array<SpannableStringBuilder> {
59 // Initialize the spannable string builders.
60 val requestHeadersBuilder = SpannableStringBuilder()
61 val responseMessageBuilder = SpannableStringBuilder()
62 val responseHeadersBuilder = SpannableStringBuilder()
63 val responseBodyBuilder = SpannableStringBuilder()
65 if (urlString.startsWith("content://")) { // This is a content URL.
66 // Attempt to read the content data. Return an error if this fails.
68 // Get a URI for the content URL.
69 val contentUri = Uri.parse(urlString)
71 // Get a cursor with metadata about the content URL.
72 val contentCursor = contentResolver.query(contentUri, null, null, null, null)!!
74 // Move the content cursor to the first row.
75 contentCursor.moveToFirst()
77 // Populate the response header.
78 for (i in 0 until contentCursor.columnCount) {
79 // Add a new line if this is not the first entry.
81 responseHeadersBuilder.append(System.getProperty("line.separator"))
83 // Add each header to the string builder.
84 responseHeadersBuilder.append(contentCursor.getColumnName(i), StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
85 responseHeadersBuilder.append(": ")
86 responseHeadersBuilder.append(contentCursor.getString(i))
89 // Close the content cursor.
92 // Create a buffered string reader for the content data.
93 val bufferedReader = BufferedReader(InputStreamReader(contentResolver.openInputStream(contentUri)))
95 // Create a buffered string reader for the content data.
96 var contentLineString: String?
98 // Get the data from the buffered reader one line at a time.
99 while (bufferedReader.readLine().also { contentLineString = it } != null) {
100 // Add the line to the response body builder.
101 responseBodyBuilder.append(contentLineString)
103 // Append a new line.
104 responseBodyBuilder.append("\n")
106 } catch (exception: Exception) {
107 // Return the error message.
108 headersViewModel.returnError(exception.toString())
110 } else { // This is not a content URL.
111 // Because everything relating to requesting data from a webserver can throw errors, the entire section must catch `IOExceptions`.
113 // Get the current URL from the main activity.
114 val url = URL(urlString)
116 // Open a connection to the URL. No data is actually sent at this point.
117 val httpUrlConnection = url.openConnection(proxy) as HttpURLConnection
119 // Set the `Host` header property.
120 httpUrlConnection.setRequestProperty("Host", url.host)
122 // Add the `Host` header to the string builder and format the text.
123 requestHeadersBuilder.append("Host", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
124 requestHeadersBuilder.append(": ")
125 requestHeadersBuilder.append(url.host)
128 // Set the `Connection` header property.
129 httpUrlConnection.setRequestProperty("Connection", "keep-alive")
131 // Add the `Connection` header to the string builder and format the text.
132 requestHeadersBuilder.append(System.getProperty("line.separator"))
133 requestHeadersBuilder.append("Connection", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
134 requestHeadersBuilder.append(": keep-alive")
137 // Set the `Upgrade-Insecure-Requests` header property.
138 httpUrlConnection.setRequestProperty("Upgrade-Insecure-Requests", "1")
140 // Add the `Upgrade-Insecure-Requests` header to the string builder and format the text.
141 requestHeadersBuilder.append(System.getProperty("line.separator"))
142 requestHeadersBuilder.append("Upgrade-Insecure-Requests", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
143 requestHeadersBuilder.append(": 1")
146 // Set the `User-Agent` header property.
147 httpUrlConnection.setRequestProperty("User-Agent", userAgent)
149 // Add the `User-Agent` header to the string builder and format the text.
150 requestHeadersBuilder.append(System.getProperty("line.separator"))
151 requestHeadersBuilder.append("User-Agent", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
152 requestHeadersBuilder.append(": ")
153 requestHeadersBuilder.append(userAgent)
156 // Set the `Sec-Fetch-Site` header property.
157 httpUrlConnection.setRequestProperty("Sec-Fetch-Site", "none")
159 // Add the `Sec-Fetch-Site` header to the string builder and format the text.
160 requestHeadersBuilder.append(System.getProperty("line.separator"))
161 requestHeadersBuilder.append("Sec-Fetch-Site", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
162 requestHeadersBuilder.append(": none")
165 // Set the `Sec-Fetch-Mode` header property.
166 httpUrlConnection.setRequestProperty("Sec-Fetch-Mode", "navigate")
168 // Add the `Sec-Fetch-Mode` header to the string builder and format the text.
169 requestHeadersBuilder.append(System.getProperty("line.separator"))
170 requestHeadersBuilder.append("Sec-Fetch-Mode", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
171 requestHeadersBuilder.append(": navigate")
174 // Set the `Sec-Fetch-User` header property.
175 httpUrlConnection.setRequestProperty("Sec-Fetch-User", "?1")
177 // Add the `Sec-Fetch-User` header to the string builder and format the text.
178 requestHeadersBuilder.append(System.getProperty("line.separator"))
179 requestHeadersBuilder.append("Sec-Fetch-User", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
180 requestHeadersBuilder.append(": ?1")
183 // Set the `Accept` header property.
184 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")
186 // Add the `Accept` header to the string builder and format the text.
187 requestHeadersBuilder.append(System.getProperty("line.separator"))
188 requestHeadersBuilder.append("Accept", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
189 requestHeadersBuilder.append(": ")
190 requestHeadersBuilder.append("text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3")
193 // Set the `Accept-Language` header property.
194 httpUrlConnection.setRequestProperty("Accept-Language", localeString)
196 // Add the `Accept-Language` header to the string builder and format the text.
197 requestHeadersBuilder.append(System.getProperty("line.separator"))
198 requestHeadersBuilder.append("Accept-Language", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
199 requestHeadersBuilder.append(": ")
200 requestHeadersBuilder.append(localeString)
203 // Get the cookies for the current domain.
204 val cookiesString = CookieManager.getInstance().getCookie(url.toString())
206 // Only process the cookies if they are not null.
207 if (cookiesString != null) {
208 // Add the cookies to the header property.
209 httpUrlConnection.setRequestProperty("Cookie", cookiesString)
211 // Add the cookie header to the string builder and format the text.
212 requestHeadersBuilder.append(System.getProperty("line.separator"))
213 requestHeadersBuilder.append("Cookie", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
214 requestHeadersBuilder.append(": ")
215 requestHeadersBuilder.append(cookiesString)
219 // `HttpUrlConnection` sets `Accept-Encoding` to be `gzip` by default. If the property is manually set, than `HttpUrlConnection` does not process the decoding.
220 // Add the `Accept-Encoding` header to the string builder and format the text.
221 requestHeadersBuilder.append(System.getProperty("line.separator"))
222 requestHeadersBuilder.append("Accept-Encoding", StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
223 requestHeadersBuilder.append(": gzip")
225 // Ignore SSL errors if requested.
226 if (ignoreSslErrors) {
227 // Create a new host name verifier that allows all host names without checking for SSL errors.
228 val hostnameVerifier = HostnameVerifier { _: String?, _: SSLSession? -> true }
230 // Create a new trust manager. Lint wants to warn us that it is hard to securely implement an X509 trust manager.
231 // But the point of this trust manager is that it should accept all certificates no matter what, so that isn't an issue in our case.
232 @SuppressLint("CustomX509TrustManager") val trustManager = arrayOf<TrustManager>(
233 object : X509TrustManager {
234 @SuppressLint("TrustAllX509TrustManager")
235 override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {
236 // Do nothing, which trusts all client certificates.
239 @SuppressLint("TrustAllX509TrustManager")
240 override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {
241 // Do nothing, which trusts all server certificates.
244 override fun getAcceptedIssuers(): Array<X509Certificate>? {
250 // Get an SSL context. `TLS` provides a base instance available from API 1. <https://developer.android.com/reference/javax/net/ssl/SSLContext>
251 val sslContext = SSLContext.getInstance("TLS")
253 // Initialize the SSL context with the blank trust manager.
254 sslContext.init(null, trustManager, SecureRandom())
256 // Get the SSL socket factory with the blank trust manager.
257 val socketFactory = sslContext.socketFactory
259 // Set the HTTPS URL Connection to use the blank host name verifier.
260 (httpUrlConnection as HttpsURLConnection).hostnameVerifier = hostnameVerifier
262 // Set the HTTPS URL connection to use the socket factory with the blank trust manager.
263 httpUrlConnection.sslSocketFactory = socketFactory
266 // 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.
268 // Get the response code, which causes the connection to the server to be made.
269 val responseCode = httpUrlConnection.responseCode
271 // Populate the response message string builder.
272 responseMessageBuilder.append(responseCode.toString(), StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
273 responseMessageBuilder.append(": ")
274 responseMessageBuilder.append(httpUrlConnection.responseMessage)
276 // Initialize the iteration variable.
279 // Iterate through the received header fields.
280 while (httpUrlConnection.getHeaderField(i) != null) {
281 // Add a new line if there is already information in the string builder.
283 responseHeadersBuilder.append(System.getProperty("line.separator"))
285 // Add the header to the string builder and format the text.
286 responseHeadersBuilder.append(httpUrlConnection.getHeaderFieldKey(i), StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
287 responseHeadersBuilder.append(": ")
288 responseHeadersBuilder.append(httpUrlConnection.getHeaderField(i))
290 // Increment the iteration variable.
294 // Get the correct input stream based on the response code.
295 val inputStream: InputStream = if (responseCode == 404) // Get the error stream.
296 BufferedInputStream(httpUrlConnection.errorStream)
297 else // Get the response body stream.
298 BufferedInputStream(httpUrlConnection.inputStream)
300 // Initialize the byte array output stream and the conversion buffer byte array.
301 val byteArrayOutputStream = ByteArrayOutputStream()
302 val conversionBufferByteArray = ByteArray(1024)
304 // Define the buffer length variable.
305 var bufferLength: Int
308 // 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.
309 while (inputStream.read(conversionBufferByteArray).also { bufferLength = it } > 0) { // Proceed while the amount of data stored in the buffer is > 0.
310 // Write the contents of the conversion buffer to the byte array output stream.
311 byteArrayOutputStream.write(conversionBufferByteArray, 0, bufferLength)
313 } catch (exception: IOException) {
314 // Return the error message.
315 headersViewModel.returnError(exception.toString())
318 // Close the input stream.
321 // Populate the response body string with the contents of the byte array output stream.
322 responseBodyBuilder.append(byteArrayOutputStream.toString())
324 // Disconnect HTTP URL connection.
325 httpUrlConnection.disconnect()
327 } catch (exception: Exception) {
328 // Return the error message.
329 headersViewModel.returnError(exception.toString())
333 // Return the spannable string builders.
334 return arrayOf(requestHeadersBuilder, responseMessageBuilder, responseHeadersBuilder, responseBodyBuilder)