Skip to content

Commit 8cd3559

Browse files
committed
init file from sshd_config_default on Windows
1 parent 44a6b07 commit 8cd3559

7 files changed

Lines changed: 173 additions & 27 deletions

File tree

resources/sshdconfig/locales/en-us.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ writingTempConfig = "Writing temporary sshd_config file"
9999
[util]
100100
cleanupFailed = "Failed to clean up temporary file %{path}: %{error}"
101101
getIgnoresInputFilters = "get command does not support filtering based on input settings, provided input will be ignored"
102+
seededConfigFromDefault = "Seeded missing sshd_config from '%{source}' to '%{target}'"
103+
sshdConfigDefaultNotFound = "sshd_config file does not exist and no default source could be found. Checked: %{paths}"
102104
sshdConfigReadFailed = "failed to read sshd_config at path: '%{path}'"
103105
sshdElevation = "elevated security context required"
104106
tempFileCreated = "temporary file created at: %{path}"

resources/sshdconfig/src/set.rs

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ use crate::repeat_keyword::{
2424
RepeatInput, RepeatListInput, NameValueEntry,
2525
add_or_update_entry, extract_single_keyword, remove_entry, parse_and_validate_entries
2626
};
27-
use crate::util::{build_command_info, get_default_sshd_config_path, invoke_sshd_config_validation};
27+
use crate::util::{build_command_info, ensure_sshd_config_exists, get_default_sshd_config_path, invoke_sshd_config_validation};
2828

2929
/// Invoke the set command.
3030
///
@@ -189,16 +189,9 @@ fn set_sshd_config(cmd_info: &mut CommandInfo) -> Result<(), SshdConfigError> {
189189
let mut get_cmd_info = cmd_info.clone();
190190
get_cmd_info.include_defaults = false;
191191
get_cmd_info.input = Map::new();
192+
ensure_sshd_config_exists(get_cmd_info.metadata.filepath.clone())?;
192193

193-
let mut existing_config = match get_sshd_settings(&get_cmd_info, true) {
194-
Ok(config) => config,
195-
Err(SshdConfigError::FileNotFound(_)) => {
196-
return Err(SshdConfigError::InvalidInput(
197-
t!("set.purgeFalseRequiresExistingFile").to_string()
198-
));
199-
}
200-
Err(e) => return Err(e),
201-
};
194+
let mut existing_config = get_sshd_settings(&get_cmd_info, true)?;
202195
for (key, value) in &cmd_info.input {
203196
if value.is_null() {
204197
existing_config.remove(key);
@@ -281,12 +274,6 @@ fn get_existing_config(cmd_info: &CommandInfo) -> Result<Map<String, Value>, Ssh
281274
let mut get_cmd_info = cmd_info.clone();
282275
get_cmd_info.include_defaults = false;
283276
get_cmd_info.input = Map::new();
284-
match get_sshd_settings(&get_cmd_info, false) {
285-
Ok(config) => Ok(config),
286-
Err(SshdConfigError::FileNotFound(_)) => {
287-
// If file doesn't exist, create empty config
288-
Ok(Map::new())
289-
}
290-
Err(e) => Err(e),
291-
}
277+
ensure_sshd_config_exists(get_cmd_info.metadata.filepath.clone())?;
278+
get_sshd_settings(&get_cmd_info, false)
292279
}

resources/sshdconfig/src/util.rs

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,59 @@ pub fn get_default_sshd_config_path(input: Option<PathBuf>) -> Result<PathBuf, S
107107
}
108108
}
109109

