1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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
73
74 public class HttpClientConnector extends AbstractClientProxyConnector {
75
76 private static final String HTTP_HEADER_PROXY_AUTHENTICATION = "Proxy-Authentication:";
77
78 private static final String HTTP_HEADER_PROXY_AUTHORIZATION = "Proxy-Authorization:";
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
94
95
96
97
98
99
100
101
102 public HttpClientConnector(@NonNull InetSocketAddress proxyAddress,
103 @NonNull InetSocketAddress remoteAddress) {
104 this(proxyAddress, remoteAddress, null, null);
105 }
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
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
177
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",
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");
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>"));
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
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";
332 }
333
334 @Override
335 protected void askCredentials() {
336
337 if (asked) {
338 throw new IllegalStateException(
339 "Basic auth: already asked user for password");
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
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";
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 }