fix(core): fix ios blob upload (#10263)

This commit is contained in:
EYHN
2025-02-19 10:07:46 +08:00
committed by GitHub
parent b20d316d60
commit 5a7ab880c1
21 changed files with 481 additions and 226 deletions

View File

@@ -14,6 +14,7 @@
50A285D82D112A5E000D5A6D /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 50A285D52D112A5E000D5A6D /* InfoPlist.xcstrings */; };
50A285DC2D112B24000D5A6D /* Intelligents in Frameworks */ = {isa = PBXBuildFile; productRef = 50A285DB2D112B24000D5A6D /* Intelligents */; };
9D52FC432D26CDBF00105D0A /* JSValueContainerExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D52FC422D26CDB600105D0A /* JSValueContainerExt.swift */; };
9D5622962D64A6A5009F1BE4 /* AuthPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D5622952D64A6A4009F1BE4 /* AuthPlugin.swift */; };
9D6A85332CCF6DA700DAB35F /* HashcashPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D6A85322CCF6DA700DAB35F /* HashcashPlugin.swift */; };
9D90BE252CCB9876006677DB /* CookieManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D90BE172CCB9876006677DB /* CookieManager.swift */; };
9D90BE262CCB9876006677DB /* CookiePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D90BE182CCB9876006677DB /* CookiePlugin.swift */; };
@@ -24,7 +25,6 @@
9D90BE2B2CCB9876006677DB /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 9D90BE1F2CCB9876006677DB /* config.xml */; };
9D90BE2D2CCB9876006677DB /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9D90BE222CCB9876006677DB /* Main.storyboard */; };
9D90BE2E2CCB9876006677DB /* public in Resources */ = {isa = PBXBuildFile; fileRef = 9D90BE232CCB9876006677DB /* public */; };
9DEC593B2D3002E70027CEBD /* AffineHttpHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DEC593A2D3002C70027CEBD /* AffineHttpHandler.swift */; };
9DEC59432D323EE40027CEBD /* Mutex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DEC59422D323EE00027CEBD /* Mutex.swift */; };
9DFCD1462D27D1D70028C92B /* libaffine_mobile_native.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9DFCD1452D27D1D70028C92B /* libaffine_mobile_native.a */; };
C4C413792CBE705D00337889 /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */; };
@@ -43,6 +43,7 @@
50A285D52D112A5E000D5A6D /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = "<group>"; };
50A285D62D112A5E000D5A6D /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
9D52FC422D26CDB600105D0A /* JSValueContainerExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSValueContainerExt.swift; sourceTree = "<group>"; };
9D5622952D64A6A4009F1BE4 /* AuthPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthPlugin.swift; sourceTree = "<group>"; };
9D6A85322CCF6DA700DAB35F /* HashcashPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashcashPlugin.swift; sourceTree = "<group>"; };
9D90BE172CCB9876006677DB /* CookieManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookieManager.swift; sourceTree = "<group>"; };
9D90BE182CCB9876006677DB /* CookiePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookiePlugin.swift; sourceTree = "<group>"; };
@@ -54,7 +55,6 @@
9D90BE202CCB9876006677DB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
9D90BE212CCB9876006677DB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
9D90BE232CCB9876006677DB /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
9DEC593A2D3002C70027CEBD /* AffineHttpHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AffineHttpHandler.swift; sourceTree = "<group>"; };
9DEC59422D323EE00027CEBD /* Mutex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mutex.swift; sourceTree = "<group>"; };
9DFCD1452D27D1D70028C92B /* libaffine_mobile_native.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libaffine_mobile_native.a; sourceTree = "<group>"; };
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -136,6 +136,14 @@
name = Pods;
sourceTree = "<group>";
};
9D5622942D64A69C009F1BE4 /* Auth */ = {
isa = PBXGroup;
children = (
9D5622952D64A6A4009F1BE4 /* AuthPlugin.swift */,
);
path = Auth;
sourceTree = "<group>";
};
9D90BE192CCB9876006677DB /* Cookie */ = {
isa = PBXGroup;
children = (
@@ -150,6 +158,7 @@
9D90BE1A2CCB9876006677DB /* Plugins */ = {
isa = PBXGroup;
children = (
9D5622942D64A69C009F1BE4 /* Auth */,
C45499AB2D140B5000E21978 /* NBStore */,
E93B276A2CED9298001409B8 /* NavigationGesture */,
9D90BE192CCB9876006677DB /* Cookie */,
@@ -161,7 +170,6 @@
isa = PBXGroup;
children = (
9DEC59422D323EE00027CEBD /* Mutex.swift */,
9DEC593A2D3002C70027CEBD /* AffineHttpHandler.swift */,
9D52FC422D26CDB600105D0A /* JSValueContainerExt.swift */,
9D90BE1A2CCB9876006677DB /* Plugins */,
9D90BE1C2CCB9876006677DB /* AppDelegate.swift */,
@@ -337,9 +345,9 @@
9D52FC432D26CDBF00105D0A /* JSValueContainerExt.swift in Sources */,
5075136E2D1925BC00AD60C0 /* IntelligentsPlugin.swift in Sources */,
5075136A2D1924C600AD60C0 /* RootViewController.swift in Sources */,
9DEC593B2D3002E70027CEBD /* AffineHttpHandler.swift in Sources */,
C4C97C7C2D030BE000BC2AD1 /* affine_mobile_native.swift in Sources */,
C4C97C7D2D030BE000BC2AD1 /* affine_mobile_nativeFFI.h in Sources */,
9D5622962D64A6A5009F1BE4 /* AuthPlugin.swift in Sources */,
C4C97C7E2D030BE000BC2AD1 /* affine_mobile_nativeFFI.modulemap in Sources */,
E93B276C2CED92B1001409B8 /* NavigationGesturePlugin.swift in Sources */,
9DEC59432D323EE40027CEBD /* Mutex.swift in Sources */,

View File

@@ -1,126 +0,0 @@
//
// RequestUrlSchemeHandler.swift
// App
//
// Created by EYHN on 2025/1/9.
//
import WebKit
enum AffineHttpError: Error {
case invalidOperation(reason: String), invalidState(reason: String)
}
class AffineHttpHandler: NSObject, WKURLSchemeHandler {
func webView(_ webView: WKWebView, start urlSchemeTask: any WKURLSchemeTask) {
urlSchemeTask.stopped = false
guard let rawUrl = urlSchemeTask.request.url else {
urlSchemeTask.didFailWithError(AffineHttpError.invalidOperation(reason: "bad request"))
return
}
guard let scheme = rawUrl.scheme else {
urlSchemeTask.didFailWithError(AffineHttpError.invalidOperation(reason: "bad request"))
return
}
let httpProtocol = scheme == "affine-http" ? "http" : "https"
guard let urlComponents = URLComponents(url: rawUrl, resolvingAgainstBaseURL: true) else {
urlSchemeTask.didFailWithError(AffineHttpError.invalidOperation(reason: "bad request"))
return
}
guard let host = urlComponents.host else {
urlSchemeTask.didFailWithError(AffineHttpError.invalidOperation(reason: "bad url"))
return
}
let path = urlComponents.path
let query = urlComponents.query != nil ? "?\(urlComponents.query!)" : ""
let port = urlComponents.port != nil ? ":\(urlComponents.port!)" : ""
guard let targetUrl = URL(string: "\(httpProtocol)://\(host)\(port)\(path)\(query)") else {
urlSchemeTask.didFailWithError(AffineHttpError.invalidOperation(reason: "bad url"))
return
}
var request = URLRequest(url: targetUrl);
request.httpMethod = urlSchemeTask.request.httpMethod;
request.httpShouldHandleCookies = true
request.httpBody = urlSchemeTask.request.httpBody
urlSchemeTask.request.allHTTPHeaderFields?.filter({
key, value in
let normalizedKey = key.lowercased()
return normalizedKey == "content-type" ||
normalizedKey == "content-length" ||
normalizedKey == "accept"
}).forEach {
key, value in
request.setValue(value, forHTTPHeaderField: key)
}
let task = URLSession.shared.dataTask(with: request) {
(rawData, rawResponse, error) in
DispatchQueue.main.async {
if urlSchemeTask.stopped {
return
}
if error != nil {
urlSchemeTask.didFailWithError(error!)
} else {
guard let httpResponse = rawResponse as? HTTPURLResponse else {
urlSchemeTask.didFailWithError(AffineHttpError.invalidState(reason: "bad response"))
return
}
let inheritedHeaders = httpResponse.allHeaderFields.filter({
key, value in
let normalizedKey = (key as? String)?.lowercased()
return normalizedKey == "content-type" ||
normalizedKey == "content-length"
}) as? [String: String] ?? [:]
let newHeaders: [String: String] = [
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "*"
]
guard let response = HTTPURLResponse.init(url: rawUrl, statusCode: httpResponse.statusCode, httpVersion: nil, headerFields: inheritedHeaders.merging(newHeaders, uniquingKeysWith: { (_, newHeaders) in newHeaders })) else {
urlSchemeTask.didFailWithError(AffineHttpError.invalidState(reason: "failed to create response"))
return
}
urlSchemeTask.didReceive(response)
if rawData != nil {
urlSchemeTask.didReceive(rawData!)
}
urlSchemeTask.didFinish()
}
}
}
task.resume()
urlSchemeTask.dataTask = task
}
func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
urlSchemeTask.stopped = true
urlSchemeTask.dataTask?.cancel()
}
}
private extension WKURLSchemeTask {
var stopped: Bool {
get {
return objc_getAssociatedObject(self, &stoppedKey) as? Bool ?? false
}
set {
objc_setAssociatedObject(self, &stoppedKey, newValue, .OBJC_ASSOCIATION_ASSIGN)
}
}
var dataTask: URLSessionDataTask? {
get {
return objc_getAssociatedObject(self, &dataTaskKey) as? URLSessionDataTask
}
set {
objc_setAssociatedObject(self, &dataTaskKey, newValue, .OBJC_ASSOCIATION_RETAIN)
}
}
}
private var stoppedKey = malloc(1)
private var dataTaskKey = malloc(1)

View File

@@ -20,13 +20,12 @@ class AFFiNEViewController: CAPBridgeViewController {
}
override func webView(with frame: CGRect, configuration: WKWebViewConfiguration) -> WKWebView {
configuration.setURLSchemeHandler(AffineHttpHandler(), forURLScheme: "affine-http")
configuration.setURLSchemeHandler(AffineHttpHandler(), forURLScheme: "affine-https")
return super.webView(with: frame, configuration: configuration)
}
override func capacitorDidLoad() {
let plugins: [CAPPlugin] = [
AuthPlugin(),
CookiePlugin(),
HashcashPlugin(),
NavigationGesturePlugin(),

View File

@@ -0,0 +1,170 @@
import Capacitor
import Foundation
public class AuthPlugin: CAPPlugin, CAPBridgedPlugin {
public let identifier = "AuthPlugin"
public let jsName = "Auth"
public let pluginMethods: [CAPPluginMethod] = [
CAPPluginMethod(name: "signInMagicLink", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "signInOauth", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "signInPassword", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "signOut", returnType: CAPPluginReturnPromise),
]
@objc public func signInMagicLink(_ call: CAPPluginCall) {
Task {
do {
let endpoint = try call.getStringEnsure("endpoint")
let email = try call.getStringEnsure("email")
let token = try call.getStringEnsure("token")
let (data, response) = try await self.fetch(endpoint, method: "POST", action: "/api/auth/magic-link", headers: [:], body: ["email": email, "token": token])
if response.statusCode != 200 {
if let textBody = String(data: data, encoding: .utf8) {
call.reject(textBody)
} else {
call.reject("Failed to sign in")
}
}
guard let token = try self.tokenFromCookie(endpoint) else {
call.reject("token not found")
return
}
call.resolve(["token": token])
} catch {
call.reject("Failed to sign in, \(error)", nil, error)
}
}
}
@objc public func signInOauth(_ call: CAPPluginCall) {
Task {
do {
let endpoint = try call.getStringEnsure("endpoint")
let code = try call.getStringEnsure("code")
let state = try call.getStringEnsure("state")
let (data, response) = try await self.fetch(endpoint, method: "POST", action: "/api/oauth/callback", headers: [:], body: ["code": code, "state": state])
if response.statusCode != 200 {
if let textBody = String(data: data, encoding: .utf8) {
call.reject(textBody)
} else {
call.reject("Failed to sign in")
}
}
guard let token = try self.tokenFromCookie(endpoint) else {
call.reject("token not found")
return
}
call.resolve(["token": token])
} catch {
call.reject("Failed to sign in, \(error)", nil, error)
}
}
}
@objc public func signInPassword(_ call: CAPPluginCall) {
Task {
do {
let endpoint = try call.getStringEnsure("endpoint")
let email = try call.getStringEnsure("email")
let password = try call.getStringEnsure("password")
let verifyToken = call.getString("verifyToken")
let challenge = call.getString("challenge")
let (data, response) = try await self.fetch(endpoint, method: "POST", action: "/api/auth/sign-in", headers: [
"x-captcha-token": verifyToken,
"x-captcha-challenge": challenge,
], body: ["email": email, "password": password])
if response.statusCode != 200 {
if let textBody = String(data: data, encoding: .utf8) {
call.reject(textBody)
} else {
call.reject("Failed to sign in")
}
}
guard let token = try self.tokenFromCookie(endpoint) else {
call.reject("token not found")
return
}
call.resolve(["token": token])
} catch {
call.reject("Failed to sign in, \(error)", nil, error)
}
}
}
@objc public func signOut(_ call: CAPPluginCall) {
Task {
do {
let endpoint = try call.getStringEnsure("endpoint")
let (data, response) = try await self.fetch(endpoint, method: "GET", action: "/api/auth/sign-out", headers: [:], body: nil)
if response.statusCode != 200 {
if let textBody = String(data: data, encoding: .utf8) {
call.reject(textBody)
} else {
call.reject("Failed to sign in")
}
}
call.resolve(["ok": true])
} catch {
call.reject("Failed to sign in, \(error)", nil, error)
}
}
}
private func tokenFromCookie(_ endpoint: String) throws -> String? {
guard let endpointUrl = URL(string: endpoint) else {
throw AuthError.invalidEndpoint
}
if let cookie = HTTPCookieStorage.shared.cookies(for: endpointUrl)?.first(where: {
$0.name == "affine_session"
}) {
return cookie.value
} else {
return nil
}
}
private func fetch(_ endpoint: String, method: String, action: String, headers: Dictionary<String, String?>, body: Encodable?) async throws -> (Data, HTTPURLResponse) {
guard let targetUrl = URL(string: "\(endpoint)\(action)") else {
throw AuthError.invalidEndpoint
}
var request = URLRequest(url: targetUrl);
request.httpMethod = method;
request.httpShouldHandleCookies = true
for (key, value) in headers {
request.setValue(value, forHTTPHeaderField: key)
}
if body != nil {
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONEncoder().encode(body!)
}
request.timeoutInterval = 10 // time out 10s
let (data, response) = try await URLSession.shared.data(for: request);
guard let httpResponse = response as? HTTPURLResponse else {
throw AuthError.internalError
}
return (data, httpResponse)
}
}
enum AuthError: Error {
case invalidEndpoint, internalError
}