Skip to content

Commit

Permalink
Add CSP parser.
Browse files Browse the repository at this point in the history
Store Response Headers for use in Reader-Mode or elsewhere.
Copy page's CSP meta tag into Reader-Mode page.
Encode ReaderMode headers in the URL
  • Loading branch information
Brandon-T committed Apr 11, 2024
1 parent e4ecdb5 commit 05bb754
Show file tree
Hide file tree
Showing 11 changed files with 136 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,9 @@ extension BrowserViewController {
let forwardList = webView.backForwardList.forwardList

guard let currentURL = webView.backForwardList.currentItem?.url,
let readerModeURL = currentURL.encodeEmbeddedInternalURL(for: .readermode)
let headers = (tab.responses[currentURL] as? HTTPURLResponse)?.allHeaderFields
as? [String: String],
let readerModeURL = currentURL.encodeEmbeddedInternalURL(for: .readermode, headers: headers)
else { return }

recordTimeBasedNumberReaderModeUsedP3A(activated: true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,11 @@ extension BrowserViewController: WKNavigationDelegate {
let responseURL = response.url
let tab = tab(for: webView)

// Store the response in the tab
if let responseURL = responseURL {
tab?.responses[responseURL] = response
}

// Check if we upgraded to https and if so we need to update the url of frame evaluations
if let responseURL = responseURL,
let domain = tab?.currentPageData?.domain(persistent: !isPrivateBrowsing),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,92 @@ public class ReaderModeHandler: InternalSchemeResponse {
return nil
}

// Decode the original page's response headers
var headers = [String: String]()
if let base64EncodedHeaders = _url.getQuery()["headers"]?.unescape(),
let data = Data(base64Encoded: base64EncodedHeaders),
let decodedHeaders = try? JSONSerialization.jsonObject(with: data) as? [String: String]
{
headers = decodedHeaders
}

headers = headers.filter({
let key = $0.key.lowercased()

// These are the only headers kept from the original page
return key == "access-control-allow-origin" || key == "content-security-policy"
|| key == "strict-transport-security" || key == "content-language"
})

// Tighten security by adding some of our own headers
headers["X-Frame-Options"] = "DENY"
headers["X-Content-Type-Options"] = "nosniff"
headers["Referrer-Policy"] = "no-referrer"
headers["Cache-Control"] = "private, s-maxage=0, max-age=0, must-revalidate"

// Add Generic headers
headers["Content-Type"] = "text/html; charset=UTF-8"

// Handle CSP header
// Must generate a unique nonce, every single time as per Content-Policy spec.
let setTitleNonce = UUID().uuidString.replacingOccurrences(of: "-", with: "")

// Create our own CSPs
var policies = [
("default-src", "'none'"),
("base-uri", "'none'"),
("form-action", "'none'"),
("frame-ancestors", "'none'"),
//("sandbox", ""), // Do not enable `sandbox` as it causes `Wikipedia` to not work and possibly other pages
("upgrade-insecure-requests", "1"),
("img-src", "*"),
("style-src", "\(InternalURL.baseUrl) '\(ReaderModeHandler.readerModeStyleHash)'"),
("font-src", "\(InternalURL.baseUrl)"),
("script-src", "'nonce-\(setTitleNonce)'"),
]

// Parse CSP Header
if let originalCSP = headers.first(where: { $0.key.lowercased() == "content-security-policy" })?
.value
{
var originalPolicies = [(String, String)]()
for policy in originalCSP.components(separatedBy: ";") {
let components = policy.components(separatedBy: " ")
if components.count == 1 {
originalPolicies.append((policy, ""))
} else {
let key = components[0]
let value = components[1...].joined(separator: " ")
originalPolicies.append((key, value))
}
}

// Remove unwanted policies
originalPolicies.removeAll(where: { key, _ in
key == "report-uri" || key == "report-to"
})

if originalPolicies.contains(where: { key, _ in key == "img-src" }) {
policies.removeAll(where: { key, _ in key == "img-src" })
}

// Add original CSPs onto our own
policies.append(contentsOf: originalPolicies)
}

headers["Content-Security-Policy"] = String(
policies.map({ (key, value) in
return value.isEmpty ? "\(key);" : "\(key) \(value);"
}).joined(by: " ")
)

if url.url.lastPathComponent == "page-exists" {
let statusCode = ReaderModeHandler.readerModeCache.contains(readerModeUrl) ? 200 : 400
if let response = HTTPURLResponse(
url: url.url,
statusCode: statusCode,
httpVersion: "HTTP/1.1",
headerFields: ["Content-Type": "text/html; charset=UTF-8"]
headerFields: headers
) {
return (response, Data())
}
Expand Down Expand Up @@ -62,9 +141,6 @@ public class ReaderModeHandler: InternalSchemeResponse {
}
}

// Must generate a unique nonce, every single time as per Content-Policy spec.
let setTitleNonce = UUID().uuidString.replacingOccurrences(of: "-", with: "")

if let html = ReaderModeUtils.generateReaderContent(
readabilityResult,
initialStyle: readerModeStyle,
Expand All @@ -78,11 +154,7 @@ public class ReaderModeHandler: InternalSchemeResponse {
url: url.url,
statusCode: 200,
httpVersion: "HTTP/1.1",
headerFields: [
"Content-Type": "text/html; charset=UTF-8",
"Content-Security-Policy":
"default-src 'none'; img-src *; style-src \(InternalURL.baseUrl) '\(ReaderModeHandler.readerModeStyleHash)'; font-src \(InternalURL.baseUrl); script-src 'nonce-\(setTitleNonce)'",
]
headerFields: headers
) {
return (response, data)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ public protocol InternalSchemeResponse {

public class InternalSchemeHandler: NSObject, WKURLSchemeHandler {

private weak var tab: Tab?

init(tab: Tab?) {
self.tab = tab
super.init()
}

public static func response(forUrl url: URL) -> URLResponse {
return URLResponse(
url: url,
Expand Down
3 changes: 2 additions & 1 deletion ios/brave-ios/Sources/Brave/Frontend/Browser/Tab.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ class Tab: NSObject {
private(set) var type: TabType = .regular

var redirectURLs = [URL]()
var responses = [URL: URLResponse]()

var isPrivate: Bool {
return type.isPrivate
Expand Down Expand Up @@ -463,7 +464,7 @@ class Tab: NSObject {

if configuration!.urlSchemeHandler(forURLScheme: InternalURL.scheme) == nil {
configuration!.setURLSchemeHandler(
InternalSchemeHandler(),
InternalSchemeHandler(tab: self),
forURLScheme: InternalURL.scheme
)
}
Expand Down
1 change: 1 addition & 0 deletions ios/brave-ios/Sources/Brave/Frontend/Reader/Reader.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<meta content="text/html; charset=UTF-8" http-equiv="content-type">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=.25, maximum-scale=1.6, initial-scale=1.0">
<meta name="referrer" content="no-referrer">
%READER-ORIGINAL-PAGE-META-TAGS%
<link rel="stylesheet" type="text/css" href="/reader-mode/styles/Reader.css">
<title id="reader-page-title"></title>
</head>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ struct ReaderModeUtils {
?? readabilityResult.direction.htmlEntityEncodedString
)
.replacingOccurrences(of: "%READER-MESSAGE%", with: "")
.replacingOccurrences(
of: "%READER-ORIGINAL-PAGE-META-TAGS%",
with: readabilityResult.cspMetaTags.joined(separator: " \n")
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ class SettingsContentViewController: UIViewController, WKNavigationDelegate {
func makeWebView() -> BraveWebView {
let frame = CGRect(width: 1, height: 1)
let configuration = WKWebViewConfiguration().then {
$0.setURLSchemeHandler(InternalSchemeHandler(), forURLScheme: InternalURL.scheme)
$0.setURLSchemeHandler(InternalSchemeHandler(tab: nil), forURLScheme: InternalURL.scheme)
}
let webView = BraveWebView(frame: frame, configuration: configuration)
webView.allowsLinkPreview = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,15 @@ function checkReadability() {
return;
}


// Preserve the CSP meta tag
delete document.querySelector;
delete document.querySelectorAll;
let cspMetaTags = document.querySelectorAll("meta[http-equiv=\"Content-Security-Policy\"]");
if (cspMetaTags) {
readabilityResult.cspMetaTags = [...cspMetaTags].map((e) => new XMLSerializer().serializeToString(e));
}

debug({Type: "ReaderModeStateChange", Value: readabilityResult !== null ? "Available" : "Unavailable"});
webkit.messageHandlers.readerModeMessageHandler.postMessage({"securityToken": SECURITY_TOKEN, "data": {Type: "ReaderModeStateChange", Value: readabilityResult !== null ? "Available" : "Unavailable"}});
webkit.messageHandlers.readerModeMessageHandler.postMessage({"securityToken": SECURITY_TOKEN, "data": {Type: "ReaderContentParsed", Value: readabilityResult}});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ struct ReadabilityResult {
var title = ""
var credits = ""
var direction = "auto"
var cspMetaTags = [String]()

init?(object: AnyObject?) {
if let dict = object as? NSDictionary {
Expand Down Expand Up @@ -199,6 +200,9 @@ struct ReadabilityResult {
if let direction = dict["dir"] as? String {
self.direction = direction
}
if let cspMetaTags = dict["cspMetaTags"] as? [String] {
self.cspMetaTags = cspMetaTags
}
} else {
return nil
}
Expand All @@ -213,6 +217,7 @@ struct ReadabilityResult {
let title = object["title"].string
let credits = object["credits"].string
let direction = object["dir"].string
let cspMetaTags = object["cspMetaTags"].arrayObject as? [String]

if domain == nil || url == nil || content == nil || title == nil || credits == nil {
return nil
Expand All @@ -224,13 +229,14 @@ struct ReadabilityResult {
self.title = title!
self.credits = credits!
self.direction = direction ?? "auto"
self.cspMetaTags = cspMetaTags ?? []
}

/// Encode to a dictionary, which can then for example be json encoded
func encode() -> [String: Any] {
return [
"domain": domain, "url": url, "content": content, "title": title, "credits": credits,
"dir": direction,
"dir": direction, "cspMetaTags": cspMetaTags,
]
}

Expand Down
17 changes: 16 additions & 1 deletion ios/brave-ios/Sources/BraveShared/Extensions/URLExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,10 @@ extension URL {
}

/// Embed a url into an internal URL for the given path. The url will be placed in a `url` querey param
public func encodeEmbeddedInternalURL(for path: InternalURL.Path) -> URL? {
public func encodeEmbeddedInternalURL(
for path: InternalURL.Path,
headers: [String: String]? = nil
) -> URL? {
let baseURL = "\(InternalURL.baseUrl)/\(path.rawValue)"

guard
Expand All @@ -115,6 +118,18 @@ extension URL {
return nil
}

if let headers = headers, !headers.isEmpty,
let data = try? JSONSerialization.data(withJSONObject: headers),
let encodedHeaders = data.base64EncodedString.addingPercentEncoding(
withAllowedCharacters: .alphanumerics
)
{
return URL(
string:
"\(baseURL)?\(InternalURL.Param.url.rawValue)=\(encodedURL)&headers=\(encodedHeaders)"
)
}

return URL(string: "\(baseURL)?\(InternalURL.Param.url.rawValue)=\(encodedURL)")
}
}
Expand Down

0 comments on commit 05bb754

Please sign in to comment.