001package org.apache.maven.wagon.providers.ssh.jsch; 002 003/* 004 * Licensed to the Apache Software Foundation (ASF) under one 005 * or more contributor license agreements. See the NOTICE file 006 * distributed with this work for additional information 007 * regarding copyright ownership. The ASF licenses this file 008 * to you under the Apache License, Version 2.0 (the 009 * "License"); you may not use this file except in compliance 010 * with the License. You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, 015 * software distributed under the License is distributed on an 016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 017 * KIND, either express or implied. See the License for the 018 * specific language governing permissions and limitations 019 * under the License. 020 */ 021 022import java.io.BufferedReader; 023import java.io.ByteArrayInputStream; 024import java.io.File; 025import java.io.FileNotFoundException; 026import java.io.IOException; 027import java.io.InputStreamReader; 028import java.util.List; 029import java.util.Properties; 030 031import org.apache.maven.wagon.CommandExecutionException; 032import org.apache.maven.wagon.CommandExecutor; 033import org.apache.maven.wagon.ResourceDoesNotExistException; 034import org.apache.maven.wagon.StreamWagon; 035import org.apache.maven.wagon.Streams; 036import org.apache.maven.wagon.TransferFailedException; 037import org.apache.maven.wagon.WagonConstants; 038import org.apache.maven.wagon.authentication.AuthenticationException; 039import org.apache.maven.wagon.authentication.AuthenticationInfo; 040import org.apache.maven.wagon.authorization.AuthorizationException; 041import org.apache.maven.wagon.events.TransferEvent; 042import org.apache.maven.wagon.providers.ssh.CommandExecutorStreamProcessor; 043import org.apache.maven.wagon.providers.ssh.ScpHelper; 044import org.apache.maven.wagon.providers.ssh.SshWagon; 045import org.apache.maven.wagon.providers.ssh.interactive.InteractiveUserInfo; 046import org.apache.maven.wagon.providers.ssh.interactive.NullInteractiveUserInfo; 047import org.apache.maven.wagon.providers.ssh.jsch.interactive.UserInfoUIKeyboardInteractiveProxy; 048import org.apache.maven.wagon.providers.ssh.knownhost.KnownHostChangedException; 049import org.apache.maven.wagon.providers.ssh.knownhost.KnownHostEntry; 050import org.apache.maven.wagon.providers.ssh.knownhost.KnownHostsProvider; 051import org.apache.maven.wagon.providers.ssh.knownhost.UnknownHostException; 052import org.apache.maven.wagon.proxy.ProxyInfo; 053import org.apache.maven.wagon.resource.Resource; 054import org.codehaus.plexus.util.IOUtil; 055 056import com.jcraft.jsch.ChannelExec; 057import com.jcraft.jsch.HostKey; 058import com.jcraft.jsch.HostKeyRepository; 059import com.jcraft.jsch.IdentityRepository; 060import com.jcraft.jsch.JSch; 061import com.jcraft.jsch.JSchException; 062import com.jcraft.jsch.Proxy; 063import com.jcraft.jsch.ProxyHTTP; 064import com.jcraft.jsch.ProxySOCKS5; 065import com.jcraft.jsch.Session; 066import com.jcraft.jsch.UIKeyboardInteractive; 067import com.jcraft.jsch.UserInfo; 068import com.jcraft.jsch.agentproxy.AgentProxyException; 069import com.jcraft.jsch.agentproxy.Connector; 070import com.jcraft.jsch.agentproxy.ConnectorFactory; 071import com.jcraft.jsch.agentproxy.RemoteIdentityRepository; 072 073/** 074 * AbstractJschWagon 075 */ 076public abstract class AbstractJschWagon 077 extends StreamWagon 078 implements SshWagon, CommandExecutor 079{ 080 protected ScpHelper sshTool = new ScpHelper( this ); 081 082 protected Session session; 083 084 private String strictHostKeyChecking; 085 086 /** 087 * @plexus.requirement role-hint="file" 088 */ 089 private volatile KnownHostsProvider knownHostsProvider; 090 091 /** 092 * @plexus.requirement 093 */ 094 private volatile InteractiveUserInfo interactiveUserInfo; 095 096 /** 097 * @plexus.configuration default-value="gssapi-with-mic,publickey,password,keyboard-interactive" 098 */ 099 private volatile String preferredAuthentications; 100 101 /** 102 * @plexus.requirement 103 */ 104 private volatile UIKeyboardInteractive uIKeyboardInteractive; 105 106 private static final int SOCKS5_PROXY_PORT = 1080; 107 108 protected static final String EXEC_CHANNEL = "exec"; 109 110 public void openConnectionInternal() 111 throws AuthenticationException 112 { 113 if ( authenticationInfo == null ) 114 { 115 authenticationInfo = new AuthenticationInfo(); 116 } 117 118 if ( !interactive ) 119 { 120 uIKeyboardInteractive = null; 121 setInteractiveUserInfo( new NullInteractiveUserInfo() ); 122 } 123 124 JSch sch = new JSch(); 125 126 File privateKey; 127 try 128 { 129 privateKey = ScpHelper.getPrivateKey( authenticationInfo ); 130 } 131 catch ( FileNotFoundException e ) 132 { 133 throw new AuthenticationException( e.getMessage() ); 134 } 135 136 //can only pick one method of authentication 137 if ( privateKey != null && privateKey.exists() ) 138 { 139 fireSessionDebug( "Using private key: " + privateKey ); 140 try 141 { 142 sch.addIdentity( privateKey.getAbsolutePath(), authenticationInfo.getPassphrase() ); 143 } 144 catch ( JSchException e ) 145 { 146 throw new AuthenticationException( "Cannot connect. Reason: " + e.getMessage(), e ); 147 } 148 } 149 else 150 { 151 try 152 { 153 Connector connector = ConnectorFactory.getDefault().createConnector(); 154 if ( connector != null ) 155 { 156 IdentityRepository repo = new RemoteIdentityRepository( connector ); 157 sch.setIdentityRepository( repo ); 158 } 159 } 160 catch ( AgentProxyException e ) 161 { 162 fireSessionDebug( "Unable to connect to agent: " + e.toString() ); 163 } 164 165 } 166 167 String host = getRepository().getHost(); 168 int port = 169 repository.getPort() == WagonConstants.UNKNOWN_PORT ? ScpHelper.DEFAULT_SSH_PORT : repository.getPort(); 170 try 171 { 172 String userName = authenticationInfo.getUserName(); 173 if ( userName == null ) 174 { 175 userName = System.getProperty( "user.name" ); 176 } 177 session = sch.getSession( userName, host, port ); 178 session.setTimeout( getTimeout() ); 179 } 180 catch ( JSchException e ) 181 { 182 throw new AuthenticationException( "Cannot connect. Reason: " + e.getMessage(), e ); 183 } 184 185 Proxy proxy = null; 186 ProxyInfo proxyInfo = getProxyInfo( ProxyInfo.PROXY_SOCKS5, getRepository().getHost() ); 187 if ( proxyInfo != null && proxyInfo.getHost() != null ) 188 { 189 proxy = new ProxySOCKS5( proxyInfo.getHost(), proxyInfo.getPort() ); 190 ( (ProxySOCKS5) proxy ).setUserPasswd( proxyInfo.getUserName(), proxyInfo.getPassword() ); 191 } 192 else 193 { 194 proxyInfo = getProxyInfo( ProxyInfo.PROXY_HTTP, getRepository().getHost() ); 195 if ( proxyInfo != null && proxyInfo.getHost() != null ) 196 { 197 proxy = new ProxyHTTP( proxyInfo.getHost(), proxyInfo.getPort() ); 198 ( (ProxyHTTP) proxy ).setUserPasswd( proxyInfo.getUserName(), proxyInfo.getPassword() ); 199 } 200 else 201 { 202 // Backwards compatibility 203 proxyInfo = getProxyInfo( getRepository().getProtocol(), getRepository().getHost() ); 204 if ( proxyInfo != null && proxyInfo.getHost() != null ) 205 { 206 // if port == 1080 we will use SOCKS5 Proxy, otherwise will use HTTP Proxy 207 if ( proxyInfo.getPort() == SOCKS5_PROXY_PORT ) 208 { 209 proxy = new ProxySOCKS5( proxyInfo.getHost(), proxyInfo.getPort() ); 210 ( (ProxySOCKS5) proxy ).setUserPasswd( proxyInfo.getUserName(), proxyInfo.getPassword() ); 211 } 212 else 213 { 214 proxy = new ProxyHTTP( proxyInfo.getHost(), proxyInfo.getPort() ); 215 ( (ProxyHTTP) proxy ).setUserPasswd( proxyInfo.getUserName(), proxyInfo.getPassword() ); 216 } 217 } 218 } 219 } 220 session.setProxy( proxy ); 221 222 // username and password will be given via UserInfo interface. 223 UserInfo ui = new WagonUserInfo( authenticationInfo, getInteractiveUserInfo() ); 224 225 if ( uIKeyboardInteractive != null ) 226 { 227 ui = new UserInfoUIKeyboardInteractiveProxy( ui, uIKeyboardInteractive ); 228 } 229 230 Properties config = new Properties(); 231 if ( getKnownHostsProvider() != null ) 232 { 233 try 234 { 235 String contents = getKnownHostsProvider().getContents(); 236 if ( contents != null ) 237 { 238 sch.setKnownHosts( new ByteArrayInputStream( contents.getBytes() ) ); 239 } 240 } 241 catch ( JSchException e ) 242 { 243 // continue without known_hosts 244 } 245 if ( strictHostKeyChecking == null ) 246 { 247 strictHostKeyChecking = getKnownHostsProvider().getHostKeyChecking(); 248 } 249 config.setProperty( "StrictHostKeyChecking", strictHostKeyChecking ); 250 } 251 252 if ( authenticationInfo.getPassword() != null ) 253 { 254 config.setProperty( "PreferredAuthentications", preferredAuthentications ); 255 } 256 257 config.setProperty( "BatchMode", interactive ? "no" : "yes" ); 258 259 session.setConfig( config ); 260 261 session.setUserInfo( ui ); 262 263 try 264 { 265 session.connect(); 266 } 267 catch ( JSchException e ) 268 { 269 if ( e.getMessage().startsWith( "UnknownHostKey:" ) || e.getMessage().startsWith( "reject HostKey:" ) ) 270 { 271 throw new UnknownHostException( host, e ); 272 } 273 else if ( e.getMessage().contains( "HostKey has been changed" ) ) 274 { 275 throw new KnownHostChangedException( host, e ); 276 } 277 else 278 { 279 throw new AuthenticationException( "Cannot connect. Reason: " + e.getMessage(), e ); 280 } 281 } 282 283 if ( getKnownHostsProvider() != null ) 284 { 285 HostKeyRepository hkr = sch.getHostKeyRepository(); 286 287 HostKey[] hk = hkr.getHostKey( host, null ); 288 try 289 { 290 if ( hk != null ) 291 { 292 for ( HostKey hostKey : hk ) 293 { 294 KnownHostEntry knownHostEntry = new KnownHostEntry( hostKey.getHost(), hostKey.getType(), 295 hostKey.getKey() ); 296 getKnownHostsProvider().addKnownHost( knownHostEntry ); 297 } 298 } 299 } 300 catch ( IOException e ) 301 { 302 closeConnection(); 303 304 throw new AuthenticationException( 305 "Connection aborted - failed to write to known_hosts. Reason: " + e.getMessage(), e ); 306 } 307 } 308 } 309 310 public void closeConnection() 311 { 312 if ( session != null ) 313 { 314 session.disconnect(); 315 session = null; 316 } 317 } 318 319 public Streams executeCommand( String command, boolean ignoreStdErr, boolean ignoreNoneZeroExitCode ) 320 throws CommandExecutionException 321 { 322 ChannelExec channel = null; 323 BufferedReader stdoutReader = null; 324 BufferedReader stderrReader = null; 325 Streams streams = null; 326 try 327 { 328 channel = (ChannelExec) session.openChannel( EXEC_CHANNEL ); 329 330 fireSessionDebug( "Executing: " + command ); 331 channel.setCommand( command + "\n" ); 332 333 stdoutReader = new BufferedReader( new InputStreamReader( channel.getInputStream() ) ); 334 stderrReader = new BufferedReader( new InputStreamReader( channel.getErrStream() ) ); 335 336 channel.connect(); 337 338 streams = CommandExecutorStreamProcessor.processStreams( stderrReader, stdoutReader ); 339 340 stdoutReader.close(); 341 stdoutReader = null; 342 343 stderrReader.close(); 344 stderrReader = null; 345 346 int exitCode = channel.getExitStatus(); 347 348 if ( streams.getErr().length() > 0 && !ignoreStdErr ) 349 { 350 throw new CommandExecutionException( "Exit code: " + exitCode + " - " + streams.getErr() ); 351 } 352 353 if ( exitCode != 0 && !ignoreNoneZeroExitCode ) 354 { 355 throw new CommandExecutionException( "Exit code: " + exitCode + " - " + streams.getErr() ); 356 } 357 358 return streams; 359 } 360 catch ( IOException e ) 361 { 362 throw new CommandExecutionException( "Cannot execute remote command: " + command, e ); 363 } 364 catch ( JSchException e ) 365 { 366 throw new CommandExecutionException( "Cannot execute remote command: " + command, e ); 367 } 368 finally 369 { 370 if ( streams != null ) 371 { 372 fireSessionDebug( "Stdout results:" + streams.getOut() ); 373 fireSessionDebug( "Stderr results:" + streams.getErr() ); 374 } 375 376 IOUtil.close( stdoutReader ); 377 IOUtil.close( stderrReader ); 378 if ( channel != null ) 379 { 380 channel.disconnect(); 381 } 382 } 383 } 384 385 protected void handleGetException( Resource resource, Exception e ) 386 throws TransferFailedException 387 { 388 fireTransferError( resource, e, TransferEvent.REQUEST_GET ); 389 390 String msg = 391 "Error occurred while downloading '" + resource + "' from the remote repository:" + getRepository() + ": " 392 + e.getMessage(); 393 394 throw new TransferFailedException( msg, e ); 395 } 396 397 public List<String> getFileList( String destinationDirectory ) 398 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 399 { 400 return sshTool.getFileList( destinationDirectory, repository ); 401 } 402 403 public void putDirectory( File sourceDirectory, String destinationDirectory ) 404 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 405 { 406 sshTool.putDirectory( this, sourceDirectory, destinationDirectory ); 407 } 408 409 public boolean resourceExists( String resourceName ) 410 throws TransferFailedException, AuthorizationException 411 { 412 return sshTool.resourceExists( resourceName, repository ); 413 } 414 415 public boolean supportsDirectoryCopy() 416 { 417 return true; 418 } 419 420 public void executeCommand( String command ) 421 throws CommandExecutionException 422 { 423 fireTransferDebug( "Executing command: " + command ); 424 425 //backward compatible with wagon 2.10 426 executeCommand( command, false, true ); 427 } 428 429 public Streams executeCommand( String command, boolean ignoreFailures ) 430 throws CommandExecutionException 431 { 432 fireTransferDebug( "Executing command: " + command ); 433 434 //backward compatible with wagon 2.10 435 return executeCommand( command, ignoreFailures, true ); 436 } 437 438 public InteractiveUserInfo getInteractiveUserInfo() 439 { 440 return this.interactiveUserInfo; 441 } 442 443 public KnownHostsProvider getKnownHostsProvider() 444 { 445 return this.knownHostsProvider; 446 } 447 448 public void setInteractiveUserInfo( InteractiveUserInfo interactiveUserInfo ) 449 { 450 this.interactiveUserInfo = interactiveUserInfo; 451 } 452 453 public void setKnownHostsProvider( KnownHostsProvider knownHostsProvider ) 454 { 455 this.knownHostsProvider = knownHostsProvider; 456 } 457 458 public void setUIKeyboardInteractive( UIKeyboardInteractive uIKeyboardInteractive ) 459 { 460 this.uIKeyboardInteractive = uIKeyboardInteractive; 461 } 462 463 public String getPreferredAuthentications() 464 { 465 return preferredAuthentications; 466 } 467 468 public void setPreferredAuthentications( String preferredAuthentications ) 469 { 470 this.preferredAuthentications = preferredAuthentications; 471 } 472 473 public String getStrictHostKeyChecking() 474 { 475 return strictHostKeyChecking; 476 } 477 478 public void setStrictHostKeyChecking( String strictHostKeyChecking ) 479 { 480 this.strictHostKeyChecking = strictHostKeyChecking; 481 } 482}