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