-
Notifications
You must be signed in to change notification settings - Fork 1
/
YggdrasilMinecraftSessionService.java
207 lines (189 loc) · 9.52 KB
/
YggdrasilMinecraftSessionService.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
package com.mojang.authlib.yggdrasil;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.Iterables;
import com.google.common.collect.Multimap;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException;
import com.mojang.authlib.Environment;
import com.mojang.authlib.GameProfile;
import com.mojang.authlib.HttpAuthenticationService;
import com.mojang.authlib.exceptions.AuthenticationException;
import com.mojang.authlib.exceptions.AuthenticationUnavailableException;
import com.mojang.authlib.minecraft.HttpMinecraftSessionService;
import com.mojang.authlib.minecraft.InsecureTextureException;
import com.mojang.authlib.minecraft.MinecraftProfileTexture;
import com.mojang.authlib.properties.Property;
import com.mojang.authlib.yggdrasil.request.JoinMinecraftServerRequest;
import com.mojang.authlib.yggdrasil.response.HasJoinedMinecraftServerResponse;
import com.mojang.authlib.yggdrasil.response.MinecraftProfilePropertiesResponse;
import com.mojang.authlib.yggdrasil.response.MinecraftTexturesPayload;
import com.mojang.authlib.yggdrasil.response.Response;
import com.mojang.util.UUIDTypeAdapter;
import java.io.IOException;
import java.net.InetAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import org.apache.commons.codec.Charsets;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class YggdrasilMinecraftSessionService extends HttpMinecraftSessionService {
private static final String[] WHITELISTED_DOMAINS = new String[]{".minecraft.net", ".mojang.com"};
private static final Logger LOGGER = LogManager.getLogger();
private final String baseUrl;
private final URL joinUrl;
private final URL checkUrl;
private final PublicKey publicKey;
private final Gson gson = (new GsonBuilder()).registerTypeAdapter(UUID.class, new UUIDTypeAdapter()).create();
private final LoadingCache<GameProfile, GameProfile> insecureProfiles = CacheBuilder.newBuilder()
.expireAfterWrite(6L, TimeUnit.HOURS)
.build(new CacheLoader<GameProfile, GameProfile>() {
@Override
public GameProfile load(GameProfile key) throws Exception {
return YggdrasilMinecraftSessionService.this.fillGameProfile(key, false);
}
});
protected YggdrasilMinecraftSessionService(YggdrasilAuthenticationService service, Environment env) {
super(service);
this.baseUrl = env.getSessionHost() + "/session/minecraft/";
this.joinUrl = HttpAuthenticationService.constantURL(this.baseUrl + "join");
this.checkUrl = HttpAuthenticationService.constantURL(this.baseUrl + "hasJoined");
try {
X509EncodedKeySpec spec = new X509EncodedKeySpec(IOUtils.toByteArray(YggdrasilMinecraftSessionService.class.getResourceAsStream("/yggdrasil_session_pubkey.der")));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
this.publicKey = keyFactory.generatePublic(spec);
} catch (IOException | NoSuchAlgorithmException | InvalidKeySpecException ignored) {
throw new Error("Missing/invalid yggdrasil public key!");
}
}
@Override
public void joinServer(GameProfile profile, String authenticationToken, String serverId) throws AuthenticationException {
JoinMinecraftServerRequest request = new JoinMinecraftServerRequest();
request.accessToken = authenticationToken;
request.selectedProfile = profile.getId();
request.serverId = serverId;
getAuthenticationService().makeRequest(this.joinUrl, request, Response.class);
}
@Override
public GameProfile hasJoinedServer(GameProfile user, String serverId, InetAddress address) throws AuthenticationUnavailableException {
Map<String, Object> arguments = new HashMap<>();
arguments.put("username", user.getName());
arguments.put("serverId", serverId);
if (address != null) {
arguments.put("ip", address.getHostAddress());
}
URL url = HttpAuthenticationService.concatenateURL(this.checkUrl, HttpAuthenticationService.buildQuery(arguments));
try {
HasJoinedMinecraftServerResponse response = getAuthenticationService().<HasJoinedMinecraftServerResponse>makeRequest(url, null, HasJoinedMinecraftServerResponse.class);
if (response != null && response.getId() != null) {
GameProfile result = new GameProfile(response.getId(), user.getName());
if (response.getProperties() != null) {
result.getProperties().putAll((Multimap) response.getProperties());
}
return result;
}
return null;
} catch (AuthenticationUnavailableException e) {
throw e;
} catch (AuthenticationException ignored) {
return null;
}
}
@Override
public Map<MinecraftProfileTexture.Type, MinecraftProfileTexture> getTextures(GameProfile profile, boolean requireSecure) {
MinecraftTexturesPayload result;
Property textureProperty = (Property) Iterables.getFirst(profile.getProperties().get("textures"), null);
if (textureProperty == null) {
return new HashMap<>();
}
if (requireSecure) {
if (!textureProperty.hasSignature()) {
LOGGER.error("Signature is missing from textures payload");
throw new InsecureTextureException("Signature is missing from textures payload");
}
if (!textureProperty.isSignatureValid(this.publicKey)) {
LOGGER.error("Textures payload has been tampered with (signature invalid)");
throw new InsecureTextureException("Textures payload has been tampered with (signature invalid)");
}
}
try {
String json = new String(Base64.decodeBase64(textureProperty.getValue()), Charsets.UTF_8);
result = (MinecraftTexturesPayload) this.gson.fromJson(json, MinecraftTexturesPayload.class);
} catch (JsonParseException e) {
LOGGER.error("Could not decode textures payload", (Throwable) e);
return new HashMap<>();
}
if (result == null || result.getTextures() == null) {
return new HashMap<>();
}
for (Map.Entry<MinecraftProfileTexture.Type, MinecraftProfileTexture> entry : (Iterable<Map.Entry<MinecraftProfileTexture.Type, MinecraftProfileTexture>>) result.getTextures().entrySet()) {
if (!isWhitelistedDomain(((MinecraftProfileTexture) entry.getValue()).getUrl())) {
LOGGER.error("Textures payload has been tampered with (non-whitelisted domain)");
return new HashMap<>();
}
}
return result.getTextures();
}
@Override
public GameProfile fillProfileProperties(GameProfile profile, boolean requireSecure) {
if (profile.getId() == null) {
return profile;
}
if (!requireSecure) {
return (GameProfile) this.insecureProfiles.getUnchecked(profile);
}
return fillGameProfile(profile, true);
}
protected GameProfile fillGameProfile(GameProfile profile, boolean requireSecure) {
try {
URL url = HttpAuthenticationService.constantURL(this.baseUrl + "profile/" + UUIDTypeAdapter.fromUUID(profile.getId()));
url = HttpAuthenticationService.concatenateURL(url, "unsigned=" + (!requireSecure ? 1 : 0));
MinecraftProfilePropertiesResponse response = getAuthenticationService().<MinecraftProfilePropertiesResponse>makeRequest(url, null, MinecraftProfilePropertiesResponse.class);
if (response == null) {
LOGGER.debug("Couldn't fetch profile properties for " + profile + " as the profile does not exist");
return profile;
}
GameProfile result = new GameProfile(response.getId(), response.getName());
result.getProperties().putAll((Multimap) response.getProperties());
profile.getProperties().putAll((Multimap) response.getProperties());
LOGGER.debug("Successfully fetched profile properties for " + profile);
return result;
} catch (AuthenticationException e) {
LOGGER.warn("Couldn't look up profile properties for " + profile, (Throwable) e);
return profile;
}
}
@Override
public YggdrasilAuthenticationService getAuthenticationService() {
return (YggdrasilAuthenticationService) super.getAuthenticationService();
}
private static boolean isWhitelistedDomain(String url) {
URI uri = null;
try {
uri = new URI(url);
} catch (URISyntaxException ignored) {
throw new IllegalArgumentException("Invalid URL '" + url + "'");
}
String domain = uri.getHost();
for (String whitelistedDomain : WHITELISTED_DOMAINS) {
if (domain.endsWith(whitelistedDomain)) {
return true;
}
}
return false;
}
}