Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- `%h` and the other `~/.ssh/config` tokens reaching the connection as literal text. (#2687)
- Trailing comments on a `~/.ssh/config` line kept as part of the value. (#2687)
- `Host` blocks matched against a substituted `HostName` rather than the host as typed. (#2687)
- `%p` and `%r` left unexpanded in a `Match exec` command. (#2687)
- `Match !host` and the other negated `Match` criteria matching every host. (#2687)
- `Include` lines naming more than one file reading none of them. (#2687)
- `Include` inside a `Host` block applying to every connection. (#2687)
- The same file included from a second `Host` block contributing nothing. (#2687)
- `Match final` overriding values earlier blocks had already set. (#2687)
- `IdentityFile` entries from earlier blocks dropped by a later `Match` block. (#2687)
- Comma-separated `Host` patterns treated as a list, which `Match host` alone accepts. (#2687)
- A `Match exec` command that ignores `SIGTERM` hanging the connection. (#2687)
- Whole-result copy after Select All ignoring the 50,000-row clipboard limit. (#2667)
- Row-gutter geometry observer left registered every time a data grid was rebuilt. (#2667)
- Unsaved cell edits discarded without asking when a column was hidden, shown or reset. (#2667)
Expand Down
8 changes: 8 additions & 0 deletions TablePro/Core/SSH/LibSSH2TunnelFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,14 @@ internal enum LibSSH2TunnelFactory {
let resolvedJumps: [ResolvedSSHTarget] = (formJumps.isEmpty ? resolvedPrimary.proxyJump : formJumps)
.map { SSHConfigResolver.resolve($0, document: document) }

// A value whose tokens could not be expanded is reported by name. Dialling it anyway is how
// `Hostname %h` reached getaddrinfo and came back as a DNS failure for a two-character host.
for target in [resolvedPrimary] + resolvedJumps {
if let failure = target.expansionFailure {
throw SSHTunnelError.configExpansionFailed(failure.explanation)
}
}

if resolvedPrimary.username.isEmpty {
throw SSHTunnelError.tunnelCreationFailed(
"SSH username not set. Add it to the form or set `User` for `\(config.host)` in ~/.ssh/config."
Expand Down
33 changes: 33 additions & 0 deletions TablePro/Core/SSH/ResolvedSSHTarget.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,37 @@ struct ResolvedSSHTarget: Sendable, Hashable {
let useKeychain: Bool
let addKeysToAgent: Bool
let proxyJump: [SSHJumpHost]
/// The first `~/.ssh/config` value whose tokens could not be expanded. Resolution keeps going
/// so the rest of the target is still built, and the connect path reports this instead of
/// dialling whatever half-resolved string came out. It defaults to nil because most callers,
/// the tests included, build a target that never went through expansion.
var expansionFailure: SSHTokenExpansionError?

init(
originalHost: String,
host: String,
port: Int,
username: String,
identityFiles: [String],
agentSocketPath: String,
agentSocketOrigin: AgentSocketOrigin,
identitiesOnly: Bool,
useKeychain: Bool,
addKeysToAgent: Bool,
proxyJump: [SSHJumpHost],
expansionFailure: SSHTokenExpansionError? = nil
) {
self.originalHost = originalHost
self.host = host
self.port = port
self.username = username
self.identityFiles = identityFiles
self.agentSocketPath = agentSocketPath
self.agentSocketOrigin = agentSocketOrigin
self.identitiesOnly = identitiesOnly
self.useKeychain = useKeychain
self.addKeysToAgent = addKeysToAgent
self.proxyJump = proxyJump
self.expansionFailure = expansionFailure
}
}
16 changes: 15 additions & 1 deletion TablePro/Core/SSH/SSHConfigDocument.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ struct HostPattern: Sendable, Hashable {
let negated: Bool
}

enum MatchCondition: Sendable, Hashable {
enum MatchTest: Sendable, Hashable {
case all
case canonical
case final
Expand All @@ -39,6 +39,19 @@ enum MatchCondition: Sendable, Hashable {
case exec(command: String)
}

/// A `Match` criterion and whether a leading `!` negated it. ssh evaluates the criterion and then
/// inverts the answer, which is not the same as negating each pattern in the list: `Match !host a,b`
/// has to match every host except `a` and `b`, where a list of negated patterns matches nothing.
struct MatchCondition: Sendable, Hashable {
let test: MatchTest
let negated: Bool

init(test: MatchTest, negated: Bool = false) {
self.test = test
self.negated = negated
}
}

enum CanonicalizeMode: String, Sendable, Hashable {
case no
case yes
Expand All @@ -49,6 +62,7 @@ enum SSHDirective: Sendable, Hashable {
case hostName(String)
case port(Int)
case user(String)
case hostKeyAlias(String)
case identityFile(String)
case identityAgent(String)
case identitiesOnly(Bool)
Expand Down
Loading
Loading