View Javadoc
1   /*
2    * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch>
3    * and other copyright owners as documented in the project's IP log.
4    *
5    * This program and the accompanying materials are made available
6    * under the terms of the Eclipse Distribution License v1.0 which
7    * accompanies this distribution, is reproduced below, and is
8    * available at http://www.eclipse.org/org/documents/edl-v10.php
9    *
10   * All rights reserved.
11   *
12   * Redistribution and use in source and binary forms, with or
13   * without modification, are permitted provided that the following
14   * conditions are met:
15   *
16   * - Redistributions of source code must retain the above copyright
17   *   notice, this list of conditions and the following disclaimer.
18   *
19   * - Redistributions in binary form must reproduce the above
20   *   copyright notice, this list of conditions and the following
21   *   disclaimer in the documentation and/or other materials provided
22   *   with the distribution.
23   *
24   * - Neither the name of the Eclipse Foundation, Inc. nor the
25   *   names of its contributors may be used to endorse or promote
26   *   products derived from this software without specific prior
27   *   written permission.
28   *
29   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
30   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
31   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
32   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
34   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
36   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
37   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
38   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
39   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
40   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
41   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42   */
43  package org.eclipse.jgit.internal.transport.sshd.proxy;
44  
45  import static java.nio.charset.StandardCharsets.US_ASCII;
46  import static java.nio.charset.StandardCharsets.UTF_8;
47  import static java.text.MessageFormat.format;
48  
49  import java.io.IOException;
50  import java.net.HttpURLConnection;
51  import java.net.InetSocketAddress;
52  import java.util.ArrayList;
53  import java.util.Arrays;
54  import java.util.Iterator;
55  import java.util.List;
56  
57  import org.apache.sshd.client.session.ClientSession;
58  import org.apache.sshd.common.io.IoSession;
59  import org.apache.sshd.common.util.Readable;
60  import org.apache.sshd.common.util.buffer.Buffer;
61  import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
62  import org.eclipse.jgit.annotations.NonNull;
63  import org.eclipse.jgit.internal.transport.sshd.GssApiMechanisms;
64  import org.eclipse.jgit.internal.transport.sshd.SshdText;
65  import org.eclipse.jgit.internal.transport.sshd.auth.AuthenticationHandler;
66  import org.eclipse.jgit.internal.transport.sshd.auth.BasicAuthentication;
67  import org.eclipse.jgit.internal.transport.sshd.auth.GssApiAuthentication;
68  import org.eclipse.jgit.util.Base64;
69  import org.ietf.jgss.GSSContext;
70  
71  /**
72   * Simple HTTP proxy connector using Basic Authentication.
73   */
74  public class HttpClientConnector extends AbstractClientProxyConnector {
75  
76  	private static final String HTTP_HEADER_PROXY_AUTHENTICATION = "Proxy-Authentication:"; //$NON-NLS-1$
77  
78  	private static final String HTTP_HEADER_PROXY_AUTHORIZATION = "Proxy-Authorization:"; //$NON-NLS-1$
79  
80  	private HttpAuthenticationHandler basic;
81  
82  	private HttpAuthenticationHandler negotiate;
83  
84  	private List<HttpAuthenticationHandler> availableAuthentications;
85  
86  	private Iterator<HttpAuthenticationHandler> clientAuthentications;
87  
88  	private HttpAuthenticationHandler authenticator;
89  
90  	private boolean ongoing;
91  
92  	/**
93  	 * Creates a new {@link HttpClientConnector}. The connector supports
94  	 * anonymous proxy connections as well as Basic and Negotiate
95  	 * authentication.
96  	 *
97  	 * @param proxyAddress
98  	 *            of the proxy server we're connecting to
99  	 * @param remoteAddress
100 	 *            of the target server to connect to
101 	 */
102 	public HttpClientConnector(@NonNull InetSocketAddress proxyAddress,
103 			@NonNull InetSocketAddress remoteAddress) {
104 		this(proxyAddress, remoteAddress, null, null);
105 	}
106 
107 	/**
108 	 * Creates a new {@link HttpClientConnector}. The connector supports
109 	 * anonymous proxy connections as well as Basic and Negotiate
110 	 * authentication. If a user name and password are given, the connector
111 	 * tries pre-emptive Basic authentication.
112 	 *
113 	 * @param proxyAddress
114 	 *            of the proxy server we're connecting to
115 	 * @param remoteAddress
116 	 *            of the target server to connect to
117 	 * @param proxyUser
118 	 *            to authenticate at the proxy with
119 	 * @param proxyPassword
120 	 *            to authenticate at the proxy with
121 	 */
122 	public HttpClientConnector(@NonNull InetSocketAddress proxyAddress,
123 			@NonNull InetSocketAddress remoteAddress, String proxyUser,
124 			char[] proxyPassword) {
125 		super(proxyAddress, remoteAddress, proxyUser, proxyPassword);
126 		basic = new HttpBasicAuthentication();
127 		negotiate = new NegotiateAuthentication();
128 		availableAuthentications = new ArrayList<>(2);
129 		availableAuthentications.add(negotiate);
130 		availableAuthentications.add(basic);
131 		clientAuthentications = availableAuthentications.iterator();
132 	}
133 
134 	private void close() {
135 		HttpAuthenticationHandler current = authenticator;
136 		authenticator = null;
137 		if (current != null) {
138 			current.close();
139 		}
140 	}
141 
142 	@Override
143 	public void sendClientProxyMetadata(ClientSession sshSession)
144 			throws Exception {
145 		init(sshSession);
146 		IoSession session = sshSession.getIoSession();
147 		session.addCloseFutureListener(f -> close());
148 		StringBuilder msg = connect();
149 		if (proxyUser != null && !proxyUser.isEmpty()
150 				|| proxyPassword != null && proxyPassword.length > 0) {
151 			authenticator = basic;
152 			basic.setParams(null);
153 			basic.start();
154 			msg = authenticate(msg, basic.getToken());
155 			clearPassword();
156 			proxyUser = null;
157 		}
158 		ongoing = true;
159 		try {
160 			send(msg, session);
161 		} catch (Exception e) {
162 			ongoing = false;
163 			throw e;
164 		}
165 	}
166 
167 	private void send(StringBuilder msg, IoSession session) throws Exception {
168 		byte[] data = eol(msg).toString().getBytes(US_ASCII);
169 		Buffer buffer = new ByteArrayBuffer(data.length, false);
170 		buffer.putRawBytes(data);
171 		session.writePacket(buffer).verify(getTimeout());
172 	}
173 
174 	private StringBuilder connect() {
175 		StringBuilder msg = new StringBuilder();
176 		// Persistent connections are the default in HTTP 1.1 (see RFC 2616),
177 		// but let's be explicit.
178 		return msg.append(format(
179 				"CONNECT {0}:{1} HTTP/1.1\r\nProxy-Connection: keep-alive\r\nConnection: keep-alive\r\nHost: {0}:{1}\r\n", //$NON-NLS-1$
180 				remoteAddress.getHostString(),
181 				Integer.toString(remoteAddress.getPort())));
182 	}
183 
184 	private StringBuilder authenticate(StringBuilder msg, String token) {
185 		msg.append(HTTP_HEADER_PROXY_AUTHORIZATION).append(' ').append(token);
186 		return eol(msg);
187 	}
188 
189 	private StringBuilder eol(StringBuilder msg) {
190 		return msg.append('\r').append('\n');
191 	}
192 
193 	@Override
194 	public void messageReceived(IoSession session, Readable buffer)
195 			throws Exception {
196 		try {
197 			int length = buffer.available();
198 			byte[] data = new byte[length];
199 			buffer.getRawBytes(data, 0, length);
200 			String[] reply = new String(data, US_ASCII)
201 					.split("\r\n"); //$NON-NLS-1$
202 			handleMessage(session, Arrays.asList(reply));
203 		} catch (Exception e) {
204 			if (authenticator != null) {
205 				authenticator.close();
206 				authenticator = null;
207 			}
208 			ongoing = false;
209 			try {
210 				setDone(false);
211 			} catch (Exception inner) {
212 				e.addSuppressed(inner);
213 			}
214 			throw e;
215 		}
216 	}
217 
218 	private void handleMessage(IoSession session, List<String> reply)
219 			throws Exception {
220 		if (reply.isEmpty() || reply.get(0).isEmpty()) {
221 			throw new IOException(
222 					format(SshdText.get().proxyHttpUnexpectedReply,
223 							proxyAddress, "<empty>")); //$NON-NLS-1$
224 		}
225 		try {
226 			StatusLine status = HttpParser.parseStatusLine(reply.get(0));
227 			if (!ongoing) {
228 				throw new IOException(format(
229 						SshdText.get().proxyHttpUnexpectedReply, proxyAddress,
230 						Integer.toString(status.getResultCode()),
231 						status.getReason()));
232 			}
233 			switch (status.getResultCode()) {
234 			case HttpURLConnection.HTTP_OK:
235 				if (authenticator != null) {
236 					authenticator.close();
237 				}
238 				authenticator = null;
239 				ongoing = false;
240 				setDone(true);
241 				break;
242 			case HttpURLConnection.HTTP_PROXY_AUTH:
243 				List<AuthenticationChallenge> challenges = HttpParser
244 						.getAuthenticationHeaders(reply,
245 								HTTP_HEADER_PROXY_AUTHENTICATION);
246 				authenticator = selectProtocol(challenges, authenticator);
247 				if (authenticator == null) {
248 					throw new IOException(
249 							format(SshdText.get().proxyCannotAuthenticate,
250 									proxyAddress));
251 				}
252 				String token = authenticator.getToken();
253 				if (token == null) {
254 					throw new IOException(
255 							format(SshdText.get().proxyCannotAuthenticate,
256 									proxyAddress));
257 				}
258 				send(authenticate(connect(), token), session);
259 				break;
260 			default:
261 				throw new IOException(format(SshdText.get().proxyHttpFailure,
262 						proxyAddress, Integer.toString(status.getResultCode()),
263 						status.getReason()));
264 			}
265 		} catch (HttpParser.ParseException e) {
266 			throw new IOException(
267 					format(SshdText.get().proxyHttpUnexpectedReply,
268 					proxyAddress, reply.get(0)));
269 		}
270 	}
271 
272 	private HttpAuthenticationHandler selectProtocol(
273 			List<AuthenticationChallenge> challenges,
274 			HttpAuthenticationHandler current) throws Exception {
275 		if (current != null && !current.isDone()) {
276 			AuthenticationChallenge challenge = getByName(challenges,
277 					current.getName());
278 			if (challenge != null) {
279 				current.setParams(challenge);
280 				current.process();
281 				return current;
282 			}
283 		}
284 		if (current != null) {
285 			current.close();
286 		}
287 		while (clientAuthentications.hasNext()) {
288 			HttpAuthenticationHandler next = clientAuthentications.next();
289 			if (!next.isDone()) {
290 				AuthenticationChallenge challenge = getByName(challenges,
291 						next.getName());
292 				if (challenge != null) {
293 					next.setParams(challenge);
294 					next.start();
295 					return next;
296 				}
297 			}
298 		}
299 		return null;
300 	}
301 
302 	private AuthenticationChallenge getByName(
303 			List<AuthenticationChallenge> challenges,
304 			String name) {
305 		return challenges.stream()
306 				.filter(c -> c.getMechanism().equalsIgnoreCase(name))
307 				.findFirst().orElse(null);
308 	}
309 
310 	private interface HttpAuthenticationHandler
311 			extends AuthenticationHandler<AuthenticationChallenge, String> {
312 
313 		public String getName();
314 	}
315 
316 	/**
317 	 * @see <a href="https://tools.ietf.org/html/rfc7617">RFC 7617</a>
318 	 */
319 	private class HttpBasicAuthentication
320 			extends BasicAuthentication<AuthenticationChallenge, String>
321 			implements HttpAuthenticationHandler {
322 
323 		private boolean asked;
324 
325 		public HttpBasicAuthentication() {
326 			super(proxyAddress, proxyUser, proxyPassword);
327 		}
328 
329 		@Override
330 		public String getName() {
331 			return "Basic"; //$NON-NLS-1$
332 		}
333 
334 		@Override
335 		protected void askCredentials() {
336 			// We ask only once.
337 			if (asked) {
338 				throw new IllegalStateException(
339 						"Basic auth: already asked user for password"); //$NON-NLS-1$
340 			}
341 			asked = true;
342 			super.askCredentials();
343 			done = true;
344 		}
345 
346 		@Override
347 		public String getToken() throws Exception {
348 			if (user.indexOf(':') >= 0) {
349 				throw new IOException(format(
350 						SshdText.get().proxyHttpInvalidUserName, proxy, user));
351 			}
352 			byte[] rawUser = user.getBytes(UTF_8);
353 			byte[] toEncode = new byte[rawUser.length + 1 + password.length];
354 			System.arraycopy(rawUser, 0, toEncode, 0, rawUser.length);
355 			toEncode[rawUser.length] = ':';
356 			System.arraycopy(password, 0, toEncode, rawUser.length + 1,
357 					password.length);
358 			Arrays.fill(password, (byte) 0);
359 			String result = Base64.encodeBytes(toEncode);
360 			Arrays.fill(toEncode, (byte) 0);
361 			return getName() + ' ' + result;
362 		}
363 
364 	}
365 
366 	/**
367 	 * @see <a href="https://tools.ietf.org/html/rfc4559">RFC 4559</a>
368 	 */
369 	private class NegotiateAuthentication
370 			extends GssApiAuthentication<AuthenticationChallenge, String>
371 			implements HttpAuthenticationHandler {
372 
373 		public NegotiateAuthentication() {
374 			super(proxyAddress);
375 		}
376 
377 		@Override
378 		public String getName() {
379 			return "Negotiate"; //$NON-NLS-1$
380 		}
381 
382 		@Override
383 		public String getToken() throws Exception {
384 			return getName() + ' ' + Base64.encodeBytes(token);
385 		}
386 
387 		@Override
388 		protected GSSContext createContext() throws Exception {
389 			return GssApiMechanisms.createContext(GssApiMechanisms.SPNEGO,
390 					GssApiMechanisms.getCanonicalName(proxyAddress));
391 		}
392 
393 		@Override
394 		protected byte[] extractToken(AuthenticationChallenge input)
395 				throws Exception {
396 			String received = input.getToken();
397 			if (received == null) {
398 				return new byte[0];
399 			}
400 			return Base64.decode(received);
401 		}
402 
403 	}
404 }