Skip to content

Commit cb0067c

Browse files
authored
Merge pull request #351 from funtax/dev
Centralize Base64-handling and support for Android below version 8/API24
2 parents 9156a7f + bf36a57 commit cb0067c

File tree

11 files changed

+90
-23
lines changed

11 files changed

+90
-23
lines changed

dacp/pom.xml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,15 @@
3131
<name>librespot-java DACP interface</name>
3232

3333
<dependencies>
34+
<dependency>
35+
<groupId>xyz.gianlu.librespot</groupId>
36+
<artifactId>librespot-lib</artifactId>
37+
<version>${project.version}</version>
38+
</dependency>
3439
<dependency>
3540
<groupId>org.slf4j</groupId>
3641
<artifactId>slf4j-api</artifactId>
3742
<version>${slf4j-api.version}</version>
3843
</dependency>
3944
</dependencies>
40-
</project>
45+
</project>

dacp/src/main/java/xyz/gianlu/librespot/dacp/DacpMetadataPipe.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@
2121
import org.jetbrains.annotations.Range;
2222
import org.slf4j.Logger;
2323
import org.slf4j.LoggerFactory;
24+
import xyz.gianlu.librespot.common.Utils;
2425

2526
import java.io.Closeable;
2627
import java.io.File;
2728
import java.io.FileOutputStream;
2829
import java.io.IOException;
2930
import java.nio.charset.StandardCharsets;
30-
import java.util.Base64;
3131

