mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-08-20 19:33:08 +09:00
298 lines
17 KiB
Swift
298 lines
17 KiB
Swift
/* 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: TabContentScript {
|
|
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
|
|
}
|
|
}
|