@ -72,6 +72,9 @@ import java.net.Proxy;
import java.net.ProxySelector ;
import java.net.URISyntaxException ;
import java.net.URL ;
import java.security.cert.CertPathBuilderException ;
import java.security.cert.CertPathValidatorException ;
import java.security.cert.CertificateException ;
import java.text.MessageFormat ;
import java.util.ArrayList ;
import java.util.Arrays ;
@ -87,6 +90,8 @@ import java.util.TreeMap;
import java.util.zip.GZIPInputStream ;
import java.util.zip.GZIPOutputStream ;
import javax.net.ssl.SSLHandshakeException ;
import org.eclipse.jgit.errors.NoRemoteRepositoryException ;
import org.eclipse.jgit.errors.NotSupportedException ;
import org.eclipse.jgit.errors.PackProtocolException ;
@ -99,13 +104,16 @@ import org.eclipse.jgit.lib.ObjectIdRef;
import org.eclipse.jgit.lib.ProgressMonitor ;
import org.eclipse.jgit.lib.Ref ;
import org.eclipse.jgit.lib.Repository ;
import org.eclipse.jgit.lib.StoredConfig ;
import org.eclipse.jgit.lib.SymbolicRef ;
import org.eclipse.jgit.transport.HttpAuthMethod.Type ;
import org.eclipse.jgit.transport.HttpConfig.HttpRedirectMode ;
import org.eclipse.jgit.transport.http.HttpConnection ;
import org.eclipse.jgit.util.FS ;
import org.eclipse.jgit.util.HttpSupport ;
import org.eclipse.jgit.util.IO ;
import org.eclipse.jgit.util.RawParseUtils ;
import org.eclipse.jgit.util.SystemReader ;
import org.eclipse.jgit.util.TemporaryBuffer ;
import org.eclipse.jgit.util.io.DisabledOutputStream ;
import org.eclipse.jgit.util.io.UnionInputStream ;
@ -249,7 +257,7 @@ public class TransportHttp extends HttpTransport implements WalkTransport,
private URL objectsUrl ;
final HttpConfig http ;
private final HttpConfig http ;
private final ProxySelector proxySelector ;
@ -259,12 +267,17 @@ public class TransportHttp extends HttpTransport implements WalkTransport,
private Map < String , String > headers ;
private boolean sslVerify ;
private boolean sslFailure = false ;
TransportHttp ( final Repository local , final URIish uri )
throws NotSupportedException {
super ( local , uri ) ;
setURI ( uri ) ;
http = new HttpConfig ( local . getConfig ( ) , uri ) ;
proxySelector = ProxySelector . getDefault ( ) ;
sslVerify = http . isSslVerify ( ) ;
}
private URL toURL ( URIish urish ) throws MalformedURLException {
@ -301,6 +314,7 @@ public class TransportHttp extends HttpTransport implements WalkTransport,
setURI ( uri ) ;
http = new HttpConfig ( uri ) ;
proxySelector = ProxySelector . getDefault ( ) ;
sslVerify = http . isSslVerify ( ) ;
}
/ * *
@ -549,6 +563,9 @@ public class TransportHttp extends HttpTransport implements WalkTransport,
throw e ;
} catch ( TransportException e ) {
throw e ;
} catch ( SSLHandshakeException e ) {
handleSslFailure ( e ) ;
continue ; // Re-try
} catch ( IOException e ) {
if ( authMethod . getType ( ) ! = HttpAuthMethod . Type . NONE ) {
if ( ignoreTypes = = null ) {
@ -569,6 +586,120 @@ public class TransportHttp extends HttpTransport implements WalkTransport,
}
}
private static class CredentialItems {
CredentialItem . InformationalMessage message ;
/** Trust the server for this git operation */
CredentialItem . YesNoType now ;
/ * *
* Trust the server for all git operations from this repository ; may be
* { @code null } if the transport was created via
* { @link # TransportHttp ( URIish ) } .
* /
CredentialItem . YesNoType forRepo ;
/** Always trust the server from now on. */
CredentialItem . YesNoType always ;
public CredentialItem [ ] items ( ) {
if ( forRepo = = null ) {
return new CredentialItem [ ] { message , now , always } ;
} else {
return new CredentialItem [ ] { message , now , forRepo , always } ;
}
}
}
private void handleSslFailure ( Throwable e ) throws TransportException {
if ( sslFailure | | ! trustInsecureSslConnection ( e . getCause ( ) ) ) {
throw new TransportException ( uri ,
MessageFormat . format (
JGitText . get ( ) . sslFailureExceptionMessage ,
currentUri . setPass ( null ) ) ,
e ) ;
}
sslFailure = true ;
}
private boolean trustInsecureSslConnection ( Throwable cause ) {
if ( cause instanceof CertificateException
| | cause instanceof CertPathBuilderException
| | cause instanceof CertPathValidatorException ) {
// Certificate expired or revoked, PKIX path building not
// possible, self-signed certificate, host does not match ...
CredentialsProvider provider = getCredentialsProvider ( ) ;
if ( provider ! = null ) {
CredentialItems trust = constructSslTrustItems ( cause ) ;
CredentialItem [ ] items = trust . items ( ) ;
if ( provider . supports ( items ) ) {
boolean answered = provider . get ( uri , items ) ;
if ( answered ) {
// Not canceled
boolean trustNow = trust . now . getValue ( ) ;
boolean trustLocal = trust . forRepo ! = null
& & trust . forRepo . getValue ( ) ;
boolean trustAlways = trust . always . getValue ( ) ;
if ( trustNow | | trustLocal | | trustAlways ) {
sslVerify = false ;
if ( trustAlways ) {
updateSslVerify ( SystemReader . getInstance ( )
. openUserConfig ( null , FS . DETECTED ) ,
false ) ;
} else if ( trustLocal ) {
updateSslVerify ( local . getConfig ( ) , false ) ;
}
return true ;
}
}
}
}
}
return false ;
}
private CredentialItems constructSslTrustItems ( Throwable cause ) {
CredentialItems items = new CredentialItems ( ) ;
String info = MessageFormat . format ( JGitText . get ( ) . sslFailureInfo ,
currentUri . setPass ( null ) ) ;
String sslMessage = cause . getLocalizedMessage ( ) ;
if ( sslMessage = = null ) {
sslMessage = cause . toString ( ) ;
}
sslMessage = MessageFormat . format ( JGitText . get ( ) . sslFailureCause ,
sslMessage ) ;
items . message = new CredentialItem . InformationalMessage ( info + '\n'
+ sslMessage + '\n'
+ JGitText . get ( ) . sslFailureTrustExplanation ) ;
items . now = new CredentialItem . YesNoType ( JGitText . get ( ) . sslTrustNow ) ;
if ( local ! = null ) {
items . forRepo = new CredentialItem . YesNoType (
MessageFormat . format ( JGitText . get ( ) . sslTrustForRepo ,
local . getDirectory ( ) ) ) ;
}
items . always = new CredentialItem . YesNoType (
JGitText . get ( ) . sslTrustAlways ) ;
return items ;
}
private void updateSslVerify ( StoredConfig config , boolean value ) {
// Since git uses the original URI for matching, we must also use the
// original URI and cannot use the current URI (which might be different
// after redirects)
String uriPattern = uri . getScheme ( ) + "://" + uri . getHost ( ) ; //$NON-NLS-1$
int port = uri . getPort ( ) ;
if ( port > 0 ) {
uriPattern + = ":" + port ; //$NON-NLS-1$
}
config . setBoolean ( HttpConfig . HTTP , uriPattern ,
HttpConfig . SSL_VERIFY_KEY , value ) ;
try {
config . save ( ) ;
} catch ( IOException e ) {
LOG . error ( JGitText . get ( ) . sslVerifyCannotSave , e ) ;
}
}
private URIish redirect ( String location , String checkFor , int redirects )
throws TransportException {
if ( location = = null | | location . isEmpty ( ) ) {
@ -687,7 +818,7 @@ public class TransportHttp extends HttpTransport implements WalkTransport,
final Proxy proxy = HttpSupport . proxyFor ( proxySelector , u ) ;
HttpConnection conn = connectionFactory . create ( u , proxy ) ;
if ( ! http . isS slVerify( ) & & "https" . equals ( u . getProtocol ( ) ) ) { //$NON-NLS-1$
if ( ! s slVerify & & "https" . equals ( u . getProtocol ( ) ) ) { //$NON-NLS-1$
HttpSupport . disableSslVerify ( conn ) ;
}
@ -1034,123 +1165,133 @@ public class TransportHttp extends HttpTransport implements WalkTransport,
int authAttempts = 1 ;
int redirects = 0 ;
for ( ; ; ) {
// The very first time we will try with the authentication
// method used on the initial GET request. This is a hint only;
// it may fail. If so, we'll then re-try with proper 401
// handling, going through the available authentication schemes.
openStream ( ) ;
if ( buf ! = out ) {
conn . setRequestProperty ( HDR_CONTENT_ENCODING , ENCODING_GZIP ) ;
}
conn . setFixedLengthStreamingMode ( ( int ) buf . length ( ) ) ;
try ( OutputStream httpOut = conn . getOutputStream ( ) ) {
buf . writeTo ( httpOut , null ) ;
}
final int status = HttpSupport . response ( conn ) ;
switch ( status ) {
case HttpConnection . HTTP_OK :
// We're done.
return ;
case HttpConnection . HTTP_NOT_FOUND :
throw new NoRemoteRepositoryException ( uri , MessageFormat
. format ( JGitText . get ( ) . uriNotFound , conn . getURL ( ) ) ) ;
case HttpConnection . HTTP_FORBIDDEN :
throw new TransportException ( uri ,
MessageFormat . format (
JGitText . get ( ) . serviceNotPermitted ,
baseUrl , serviceName ) ) ;
case HttpConnection . HTTP_MOVED_PERM :
case HttpConnection . HTTP_MOVED_TEMP :
case HttpConnection . HTTP_11_MOVED_TEMP :
// SEE_OTHER after a POST doesn't make sense for a git
// server, so we don't handle it here and thus we'll
// report an error in openResponse() later on.
if ( http . getFollowRedirects ( ) ! = HttpRedirectMode . TRUE ) {
// Let openResponse() issue an error
return ;
try {
// The very first time we will try with the authentication
// method used on the initial GET request. This is a hint
// only; it may fail. If so, we'll then re-try with proper
// 401 handling, going through the available authentication
// schemes.
openStream ( ) ;
if ( buf ! = out ) {
conn . setRequestProperty ( HDR_CONTENT_ENCODING ,
ENCODING_GZIP ) ;
}
currentUri = redirect (
conn . getHeaderField ( HDR_LOCATION ) ,
'/' + serviceName , redirects + + ) ;
try {
baseUrl = toURL ( currentUri ) ;
} catch ( MalformedURLException e ) {
throw new TransportException ( uri , MessageFormat . format (
JGitText . get ( ) . invalidRedirectLocation ,
baseUrl , currentUri ) , e ) ;
conn . setFixedLengthStreamingMode ( ( int ) buf . length ( ) ) ;
try ( OutputStream httpOut = conn . getOutputStream ( ) ) {
buf . writeTo ( httpOut , null ) ;
}
continue ;
case HttpConnection . HTTP_UNAUTHORIZED :
HttpAuthMethod nextMethod = HttpAuthMethod
. scanResponse ( conn , ignoreTypes ) ;
switch ( nextMethod . getType ( ) ) {
case NONE :
final int status = HttpSupport . response ( conn ) ;
switch ( status ) {
case HttpConnection . HTTP_OK :
// We're done.
return ;
case HttpConnection . HTTP_NOT_FOUND :
throw new NoRemoteRepositoryException ( uri ,
MessageFormat . format ( JGitText . get ( ) . uriNotFound ,
conn . getURL ( ) ) ) ;
case HttpConnection . HTTP_FORBIDDEN :
throw new TransportException ( uri ,
MessageFormat . format (
JGitText . get ( ) . authenticationNotSupported ,
conn . getURL ( ) ) ) ;
case NEGOTIATE :
// RFC 4559 states "When using the SPNEGO [...] with
// [...] POST, the authentication should be complete
// [...] before sending the user data." So in theory
// the initial GET should have been authenticated
// already. (Unless there was a redirect?)
//
// We try this only once:
ignoreTypes . add ( HttpAuthMethod . Type . NEGOTIATE ) ;
if ( authenticator ! = null ) {
ignoreTypes . add ( authenticator . getType ( ) ) ;
JGitText . get ( ) . serviceNotPermitted ,
baseUrl , serviceName ) ) ;
case HttpConnection . HTTP_MOVED_PERM :
case HttpConnection . HTTP_MOVED_TEMP :
case HttpConnection . HTTP_11_MOVED_TEMP :
// SEE_OTHER after a POST doesn't make sense for a git
// server, so we don't handle it here and thus we'll
// report an error in openResponse() later on.
if ( http . getFollowRedirects ( ) ! = HttpRedirectMode . TRUE ) {
// Let openResponse() issue an error
return ;
}
authAttempts = 1 ;
// We only do the Kerberos part of SPNEGO, which
// requires only one attempt. We do *not* to the
// NTLM part of SPNEGO; it's a multi-round
// negotiation and among other problems it would
// be unclear when to stop if no HTTP_OK is
// forthcoming. In theory a malicious server
// could keep sending requests for another NTLM
// round, keeping a client stuck here.
break ;
default :
// DIGEST or BASIC. Let's be sure we ignore NEGOTIATE;
// if it was available, we have tried it before.
ignoreTypes . add ( HttpAuthMethod . Type . NEGOTIATE ) ;
if ( authenticator = = null | | authenticator
. getType ( ) ! = nextMethod . getType ( ) ) {
currentUri = redirect ( conn . getHeaderField ( HDR_LOCATION ) ,
'/' + serviceName , redirects + + ) ;
try {
baseUrl = toURL ( currentUri ) ;
} catch ( MalformedURLException e ) {
throw new TransportException ( uri ,
MessageFormat . format (
JGitText . get ( ) . invalidRedirectLocation ,
baseUrl , currentUri ) ,
e ) ;
}
continue ;
case HttpConnection . HTTP_UNAUTHORIZED :
HttpAuthMethod nextMethod = HttpAuthMethod
. scanResponse ( conn , ignoreTypes ) ;
switch ( nextMethod . getType ( ) ) {
case NONE :
throw new TransportException ( uri ,
MessageFormat . format (
JGitText . get ( ) . authenticationNotSupported ,
conn . getURL ( ) ) ) ;
case NEGOTIATE :
// RFC 4559 states "When using the SPNEGO [...] with
// [...] POST, the authentication should be complete
// [...] before sending the user data." So in theory
// the initial GET should have been authenticated
// already. (Unless there was a redirect?)
//
// We try this only once:
ignoreTypes . add ( HttpAuthMethod . Type . NEGOTIATE ) ;
if ( authenticator ! = null ) {
ignoreTypes . add ( authenticator . getType ( ) ) ;
}
authAttempts = 1 ;
// We only do the Kerberos part of SPNEGO, which
// requires only one attempt. We do *not* to the
// NTLM part of SPNEGO; it's a multi-round
// negotiation and among other problems it would
// be unclear when to stop if no HTTP_OK is
// forthcoming. In theory a malicious server
// could keep sending requests for another NTLM
// round, keeping a client stuck here.
break ;
default :
// DIGEST or BASIC. Let's be sure we ignore
// NEGOTIATE; if it was available, we have tried it
// before.
ignoreTypes . add ( HttpAuthMethod . Type . NEGOTIATE ) ;
if ( authenticator = = null | | authenticator
. getType ( ) ! = nextMethod . getType ( ) ) {
if ( authenticator ! = null ) {
ignoreTypes . add ( authenticator . getType ( ) ) ;
}
authAttempts = 1 ;
}
break ;
}
break ;
}
authMethod = nextMethod ;
authenticator = nextMethod ;
CredentialsProvider credentialsProvider = getCredentialsProvider ( ) ;
if ( credentialsProvider = = null ) {
throw new TransportException ( uri ,
JGitText . get ( ) . noCredentialsProvider ) ;
}
if ( authAttempts > 1 ) {
credentialsProvider . reset ( currentUri ) ;
}
if ( 3 < authAttempts | | ! authMethod . authorize ( currentUri ,
credentialsProvider ) ) {
throw new TransportException ( uri ,
JGitText . get ( ) . notAuthorized ) ;
}
authAttempts + + ;
continue ;
authMethod = nextMethod ;
authenticator = nextMethod ;
CredentialsProvider credentialsProvider = getCredentialsProvider ( ) ;
if ( credentialsProvider = = null ) {
throw new TransportException ( uri ,
JGitText . get ( ) . noCredentialsProvider ) ;
}
if ( authAttempts > 1 ) {
credentialsProvider . reset ( currentUri ) ;
}
if ( 3 < authAttempts | | ! authMethod
. authorize ( currentUri , credentialsProvider ) ) {
throw new TransportException ( uri ,
JGitText . get ( ) . notAuthorized ) ;
}
authAttempts + + ;
continue ;
default :
// Just return here; openResponse() will report an appropriate
// error.
return ;
default :
// Just return here; openResponse() will report an
// appropriate error.
return ;
}
} catch ( SSLHandshakeException e ) {
handleSslFailure ( e ) ;
continue ; // Re-try
}
}
}