Package hudson.cli

Source Code of hudson.cli.CLI

/*
* The MIT License
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package hudson.cli;

import com.trilead.ssh2.crypto.PEMDecoder;
import hudson.cli.client.Messages;
import hudson.remoting.Channel;
import hudson.remoting.PingThread;
import hudson.remoting.Pipe;
import hudson.remoting.RemoteInputStream;
import hudson.remoting.RemoteOutputStream;
import hudson.remoting.SocketInputStream;
import hudson.remoting.SocketOutputStream;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.StringReader;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.URL;
import java.net.URLConnection;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.PublicKey;
import java.security.spec.DSAPrivateKeySpec;
import java.security.spec.DSAPublicKeySpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.io.Console;

import static java.util.logging.Level.*;

/**
* CLI entry point to Jenkins.
*
* @author Kohsuke Kawaguchi
*/
public class CLI {
    private final ExecutorService pool;
    private final Channel channel;
    private final CliEntryPoint entryPoint;
    private final boolean ownsPool;
    private final List<Closeable> closables = new ArrayList<Closeable>(); // stuff to close in the close method
    private final String httpsProxyTunnel;
    private final String authorization;

    public CLI(URL jenkins) throws IOException, InterruptedException {
        this(jenkins,null);
    }

    /**
     * @deprecated
     *      Use {@link CLIConnectionFactory} to create {@link CLI}
     */
    public CLI(URL jenkins, ExecutorService exec) throws IOException, InterruptedException {
        this(jenkins,exec,null);
    }

    /**
     * @deprecated
     *      Use {@link CLIConnectionFactory} to create {@link CLI}
     */
    public CLI(URL jenkins, ExecutorService exec, String httpsProxyTunnel) throws IOException, InterruptedException {
        this(new CLIConnectionFactory().url(jenkins).executorService(exec).httpsProxyTunnel(httpsProxyTunnel));
    }
   
    /*package*/ CLI(CLIConnectionFactory factory) throws IOException, InterruptedException {
        URL jenkins = factory.jenkins;
        this.httpsProxyTunnel = factory.httpsProxyTunnel;
        this.authorization = factory.authorization;
        ExecutorService exec = factory.exec;
       
        String url = jenkins.toExternalForm();
        if(!url.endsWith("/"))  url+='/';

        ownsPool = exec==null;
        pool = exec!=null ? exec : Executors.newCachedThreadPool();

        Channel channel = null;
        InetSocketAddress clip = getCliTcpPort(url);
        if(clip!=null) {
            // connect via CLI port
            try {
                channel = connectViaCliPort(jenkins, clip);
            } catch (IOException e) {
                LOGGER.log(Level.FINE,"Failed to connect via CLI port. Falling back to HTTP",e);
            }
        }
        if (channel==null) {
            // connect via HTTP
            channel = connectViaHttp(url);
        }
        this.channel = channel;

        // execute the command
        entryPoint = (CliEntryPoint)channel.waitForRemoteProperty(CliEntryPoint.class.getName());

        if(entryPoint.protocolVersion()!=CliEntryPoint.VERSION)
            throw new IOException(Messages.CLI_VersionMismatch());
    }

    private Channel connectViaHttp(String url) throws IOException {
        LOGGER.fine("Trying to connect to "+url+" via HTTP");
        url+="cli";
        URL jenkins = new URL(url);

        FullDuplexHttpStream con = new FullDuplexHttpStream(jenkins,authorization);
        Channel ch = new Channel("Chunked connection to "+jenkins,
                pool,con.getInputStream(),con.getOutputStream());
        final long interval = 15*1000;
        final long timeout = (interval * 3) / 4;
        new PingThread(ch,timeout,interval) {
            protected void onDead() {
                // noop. the point of ping is to keep the connection alive
                // as most HTTP servers have a rather short read time out
            }
        }.start();
        return ch;
    }

