content: /*
   * 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.camel.component.scp;
  
  import java.io.IOException;
  import java.nio.file.Paths;
  import java.security.Provider;
  import java.security.Provider.Service;
  import java.security.PublicKey;
  import java.security.Security;
  import java.util.Arrays;
  import java.util.LinkedList;
  import java.util.List;
  import java.util.function.Consumer;
  
  import com.jcraft.jsch.JSch;
  import com.jcraft.jsch.JSchException;
  import com.jcraft.jsch.Session;
  import com.jcraft.jsch.UserInfo;
  import org.apache.camel.test.AvailablePortFinder;
  import org.apache.camel.test.junit5.CamelTestSupport;
  import org.apache.sshd.common.NamedFactory;
  import org.apache.sshd.common.kex.KeyExchangeFactory;
  import org.apache.sshd.common.keyprovider.FileKeyPairProvider;
  import org.apache.sshd.common.signature.BuiltinSignatures;
  import org.apache.sshd.common.signature.Signature;
  import org.apache.sshd.scp.server.ScpCommandFactory;
  import org.apache.sshd.server.SshServer;
  import org.apache.sshd.server.auth.password.PasswordAuthenticator;
  import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator;
  import org.apache.sshd.server.session.ServerSession;
  import org.apache.sshd.sftp.server.SftpSubsystemFactory;
  import org.junit.jupiter.api.BeforeAll;
  import org.junit.jupiter.api.BeforeEach;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  import static org.apache.camel.test.junit5.TestSupport.createDirectory;
  import static org.apache.camel.test.junit5.TestSupport.deleteDirectory;
  
  public abstract class ScpServerTestSupport extends CamelTestSupport {
      protected static final Logger LOG = LoggerFactory.getLogger(ScpServerTestSupport.class);
      protected static final String SCP_ROOT_DIR = "target/test-classes/scp";
      protected static final String KNOWN_HOSTS = "known_hosts";
      protected static int port;
  
      protected Consumer<SshServer> serverConfigurer;
  
      private boolean acceptLocalhostConnections = true;
      private String knownHostsFile;
  
      private boolean setupComplete;
      private SshServer sshd;
  
      protected ScpServerTestSupport() {
          this(true);
      }
  
      protected ScpServerTestSupport(boolean acceptLocalhostConnections) {
          this.acceptLocalhostConnections = acceptLocalhostConnections;
      }
  
      protected int getPort() {
          return port;
      }
  
      protected SshServer getSshd() {
          return sshd;
      }
  
      @BeforeAll
      public static void initPort() {
          port = AvailablePortFinder.getNextAvailable();
      }
  
      @Override
      @BeforeEach
      public void doPreSetup() throws Exception {
          deleteDirectory(getScpPath());
          createDirectory(getScpPath());
  
          setupComplete = startSshd();
          setupKnownHosts();
      }
  
      @Override
      public void doPostTearDown() {
          if (sshd != null) {
              try {
                  sshd.stop(true);
                  sshd = null;
              } catch (Exception e) {
                  // ignore while shutting down as we could be polling during shutdown
                  // and get errors when the ssh server is stopping.
              }
          }
          deleteDirectory(getScpPath());
      }
  
      protected final String getScpPath() {
          // use this convention and use separate directories for tests
          // (easier to debug and avoid interference)
          return SCP_ROOT_DIR + "/" + getClass().getSimpleName();
      }
  
      protected String getScpUri() {
          return "scp://localhost:" + getPort() + "/" + getScpPath();
      }
  
      protected boolean startSshd() {
          sshd = SshServer.setUpDefaultServer();
          sshd.setPort(getPort());
          sshd.setKeyPairProvider(new FileKeyPairProvider(Paths.get("src/test/resources/hostkey.pem")));
          sshd.setSubsystemFactories(Arrays.asList(new SftpSubsystemFactory()));
          sshd.setCommandFactory(new ScpCommandFactory());
          sshd.setPasswordAuthenticator(new PasswordAuthenticator() {
              @Override
              public boolean authenticate(String username, String password, ServerSession session) {
                  // dummy authentication: allow any user whose password is the same as the username
                  return username != null && username.equals(password);
              }
          });
          sshd.setPublickeyAuthenticator(new PublickeyAuthenticator() {
              @Override
              public boolean authenticate(String username, PublicKey key, ServerSession session) {
                  return true;
              }
          });
          List<NamedFactory<Signature>> signatures = new LinkedList<NamedFactory<Signature>>(sshd.getSignatureFactories());
          signatures.remove(BuiltinSignatures.rsa);
          sshd.setSignatureFactories(signatures);
          List<KeyExchangeFactory> kexFactories = new LinkedList<KeyExchangeFactory>(sshd.getKeyExchangeFactories());
          sshd.setKeyExchangeFactories(kexFactories);
          if (serverConfigurer != null) {
              serverConfigurer.accept(sshd);
          }
          try {
              sshd.start();
              return true;
          } catch (IOException e) {
              LOG.info("Failed to start ssh server.", e);
          }
          return false;
      }
  
      protected void setupKnownHosts() {
          knownHostsFile = SCP_ROOT_DIR + "/" + KNOWN_HOSTS;
          if (!acceptLocalhostConnections) {
              return;
          }
  
          // For security reasons (avoiding man in the middle attacks),
          // camel-jsch will only connect to known hosts. For unit testing
          // we use a known key, but since the port is dynamic, the
          // known_hosts file will be generated by the following code and
          // should contain a line like below (if
          // "HashKnownHosts"=="yes" the hostname:port part will be
          // hashed and look a bit more complicated).
          //
          // [localhost]:21000 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDd \
          // fIWeSV4o68dRrKSzFd/Bk51E65UTmmSrmW0O1ohtzi6HzsDPjXgCtlTt3F \
          // qTcfFfI92IlTr4JWqC9UK1QT1ZTeng0MkPQmv68hDANHbt5CpETZHjW5q4 \
          // OOgWhVvj5IyOC2NZHtKlJBkdsMAa15ouOOJLzBvAvbqOR/yUROsEiQ==
  
          JSch jsch = new JSch();
          try {
              LOG.debug("Using '{}' for known hosts.", knownHostsFile);
              jsch.setKnownHosts(knownHostsFile);
              Session s = jsch.getSession("admin", "localhost", getPort());
              s.setConfig("StrictHostKeyChecking", "ask");
  
              // TODO: by the current jsch (0.1.51) setting "HashKnownHosts" to "no" is a workaround
              // to make the tests run green, see also http://sourceforge.net/p/jsch/bugs/63/
              s.setConfig("HashKnownHosts", "no");
              s.setUserInfo(new UserInfo() {
                  @Override
                  public String getPassphrase() {
                      return null;
                  }
  
                  @Override
                  public String getPassword() {
                      return "admin";
                  }
  
                  @Override
                  public boolean promptPassword(String message) {
                      return true;
                  }
  
                  @Override
                  public boolean promptPassphrase(String message) {
                      return false;
                  }
  
                  @Override
                  public boolean promptYesNo(String message) {
                      // accept host authenticity
                      return true;
                  }
  
                  @Override
                  public void showMessage(String message) {
                  }
              });
              // in the process of connecting, "[localhost]:<port>" is added to the knownHostsFile
              s.connect();
              s.disconnect();
          } catch (JSchException e) {
              LOG.info("Could not add [localhost] to known hosts", e);
          }
      }
  
      public String getKnownHostsFile() {
          return knownHostsFile;
      }
  
      public boolean isSetupComplete() {
          return setupComplete;
      }
  
      protected static void traceSecurityProviders() {
          for (Provider p : Security.getProviders()) {
              for (Service s : p.getServices()) {
                  LOG.trace("Security provider {} for '{}' algorithm", s.getClassName(), s.getAlgorithm());
              }
          }
      }
  }
  