11#if ! NOSOCKET
22using System ;
33using System . Collections . Generic ;
4+ using System . Linq ;
45using System . Net ;
56using System . Net . Sockets ;
67using System . Text ;
@@ -11,7 +12,16 @@ namespace NBitcoin.Socks
1112{
1213 public class SocksHelper
1314 {
14- static readonly byte [ ] SelectionMessage = new byte [ ] { 5 , 1 , 0 } ;
15+ // https://gitweb.torproject.org/torspec.git/tree/socks-extensions.txt
16+ // The "NO AUTHENTICATION REQUIRED" (SOCKS5) authentication method[00] is
17+ // supported; and as of Tor 0.2.3.2-alpha, the "USERNAME/PASSWORD" (SOCKS5)
18+ // authentication method[02] is supported too, and used as a method to
19+ // implement stream isolation.As an extension to support some broken clients,
20+ // we allow clients to pass "USERNAME/PASSWORD" authentication to us even if
21+ // no authentication was selected.
22+ static readonly byte [ ] SelectionMessageNoAuthenticationRequired = new byte [ ] { 5 , 1 , 0 } ;
23+ static readonly byte [ ] SelectionMessageUsernamePassword = new byte [ ] { 5 , 1 , 2 } ;
24+
1525 internal static byte [ ] CreateConnectMessage ( EndPoint endpoint )
1626 {
1727 if ( endpoint == null )
@@ -66,17 +76,57 @@ internal static byte[] CreateConnectMessage(EndPoint endpoint)
6676 return sendBuffer ;
6777 }
6878
69- public static async Task Handshake ( Socket socket , EndPoint endpoint , CancellationToken cancellationToken )
79+ public static async Task Handshake ( Socket socket , EndPoint endpoint , bool changeTorIdentities , CancellationToken cancellationToken )
7080 {
7181 NetworkStream stream = new NetworkStream ( socket , false ) ;
72- await stream . WriteAsync ( SelectionMessage , 0 , SelectionMessage . Length ) . WithCancellation ( cancellationToken ) . ConfigureAwait ( false ) ;
82+ var selectionMessage = changeTorIdentities ? SelectionMessageUsernamePassword : SelectionMessageNoAuthenticationRequired ;
83+ await stream . WriteAsync ( selectionMessage , 0 , selectionMessage . Length ) . WithCancellation ( cancellationToken ) . ConfigureAwait ( false ) ;
7384 await stream . FlushAsync ( ) . WithCancellation ( cancellationToken ) . ConfigureAwait ( false ) ;
7485
7586 var selectionResponse = new byte [ 2 ] ;
7687 await stream . ReadAsync ( selectionResponse , 0 , 2 ) . WithCancellation ( cancellationToken ) . ConfigureAwait ( false ) ;
7788 if ( selectionResponse [ 0 ] != 5 )
7889 throw new SocksException ( "Invalid version in selection reply" ) ;
79- if ( selectionResponse [ 1 ] != 0 )
90+ if ( selectionResponse [ 1 ] == 2 )
91+ {
92+ // https://tools.ietf.org/html/rfc1929#section-2
93+ // Once the SOCKS V5 server has started, and the client has selected the
94+ // Username / Password Authentication protocol, the Username / Password
95+ // subnegotiation begins. This begins with the client producing a
96+ // Username / Password request:
97+
98+ // Create a random string 21 char string.
99+ const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" ;
100+ var identity = new string ( Enumerable . Repeat ( chars , 21 )
101+ . Select ( s => s [ new Random ( ) . Next ( s . Length ) ] ) . ToArray ( ) ) ;
102+
103+ var username = identity ;
104+ var password = identity ;
105+ var uName = Encoding . UTF8 . GetBytes ( username ) ;
106+ var passwd = Encoding . UTF8 . GetBytes ( password ) ;
107+ var usernamePasswordRequest = ByteArrayExtensions . Concat ( new byte [ ] { 1 , ( byte ) uName . Length } , uName , new byte [ ] { ( byte ) passwd . Length } , passwd ) ;
108+
109+ await stream . WriteAsync ( usernamePasswordRequest , 0 , usernamePasswordRequest . Length ) . WithCancellation ( cancellationToken ) . ConfigureAwait ( false ) ;
110+ await stream . FlushAsync ( ) . WithCancellation ( cancellationToken ) . ConfigureAwait ( false ) ;
111+
112+ var userNamePasswordResponse = new byte [ 2 ] ;
113+ await stream . ReadAsync ( userNamePasswordResponse , 0 , 2 ) . WithCancellation ( cancellationToken ) . ConfigureAwait ( false ) ;
114+
115+ if ( userNamePasswordResponse [ 0 ] != 1 )
116+ {
117+ throw new NotSupportedException ( $ "Authentication version { userNamePasswordResponse [ 0 ] } is not supported. Only version { 1 } is supported.") ;
118+ }
119+
120+ if ( userNamePasswordResponse [ 1 ] != 0 ) // Tor authentication is different, this will never happen;
121+ {
122+ // https://tools.ietf.org/html/rfc1929#section-2
123+ // A STATUS field of X'00' indicates success. If the server returns a
124+ // `failure' (STATUS value other than X'00') status, it MUST close the
125+ // connection.
126+ throw new SocksException ( "Wrong username and/or password." ) ;
127+ }
128+ }
129+ else if ( selectionResponse [ 1 ] != 0 )
80130 throw new SocksException ( "Unsupported authentication method in selection reply" ) ;
81131
82132 var connectBytes = CreateConnectMessage ( endpoint ) ;
0 commit comments