View Javadoc
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  
12  import static java.nio.charset.StandardCharsets.US_ASCII;
13  import static java.nio.charset.StandardCharsets.UTF_8;
14  import static java.text.MessageFormat.format;
15  
16  import java.io.IOException;
17  import java.net.HttpURLConnection;
18  import java.net.InetSocketAddress;
19  import java.util.ArrayList;
20  import java.util.Arrays;
21  import java.util.Iterator;
22  import java.util.List;
23  
24  import org.apache.sshd.client.session.ClientSession;
25  import org.apache.sshd.common.io.IoSession;
26  import org.apache.sshd.common.util.Readable;
27  import org.apache.sshd.common.util.buffer.Buffer;
28  import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
29  import org.eclipse.jgit.annotations.NonNull;
30  import org.eclipse.jgit.internal.transport.sshd.GssApiMechanisms;
31  import org.eclipse.jgit.internal.transport.sshd.SshdText;
32  import org.eclipse.jgit.internal.transport.sshd.auth.AuthenticationHandler;
33  import org.eclipse.jgit.internal.transport.sshd.auth.BasicAuthentication;
34  import org.eclipse.jgit.internal.transport.sshd.auth.GssApiAuthentication;
35  import org.eclipse.jgit.util.Base64;
36  import org.ietf.jgss.GSSContext;
37  
38  /**
39   * Simple HTTP proxy connector using Basic Authentication.
40   */
41  public class HttpClientConnector extends AbstractClientProxyConnector {
42  
43  	private static final String HTTP_HEADER_PROXY_AUTHENTICATION = "Proxy-Authentication:"; //$NON-NLS-1$
44  
45  	private static final String HTTP_HEADER_PROXY_AUTHORIZATION = "Proxy-Authorization:"; //$NON-NLS-1$
46  
47  	private HttpAuthenticationHandler basic;
48  
49  	private HttpAuthenticationHandler negotiate;
50  
51  	private List<HttpAuthenticationHandler> availableAuthentications;
52  
53  	private Iterator<HttpAuthenticationHandler> clientAuthentications;
54  
55  	private HttpAuthenticationHandler authenticator;
56  
57  	private boolean ongoing;
58  
59  	/**
60  	 * Creates a new {@link HttpClientConnector}. The connector supports
61  	 * anonymous proxy connections as well as Basic and Negotiate
62  	 * authentication.
63  	 *
64  	 * @param proxyAddress
65  	 *            of the proxy server we're connecting to
66  	 * @param remoteAddress
67  	 *            of the target server to connect to
68  	 */
69  	public HttpClientConnector(@NonNull InetSocketAddress proxyAddress,
70  			@NonNull InetSocketAddress remoteAddress) {
71  		this(proxyAddress, remoteAddress, null, null);
72  	}
73  
74  	/**
75  	 * Creates a new {@link HttpClientConnector}. The connector supports
76  	 * anonymous proxy connections as well as Basic and Negotiate
77  	 * authentication. If a user name and password are given, the connector
78  	 * tries pre-emptive Basic authentication.
79  	 *
80  	 * @param proxyAddress
81  	 *            of the proxy server we're connecting to
82  	 * @param remoteAddress
83  	 *            of the target server to connect to
84  	 * @param proxyUser
85  	 *            to authenticate at the proxy with
86  	 * @param proxyPassword
87  	 *            to authenticate at the proxy with
88  	 */
89  	public HttpClientConnector(@NonNull InetSocketAddress proxyAddress,
90  			@NonNull InetSocketAddress remoteAddress, String proxyUser,
91  			char[] proxyPassword) {
92  		super(proxyAddress, remoteAddress, proxyUser, proxyPassword);
93  		basic = new HttpBasicAuthentication();
94  		negotiate = new NegotiateAuthentication();
95  		availableAuthentications = new ArrayList<>(2);
96  		availableAuthentications.add(negotiate);
97  		availableAuthentications.add(basic);
98  		clientAuthentications = availableAuthentications.iterator();
99  	}
100 
101 	private void close() {
102 		HttpAuthenticationHandler current = authenticator;
103 		authenticator = null;
104 		if (current != null) {
105 			current.close();
106 		}
107 	}
108 
109 	@Override
110 	public void sendClientProxyMetadata(ClientSession sshSession)
111 			throws Exception {
112 		init(sshSession);
113 		IoSession session = sshSession.getIoSession();
114 		session.addCloseFutureListener(f -> close());
115 		StringBuilder msg = connect();
116 		if (proxyUser != null && !proxyUser.isEmpty()
117 				|| proxyPassword != null && proxyPassword.length > 0) {
118 			authenticator = basic;
119 			basic.setParams(null);
120 			basic.start();
121 			msg = authenticate(msg, basic.getToken());
122 			clearPassword();
123 			proxyUser = null;
124 		}
125 		ongoing = true;
126 		try {
127 			send(msg, session);
128 		} catch (Exception e) {
129 			ongoing = false;
130 			throw e;
131 		}
132 	}
133 
134 	private void send(StringBuilder msg, IoSession session) throws Exception {
135 		byte[] data = eol(msg).toString().getBytes(US_ASCII);
136 		Buffer buffer = new ByteArrayBuffer(data.length, false);
137 		buffer.putRawBytes(data);
138 		session.writePacket(buffer).verify(getTimeout());
139 	}
140 
141 	private StringBuilder connect() {
142 		StringBuilder msg = new StringBuilder();
143 		// Persistent connections are the default in HTTP 1.1 (see RFC 2616),
144 		// but let's be explicit.
145 		return msg.append(format(
146 				"CONNECT {0}:{1} HTTP/1.1\r\nProxy-Connection: keep-alive\r\nConnection: keep-alive\r\nHost: {0}:{1}\r\n", //$NON-NLS-1$
147 				remoteAddress.getHostString(),
148 				Integer.toString(remoteAddress.getPort())));
149 	}
150 
151 	private StringBuilder authenticate(StringBuilder msg, String token) {
152 		msg.append(HTTP_HEADER_PROXY_AUTHORIZATION).append(' ').append(token);
153 		return eol(msg);
154 	}
155 
156 	private StringBuilder eol(StringBuilder msg) {
157 		return msg.append('\r').append('\n');
158 	}
159 
160 	@Override
161 	public void messageReceived(IoSession session, Readable buffer)
162 			throws Exception {
163 		try {
164 			int length = buffer.available();
165 			byte[] data = new byte[length];
166 			buffer.getRawBytes(data, 0, length);
167 			String[] reply = new String(data, US_ASCII)
168 					.split("\r\n"); //$NON-NLS-1$
169 			handleMessage(session, Arrays.asList(reply));
170 		} catch (Exception e) {
171 			if (authenticator != null) {
172 				authenticator.close();
173 				authenticator = null;
174 			}
175 			ongoing = false;
176 			try {
177 				setDone(false);
178 			} catch (Exception inner) {
179 				e.addSuppressed(inner);
180 			}
181 			throw e;
182 		}
183 	}
184 
185 	private void handleMessage(IoSession session, List<String> reply)
186 			throws Exception {
187 		if (reply.isEmpty() || reply.get(0).isEmpty()) {
188 			throw new IOException(
189 					format(SshdText.get().proxyHttpUnexpectedReply,
190 							proxyAddress, "<empty>")); //$NON-NLS-1$
191 		}
192 		try {
193 			StatusLine status = HttpParser.parseStatusLine(reply.get(0));
194 			if (!ongoing) {
195 				throw new IOException(format(
196 						SshdText.get().proxyHttpUnexpectedReply, proxyAddress,
197 						Integer.toString(status.getResultCode()),
198 						status.getReason()));
199 			}
200 			switch (status.getResultCode()) {
201 			case HttpURLConnection.HTTP_OK:
202 				if (authenticator != null) {
203 					authenticator.close();
204 				}
205 				authenticator = null;
206 				ongoing = false;
207 				setDone(true);
208 				break;
209 			case HttpURLConnection.HTTP_PROXY_AUTH:
210 				List<AuthenticationChallenge> challenges = HttpParser
211 						.getAuthenticationHeaders(reply,
212 								HTTP_HEADER_PROXY_AUTHENTICATION);
213 				authenticator = selectProtocol(challenges, authenticator);
214 				if (authenticator == null) {
215 					throw new IOException(
216 							format(SshdText.get().proxyCannotAuthenticate,
217 									proxyAddress));
218 				}
219 				String token = authenticator.getToken();
220 				if (token == null) {
221 					throw new IOException(
222 							format(SshdText.get().proxyCannotAuthenticate,
223 									proxyAddress));
224 				}
225 				send(authenticate(connect(), token), session);
226 				break;
227 			default:
228 				throw new IOException(format(SshdText.get().proxyHttpFailure,
229 						proxyAddress, Integer.toString(status.getResultCode()),
230 						status.getReason()));
231 			}
232 		} catch (HttpParser.ParseException e) {
233 			throw new IOException(
234 					format(SshdText.get().proxyHttpUnexpectedReply,
235 					proxyAddress, reply.get(0)));
236 		}
237 	}
238 
239 	private HttpAuthenticationHandler selectProtocol(
240 			List<AuthenticationChallenge> challenges,
241 			HttpAuthenticationHandler current) throws Exception {
242 		if (current != null && !current.isDone()) {
243 			AuthenticationChallenge challenge = getByName(challenges,
244 					current.getName());
245 			if (challenge != null) {
246 				current.setParams(challenge);
247 				current.process();
248 				return current;
249 			}
250 		}
251 		if (current != null) {
252 			current.close();
253 		}
254 		while (clientAuthentications.hasNext()) {
255 			HttpAuthenticationHandler next = clientAuthentications.next();
256 			if (!next.isDone()) {
257 				AuthenticationChallenge challenge = getByName(challenges,
258 						next.getName());
259 				if (challenge != null) {
260 					next.setParams(challenge);
261 					next.start();
262 					return next;
263 				}
264 			}
265 		}
266 		return null;
267 	}
268 
269 	private AuthenticationChallenge getByName(
270 			List<AuthenticationChallenge> challenges,
271 			String name) {
272 		return challenges.stream()
273 				.filter(c -> c.getMechanism().equalsIgnoreCase(name))
274 				.findFirst().orElse(null);
275 	}
276 
277 	private interface HttpAuthenticationHandler
278 			extends AuthenticationHandler<AuthenticationChallenge, String> {
279 
280 		public String getName();
281 	}
282 
283 	/**
284 	 * @see <a href="https://tools.ietf.org/html/rfc7617">RFC 7617</a>
285 	 */
286 	private class HttpBasicAuthentication
287 			extends BasicAuthentication<AuthenticationChallenge, String>
288 			implements HttpAuthenticationHandler {
289 
290 		private boolean asked;
291 
292 		public HttpBasicAuthentication() {
293 			super(proxyAddress, proxyUser, proxyPassword);
294 		}
295 
296 		@Override
297 		public String getName() {
298 			return "Basic"; //$NON-NLS-1$
299 		}
300 
301 		@Override
302 		protected void askCredentials() {
303 			// We ask only once.
304 			if (asked) {
305 				throw new IllegalStateException(
306 						"Basic auth: already asked user for password"); //$NON-NLS-1$
307 			}
308 			asked = true;
309 			super.askCredentials();
310 			done = true;
311 		}
312 
313 		@Override
314 		public String getToken() throws Exception {
315 			if (user.indexOf(':') >= 0) {
316 				throw new IOException(format(
317 						SshdText.get().proxyHttpInvalidUserName, proxy, user));
318 			}
319 			byte[] rawUser = user.getBytes(UTF_8);
320 			byte[] toEncode = new byte[rawUser.length + 1 + password.length];
321 			System.arraycopy(rawUser, 0, toEncode, 0, rawUser.length);
322 			toEncode[rawUser.length] = ':';
323 			System.arraycopy(password, 0, toEncode, rawUser.length + 1,
324 					password.length);
325 			Arrays.fill(password, (byte) 0);
326 			String result = Base64.encodeBytes(toEncode);
327 			Arrays.fill(toEncode, (byte) 0);
328 			return getName() + ' ' + result;
329 		}
330 
331 	}
332 
333 	/**
334 	 * @see <a href="https://tools.ietf.org/html/rfc4559">RFC 4559</a>
335 	 */
336 	private class NegotiateAuthentication
337 			extends GssApiAuthentication<AuthenticationChallenge, String>
338 			implements HttpAuthenticationHandler {
339 
340 		public NegotiateAuthentication() {
341 			super(proxyAddress);
342 		}
343 
344 		@Override
345 		public String getName() {
346 			return "Negotiate"; //$NON-NLS-1$
347 		}
348 
349 		@Override
350 		public String getToken() throws Exception {
351 			return getName() + ' ' + Base64.encodeBytes(token);
352 		}
353 
354 		@Override
355 		protected GSSContext createContext() throws Exception {
356 			return GssApiMechanisms.createContext(GssApiMechanisms.SPNEGO,
357 					GssApiMechanisms.getCanonicalName(proxyAddress));
358 		}
359 
360 		@Override
361 		protected byte[] extractToken(AuthenticationChallenge input)
362 				throws Exception {
363 			String received = input.getToken();
364 			if (received == null) {
365 				return new byte[0];
366 			}
367 			return Base64.decode(received);
368 		}
369 
370 	}
371 }