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