Skip to content

Commit fd82b2e

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

File tree

2 files changed

+88
-0
lines changed

2 files changed

+88
-0
lines changed

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

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,14 @@
99
import java.nio.file.Path;
1010
import java.nio.file.Paths;
1111
import java.nio.file.StandardCopyOption;
12+
import java.security.MessageDigest;
13+
import java.security.NoSuchAlgorithmException;
1214
import java.util.ArrayList;
1315
import java.util.Collections;
1416
import java.util.HashSet;
1517
import java.util.List;
1618
import java.util.Set;
19+
import java.util.UUID;
1720
import java.util.concurrent.Callable;
1821
import lombok.extern.slf4j.Slf4j;
1922
import me.itzg.helpers.errors.GenericException;
@@ -25,7 +28,9 @@
2528
import me.itzg.helpers.json.ObjectMappers;
2629
import me.itzg.helpers.users.model.JavaOp;
2730
import me.itzg.helpers.users.model.JavaUser;
31+
2832
import org.apache.maven.artifact.versioning.ComparableVersion;
33+
2934
import picocli.CommandLine.ArgGroup;
3035
import picocli.CommandLine.Command;
3136
import picocli.CommandLine.ExitCode;
@@ -46,6 +51,9 @@ public class ManageUsersCommand implements Callable<Integer> {
4651
@Option(names = {"--help", "-h"}, usageHelp = true)
4752
boolean help;
4853

54+
@Option(names = {"--offline"}, required = false, description = "Use for offline server, UUIDs are generated")
55+
boolean offline;
56+
4957
@Option(names = "--output-directory", defaultValue = ".")
5058
Path outputDirectory;
5159

@@ -240,6 +248,10 @@ private JavaUser resolveJavaUserId(SharedFetch sharedFetch, List<? extends JavaU
240248
}
241249
}
242250

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

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)