44import com .spotify .Authentication ;
55import com .spotify .Keyexchange ;
66import com .spotify .connectstate .Connect ;
7- import okhttp3 .OkHttpClient ;
7+ import okhttp3 .* ;
88import org .apache .log4j .Logger ;
99import org .jetbrains .annotations .NotNull ;
1010import org .jetbrains .annotations .Nullable ;
3131import javax .crypto .spec .SecretKeySpec ;
3232import java .io .*;
3333import java .math .BigInteger ;
34+ import java .net .InetSocketAddress ;
35+ import java .net .Proxy ;
3436import java .net .Socket ;
3537import java .net .SocketTimeoutException ;
3638import java .nio .ByteBuffer ;
@@ -74,7 +76,7 @@ public final class Session implements Closeable {
7476 private final ScheduledExecutorService scheduler = Executors .newSingleThreadScheduledExecutor (new NameThreadFactory (r -> "session-scheduler-" + r .hashCode ()));
7577 private final ExecutorService executorService = Executors .newCachedThreadPool (new NameThreadFactory (r -> "handle-packet-" + r .hashCode ()));
7678 private final AtomicBoolean authLock = new AtomicBoolean (false );
77- private final OkHttpClient client = new OkHttpClient . Builder (). retryOnConnectionFailure ( true ). build () ;
79+ private final OkHttpClient client ;
7880 private final List <CloseListener > closeListeners = Collections .synchronizedList (new ArrayList <>());
7981 private ConnectionHolder conn ;
8082 private CipherPair cipherPair ;
@@ -95,12 +97,39 @@ public final class Session implements Closeable {
9597 private volatile boolean closed = false ;
9698 private volatile ScheduledFuture <?> scheduledReconnect = null ;
9799
98- private Session (Inner inner , Socket socket ) throws IOException {
100+ private Session (Inner inner , String addr ) throws IOException {
99101 this .inner = inner ;
100102 this .keys = new DiffieHellman (inner .random );
101- this .conn = new ConnectionHolder (socket );
103+ this .conn = ConnectionHolder .create (addr , inner .configuration );
104+ this .client = createClient (inner .configuration );
102105
103- LOGGER .info (String .format ("Created new session! {deviceId: %s, ap: %s} " , inner .deviceId , socket .getInetAddress ()));
106+ LOGGER .info (String .format ("Created new session! {deviceId: %s, ap: %s, proxy: %b} " , inner .deviceId , addr , inner .configuration .proxyEnabled ()));
107+ }
108+
109+ @ NotNull
110+ private static OkHttpClient createClient (@ NotNull ProxyConfiguration conf ) {
111+ OkHttpClient .Builder builder = new OkHttpClient .Builder ();
112+ builder .retryOnConnectionFailure (true );
113+
114+ if (conf .proxyEnabled () && conf .proxyType () != Proxy .Type .DIRECT ) {
115+ builder .proxy (new Proxy (conf .proxyType (), new InetSocketAddress (conf .proxyAddress (), conf .proxyPort ())));
116+ if (conf .proxyAuth ()) {
117+ builder .proxyAuthenticator (new Authenticator () {
118+ final String username = conf .proxyUsername ();
119+ final String password = conf .proxyPassword ();
120+
121+ @ Override
122+ public Request authenticate (Route route , @ NotNull Response response ) {
123+ String credential = Credentials .basic (username , password );
124+ return response .request ().newBuilder ()
125+ .header ("Proxy-Authorization" , credential )
126+ .build ();
127+ }
128+ });
129+ }
130+ }
131+
132+ return builder .build ();
104133 }
105134
106135 private static int readBlobInt (ByteBuffer buffer ) {
@@ -114,7 +143,7 @@ private static int readBlobInt(ByteBuffer buffer) {
114143 static Session from (@ NotNull Inner inner ) throws IOException {
115144 ApResolver .fillPool ();
116145 TimeProvider .init (inner .configuration );
117- return new Session (inner , ApResolver .getSocketFromRandomAccessPoint ());
146+ return new Session (inner , ApResolver .getRandomAccesspoint ());
118147 }
119148
120149 @ NotNull
@@ -533,7 +562,7 @@ private void reconnect() {
533562 receiver .stop ();
534563 }
535564
536- conn = new ConnectionHolder (ApResolver .getSocketFromRandomAccessPoint ());
565+ conn = ConnectionHolder . create (ApResolver .getRandomAccesspoint (), conf ());
537566 connect ();
538567 authenticatePartial (Authentication .LoginCredentials .newBuilder ()
539568 .setUsername (apWelcome .getCanonicalUsername ())
@@ -562,6 +591,26 @@ public void addCloseListener(@NotNull CloseListener listener) {
562591 if (!closeListeners .contains (listener )) closeListeners .add (listener );
563592 }
564593
594+ public interface ProxyConfiguration {
595+ boolean proxyEnabled ();
596+
597+ @ NotNull
598+ Proxy .Type proxyType ();
599+
600+ @ NotNull
601+ String proxyAddress ();
602+
603+ int proxyPort ();
604+
605+ boolean proxyAuth ();
606+
607+ @ NotNull
608+ String proxyUsername ();
609+
610+ @ NotNull
611+ String proxyPassword ();
612+ }
613+
565614 public interface CloseListener {
566615 void onClosed ();
567616 }
@@ -776,11 +825,56 @@ private static class ConnectionHolder {
776825 final DataInputStream in ;
777826 final DataOutputStream out ;
778827
779- ConnectionHolder (Socket socket ) throws IOException {
828+ private ConnectionHolder (@ NotNull Socket socket ) throws IOException {
780829 this .socket = socket ;
781830 this .in = new DataInputStream (socket .getInputStream ());
782831 this .out = new DataOutputStream (socket .getOutputStream ());
783832 }
833+
834+ @ NotNull
835+ static ConnectionHolder create (@ NotNull String addr , @ NotNull ProxyConfiguration conf ) throws IOException {
836+ int colon = addr .indexOf (':' );
837+ String apAddr = addr .substring (0 , colon );
838+ int apPort = Integer .parseInt (addr .substring (colon + 1 ));
839+ if (!conf .proxyEnabled () || conf .proxyType () == Proxy .Type .DIRECT )
840+ return new ConnectionHolder (new Socket (apAddr , apPort ));
841+
842+ switch (conf .proxyType ()) {
843+ case HTTP :
844+ Socket sock = new Socket (conf .proxyAddress (), conf .proxyPort ());
845+ OutputStream out = sock .getOutputStream ();
846+ DataInputStream in = new DataInputStream (sock .getInputStream ());
847+
848+ out .write (String .format ("CONNECT %s:%d HTTP/1.0\n " , apAddr , apPort ).getBytes ());
849+ if (conf .proxyAuth ()) {
850+ out .write ("Proxy-Authorization: Basic " .getBytes ());
851+ out .write (Base64 .getEncoder ().encodeToString (String .format ("%s:%s\n " , conf .proxyUsername (), conf .proxyPassword ()).getBytes ()).getBytes ());
852+ }
853+
854+ out .write ('\n' );
855+ out .flush ();
856+
857+ String sl = Utils .readLine (in );
858+ if (!sl .contains ("200" )) throw new IOException ("Failed connecting: " + sl );
859+
860+ //noinspection StatementWithEmptyBody
861+ while (!Utils .readLine (in ).isEmpty ()) {
862+ // Read all headers
863+ }
864+
865+ LOGGER .info ("Successfully connected to the HTTP proxy." );
866+ return new ConnectionHolder (sock );
867+ case SOCKS :
868+ Proxy proxy = new Proxy (conf .proxyType (), new InetSocketAddress (conf .proxyAddress (), conf .proxyPort ()));
869+ Socket proxySocket = new Socket (proxy );
870+ proxySocket .connect (new InetSocketAddress (apAddr , apPort ));
871+ LOGGER .info ("Successfully connected to the SOCKS proxy." );
872+ return new ConnectionHolder (proxySocket );
873+ case DIRECT :
874+ default :
875+ throw new UnsupportedOperationException ();
876+ }
877+ }
784878 }
785879
786880 private class Receiver implements Runnable {
0 commit comments