110+
fn get_sshd_config_default_source_candidates() -> Vec<PathBuf> {
111+
let mut candidates: Vec<PathBuf> = Vec::new();
112+
113+
if cfg!(windows) {
114+
if let Ok(system_drive) = std::env::var("SystemDrive") {
115+
candidates.push(PathBuf::from(format!("{system_drive}\\Windows\\System32\\OpenSSH\\sshd_config_default")));
116+
}
117+
}
118+
119+
candidates
120+
}
121+
122+
/// Ensure the target `sshd_config` exists by seeding it from a platform default source.
123+
///
124+
/// # Errors
125+
///
126+
/// This function returns an error if the target cannot be created or no source default config is available.
127+
pub fn ensure_sshd_config_exists(input: Option<PathBuf>) -> Result<PathBuf, SshdConfigError> {
128+
let target_path = get_default_sshd_config_path(input)?;
129+
if target_path.exists() {
130+
return Ok(target_path);
131+
}
132+
133+
if !cfg!(windows) {
134+
return Err(SshdConfigError::InvalidInput(
135+
t!("util.sshdConfigDefaultNotFound", paths = "Windows seeding is only supported on Windows hosts").to_string()
136+
));
137+
}
138+
139+
let candidates = get_sshd_config_default_source_candidates();
140+
let source_path = candidates
141+
.iter()
142+
.find(|candidate| candidate.is_file())
143+
.cloned()
144+
.ok_or_else(|| {
145+
let paths = candidates
146+
.iter()
147+
.map(|path| path.display().to_string())
148+
.collect::<Vec<String>>()
149+
.join(", ");
150+
SshdConfigError::InvalidInput(t!("util.sshdConfigDefaultNotFound", paths = paths).to_string())
151+
})?;
152+
153+
if let Some(parent) = target_path.parent() {
154+
std::fs::create_dir_all(parent)?;
155+
}
156+
157+
std::fs::copy(&source_path, &target_path)?;
158+
debug!("{}", t!("util.seededConfigFromDefault", source = source_path.display(), target = target_path.display()));
159+
160+
Ok(target_path)
161+
}
162+
110163
/// Invoke sshd -T.
111164
///
112165
/// # Errors
@@ -118,7 +171,7 @@ pub fn invoke_sshd_config_validation(args: Option<SshdCommandArgs>) -> Result<St
118171

119172
if let Some(args) = args {
120173
if let Some(filepath) = args.filepath {
121-
if !filepath.exists() {
174+
let filepath = get_default_sshd_config_path(Some(filepath))?; if !filepath.exists() {
122175
return Err(SshdConfigError::FileNotFound(filepath.display().to_string()));
123176
}
124177
command.arg("-f").arg(&filepath);
@@ -244,5 +297,3 @@ pub fn read_sshd_config(input: Option<PathBuf>) -> Result<String, SshdConfigErro
244297
Err(SshdConfigError::FileNotFound(filepath.display().to_string()))
245298
}
246299
}
247-
248-

resources/sshdconfig/tests/sshdconfig.get.tests.ps1

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ PasswordAuthentication no
148148
Remove-Item -Path $stderrFile -Force -ErrorAction SilentlyContinue
149149
}
150150

