/**
 * 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.security.Provider;
import java.security.Provider.Service;
import java.security.PublicKey;
import java.security.Security;
import java.util.Arrays;

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.junit4.CamelTestSupport;
import org.apache.sshd.SshServer;
import org.apache.sshd.common.NamedFactory;
import org.apache.sshd.common.keyprovider.FileKeyPairProvider;
import org.apache.sshd.server.Command;
import org.apache.sshd.server.PasswordAuthenticator;
import org.apache.sshd.server.PublickeyAuthenticator;
import org.apache.sshd.server.command.ScpCommandFactory;
import org.apache.sshd.server.session.ServerSession;
import org.apache.sshd.server.sftp.SftpSubsystem;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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;

    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;
    }

    @BeforeClass
    public static void initPort() throws Exception {
        port = AvailablePortFinder.getNextAvailable(21000);
    }

    @Override
    @Before
    public void setUp() throws Exception {
        deleteDirectory(getScpPath());
        createDirectory(getScpPath());

        setupComplete = startSshd();
        setupKnownHosts();
        super.setUp();
    }

    @Override
    @After
    public void tearDown() throws Exception {
        super.tearDown();

        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(new String[]{"src/test/resources/hostkey.pem"}));
        sshd.setSubsystemFactories(Arrays.<NamedFactory<Command>>asList(new SftpSubsystem.Factory()));
        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;
            }
        });
        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());
            }
        }
    }
}
