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.Socks5AddressType;
25  import io.netty.handler.codec.socksx.v5.Socks5AuthMethod;
26  import io.netty.handler.codec.socksx.v5.Socks5InitialRequest;
27  import io.netty.handler.codec.socksx.v5.Socks5InitialResponse;
28  import io.netty.handler.codec.socksx.v5.Socks5InitialResponseDecoder;
29  import io.netty.handler.codec.socksx.v5.Socks5ClientEncoder;
30  import io.netty.handler.codec.socksx.v5.Socks5CommandResponse;
31  import io.netty.handler.codec.socksx.v5.Socks5CommandResponseDecoder;
32  import io.netty.handler.codec.socksx.v5.Socks5CommandStatus;
33  import io.netty.handler.codec.socksx.v5.Socks5CommandType;
34  import io.netty.handler.codec.socksx.v5.Socks5PasswordAuthResponse;
35  import io.netty.handler.codec.socksx.v5.Socks5PasswordAuthResponseDecoder;
36  import io.netty.handler.codec.socksx.v5.Socks5PasswordAuthStatus;
37  import io.netty.util.NetUtil;
38  import io.netty.util.internal.StringUtil;
39  
40  import java.net.InetSocketAddress;
41  import java.net.SocketAddress;
42  import java.util.Arrays;
43  import java.util.Collections;
44  
45  /**
46   * Handler that establishes a blind forwarding proxy tunnel using
47   * <a href="https://www.rfc-editor.org/rfc/rfc1928">SOCKS Protocol Version 5</a>.
48   */
49  public final class Socks5ProxyHandler extends ProxyHandler {
50  
51      private static final String PROTOCOL = "socks5";
52      private static final String AUTH_PASSWORD = "password";
53  
54      private static final Socks5InitialRequest INIT_REQUEST_NO_AUTH =
55              new DefaultSocks5InitialRequest(Collections.singletonList(Socks5AuthMethod.NO_AUTH));
56  
57      private static final Socks5InitialRequest INIT_REQUEST_PASSWORD =
58              new DefaultSocks5InitialRequest(Arrays.asList(Socks5AuthMethod.NO_AUTH, Socks5AuthMethod.PASSWORD));
59  
60      private final String username;
61      private final String password;
62  
63      private String decoderName;
64      private String encoderName;
65  
66      public Socks5ProxyHandler(SocketAddress proxyAddress) {
67          this(proxyAddress, null, null);
68      }
69  
70      public Socks5ProxyHandler(SocketAddress proxyAddress, String username, String password) {
71          super(proxyAddress);
72          if (username != null && username.isEmpty()) {
73              username = null;
74          }
75          if (password != null && password.isEmpty()) {
76              password = null;
77          }
78          this.username = username;
79          this.password = password;
80      }
81  
82      @Override
83      public String protocol() {
84          return PROTOCOL;
85      }
86  
87      @Override
88      public String authScheme() {
89          return socksAuthMethod() == Socks5AuthMethod.PASSWORD? AUTH_PASSWORD : AUTH_NONE;
90      }
91  
92      public String username() {
93          return username;
94      }
95  
96      public String password() {
97          return password;
98      }
99  
100     @Override
101     protected void addCodec(ChannelHandlerContext ctx) throws Exception {
102         ChannelPipeline p = ctx.pipeline();
103         String name = ctx.name();
104 
105         Socks5InitialResponseDecoder decoder = new Socks5InitialResponseDecoder();
106         p.addBefore(name, null, decoder);
107 
108         decoderName = p.context(decoder).name();
109         encoderName = decoderName + ".encoder";
110 
111         p.addBefore(name, encoderName, Socks5ClientEncoder.DEFAULT);
112     }
113 
114     @Override
115     protected void removeEncoder(ChannelHandlerContext ctx) throws Exception {
116         ctx.pipeline().remove(encoderName);
117     }
118 
119     @Override
120     protected void removeDecoder(ChannelHandlerContext ctx) throws Exception {
121         ChannelPipeline p = ctx.pipeline();
122         if (p.context(decoderName) != null) {
123             p.remove(decoderName);
124         }
125     }
126 
127     @Override
128     protected Object newInitialMessage(ChannelHandlerContext ctx) throws Exception {
129         return socksAuthMethod() == Socks5AuthMethod.PASSWORD? INIT_REQUEST_PASSWORD : INIT_REQUEST_NO_AUTH;
130     }
131 
132     @Override
133     protected boolean handleResponse(ChannelHandlerContext ctx, Object response) throws Exception {
134         if (response instanceof Socks5InitialResponse) {
135             Socks5InitialResponse res = (Socks5InitialResponse) response;
136             Socks5AuthMethod authMethod = socksAuthMethod();
137             Socks5AuthMethod resAuthMethod = res.authMethod();
138             if (resAuthMethod != Socks5AuthMethod.NO_AUTH && resAuthMethod != authMethod) {
139                 // Server did not allow unauthenticated access nor accept the requested authentication scheme.
140                 throw new ProxyConnectException(exceptionMessage("unexpected authMethod: " + res.authMethod()));
141             }
142 
143             if (resAuthMethod == Socks5AuthMethod.NO_AUTH) {
144                 sendConnectCommand(ctx);
145             } else if (resAuthMethod == Socks5AuthMethod.PASSWORD) {
146                 // In case of password authentication, send an authentication request.
147                 ctx.pipeline().replace(decoderName, decoderName, new Socks5PasswordAuthResponseDecoder());
148                 sendToProxyServer(new DefaultSocks5PasswordAuthRequest(
149                         username != null? username : "", password != null? password : ""));
150             } else {
151                 // Should never reach here.
152                 throw new Error();
153             }
154 
155             return false;
156         }
157 
158         if (response instanceof Socks5PasswordAuthResponse) {
159             // Received an authentication response from the server.
160             Socks5PasswordAuthResponse res = (Socks5PasswordAuthResponse) response;
161             if (res.status() != Socks5PasswordAuthStatus.SUCCESS) {
162                 throw new ProxyConnectException(exceptionMessage("authStatus: " + res.status()));
163             }
164 
165             sendConnectCommand(ctx);
166             return false;
167         }
168 
169         // This should be the last message from the server.
170         Socks5CommandResponse res = (Socks5CommandResponse) response;
171         if (res.status() != Socks5CommandStatus.SUCCESS) {
172             throw new ProxyConnectException(exceptionMessage("status: " + res.status()));
173         }
174 
175         return true;
176     }
177 
178     private Socks5AuthMethod socksAuthMethod() {
179         Socks5AuthMethod authMethod;
180         if (username == null && password == null) {
181             authMethod = Socks5AuthMethod.NO_AUTH;
182         } else {
183             authMethod = Socks5AuthMethod.PASSWORD;
184         }
185         return authMethod;
186     }
187 
188     private void sendConnectCommand(ChannelHandlerContext ctx) throws Exception {
189         InetSocketAddress raddr = destinationAddress();
190         Socks5AddressType addrType;
191         String rhost;
192         if (raddr.isUnresolved()) {
193             addrType = Socks5AddressType.DOMAIN;
194             rhost = raddr.getHostString();
195         } else {
196             rhost = raddr.getAddress().getHostAddress();
197             if (NetUtil.isValidIpV4Address(rhost)) {
198                 addrType = Socks5AddressType.IPv4;
199             } else if (NetUtil.isValidIpV6Address(rhost)) {
200                 addrType = Socks5AddressType.IPv6;
201             } else {
202                 throw new ProxyConnectException(
203                         exceptionMessage("unknown address type: " + StringUtil.simpleClassName(rhost)));
204             }
205         }
206 
207         ctx.pipeline().replace(decoderName, decoderName, new Socks5CommandResponseDecoder());
208         sendToProxyServer(new DefaultSocks5CommandRequest(Socks5CommandType.CONNECT, addrType, rhost, raddr.getPort()));
209     }
210 }