1
2
3
4
5
6
7
8
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
40
41 public class HttpClientConnector extends AbstractClientProxyConnector {
42
43 private static final String HTTP_HEADER_PROXY_AUTHENTICATION = "Proxy-Authentication:";
44
45 private static final String HTTP_HEADER_PROXY_AUTHORIZATION = "Proxy-Authorization:";
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
61
62
63
64
65
66
67
68
69 public HttpClientConnector(@NonNull InetSocketAddress proxyAddress,
70 @NonNull InetSocketAddress remoteAddress) {
71 this(proxyAddress, remoteAddress, null, null);
72 }
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
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
144
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",
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");
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>"));
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
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";
299 }
300
301 @Override
302 protected void askCredentials() {
303
304 if (asked) {
305 throw new IllegalStateException(
306 "Basic auth: already asked user for password");
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
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";
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 }