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