3232
/**
3333
* Metadata pipe implementation following the Shairport Sync format (https://github.com/mikebrady/shairport-sync-metadata-reader).
@@ -73,7 +73,7 @@ private synchronized void send(@NotNull String type, @NotNull String code, @Null
7373

7474
if (payload != null && payload.length > 0) {
7575
out.write(String.format("<item><type>%s</type><code>%s</code><length>%d</length>\n<data encoding=\"base64\">%s</data></item>\n", type, code,
76-
payload.length, new String(Base64.getEncoder().encode(payload), StandardCharsets.UTF_8)).getBytes(StandardCharsets.UTF_8));
76+
payload.length, Utils.toBase64(payload)).getBytes(StandardCharsets.UTF_8));
7777
} else {
7878
out.write(String.format("<item><type>%s</type><code>%s</code><length>0</length></item>\n", type, code).getBytes(StandardCharsets.UTF_8));
7979
}

lib/src/main/java/xyz/gianlu/librespot/ZeroconfServer.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ private ZeroconfServer(@NotNull Inner inner, int listenPort, boolean listenAllIn
164164
public static String getUsefulHostname() throws UnknownHostException {
165165
String host = InetAddress.getLocalHost().getHostName();
166166
if (Objects.equals(host, "localhost")) {
167-
host = Base64.getEncoder().encodeToString(BigInteger.valueOf(ThreadLocalRandom.current().nextLong()).toByteArray()) + ".local";
167+
host = Utils.toBase64(BigInteger.valueOf(ThreadLocalRandom.current().nextLong()).toByteArray()) + ".local";
168168
LOGGER.warn("Hostname cannot be `localhost`, temporary hostname: " + host);
169169
return host;
170170
}
@@ -239,7 +239,7 @@ private void handleGetInfo(OutputStream out, String httpVersion) throws IOExcept
239239
JsonObject info = DEFAULT_GET_INFO_FIELDS.deepCopy();
240240
info.addProperty("deviceID", inner.deviceId);
241241
info.addProperty("remoteName", inner.deviceName);
242-
info.addProperty("publicKey", Base64.getEncoder().encodeToString(keys.publicKeyArray()));
242+
info.addProperty("publicKey", Utils.toBase64(keys.publicKeyArray()));
243243
info.addProperty("deviceType", inner.deviceType.name().toUpperCase());
244244

245245
synchronized (connectionLock) {
@@ -292,8 +292,8 @@ private void handleAddUser(OutputStream out, Map<String, String> params, String
292292
}
293293
}
294294

295-
byte[] sharedKey = Utils.toByteArray(keys.computeSharedKey(Base64.getDecoder().decode(clientKeyStr)));
296-
byte[] blobBytes = Base64.getDecoder().decode(blobStr);
295+
byte[] sharedKey = Utils.toByteArray(keys.computeSharedKey(Utils.fromBase64(clientKeyStr)));
296+
byte[] blobBytes = Utils.fromBase64(blobStr);
297297
byte[] iv = Arrays.copyOfRange(blobBytes, 0, 16);
298298
byte[] encrypted = Arrays.copyOfRange(blobBytes, 16, blobBytes.length - 20);
299299
byte[] checksum = Arrays.copyOfRange(blobBytes, blobBytes.length - 20, blobBytes.length);

lib/src/main/java/xyz/gianlu/librespot/common/BytesArrayList.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020

2121
import java.io.InputStream;
2222
import java.util.Arrays;
23-
import java.util.Base64;
2423
import java.util.Iterator;
2524
import java.util.NoSuchElementException;
2625

@@ -44,7 +43,7 @@ private BytesArrayList(byte[][] buffer) {
4443
@NotNull
4544
public static InputStream streamBase64(@NotNull String[] payloads) {
4645
byte[][] decoded = new byte[payloads.length][];
47-
for (int i = 0; i < decoded.length; i++) decoded[i] = Base64.getDecoder().decode(payloads[i]);
46+
for (int i = 0; i < decoded.length; i++) decoded[i] = Utils.fromBase64(payloads[i]);
4847
return new BytesArrayList(decoded).stream();
4948
}
5049

lib/src/main/java/xyz/gianlu/librespot/common/Utils.java

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,12 @@
3131
import java.io.IOException;
3232
import java.io.InputStream;
3333
import java.lang.reflect.Field;
34+
import java.lang.reflect.InvocationTargetException;
35+
import java.lang.reflect.Method;
3436
import java.lang.reflect.Modifier;
3537
import java.math.BigInteger;
3638
import java.nio.ByteBuffer;
39+
import java.nio.charset.StandardCharsets;
3740
import java.security.Permission;
3841
import java.security.PermissionCollection;
3942
import java.util.*;
@@ -45,6 +48,8 @@ public final class Utils {
4548
private final static char[] hexArray = "0123456789ABCDEF".toCharArray();
4649
private static final Logger LOGGER = LoggerFactory.getLogger(Utils.class);
4750
private static final String randomString = "abcdefghijklmnopqrstuvwxyz0123456789";
51+
private static final String JAVA_UTIL_BASE_64 = "java.util.Base64";
52+
private static final String ANDROID_UTIL_BASE_64 = "android.util.Base64";
4853

4954
private Utils() {
5055
}
@@ -310,12 +315,70 @@ public static String artistsToString(List<Metadata.Artist> artists) {
310315
}
311316

312317
@NotNull
313-
public static String toBase64(@NotNull ByteString bytes) {
314-
return Base64.getEncoder().encodeToString(bytes.toByteArray());
318+
public static String toBase64(@NotNull byte[] bytes, boolean padding) {
319+
byte[] encodedBytes;
320+
try {
321+
Class<?> clazz = Class.forName(JAVA_UTIL_BASE_64);
322+
final Method getEncoder = clazz.getDeclaredMethod("getEncoder");
323+
Class<?> encoderClazz = Class.forName("java.util.Base64$Encoder");
324+
Object encoder = getEncoder.invoke(null);
325+
final Method withoutPadding = encoderClazz.getDeclaredMethod("withoutPadding");
326+
if (!padding)
327+
encoder = withoutPadding.invoke(encoder);
328+
final Method encode = encoderClazz.getDeclaredMethod("encode", byte[].class);
329+
encodedBytes = (byte[]) encode.invoke(encoder, bytes);
330+
} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException ignored) {
331+
try {
332+
Class<?> clazz = Class.forName(ANDROID_UTIL_BASE_64);
333+
final Method encode = clazz.getDeclaredMethod("encode", byte[].class, int.class);
334+
int flags = 2; // Base64.NO_WRAP
335+
if (!padding)
336+
flags |= 1; // Base64.NO_PADDING
337+
encodedBytes = (byte[]) encode.invoke(null, bytes, flags); // Base64.NO_WRAP | Base64.NO_PADDING
338+
} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException ignored2) {
339+
throw new NoClassDefFoundError("Base64 not available");
340+
}
341+
}
342+
343+
return new String(encodedBytes, StandardCharsets.UTF_8);
315344
}
316345

317346
@NotNull
318-
public static ByteString fromBase64(@NotNull String str) {
319-
return ByteString.copyFrom(Base64.getDecoder().decode(str.getBytes()));
347+
public static String toBase64NoPadding(@NotNull byte[] bytes) {
348+
return toBase64(bytes, false);
349+
}
350+
351+
@NotNull
352+
public static String toBase64(@NotNull byte[] bytes) {
353+
return toBase64(bytes, true);
354+
}
355+
356+
@NotNull
357+
public static byte[] fromBase64(@NotNull String str) {
358+
return fromBase64(str.getBytes());
359+
}
360+
361+
@NotNull
362+
public static byte[] fromBase64(@NotNull byte[] bytes) {
363+
byte[] decodedBytes;
364+
try {
365+
Class<?> clazz = Class.forName(JAVA_UTIL_BASE_64);
366+
final Method getDecoder = clazz.getDeclaredMethod("getDecoder");
367+
final Object decoder = getDecoder.invoke(null);
368+
Class<?> decoderClazz = Class.forName("java.util.Base64$Decoder");
369+
final Method decode = decoderClazz.getDeclaredMethod("decode", byte[].class);
370+
decodedBytes = (byte[]) decode.invoke(decoder, bytes);
371+
} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException ignored) {
372+
try {
373+
Class<?> clazz = Class.forName(ANDROID_UTIL_BASE_64);
374+
final Method decode = clazz.getDeclaredMethod("decode", byte[].class, int.class);
375+
int flags = 0; // android.util.Base64.DEFAULT
376+
decodedBytes = (byte[]) decode.invoke(null, bytes, flags);
377+
} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException ignored2) {
378+
throw new NoClassDefFoundError("Base64 not available");
379+
}
380+
}
381+
382+
return decodedBytes;
320383
}
321384
}

lib/src/main/java/xyz/gianlu/librespot/core/FacebookAuthenticator.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
import java.net.MalformedURLException;
3434
import java.net.Socket;
3535
import java.net.URL;
36-
import java.util.Base64;
3736

3837
/**
3938
* @author Gianlu
@@ -99,7 +98,7 @@ private void authData(@NotNull String json) {
9998
credentials = Authentication.LoginCredentials.newBuilder()
10099
.setUsername(data.get("username").getAsString())
101100
.setTyp(Authentication.AuthenticationType.forNumber(data.get("auth_type").getAsInt()))
102-
.setAuthData(ByteString.copyFrom(Base64.getDecoder().decode(data.get("encoded_auth_blob").getAsString())))
101+
.setAuthData(ByteString.copyFrom(Utils.fromBase64(data.get("encoded_auth_blob").getAsString())))
103102
.build();
104103

105104
synchronized (credentialsLock) {

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,7 @@ private void authenticatePartial(@NotNull Authentication.LoginCredentials creden
432432

433433
JsonObject obj = new JsonObject();
434434
obj.addProperty("username", apWelcome.getCanonicalUsername());
435-
obj.addProperty("credentials", Utils.toBase64(reusable));
435+
obj.addProperty("credentials", Utils.toBase64(reusable.toByteArray()));
436436
obj.addProperty("type", reusableType.name());
437437

438438
if (inner.conf.storedCredentialsFile == null) throw new IllegalArgumentException();
@@ -883,7 +883,7 @@ public Builder() {
883883
}
884884

885885
private static @NotNull Authentication.LoginCredentials decryptBlob(@NotNull String deviceId, @NotNull String username, byte[] encryptedBlob) throws GeneralSecurityException, IOException {
886-
encryptedBlob = Base64.getDecoder().decode(encryptedBlob);
886+
encryptedBlob = Utils.fromBase64(encryptedBlob);
887887

888888
byte[] secret = MessageDigest.getInstance("SHA-1").digest(deviceId.getBytes());
889889
byte[] baseKey = PBKDF2.HmacSHA1(secret, username.getBytes(), 0x100, 20);
@@ -954,7 +954,7 @@ public Builder stored(@NotNull File storedCredentials) throws IOException {
954954
loginCredentials = Authentication.LoginCredentials.newBuilder()
955955
.setTyp(Authentication.AuthenticationType.valueOf(obj.get("type").getAsString()))
956956
.setUsername(obj.get("username").getAsString())
957-
.setAuthData(Utils.fromBase64(obj.get("credentials").getAsString()))
957+
.setAuthData(ByteString.copyFrom(Utils.fromBase64(obj.get("credentials").getAsString())))
958958
.build();
959959
}
960960

lib/src/main/java/xyz/gianlu/librespot/dealer/DealerClient.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import xyz.gianlu.librespot.common.AsyncWorker;
3131
import xyz.gianlu.librespot.common.BytesArrayList;
3232
import xyz.gianlu.librespot.common.NameThreadFactory;
33+
import xyz.gianlu.librespot.common.Utils;
3334
import xyz.gianlu.librespot.core.ApResolver;
3435
import xyz.gianlu.librespot.core.Session;
3536
import xyz.gianlu.librespot.mercury.MercuryClient;
@@ -97,7 +98,7 @@ private void handleRequest(@NotNull JsonObject obj) {
9798
Map<String, String> headers = getHeaders(obj);
9899
JsonObject payload = obj.getAsJsonObject("payload");
99100
if ("gzip".equals(headers.get("Transfer-Encoding"))) {
100-
byte[] gzip = Base64.getDecoder().decode(payload.get("compressed").getAsString());
101+
byte[] gzip = Utils.fromBase64(payload.get("compressed").getAsString());
101102
try (GZIPInputStream in = new GZIPInputStream(new ByteArrayInputStream(gzip)); Reader reader = new InputStreamReader(in)) {
102103
payload = JsonParser.parseReader(reader).getAsJsonObject();
103104
} catch (IOException ex) {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,7 @@ public Session.Builder initSessionBuilder() throws IOException, GeneralSecurityE
379379
builder.facebook();
380380
break;
381381
case BLOB:
382-
builder.blob(authUsername(), Base64.getDecoder().decode(authBlob()));
382+
builder.blob(authUsername(), Utils.fromBase64(authBlob()));
383383
break;
384384
case USER_PASS:
385385
builder.userPass(authUsername(), authPassword());

player/src/main/java/xyz/gianlu/librespot/player/StateWrapper.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ public static String generatePlaybackId(@NotNull Random random) {
124124
private static String generateSessionId(@NotNull Random random) {
125125
byte[] bytes = new byte[16];
126126
random.nextBytes(bytes);
127-
return Base64.getEncoder().withoutPadding().encodeToString(bytes);
127+
return Utils.toBase64NoPadding(bytes);
128128
}
129129

130130
private boolean shouldPlay(@NotNull ContextTrack track) {
@@ -1536,4 +1536,4 @@ synchronized void updateMetadataFor(@NotNull String uri, @NotNull String key, @N
15361536
updateMetadataFor(index, key, value);
15371537
}
15381538
}
1539-
}
1539+
}

0 commit comments

Comments
 (0)