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 try 132 { 133 Connector connector = ConnectorFactory.getDefault().createConnector(); 134 if ( connector != null ) 135 { 136 IdentityRepository repo = new RemoteIdentityRepository( connector ); 137 sch.setIdentityRepository( repo ); 138 } 139 } 140 catch ( AgentProxyException e ) 141 { 142 fireSessionDebug( "Unable to connect to agent: " + e.toString() ); 143 } 144 145 if ( privateKey != null && privateKey.exists() ) 146 { 147 fireSessionDebug( "Using private key: " + privateKey ); 148 try 149 { 150 sch.addIdentity( privateKey.getAbsolutePath(), authenticationInfo.getPassphrase() ); 151 } 152 catch ( JSchException e ) 153 { 154 throw new AuthenticationException( "Cannot connect. Reason: " + e.getMessage(), e ); 155 } 156 } 157 158 String host = getRepository().getHost(); 159 int port = 160 repository.getPort() == WagonConstants.UNKNOWN_PORT ? ScpHelper.DEFAULT_SSH_PORT : repository.getPort(); 161 try 162 { 163 String userName = authenticationInfo.getUserName(); 164 if ( userName == null ) 165 { 166 userName = System.getProperty( "user.name" ); 167 } 168 session = sch.getSession( userName, host, port ); 169 session.setTimeout( getTimeout() ); 170 } 171 catch ( JSchException e ) 172 { 173 throw new AuthenticationException( "Cannot connect. Reason: " + e.getMessage(), e ); 174 } 175 176 Proxy proxy = null; 177 ProxyInfo proxyInfo = getProxyInfo( ProxyInfo.PROXY_SOCKS5, getRepository().getHost() ); 178 if ( proxyInfo != null && proxyInfo.getHost() != null ) 179 { 180 proxy = new ProxySOCKS5( proxyInfo.getHost(), proxyInfo.getPort() ); 181 ( (ProxySOCKS5) proxy ).setUserPasswd( proxyInfo.getUserName(), proxyInfo.getPassword() ); 182 } 183 else 184 { 185 proxyInfo = getProxyInfo( ProxyInfo.PROXY_HTTP, getRepository().getHost() ); 186 if ( proxyInfo != null && proxyInfo.getHost() != null ) 187 { 188 proxy = new ProxyHTTP( proxyInfo.getHost(), proxyInfo.getPort() ); 189 ( (ProxyHTTP) proxy ).setUserPasswd( proxyInfo.getUserName(), proxyInfo.getPassword() ); 190 } 191 else 192 { 193 // Backwards compatibility 194 proxyInfo = getProxyInfo( getRepository().getProtocol(), getRepository().getHost() ); 195 if ( proxyInfo != null && proxyInfo.getHost() != null ) 196 { 197 // if port == 1080 we will use SOCKS5 Proxy, otherwise will use HTTP Proxy 198 if ( proxyInfo.getPort() == SOCKS5_PROXY_PORT ) 199 { 200 proxy = new ProxySOCKS5( proxyInfo.getHost(), proxyInfo.getPort() ); 201 ( (ProxySOCKS5) proxy ).setUserPasswd( proxyInfo.getUserName(), proxyInfo.getPassword() ); 202 } 203 else 204 { 205 proxy = new ProxyHTTP( proxyInfo.getHost(), proxyInfo.getPort() ); 206 ( (ProxyHTTP) proxy ).setUserPasswd( proxyInfo.getUserName(), proxyInfo.getPassword() ); 207 } 208 } 209 } 210 } 211 session.setProxy( proxy ); 212 213 // username and password will be given via UserInfo interface. 214 UserInfo ui = new WagonUserInfo( authenticationInfo, getInteractiveUserInfo() ); 215 216 if ( uIKeyboardInteractive != null ) 217 { 218 ui = new UserInfoUIKeyboardInteractiveProxy( ui, uIKeyboardInteractive ); 219 } 220 221 Properties config = new Properties(); 222 if ( getKnownHostsProvider() != null ) 223 { 224 try 225 { 226 String contents = getKnownHostsProvider().getContents(); 227 if ( contents != null ) 228 { 229 sch.setKnownHosts( new StringInputStream( contents ) ); 230 } 231 } 232 catch ( JSchException e ) 233 { 234 // continue without known_hosts 235 } 236 config.setProperty( "StrictHostKeyChecking", getKnownHostsProvider().getHostKeyChecking() ); 237 } 238 239 if ( authenticationInfo.getPassword() != null ) 240 { 241 config.setProperty( "PreferredAuthentications", "gssapi-with-mic,publickey,password,keyboard-interactive" ); 242 } 243 244 config.setProperty( "BatchMode", interactive ? "no" : "yes" ); 245 246 session.setConfig( config ); 247 248 session.setUserInfo( ui ); 249 250 StringWriter stringWriter = new StringWriter(); 251 try 252 { 253 session.connect(); 254 255 if ( getKnownHostsProvider() != null ) 256 { 257 PrintWriter w = new PrintWriter( stringWriter ); 258 259 HostKeyRepository hkr = sch.getHostKeyRepository(); 260 HostKey[] keys = hkr.getHostKey(); 261 262 for ( int i = 0; keys != null && i < keys.length; i++ ) 263 { 264 HostKey key = keys[i]; 265 w.println( key.getHost() + " " + key.getType() + " " + key.getKey() ); 266 } 267 } 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 try 286 { 287 getKnownHostsProvider().storeKnownHosts( stringWriter.toString() ); 288 } 289 catch ( IOException e ) 290 { 291 closeConnection(); 292 293 throw new AuthenticationException( 294 "Connection aborted - failed to write to known_hosts. Reason: " + e.getMessage(), e ); 295 } 296 } 297 298 public void closeConnection() 299 { 300 if ( session != null ) 301 { 302 session.disconnect(); 303 session = null; 304 } 305 } 306 307 public Streams executeCommand( String command, boolean ignoreFailures ) 308 throws CommandExecutionException 309 { 310 ChannelExec channel = null; 311 BufferedReader stdoutReader = null; 312 BufferedReader stderrReader = null; 313 try 314 { 315 channel = (ChannelExec) session.openChannel( EXEC_CHANNEL ); 316 317 channel.setCommand( command + "\n" ); 318 319 InputStream stdout = channel.getInputStream(); 320 InputStream stderr = channel.getErrStream(); 321 322 channel.connect(); 323 324 stdoutReader = new BufferedReader( new InputStreamReader( stdout ) ); 325 stderrReader = new BufferedReader( new InputStreamReader( stderr ) ); 326 327 Streams streams = CommandExecutorStreamProcessor.processStreams( stderrReader, stdoutReader ); 328 329 if ( streams.getErr().length() > 0 && !ignoreFailures ) 330 { 331 int exitCode = channel.getExitStatus(); 332 throw new CommandExecutionException( "Exit code: " + exitCode + " - " + streams.getErr() ); 333 } 334 335 return streams; 336 } 337 catch ( IOException e ) 338 { 339 throw new CommandExecutionException( "Cannot execute remote command: " + command, e ); 340 } 341 catch ( JSchException e ) 342 { 343 throw new CommandExecutionException( "Cannot execute remote command: " + command, e ); 344 } 345 finally 346 { 347 IOUtil.close( stdoutReader ); 348 IOUtil.close( stderrReader ); 349 if ( channel != null ) 350 { 351 channel.disconnect(); 352 } 353 } 354 } 355 356 protected void handleGetException( Resource resource, Exception e ) 357 throws TransferFailedException 358 { 359 fireTransferError( resource, e, TransferEvent.REQUEST_GET ); 360 361 String msg = 362 "Error occurred while downloading '" + resource + "' from the remote repository:" + getRepository() + ": " 363 + e.getMessage(); 364 365 throw new TransferFailedException( msg, e ); 366 } 367 368 public List<String> getFileList( String destinationDirectory ) 369 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 370 { 371 return sshTool.getFileList( destinationDirectory, repository ); 372 } 373 374 public void putDirectory( File sourceDirectory, String destinationDirectory ) 375 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 376 { 377 sshTool.putDirectory( this, sourceDirectory, destinationDirectory ); 378 } 379 380 public boolean resourceExists( String resourceName ) 381 throws TransferFailedException, AuthorizationException 382 { 383 return sshTool.resourceExists( resourceName, repository ); 384 } 385 386 public boolean supportsDirectoryCopy() 387 { 388 return true; 389 } 390 391 public void executeCommand( String command ) 392 throws CommandExecutionException 393 { 394 fireTransferDebug( "Executing command: " + command ); 395 396 executeCommand( command, false ); 397 } 398 399 public InteractiveUserInfo getInteractiveUserInfo() 400 { 401 return this.interactiveUserInfo; 402 } 403 404 public KnownHostsProvider getKnownHostsProvider() 405 { 406 return this.knownHostsProvider; 407 } 408 409 public void setInteractiveUserInfo( InteractiveUserInfo interactiveUserInfo ) 410 { 411 this.interactiveUserInfo = interactiveUserInfo; 412 } 413 414 public void setKnownHostsProvider( KnownHostsProvider knownHostsProvider ) 415 { 416 this.knownHostsProvider = knownHostsProvider; 417 } 418}