Skip to content

Commit eaf165b

Browse files
committed
Added proxy support (#172)
1 parent 6b1622c commit eaf165b

5 files changed

Lines changed: 153 additions & 24 deletions

File tree

core/src/main/java/xyz/gianlu/librespot/AbsConfiguration.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,15 @@
66
import xyz.gianlu.librespot.api.ApiConfiguration;
77
import xyz.gianlu.librespot.cache.CacheManager;
88
import xyz.gianlu.librespot.core.AuthConfiguration;
9+
import xyz.gianlu.librespot.core.Session;
910
import xyz.gianlu.librespot.core.TimeProvider;
1011
import xyz.gianlu.librespot.core.ZeroconfServer;
1112
import xyz.gianlu.librespot.player.Player;
1213

1314
/**
1415
* @author Gianlu
1516
*/
16-
public abstract class AbsConfiguration implements ApiConfiguration, TimeProvider.Configuration, Player.Configuration, CacheManager.Configuration, AuthConfiguration, ZeroconfServer.Configuration {
17-
18-
public abstract int getCustomOptionInt(@NotNull String key, int fallback);
17+
public abstract class AbsConfiguration implements ApiConfiguration, Session.ProxyConfiguration, TimeProvider.Configuration, Player.Configuration, CacheManager.Configuration, AuthConfiguration, ZeroconfServer.Configuration {
1918

2019
@Nullable
2120
public abstract String deviceName();

core/src/main/java/xyz/gianlu/librespot/FileConfiguration.java

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import java.io.FileReader;
2727
import java.io.IOException;
2828
import java.io.InputStream;
29+
import java.net.Proxy;
2930
import java.util.ArrayList;
3031
import java.util.Map;
3132
import java.util.Objects;
@@ -295,12 +296,6 @@ public boolean stopPlaybackOnChunkError() {
295296
return config.get("player.stopPlaybackOnChunkError");
296297
}
297298

298-
@Override
299-
public int getCustomOptionInt(@NotNull String key, int fallback) {
300-
Integer val = config.get(key);
301-
return val == null ? fallback : val;
302-
}
303-
304299
@Override
305300
public @Nullable String deviceName() {
306301
return config.get("deviceName");
@@ -379,6 +374,41 @@ public int apiPort() {
379374
return config.get("api.host");
380375
}
381376

377+
@Override
378+
public boolean proxyEnabled() {
379+
return config.get("proxy.enabled");
380+
}
381+
382+
@Override
383+
public @NotNull Proxy.Type proxyType() {
384+
return config.getEnum("proxy.type", Proxy.Type.class);
385+
}
386+
387+
@Override
388+
public @NotNull String proxyAddress() {
389+
return config.get("proxy.address");
390+
}
391+
392+
@Override
393+
public int proxyPort() {
394+
return config.get("proxy.port");
395+
}
396+
397+
@Override
398+
public boolean proxyAuth() {
399+
return config.get("proxy.auth");
400+
}
401+
402+
@Override
403+
public @NotNull String proxyUsername() {
404+
return config.get("proxy.username");
405+
}
406+
407+
@Override
408+
public @NotNull String proxyPassword() {
409+
return config.get("proxy.password");
410+
}
411+
382412
private final static class PropertiesFormat implements ConfigFormat<Config> {
383413
@Override
384414
public ConfigWriter createWriter() {

core/src/main/java/xyz/gianlu/librespot/core/ApResolver.java

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
import java.io.IOException;
1111
import java.io.InputStreamReader;
1212
import java.net.HttpURLConnection;
13-
import java.net.Socket;
1413
import java.net.URL;
1514
import java.util.ArrayList;
1615
import java.util.HashMap;
@@ -104,9 +103,7 @@ public static String getRandomSpclient() {
104103
}
105104

106105
@NotNull
107-
public static Socket getSocketFromRandomAccessPoint() throws IOException {
108-
String ap = getRandomOf("accesspoint");
109-
int colon = ap.indexOf(':');
110-
return new Socket(ap.substring(0, colon), Integer.parseInt(ap.substring(colon + 1)));
106+
public static String getRandomAccesspoint() {
107+
return getRandomOf("accesspoint");
111108
}
112109
}

core/src/main/java/xyz/gianlu/librespot/core/Session.java

Lines changed: 102 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import com.spotify.Authentication;
55
import com.spotify.Keyexchange;
66
import com.spotify.connectstate.Connect;
7-
import okhttp3.OkHttpClient;
7+
import okhttp3.*;
88
import org.apache.log4j.Logger;
99
import org.jetbrains.annotations.NotNull;
1010
import org.jetbrains.annotations.Nullable;
@@ -31,6 +31,8 @@
3131
import javax.crypto.spec.SecretKeySpec;
3232
import java.io.*;
3333
import java.math.BigInteger;
34+
import java.net.InetSocketAddress;
35+
import java.net.Proxy;
3436
import java.net.Socket;
3537
import java.net.SocketTimeoutException;
3638
import 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 {

core/src/main/resources/default.toml

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,15 @@ releaseLineDelay = 20 # Release mixer line after set delay (in seconds)
3939
pipe = "" # Output raw (signed) PCM to this file (`player.output` must be PIPE)
4040
stopPlaybackOnChunkError = false # Whether the playback should be stopped when the current chunk cannot be downloaded
4141

42-
[api]
42+
[api] ### API ###
4343
port = 24879 # API port (`api` module only)
44-
host = "0.0.0.0" # API listen interface (`api` module only)
44+
host = "0.0.0.0" # API listen interface (`api` module only)
45+
46+
[proxy] ### Proxy ###
47+
enabled = false # Whether the proxy is enabled
48+
type = "HTTP" # The proxy type (HTTP, SOCKS)
49+
address = "" # The proxy hostname
50+
port = 0 # The proxy port
51+
auth = false # Whether authentication is enabled on the server
52+
username = "" # Basic auth username
53+
password = "" # Basic auth password

0 commit comments

Comments
 (0)