Skip to content

Commit eb1cd45

Browse files
committed
feat(users): offline UUD generation support
1 parent b3e2087 commit eb1cd45

File tree

2 files changed

+93
-0
lines changed

2 files changed

+93
-0
lines changed

src/main/java/me/itzg/helpers/users/ManageUsersCommand.java

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,21 @@
33
import com.fasterxml.jackson.core.type.TypeReference;
44
import com.fasterxml.jackson.databind.ObjectMapper;
55
import java.io.IOException;
6+
import java.math.BigInteger;
67
import java.net.URI;
78
import java.net.URISyntaxException;
89
import java.nio.file.Files;
910
import java.nio.file.Path;
1011
import java.nio.file.Paths;
1112
import java.nio.file.StandardCopyOption;
13+
import java.security.MessageDigest;
14+
import java.security.NoSuchAlgorithmException;
1215
import java.util.ArrayList;
1316
import java.util.Collections;
1417
import java.util.HashSet;
1518
import java.util.List;
1619
import java.util.Set;
20+
import java.util.UUID;
1721
import java.util.concurrent.Callable;
1822
import lombok.extern.slf4j.Slf4j;
1923
import me.itzg.helpers.errors.GenericException;
@@ -25,7 +29,13 @@
2529
import me.itzg.helpers.json.ObjectMappers;
2630
import me.itzg.helpers.users.model.JavaOp;
2731
import me.itzg.helpers.users.model.JavaUser;
32+
33+
import org.apache.commons.codec.binary.Hex;
34+
import org.apache.commons.codec.digest.Crypt;
35+
import org.apache.commons.codec.digest.Md5Crypt;
2836
import org.apache.maven.artifact.versioning.ComparableVersion;
37+
import org.codehaus.plexus.util.Os;
38+
2939
import picocli.CommandLine.ArgGroup;
3040
import picocli.CommandLine.Command;
3141
import picocli.CommandLine.ExitCode;
@@ -46,6 +56,9 @@ public class ManageUsersCommand implements Callable<Integer> {
4656
@Option(names = {"--help", "-h"}, usageHelp = true)
4757
boolean help;
4858

59+
@Option(names = {"--offline"}, required = false, description = "Use for offline server, UUIDs are generated")
60+
boolean offline;
61+
4962
@Option(names = "--output-directory", defaultValue = ".")
5063
Path outputDirectory;
5164

@@ -240,6 +253,10 @@ private JavaUser resolveJavaUserId(SharedFetch sharedFetch, List<? extends JavaU
240253
}
241254
}
242255

256+
if (offline) {
257+
return getOfflineUUID(input);
258+
}
259+
243260
final UserApi userApi;
244261
switch (userApiProvider) {
245262
case mojang:
@@ -329,4 +346,38 @@ private void processInputAsFile(SharedFetch sharedFetch, String filePathUrl) thr
329346
private boolean usesTextUserList() {
330347
return version != null && new ComparableVersion(version).compareTo(MIN_VERSION_USES_JSON) < 0;
331348
}
349+
350+
private static JavaUser getOfflineUUID(String username) {
351+
byte[] bytes = new byte[16];
352+
try {
353+
bytes = MessageDigest.getInstance("MD5").digest(("OfflinePlayer:"+username).getBytes());
354+
}catch(NoSuchAlgorithmException e){
355+
System.exit(1);
356+
return JavaUser.builder().name(username).build();
357+
}
358+
359+
// Force version = 3 (bits 12-15 of time_hi_and_version)
360+
bytes[6] &= 0x0F;
361+
bytes[6] |= 0x30;
362+
363+
// Force variant = 2 (bits 6-7 of clock_seq_hi_and_reserved)
364+
bytes[8] &= 0x3F;
365+
bytes[8] |= 0x80;
366+
367+
long msb = 0;
368+
long lsb = 0;
369+
370+
for (int i = 0; i < 8; i++) {
371+
msb = (msb << 8) | (bytes[i] & 0xFF);
372+
}
373+
374+
for (int i = 8; i < 16; i++) {
375+
lsb = (lsb << 8) | (bytes[i] & 0xFF);
376+
}
377+
378+
return JavaUser.builder()
379+
.name(username)
380+
.uuid(new UUID(msb, lsb).toString())
381+
.build();
382+
}
332383
}

src/test/java/me/itzg/helpers/users/ManageUsersCommandTest.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import static uk.org.webcompere.modelassert.json.JsonAssertions.assertJson;
66
import static uk.org.webcompere.modelassert.json.condition.ConditionList.conditions;
77

8+
import com.ctc.wstx.io.SystemId;
89
import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
910
import com.github.tomakehurst.wiremock.junit5.WireMockTest;
1011
import java.io.IOException;
@@ -25,8 +26,10 @@ class ManageUsersCommandTest {
2526

2627
private static final String USER1_ID = "3f5f20286a85445fa7b46100e70c2b3a";
2728
private static final String USER1_UUID = "3f5f2028-6a85-445f-a7b4-6100e70c2b3a";
29+
private static final String USER1_OFFLINE_UUID = "fb4cdad9-642b-358f-8f6f-717981c9f42b";
2830
private static final String USER2_ID = "5e5a1b2294b14f5892466062597e4c91";
2931
private static final String USER2_UUID = "5e5a1b22-94b1-4f58-9246-6062597e4c91";
32+
private static final String USER2_OFFLINE_UUID = "6e7d9aa0-0da2-390c-ab6a-377df9d77518";
3033

3134
@TempDir
3235
Path tempDir;
@@ -406,6 +409,45 @@ void givenUuidsAndAllExist(WireMockRuntimeInfo wmInfo) throws IOException {
406409
}
407410
}
408411

412+
@Nested
413+
public class whitelistOffline {
414+
@Test
415+
void givenNames(WireMockRuntimeInfo wmInfo) {
416+
setupUserStubs();
417+
418+
final int exitCode = new CommandLine(
419+
new ManageUsersCommand()
420+
)
421+
.execute(
422+
"--mojang-api-base-url", wmInfo.getHttpBaseUrl(),
423+
"--user-api-provider", "mojang",
424+
"--offline",
425+
"--type", "JAVA_WHITELIST",
426+
"--output-directory", tempDir.toString(),
427+
"user1", "user2"
428+
);
429+
430+
assertThat(exitCode).isEqualTo(0);
431+
432+
final Path expectedFile = tempDir.resolve("whitelist.json");
433+
434+
assertThat(expectedFile).exists();
435+
436+
assertJson(expectedFile)
437+
.isArrayContainingExactlyInAnyOrder(
438+
conditions()
439+
.satisfies(conditions()
440+
.at("/name").hasValue("user1")
441+
.at("/uuid").hasValue(USER1_OFFLINE_UUID)
442+
)
443+
.satisfies(conditions()
444+
.at("/name").hasValue("user2")
445+
.at("/uuid").hasValue(USER2_OFFLINE_UUID)
446+
)
447+
);
448+
}
449+
}
450+
409451
@Nested
410452
public class whitelistOrOpsText {
411453

0 commit comments

Comments
 (0)