fix: harden gateway password auth
This commit is contained in:
@@ -26,15 +26,24 @@ actor GatewayEndpointStore {
|
|||||||
mode: { await MainActor.run { AppStateStore.shared.connectionMode } },
|
mode: { await MainActor.run { AppStateStore.shared.connectionMode } },
|
||||||
token: { ProcessInfo.processInfo.environment["CLAWDIS_GATEWAY_TOKEN"] },
|
token: { ProcessInfo.processInfo.environment["CLAWDIS_GATEWAY_TOKEN"] },
|
||||||
password: {
|
password: {
|
||||||
// First check environment variable
|
|
||||||
let raw = ProcessInfo.processInfo.environment["CLAWDIS_GATEWAY_PASSWORD"] ?? ""
|
let raw = ProcessInfo.processInfo.environment["CLAWDIS_GATEWAY_PASSWORD"] ?? ""
|
||||||
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
if !trimmed.isEmpty {
|
if !trimmed.isEmpty {
|
||||||
return trimmed
|
return trimmed
|
||||||
}
|
}
|
||||||
// Then check config file based on connection mode
|
|
||||||
let root = ClawdisConfigFile.loadDict()
|
let root = ClawdisConfigFile.loadDict()
|
||||||
// Check gateway.auth.password (for local gateway auth)
|
if CommandResolver.connectionModeIsRemote() {
|
||||||
|
if let gateway = root["gateway"] as? [String: Any],
|
||||||
|
let remote = gateway["remote"] as? [String: Any],
|
||||||
|
let password = remote["password"] as? String
|
||||||
|
{
|
||||||
|
let pw = password.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
if !pw.isEmpty {
|
||||||
|
return pw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if let gateway = root["gateway"] as? [String: Any],
|
if let gateway = root["gateway"] as? [String: Any],
|
||||||
let auth = gateway["auth"] as? [String: Any],
|
let auth = gateway["auth"] as? [String: Any],
|
||||||
let password = auth["password"] as? String
|
let password = auth["password"] as? String
|
||||||
@@ -44,16 +53,6 @@ actor GatewayEndpointStore {
|
|||||||
return pw
|
return pw
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Check gateway.remote.password (for remote gateway auth)
|
|
||||||
if let gateway = root["gateway"] as? [String: Any],
|
|
||||||
let remote = gateway["remote"] as? [String: Any],
|
|
||||||
let password = remote["password"] as? String
|
|
||||||
{
|
|
||||||
let pw = password.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
if !pw.isEmpty {
|
|
||||||
return pw
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
localPort: { GatewayEnvironment.gatewayPort() },
|
localPort: { GatewayEnvironment.gatewayPort() },
|
||||||
|
|||||||
@@ -72,15 +72,17 @@ enum GatewayLaunchAgentManager {
|
|||||||
<string>sips</string>
|
<string>sips</string>
|
||||||
"""
|
"""
|
||||||
if let token {
|
if let token {
|
||||||
|
let escapedToken = self.escapePlistValue(token)
|
||||||
envEntries += """
|
envEntries += """
|
||||||
<key>CLAWDIS_GATEWAY_TOKEN</key>
|
<key>CLAWDIS_GATEWAY_TOKEN</key>
|
||||||
<string>\(token)</string>
|
<string>\(escapedToken)</string>
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
if let password {
|
if let password {
|
||||||
|
let escapedPassword = self.escapePlistValue(password)
|
||||||
envEntries += """
|
envEntries += """
|
||||||
<key>CLAWDIS_GATEWAY_PASSWORD</key>
|
<key>CLAWDIS_GATEWAY_PASSWORD</key>
|
||||||
<string>\(password)</string>
|
<string>\(escapedPassword)</string>
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
let plist = """
|
let plist = """
|
||||||
@@ -171,6 +173,15 @@ enum GatewayLaunchAgentManager {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func escapePlistValue(_ raw: String) -> String {
|
||||||
|
raw
|
||||||
|
.replacingOccurrences(of: "&", with: "&")
|
||||||
|
.replacingOccurrences(of: "<", with: "<")
|
||||||
|
.replacingOccurrences(of: ">", with: ">")
|
||||||
|
.replacingOccurrences(of: "\"", with: """)
|
||||||
|
.replacingOccurrences(of: "'", with: "'")
|
||||||
|
}
|
||||||
|
|
||||||
private struct LaunchctlResult {
|
private struct LaunchctlResult {
|
||||||
let status: Int32
|
let status: Int32
|
||||||
let output: String
|
let output: String
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import Testing
|
|||||||
let store = GatewayEndpointStore(deps: .init(
|
let store = GatewayEndpointStore(deps: .init(
|
||||||
mode: { mode.get() },
|
mode: { mode.get() },
|
||||||
token: { "t" },
|
token: { "t" },
|
||||||
|
password: { nil },
|
||||||
localPort: { 1234 },
|
localPort: { 1234 },
|
||||||
remotePortIfRunning: { nil },
|
remotePortIfRunning: { nil },
|
||||||
ensureRemoteTunnel: { 18789 }))
|
ensureRemoteTunnel: { 18789 }))
|
||||||
@@ -44,6 +45,7 @@ import Testing
|
|||||||
let store = GatewayEndpointStore(deps: .init(
|
let store = GatewayEndpointStore(deps: .init(
|
||||||
mode: { mode.get() },
|
mode: { mode.get() },
|
||||||
token: { nil },
|
token: { nil },
|
||||||
|
password: { nil },
|
||||||
localPort: { 18789 },
|
localPort: { 18789 },
|
||||||
remotePortIfRunning: { nil },
|
remotePortIfRunning: { nil },
|
||||||
ensureRemoteTunnel: { 18789 }))
|
ensureRemoteTunnel: { 18789 }))
|
||||||
@@ -58,6 +60,7 @@ import Testing
|
|||||||
let store = GatewayEndpointStore(deps: .init(
|
let store = GatewayEndpointStore(deps: .init(
|
||||||
mode: { mode.get() },
|
mode: { mode.get() },
|
||||||
token: { "tok" },
|
token: { "tok" },
|
||||||
|
password: { "pw" },
|
||||||
localPort: { 1 },
|
localPort: { 1 },
|
||||||
remotePortIfRunning: { 5555 },
|
remotePortIfRunning: { 5555 },
|
||||||
ensureRemoteTunnel: { 5555 }))
|
ensureRemoteTunnel: { 5555 }))
|
||||||
@@ -69,13 +72,14 @@ import Testing
|
|||||||
_ = try await store.ensureRemoteControlTunnel()
|
_ = try await store.ensureRemoteControlTunnel()
|
||||||
|
|
||||||
let next = await iterator.next()
|
let next = await iterator.next()
|
||||||
guard case let .ready(mode, url, token) = next else {
|
guard case let .ready(mode, url, token, password) = next else {
|
||||||
Issue.record("expected .ready after ensure, got \(String(describing: next))")
|
Issue.record("expected .ready after ensure, got \(String(describing: next))")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
#expect(mode == .remote)
|
#expect(mode == .remote)
|
||||||
#expect(url.absoluteString == "ws://127.0.0.1:5555")
|
#expect(url.absoluteString == "ws://127.0.0.1:5555")
|
||||||
#expect(token == "tok")
|
#expect(token == "tok")
|
||||||
|
#expect(password == "pw")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func unconfiguredModeRejectsConfig() async {
|
@Test func unconfiguredModeRejectsConfig() async {
|
||||||
@@ -83,6 +87,7 @@ import Testing
|
|||||||
let store = GatewayEndpointStore(deps: .init(
|
let store = GatewayEndpointStore(deps: .init(
|
||||||
mode: { mode.get() },
|
mode: { mode.get() },
|
||||||
token: { nil },
|
token: { nil },
|
||||||
|
password: { nil },
|
||||||
localPort: { 18789 },
|
localPort: { 18789 },
|
||||||
remotePortIfRunning: { nil },
|
remotePortIfRunning: { nil },
|
||||||
ensureRemoteTunnel: { 18789 }))
|
ensureRemoteTunnel: { 18789 }))
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export async function callGateway<T = unknown>(
|
|||||||
(typeof opts.password === "string" && opts.password.trim().length > 0
|
(typeof opts.password === "string" && opts.password.trim().length > 0
|
||||||
? opts.password.trim()
|
? opts.password.trim()
|
||||||
: undefined) ||
|
: undefined) ||
|
||||||
(process.env.CLAWDIS_GATEWAY_PASSWORD?.trim()) ||
|
process.env.CLAWDIS_GATEWAY_PASSWORD?.trim() ||
|
||||||
(typeof remote?.password === "string" && remote.password.trim().length > 0
|
(typeof remote?.password === "string" && remote.password.trim().length > 0
|
||||||
? remote.password.trim()
|
? remote.password.trim()
|
||||||
: undefined);
|
: undefined);
|
||||||
|
|||||||
@@ -18,7 +18,13 @@ export type HookMappingResolved = {
|
|||||||
messageTemplate?: string;
|
messageTemplate?: string;
|
||||||
textTemplate?: string;
|
textTemplate?: string;
|
||||||
deliver?: boolean;
|
deliver?: boolean;
|
||||||
channel?: "last" | "whatsapp" | "telegram" | "discord" | "signal" | "imessage";
|
channel?:
|
||||||
|
| "last"
|
||||||
|
| "whatsapp"
|
||||||
|
| "telegram"
|
||||||
|
| "discord"
|
||||||
|
| "signal"
|
||||||
|
| "imessage";
|
||||||
to?: string;
|
to?: string;
|
||||||
thinking?: string;
|
thinking?: string;
|
||||||
timeoutSeconds?: number;
|
timeoutSeconds?: number;
|
||||||
@@ -50,7 +56,13 @@ export type HookAction =
|
|||||||
wakeMode: "now" | "next-heartbeat";
|
wakeMode: "now" | "next-heartbeat";
|
||||||
sessionKey?: string;
|
sessionKey?: string;
|
||||||
deliver?: boolean;
|
deliver?: boolean;
|
||||||
channel?: "last" | "whatsapp" | "telegram" | "discord" | "signal" | "imessage";
|
channel?:
|
||||||
|
| "last"
|
||||||
|
| "whatsapp"
|
||||||
|
| "telegram"
|
||||||
|
| "discord"
|
||||||
|
| "signal"
|
||||||
|
| "imessage";
|
||||||
to?: string;
|
to?: string;
|
||||||
thinking?: string;
|
thinking?: string;
|
||||||
timeoutSeconds?: number;
|
timeoutSeconds?: number;
|
||||||
@@ -86,7 +98,7 @@ type HookTransformResult = Partial<{
|
|||||||
name: string;
|
name: string;
|
||||||
sessionKey: string;
|
sessionKey: string;
|
||||||
deliver: boolean;
|
deliver: boolean;
|
||||||
channel: "last" | "whatsapp" | "telegram" | "discord";
|
channel: "last" | "whatsapp" | "telegram" | "discord" | "signal" | "imessage";
|
||||||
to: string;
|
to: string;
|
||||||
thinking: string;
|
thinking: string;
|
||||||
timeoutSeconds: number;
|
timeoutSeconds: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user