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