View Javadoc
1   /*
2    * Copyright 2014 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  
17  package io.netty.handler.proxy;
18  
19  import io.netty.channel.ChannelHandlerContext;
20  import io.netty.channel.ChannelPipeline;
21  import io.netty.handler.codec.socksx.v5.DefaultSocks5InitialRequest;
22  import io.netty.handler.codec.socksx.v5.DefaultSocks5CommandRequest;
23  import io.netty.handler.codec.socksx.v5.DefaultSocks5PasswordAuthRequest;
24  import io.netty.handler.codec.socksx.v5.DefaultSocks5PrivateAuthRequest;
25  import io.netty.handler.codec.socksx.v5.Socks5AddressType;
26  import io.netty.handler.codec.socksx.v5.Socks5AuthMethod;
27  import io.netty.handler.codec.socksx.v5.Socks5InitialRequest;
28  import io.netty.handler.codec.socksx.v5.Socks5InitialResponse;
29  import io.netty.handler.codec.socksx.v5.Socks5InitialResponseDecoder;
30  import io.netty.handler.codec.socksx.v5.Socks5ClientEncoder;
31  import io.netty.handler.codec.socksx.v5.Socks5CommandResponse;
32  import io.netty.handler.codec.socksx.v5.Socks5CommandResponseDecoder;
33  import io.netty.handler.codec.socksx.v5.Socks5CommandStatus;
34  import io.netty.handler.codec.socksx.v5.Socks5CommandType;
35  import io.netty.handler.codec.socksx.v5.Socks5PasswordAuthResponse;
36  import io.netty.handler.codec.socksx.v5.Socks5PasswordAuthResponseDecoder;
37  import io.netty.handler.codec.socksx.v5.Socks5PasswordAuthStatus;
38  import io.netty.handler.codec.socksx.v5.Socks5PrivateAuthResponse;
39  import io.netty.handler.codec.socksx.v5.Socks5PrivateAuthResponseDecoder;
40  import io.netty.handler.codec.socksx.v5.Socks5PrivateAuthStatus;
41  import io.netty.util.NetUtil;
42  import io.netty.util.internal.StringUtil;
43  
44  import java.net.InetSocketAddress;
45  import java.net.SocketAddress;
46  import java.util.Arrays;
47  import java.util.Collections;
48  
49  /**
50   * Handler that establishes a blind forwarding proxy tunnel using
51   * <a href="https://www.rfc-editor.org/rfc/rfc1928">SOCKS Protocol Version 5</a>.
52   */
53  public final class Socks5ProxyHandler extends ProxyHandler {
54  
55      private static final String PROTOCOL = "socks5";
56      private static final String AUTH_PASSWORD = "password";
57      private static final String AUTH_PRIVATE = "private";
58  
59      private static final byte NO_PRIVATE_AUTH_METHOD =
60          Socks5AuthMethod.NO_AUTH.byteValue();
61  
62      private static final Socks5InitialRequest INIT_REQUEST_NO_AUTH =
63              new DefaultSocks5InitialRequest(Collections.singletonList(Socks5AuthMethod.NO_AUTH));
64  
65      private static final Socks5InitialRequest INIT_REQUEST_PASSWORD =
66              new DefaultSocks5InitialRequest(Arrays.asList(Socks5AuthMethod.NO_AUTH, Socks5AuthMethod.PASSWORD));
67  
68      private final String username;
69      private final String password;
70      private final byte privateAuthMethod;
71      private final byte[] privateToken;
72      private final Socks5ClientEncoder clientEncoder;
73  
74      private String decoderName;
75      private String encoderName;
76  
77      public Socks5ProxyHandler(SocketAddress proxyAddress) {
78          this(proxyAddress, null, null);
79      }
80  
81      public Socks5ProxyHandler(SocketAddress proxyAddress, String username, String password) {
82          super(proxyAddress);
83          if (username != null && username.isEmpty()) {
84              username = null;
85          }
86          if (password != null && password.isEmpty()) {
87              password = null;
88          }
89          this.username = username;
90          this.password = password;
91          this.privateToken = null;
92          this.privateAuthMethod = NO_PRIVATE_AUTH_METHOD; // No private authentication method specified
93          this.clientEncoder = Socks5ClientEncoder.DEFAULT;
94      }
95  
96      /**
97       * Creates a new SOCKS5 proxy handler with a custom private authentication method.
98       *
99       * @param proxyAddress     The address of the SOCKS5 proxy server
100      * @param privateAuthMethod The private authentication method code (must be in range 0x80-0xFE)
101      * @param privateToken     The token to use for private authentication
102      * @param customEncoder    The custom encoder to use for encoding SOCKS5 messages, if {@code null} the
103      *                         {@link Socks5ClientEncoder#DEFAULT} will be used
104      * @throws IllegalArgumentException If privateAuthMethod is not in the valid range
105      */
106     public Socks5ProxyHandler(SocketAddress proxyAddress, byte privateAuthMethod, byte[] privateToken,
107                               Socks5ClientEncoder customEncoder) {
108         super(proxyAddress);
109         if (!Socks5AuthMethod.isPrivateMethod(privateAuthMethod)) {
110             throw new IllegalArgumentException(
111                     "privateAuthMethod: " + (privateAuthMethod & 0xFF) + " (expected: 0x80-0xFE)");
112         }
113         this.username = this.password = null;
114         this.privateToken = privateToken;
115         this.privateAuthMethod = privateAuthMethod;
116         this.clientEncoder = customEncoder != null ? customEncoder : Socks5ClientEncoder.DEFAULT;
117     }
118 
119     @Override
120     public String protocol() {
121         return PROTOCOL;
122     }
123 
124     @Override
125     public String authScheme() {
126         Socks5AuthMethod authMethod = socksAuthMethod();
127         if (Socks5AuthMethod.isPrivateMethod(authMethod.byteValue())) {
128             return AUTH_PRIVATE;
129         }
130         if (authMethod == Socks5AuthMethod.PASSWORD) {
131             return AUTH_PASSWORD;
132         }
133         return AUTH_NONE;
134     }
135 
136     public String username() {
137         return username;
138     }
139 
140     public String password() {
141         return password;
142     }
143 
144     @Override
145     protected void addCodec(ChannelHandlerContext ctx) throws Exception {
146         ChannelPipeline p = ctx.pipeline();
147         String name = ctx.name();
148 
149         Socks5InitialResponseDecoder decoder = new Socks5InitialResponseDecoder();
150         p.addBefore(name, null, decoder);
151 
152         decoderName = p.context(decoder).name();
153         encoderName = decoderName + ".encoder";
154 
155         p.addBefore(name, encoderName, clientEncoder);
156     }
157 
158     @Override
159     protected void removeEncoder(ChannelHandlerContext ctx) throws Exception {
160         ctx.pipeline().remove(encoderName);
161     }
162 
163     @Override
164     protected void removeDecoder(ChannelHandlerContext ctx) throws Exception {
165         ChannelPipeline p = ctx.pipeline();
166         if (p.context(decoderName) != null) {
167             p.remove(decoderName);
168         }
169     }
170 
171     @Override
172     protected Object newInitialMessage(ChannelHandlerContext ctx) throws Exception {
173         Socks5AuthMethod authMethod = socksAuthMethod();
174         if (authMethod == Socks5AuthMethod.PASSWORD) {
175             return INIT_REQUEST_PASSWORD;
176         }
177         if (Socks5AuthMethod.isPrivateMethod(authMethod.byteValue())) {
178             return new DefaultSocks5InitialRequest(Arrays.asList(Socks5AuthMethod.NO_AUTH,
179                 authMethod));
180         }
181         return INIT_REQUEST_NO_AUTH;
182     }
183 
184     @Override
185     protected boolean handleResponse(ChannelHandlerContext ctx, Object response) throws Exception {
186         if (response instanceof Socks5InitialResponse) {
187             Socks5InitialResponse res = (Socks5InitialResponse) response;
188             Socks5AuthMethod authMethod = socksAuthMethod();
189             Socks5AuthMethod resAuthMethod = res.authMethod();
190             if (resAuthMethod != Socks5AuthMethod.NO_AUTH && resAuthMethod != authMethod
191                 && !Socks5AuthMethod.isPrivateMethod(resAuthMethod.byteValue())) {
192                 // Server did not allow unauthenticated access nor accept the requested authentication scheme.
193                 throw new ProxyConnectException(exceptionMessage("unexpected authMethod: " + res.authMethod()));
194             }
195 
196             if (resAuthMethod == Socks5AuthMethod.NO_AUTH) {
197                 sendConnectCommand(ctx);
198             } else if (resAuthMethod == Socks5AuthMethod.PASSWORD) {
199                 // In case of password authentication, send an authentication request.
200                 ctx.pipeline().replace(decoderName, decoderName, new Socks5PasswordAuthResponseDecoder());
201                 sendToProxyServer(new DefaultSocks5PasswordAuthRequest(
202                         username != null? username : "", password != null? password : ""));
203             } else if (Socks5AuthMethod.isPrivateMethod(resAuthMethod.byteValue())) {
204                 ctx.pipeline().replace(decoderName, decoderName, new Socks5PrivateAuthResponseDecoder());
205                 sendToProxyServer(new DefaultSocks5PrivateAuthRequest(privateToken));
206             } else {
207                 // Should never reach here.
208                 throw new Error();
209             }
210 
211             return false;
212         }
213 
214         if (response instanceof Socks5PasswordAuthResponse) {
215             // Received an authentication response from the server.
216             Socks5PasswordAuthResponse res = (Socks5PasswordAuthResponse) response;
217             if (res.status() != Socks5PasswordAuthStatus.SUCCESS) {
218                 throw new ProxyConnectException(exceptionMessage("authStatus: " + res.status()));
219             }
220 
221             sendConnectCommand(ctx);
222             return false;
223         }
224 
225         if (response instanceof Socks5PrivateAuthResponse) {
226             Socks5PrivateAuthResponse res = (Socks5PrivateAuthResponse) response;
227             if (res.status() != Socks5PrivateAuthStatus.SUCCESS) {
228                 throw new ProxyConnectException(exceptionMessage("privateAuthStatus: " + res.status()));
229             }
230 
231             sendConnectCommand(ctx);
232             return false;
233         }
234 
235         // This should be the last message from the server.
236         Socks5CommandResponse res = (Socks5CommandResponse) response;
237         if (res.status() != Socks5CommandStatus.SUCCESS) {
238             throw new ProxyConnectException(exceptionMessage("status: " + res.status()));
239         }
240 
241         return true;
242     }
243 
244     private Socks5AuthMethod socksAuthMethod() {
245         Socks5AuthMethod authMethod;
246         if (privateToken != null && privateToken.length > 0) {
247             authMethod = new Socks5AuthMethod(privateAuthMethod & 0xFF, "PRIVATE_" + (privateAuthMethod & 0xFF));
248         } else if (username == null && password == null) {
249             authMethod = Socks5AuthMethod.NO_AUTH;
250         } else {
251             authMethod = Socks5AuthMethod.PASSWORD;
252         }
253         return authMethod;
254     }
255 
256     private void sendConnectCommand(ChannelHandlerContext ctx) throws Exception {
257         InetSocketAddress raddr = destinationAddress();
258         Socks5AddressType addrType;
259         String rhost;
260         if (raddr.isUnresolved()) {
261             addrType = Socks5AddressType.DOMAIN;
262             rhost = raddr.getHostString();
263         } else {
264             rhost = raddr.getAddress().getHostAddress();
265             if (NetUtil.isValidIpV4Address(rhost)) {
266                 addrType = Socks5AddressType.IPv4;
267             } else if (NetUtil.isValidIpV6Address(rhost)) {
268                 addrType = Socks5AddressType.IPv6;
269             } else {
270                 throw new ProxyConnectException(
271                         exceptionMessage("unknown address type: " + StringUtil.simpleClassName(rhost)));
272             }
273         }
274 
275         ctx.pipeline().replace(decoderName, decoderName, new Socks5CommandResponseDecoder());
276         sendToProxyServer(new DefaultSocks5CommandRequest(Socks5CommandType.CONNECT, addrType, rhost, raddr.getPort()));
277     }
278 }