@protobase/mobile 0.56.0 → 0.57.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ios/Sources/ProtobaseMobile/AuthHeaders.swift +34 -0
- package/ios/Sources/ProtobaseMobile/AuthRequest.swift +66 -0
- package/ios/Sources/ProtobaseMobile/AuthUrl.swift +36 -0
- package/ios/Sources/ProtobaseMobile/CookiePromptFilter.swift +48 -0
- package/ios/Sources/ProtobaseMobile/GuardedAssetHandler.swift +50 -0
- package/ios/Sources/ProtobaseMobile/LocalFileGuard.swift +59 -0
- package/ios/Sources/ProtobaseMobile/PageScriptFilter.swift +70 -0
- package/ios/Sources/ProtobaseMobile/ProtobaseAuthPlugin.swift +28 -0
- package/package.json +1 -1
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The request headers ProtobaseAuth sends of those page script gives, as Android's AuthHeaders.java keeps them: the
|
|
5
|
+
* string-valued ones, without the Fetch spec's forbidden request header names, which a page's Request never carries.
|
|
6
|
+
* So page script cannot set the cookie, the origin, or the host a shared ingress routes by, nor override the method.
|
|
7
|
+
* A name that is no HTTP token, or a value holding CR, LF or NUL, is dropped too.
|
|
8
|
+
*/
|
|
9
|
+
enum AuthHeaders {
|
|
10
|
+
private static let forbidden: Set<String> = [
|
|
11
|
+
"accept-charset", "accept-encoding", "access-control-request-headers", "access-control-request-method",
|
|
12
|
+
"connection", "content-length", "cookie", "cookie2", "date", "dnt", "expect", "host", "keep-alive", "origin",
|
|
13
|
+
"referer", "set-cookie", "te", "trailer", "transfer-encoding", "upgrade", "via", "x-http-method",
|
|
14
|
+
"x-http-method-override", "x-method-override",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
// RFC 9110's tchar.
|
|
18
|
+
private static let token = CharacterSet(charactersIn: "!#$%&'*+-.^_`|~0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
|
19
|
+
|
|
20
|
+
static func allowed(_ given: [String: Any]?) -> [String: String] {
|
|
21
|
+
var headers: [String: String] = [:]
|
|
22
|
+
for (name, value) in given ?? [:] {
|
|
23
|
+
guard let text = value as? String, !isForbidden(name), !text.contains(where: { "\r\n\0".contains($0) }) else { continue }
|
|
24
|
+
headers[name] = text
|
|
25
|
+
}
|
|
26
|
+
return headers
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private static func isForbidden(_ name: String) -> Bool {
|
|
30
|
+
let lower = name.lowercased()
|
|
31
|
+
let isToken = !name.isEmpty && name.unicodeScalars.allSatisfy { token.contains($0) }
|
|
32
|
+
return !isToken || forbidden.contains(lower) || lower.hasPrefix("proxy-") || lower.hasPrefix("sec-")
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import Capacitor
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* One sign-in request of ProtobaseAuth, checked in the order of Android's ProtobaseAuth.java and sent through
|
|
6
|
+
* Capacitor's own request type, which refuses redirects, with the device's cookie jar. The call's options are never
|
|
7
|
+
* handed to Capacitor code, so page script cannot add params, a file body, timeouts or redirects.
|
|
8
|
+
*/
|
|
9
|
+
enum AuthRequest {
|
|
10
|
+
static func send(_ call: CAPPluginCall, config: PluginConfig, capConfig: InstanceConfiguration?) {
|
|
11
|
+
guard let apiOrigin = config.getString("apiOrigin"), let origin = config.getString("origin") else {
|
|
12
|
+
call.reject("ProtobaseAuth has no apiOrigin or origin in its config")
|
|
13
|
+
return
|
|
14
|
+
}
|
|
15
|
+
guard let text = call.options["url"] as? String, let url = AuthUrl.allowed(text, apiOrigin: apiOrigin) else {
|
|
16
|
+
call.reject("ProtobaseAuth calls \(apiOrigin)\(AuthUrl.authBasePath)/ only")
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
let method = (call.getString("method") ?? "GET").uppercased()
|
|
20
|
+
// All Better Auth serves.
|
|
21
|
+
guard method == "GET" || method == "POST" else {
|
|
22
|
+
call.reject("ProtobaseAuth sends GET or POST only, not \(method)")
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
let data = call.options["data"]
|
|
26
|
+
guard data == nil || data is String else {
|
|
27
|
+
call.reject("ProtobaseAuth sends a text body only")
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
let body = data as? String
|
|
31
|
+
// URLSession fails a GET with a body, and the shell never sends one.
|
|
32
|
+
guard body == nil || method == "POST" else {
|
|
33
|
+
call.reject("ProtobaseAuth sends a body with POST only")
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
var headers = AuthHeaders.allowed(call.getObject("headers"))
|
|
37
|
+
headers["Origin"] = origin
|
|
38
|
+
|
|
39
|
+
let request = CapacitorUrlRequest(url, method: method)
|
|
40
|
+
request.setRequestHeaders(headers)
|
|
41
|
+
if let body { request.request.httpBody = Data(body.utf8) }
|
|
42
|
+
// The session holds its delegate, the request that refuses redirects, until it is invalidated.
|
|
43
|
+
let session = URLSession(configuration: .default, delegate: request, delegateQueue: nil)
|
|
44
|
+
let task = session.dataTask(with: request.getUrlRequest()) { data, response, error in
|
|
45
|
+
session.invalidateAndCancel()
|
|
46
|
+
if let error {
|
|
47
|
+
call.reject(error.localizedDescription, (error as NSError).domain, error, nil)
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
guard let response = response as? HTTPURLResponse else {
|
|
51
|
+
call.reject("ProtobaseAuth got no HTTP answer")
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
HttpRequestHandler.setCookiesFromResponse(response, capConfig)
|
|
55
|
+
var output = HttpRequestHandler.buildResponse(data, response, responseType: .text)
|
|
56
|
+
// Set-Cookie stays in the jar; buildResponse lower-cases the names.
|
|
57
|
+
if var answerHeaders = output["headers"] as? [String: Any] {
|
|
58
|
+
answerHeaders.removeValue(forKey: "set-cookie")
|
|
59
|
+
answerHeaders.removeValue(forKey: "set-cookie2")
|
|
60
|
+
output["headers"] = answerHeaders
|
|
61
|
+
}
|
|
62
|
+
call.resolve(output)
|
|
63
|
+
}
|
|
64
|
+
task.resume()
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/** The sign-in addresses ProtobaseAuth calls: `<apiOrigin>/api/auth/...`, as Android's AuthUrl.java allows them. */
|
|
4
|
+
enum AuthUrl {
|
|
5
|
+
/** Where Better Auth's endpoints are served, as packages/schema/src/auth-base-path.ts names it. */
|
|
6
|
+
static let authBasePath = "/api/auth"
|
|
7
|
+
|
|
8
|
+
private struct Parts {
|
|
9
|
+
let scheme: String
|
|
10
|
+
let host: String
|
|
11
|
+
let port: Int
|
|
12
|
+
let path: String
|
|
13
|
+
let hasUser: Bool
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
private static func parse(_ text: String) -> Parts? {
|
|
17
|
+
guard let url = URL(string: text), let parts = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return nil }
|
|
18
|
+
let scheme = (parts.scheme ?? "").lowercased()
|
|
19
|
+
guard scheme == "http" || scheme == "https", let host = parts.percentEncodedHost, !host.isEmpty else { return nil }
|
|
20
|
+
let port = parts.port ?? (scheme == "https" ? 443 : 80)
|
|
21
|
+
let hasUser = parts.percentEncodedUser != nil || parts.percentEncodedPassword != nil
|
|
22
|
+
return Parts(scheme: scheme, host: host.lowercased(), port: port, path: parts.percentEncodedPath, hasUser: hasUser)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The URL to send when `text` is allowed, so the URL checked is the URL sent; `nil` otherwise. */
|
|
26
|
+
static func allowed(_ text: String, apiOrigin: String) -> URL? {
|
|
27
|
+
guard let origin = parse(apiOrigin), let target = parse(text), origin.path.isEmpty, !target.hasUser else { return nil }
|
|
28
|
+
guard target.scheme == origin.scheme, target.host == origin.host, target.port == origin.port else { return nil }
|
|
29
|
+
let path = target.path
|
|
30
|
+
guard path.hasPrefix(authBasePath + "/"), !path.contains("\\") else { return nil }
|
|
31
|
+
let lower = path.lowercased()
|
|
32
|
+
guard !["%2e", "%2f", "%5c"].contains(where: { lower.contains($0) }) else { return nil }
|
|
33
|
+
guard !path.split(separator: "/", omittingEmptySubsequences: false).contains(where: { $0 == "." || $0 == ".." }) else { return nil }
|
|
34
|
+
return URL(string: text)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import WebKit
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Stands in front of the WebView's UI delegate, Capacitor's WebViewDelegationHandler, which answers a JSON `prompt` of
|
|
5
|
+
* type `CapacitorCookies.set` or `CapacitorCookies.get` without a dialog, whether CapacitorCookies is enabled or not,
|
|
6
|
+
* and crashes on a `set` without its fields. Those prompts answer `null`; everything else goes to Capacitor's delegate.
|
|
7
|
+
*/
|
|
8
|
+
final class CookiePromptFilter: NSObject, WKUIDelegate {
|
|
9
|
+
private static let refusedTypes: Set<String> = ["CapacitorCookies.set", "CapacitorCookies.get"]
|
|
10
|
+
|
|
11
|
+
private let original: NSObject & WKUIDelegate
|
|
12
|
+
|
|
13
|
+
private init(original: NSObject & WKUIDelegate) {
|
|
14
|
+
self.original = original
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Puts the filter in front; the caller keeps it, as the WebView holds its UI delegate weakly. */
|
|
18
|
+
static func install(on webView: WKWebView) -> CookiePromptFilter {
|
|
19
|
+
guard let original = webView.uiDelegate as? NSObject & WKUIDelegate else { fatalError("ProtobaseAuth needs Capacitor's UI delegate on the WebView") }
|
|
20
|
+
let filter = CookiePromptFilter(original: original)
|
|
21
|
+
webView.uiDelegate = filter
|
|
22
|
+
return filter
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (String?) -> Void) {
|
|
26
|
+
// Parsed as WebViewDelegationHandler.swift parses it.
|
|
27
|
+
if let data = prompt.data(using: .utf8),
|
|
28
|
+
let payload = try? JSONSerialization.jsonObject(with: data, options: .fragmentsAllowed) as? [String: AnyObject],
|
|
29
|
+
let type = payload["type"] as? String,
|
|
30
|
+
Self.refusedTypes.contains(type) {
|
|
31
|
+
completionHandler(nil)
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
guard let forward = original.webView(_:runJavaScriptTextInputPanelWithPrompt:defaultText:initiatedByFrame:completionHandler:) else {
|
|
35
|
+
completionHandler(nil)
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
forward(webView, prompt, defaultText, frame, completionHandler)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
override func responds(to aSelector: Selector!) -> Bool {
|
|
42
|
+
super.responds(to: aSelector) || original.responds(to: aSelector)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
override func forwardingTarget(for aSelector: Selector!) -> Any? {
|
|
46
|
+
original
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import Capacitor
|
|
2
|
+
import ObjectiveC
|
|
3
|
+
import WebKit
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Capacitor's asset handler with LocalFileGuard in front. WebKit holds the handler from the WebView's creation, before
|
|
7
|
+
* any plugin loads, so `install` switches the live instance to this class, which adds no stored state; the guard is an
|
|
8
|
+
* associated object. One lock covers the check and the serving, and `setAssetPath`, so the base path cannot change
|
|
9
|
+
* between them.
|
|
10
|
+
*/
|
|
11
|
+
final class GuardedAssetHandler: WebViewAssetHandler {
|
|
12
|
+
private static var guardKey: UInt8 = 0
|
|
13
|
+
|
|
14
|
+
static func install(on handler: WebViewAssetHandler, _ fileGuard: LocalFileGuard) {
|
|
15
|
+
objc_setAssociatedObject(handler, &guardKey, fileGuard, .OBJC_ASSOCIATION_RETAIN)
|
|
16
|
+
object_setClass(handler, GuardedAssetHandler.self)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
private var fileGuard: LocalFileGuard {
|
|
20
|
+
guard let fileGuard = objc_getAssociatedObject(self, &Self.guardKey) as? LocalFileGuard else { fatalError("GuardedAssetHandler has no LocalFileGuard") }
|
|
21
|
+
return fileGuard
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
override func setAssetPath(_ assetPath: String) {
|
|
25
|
+
let fileGuard = fileGuard
|
|
26
|
+
fileGuard.withLock {
|
|
27
|
+
fileGuard.basePath = assetPath
|
|
28
|
+
super.setAssetPath(assetPath)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
override func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
|
|
33
|
+
let fileGuard = fileGuard
|
|
34
|
+
fileGuard.withLock {
|
|
35
|
+
if fileGuard.refuses(path: urlSchemeTask.request.url?.path ?? "") {
|
|
36
|
+
forbid(urlSchemeTask)
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
super.webView(webView, start: urlSchemeTask)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// An empty 403, as Android's LocalServerGuard answers.
|
|
44
|
+
private func forbid(_ urlSchemeTask: WKURLSchemeTask) {
|
|
45
|
+
let url = urlSchemeTask.request.url ?? URL(fileURLWithPath: "/")
|
|
46
|
+
urlSchemeTask.didReceive(HTTPURLResponse(url: url, statusCode: 403, httpVersion: nil, headerFields: [:])!)
|
|
47
|
+
urlSchemeTask.didReceive(Data())
|
|
48
|
+
urlSchemeTask.didFinish()
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import Capacitor
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Decides which `capacitor://localhost` paths the app's asset handler may serve to page script: none of the local file
|
|
6
|
+
* or native HTTP routes, no `..`, and from a base path outside the app bundle only files that are really inside the
|
|
7
|
+
* live updater's bundles folder. Each check runs on the decoded path, the one the asset handler opens.
|
|
8
|
+
*/
|
|
9
|
+
final class LocalFileGuard {
|
|
10
|
+
// Where the live updater unzips bundles (capacitor-live-update's LiveUpdate.swift) and Capacitor serves a persisted
|
|
11
|
+
// server base path from (CAPBridgeViewController.swift).
|
|
12
|
+
private static let bundlesFolder = "Library/NoCloud/ionic_built_snapshots"
|
|
13
|
+
private static let refusedPrefixes = [
|
|
14
|
+
CapacitorBridge.fileStartIdentifier,
|
|
15
|
+
CapacitorBridge.httpInterceptorStartIdentifier,
|
|
16
|
+
CapacitorBridge.httpsInterceptorStartIdentifier,
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
private let lock = NSLock()
|
|
20
|
+
private let appBundle: String?
|
|
21
|
+
// Resolved once: with no bundles folder at launch, nothing outside the app bundle is served.
|
|
22
|
+
private let bundles: String?
|
|
23
|
+
var basePath: String
|
|
24
|
+
|
|
25
|
+
init(basePath: String) {
|
|
26
|
+
self.basePath = basePath
|
|
27
|
+
appBundle = Self.realPath(Bundle.main.bundlePath)
|
|
28
|
+
bundles = Self.realPath(NSHomeDirectory() + "/" + Self.bundlesFolder)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
func withLock<T>(_ body: () throws -> T) rethrows -> T {
|
|
32
|
+
lock.lock()
|
|
33
|
+
defer { lock.unlock() }
|
|
34
|
+
return try body()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func refuses(path: String) -> Bool {
|
|
38
|
+
if Self.refusedPrefixes.contains(where: { path.hasPrefix($0) }) { return true }
|
|
39
|
+
if path.contains("\0") || path.split(separator: "/", omittingEmptySubsequences: false).contains("..") { return true }
|
|
40
|
+
guard let base = Self.realPath(basePath) else { return true }
|
|
41
|
+
if let appBundle, Self.isInside(base, appBundle) { return false }
|
|
42
|
+
guard let bundles else { return true }
|
|
43
|
+
// The file the asset handler opens, by Capacitor's own routing.
|
|
44
|
+
var router = CapacitorRouter()
|
|
45
|
+
router.basePath = basePath
|
|
46
|
+
guard let file = Self.realPath(router.route(for: path)) else { return true }
|
|
47
|
+
return !Self.isInside(file, bundles)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private static func isInside(_ path: String, _ folder: String) -> Bool {
|
|
51
|
+
path == folder || path.hasPrefix(folder.hasSuffix("/") ? folder : folder + "/")
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private static func realPath(_ path: String) -> String? {
|
|
55
|
+
guard let resolved = realpath(path, nil) else { return nil }
|
|
56
|
+
defer { free(resolved) }
|
|
57
|
+
return String(cString: resolved)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import Capacitor
|
|
2
|
+
import WebKit
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Stands in front of Capacitor's `bridge` script message handler, the way page script calls native code. It drops
|
|
6
|
+
* messages from any frame but the main one of the app's own origin, as Capacitor Android's MessageHandler does, and
|
|
7
|
+
* refuses `CapacitorHttp`, `CapacitorCookies` and `WebView`, which hand over or plant the session, and any plugin id
|
|
8
|
+
* the bridge has not loaded: Capacitor would otherwise load a plugin by class name, such as `CAPHttpPlugin`.
|
|
9
|
+
*/
|
|
10
|
+
final class PageScriptFilter: NSObject, WKScriptMessageHandler {
|
|
11
|
+
private static let handlerName = "bridge"
|
|
12
|
+
private static let refusedPlugins: Set<String> = ["CapacitorHttp", "CapacitorCookies", "WebView"]
|
|
13
|
+
|
|
14
|
+
private weak var bridge: CapacitorBridge?
|
|
15
|
+
|
|
16
|
+
private init(bridge: CapacitorBridge) {
|
|
17
|
+
self.bridge = bridge
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static func install(on bridge: CapacitorBridge) {
|
|
21
|
+
let contentController = bridge.webViewDelegationHandler.contentController
|
|
22
|
+
contentController.removeScriptMessageHandler(forName: handlerName)
|
|
23
|
+
contentController.add(PageScriptFilter(bridge: bridge), name: handlerName)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
|
27
|
+
guard let bridge, message.frameInfo.isMainFrame, isAppOrigin(message.frameInfo.securityOrigin, bridge.config.serverURL) else { return }
|
|
28
|
+
guard let body = message.body as? [String: Any], let type = body["type"] as? String else { return }
|
|
29
|
+
if type == "js.error" || type == "cordova" {
|
|
30
|
+
bridge.webViewDelegationHandler.userContentController(userContentController, didReceive: message)
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
guard type == "message" else { return }
|
|
34
|
+
let pluginId = body["pluginId"] as? String ?? ""
|
|
35
|
+
if !Self.refusedPlugins.contains(pluginId), bridge.plugin(withName: pluginId) != nil {
|
|
36
|
+
bridge.webViewDelegationHandler.userContentController(userContentController, didReceive: message)
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
refuse(body, pluginId: pluginId, in: message.webView)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Rejects the call's promise, as Capacitor answers a failed call (CapacitorBridge.swift, toJs).
|
|
43
|
+
private func refuse(_ body: [String: Any], pluginId: String, in webView: WKWebView?) {
|
|
44
|
+
guard let callbackId = body["callbackId"] as? String else { return }
|
|
45
|
+
let answer: [String: Any] = [
|
|
46
|
+
"callbackId": callbackId,
|
|
47
|
+
"pluginId": pluginId,
|
|
48
|
+
"methodName": body["methodName"] as? String ?? "",
|
|
49
|
+
"success": false,
|
|
50
|
+
"error": ["message": "\(pluginId) is not available to page script in a Protobase app"],
|
|
51
|
+
]
|
|
52
|
+
guard let json = try? JSONSerialization.data(withJSONObject: answer), let text = String(data: json, encoding: .utf8) else { return }
|
|
53
|
+
webView?.evaluateJavaScript("window.Capacitor.fromNative(\(text))")
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private func isAppOrigin(_ origin: WKSecurityOrigin, _ serverURL: URL) -> Bool {
|
|
57
|
+
let scheme = (serverURL.scheme ?? "").lowercased()
|
|
58
|
+
let serverPort = serverURL.port ?? defaultPort(scheme)
|
|
59
|
+
let originPort = origin.port == 0 ? defaultPort(origin.protocol.lowercased()) : origin.port
|
|
60
|
+
return origin.protocol.lowercased() == scheme && origin.host.lowercased() == (serverURL.host ?? "").lowercased() && originPort == serverPort
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private func defaultPort(_ scheme: String) -> Int {
|
|
64
|
+
switch scheme {
|
|
65
|
+
case "http": return 80
|
|
66
|
+
case "https": return 443
|
|
67
|
+
default: return 0
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import Capacitor
|
|
2
|
+
import WebKit
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The shell's sign-in requests (AuthRequest), and what keeps page script away from the session: it filters the
|
|
6
|
+
* bridge's script messages and cookie prompts, and guards the asset handler's local file routes. The bridge loads
|
|
7
|
+
* plugins once the WebView, its delegates and its scheme handler exist and before any page script runs.
|
|
8
|
+
* `apiOrigin` and `origin` come from capacitor.config.json, which the CLI writes outside the app's web part.
|
|
9
|
+
*/
|
|
10
|
+
@objc(ProtobaseAuthPlugin)
|
|
11
|
+
public class ProtobaseAuthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
12
|
+
public let identifier = "ProtobaseAuthPlugin"
|
|
13
|
+
public let jsName = "ProtobaseAuth"
|
|
14
|
+
public let pluginMethods: [CAPPluginMethod] = [CAPPluginMethod(name: "request", returnType: CAPPluginReturnPromise)]
|
|
15
|
+
|
|
16
|
+
private var promptFilter: CookiePromptFilter?
|
|
17
|
+
|
|
18
|
+
override public func load() {
|
|
19
|
+
guard let bridge = bridge as? CapacitorBridge, let webView = bridge.webView else { fatalError("ProtobaseAuth needs Capacitor's own bridge and WebView") }
|
|
20
|
+
GuardedAssetHandler.install(on: bridge.webViewAssetHandler, LocalFileGuard(basePath: bridge.config.appLocation.path))
|
|
21
|
+
PageScriptFilter.install(on: bridge)
|
|
22
|
+
promptFilter = CookiePromptFilter.install(on: webView)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@objc func request(_ call: CAPPluginCall) {
|
|
26
|
+
AuthRequest.send(call, config: getConfig(), capConfig: bridge?.config)
|
|
27
|
+
}
|
|
28
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/mobile",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.57.0",
|
|
4
4
|
"description": "The Capacitor that protobase mobile builds a Protobase app's iOS and Android projects with, at fixed versions, and the native plugins that keep page script away from the session and give the iOS app its bounce; installed by the CLI on first use.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|