151-
It 'Should fail when config file does not exist' {
151+
It 'Should fail without creating target config when file does not exist' {
152152
$nonExistentPath = Join-Path $TestDrive 'nonexistent_sshd_config'
153153

154154
$inputData = @{
@@ -161,6 +161,27 @@ PasswordAuthentication no
161161
sshdconfig get --input $inputData -s sshd-config 2>$stderrFile
162162
$LASTEXITCODE | Should -Not -Be 0
163163

164+
Test-Path $nonExistentPath | Should -Be $false
165+
166+
$stderr = Get-Content -Path $stderrFile -Raw -ErrorAction SilentlyContinue
167+
$stderr | Should -Match "File not found"
168+
169+
Remove-Item -Path $stderrFile -Force -ErrorAction SilentlyContinue
170+
}
171+
172+
It 'Should fail when config file does not exist even when default source is missing' {
173+
$nonExistentPath = Join-Path $TestDrive 'nonexistent_sshd_config'
174+
175+
$inputData = @{
176+
_metadata = @{
177+
filepath = $nonExistentPath
178+
}
179+
} | ConvertTo-Json
180+
181+
$stderrFile = Join-Path $TestDrive "stderr_missing_default_source.txt"
182+
sshdconfig get --input $inputData -s sshd-config 2>$stderrFile
183+
$LASTEXITCODE | Should -Not -Be 0
184+
164185
$stderr = Get-Content -Path $stderrFile -Raw -ErrorAction SilentlyContinue
165186
$stderr | Should -Match "File not found"
166187
Remove-Item -Path $stderrFile -Force -ErrorAction SilentlyContinue

resources/sshdconfig/tests/sshdconfig.set.tests.ps1

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ Describe 'sshd_config Set Tests' -Skip:($skipTest) {
2121
$TestDir = Join-Path $TestDrive "sshd_test"
2222
New-Item -Path $TestDir -ItemType Directory -Force | Out-Null
2323
$TestConfigPath = Join-Path $TestDir "sshd_config"
24+
25+
$script:DefaultSourceExists = $IsWindows -and
26+
(Test-Path -Path "$env:SystemDrive\Windows\System32\OpenSSH\sshd_config_default" -PathType Leaf -ErrorAction SilentlyContinue)
2427
}
2528

2629
AfterEach {
@@ -180,7 +183,7 @@ Describe 'sshd_config Set Tests' -Skip:($skipTest) {
180183
sshdconfig set --input $validConfig -s sshd-config
181184
}
182185

183-
It 'Should fail with purge=false when file does not exist' {
186+
It 'Should seed missing file from default source when available on Windows, or fail otherwise' {
184187
$nonExistentPath = Join-Path $TestDrive "nonexistent_sshd_config"
185188

186189
$inputConfig = @{
@@ -193,12 +196,26 @@ Describe 'sshd_config Set Tests' -Skip:($skipTest) {
193196

194197
$stderrFile = Join-Path $TestDrive "stderr_purgefalse_nofile.txt"
195198
sshdconfig set --input $inputConfig -s sshd-config 2>$stderrFile
196-
$LASTEXITCODE | Should -Not -Be 0
197199

198-
$stderr = Get-Content -Path $stderrFile -Raw -ErrorAction SilentlyContinue
199-
$stderr | Should -Match "_purge=false requires an existing sshd_config file"
200-
$stderr | Should -Match "Use _purge=true to create a new configuration file"
200+
if ($IsWindows -and $script:DefaultSourceExists) {
201+
$LASTEXITCODE | Should -Be 0
202+
Test-Path $nonExistentPath | Should -Be $true
203+
204+
$getInput = @{
205+
_metadata = @{
206+
filepath = $nonExistentPath
207+
}
208+
} | ConvertTo-Json
209+
$result = sshdconfig get --input $getInput -s sshd-config 2>$null | ConvertFrom-Json
210+
$result.Port | Should -Be "8888"
211+
} else {
212+
$LASTEXITCODE | Should -Not -Be 0
213+
$stderr = Get-Content -Path $stderrFile -Raw -ErrorAction SilentlyContinue
214+
$stderr | Should -Match "no default source could be found"
215+
}
216+
201217
Remove-Item -Path $stderrFile -Force -ErrorAction SilentlyContinue
218+
Remove-Item -Path $nonExistentPath -Force -ErrorAction SilentlyContinue
202219
}
203220

204221
It 'Should fail with invalid keyword and not modify file' {

resources/sshdconfig/tests/sshdconfigRepeat.tests.ps1

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ Describe 'sshd-config-repeat Set Tests' -Skip:($skipTest) {
3333
$script:DefaultSftpPath = "/usr/lib/openssh/sftp-server"
3434
$script:AlternatePath = "/usr/libexec/sftp-server"
3535
}
36+
37+
$script:DefaultSourceExists = $IsWindows -and
38+
(Test-Path -Path "$env:SystemDrive\Windows\System32\OpenSSH\sshd_config_default" -PathType Leaf -ErrorAction SilentlyContinue)
3639
}
3740

3841
AfterEach {
@@ -212,5 +215,35 @@ PasswordAuthentication yes
212215

213216
Remove-Item -Path $stderrFile -Force -ErrorAction SilentlyContinue
214217
}
218+
219+
It 'Should seed missing file from default source when available on Windows, or fail otherwise' {
220+
$nonExistentPath = Join-Path $TestDrive "nonexistent_sshd_config"
221+
222+
$inputConfig = @{
223+
_metadata = @{
224+
filepath = $nonExistentPath
225+
}
226+
_exist = $true
227+
subsystem = @{
228+
name = "powershell"
229+
value = "/usr/bin/pwsh -sshs"
230+
}
231+
} | ConvertTo-Json
232+
233+
$stderrFile = Join-Path $TestDrive "stderr_missing_default_repeat.txt"
234+
sshdconfig set --input $inputConfig -s sshd-config-repeat 2>$stderrFile
235+
236+
if ($IsWindows -and $script:DefaultSourceExists) {
237+
$LASTEXITCODE | Should -Be 0
238+
Test-Path $nonExistentPath | Should -Be $true
239+
} else {
240+
$LASTEXITCODE | Should -Not -Be 0
241+
$stderr = Get-Content -Path $stderrFile -Raw -ErrorAction SilentlyContinue
242+
$stderr | Should -Match "no default source could be found"
243+
}
244+
245+
Remove-Item -Path $stderrFile -Force -ErrorAction SilentlyContinue
246+
Remove-Item -Path $nonExistentPath -Force -ErrorAction SilentlyContinue
247+
}
215248
}
216249
}

resources/sshdconfig/tests/sshdconfigRepeatList.tests.ps1

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ Describe 'sshd-config-repeat-list Set Tests' -Skip:($skipTest) {
3333
$script:DefaultSftpPath = "/usr/lib/openssh/sftp-server"
3434
$script:AlternatePath = "/usr/libexec/sftp-server"
3535
}
36+
37+
$script:DefaultSourceExists = $IsWindows -and
38+
(Test-Path -Path "$env:SystemDrive\Windows\System32\OpenSSH\sshd_config_default" -PathType Leaf -ErrorAction SilentlyContinue)
3639
}
3740

3841
AfterEach {
@@ -320,5 +323,37 @@ PasswordAuthentication yes
320323
$subsystems = Get-Content $TestConfigPath | Where-Object { $_ -match '^\s*subsystem\s+' }
321324
$subsystems.Count | Should -Be 0
322325
}
326+
327+
It 'Should seed missing file from default source when available on Windows, or fail otherwise' {
328+
$nonExistentPath = Join-Path $TestDrive "nonexistent_sshd_config"
329+
330+
$inputConfig = @{
331+
_metadata = @{
332+
filepath = $nonExistentPath
333+
}
334+
_purge = $false
335+
subsystem = @(
336+
@{
337+
name = "powershell"
338+
value = "/usr/bin/pwsh -sshs"
339+
}
340+
)
341+
} | ConvertTo-Json -Depth 10
342+
343+
$stderrFile = Join-Path $TestDrive "stderr_missing_default_repeat_list.txt"
344+
sshdconfig set --input $inputConfig -s sshd-config-repeat-list 2>$stderrFile
345+
346+
if ($IsWindows -and $script:DefaultSourceExists) {
347+
$LASTEXITCODE | Should -Be 0
348+
Test-Path $nonExistentPath | Should -Be $true
349+
} else {
350+
$LASTEXITCODE | Should -Not -Be 0
351+
$stderr = Get-Content -Path $stderrFile -Raw -ErrorAction SilentlyContinue
352+
$stderr | Should -Match "no default source could be found"
353+
}
354+
355+
Remove-Item -Path $stderrFile -Force -ErrorAction SilentlyContinue
356+
Remove-Item -Path $nonExistentPath -Force -ErrorAction SilentlyContinue
357+
}
323358
}
324359
}

0 commit comments

Comments
 (0)