PageRenderTime 37ms CodeModel.GetById 29ms RepoModel.GetById 0ms app.codeStats 1ms

/cli/src/main/java/hudson/cli/CLI.java

https://gitlab.com/kalyanjk29/jenkins
Java | 382 lines | 265 code | 42 blank | 75 comment | 28 complexity | fec5e3014b508d9dec20543cf3d82ba9 MD5 | raw file
  1. /*
  2. * The MIT License
  3. *
  4. * Copyright (c) 2004-2009, Sun Microsystems, Inc.
  5. *
  6. * Permission is hereby granted, free of charge, to any person obtaining a copy
  7. * of this software and associated documentation files (the "Software"), to deal
  8. * in the Software without restriction, including without limitation the rights
  9. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  10. * copies of the Software, and to permit persons to whom the Software is
  11. * furnished to do so, subject to the following conditions:
  12. *
  13. * The above copyright notice and this permission notice shall be included in
  14. * all copies or substantial portions of the Software.
  15. *
  16. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  22. * THE SOFTWARE.
  23. */
  24. package hudson.cli;
  25. import com.trilead.ssh2.crypto.Base64;
  26. import com.trilead.ssh2.crypto.PEMDecoder;
  27. import com.trilead.ssh2.signature.DSASHA1Verify;
  28. import com.trilead.ssh2.signature.RSASHA1Verify;
  29. import hudson.cli.client.Messages;
  30. import hudson.remoting.Channel;
  31. import hudson.remoting.PingThread;
  32. import hudson.remoting.Pipe;
  33. import hudson.remoting.RemoteInputStream;
  34. import hudson.remoting.RemoteOutputStream;
  35. import hudson.remoting.SocketInputStream;
  36. import hudson.remoting.SocketOutputStream;
  37. import java.io.BufferedInputStream;
  38. import java.io.BufferedOutputStream;
  39. import java.io.ByteArrayInputStream;
  40. import java.io.ByteArrayOutputStream;
  41. import java.io.DataInputStream;
  42. import java.io.DataOutputStream;
  43. import java.io.File;
  44. import java.io.FileInputStream;
  45. import java.io.IOException;
  46. import java.io.InputStream;
  47. import java.io.OutputStream;
  48. import java.net.Socket;
  49. import java.net.URL;
  50. import java.net.URLConnection;
  51. import java.security.GeneralSecurityException;
  52. import java.security.KeyFactory;
  53. import java.security.KeyPair;
  54. import java.security.PublicKey;
  55. import java.security.spec.DSAPrivateKeySpec;
  56. import java.security.spec.DSAPublicKeySpec;
  57. import java.util.ArrayList;
  58. import java.util.Arrays;
  59. import java.util.Collections;
  60. import java.util.List;
  61. import java.util.Locale;
  62. import java.util.concurrent.ExecutorService;
  63. import java.util.concurrent.Executors;
  64. import java.util.logging.Logger;
  65. import static java.util.logging.Level.*;
  66. /**
  67. * CLI entry point to Jenkins.
  68. *
  69. * @author Kohsuke Kawaguchi
  70. */
  71. public class CLI {
  72. private final ExecutorService pool;
  73. private final Channel channel;
  74. private final CliEntryPoint entryPoint;
  75. private final boolean ownsPool;
  76. public CLI(URL jenkins) throws IOException, InterruptedException {
  77. this(jenkins,null);
  78. }
  79. public CLI(URL jenkins, ExecutorService exec) throws IOException, InterruptedException {
  80. String url = jenkins.toExternalForm();
  81. if(!url.endsWith("/")) url+='/';
  82. ownsPool = exec==null;
  83. pool = exec!=null ? exec : Executors.newCachedThreadPool();
  84. int clip = getCliTcpPort(url);
  85. if(clip>=0) {
  86. // connect via CLI port
  87. String host = new URL(url).getHost();
  88. LOGGER.fine("Trying to connect directly via TCP/IP to port "+clip+" of "+host);
  89. Socket s = new Socket(host,clip);
  90. DataOutputStream dos = new DataOutputStream(s.getOutputStream());
  91. dos.writeUTF("Protocol:CLI-connect");
  92. channel = new Channel("CLI connection to "+jenkins, pool,
  93. new BufferedInputStream(new SocketInputStream(s)),
  94. new BufferedOutputStream(new SocketOutputStream(s)));
  95. } else {
  96. // connect via HTTP
  97. LOGGER.fine("Trying to connect to "+url+" via HTTP");
  98. url+="cli";
  99. jenkins = new URL(url);
  100. FullDuplexHttpStream con = new FullDuplexHttpStream(jenkins);
  101. channel = new Channel("Chunked connection to "+jenkins,
  102. pool,con.getInputStream(),con.getOutputStream());
  103. new PingThread(channel,30*1000) {
  104. protected void onDead() {
  105. // noop. the point of ping is to keep the connection alive
  106. // as most HTTP servers have a rather short read time out
  107. }
  108. }.start();
  109. }
  110. // execute the command
  111. entryPoint = (CliEntryPoint)channel.waitForRemoteProperty(CliEntryPoint.class.getName());
  112. if(entryPoint.protocolVersion()!=CliEntryPoint.VERSION)
  113. throw new IOException(Messages.CLI_VersionMismatch());
  114. }
  115. /**
  116. * If the server advertises CLI port, returns it.
  117. */
  118. private int getCliTcpPort(String url) throws IOException {
  119. URLConnection head = new URL(url).openConnection();
  120. try {
  121. head.connect();
  122. } catch (IOException e) {
  123. throw (IOException)new IOException("Failed to connect to "+url).initCause(e);
  124. }
  125. String p = head.getHeaderField("X-Hudson-CLI-Port");
  126. if(p==null) return -1;
  127. return Integer.parseInt(p);
  128. }
  129. /**
  130. * Shuts down the channel and closes the underlying connection.
  131. */
  132. public void close() throws IOException, InterruptedException {
  133. channel.close();
  134. channel.join();
  135. if(ownsPool)
  136. pool.shutdown();
  137. }
  138. public int execute(List<String> args, InputStream stdin, OutputStream stdout, OutputStream stderr) {
  139. return entryPoint.main(args, Locale.getDefault(),
  140. new RemoteInputStream(stdin),
  141. new RemoteOutputStream(stdout),
  142. new RemoteOutputStream(stderr));
  143. }
  144. public int execute(List<String> args) {
  145. return execute(args, System.in, System.out, System.err);
  146. }
  147. public int execute(String... args) {
  148. return execute(Arrays.asList(args));
  149. }
  150. /**
  151. * Returns true if the named command exists.
  152. */
  153. public boolean hasCommand(String name) {
  154. return entryPoint.hasCommand(name);
  155. }
  156. /**
  157. * Accesses the underlying communication channel.
  158. * @since 1.419
  159. */
  160. public Channel getChannel() {
  161. return channel;
  162. }
  163. /**
  164. * Attempts to lift the security restriction on the underlying channel.
  165. * This requires the administer privilege on the server.
  166. *
  167. * @throws SecurityException
  168. * If we fail to upgrade the connection.
  169. */
  170. public void upgrade() {
  171. ByteArrayOutputStream out = new ByteArrayOutputStream();
  172. if (execute(Arrays.asList("groovy", "="),
  173. new ByteArrayInputStream("hudson.remoting.Channel.current().setRestricted(false)".getBytes()),
  174. out,out)!=0)
  175. throw new SecurityException(out.toString()); // failed to upgrade
  176. }
  177. public static void main(final String[] _args) throws Exception {
  178. System.exit(_main(_args));
  179. }
  180. public static int _main(String[] _args) throws Exception {
  181. List<String> args = Arrays.asList(_args);
  182. List<KeyPair> candidateKeys = new ArrayList<KeyPair>();
  183. boolean sshAuthRequestedExplicitly = false;
  184. String url = System.getenv("JENKINS_URL");
  185. if (url==null)
  186. url = System.getenv("HUDSON_URL");
  187. while(!args.isEmpty()) {
  188. String head = args.get(0);
  189. if(head.equals("-s") && args.size()>=2) {
  190. url = args.get(1);
  191. args = args.subList(2,args.size());
  192. continue;
  193. }
  194. if(head.equals("-i") && args.size()>=2) {
  195. File f = new File(args.get(1));
  196. if (!f.exists()) {
  197. printUsage(Messages.CLI_NoSuchFileExists(f));
  198. return -1;
  199. }
  200. try {
  201. candidateKeys.add(loadKey(f));
  202. } catch (IOException e) {
  203. throw new Exception("Failed to load key: "+f,e);
  204. } catch (GeneralSecurityException e) {
  205. throw new Exception("Failed to load key: "+f,e);
  206. }
  207. args = args.subList(2,args.size());
  208. sshAuthRequestedExplicitly = true;
  209. continue;
  210. }
  211. break;
  212. }
  213. if(url==null) {
  214. printUsage(Messages.CLI_NoURL());
  215. return -1;
  216. }
  217. if(args.isEmpty())
  218. args = Arrays.asList("help"); // default to help
  219. if (candidateKeys.isEmpty())
  220. addDefaultPrivateKeyLocations(candidateKeys);
  221. CLI cli = new CLI(new URL(url));
  222. try {
  223. if (!candidateKeys.isEmpty()) {
  224. try {
  225. // TODO: server verification
  226. cli.authenticate(candidateKeys);
  227. } catch (IllegalStateException e) {
  228. if (sshAuthRequestedExplicitly) {
  229. System.err.println("The server doesn't support public key authentication");
  230. return -1;
  231. }
  232. } catch (UnsupportedOperationException e) {
  233. if (sshAuthRequestedExplicitly) {
  234. System.err.println("The server doesn't support public key authentication");
  235. return -1;
  236. }
  237. } catch (GeneralSecurityException e) {
  238. if (sshAuthRequestedExplicitly) {
  239. System.err.println(e.getMessage());
  240. LOGGER.log(FINE,e.getMessage(),e);
  241. return -1;
  242. }
  243. System.err.println("Failed to authenticate with your SSH keys. Proceeding with anonymous access");
  244. LOGGER.log(FINE,"Failed to authenticate with your SSH keys. Proceeding with anonymous access",e);
  245. }
  246. }
  247. // execute the command
  248. // Arrays.asList is not serializable --- see 6835580
  249. args = new ArrayList<String>(args);
  250. return cli.execute(args, System.in, System.out, System.err);
  251. } finally {
  252. cli.close();
  253. }
  254. }
  255. /**
  256. * Loads RSA/DSA private key in a PEM format into {@link KeyPair}.
  257. */
  258. public static KeyPair loadKey(File f) throws IOException, GeneralSecurityException {
  259. DataInputStream dis = new DataInputStream(new FileInputStream(f));
  260. byte[] bytes = new byte[(int) f.length()];
  261. dis.readFully(bytes);
  262. dis.close();
  263. return loadKey(new String(bytes));
  264. }
  265. /**
  266. * Loads RSA/DSA private key in a PEM format into {@link KeyPair}.
  267. */
  268. public static KeyPair loadKey(String pemString) throws IOException, GeneralSecurityException {
  269. Object key = PEMDecoder.decode(pemString.toCharArray(), null);
  270. if (key instanceof com.trilead.ssh2.signature.RSAPrivateKey) {
  271. com.trilead.ssh2.signature.RSAPrivateKey x = (com.trilead.ssh2.signature.RSAPrivateKey)key;
  272. // System.out.println("ssh-rsa " + new String(Base64.encode(RSASHA1Verify.encodeSSHRSAPublicKey(x.getPublicKey()))));
  273. return x.toJCEKeyPair();
  274. }
  275. if (key instanceof com.trilead.ssh2.signature.DSAPrivateKey) {
  276. com.trilead.ssh2.signature.DSAPrivateKey x = (com.trilead.ssh2.signature.DSAPrivateKey)key;
  277. KeyFactory kf = KeyFactory.getInstance("DSA");
  278. // System.out.println("ssh-dsa " + new String(Base64.encode(DSASHA1Verify.encodeSSHDSAPublicKey(x.getPublicKey()))));
  279. return new KeyPair(
  280. kf.generatePublic(new DSAPublicKeySpec(x.getY(), x.getP(), x.getQ(), x.getG())),
  281. kf.generatePrivate(new DSAPrivateKeySpec(x.getX(), x.getP(), x.getQ(), x.getG())));
  282. }
  283. throw new UnsupportedOperationException("Unrecognizable key format: "+key);
  284. }
  285. /**
  286. * try all the default key locations
  287. */
  288. private static void addDefaultPrivateKeyLocations(List<KeyPair> keyFileCandidates) {
  289. File home = new File(System.getProperty("user.home"));
  290. for (String path : new String[]{".ssh/id_rsa",".ssh/id_dsa",".ssh/identity"}) {
  291. File key = new File(home,path);
  292. if (key.exists()) {
  293. try {
  294. keyFileCandidates.add(loadKey(key));
  295. } catch (IOException e) {
  296. // don't report an error. the user can still see it by using the -i option
  297. LOGGER.log(FINE, "Failed to load "+key,e);
  298. } catch (GeneralSecurityException e) {
  299. LOGGER.log(FINE, "Failed to load " + key, e);
  300. }
  301. }
  302. }
  303. }
  304. /**
  305. * Authenticate ourselves against the server.
  306. *
  307. * @return
  308. * identity of the server represented as a public key.
  309. */
  310. public PublicKey authenticate(Iterable<KeyPair> privateKeys) throws IOException, GeneralSecurityException {
  311. Pipe c2s = Pipe.createLocalToRemote();
  312. Pipe s2c = Pipe.createRemoteToLocal();
  313. entryPoint.authenticate("ssh",c2s, s2c);
  314. Connection c = new Connection(s2c.getIn(), c2s.getOut());
  315. try {
  316. byte[] sharedSecret = c.diffieHellman(false).generateSecret();
  317. PublicKey serverIdentity = c.verifyIdentity(sharedSecret);
  318. // try all the public keys
  319. for (KeyPair key : privateKeys) {
  320. c.proveIdentity(sharedSecret,key);
  321. if (c.readBoolean())
  322. return serverIdentity; // succeeded
  323. }
  324. if (privateKeys.iterator().hasNext())
  325. throw new GeneralSecurityException("Authentication failed. No private key accepted.");
  326. else
  327. throw new GeneralSecurityException("No private key is available for use in authentication");
  328. } finally {
  329. c.close();
  330. }
  331. }
  332. public PublicKey authenticate(KeyPair key) throws IOException, GeneralSecurityException {
  333. return authenticate(Collections.singleton(key));
  334. }
  335. private static void printUsage(String msg) {
  336. if(msg!=null) System.out.println(msg);
  337. System.err.println(Messages.CLI_Usage());
  338. }
  339. private static final Logger LOGGER = Logger.getLogger(CLI.class.getName());
  340. }