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.InetAddress;
51 import java.net.InetSocketAddress;
52
53 import org.apache.sshd.client.session.ClientSession;
54 import org.apache.sshd.common.io.IoSession;
55 import org.apache.sshd.common.util.Readable;
56 import org.apache.sshd.common.util.buffer.Buffer;
57 import org.apache.sshd.common.util.buffer.BufferUtils;
58 import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
59 import org.eclipse.jgit.annotations.NonNull;
60 import org.eclipse.jgit.internal.transport.sshd.GssApiMechanisms;
61 import org.eclipse.jgit.internal.transport.sshd.SshdText;
62 import org.eclipse.jgit.internal.transport.sshd.auth.AuthenticationHandler;
63 import org.eclipse.jgit.internal.transport.sshd.auth.BasicAuthentication;
64 import org.eclipse.jgit.internal.transport.sshd.auth.GssApiAuthentication;
65 import org.eclipse.jgit.transport.SshConstants;
66 import org.ietf.jgss.GSSContext;
67
68
69
70
71
72
73 public class Socks5ClientConnector extends AbstractClientProxyConnector {
74
75
76 private static final byte SOCKS_VERSION_5 = 5;
77
78 private static final byte SOCKS_CMD_CONNECT = 1;
79
80
81
82
83
84 private static final byte SOCKS_ADDRESS_IPv4 = 1;
85
86 private static final byte SOCKS_ADDRESS_FQDN = 3;
87
88 private static final byte SOCKS_ADDRESS_IPv6 = 4;
89
90
91
92 private static final byte SOCKS_REPLY_SUCCESS = 0;
93
94 private static final byte SOCKS_REPLY_FAILURE = 1;
95
96 private static final byte SOCKS_REPLY_FORBIDDEN = 2;
97
98 private static final byte SOCKS_REPLY_NETWORK_UNREACHABLE = 3;
99
100 private static final byte SOCKS_REPLY_HOST_UNREACHABLE = 4;
101
102 private static final byte SOCKS_REPLY_CONNECTION_REFUSED = 5;
103
104 private static final byte SOCKS_REPLY_TTL_EXPIRED = 6;
105
106 private static final byte SOCKS_REPLY_COMMAND_UNSUPPORTED = 7;
107
108 private static final byte SOCKS_REPLY_ADDRESS_UNSUPPORTED = 8;
109
110
111
112
113
114
115
116
117 private enum SocksAuthenticationMethod {
118
119 ANONYMOUS(0),
120 GSSAPI(1),
121 PASSWORD(2),
122
123
124
125
126
127
128 NONE_ACCEPTABLE(0xFF);
129
130 private byte value;
131
132 SocksAuthenticationMethod(int value) {
133 this.value = (byte) value;
134 }
135
136 public byte getValue() {
137 return value;
138 }
139 }
140
141 private enum ProtocolState {
142 NONE,
143
144 INIT {
145 @Override
146 public void handleMessage(Socks5ClientConnector connector,
147 IoSession session, Buffer data) throws Exception {
148 connector.versionCheck(data.getByte());
149 SocksAuthenticationMethod authMethod = connector.getAuthMethod(
150 data.getByte());
151 switch (authMethod) {
152 case ANONYMOUS:
153 connector.sendConnectInfo(session);
154 break;
155 case PASSWORD:
156 connector.doPasswordAuth(session);
157 break;
158 case GSSAPI:
159 connector.doGssApiAuth(session);
160 break;
161 default:
162 throw new IOException(
163 format(SshdText.get().proxyCannotAuthenticate,
164 connector.proxyAddress));
165 }
166 }
167 },
168
169 AUTHENTICATING {
170 @Override
171 public void handleMessage(Socks5ClientConnector connector,
172 IoSession session, Buffer data) throws Exception {
173 connector.authStep(session, data);
174 }
175 },
176
177 CONNECTING {
178 @Override
179 public void handleMessage(Socks5ClientConnector connector,
180 IoSession session, Buffer data) throws Exception {
181
182
183
184
185
186
187
188
189
190
191
192
193 if (data.available() != 4) {
194 connector.versionCheck(data.getByte());
195 connector.establishConnection(data);
196 }
197 }
198 },
199
200 CONNECTED,
201
202 FAILED;
203
204 public void handleMessage(Socks5ClientConnector connector,
205 @SuppressWarnings("unused") IoSession session, Buffer data)
206 throws Exception {
207 throw new IOException(
208 format(SshdText.get().proxySocksUnexpectedMessage,
209 connector.proxyAddress, this,
210 BufferUtils.toHex(data.array())));
211 }
212 }
213
214 private ProtocolState state;
215
216 private AuthenticationHandler<Buffer, Buffer> authenticator;
217
218 private GSSContext context;
219
220 private byte[] authenticationProposals;
221
222
223
224
225
226
227
228
229
230
231
232 public Socks5ClientConnector(@NonNull InetSocketAddress proxyAddress,
233 @NonNull InetSocketAddress remoteAddress) {
234 this(proxyAddress, remoteAddress, null, null);
235 }
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251 public Socks5ClientConnector(@NonNull InetSocketAddress proxyAddress,
252 @NonNull InetSocketAddress remoteAddress,
253 String proxyUser, char[] proxyPassword) {
254 super(proxyAddress, remoteAddress, proxyUser, proxyPassword);
255 this.state = ProtocolState.NONE;
256 }
257
258 @Override
259 public void sendClientProxyMetadata(ClientSession sshSession)
260 throws Exception {
261 init(sshSession);
262 IoSession session = sshSession.getIoSession();
263
264 Buffer buffer = new ByteArrayBuffer(5, false);
265 buffer.putByte(SOCKS_VERSION_5);
266 context = getGSSContext(remoteAddress);
267 authenticationProposals = getAuthenticationProposals();
268 buffer.putByte((byte) authenticationProposals.length);
269 buffer.putRawBytes(authenticationProposals);
270 state = ProtocolState.INIT;
271 session.writePacket(buffer).verify(getTimeout());
272 }
273
274 private byte[] getAuthenticationProposals() {
275 byte[] proposals = new byte[3];
276 int i = 0;
277 proposals[i++] = SocksAuthenticationMethod.ANONYMOUS.getValue();
278 proposals[i++] = SocksAuthenticationMethod.PASSWORD.getValue();
279 if (context != null) {
280 proposals[i++] = SocksAuthenticationMethod.GSSAPI.getValue();
281 }
282 if (i == proposals.length) {
283 return proposals;
284 } else {
285 byte[] result = new byte[i];
286 System.arraycopy(proposals, 0, result, 0, i);
287 return result;
288 }
289 }
290
291 private void sendConnectInfo(IoSession session) throws Exception {
292 GssApiMechanisms.closeContextSilently(context);
293
294 byte[] rawAddress = getRawAddress(remoteAddress);
295 byte[] remoteName = null;
296 byte type;
297 int length = 0;
298 if (rawAddress == null) {
299 remoteName = remoteAddress.getHostString().getBytes(US_ASCII);
300 if (remoteName == null || remoteName.length == 0) {
301 throw new IOException(
302 format(SshdText.get().proxySocksNoRemoteHostName,
303 remoteAddress));
304 } else if (remoteName.length > 255) {
305
306
307 throw new IOException(format(
308 "Proxy host name too long for SOCKS (at most 255 characters): {0}",
309 remoteAddress.getHostString()));
310 }
311 type = SOCKS_ADDRESS_FQDN;
312 length = remoteName.length + 1;
313 } else {
314 length = rawAddress.length;
315 type = length == 4 ? SOCKS_ADDRESS_IPv4 : SOCKS_ADDRESS_IPv6;
316 }
317 Buffer buffer = new ByteArrayBuffer(4 + length + 2, false);
318 buffer.putByte(SOCKS_VERSION_5);
319 buffer.putByte(SOCKS_CMD_CONNECT);
320 buffer.putByte((byte) 0);
321 buffer.putByte(type);
322 if (remoteName != null) {
323 buffer.putByte((byte) remoteName.length);
324 buffer.putRawBytes(remoteName);
325 } else {
326 buffer.putRawBytes(rawAddress);
327 }
328 int port = remoteAddress.getPort();
329 if (port <= 0) {
330 port = SshConstants.SSH_DEFAULT_PORT;
331 }
332 buffer.putByte((byte) ((port >> 8) & 0xFF));
333 buffer.putByte((byte) (port & 0xFF));
334 state = ProtocolState.CONNECTING;
335 session.writePacket(buffer).verify(getTimeout());
336 }
337
338 private void doPasswordAuth(IoSession session) throws Exception {
339 GssApiMechanisms.closeContextSilently(context);
340 authenticator = new SocksBasicAuthentication();
341 session.addCloseFutureListener(f -> close());
342 startAuth(session);
343 }
344
345 private void doGssApiAuth(IoSession session) throws Exception {
346 authenticator = new SocksGssApiAuthentication();
347 session.addCloseFutureListener(f -> close());
348 startAuth(session);
349 }
350
351 private void close() {
352 AuthenticationHandler<?, ?> handler = authenticator;
353 authenticator = null;
354 if (handler != null) {
355 handler.close();
356 }
357 }
358
359 private void startAuth(IoSession session) throws Exception {
360 Buffer buffer = null;
361 try {
362 authenticator.setParams(null);
363 authenticator.start();
364 buffer = authenticator.getToken();
365 state = ProtocolState.AUTHENTICATING;
366 if (buffer == null) {
367
368 throw new IOException(
369 "No data for proxy authentication with "
370 + proxyAddress);
371 }
372 session.writePacket(buffer).verify(getTimeout());
373 } finally {
374 if (buffer != null) {
375 buffer.clear(true);
376 }
377 }
378 }
379
380 private void authStep(IoSession session, Buffer input) throws Exception {
381 Buffer buffer = null;
382 try {
383 authenticator.setParams(input);
384 authenticator.process();
385 buffer = authenticator.getToken();
386 if (buffer != null) {
387 session.writePacket(buffer).verify(getTimeout());
388 }
389 } finally {
390 if (buffer != null) {
391 buffer.clear(true);
392 }
393 }
394 if (authenticator.isDone()) {
395 sendConnectInfo(session);
396 }
397 }
398
399 private void establishConnection(Buffer data) throws Exception {
400 byte reply = data.getByte();
401 switch (reply) {
402 case SOCKS_REPLY_SUCCESS:
403 state = ProtocolState.CONNECTED;
404 setDone(true);
405 return;
406 case SOCKS_REPLY_FAILURE:
407 throw new IOException(format(
408 SshdText.get().proxySocksFailureGeneral, proxyAddress));
409 case SOCKS_REPLY_FORBIDDEN:
410 throw new IOException(
411 format(SshdText.get().proxySocksFailureForbidden,
412 proxyAddress, remoteAddress));
413 case SOCKS_REPLY_NETWORK_UNREACHABLE:
414 throw new IOException(
415 format(SshdText.get().proxySocksFailureNetworkUnreachable,
416 proxyAddress, remoteAddress));
417 case SOCKS_REPLY_HOST_UNREACHABLE:
418 throw new IOException(
419 format(SshdText.get().proxySocksFailureHostUnreachable,
420 proxyAddress, remoteAddress));
421 case SOCKS_REPLY_CONNECTION_REFUSED:
422 throw new IOException(
423 format(SshdText.get().proxySocksFailureRefused,
424 proxyAddress, remoteAddress));
425 case SOCKS_REPLY_TTL_EXPIRED:
426 throw new IOException(
427 format(SshdText.get().proxySocksFailureTTL, proxyAddress));
428 case SOCKS_REPLY_COMMAND_UNSUPPORTED:
429 throw new IOException(
430 format(SshdText.get().proxySocksFailureUnsupportedCommand,
431 proxyAddress));
432 case SOCKS_REPLY_ADDRESS_UNSUPPORTED:
433 throw new IOException(
434 format(SshdText.get().proxySocksFailureUnsupportedAddress,
435 proxyAddress));
436 default:
437 throw new IOException(format(
438 SshdText.get().proxySocksFailureUnspecified, proxyAddress));
439 }
440 }
441
442 @Override
443 public void messageReceived(IoSession session, Readable buffer)
444 throws Exception {
445 try {
446
447 ByteArrayBuffer data = new ByteArrayBuffer(buffer.available(),
448 false);
449 data.putBuffer(buffer);
450 data.compact();
451 state.handleMessage(this, session, data);
452 } catch (Exception e) {
453 state = ProtocolState.FAILED;
454 if (authenticator != null) {
455 authenticator.close();
456 authenticator = null;
457 }
458 try {
459 setDone(false);
460 } catch (Exception inner) {
461 e.addSuppressed(inner);
462 }
463 throw e;
464 }
465 }
466
467 private void versionCheck(byte version) throws Exception {
468 if (version != SOCKS_VERSION_5) {
469 throw new IOException(
470 format(SshdText.get().proxySocksUnexpectedVersion,
471 Integer.toString(version & 0xFF)));
472 }
473 }
474
475 private SocksAuthenticationMethod getAuthMethod(byte value) {
476 if (value != SocksAuthenticationMethod.NONE_ACCEPTABLE.getValue()) {
477 for (byte proposed : authenticationProposals) {
478 if (proposed == value) {
479 for (SocksAuthenticationMethod method : SocksAuthenticationMethod
480 .values()) {
481 if (method.getValue() == value) {
482 return method;
483 }
484 }
485 break;
486 }
487 }
488 }
489 return SocksAuthenticationMethod.NONE_ACCEPTABLE;
490 }
491
492 private static byte[] getRawAddress(@NonNull InetSocketAddress address) {
493 InetAddress ipAddress = GssApiMechanisms.resolve(address);
494 return ipAddress == null ? null : ipAddress.getAddress();
495 }
496
497 private static GSSContext getGSSContext(
498 @NonNull InetSocketAddress address) {
499 if (!GssApiMechanisms.getSupportedMechanisms()
500 .contains(GssApiMechanisms.KERBEROS_5)) {
501 return null;
502 }
503 return GssApiMechanisms.createContext(GssApiMechanisms.KERBEROS_5,
504 GssApiMechanisms.getCanonicalName(address));
505 }
506
507
508
509
510 private class SocksBasicAuthentication
511 extends BasicAuthentication<Buffer, Buffer> {
512
513 private static final byte SOCKS_BASIC_PROTOCOL_VERSION = 1;
514
515 private static final byte SOCKS_BASIC_AUTH_SUCCESS = 0;
516
517 public SocksBasicAuthentication() {
518 super(proxyAddress, proxyUser, proxyPassword);
519 }
520
521 @Override
522 public void process() throws Exception {
523
524
525 done = true;
526 if (params.getByte() != SOCKS_BASIC_PROTOCOL_VERSION
527 || params.getByte() != SOCKS_BASIC_AUTH_SUCCESS) {
528 throw new IOException(format(
529 SshdText.get().proxySocksAuthenticationFailed, proxy));
530 }
531 }
532
533 @Override
534 protected void askCredentials() {
535 super.askCredentials();
536 adjustTimeout();
537 }
538
539 @Override
540 public Buffer getToken() throws IOException {
541 if (done) {
542 return null;
543 }
544 try {
545 byte[] rawUser = user.getBytes(UTF_8);
546 if (rawUser.length > 255) {
547 throw new IOException(format(
548 SshdText.get().proxySocksUsernameTooLong, proxy,
549 Integer.toString(rawUser.length), user));
550 }
551
552 if (password.length > 255) {
553 throw new IOException(
554 format(SshdText.get().proxySocksPasswordTooLong,
555 proxy, Integer.toString(password.length)));
556 }
557 ByteArrayBuffer buffer = new ByteArrayBuffer(
558 3 + rawUser.length + password.length, false);
559 buffer.putByte(SOCKS_BASIC_PROTOCOL_VERSION);
560 buffer.putByte((byte) rawUser.length);
561 buffer.putRawBytes(rawUser);
562 buffer.putByte((byte) password.length);
563 buffer.putRawBytes(password);
564 return buffer;
565 } finally {
566 clearPassword();
567 done = true;
568 }
569 }
570 }
571
572
573
574
575 private class SocksGssApiAuthentication
576 extends GssApiAuthentication<Buffer, Buffer> {
577
578 private static final byte SOCKS5_GSSAPI_VERSION = 1;
579
580 private static final byte SOCKS5_GSSAPI_TOKEN = 1;
581
582 private static final int SOCKS5_GSSAPI_FAILURE = 0xFF;
583
584 public SocksGssApiAuthentication() {
585 super(proxyAddress);
586 }
587
588 @Override
589 protected GSSContext createContext() throws Exception {
590 return context;
591 }
592
593 @Override
594 public Buffer getToken() throws Exception {
595 if (token == null) {
596 return null;
597 }
598 Buffer buffer = new ByteArrayBuffer(4 + token.length, false);
599 buffer.putByte(SOCKS5_GSSAPI_VERSION);
600 buffer.putByte(SOCKS5_GSSAPI_TOKEN);
601 buffer.putByte((byte) ((token.length >> 8) & 0xFF));
602 buffer.putByte((byte) (token.length & 0xFF));
603 buffer.putRawBytes(token);
604 return buffer;
605 }
606
607 @Override
608 protected byte[] extractToken(Buffer input) throws Exception {
609 if (context == null) {
610 return null;
611 }
612 int version = input.getUByte();
613 if (version != SOCKS5_GSSAPI_VERSION) {
614 throw new IOException(
615 format(SshdText.get().proxySocksGssApiVersionMismatch,
616 remoteAddress, Integer.toString(version)));
617 }
618 int msgType = input.getUByte();
619 if (msgType == SOCKS5_GSSAPI_FAILURE) {
620 throw new IOException(format(
621 SshdText.get().proxySocksGssApiFailure, remoteAddress));
622 } else if (msgType != SOCKS5_GSSAPI_TOKEN) {
623 throw new IOException(format(
624 SshdText.get().proxySocksGssApiUnknownMessage,
625 remoteAddress, Integer.toHexString(msgType & 0xFF)));
626 }
627 if (input.available() >= 2) {
628 int length = (input.getUByte() << 8) + input.getUByte();
629 if (input.available() >= length) {
630 byte[] value = new byte[length];
631 if (length > 0) {
632 input.getRawBytes(value);
633 }
634 return value;
635 }
636 }
637 throw new IOException(
638 format(SshdText.get().proxySocksGssApiMessageTooShort,
639 remoteAddress));
640 }
641 }
642 }