mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-02 09:08:35 +02:00
refactor(ios): isolate and test extension callbacks
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||
A11CE0012F00000000000001 /* ExtensionCallbackParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11CE0022F00000000000001 /* ExtensionCallbackParser.swift */; };
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||
@@ -47,6 +48,7 @@
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
A11CE0022F00000000000001 /* ExtensionCallbackParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionCallbackParser.swift; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||
@@ -116,6 +118,7 @@
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
||||
A11CE0022F00000000000001 /* ExtensionCallbackParser.swift */,
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
||||
);
|
||||
path = Runner;
|
||||
@@ -269,6 +272,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||
A11CE0012F00000000000001 /* ExtensionCallbackParser.swift in Sources */,
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
||||
@@ -107,44 +107,29 @@ import Gobackend
|
||||
/// - Signed session: spotiflac://session-grant?grant=...&state=<extension_id>
|
||||
@discardableResult
|
||||
private func handleExtensionOAuthRedirect(url: URL) -> Bool {
|
||||
guard let scheme = url.scheme?.lowercased(), scheme == "spotiflac" else { return false }
|
||||
let host = (url.host ?? "").lowercased()
|
||||
let path = url.path.lowercased()
|
||||
let isSessionGrant = host == "session-grant"
|
||||
let ok =
|
||||
isSessionGrant || host == "callback" || host == "spotify-callback" || path.contains("callback")
|
||||
guard ok else { return false }
|
||||
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
|
||||
return false
|
||||
}
|
||||
let q = components.queryItems ?? []
|
||||
let code =
|
||||
q.first { $0.name == (isSessionGrant ? "grant" : "code") }?.value?.trimmingCharacters(
|
||||
in: .whitespacesAndNewlines) ??
|
||||
q.first { $0.name == "code" }?.value?.trimmingCharacters(
|
||||
in: .whitespacesAndNewlines) ?? ""
|
||||
let state =
|
||||
q.first { $0.name == "state" }?.value?.trimmingCharacters(
|
||||
in: .whitespacesAndNewlines) ?? ""
|
||||
if code.isEmpty { return false }
|
||||
if state.isEmpty {
|
||||
NSLog("SpotiFLAC: Extension OAuth redirect missing state (extension id)")
|
||||
return false
|
||||
}
|
||||
guard let route = ExtensionCallbackParser.parse(url) else { return false }
|
||||
streamQueue.async {
|
||||
var err: NSError?
|
||||
var response: String?
|
||||
if isSessionGrant {
|
||||
GobackendSetExtensionSessionGrantByID(state, code)
|
||||
response = GobackendInvokeExtensionActionJSON(state, "completeGrant", &err)
|
||||
if route.isSessionGrant {
|
||||
GobackendSetExtensionSessionGrantByID(route.extensionId, route.code)
|
||||
response = GobackendInvokeExtensionActionJSON(
|
||||
route.extensionId,
|
||||
"completeGrant",
|
||||
&err
|
||||
)
|
||||
} else {
|
||||
GobackendSetExtensionAuthCodeByID(state, code)
|
||||
response = GobackendInvokeExtensionActionJSON(state, "completeSpotifyLogin", &err)
|
||||
GobackendSetExtensionAuthCodeByID(route.extensionId, route.code)
|
||||
response = GobackendInvokeExtensionActionJSON(
|
||||
route.extensionId,
|
||||
"completeSpotifyLogin",
|
||||
&err
|
||||
)
|
||||
}
|
||||
if err == nil && isSessionGrant {
|
||||
if err == nil && route.isSessionGrant {
|
||||
do {
|
||||
try self.requireSuccessfulExtensionAction(
|
||||
extensionId: state,
|
||||
extensionId: route.extensionId,
|
||||
actionName: "completeGrant",
|
||||
response: response
|
||||
)
|
||||
@@ -155,9 +140,11 @@ import Gobackend
|
||||
if let err = err {
|
||||
NSLog(
|
||||
"SpotiFLAC: Extension callback complete failed: \(err.localizedDescription)")
|
||||
} else if isSessionGrant {
|
||||
} else if route.isSessionGrant {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.notifySessionGrantCompleted(extensionId: state)
|
||||
self?.notifySessionGrantCompleted(
|
||||
extensionId: route.extensionId
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import Foundation
|
||||
|
||||
struct ExtensionCallbackRoute: Equatable {
|
||||
let code: String
|
||||
let extensionId: String
|
||||
let isSessionGrant: Bool
|
||||
}
|
||||
|
||||
enum ExtensionCallbackParser {
|
||||
static func parse(_ url: URL) -> ExtensionCallbackRoute? {
|
||||
guard url.scheme?.lowercased() == "spotiflac" else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let host = (url.host ?? "").lowercased()
|
||||
let path = url.path.lowercased()
|
||||
let isSessionGrant = host == "session-grant"
|
||||
let isSupportedCallback =
|
||||
isSessionGrant ||
|
||||
host == "callback" ||
|
||||
host == "spotify-callback" ||
|
||||
path.contains("callback")
|
||||
guard isSupportedCallback,
|
||||
let components = URLComponents(
|
||||
url: url,
|
||||
resolvingAgainstBaseURL: false
|
||||
) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let queryItems = components.queryItems ?? []
|
||||
let primaryCodeKey = isSessionGrant ? "grant" : "code"
|
||||
let code =
|
||||
queryItems.first { $0.name == primaryCodeKey }?.value?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
?? queryItems.first { $0.name == "code" }?.value?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
?? ""
|
||||
let extensionId =
|
||||
queryItems.first { $0.name == "state" }?.value?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
?? ""
|
||||
|
||||
guard !code.isEmpty, !extensionId.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
return ExtensionCallbackRoute(
|
||||
code: code,
|
||||
extensionId: extensionId,
|
||||
isSessionGrant: isSessionGrant
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,61 @@
|
||||
import Flutter
|
||||
import UIKit
|
||||
import Foundation
|
||||
import XCTest
|
||||
@testable import Runner
|
||||
|
||||
class RunnerTests: XCTestCase {
|
||||
func testParsesOAuthCallback() {
|
||||
let route = ExtensionCallbackParser.parse(
|
||||
URL(string: "spotiflac://callback?code=auth-code&state=spotify-web")!
|
||||
)
|
||||
|
||||
func testExample() {
|
||||
// If you add code to the Runner application, consider adding tests here.
|
||||
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
|
||||
}
|
||||
XCTAssertEqual(
|
||||
route,
|
||||
ExtensionCallbackRoute(
|
||||
code: "auth-code",
|
||||
extensionId: "spotify-web",
|
||||
isSessionGrant: false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func testParsesSignedSessionGrant() {
|
||||
let route = ExtensionCallbackParser.parse(
|
||||
URL(
|
||||
string:
|
||||
"spotiflac://session-grant?grant=session-token&state=provider"
|
||||
)!
|
||||
)
|
||||
|
||||
XCTAssertEqual(
|
||||
route,
|
||||
ExtensionCallbackRoute(
|
||||
code: "session-token",
|
||||
extensionId: "provider",
|
||||
isSessionGrant: true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func testRejectsUntrustedOrIncompleteCallbacks() {
|
||||
XCTAssertNil(
|
||||
ExtensionCallbackParser.parse(
|
||||
URL(string: "https://callback?code=auth&state=provider")!
|
||||
)
|
||||
)
|
||||
XCTAssertNil(
|
||||
ExtensionCallbackParser.parse(
|
||||
URL(string: "spotiflac://unknown?code=auth&state=provider")!
|
||||
)
|
||||
)
|
||||
XCTAssertNil(
|
||||
ExtensionCallbackParser.parse(
|
||||
URL(string: "spotiflac://callback?code=auth")!
|
||||
)
|
||||
)
|
||||
XCTAssertNil(
|
||||
ExtensionCallbackParser.parse(
|
||||
URL(string: "spotiflac://callback?state=provider")!
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user