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
4 changes: 4 additions & 0 deletions .github/workflows/maven-verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,7 @@ jobs:
with:
install-subversion: true
matrix-exclude: '[ {"jdk": "25"} ]'
# -Dssh-tests turns off the no-ssh-tests profiles, -Dssh-embedded=true then narrows the ssh providers down
# to the tests that run against the embedded Apache MINA sshd; the rest need a real sshd on localhost:22.
# Repeats the workflow's own default for maven-args, which this input replaces rather than extends.
maven-args: '-D"invoker.streamLogsOnFailures" -Dssh-tests -Dssh-embedded=true'
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,10 @@ protected AuthenticationInfo getAuthInfo() {
}

protected long getExpectedLastModifiedOnGet(Repository repository, Resource resource) {
return new File(repository.getBasedir(), resource.getName()).lastModified();
// the scp protocol carries the modification time in whole seconds (the "T" header), so that is the
// precision the wagon reports back - truncate to match, the "remote" file here is a local file whose
// timestamp still has millisecond precision
return new File(repository.getBasedir(), resource.getName()).lastModified() / 1000L * 1000L;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,10 @@ protected AuthenticationInfo getAuthInfo() {
}

protected long getExpectedLastModifiedOnGet(Repository repository, Resource resource) {
return new File(repository.getBasedir(), resource.getName()).lastModified();
// the scp protocol carries the modification time in whole seconds (the "T" header), so that is the
// precision the wagon reports back - truncate to match, the "remote" file here is a local file whose
// timestamp still has millisecond precision
return new File(repository.getBasedir(), resource.getName()).lastModified() / 1000L * 1000L;
}

public void testConnect() throws Exception {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,12 +126,6 @@ public void start(ChannelSession channel, Environment env) throws IOException {
callback.onExit(exitValue, stdout.getOutput());
}
}
/*
out.write( exitValue );
out.write( '\n' );

*/
out.flush();
}

