-
Notifications
You must be signed in to change notification settings - Fork 3k
/
Copy pathErrorPageHelper.swift
298 lines (262 loc) · 17.1 KB
/
ErrorPageHelper.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Foundation
import WebKit
import GCDWebServers
import Shared
import Storage
class ErrorPageHelper {
static let MozDomain = "mozilla"
static let MozErrorDownloadsNotEnabled = 100
fileprivate static let MessageOpenInSafari = "openInSafari"
fileprivate static let MessageCertVisitOnce = "certVisitOnce"
// When an error page is intentionally loaded, its added to this set. If its in the set, we show
// it as an error page. If its not, we assume someone is trying to reload this page somehow, and
// we'll instead redirect back to the original URL.
fileprivate static var redirecting = [URL]()
fileprivate static weak var certStore: CertStore?
// Regardless of cause, NSURLErrorServerCertificateUntrusted is currently returned in all cases.
// Check the other cases in case this gets fixed in the future.
fileprivate static let CertErrors = [
NSURLErrorServerCertificateUntrusted,
NSURLErrorServerCertificateHasBadDate,
NSURLErrorServerCertificateHasUnknownRoot,
NSURLErrorServerCertificateNotYetValid
]
// Error codes copied from Gecko. The ints corresponding to these codes were determined
// by inspecting the NSError in each of these cases.
fileprivate static let CertErrorCodes = [
-9813: "SEC_ERROR_UNKNOWN_ISSUER",
-9814: "SEC_ERROR_EXPIRED_CERTIFICATE",
-9843: "SSL_ERROR_BAD_CERT_DOMAIN",
]
class func cfErrorToName(_ err: CFNetworkErrors) -> String {
switch err {
case .cfHostErrorHostNotFound: return "CFHostErrorHostNotFound"
case .cfHostErrorUnknown: return "CFHostErrorUnknown"
case .cfsocksErrorUnknownClientVersion: return "CFSOCKSErrorUnknownClientVersion"
case .cfsocksErrorUnsupportedServerVersion: return "CFSOCKSErrorUnsupportedServerVersion"
case .cfsocks4ErrorRequestFailed: return "CFSOCKS4ErrorRequestFailed"
case .cfsocks4ErrorIdentdFailed: return "CFSOCKS4ErrorIdentdFailed"
case .cfsocks4ErrorIdConflict: return "CFSOCKS4ErrorIdConflict"
case .cfsocks4ErrorUnknownStatusCode: return "CFSOCKS4ErrorUnknownStatusCode"
case .cfsocks5ErrorBadState: return "CFSOCKS5ErrorBadState"
case .cfsocks5ErrorBadResponseAddr: return "CFSOCKS5ErrorBadResponseAddr"
case .cfsocks5ErrorBadCredentials: return "CFSOCKS5ErrorBadCredentials"
case .cfsocks5ErrorUnsupportedNegotiationMethod: return "CFSOCKS5ErrorUnsupportedNegotiationMethod"
case .cfsocks5ErrorNoAcceptableMethod: return "CFSOCKS5ErrorNoAcceptableMethod"
case .cfftpErrorUnexpectedStatusCode: return "CFFTPErrorUnexpectedStatusCode"
case .cfErrorHTTPAuthenticationTypeUnsupported: return "CFErrorHTTPAuthenticationTypeUnsupported"
case .cfErrorHTTPBadCredentials: return "CFErrorHTTPBadCredentials"
case .cfErrorHTTPConnectionLost: return "CFErrorHTTPConnectionLost"
case .cfErrorHTTPParseFailure: return "CFErrorHTTPParseFailure"
case .cfErrorHTTPRedirectionLoopDetected: return "CFErrorHTTPRedirectionLoopDetected"
case .cfErrorHTTPBadURL: return "CFErrorHTTPBadURL"
case .cfErrorHTTPProxyConnectionFailure: return "CFErrorHTTPProxyConnectionFailure"
case .cfErrorHTTPBadProxyCredentials: return "CFErrorHTTPBadProxyCredentials"
case .cfErrorPACFileError: return "CFErrorPACFileError"
case .cfErrorPACFileAuth: return "CFErrorPACFileAuth"
case .cfErrorHTTPSProxyConnectionFailure: return "CFErrorHTTPSProxyConnectionFailure"
case .cfStreamErrorHTTPSProxyFailureUnexpectedResponseToCONNECTMethod: return "CFStreamErrorHTTPSProxyFailureUnexpectedResponseToCONNECTMethod"
case .cfurlErrorBackgroundSessionInUseByAnotherProcess: return "CFURLErrorBackgroundSessionInUseByAnotherProcess"
case .cfurlErrorBackgroundSessionWasDisconnected: return "CFURLErrorBackgroundSessionWasDisconnected"
case .cfurlErrorUnknown: return "CFURLErrorUnknown"
case .cfurlErrorCancelled: return "CFURLErrorCancelled"
case .cfurlErrorBadURL: return "CFURLErrorBadURL"
case .cfurlErrorTimedOut: return "CFURLErrorTimedOut"
case .cfurlErrorUnsupportedURL: return "CFURLErrorUnsupportedURL"
case .cfurlErrorCannotFindHost: return "CFURLErrorCannotFindHost"
case .cfurlErrorCannotConnectToHost: return "CFURLErrorCannotConnectToHost"
case .cfurlErrorNetworkConnectionLost: return "CFURLErrorNetworkConnectionLost"
case .cfurlErrorDNSLookupFailed: return "CFURLErrorDNSLookupFailed"
case .cfurlErrorHTTPTooManyRedirects: return "CFURLErrorHTTPTooManyRedirects"
case .cfurlErrorResourceUnavailable: return "CFURLErrorResourceUnavailable"
case .cfurlErrorNotConnectedToInternet: return "CFURLErrorNotConnectedToInternet"
case .cfurlErrorRedirectToNonExistentLocation: return "CFURLErrorRedirectToNonExistentLocation"
case .cfurlErrorBadServerResponse: return "CFURLErrorBadServerResponse"
case .cfurlErrorUserCancelledAuthentication: return "CFURLErrorUserCancelledAuthentication"
case .cfurlErrorUserAuthenticationRequired: return "CFURLErrorUserAuthenticationRequired"
case .cfurlErrorZeroByteResource: return "CFURLErrorZeroByteResource"
case .cfurlErrorCannotDecodeRawData: return "CFURLErrorCannotDecodeRawData"
case .cfurlErrorCannotDecodeContentData: return "CFURLErrorCannotDecodeContentData"
case .cfurlErrorCannotParseResponse: return "CFURLErrorCannotParseResponse"
case .cfurlErrorInternationalRoamingOff: return "CFURLErrorInternationalRoamingOff"
case .cfurlErrorCallIsActive: return "CFURLErrorCallIsActive"
case .cfurlErrorDataNotAllowed: return "CFURLErrorDataNotAllowed"
case .cfurlErrorRequestBodyStreamExhausted: return "CFURLErrorRequestBodyStreamExhausted"
case .cfurlErrorFileDoesNotExist: return "CFURLErrorFileDoesNotExist"
case .cfurlErrorFileIsDirectory: return "CFURLErrorFileIsDirectory"
case .cfurlErrorNoPermissionsToReadFile: return "CFURLErrorNoPermissionsToReadFile"
case .cfurlErrorDataLengthExceedsMaximum: return "CFURLErrorDataLengthExceedsMaximum"
case .cfurlErrorSecureConnectionFailed: return "CFURLErrorSecureConnectionFailed"
case .cfurlErrorServerCertificateHasBadDate: return "CFURLErrorServerCertificateHasBadDate"
case .cfurlErrorServerCertificateUntrusted: return "CFURLErrorServerCertificateUntrusted"
case .cfurlErrorServerCertificateHasUnknownRoot: return "CFURLErrorServerCertificateHasUnknownRoot"
case .cfurlErrorServerCertificateNotYetValid: return "CFURLErrorServerCertificateNotYetValid"
case .cfurlErrorClientCertificateRejected: return "CFURLErrorClientCertificateRejected"
case .cfurlErrorClientCertificateRequired: return "CFURLErrorClientCertificateRequired"
case .cfurlErrorCannotLoadFromNetwork: return "CFURLErrorCannotLoadFromNetwork"
case .cfurlErrorCannotCreateFile: return "CFURLErrorCannotCreateFile"
case .cfurlErrorCannotOpenFile: return "CFURLErrorCannotOpenFile"
case .cfurlErrorCannotCloseFile: return "CFURLErrorCannotCloseFile"
case .cfurlErrorCannotWriteToFile: return "CFURLErrorCannotWriteToFile"
case .cfurlErrorCannotRemoveFile: return "CFURLErrorCannotRemoveFile"
case .cfurlErrorCannotMoveFile: return "CFURLErrorCannotMoveFile"
case .cfurlErrorDownloadDecodingFailedMidStream: return "CFURLErrorDownloadDecodingFailedMidStream"
case .cfurlErrorDownloadDecodingFailedToComplete: return "CFURLErrorDownloadDecodingFailedToComplete"
case .cfhttpCookieCannotParseCookieFile: return "CFHTTPCookieCannotParseCookieFile"
case .cfNetServiceErrorUnknown: return "CFNetServiceErrorUnknown"
case .cfNetServiceErrorCollision: return "CFNetServiceErrorCollision"
case .cfNetServiceErrorNotFound: return "CFNetServiceErrorNotFound"
case .cfNetServiceErrorInProgress: return "CFNetServiceErrorInProgress"
case .cfNetServiceErrorBadArgument: return "CFNetServiceErrorBadArgument"
case .cfNetServiceErrorCancel: return "CFNetServiceErrorCancel"
case .cfNetServiceErrorInvalid: return "CFNetServiceErrorInvalid"
case .cfNetServiceErrorTimeout: return "CFNetServiceErrorTimeout"
case .cfNetServiceErrorDNSServiceFailure: return "CFNetServiceErrorDNSServiceFailure"
default: return "Unknown"
}
}
class func register(_ server: WebServer, certStore: CertStore?) {
self.certStore = certStore
server.registerHandlerForMethod("GET", module: "errors", resource: "error.html", handler: { (request) -> GCDWebServerResponse! in
guard let url = request?.url.originalURLFromErrorURL else {
return GCDWebServerResponse(statusCode: 404)
}
guard let index = self.redirecting.index(of: url) else {
return GCDWebServerDataResponse(redirect: url, permanent: false)
}
self.redirecting.remove(at: index)
guard let code = request?.query["code"] as? String,
let errCode = Int(code),
let errDescription = request?.query["description"] as? String,
let errURLString = request?.query["url"] as? String,
let errURLDomain = URL(string: errURLString)?.host,
var errDomain = request?.query["domain"] as? String else {
return GCDWebServerResponse(statusCode: 404)
}
var asset = Bundle.main.path(forResource: "NetError", ofType: "html")
var variables = [
"error_code": "\(errCode)",
"error_title": errDescription,
"short_description": errDomain,
]
let tryAgain = NSLocalizedString("Try again", tableName: "ErrorPages", comment: "Shown in error pages on a button that will try to load the page again")
var actions = "<button onclick='webkit.messageHandlers.localRequestHelper.postMessage({ type: \"reload\" })'>\(tryAgain)</button>"
if errDomain == kCFErrorDomainCFNetwork as String {
if let code = CFNetworkErrors(rawValue: Int32(errCode)) {
errDomain = self.cfErrorToName(code)
}
} else if errDomain == ErrorPageHelper.MozDomain {
if errCode == ErrorPageHelper.MozErrorDownloadsNotEnabled {
let downloadInSafari = NSLocalizedString("Open in Safari", tableName: "ErrorPages", comment: "Shown in error pages for files that can't be shown and need to be downloaded.")
// Overwrite the normal try-again action.
actions = "<button onclick='webkit.messageHandlers.errorPageHelperMessageManager.postMessage({type: \"\(MessageOpenInSafari)\"})'>\(downloadInSafari)</button>"
}
errDomain = ""
} else if CertErrors.contains(errCode) {
guard let certError = request?.query["certerror"] as? String else {
return GCDWebServerResponse(statusCode: 404)
}
asset = Bundle.main.path(forResource: "CertError", ofType: "html")
actions = "<button onclick='history.back()'>\(Strings.ErrorPagesGoBackButton)</button>"
variables["error_title"] = Strings.ErrorPagesCertWarningTitle
variables["cert_error"] = certError
variables["long_description"] = String(format: Strings.ErrorPagesCertWarningDescription, "<b>\(errURLDomain)</b>")
variables["advanced_button"] = Strings.ErrorPagesAdvancedButton
variables["warning_description"] = Strings.ErrorPagesCertWarningDescription
variables["warning_advanced1"] = Strings.ErrorPagesAdvancedWarning1
variables["warning_advanced2"] = Strings.ErrorPagesAdvancedWarning2
variables["warning_actions"] =
"<p><a href='javascript:webkit.messageHandlers.errorPageHelperMessageManager.postMessage({type: \"\(MessageCertVisitOnce)\"})'>\(Strings.ErrorPagesVisitOnceButton)</button></p>"
}
variables["actions"] = actions
let response = GCDWebServerDataResponse(htmlTemplate: asset, variables: variables)
response?.setValue("no cache", forAdditionalHeader: "Pragma")
response?.setValue("no-cache,must-revalidate", forAdditionalHeader: "Cache-Control")
response?.setValue(Date().description, forAdditionalHeader: "Expires")
return response
})
server.registerHandlerForMethod("GET", module: "errors", resource: "NetError.css", handler: { (request) -> GCDWebServerResponse! in
let path = Bundle(for: self).path(forResource: "NetError", ofType: "css")!
return GCDWebServerDataResponse(data: try? Data(contentsOf: URL(fileURLWithPath: path)), contentType: "text/css")
})
server.registerHandlerForMethod("GET", module: "errors", resource: "CertError.css", handler: { (request) -> GCDWebServerResponse! in
let path = Bundle(for: self).path(forResource: "CertError", ofType: "css")!
return GCDWebServerDataResponse(data: try? Data(contentsOf: URL(fileURLWithPath: path)), contentType: "text/css")
})
}
func showPage(_ error: NSError, forUrl url: URL, inWebView webView: WKWebView) {
// Don't show error pages for error pages.
if url.isErrorPageURL {
if let previousURL = url.originalURLFromErrorURL,
let index = ErrorPageHelper.redirecting.index(of: previousURL) {
ErrorPageHelper.redirecting.remove(at: index)
}
return
}
// Add this page to the redirecting list. This will cause the server to actually show the error page
// (instead of redirecting to the original URL).
ErrorPageHelper.redirecting.append(url)
var components = URLComponents(string: WebServer.sharedInstance.base + "/errors/error.html")!
var queryItems = [
URLQueryItem(name: "url", value: url.absoluteString),
URLQueryItem(name: "code", value: String(error.code)),
URLQueryItem(name: "domain", value: error.domain),
URLQueryItem(name: "description", value: error.localizedDescription)
]
// If this is an invalid certificate, show a certificate error allowing the
// user to go back or continue. The certificate itself is encoded and added as
// a query parameter to the error page URL; we then read the certificate from
// the URL if the user wants to continue.
if ErrorPageHelper.CertErrors.contains(error.code),
let certChain = error.userInfo["NSErrorPeerCertificateChainKey"] as? [SecCertificate],
let cert = certChain.first,
let underlyingError = error.userInfo[NSUnderlyingErrorKey] as? NSError,
let certErrorCode = underlyingError.userInfo["_kCFStreamErrorCodeKey"] as? Int {
let encodedCert = (SecCertificateCopyData(cert) as Data).base64EncodedString
queryItems.append(URLQueryItem(name: "badcert", value: encodedCert))
let certError = ErrorPageHelper.CertErrorCodes[certErrorCode] ?? ""
queryItems.append(URLQueryItem(name: "certerror", value: String(certError)))
}
components.queryItems = queryItems
webView.load(PrivilegedRequest(url: components.url!) as URLRequest)
}
}
extension ErrorPageHelper: TabHelper {
static func name() -> String {
return "ErrorPageHelper"
}
func scriptMessageHandlerName() -> String? {
return "errorPageHelperMessageManager"
}
func userContentController(_ userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) {
if let errorURL = message.frameInfo.request.url, errorURL.isErrorPageURL,
let res = message.body as? [String: String],
let originalURL = errorURL.originalURLFromErrorURL,
let type = res["type"] {
switch type {
case ErrorPageHelper.MessageOpenInSafari:
UIApplication.shared.openURL(originalURL)
case ErrorPageHelper.MessageCertVisitOnce:
if let cert = certFromErrorURL(errorURL),
let host = originalURL.host {
let origin = "\(host):\(originalURL.port ?? 443)"
ErrorPageHelper.certStore?.addCertificate(cert, forOrigin: origin)
_ = message.webView?.reload()
}
default:
assertionFailure("Unknown error message")
}
}
}
fileprivate func certFromErrorURL(_ url: URL) -> SecCertificate? {
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
if let encodedCert = components?.queryItems?.filter({ $0.name == "badcert" }).first?.value,
let certData = Data(base64Encoded: encodedCert, options: []) {
return SecCertificateCreateWithData(nil, certData as CFData)
}
return nil
}
}