    private Channel connectViaCliPort(URL jenkins, InetSocketAddress endpoint) throws IOException {
        LOGGER.fine("Trying to connect directly via TCP/IP to "+endpoint);
        final Socket s;
        OutputStream out;

        if (httpsProxyTunnel!=null) {
            String[] tokens = httpsProxyTunnel.split(":");
            s = new Socket(tokens[0], Integer.parseInt(tokens[1]));
            PrintStream o = new PrintStream(s.getOutputStream());
            o.print("CONNECT " + endpoint.getHostName() + ":" + endpoint.getPort() + " HTTP/1.0\r\n\r\n");

            // read the response from the proxy
            ByteArrayOutputStream rsp = new ByteArrayOutputStream();
            while (!rsp.toString().endsWith("\r\n\r\n")) {
                int ch = s.getInputStream().read();
                if (ch<0)   throw new IOException("Failed to read the HTTP proxy response: "+rsp);
                rsp.write(ch);
            }
            String head = new BufferedReader(new StringReader(rsp.toString())).readLine();
            if (!head.startsWith("HTTP/1.0 200 "))
                throw new IOException("Failed to establish a connection through HTTP proxy: "+rsp);

            // HTTP proxies (at least the one I tried --- squid) doesn't seem to do half-close very well.
            // So instead of relying on it, we'll just send the close command and then let the server
            // cut their side, then close the socket after the join.
            out = new SocketOutputStream(s) {
                @Override
                public void close() throws IOException {
                    // ignore
                }
            };
        } else {
            s = new Socket();
            s.connect(endpoint,3000);
            out = new SocketOutputStream(s);
        }

        closables.add(new Closeable() {
            public void close() throws IOException {
                s.close();
            }
        });

        DataOutputStream dos = new DataOutputStream(s.getOutputStream());
        dos.writeUTF("Protocol:CLI-connect");

        return new Channel("CLI connection to "+jenkins, pool,
                new BufferedInputStream(new SocketInputStream(s)),
                new BufferedOutputStream(out));
    }

    /**
     * If the server advertises CLI endpoint, returns its location.
     */
    private InetSocketAddress getCliTcpPort(String url) throws IOException {
        URL _url = new URL(url);
        if (_url.getHost()==null || _url.getHost().length()==0) {
            throw new IOException("Invalid URL: "+url);
        }
        URLConnection head = _url.openConnection();
        try {
            head.connect();
        } catch (IOException e) {
            throw (IOException)new IOException("Failed to connect to "+url).initCause(e);
        }
        String p = head.getHeaderField("X-Jenkins-CLI-Port");
        if (p==null)    p = head.getHeaderField("X-Hudson-CLI-Port");   // backward compatibility
        String h = head.getHeaderField("X-Jenkins-CLI-Host");
        if (h==null)    h = head.getURL().getHost();
       
        flushURLConnection(head);
        if (p==null)     return null;
       
        return new InetSocketAddress(h,Integer.parseInt(p));
    }

    /**
     * Flush the supplied {@link URLConnection} input and close the
     * connection nicely.
     * @param conn the connection to flush/close
     */
    private void flushURLConnection(URLConnection conn) {
        byte[] buf = new byte[1024];
        try {
            InputStream is = conn.getInputStream();
            while (is.read(buf) > 0) {
                // Ignore
            }
            is.close();
        } catch (IOException e) {
            try {
                InputStream es = ((HttpURLConnection)conn).getErrorStream();
                while (es.read(buf) > 0) {
                    // Ignore
                }
                es.close();
            } catch (IOException ex) {
                // Ignore
            }
        }
    }

    /**
     * Shuts down the channel and closes the underlying connection.
     */
    public void close() throws IOException, InterruptedException {
        channel.close();
        channel.join();
        if(ownsPool)
            pool.shutdown();
        for (Closeable c : closables)
            c.close();
    }

    public int execute(List<String> args, InputStream stdin, OutputStream stdout, OutputStream stderr) {
        return entryPoint.main(args, Locale.getDefault(),
                new RemoteInputStream(stdin),
                new RemoteOutputStream(stdout),
                new RemoteOutputStream(stderr));
    }

    public int execute(List<String> args) {
        return execute(args, System.in, System.out, System.err);
    }

    public int execute(String... args) {
        return execute(Arrays.asList(args));
    }

    /**
     * Returns true if the named command exists.
     */
    public boolean hasCommand(String name) {
        return entryPoint.hasCommand(name);
    }

    /**
     * Accesses the underlying communication channel.
     * @since 1.419
     */
    public Channel getChannel() {
        return channel;
    }

    /**
     * Attempts to lift the security restriction on the underlying channel.
     * This requires the administer privilege on the server.
     *
     * @throws SecurityException
     *      If we fail to upgrade the connection.
     */
    public void upgrade() {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        if (execute(Arrays.asList("groovy", "="),
                new ByteArrayInputStream("hudson.remoting.Channel.current().setRestricted(false)".getBytes()),
                out,out)!=0)
            throw new SecurityException(out.toString()); // failed to upgrade
    }