public void destroy(ChannelSession channel) throws Exception {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.apache.sshd.common.file.nativefs.NativeFileSystemFactory;
Expand All @@ -29,6 +30,7 @@
import org.apache.sshd.server.auth.password.PasswordAuthenticator;
import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
import org.apache.sshd.server.shell.ProcessShellFactory;
import org.apache.sshd.sftp.server.SftpSubsystemFactory;

/**
* @author Olivier Lamy
Expand Down Expand Up @@ -97,6 +99,10 @@ public int start() throws IOException {
.build();
sshd.setCommandFactory(commandFactory);

// OpenSSH 9 and later drive "scp" over the SFTP protocol rather than the legacy scp protocol, so the
// wagon-ssh-external tests, which shell out to the system scp, need the subsystem to be available
sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory()));

sshd.setFileSystemFactory(new NativeFileSystemFactory());
sshd.start();
this.port = sshd.getPort();
Expand Down
6 changes: 6 additions & 0 deletions wagon-providers/wagon-ssh-external/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,12 @@ under the License.
<excludes>
<exclude>**/SshCommandExecutorTest.*</exclude>
<exclude>**/Scp*Test.*</exclude>
<!-- This one does use the embedded server, but through the host's own ssh/scp binaries, and
OpenSSH 9 and later drive scp over SFTP instead of the legacy scp protocol. The remote path
is no longer expanded by a remote shell, so the backslash escaping ScpExternalWagon applies
to paths with spaces reaches the server verbatim and the transfer fails. Until that is
sorted out the result depends on the host's OpenSSH version, so it stays out of CI. -->
<exclude>**/EmbeddedScp*WagonWithKeyTest.*</exclude>
</excludes>
</configuration>
</plugin>
Expand Down
8 changes: 7 additions & 1 deletion wagon-providers/wagon-ssh/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,12 @@ under the License.
<profile>
<id>ssh-embedded</id>
<activation>
<!-- the embedded server runs the commands it receives through /bin/sh, so keep windauze in charge on
Windows: surefire <excludes> from two active profiles override rather than merge, and this profile
is declared last -->
<os>
<family>!windows</family>
</os>
<property>
<name>ssh-embedded</name>
<value>true</value>
Expand All @@ -175,7 +181,7 @@ under the License.
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<!-- Tests that currently doesn't work with embedded ssh server -->
<!-- Tests that need an ssh server on localhost:22 and the developer's own account there -->
<excludes>
<exclude>**/SftpWagonTest.*</exclude>
<exclude>**/SshCommandExecutorTest.*</exclude>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Locale;

import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.JSchException;
Expand Down Expand Up @@ -54,6 +55,12 @@
public class ScpWagon extends AbstractJschWagon {
private static final char COPY_START_CHAR = 'C';

/** scp acknowledgement byte for a recoverable error, what OpenSSH sends for a missing file */
private static final int SCP_WARNING = 1;

/** scp acknowledgement byte for a fatal error */
private static final int SCP_ERROR = 2;
Comment on lines +58 to +62

private static final char ACK_SEPARATOR = ' ';

private static final String END_OF_FILES_MSG = "E\n";
Expand Down Expand Up @@ -222,9 +229,13 @@ public void fillInputData(InputData inputData) throws TransferFailedException, R
String line = readLine(in);

if (exitCode != COPY_START_CHAR) {
if (exitCode == 1
&& (line.contains("No such file or directory")
|| line.indexOf("no such file or directory") != 1)) {
// OpenSSH reports a missing file with the scp "warning" code (1); other servers - Apache MINA
// sshd, which the tests run against - report it with the "fatal error" code (2), so fall back on
// the message for those
if (exitCode == SCP_WARNING
|| (exitCode == SCP_ERROR
&& line != null
&& line.toLowerCase(Locale.ROOT).contains("no such file or directory"))) {
throw new ResourceDoesNotExistException(line);
} else {
throw new IOException("Exit code: " + exitCode + " - " + line);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.maven.wagon.providers.ssh.jsch;

import java.util.List;

import org.codehaus.plexus.components.interactivity.Prompter;
import org.codehaus.plexus.components.interactivity.PrompterException;

/**
* A {@link Prompter} for the tests, wired in through
* {@code src/test/resources/META-INF/plexus/components.xml}.
* <p>
* {@code plexus-interactivity-api} stopped shipping a {@code META-INF/plexus/components.xml} in 1.3, so its
* {@code DefaultPrompter} is invisible to the {@code plexus-container-default} that {@code PlexusTestCase} runs;
* without a replacement every {@code lookup( Wagon.ROLE, "scp" )} fails because {@code ConsoleInteractiveUserInfo}
* and {@code PrompterUIKeyboardInteractive} both require one.
* <p>
* The tests are non-interactive, so nothing should ever prompt: every method throws rather than blocking on
* {@code System.in}.
*/
public class TestPrompter implements Prompter {

public String prompt(String message) throws PrompterException {
throw unexpected(message);
}

public String prompt(String message, String defaultReply) throws PrompterException {
throw unexpected(message);
}

public String prompt(String message, List<String> possibleValues) throws PrompterException {
throw unexpected(message);
}

public String prompt(String message, List<String> possibleValues, String defaultReply) throws PrompterException {
throw unexpected(message);
}

public String promptForPassword(String message) throws PrompterException {
throw unexpected(message);
}

public void showMessage(String message) throws PrompterException {
// tests run non-interactively, but a message costs nothing and helps when a test does go interactive
System.out.println(message);
}

private PrompterException unexpected(String message) {
return new PrompterException("The tests are non-interactive, unexpected prompt: " + message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Licensed to the Apache Software Foundation (ASF) under one
~ or more contributor license agreements. See the NOTICE file
~ distributed with this work for additional information
~ regarding copyright ownership. The ASF licenses this file
~ to you under the Apache License, Version 2.0 (the
~ "License"); you may not use this file except in compliance
~ with the License. You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing,
~ software distributed under the License is distributed on an
~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
~ KIND, either express or implied. See the License for the
~ specific language governing permissions and limitations
~ under the License.
-->

<!--
plexus-interactivity-api dropped its own META-INF/plexus/components.xml in 1.3 and only ships a sisu index, so
plexus-container-default cannot see DefaultPrompter (which in any case now takes its collaborators through a
constructor, which plexus-container-default cannot satisfy). Maven itself runs on sisu and is unaffected; only
the PlexusTestCase-based tests here need this descriptor.
-->
<component-set>
<components>
<component>
<role>org.codehaus.plexus.components.interactivity.Prompter</role>
<role-hint>default</role-hint>
<implementation>org.apache.maven.wagon.providers.ssh.jsch.TestPrompter</implementation>
</component>
</components>
</component-set>
Loading