mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-08 11:14:51 +00:00
350 lines
19 KiB
Swift
350 lines
19 KiB
Swift
import Foundation
|
|
import Testing
|
|
|
|
@testable import ShadowsocksX_NG2
|
|
|
|
struct PluginRecordTests {
|
|
@Test(arguments: [
|
|
#"""
|
|
{"userPlugins":[{"id":"00000000-0000-0000-0000-000000000001","names":["tool"],
|
|
"path":"/missing/tool","arguments":[],"plugin_mode":"udp_only"}],"managedArguments":{}}
|
|
"""#,
|
|
#"""
|
|
{"userPlugins":[],"managedArguments":{},
|
|
"managedModes":{"v2ray-plugin":"udp_only"}}
|
|
"""#,
|
|
])
|
|
func unknownTransportModeBlocksReadingWithoutRewriting(configurationJSON: String) throws {
|
|
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
|
|
let store = PluginMappingFileStore(fileURL: root.appendingPathComponent("plugins.json"))
|
|
let original = Data(
|
|
"{\"version\":2,\"configuration\":\(configurationJSON)}".utf8)
|
|
try original.write(to: store.fileURL)
|
|
#expect(throws: PluginMappingError.unreadable) { try store.load() }
|
|
#expect(try Data(contentsOf: store.fileURL) == original)
|
|
}
|
|
|
|
@Test func oldV2RecordsDefaultToTCPWithoutRewriting() throws {
|
|
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
|
|
let store = PluginMappingFileStore(fileURL: root.appendingPathComponent("plugins.json"))
|
|
let original = Data(
|
|
#"""
|
|
{"version":2,"configuration":{"userPlugins":[
|
|
{"id":"00000000-0000-0000-0000-000000000001","names":["tool"],
|
|
"path":"/missing/tool","arguments":[]}],"managedArguments":{}}}
|
|
"""#
|
|
.utf8)
|
|
try original.write(to: store.fileURL)
|
|
let configuration = try store.load()
|
|
#expect(configuration.userPlugins.first?.mode == .tcpOnly)
|
|
#expect(
|
|
PluginCatalogSnapshot(configuration: configuration).managedRecords["v2ray-plugin"]?.mode
|
|
== .tcpOnly)
|
|
#expect(try Data(contentsOf: store.fileURL) == original)
|
|
try store.save(configuration)
|
|
#expect(try store.load() == configuration)
|
|
}
|
|
|
|
@Test @MainActor func managedModePersistsAcrossAliasesAndArgumentClearing() throws {
|
|
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
let binary = root.appendingPathComponent("Contents/Helpers/Plugins/v2ray-plugin")
|
|
try FileManager.default.createDirectory(
|
|
at: binary.deletingLastPathComponent(), withIntermediateDirectories: true)
|
|
try Data("#!/bin/sh\n".utf8).write(to: binary)
|
|
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: binary.path)
|
|
let store = PluginMappingFileStore(fileURL: root.appendingPathComponent("plugins.json"))
|
|
let managed = BundleManagedPluginProvider(bundleURL: root)
|
|
let initial = PluginCatalog(store: store, managed: managed, inspector: QuietPluginInspector())
|
|
let originalArguments = try #require(initial.resolve(program: "v2ray-plugin")).arguments
|
|
try Data(
|
|
#"""
|
|
{"version":2,"configuration":{"userPlugins":[],
|
|
"managedArguments":{"v2ray-plugin":["--verbose"]},
|
|
"managedModes":{"v2ray-plugin":"tcp_and_udp"}}}
|
|
"""#
|
|
.utf8
|
|
).write(to: store.fileURL)
|
|
let catalog = PluginCatalog(store: store, managed: managed, inspector: QuietPluginInspector())
|
|
#expect(catalog.resolve(program: "v2ray-plugin-v1")?.mode == .tcpAndUDP)
|
|
try catalog.commit([.managedArguments(program: "v2ray-plugin", arguments: [])])
|
|
let restored = PluginCatalog(store: store, managed: managed, inspector: QuietPluginInspector())
|
|
#expect(restored.resolve(program: "v2ray-plugin")?.mode == .tcpAndUDP)
|
|
#expect(restored.resolve(program: "v2ray-plugin")?.arguments == originalArguments)
|
|
try restored.commit([.save(UserPluginRecord(names: ["v2ray-plugin"], path: binary.path))])
|
|
#expect(restored.resolve(program: "v2ray-plugin")?.mode == .tcpOnly)
|
|
#expect(restored.resolve(program: "v2ray-plugin-v1")?.mode == .tcpAndUDP)
|
|
}
|
|
|
|
@Test @MainActor func nameConflictAndInvalidArgumentsDoNotPublish() throws {
|
|
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
|
|
let binary = root.appendingPathComponent("tool")
|
|
try Data("#!/bin/sh\n".utf8).write(to: binary)
|
|
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: binary.path)
|
|
let store = PluginMappingFileStore(fileURL: root.appendingPathComponent("plugins.json"))
|
|
let catalog = PluginCatalog(store: store, inspector: QuietPluginInspector())
|
|
let first = UserPluginRecord(names: ["tool", "tool-v2"], path: binary.path)
|
|
try catalog.commit([.save(first)])
|
|
let bytes = try Data(contentsOf: store.fileURL)
|
|
let generation = catalog.catalogSnapshot().generation
|
|
#expect(throws: PluginMappingError.nameExists) {
|
|
try catalog.commit([.save(UserPluginRecord(names: ["tool-v2"], path: binary.path))])
|
|
}
|
|
#expect(throws: PluginMappingError.invalidArguments) {
|
|
try catalog.commit([.managedArguments(program: "v2ray-plugin", arguments: ["bad\0value"])])
|
|
}
|
|
#expect(try Data(contentsOf: store.fileURL) == bytes)
|
|
#expect(catalog.catalogSnapshot().generation == generation)
|
|
#expect(catalog.userRecords == [first])
|
|
var renamed = first
|
|
renamed.names = ["tool-v2"]
|
|
try catalog.commit([.save(renamed)])
|
|
#expect(catalog.resolve(program: "tool") == nil)
|
|
#expect(catalog.userRecords.first?.id == first.id)
|
|
#expect(catalog.resolve(program: "tool-v2")?.path == binary.path)
|
|
}
|
|
|
|
@Test func legacyMappingsMigrateIndependentlyAndReadDoesNotRewriteFile() throws {
|
|
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
|
|
let store = PluginMappingFileStore(fileURL: root.appendingPathComponent("plugins.json"))
|
|
let original = Data(
|
|
#"{"version":1,"mappings":{"tool":"/missing/tool","tool-v2":"/missing/tool"}}"#.utf8)
|
|
try original.write(to: store.fileURL)
|
|
let configuration = try store.load()
|
|
#expect(configuration.userPlugins.count == 2)
|
|
#expect(Set(configuration.userPlugins.map(\.id)).count == 2)
|
|
#expect(configuration.userPlugins.map(\.names) == [["tool"], ["tool-v2"]])
|
|
#expect(
|
|
configuration.userPlugins.allSatisfy {
|
|
$0.arguments.isEmpty && $0.path == "/missing/tool" && $0.mode == .tcpOnly
|
|
})
|
|
#expect(try Data(contentsOf: store.fileURL) == original)
|
|
try store.save(configuration)
|
|
#expect(try store.load() == configuration)
|
|
let json = try #require(
|
|
JSONSerialization.jsonObject(with: Data(contentsOf: store.fileURL)) as? [String: Any])
|
|
#expect(json["version"] as? Int == 2)
|
|
}
|
|
|
|
@Test(arguments: [PluginTransportMode.tcpOnly, .tcpAndUDP])
|
|
@MainActor func activationWritesLocalArgumentsWithoutChangingServerOptions(
|
|
mode: PluginTransportMode
|
|
) throws {
|
|
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
|
|
let binary = root.appendingPathComponent("tool")
|
|
try Data("#!/bin/sh\n".utf8).write(to: binary)
|
|
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: binary.path)
|
|
let plugins = PluginCatalog(
|
|
store: PluginMappingFileStore(fileURL: root.appendingPathComponent("plugins.json")),
|
|
inspector: QuietPluginInspector())
|
|
try plugins.commit([
|
|
.save(
|
|
UserPluginRecord(
|
|
names: ["tool", "tool-v2"], path: binary.path,
|
|
arguments: ["--threads", "4", "--log-file", "/tmp/My Log.txt", ""], mode: mode))
|
|
])
|
|
var catalog = ConfigurationCatalog()
|
|
let leaf = try ActivationFixture.addPlainServer(
|
|
"Plugin", in: &catalog)
|
|
var fields = try ActivationFixture.serverFields(of: leaf, in: catalog)
|
|
fields.pluginProgram = "tool-v2"
|
|
fields.pluginOptions = "options"
|
|
fields.pluginOptions = "tls;host=example.com"
|
|
try catalog.updateServer(leaf, with: fields)
|
|
var machine = ActivationStateMachine()
|
|
let runtime = try machine.activate(
|
|
leaf, in: catalog, plugins: plugins,
|
|
options: RuntimeDocumentOptions(listen: ActivationFixture.listen))
|
|
let json = try #require(
|
|
JSONSerialization.jsonObject(with: runtime.document.jsonData()) as? [String: Any])
|
|
let server = try #require((json["servers"] as? [[String: Any]])?.first)
|
|
#expect(
|
|
server["plugin_args"] as? [String] == ["--threads", "4", "--log-file", "/tmp/My Log.txt", ""])
|
|
#expect(server["plugin_mode"] as? String == (mode == .tcpAndUDP ? "tcp_and_udp" : nil))
|
|
#expect(server["plugin"] as? String == binary.path)
|
|
#expect(server["plugin_opts"] as? String == "tls;host=example.com")
|
|
#expect(try ActivationFixture.serverFields(of: leaf, in: catalog).pluginProgram == "tool-v2")
|
|
}
|
|
|
|
@Test(arguments: ["v2ray-plugin", "v2ray-plugin-v1"], [false, true])
|
|
@MainActor func activationAppliesAccessLogDefaultOnlyToManagedPlugins(
|
|
program: String, overrideManaged: Bool
|
|
) throws {
|
|
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
let binary = root.appendingPathComponent("Contents/Helpers/Plugins/v2ray-plugin")
|
|
try FileManager.default.createDirectory(
|
|
at: binary.deletingLastPathComponent(), withIntermediateDirectories: true)
|
|
try Data("#!/bin/sh\n".utf8).write(to: binary)
|
|
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: binary.path)
|
|
let plugins = PluginCatalog(
|
|
store: PluginMappingFileStore(fileURL: root.appendingPathComponent("plugins.json")),
|
|
managed: BundleManagedPluginProvider(bundleURL: root), inspector: QuietPluginInspector())
|
|
try plugins.commit([.managedArguments(program: "v2ray-plugin", arguments: ["--verbose"])])
|
|
if overrideManaged {
|
|
try plugins.commit([
|
|
.save(UserPluginRecord(names: [program], path: binary.path, arguments: ["--user-option"]))
|
|
])
|
|
}
|
|
var catalog = ConfigurationCatalog()
|
|
let leaf = try ActivationFixture.addPlainServer("Plugin", in: &catalog)
|
|
var fields = try ActivationFixture.serverFields(of: leaf, in: catalog)
|
|
fields.pluginProgram = program
|
|
fields.pluginOptions = "tls;host=example.com"
|
|
try catalog.updateServer(leaf, with: fields)
|
|
var machine = ActivationStateMachine()
|
|
let runtime = try machine.activate(
|
|
leaf, in: catalog, plugins: plugins,
|
|
options: RuntimeDocumentOptions(listen: ActivationFixture.listen))
|
|
let json = try #require(
|
|
JSONSerialization.jsonObject(with: runtime.document.jsonData()) as? [String: Any])
|
|
let server = try #require((json["servers"] as? [[String: Any]])?.first)
|
|
#expect(
|
|
server["plugin_args"] as? [String]
|
|
== (overrideManaged ? ["--user-option"] : ["-disableAccessLog", "--verbose"]))
|
|
#expect(server["plugin"] as? String == binary.path)
|
|
#expect(server["plugin_opts"] as? String == "tls;host=example.com")
|
|
}
|
|
|
|
@Test @MainActor func aliasesShareArgumentsAndOnlyExplicitNamesOverrideManaged() throws {
|
|
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
let binary = root.appendingPathComponent("Contents/Helpers/Plugins/v2ray-plugin")
|
|
try FileManager.default.createDirectory(
|
|
at: binary.deletingLastPathComponent(), withIntermediateDirectories: true)
|
|
try Data("#!/bin/sh\n".utf8).write(to: binary)
|
|
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: binary.path)
|
|
let store = PluginMappingFileStore(fileURL: root.appendingPathComponent("plugins.json"))
|
|
let catalog = PluginCatalog(
|
|
store: store, managed: BundleManagedPluginProvider(bundleURL: root),
|
|
inspector: QuietPluginInspector())
|
|
let originalAlias = try #require(catalog.resolve(program: "v2ray-plugin-v1"))
|
|
let record = UserPluginRecord(
|
|
names: ["v2ray-plugin", "custom-v2"], path: binary.path,
|
|
arguments: ["--config", "/Users/me/My Config.json", "", "--verbose"])
|
|
try catalog.commit([.save(record)])
|
|
let snapshot = catalog.catalogSnapshot()
|
|
#expect(snapshot.resolve(program: "custom-v2")?.arguments == record.arguments)
|
|
#expect(snapshot.resolve(program: "v2ray-plugin") == snapshot.resolve(program: "custom-v2"))
|
|
#expect(snapshot.entry(for: "v2ray-plugin-v1")?.source == .managed)
|
|
#expect(snapshot.resolve(program: "v2ray-plugin-v1") == originalAlias)
|
|
let restored = PluginCatalog(store: store, inspector: QuietPluginInspector())
|
|
#expect(restored.userRecords == [record])
|
|
}
|
|
|
|
}
|
|
|
|
extension PluginRecordTests {
|
|
@Test @MainActor func cloakOverrideOnlyReplacesExplicitAlias() throws {
|
|
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
let client = root.appendingPathComponent("Contents/Helpers/Plugins/ck-client")
|
|
try FileManager.default.createDirectory(
|
|
at: client.deletingLastPathComponent(),
|
|
withIntermediateDirectories: true)
|
|
try Data("#!/bin/sh\n".utf8).write(to: client)
|
|
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: client.path)
|
|
let catalog = PluginCatalog(
|
|
store: PluginMappingFileStore(fileURL: root.appendingPathComponent("plugins.json")),
|
|
managed: BundleManagedPluginProvider(bundleURL: root), inspector: QuietPluginInspector())
|
|
try catalog.commit([.managedArguments(program: "ck-client", arguments: ["-v"])])
|
|
let userClient = root.appendingPathComponent("user-client")
|
|
try Data("#!/bin/sh\n".utf8).write(to: userClient)
|
|
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: userClient.path)
|
|
let user = UserPluginRecord(
|
|
names: ["cloak"], path: userClient.path, arguments: ["-c", "local.json"])
|
|
try catalog.commit([.save(user)])
|
|
#expect(catalog.resolve(program: "cloak")?.arguments == user.arguments)
|
|
#expect(catalog.resolve(program: "cloak-v2")?.arguments == ["-v"])
|
|
#expect(catalog.catalogSnapshot().entry(for: "cloak")?.source == .user)
|
|
#expect(catalog.catalogSnapshot().entry(for: "cloak-v2")?.source == .managed)
|
|
try FileManager.default.removeItem(at: userClient)
|
|
#expect(catalog.resolve(program: "cloak") == nil)
|
|
#expect(catalog.resolve(program: "cloak-v2")?.path == client.path)
|
|
#expect(catalog.catalogSnapshot().entry(for: "cloak")?.source == .user)
|
|
try catalog.commit([.remove(user.id)])
|
|
#expect(catalog.resolve(program: "cloak")?.arguments == ["-v"])
|
|
}
|
|
}
|
|
|
|
extension PluginRecordTests {
|
|
@Test @MainActor func sharingKeepsReferenceAndOptionsWithoutLocalArguments() async throws {
|
|
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
|
|
let binary = root.appendingPathComponent("tool")
|
|
try Data("#!/bin/sh\n".utf8).write(to: binary)
|
|
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: binary.path)
|
|
let plugins = PluginCatalog(
|
|
store: PluginMappingFileStore(fileURL: root.appendingPathComponent("plugins.json")),
|
|
inspector: QuietPluginInspector())
|
|
try plugins.commit([
|
|
.save(
|
|
UserPluginRecord(
|
|
names: ["tool", "tool-v2"], path: binary.path,
|
|
arguments: ["--local-argument-sentinel"], mode: .tcpAndUDP))
|
|
])
|
|
let workflow = makeCatalogWorkflow(
|
|
coordinator: CatalogCommitCoordinator(
|
|
fileStore: makeTestCatalogStore(fileURL: root.appendingPathComponent("catalog.json")),
|
|
runtime: FakeCatalogRuntime()), plugins: plugins)
|
|
let group = try await workflow.createGroup(named: "Share", into: nil)
|
|
let id = try await workflow.createServer(
|
|
ServerEditDraft(
|
|
address: "example.com", port: 8388,
|
|
encryptionMethod: "aes-256-gcm", password: "pw", remark: "Plugin",
|
|
plugin: .named(program: "tool-v2"), pluginOptions: "tls;host=example.com"), into: group)
|
|
let draft = try workflow.configurationGroupExportDraft(for: group)
|
|
let json = try #require(JSONSerialization.jsonObject(with: draft.data) as? [String: Any])
|
|
let server = try #require((json["servers"] as? [[String: Any]])?.first)
|
|
#expect(server["plugin"] as? String == "tool-v2")
|
|
#expect(server["plugin_opts"] as? String == "tls;host=example.com")
|
|
#expect(server["plugin_args"] == nil)
|
|
#expect(server["plugin_mode"] == nil)
|
|
let uri = try workflow.shareURI(for: id)
|
|
#expect(uri.contains("tool-v2"))
|
|
#expect(!uri.contains("local-argument-sentinel"))
|
|
#expect(!uri.contains("tcp_and_udp"))
|
|
let exportedText = try #require(String(bytes: draft.data, encoding: .utf8))
|
|
#expect(!exportedText.contains(binary.path))
|
|
}
|
|
|
|
@Test @MainActor func managedArgumentsRemainWithUncoveredAliasesAndCanBeCleared() throws {
|
|
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
let binary = root.appendingPathComponent("Contents/Helpers/Plugins/v2ray-plugin")
|
|
try FileManager.default.createDirectory(
|
|
at: binary.deletingLastPathComponent(), withIntermediateDirectories: true)
|
|
try Data("#!/bin/sh\n".utf8).write(to: binary)
|
|
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: binary.path)
|
|
let store = PluginMappingFileStore(fileURL: root.appendingPathComponent("plugins.json"))
|
|
let managed = BundleManagedPluginProvider(bundleURL: root)
|
|
let catalog = PluginCatalog(store: store, managed: managed, inspector: QuietPluginInspector())
|
|
let originalArguments = try #require(catalog.resolve(program: "v2ray-plugin-v1")).arguments
|
|
try catalog.commit([.managedArguments(program: "v2ray-plugin", arguments: ["--verbose"])])
|
|
let configuredAlias = try #require(catalog.resolve(program: "v2ray-plugin-v1"))
|
|
#expect(configuredAlias.arguments == originalArguments + ["--verbose"])
|
|
let user = UserPluginRecord(
|
|
names: ["v2ray-plugin"], path: binary.path, arguments: ["--threads", "4"])
|
|
try catalog.commit([.save(user)])
|
|
#expect(catalog.resolve(program: "v2ray-plugin")?.arguments == ["--threads", "4"])
|
|
let restored = PluginCatalog(store: store, managed: managed, inspector: QuietPluginInspector())
|
|
#expect(restored.resolve(program: "v2ray-plugin-v1") == configuredAlias)
|
|
try restored.commit([.managedArguments(program: "v2ray-plugin", arguments: [])])
|
|
#expect(restored.resolve(program: "v2ray-plugin-v1")?.arguments == originalArguments)
|
|
#expect(restored.resolve(program: "v2ray-plugin")?.arguments == ["--threads", "4"])
|
|
try restored.commit([.remove(user.id)])
|
|
#expect(restored.resolve(program: "v2ray-plugin")?.arguments == originalArguments)
|
|
}
|
|
}
|