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.File; 024import java.io.FileNotFoundException; 025import java.io.IOException; 026import java.io.InputStream; 027import java.io.InputStreamReader; 028import java.io.PrintWriter; 029import java.io.StringWriter; 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.KnownHostsProvider; 052import org.apache.maven.wagon.providers.ssh.knownhost.UnknownHostException; 053import org.apache.maven.wagon.proxy.ProxyInfo; 054import org.apache.maven.wagon.resource.Resource; 055import org.codehaus.plexus.util.IOUtil; 056import org.codehaus.plexus.util.StringInputStream; 057 058import com.jcraft.jsch.agentproxy.AgentProxyException; 059import com.jcraft.jsch.agentproxy.Connector; 060import com.jcraft.jsch.agentproxy.ConnectorFactory; 061import com.jcraft.jsch.agentproxy.RemoteIdentityRepository; 062import com.jcraft.jsch.ChannelExec; 063import com.jcraft.jsch.HostKey; 064import com.jcraft.jsch.HostKeyRepository; 065import com.jcraft.jsch.IdentityRepository; 066import com.jcraft.jsch.JSch; 067import com.jcraft.jsch.JSchException; 068import com.jcraft.jsch.Proxy; 069import com.jcraft.jsch.ProxyHTTP; 070import com.jcraft.jsch.ProxySOCKS5; 071import com.jcraft.jsch.Session; 072import com.jcraft.jsch.UIKeyboardInteractive; 073import com.jcraft.jsch.UserInfo; 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 /** 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.requirement 098 */ 099 private volatile UIKeyboardInteractive uIKeyboardInteractive; 100 101 private static final int SOCKS5_PROXY_PORT = 1080; 102 103 protected static final String EXEC_CHANNEL = "exec"; 104 105 public void openConnectionInternal() 106 throws AuthenticationException 107 { 108 if ( authenticationInfo == null ) 109 { 110 authenticationInfo = new AuthenticationInfo(); 111 } 112 113 if ( !interactive ) 114 { 115 uIKeyboardInteractive = null; 116 setInteractiveUserInfo( new NullInteractiveUserInfo() ); 117 } 118 119 JSch sch = new JSch(); 120 121 File privateKey; 122 try 123 { 124 privateKey = ScpHelper.getPrivateKey( authenticationInfo ); 125 } 126 catch ( FileNotFoundException e ) 127 { 128 throw new AuthenticationException( e.getMessage() ); 129 } 130 131 //can only pick one method of authentication 132 if ( privateKey != null && privateKey.exists() ) 133 { 134 fireSessionDebug( "Using private key: " + privateKey ); 135 try 136 { 137 sch.addIdentity( privateKey.getAbsolutePath(), authenticationInfo.getPassphrase() ); 138 } 139 catch ( JSchException e ) 140 { 141 throw new AuthenticationException( "Cannot connect. Reason: " + e.getMessage(), e ); 142 } 143 } 144 else 145 { 146 try 147 { 148 Connector connector = ConnectorFactory.getDefault().createConnector(); 149 if ( connector != null ) 150 { 151 IdentityRepository repo = new RemoteIdentityRepository( connector ); 152 sch.setIdentityRepository( repo ); 153 } 154 } 155 catch ( AgentProxyException e ) 156 { 157 fireSessionDebug( "Unable to connect to agent: " + e.toString() ); 158 } 159 160 } 161 162 String host = getRepository().getHost(); 163 int port = 164 repository.getPort() == WagonConstants.UNKNOWN_PORT ? ScpHelper.DEFAULT_SSH_PORT : repository.getPort(); 165 try 166 { 167 String userName = authenticationInfo.getUserName(); 168 if ( userName == null ) 169 { 170 userName = System.getProperty( "user.name" ); 171 } 172 session = sch.getSession( userName, host, port ); 173 session.setTimeout( getTimeout() ); 174 } 175 catch ( JSchException e ) 176 { 177 throw new AuthenticationException( "Cannot connect. Reason: " + e.getMessage(), e ); 178 } 179 180 Proxy proxy = null; 181 ProxyInfo proxyInfo = getProxyInfo( ProxyInfo.PROXY_SOCKS5, getRepository().getHost() ); 182 if ( proxyInfo != null && proxyInfo.getHost() != null ) 183 { 184 proxy = new ProxySOCKS5( proxyInfo.getHost(), proxyInfo.getPort() ); 185 ( (ProxySOCKS5) proxy ).setUserPasswd( proxyInfo.getUserName(), proxyInfo.getPassword() ); 186 } 187 else 188 { 189 proxyInfo = getProxyInfo( ProxyInfo.PROXY_HTTP, getRepository().getHost() ); 190 if ( proxyInfo != null && proxyInfo.getHost() != null ) 191 { 192 proxy = new ProxyHTTP( proxyInfo.getHost(), proxyInfo.getPort() ); 193 ( (ProxyHTTP) proxy ).setUserPasswd( proxyInfo.getUserName(), proxyInfo.getPassword() ); 194 } 195 else 196 { 197 // Backwards compatibility 198 proxyInfo = getProxyInfo( getRepository().getProtocol(), getRepository().getHost() ); 199 if ( proxyInfo != null && proxyInfo.getHost() != null ) 200 { 201 // if port == 1080 we will use SOCKS5 Proxy, otherwise will use HTTP Proxy 202 if ( proxyInfo.getPort() == SOCKS5_PROXY_PORT ) 203 { 204 proxy = new ProxySOCKS5( proxyInfo.getHost(), proxyInfo.getPort() ); 205 ( (ProxySOCKS5) proxy ).setUserPasswd( proxyInfo.getUserName(), proxyInfo.getPassword() ); 206 } 207 else 208 { 209 proxy = new ProxyHTTP( proxyInfo.getHost(), proxyInfo.getPort() ); 210 ( (ProxyHTTP) proxy ).setUserPasswd( proxyInfo.getUserName(), proxyInfo.getPassword() ); 211 } 212 } 213 } 214 } 215 session.setProxy( proxy ); 216 217 // username and password will be given via UserInfo interface. 218 UserInfo ui = new WagonUserInfo( authenticationInfo, getInteractiveUserInfo() ); 219 220 if ( uIKeyboardInteractive != null ) 221 { 222 ui = new UserInfoUIKeyboardInteractiveProxy( ui, uIKeyboardInteractive ); 223 } 224 225 Properties config = new Properties(); 226 if ( getKnownHostsProvider() != null ) 227 { 228 try 229 { 230 String contents = getKnownHostsProvider().getContents(); 231 if ( contents != null ) 232 { 233 sch.setKnownHosts( new StringInputStream( contents ) ); 234 } 235 } 236 catch ( JSchException e ) 237 { 238 // continue without known_hosts 239 } 240 config.setProperty( "StrictHostKeyChecking", getKnownHostsProvider().getHostKeyChecking() ); 241 } 242 243 if ( authenticationInfo.getPassword() != null ) 244 { 245 config.setProperty( "PreferredAuthentications", "gssapi-with-mic,publickey,password,keyboard-interactive" ); 246 } 247 248 config.setProperty( "BatchMode", interactive ? "no" : "yes" ); 249 250 session.setConfig( config ); 251 252 session.setUserInfo( ui ); 253 254 StringWriter stringWriter = new StringWriter(); 255 try 256 { 257 session.connect(); 258 259 if ( getKnownHostsProvider() != null ) 260 { 261 PrintWriter w = new PrintWriter( stringWriter ); 262 263 HostKeyRepository hkr = sch.getHostKeyRepository(); 264 HostKey[] keys = hkr.getHostKey(); 265 266 for ( int i = 0; keys != null && i < keys.length; i++ ) 267 { 268 HostKey key = keys[i]; 269 w.println( key.getHost() + " " + key.getType() + " " + key.getKey() ); 270 } 271 } 272 } 273 catch ( JSchException e ) 274 { 275 if ( e.getMessage().startsWith( "UnknownHostKey:" ) || e.getMessage().startsWith( "reject HostKey:" ) ) 276 { 277 throw new UnknownHostException( host, e ); 278 } 279 else if ( e.getMessage().contains( "HostKey has been changed" ) ) 280 { 281 throw new KnownHostChangedException( host, e ); 282 } 283 else 284 { 285 throw new AuthenticationException( "Cannot connect. Reason: " + e.getMessage(), e ); 286 } 287 } 288 289 try 290 { 291 getKnownHostsProvider().storeKnownHosts( stringWriter.toString() ); 292 } 293 catch ( IOException e ) 294 { 295 closeConnection(); 296 297 throw new AuthenticationException( 298 "Connection aborted - failed to write to known_hosts. Reason: " + e.getMessage(), e ); 299 } 300 } 301 302 public void closeConnection() 303 { 304 if ( session != null ) 305 { 306 session.disconnect(); 307 session = null; 308 } 309 } 310 311 public Streams executeCommand( String command, boolean ignoreFailures ) 312 throws CommandExecutionException 313 { 314 ChannelExec channel = null; 315 BufferedReader stdoutReader = null; 316 BufferedReader stderrReader = null; 317 try 318 { 319 channel = (ChannelExec) session.openChannel( EXEC_CHANNEL ); 320 321 channel.setCommand( command + "\n" ); 322 323 InputStream stdout = channel.getInputStream(); 324 InputStream stderr = channel.getErrStream(); 325 326 channel.connect(); 327 328 stdoutReader = new BufferedReader( new InputStreamReader( stdout ) ); 329 stderrReader = new BufferedReader( new InputStreamReader( stderr ) ); 330 331 Streams streams = CommandExecutorStreamProcessor.processStreams( stderrReader, stdoutReader ); 332 333 if ( streams.getErr().length() > 0 && !ignoreFailures ) 334 { 335 int exitCode = channel.getExitStatus(); 336 throw new CommandExecutionException( "Exit code: " + exitCode + " - " + streams.getErr() ); 337 } 338 339 return streams; 340 } 341 catch ( IOException e ) 342 { 343 throw new CommandExecutionException( "Cannot execute remote command: " + command, e ); 344 } 345 catch ( JSchException e ) 346 { 347 throw new CommandExecutionException( "Cannot execute remote command: " + command, e ); 348 } 349 finally 350 { 351 IOUtil.close( stdoutReader ); 352 IOUtil.close( stderrReader ); 353 if ( channel != null ) 354 { 355 channel.disconnect(); 356 } 357 } 358 } 359 360 protected void handleGetException( Resource resource, Exception e ) 361 throws TransferFailedException 362 { 363 fireTransferError( resource, e, TransferEvent.REQUEST_GET ); 364 365 String msg = 366 "Error occurred while downloading '" + resource + "' from the remote repository:" + getRepository() + ": " 367 + e.getMessage(); 368 369 throw new TransferFailedException( msg, e ); 370 } 371 372 public List<String> getFileList( String destinationDirectory ) 373 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 374 { 375 return sshTool.getFileList( destinationDirectory, repository ); 376 } 377 378 public void putDirectory( File sourceDirectory, String destinationDirectory ) 379 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 380 { 381 sshTool.putDirectory( this, sourceDirectory, destinationDirectory ); 382 } 383 384 public boolean resourceExists( String resourceName ) 385 throws TransferFailedException, AuthorizationException 386 { 387 return sshTool.resourceExists( resourceName, repository ); 388 } 389 390 public boolean supportsDirectoryCopy() 391 { 392 return true; 393 } 394 395 public void executeCommand( String command ) 396 throws CommandExecutionException 397 { 398 fireTransferDebug( "Executing command: " + command ); 399 400 executeCommand( command, false ); 401 } 402 403 public InteractiveUserInfo getInteractiveUserInfo() 404 { 405 return this.interactiveUserInfo; 406 } 407 408 public KnownHostsProvider getKnownHostsProvider() 409 { 410 return this.knownHostsProvider; 411 } 412 413 public void setInteractiveUserInfo( InteractiveUserInfo interactiveUserInfo ) 414 { 415 this.interactiveUserInfo = interactiveUserInfo; 416 } 417 418 public void setKnownHostsProvider( KnownHostsProvider knownHostsProvider ) 419 { 420 this.knownHostsProvider = knownHostsProvider; 421 } 422 423 public void setUIKeyboardInteractive( UIKeyboardInteractive uIKeyboardInteractive ) 424 { 425 this.uIKeyboardInteractive = uIKeyboardInteractive; 426 } 427}