    public static void main(final String[] _args) throws Exception {
//        Logger l = Logger.getLogger(Channel.class.getName());
//        l.setLevel(ALL);
//        ConsoleHandler h = new ConsoleHandler();
//        h.setLevel(ALL);
//        l.addHandler(h);
//
        System.exit(_main(_args));
    }

    public static int _main(String[] _args) throws Exception {
        List<String> args = Arrays.asList(_args);
        List<KeyPair> candidateKeys = new ArrayList<KeyPair>();
        boolean sshAuthRequestedExplicitly = false;
        String httpProxy=null;

        String url = System.getenv("JENKINS_URL");

        if (url==null)
            url = System.getenv("HUDSON_URL");

        while(!args.isEmpty()) {
            String head = args.get(0);
            if (head.equals("-version")) {
                System.out.println("Version: "+computeVersion());
                return 0;
            }
            if(head.equals("-s") && args.size()>=2) {
                url = args.get(1);
                args = args.subList(2,args.size());
                continue;
            }
            if(head.equals("-i") && args.size()>=2) {
                File f = new File(args.get(1));
                if (!f.exists()) {
                    printUsage(Messages.CLI_NoSuchFileExists(f));
                    return -1;
                }
                KeyPair kp = null;
                try {
                    kp = loadKey(f);
                } catch (IOException e) {
                    //if the PEM file is encrypted, IOException is thrown
                    kp = tryEncryptedFile(f);                   
                } catch (GeneralSecurityException e) {
                    throw new Exception("Failed to load key: "+f,e);
                }
                if(kp != null)
                    candidateKeys.add(kp);
                args = args.subList(2,args.size());
                sshAuthRequestedExplicitly = true;
                continue;
            }
            if(head.equals("-p") && args.size()>=2) {
                httpProxy = args.get(1);
                args = args.subList(2,args.size());
                continue;
            }
            break;
        }

        if(url==null) {
            printUsage(Messages.CLI_NoURL());
            return -1;
        }

        if(args.isEmpty())
            args = Arrays.asList("help"); // default to help

        if (candidateKeys.isEmpty())
            addDefaultPrivateKeyLocations(candidateKeys);

        CLI cli = new CLI(new URL(url),null,httpProxy);
        try {
            if (!candidateKeys.isEmpty()) {
                try {
                    // TODO: server verification
                    cli.authenticate(candidateKeys);
                } catch (IllegalStateException e) {
                    if (sshAuthRequestedExplicitly) {
                        System.err.println("The server doesn't support public key authentication");
                        return -1;
                    }
                } catch (UnsupportedOperationException e) {
                    if (sshAuthRequestedExplicitly) {
                        System.err.println("The server doesn't support public key authentication");
                        return -1;
                    }
                } catch (GeneralSecurityException e) {
                    if (sshAuthRequestedExplicitly) {
                        System.err.println(e.getMessage());
                        LOGGER.log(FINE,e.getMessage(),e);
                        return -1;
                    }
                    System.err.println("Failed to authenticate with your SSH keys.");
                    LOGGER.log(FINE,"Failed to authenticate with your SSH keys.",e);
                }
            }

            // execute the command
            // Arrays.asList is not serializable --- see 6835580
            args = new ArrayList<String>(args);
            return cli.execute(args, System.in, System.out, System.err);
        } finally {
            cli.close();
        }
    }

    private static String computeVersion() {
        Properties props = new Properties();
        try {
            InputStream is = CLI.class.getResourceAsStream("/jenkins/cli/jenkins-cli-version.properties");
            if(is!=null)
                props.load(is);
        } catch (IOException e) {
            e.printStackTrace(); // if the version properties is missing, that's OK.
        }
        return props.getProperty("version","?");
    }

    /**
     * Loads RSA/DSA private key in a PEM format into {@link KeyPair}.
     */
    public static KeyPair loadKey(File f, String passwd) throws IOException, GeneralSecurityException {
        return loadKey(readPemFile(f), passwd);
    }

    public static KeyPair loadKey(File f) throws IOException, GeneralSecurityException {
      return loadKey(f, null);
    }
   
    private static String readPemFile(File f) throws IOException{
        DataInputStream dis = new DataInputStream(new FileInputStream(f));
        byte[] bytes = new byte[(int) f.length()];
        dis.readFully(bytes);
        dis.close();
        return new String(bytes);
    }
   
