· 6 years ago · Aug 26, 2019, 02:12 PM
1package net.md_5.bungee.connection;
2
3import com.google.common.base.Charsets;
4import com.google.common.base.Preconditions;
5import com.google.gson.Gson;
6import lombok.Getter;
7import lombok.RequiredArgsConstructor;
8import net.md_5.bungee.*;
9import net.md_5.bungee.api.*;
10import net.md_5.bungee.api.chat.BaseComponent;
11import net.md_5.bungee.api.chat.TextComponent;
12import net.md_5.bungee.api.config.ListenerInfo;
13import net.md_5.bungee.api.config.ServerInfo;
14import net.md_5.bungee.api.connection.PendingConnection;
15import net.md_5.bungee.api.connection.ProxiedPlayer;
16import net.md_5.bungee.api.event.*;
17import net.md_5.bungee.chat.ComponentSerializer;
18import net.md_5.bungee.http.HttpClient;
19import net.md_5.bungee.jni.cipher.BungeeCipher;
20import net.md_5.bungee.netty.ChannelWrapper;
21import net.md_5.bungee.netty.HandlerBoss;
22import net.md_5.bungee.netty.PacketHandler;
23import net.md_5.bungee.netty.PipelineUtils;
24import net.md_5.bungee.netty.cipher.CipherDecoder;
25import net.md_5.bungee.netty.cipher.CipherEncoder;
26import net.md_5.bungee.protocol.DefinedPacket;
27import net.md_5.bungee.protocol.PacketWrapper;
28import net.md_5.bungee.protocol.Protocol;
29import net.md_5.bungee.protocol.ProtocolConstants;
30import net.md_5.bungee.protocol.packet.*;
31import net.md_5.bungee.util.BoundedArrayList;
32import net.md_5.bungee.util.BufUtil;
33import net.md_5.bungee.util.QuietException;
34
35import javax.crypto.SecretKey;
36import java.math.BigInteger;
37import java.net.InetSocketAddress;
38import java.net.URI;
39import java.net.URISyntaxException;
40import java.net.URLEncoder;
41import java.security.MessageDigest;
42import java.util.List;
43import java.util.UUID;
44import java.util.logging.Level;
45
46@RequiredArgsConstructor
47public class InitialHandler extends PacketHandler implements PendingConnection {
48
49 private final BungeeCord bungee;
50 @Getter
51 private final ListenerInfo listener;
52 @Getter
53 private final List<PluginMessage> relayMessages = new BoundedArrayList<>(128);
54 public ChannelWrapper ch;
55 private final Unsafe unsafe =
56 new Unsafe() {
57 @Override
58 public void sendPacket(DefinedPacket packet) {
59 ch.write(packet);
60 }
61 };
62 @Getter
63 private Handshake handshake;
64 @Getter
65 private LoginRequest loginRequest;
66 private EncryptionRequest request;
67 private State thisState = State.HANDSHAKE;
68 @Getter
69 private boolean onlineMode = BungeeCord.getInstance().config.isOnlineMode();
70 @Getter
71 private InetSocketAddress virtualHost;
72 private String name;
73 @Getter
74 private UUID uniqueId;
75 @Getter
76 private UUID offlineId;
77 @Getter
78 private LoginResult loginProfile;
79 @Getter
80 private boolean legacy;
81 @Getter
82 private String extraDataInHandshake = "";
83
84 private static String getFirstLine(String str) {
85 int pos = str.indexOf('\n');
86 return pos == -1 ? str : str.substring(0, pos);
87 }
88
89 @Override
90 public boolean shouldHandle(PacketWrapper packet) throws Exception {
91 return !ch.isClosing();
92 }
93
94 @Override
95 public void connected(ChannelWrapper channel) throws Exception {
96 this.ch = channel;
97 }
98
99 @Override
100 public void exception(Throwable t) throws Exception {
101 disconnect(ChatColor.RED + Util.exception(t));
102 }
103
104 @Override
105 public void handle(PacketWrapper packet) throws Exception {
106 if (packet.packet == null) {
107 throw new QuietException(
108 "Unexpected packet received during login process! " + BufUtil.dump(packet.buf, 16));
109 }
110 }
111
112 @Override
113 public void handle(PluginMessage pluginMessage) throws Exception {
114 // TODO: Unregister?
115 if (PluginMessage.SHOULD_RELAY.apply(pluginMessage)) {
116 relayMessages.add(pluginMessage);
117 }
118 }
119
120 @Override
121 public void handle(LegacyHandshake legacyHandshake) throws Exception {
122 this.legacy = true;
123 ch.close(bungee.getTranslation("outdated_client", bungee.getGameVersion()));
124 }
125
126 @Override
127 public void handle(LegacyPing ping) throws Exception {
128 this.legacy = true;
129 final boolean v1_5 = ping.isV1_5();
130
131 ServerPing legacy =
132 new ServerPing(
133 new ServerPing.Protocol(
134 bungee.getName() + " " + bungee.getGameVersion(), bungee.getProtocolVersion()),
135 new ServerPing.Players(listener.getMaxPlayers(), bungee.getOnlineCount(), null),
136 new TextComponent(TextComponent.fromLegacyText(listener.getMotd())),
137 (Favicon) null);
138
139 Callback<ProxyPingEvent> callback =
140 new Callback<ProxyPingEvent>() {
141 @Override
142 public void done(ProxyPingEvent result, Throwable error) {
143 if (ch.isClosed()) {
144 return;
145 }
146
147 ServerPing legacy = result.getResponse();
148 String kickMessage;
149
150 if (v1_5) {
151 kickMessage =
152 ChatColor.DARK_BLUE
153 + "\00"
154 + 127
155 + '\00'
156 + legacy.getVersion().getName()
157 + '\00'
158 + getFirstLine(legacy.getDescription())
159 + '\00'
160 + legacy.getPlayers().getOnline()
161 + '\00'
162 + legacy.getPlayers().getMax();
163 } else {
164 // Clients <= 1.3 don't support colored motds because the color char is used as
165 // delimiter
166 kickMessage =
167 ChatColor.stripColor(getFirstLine(legacy.getDescription()))
168 + '\u00a7'
169 + legacy.getPlayers().getOnline()
170 + '\u00a7'
171 + legacy.getPlayers().getMax();
172 }
173
174 ch.close(kickMessage);
175 }
176 };
177
178 bungee.getPluginManager().callEvent(new ProxyPingEvent(this, legacy, callback));
179 }
180
181 @Override
182 public void handle(StatusRequest statusRequest) throws Exception {
183 Preconditions.checkState(thisState == State.STATUS, "Not expecting STATUS");
184
185 //bungee.cachedStatusPacket(this, listener, (packet, error) -> ch.write(packet));
186 ServerInfo forced = AbstractReconnectHandler.getForcedHost(this);
187 final String motd = (forced != null) ? forced.getMotd() : listener.getMotd();
188
189 Callback<ServerPing> pingBack =
190 new Callback<ServerPing>() {
191 @Override
192 public void done(ServerPing result, Throwable error) {
193 if (error != null) {
194 result = new ServerPing();
195 result.setDescription(bungee.getTranslation("ping_cannot_connect"));
196 bungee.getLogger().log(Level.WARNING, "Error pinging remote server", error);
197 }
198
199 Callback<ProxyPingEvent> callback =
200 new Callback<ProxyPingEvent>() {
201 @Override
202 public void done(ProxyPingEvent pingResult, Throwable error) {
203 Gson gson = BungeeCord.getInstance().gson;
204 unsafe.sendPacket(new StatusResponse(gson.toJson(pingResult.getResponse())));
205 if (bungee.getConnectionThrottle() != null) {
206 bungee.getConnectionThrottle().unthrottle(getAddress().getAddress());
207 }
208 }
209 };
210
211 bungee
212 .getPluginManager()
213 .callEvent(new ProxyPingEvent(InitialHandler.this, result, callback));
214 }
215 };
216
217 if (forced != null && listener.isPingPassthrough()) {
218 ((BungeeServerInfo) forced).ping(pingBack, handshake.getProtocolVersion());
219 } else {
220 int protocol =
221 (ProtocolConstants.SUPPORTED_VERSION_IDS.contains(handshake.getProtocolVersion()))
222 ? handshake.getProtocolVersion()
223 : bungee.getProtocolVersion();
224 pingBack.done(
225 new ServerPing(
226 new ServerPing.Protocol(bungee.getName() + " " + bungee.getGameVersion(), protocol),
227 new ServerPing.Players(listener.getMaxPlayers(), bungee.getOnlineCount(), null),
228 motd,
229 BungeeCord.getInstance().config.getFaviconObject()),
230 null);
231 }
232
233 thisState = State.PING;
234 }
235
236 @Override
237 public void handle(PingPacket ping) throws Exception {
238 Preconditions.checkState(thisState == State.PING, "Not expecting PING");
239 unsafe.sendPacket(ping);
240 disconnect("");
241 }
242
243 @Override
244 public void handle(Handshake handshake) throws Exception {
245 Preconditions.checkState(thisState == State.HANDSHAKE, "Not expecting HANDSHAKE");
246
247 // Verify handshake host
248 if (handshake.getHost().length() > 40) {
249 ch.close();
250 return;
251 }
252
253 // Optional
254 // resolveHandshakeHost(handshake.getHost());
255
256 this.virtualHost = new InetSocketAddress(handshake.getHost(), handshake.getPort());
257 this.handshake = handshake;
258
259 ch.setVersion(handshake.getProtocolVersion());
260
261 // Starting with FML 1.8, a "\0FML\0" token is appended to the handshake. This interferes
262 // with Bungee's IP forwarding, so we detect it, and remove it from the host string, for now.
263 // We know FML appends \00FML\00. However, we need to also consider that other systems might
264 // add their own data to the end of the string. So, we just take everything from the \0
265 // character
266 // and save it for later.
267 if (handshake.getHost().contains("\0")) {
268 String[] split = handshake.getHost().split("\0", 2);
269 handshake.setHost(split[0]);
270 extraDataInHandshake = "\0" + split[1];
271 }
272
273 // SRV records can end with a . depending on DNS / client.
274 if (handshake.getHost().endsWith(".")) {
275 handshake.setHost(handshake.getHost().substring(0, handshake.getHost().length() - 1));
276 }
277 if (bungee.getConfig().isLogPings()) {
278 bungee.getLogger().log(Level.INFO, "{0} has connected", this);
279 }
280
281 bungee.getPluginManager().callEvent(new PlayerHandshakeEvent(InitialHandler.this, handshake));
282
283 switch (handshake.getRequestedProtocol()) {
284 case 1:
285 // Ping
286 thisState = State.STATUS;
287 ch.setProtocol(Protocol.STATUS);
288 break;
289 case 2:
290 // Login
291 if (!bungee.getConfig().isLogPings()) {
292 bungee.getLogger().log(Level.INFO, "{0} has connected", this);
293 }
294 thisState = State.USERNAME;
295 ch.setProtocol(Protocol.LOGIN);
296
297 if (!ProtocolConstants.SUPPORTED_VERSION_IDS.contains(handshake.getProtocolVersion())) {
298 if (handshake.getProtocolVersion() > bungee.getProtocolVersion()) {
299 disconnect(bungee.getTranslation("outdated_server", bungee.getGameVersion()));
300 } else {
301 disconnect(bungee.getTranslation("outdated_client", bungee.getGameVersion()));
302 }
303 return;
304 }
305 break;
306 default:
307 throw new IllegalArgumentException(
308 "Cannot request protocol " + handshake.getRequestedProtocol());
309 }
310 }
311
312 @Override
313 public void handle(LoginRequest loginRequest) throws Exception {
314 Preconditions.checkState(thisState == State.USERNAME, "Not expecting USERNAME");
315 this.loginRequest = loginRequest;
316
317 if (getName().contains(".")) {
318 disconnect(bungee.getTranslation("name_invalid"));
319 return;
320 }
321
322 if (getName().length() > 16) {
323 disconnect(bungee.getTranslation("name_too_long"));
324 return;
325 }
326
327 int limit = BungeeCord.getInstance().config.getPlayerLimit();
328 if (limit > 0 && bungee.getOnlineCount() > limit) {
329 disconnect(bungee.getTranslation("proxy_full"));
330 return;
331 }
332
333 // If offline mode and they are already on, don't allow connect
334 // We can just check by UUID here as names are based on UUID
335 if (!isOnlineMode() && bungee.getPlayer(getUniqueId()) != null) {
336 disconnect(bungee.getTranslation("already_connected_proxy"));
337 return;
338 }
339
340 Callback<PreLoginEvent> callback =
341 new Callback<PreLoginEvent>() {
342
343 @Override
344 public void done(PreLoginEvent result, Throwable error) {
345 if (result.isCancelled()) {
346 disconnect(result.getCancelReasonComponents());
347 return;
348 }
349 if (ch.isClosed()) {
350 return;
351 }
352 if (onlineMode) {
353 unsafe().sendPacket(request = EncryptionUtil.encryptRequest());
354 } else {
355 finish();
356 }
357 thisState = State.ENCRYPT;
358 }
359 };
360
361 // fire pre login event
362 bungee.getPluginManager().callEvent(new PreLoginEvent(InitialHandler.this, callback));
363 }
364
365 @Override
366 public void handle(final EncryptionResponse encryptResponse) throws Exception {
367 Preconditions.checkState(thisState == State.ENCRYPT, "Not expecting ENCRYPT");
368
369 SecretKey sharedKey = EncryptionUtil.getSecret(encryptResponse, request);
370 BungeeCipher decrypt = EncryptionUtil.getCipher(false, sharedKey);
371 ch.addBefore(
372 PipelineUtils.FRAME_DECODER, PipelineUtils.DECRYPT_HANDLER, new CipherDecoder(decrypt));
373 BungeeCipher encrypt = EncryptionUtil.getCipher(true, sharedKey);
374 ch.addBefore(
375 PipelineUtils.FRAME_PREPENDER, PipelineUtils.ENCRYPT_HANDLER, new CipherEncoder(encrypt));
376
377 String encName = URLEncoder.encode(InitialHandler.this.getName(), "UTF-8");
378
379 MessageDigest sha = MessageDigest.getInstance("SHA-1");
380 for (byte[] bit :
381 new byte[][]{
382 request.getServerId().getBytes("ISO_8859_1"),
383 sharedKey.getEncoded(),
384 EncryptionUtil.keys.getPublic().getEncoded()
385 }) {
386 sha.update(bit);
387 }
388 String encodedHash = URLEncoder.encode(new BigInteger(sha.digest()).toString(16), "UTF-8");
389
390 String preventProxy =
391 ((BungeeCord.getInstance().config.isPreventProxyConnections())
392 ? "&ip=" + URLEncoder.encode(getAddress().getAddress().getHostAddress(), "UTF-8")
393 : "");
394 String authURL =
395 "https://sessionserver.mojang.com/session/minecraft/hasJoined?username="
396 + encName
397 + "&serverId="
398 + encodedHash
399 + preventProxy;
400
401 Callback<String> handler =
402 new Callback<String>() {
403 @Override
404 public void done(String result, Throwable error) {
405 if (error == null) {
406 LoginResult obj = BungeeCord.getInstance().gson.fromJson(result, LoginResult.class);
407 if (obj != null && obj.getId() != null) {
408 loginProfile = obj;
409 name = obj.getName();
410 uniqueId = Util.getUUID(obj.getId());
411 finish();
412 return;
413 }
414 disconnect(bungee.getTranslation("offline_mode_player"));
415 } else {
416 disconnect(bungee.getTranslation("mojang_fail"));
417 bungee
418 .getLogger()
419 .log(
420 Level.SEVERE,
421 "Error authenticating " + getName() + " with minecraft.net",
422 error);
423 }
424 }
425 };
426
427 HttpClient.get(authURL, ch.getHandle().eventLoop(), handler);
428 }
429
430 private void finish() {
431 if (isOnlineMode()) {
432 // Check for multiple connections
433 // We have to check for the old name first
434 ProxiedPlayer oldName = bungee.getPlayer(getName());
435 if (oldName != null) {
436 // TODO See #1218
437 oldName.disconnect(bungee.getTranslation("already_connected_proxy"));
438 }
439 // And then also for their old UUID
440 ProxiedPlayer oldID = bungee.getPlayer(getUniqueId());
441 if (oldID != null) {
442 // TODO See #1218
443 oldID.disconnect(bungee.getTranslation("already_connected_proxy"));
444 }
445 } else {
446 // In offline mode the existing user stays and we kick the new one
447 ProxiedPlayer oldName = bungee.getPlayer(getName());
448 if (oldName != null) {
449 // TODO See #1218
450 disconnect(bungee.getTranslation("already_connected_proxy"));
451 return;
452 }
453 }
454
455 offlineId = UUID.nameUUIDFromBytes(("OfflinePlayer:" + getName()).getBytes(Charsets.UTF_8));
456 if (uniqueId == null) {
457 uniqueId = offlineId;
458 }
459
460 Callback<LoginEvent> complete =
461 new Callback<LoginEvent>() {
462 @Override
463 public void done(LoginEvent result, Throwable error) {
464 if (result.isCancelled()) {
465 disconnect(result.getCancelReasonComponents());
466 return;
467 }
468 if (ch.isClosed()) {
469 return;
470 }
471
472 ch.getHandle()
473 .eventLoop()
474 .execute(
475 new Runnable() {
476 @Override
477 public void run() {
478 if (!ch.isClosing()) {
479 UserConnection userCon =
480 new UserConnection(bungee, ch, getName(), InitialHandler.this);
481 userCon.setCompressionThreshold(
482 BungeeCord.getInstance().config.getCompressionThreshold());
483 userCon.setClientEntityId(result.getClientEntityId());
484 userCon.init();
485
486 unsafe.sendPacket(
487 new LoginSuccess(
488 getUniqueId().toString(), getName())); // With dashes in between
489 ch.setProtocol(Protocol.GAME);
490
491 result
492 .getLoginProcessor()
493 .processLogin(bungee, userCon, InitialHandler.this);
494
495 thisState = State.FINISHED;
496 }
497 }
498 });
499 }
500 };
501
502 // fire login event
503 bungee
504 .getPluginManager()
505 .callEvent(new LoginEvent(InitialHandler.this, new DefaultLoginProcessor(), complete));
506 }
507
508 @Override
509 public void handle(LoginPayloadResponse response) throws Exception {
510 disconnect("Unexpected custom LoginPayloadResponse");
511 }
512
513 @Override
514 public void disconnect(String reason) {
515 disconnect(TextComponent.fromLegacyText(reason));
516 }
517
518 @Override
519 public void disconnect(final BaseComponent... reason) {
520 if (thisState != State.STATUS && thisState != State.PING) {
521 ch.delayedClose(new Kick(ComponentSerializer.toString(reason)));
522 } else {
523 ch.close();
524 }
525 }
526
527 @Override
528 public void disconnect(BaseComponent reason) {
529 disconnect(new BaseComponent[]{reason});
530 }
531
532 @Override
533 public String getName() {
534 return (name != null) ? name : (loginRequest == null) ? null : loginRequest.getData();
535 }
536
537 @Override
538 public int getVersion() {
539 return (handshake == null) ? -1 : handshake.getProtocolVersion();
540 }
541
542 @Override
543 public InetSocketAddress getAddress() {
544 return ch.getRemoteAddress();
545 }
546
547 @Override
548 public Unsafe unsafe() {
549 return unsafe;
550 }
551
552 @Override
553 public void setOnlineMode(boolean onlineMode) {
554 Preconditions.checkState(
555 thisState == State.USERNAME, "Can only set online mode status whilst state is username");
556 this.onlineMode = onlineMode;
557 }
558
559 @Override
560 public void setUniqueId(UUID uuid) {
561 Preconditions.checkState(
562 thisState == State.USERNAME, "Can only set uuid while state is username");
563 Preconditions.checkState(!onlineMode, "Can only set uuid when online mode is false");
564 this.uniqueId = uuid;
565 }
566
567 public void setLoginProfile(LoginResult loginProfile) {
568 this.loginProfile = loginProfile;
569 }
570
571 @Override
572 public String getUUID() {
573 return uniqueId.toString().replace("-", "");
574 }
575
576 @Override
577 public String toString() {
578 return "[" + ((getName() != null) ? getName() : getAddress()) + "] <-> InitialHandler";
579 }
580
581 @Override
582 public boolean isConnected() {
583 return !ch.isClosed();
584 }
585
586 private enum State {
587 HANDSHAKE,
588 STATUS,
589 PING,
590 USERNAME,
591 ENCRYPT,
592 FINISHED;
593 }
594
595 private static void resolveHandshakeHost(String host) {
596 try {
597 new URI("tcp://" + host);
598 } catch (URISyntaxException ex) {
599 throw new QuietException("Invalid handshake host");
600 }
601 }
602
603 private static class DefaultLoginProcessor implements LoginProcessor {
604
605 @Override
606 public void processLogin(
607 ProxyServer proxy, ProxiedPlayer player, PendingConnection pendingConnection) {
608 UserConnection con = (UserConnection) player;
609 InitialHandler initialHandler = (InitialHandler) pendingConnection;
610
611 con.getCh()
612 .getHandle()
613 .pipeline()
614 .get(HandlerBoss.class)
615 .setHandler(new UpstreamBridge(proxy, con));
616
617 proxy.getPluginManager().callEvent(new PostLoginEvent(con));
618 ServerInfo server = AbstractReconnectHandler.getForcedHost(initialHandler);
619 if (server == null) {
620 server = proxy.getServerInfo(initialHandler.getListener().getDefaultServer());
621 }
622
623 con.connect(server, null, true, ServerConnectEvent.Reason.JOIN_PROXY);
624 }
625 }
626}