HttpClientConnector.java

  1. /*
  2.  * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> and others
  3.  *
  4.  * This program and the accompanying materials are made available under the
  5.  * terms of the Eclipse Distribution License v. 1.0 which is available at
  6.  * https://www.eclipse.org/org/documents/edl-v10.php.
  7.  *
  8.  * SPDX-License-Identifier: BSD-3-Clause
  9.  */
  10. package org.eclipse.jgit.internal.transport.sshd.proxy;

  11. import static java.nio.charset.StandardCharsets.US_ASCII;
  12. import static java.nio.charset.StandardCharsets.UTF_8;
  13. import static java.text.MessageFormat.format;

  14. import java.io.IOException;
  15. import java.net.HttpURLConnection;
  16. import java.net.InetSocketAddress;
  17. import java.util.ArrayList;
  18. import java.util.Arrays;
  19. import java.util.Iterator;
  20. import java.util.List;

  21. import org.apache.sshd.client.session.ClientSession;
  22. import org.apache.sshd.common.io.IoSession;
  23. import org.apache.sshd.common.util.Readable;
  24. import org.apache.sshd.common.util.buffer.Buffer;
  25. import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
  26. import org.eclipse.jgit.annotations.NonNull;
  27. import org.eclipse.jgit.internal.transport.sshd.GssApiMechanisms;
  28. import org.eclipse.jgit.internal.transport.sshd.SshdText;
  29. import org.eclipse.jgit.internal.transport.sshd.auth.AuthenticationHandler;
  30. import org.eclipse.jgit.internal.transport.sshd.auth.BasicAuthentication;
  31. import org.eclipse.jgit.internal.transport.sshd.auth.GssApiAuthentication;
  32. import org.eclipse.jgit.util.Base64;
  33. import org.ietf.jgss.GSSContext;

  34. /**
  35.  * Simple HTTP proxy connector using Basic Authentication.
  36.  */
  37. public class HttpClientConnector extends AbstractClientProxyConnector {

  38.     private static final String HTTP_HEADER_PROXY_AUTHENTICATION = "Proxy-Authentication:"; //$NON-NLS-1$

  39.     private static final String HTTP_HEADER_PROXY_AUTHORIZATION = "Proxy-Authorization:"; //$NON-NLS-1$

  40.     private HttpAuthenticationHandler basic;

  41.     private HttpAuthenticationHandler negotiate;

  42.     private List<HttpAuthenticationHandler> availableAuthentications;

  43.     private Iterator<HttpAuthenticationHandler> clientAuthentications;

  44.     private HttpAuthenticationHandler authenticator;

  45.     private boolean ongoing;

  46.     /**
  47.      * Creates a new {@link HttpClientConnector}. The connector supports
  48.      * anonymous proxy connections as well as Basic and Negotiate
  49.      * authentication.
  50.      *
  51.      * @param proxyAddress
  52.      *            of the proxy server we're connecting to
  53.      * @param remoteAddress
  54.      *            of the target server to connect to
  55.      */
  56.     public HttpClientConnector(@NonNull InetSocketAddress proxyAddress,
  57.             @NonNull InetSocketAddress remoteAddress) {
  58.         this(proxyAddress, remoteAddress, null, null);
  59.     }

  60.     /**
  61.      * Creates a new {@link HttpClientConnector}. The connector supports
  62.      * anonymous proxy connections as well as Basic and Negotiate
  63.      * authentication. If a user name and password are given, the connector
  64.      * tries pre-emptive Basic authentication.
  65.      *
  66.      * @param proxyAddress
  67.      *            of the proxy server we're connecting to
  68.      * @param remoteAddress
  69.      *            of the target server to connect to
  70.      * @param proxyUser
  71.      *            to authenticate at the proxy with
  72.      * @param proxyPassword
  73.      *            to authenticate at the proxy with
  74.      */
  75.     public HttpClientConnector(@NonNull InetSocketAddress proxyAddress,
  76.             @NonNull InetSocketAddress remoteAddress, String proxyUser,
  77.             char[] proxyPassword) {
  78.         super(proxyAddress, remoteAddress, proxyUser, proxyPassword);
  79.         basic = new HttpBasicAuthentication();
  80.         negotiate = new NegotiateAuthentication();
  81.         availableAuthentications = new ArrayList<>(2);
  82.         availableAuthentications.add(negotiate);
  83.         availableAuthentications.add(basic);
  84.         clientAuthentications = availableAuthentications.iterator();
  85.     }

  86.     private void close() {
  87.         HttpAuthenticationHandler current = authenticator;
  88.         authenticator = null;
  89.         if (current != null) {
  90.             current.close();
  91.         }
  92.     }

  93.     @Override
  94.     public void sendClientProxyMetadata(ClientSession sshSession)
  95.             throws Exception {
  96.         init(sshSession);
  97.         IoSession session = sshSession.getIoSession();
  98.         session.addCloseFutureListener(f -> close());
  99.         StringBuilder msg = connect();
  100.         if ((proxyUser != null && !proxyUser.isEmpty())
  101.                 || (proxyPassword != null && proxyPassword.length > 0)) {
  102.             authenticator = basic;
  103.             basic.setParams(null);
  104.             basic.start();
  105.             msg = authenticate(msg, basic.getToken());
  106.             clearPassword();
  107.             proxyUser = null;
  108.         }
  109.         ongoing = true;
  110.         try {
  111.             send(msg, session);
  112.         } catch (Exception e) {
  113.             ongoing = false;
  114.             throw e;
  115.         }
  116.     }

  117.     private void send(StringBuilder msg, IoSession session) throws Exception {
  118.         byte[] data = eol(msg).toString().getBytes(US_ASCII);
  119.         Buffer buffer = new ByteArrayBuffer(data.length, false);
  120.         buffer.putRawBytes(data);
  121.         session.writeBuffer(buffer).verify(getTimeout());
  122.     }

  123.     private StringBuilder connect() {
  124.         StringBuilder msg = new StringBuilder();
  125.         // Persistent connections are the default in HTTP 1.1 (see RFC 2616),
  126.         // but let's be explicit.
  127.         return msg.append(format(
  128.                 "CONNECT {0}:{1} HTTP/1.1\r\nProxy-Connection: keep-alive\r\nConnection: keep-alive\r\nHost: {0}:{1}\r\n", //$NON-NLS-1$
  129.                 remoteAddress.getHostString(),
  130.                 Integer.toString(remoteAddress.getPort())));
  131.     }

  132.     private StringBuilder authenticate(StringBuilder msg, String token) {
  133.         msg.append(HTTP_HEADER_PROXY_AUTHORIZATION).append(' ').append(token);
  134.         return eol(msg);
  135.     }

  136.     private StringBuilder eol(StringBuilder msg) {
  137.         return msg.append('\r').append('\n');
  138.     }

  139.     @Override
  140.     public void messageReceived(IoSession session, Readable buffer)
  141.             throws Exception {
  142.         try {
  143.             int length = buffer.available();
  144.             byte[] data = new byte[length];
  145.             buffer.getRawBytes(data, 0, length);
  146.             String[] reply = new String(data, US_ASCII)
  147.                     .split("\r\n"); //$NON-NLS-1$
  148.             handleMessage(session, Arrays.asList(reply));
  149.         } catch (Exception e) {
  150.             if (authenticator != null) {
  151.                 authenticator.close();
  152.                 authenticator = null;
  153.             }
  154.             ongoing = false;
  155.             try {
  156.                 setDone(false);
  157.             } catch (Exception inner) {
  158.                 e.addSuppressed(inner);
  159.             }
  160.             throw e;
  161.         }
  162.     }

  163.     private void handleMessage(IoSession session, List<String> reply)
  164.             throws Exception {
  165.         if (reply.isEmpty() || reply.get(0).isEmpty()) {
  166.             throw new IOException(
  167.                     format(SshdText.get().proxyHttpUnexpectedReply,
  168.                             proxyAddress, "<empty>")); //$NON-NLS-1$
  169.         }
  170.         try {
  171.             StatusLine status = HttpParser.parseStatusLine(reply.get(0));
  172.             if (!ongoing) {
  173.                 throw new IOException(format(
  174.                         SshdText.get().proxyHttpUnexpectedReply, proxyAddress,
  175.                         Integer.toString(status.getResultCode()),
  176.                         status.getReason()));
  177.             }
  178.             switch (status.getResultCode()) {
  179.             case HttpURLConnection.HTTP_OK:
  180.                 if (authenticator != null) {
  181.                     authenticator.close();
  182.                 }
  183.                 authenticator = null;
  184.                 ongoing = false;
  185.                 setDone(true);
  186.                 break;
  187.             case HttpURLConnection.HTTP_PROXY_AUTH:
  188.                 List<AuthenticationChallenge> challenges = HttpParser
  189.                         .getAuthenticationHeaders(reply,
  190.                                 HTTP_HEADER_PROXY_AUTHENTICATION);
  191.                 authenticator = selectProtocol(challenges, authenticator);
  192.                 if (authenticator == null) {
  193.                     throw new IOException(
  194.                             format(SshdText.get().proxyCannotAuthenticate,
  195.                                     proxyAddress));
  196.                 }
  197.                 String token = authenticator.getToken();
  198.                 if (token == null) {
  199.                     throw new IOException(
  200.                             format(SshdText.get().proxyCannotAuthenticate,
  201.                                     proxyAddress));
  202.                 }
  203.                 send(authenticate(connect(), token), session);
  204.                 break;
  205.             default:
  206.                 throw new IOException(format(SshdText.get().proxyHttpFailure,
  207.                         proxyAddress, Integer.toString(status.getResultCode()),
  208.                         status.getReason()));
  209.             }
  210.         } catch (HttpParser.ParseException e) {
  211.             throw new IOException(
  212.                     format(SshdText.get().proxyHttpUnexpectedReply,
  213.                             proxyAddress, reply.get(0)),
  214.                     e);
  215.         }
  216.     }

  217.     private HttpAuthenticationHandler selectProtocol(
  218.             List<AuthenticationChallenge> challenges,
  219.             HttpAuthenticationHandler current) throws Exception {
  220.         if (current != null && !current.isDone()) {
  221.             AuthenticationChallenge challenge = getByName(challenges,
  222.                     current.getName());
  223.             if (challenge != null) {
  224.                 current.setParams(challenge);
  225.                 current.process();
  226.                 return current;
  227.             }
  228.         }
  229.         if (current != null) {
  230.             current.close();
  231.         }
  232.         while (clientAuthentications.hasNext()) {
  233.             HttpAuthenticationHandler next = clientAuthentications.next();
  234.             if (!next.isDone()) {
  235.                 AuthenticationChallenge challenge = getByName(challenges,
  236.                         next.getName());
  237.                 if (challenge != null) {
  238.                     next.setParams(challenge);
  239.                     next.start();
  240.                     return next;
  241.                 }
  242.             }
  243.         }
  244.         return null;
  245.     }

  246.     private AuthenticationChallenge getByName(
  247.             List<AuthenticationChallenge> challenges,
  248.             String name) {
  249.         return challenges.stream()
  250.                 .filter(c -> c.getMechanism().equalsIgnoreCase(name))
  251.                 .findFirst().orElse(null);
  252.     }

  253.     private interface HttpAuthenticationHandler
  254.             extends AuthenticationHandler<AuthenticationChallenge, String> {

  255.         public String getName();
  256.     }

  257.     /**
  258.      * @see <a href="https://tools.ietf.org/html/rfc7617">RFC 7617</a>
  259.      */
  260.     private class HttpBasicAuthentication
  261.             extends BasicAuthentication<AuthenticationChallenge, String>
  262.             implements HttpAuthenticationHandler {

  263.         private boolean asked;

  264.         public HttpBasicAuthentication() {
  265.             super(proxyAddress, proxyUser, proxyPassword);
  266.         }

  267.         @Override
  268.         public String getName() {
  269.             return "Basic"; //$NON-NLS-1$
  270.         }

  271.         @Override
  272.         protected void askCredentials() {
  273.             // We ask only once.
  274.             if (asked) {
  275.                 throw new IllegalStateException(
  276.                         "Basic auth: already asked user for password"); //$NON-NLS-1$
  277.             }
  278.             asked = true;
  279.             super.askCredentials();
  280.             done = true;
  281.         }

  282.         @Override
  283.         public String getToken() throws Exception {
  284.             if (user.indexOf(':') >= 0) {
  285.                 throw new IOException(format(
  286.                         SshdText.get().proxyHttpInvalidUserName, proxy, user));
  287.             }
  288.             byte[] rawUser = user.getBytes(UTF_8);
  289.             byte[] toEncode = new byte[rawUser.length + 1 + password.length];
  290.             System.arraycopy(rawUser, 0, toEncode, 0, rawUser.length);
  291.             toEncode[rawUser.length] = ':';
  292.             System.arraycopy(password, 0, toEncode, rawUser.length + 1,
  293.                     password.length);
  294.             Arrays.fill(password, (byte) 0);
  295.             String result = Base64.encodeBytes(toEncode);
  296.             Arrays.fill(toEncode, (byte) 0);
  297.             return getName() + ' ' + result;
  298.         }

  299.     }

  300.     /**
  301.      * @see <a href="https://tools.ietf.org/html/rfc4559">RFC 4559</a>
  302.      */
  303.     private class NegotiateAuthentication
  304.             extends GssApiAuthentication<AuthenticationChallenge, String>
  305.             implements HttpAuthenticationHandler {

  306.         public NegotiateAuthentication() {
  307.             super(proxyAddress);
  308.         }

  309.         @Override
  310.         public String getName() {
  311.             return "Negotiate"; //$NON-NLS-1$
  312.         }

  313.         @Override
  314.         public String getToken() throws Exception {
  315.             return getName() + ' ' + Base64.encodeBytes(token);
  316.         }

  317.         @Override
  318.         protected GSSContext createContext() throws Exception {
  319.             return GssApiMechanisms.createContext(GssApiMechanisms.SPNEGO,
  320.                     GssApiMechanisms.getCanonicalName(proxyAddress));
  321.         }

  322.         @Override
  323.         protected byte[] extractToken(AuthenticationChallenge input)
  324.                 throws Exception {
  325.             String received = input.getToken();
  326.             if (received == null) {
  327.                 return new byte[0];
  328.             }
  329.             return Base64.decode(received);
  330.         }

  331.     }
  332. }