    /**
     * Loads RSA/DSA private key in a PEM format into {@link KeyPair}.
     */
    public static KeyPair loadKey(String pemString, String passwd) throws IOException, GeneralSecurityException {
        Object key = PEMDecoder.decode(pemString.toCharArray(), passwd);
        if (key instanceof com.trilead.ssh2.signature.RSAPrivateKey) {
            com.trilead.ssh2.signature.RSAPrivateKey x = (com.trilead.ssh2.signature.RSAPrivateKey)key;
//            System.out.println("ssh-rsa " + new String(Base64.encode(RSASHA1Verify.encodeSSHRSAPublicKey(x.getPublicKey()))));

            return x.toJCEKeyPair();
        }
        if (key instanceof com.trilead.ssh2.signature.DSAPrivateKey) {
            com.trilead.ssh2.signature.DSAPrivateKey x = (com.trilead.ssh2.signature.DSAPrivateKey)key;
            KeyFactory kf = KeyFactory.getInstance("DSA");
//            System.out.println("ssh-dsa " + new String(Base64.encode(DSASHA1Verify.encodeSSHDSAPublicKey(x.getPublicKey()))));

            return new KeyPair(
                    kf.generatePublic(new DSAPublicKeySpec(x.getY(), x.getP(), x.getQ(), x.getG())),
                    kf.generatePrivate(new DSAPrivateKeySpec(x.getX(), x.getP(), x.getQ(), x.getG())));
        }

        throw new UnsupportedOperationException("Unrecognizable key format: "+key);
    }

    public static KeyPair loadKey(String pemString) throws IOException, GeneralSecurityException {
      return loadKey(pemString, null);
    }
   
    private static KeyPair tryEncryptedFile(File f) throws IOException, GeneralSecurityException{
        KeyPair kp = null;
        if(isPemEncrypted(f)){
            String passwd = askForPasswd(f.getCanonicalPath());
            kp = loadKey(f,passwd);
        }
        return kp;
    }
   
    private static boolean isPemEncrypted(File f) throws IOException{
        String pemString = readPemFile(f);
        //simple check if the file is encrypted
        if(pemString.contains("4,ENCRYPTED"))
            return true;
        return false;
    }
   
    private static String askForPasswd(String filePath){
        try {
            Console cons = System.console();
            String passwd = null;
            if (cons != null){
                char[] p = cons.readPassword("%s", "Enter passphrase for "+filePath+":");
                passwd = String.valueOf(p);
            }
            return passwd;
        } catch (LinkageError e) {
            throw new Error("Your private key is encrypted, but we need Java6 to ask you password safely",e);
        }
    }
   
    /**
     * try all the default key locations
     */
    private static void addDefaultPrivateKeyLocations(List<KeyPair> keyFileCandidates) {
        File home = new File(System.getProperty("user.home"));
        for (String path : new String[]{".ssh/id_rsa",".ssh/id_dsa",".ssh/identity"}) {
            File key = new File(home,path);
            if (key.exists()) {
                try {
                    keyFileCandidates.add(loadKey(key));
                } catch (IOException e) {
                    // don't report an error. the user can still see it by using the -i option
                    LOGGER.log(FINE, "Failed to load "+key,e);
                } catch (GeneralSecurityException e) {
                    LOGGER.log(FINE, "Failed to load " + key, e);
                }
            }
        }
    }

    /**
     * Authenticate ourselves against the server.
     *
     * @return
     *      identity of the server represented as a public key.
     */
    public PublicKey authenticate(Iterable<KeyPair> privateKeys) throws IOException, GeneralSecurityException {
        Pipe c2s = Pipe.createLocalToRemote();
        Pipe s2c = Pipe.createRemoteToLocal();
        entryPoint.authenticate("ssh",c2s, s2c);
        Connection c = new Connection(s2c.getIn(), c2s.getOut());

        try {
            byte[] sharedSecret = c.diffieHellman(false).generateSecret();
            PublicKey serverIdentity = c.verifyIdentity(sharedSecret);

            // try all the public keys
            for (KeyPair key : privateKeys) {
                c.proveIdentity(sharedSecret,key);
                if (c.readBoolean())
                    return serverIdentity;  // succeeded
            }
            if (privateKeys.iterator().hasNext())
                throw new GeneralSecurityException("Authentication failed. No private key accepted.");
            else
                throw new GeneralSecurityException("No private key is available for use in authentication");
        } finally {
            c.close();
        }
    }

    public PublicKey authenticate(KeyPair key) throws IOException, GeneralSecurityException {
        return authenticate(Collections.singleton(key));
    }

    private static void printUsage(String msg) {
        if(msg!=null)   System.out.println(msg);
        System.err.println(Messages.CLI_Usage());
    }

    private static final Logger LOGGER = Logger.getLogger(CLI.class.getName());
}
TOP

Related Classes of hudson.cli.CLI

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.