| package net.glowstone.net; |
| |
| import com.flowpowered.networking.AsyncableMessage; |
| import com.flowpowered.networking.Message; |
| import com.flowpowered.networking.MessageHandler; |
| import com.flowpowered.networking.session.BasicSession; |
| import io.netty.channel.Channel; |
| import io.netty.channel.ChannelFuture; |
| import io.netty.channel.ChannelFutureListener; |
| import io.netty.channel.ChannelHandler; |
| import io.netty.handler.codec.CodecException; |
| import net.glowstone.EventFactory; |
| import net.glowstone.GlowServer; |
| import net.glowstone.entity.GlowPlayer; |
| import net.glowstone.entity.meta.profile.PlayerProfile; |
| import net.glowstone.io.PlayerDataService; |
| import net.glowstone.net.message.KickMessage; |
| import net.glowstone.net.message.SetCompressionMessage; |
| import net.glowstone.net.message.play.game.PingMessage; |
| import net.glowstone.net.message.play.game.UserListItemMessage; |
| import net.glowstone.net.message.play.player.BlockPlacementMessage; |
| import net.glowstone.net.pipeline.CodecsHandler; |
| import net.glowstone.net.pipeline.CompressionHandler; |
| import net.glowstone.net.pipeline.EncryptionHandler; |
| import net.glowstone.net.pipeline.NoopHandler; |
| import net.glowstone.net.protocol.GlowProtocol; |
| import net.glowstone.net.protocol.LoginProtocol; |
| import net.glowstone.net.protocol.PlayProtocol; |
| import net.glowstone.net.protocol.ProtocolType; |
| import org.bukkit.event.player.PlayerKickEvent; |
| import org.bukkit.event.player.PlayerLoginEvent; |
| |
| import javax.crypto.SecretKey; |
| import java.net.InetSocketAddress; |
| import java.util.*; |
| import java.util.logging.Level; |
| |
| /** |
| * A single connection to the server, which may or may not be associated with a |
| * player. |
| * @author Graham Edgecombe |
| */ |
| public final class GlowSession extends BasicSession { |
| |
| /** |
| * The number of ticks which are elapsed before a client is disconnected due |
| * to a timeout. |
| */ |
| private static final int TIMEOUT_TICKS = 300; |
| |
| /** |
| * The server this session belongs to. |
| */ |
| private final GlowServer server; |
| |
| /** |
| * The Random for this session |
| */ |
| private final Random random = new Random(); |
| |
| /** |
| * A queue of incoming and unprocessed messages. |
| */ |
| private final Queue<Message> messageQueue = new ArrayDeque<>(); |
| |
| /** |
| * The remote address of the connection. |
| */ |
| private InetSocketAddress address; |
| |
| /** |
| * The verify token used in authentication |
| */ |
| private byte[] verifyToken; |
| |
| /** |
| * The verify username used in authentication |
| */ |
| private String verifyUsername; |
| |
| /** |
| * A message describing under what circumstances the connection ended. |
| */ |
| private String quitReason; |
| |
| /** |
| * The hostname used to connect. |
| */ |
| private String hostname; |
| |
| /** |
| * A timeout counter. This is increment once every tick and if it goes above |
| * a certain value the session is disconnected. |
| */ |
| private int readTimeoutCounter = 0; |
| |
| /** |
| * Data regarding a user who has connected through a proxy, used to |
| * provide online-mode UUID and properties and other data even if the |
| * server is running in offline mode. Null for non-proxied sessions. |
| */ |
| private ProxyData proxyData; |
| |
| /** |
| * Similar to readTimeoutCounter but for writes. |
| */ |
| private int writeTimeoutCounter = 0; |
| |
| /** |
| * The player associated with this session (if there is one). |
| */ |
| private GlowPlayer player; |
| |
| /** |
| * The ID of the last ping message sent, used to ensure the client responded correctly. |
| */ |
| private int pingMessageId; |
| |
| /** |
| * Stores the last block placement message sent, see BlockPlacementHandler. |
| */ |
| private BlockPlacementMessage previousPlacement; |
| |
| /** |
| * The number of ticks until previousPlacement must be cleared. |
| */ |
| private int previousPlacementTicks; |
| |
| /** |
| * Creates a new session. |
| * @param server The server this session belongs to. |
| * @param channel The channel associated with this session. |
| */ |
| public GlowSession(GlowServer server, Channel channel) { |
| super(channel, ProtocolType.HANDSHAKE.getProtocol()); |
| this.server = server; |
| address = super.getAddress(); |
| } |
| |
| /** |
| * Gets the server associated with this session. |
| * @return The server. |
| */ |
| public GlowServer getServer() { |
| return server; |
| } |
| |
| //////////////////////////////////////////////////////////////////////////// |
| // Auxiliary state |
| |
| /** |
| * Get the randomly-generated verify token for this session. |
| * @return The verify token |
| */ |
| public byte[] getVerifyToken() { |
| return verifyToken; |
| } |
| |
| /** |
| * Sets the verify token of this session. |
| * @param verifyToken The verify token. |
| */ |
| public void setVerifyToken(byte[] verifyToken) { |
| this.verifyToken = verifyToken; |
| } |
| |
| /** |
| * Gets the verify username for this session. |
| * @return The verify username. |
| */ |
| public String getVerifyUsername() { |
| return verifyUsername; |
| } |
| |
| /** |
| * Sets the verify username for this session. |
| * @param verifyUsername The verify username. |
| */ |
| public void setVerifyUsername(String verifyUsername) { |
| this.verifyUsername = verifyUsername; |
| } |
| |
| /** |
| * Get the {@link ProxyData} for this session if available. |
| * @return The proxy data to use, or null for an unproxied connection. |
| */ |
| public ProxyData getProxyData() { |
| return proxyData; |
| } |
| |
| /** |
| * Set the {@link ProxyData} for this session. |
| * @param proxyData The proxy data to use. |
| */ |
| public void setProxyData(ProxyData proxyData) { |
| this.proxyData = proxyData; |
| address = proxyData.getAddress(); |
| hostname = proxyData.getHostname(); |
| } |
| |
| /** |
| * Set the hostname the player used to connect to the server. |
| * @param hostname Hostname in "addr:port" format. |
| */ |
| public void setHostname(String hostname) { |
| this.hostname = hostname; |
| } |
| |
| /** |
| * Note that the client has responded to a keep-alive. |
| * @param pingId The pingId to check for validity. |
| */ |
| public void pong(long pingId) { |
| if (pingId == pingMessageId) { |
| readTimeoutCounter = 0; |
| pingMessageId = 0; |
| } |
| } |
| |
| /** |
| * Get the saved previous BlockPlacementMessage for this session. |
| * @return The message. |
| */ |
| public BlockPlacementMessage getPreviousPlacement() { |
| return previousPlacement; |
| } |
| |
| /** |
| * Set the previous BlockPlacementMessage for this session. |
| * @param message The message. |
| */ |
| public void setPreviousPlacement(BlockPlacementMessage message) { |
| previousPlacement = message; |
| previousPlacementTicks = 2; |
| } |
| |
| @Override |
| public InetSocketAddress getAddress() { |
| return address; |
| } |
| |
| //////////////////////////////////////////////////////////////////////////// |
| // Player and state management |
| |
| /** |
| * Gets the player associated with this session. |
| * @return The player, or {@code null} if no player is associated with it. |
| */ |
| public GlowPlayer getPlayer() { |
| return player; |
| } |
| |
| /** |
| * Sets the player associated with this session. |
| * @param profile The player's profile with name and UUID information. |
| * @throws IllegalStateException if there is already a player associated |
| * with this session. |
| */ |
| public void setPlayer(PlayerProfile profile) { |
| if (player != null) { |
| throw new IllegalStateException("Cannot set player twice"); |
| } |
| |
| // isActive check here in case player disconnected during authentication |
| if (!isActive()) { |
| // no need to call onDisconnect() since it only does anything if there's a player set |
| return; |
| } |
| |
| // initialize the player |
| PlayerDataService.PlayerReader reader = server.getPlayerDataService().beginReadingData(profile.getUniqueId()); |
| player = new GlowPlayer(this, profile, reader); |
| |
| // isActive check here in case player disconnected after authentication, |
| // but before the GlowPlayer initialization was completed |
| if (!isActive()) { |
| onDisconnect(); |
| return; |
| } |
| |
| // login event |
| PlayerLoginEvent event = EventFactory.onPlayerLogin(player, hostname); |
| if (event.getResult() != PlayerLoginEvent.Result.ALLOWED) { |
| disconnect(event.getKickMessage(), true); |
| return; |
| } |
| |
| // Kick other players with the same UUID |
| for (GlowPlayer other : getServer().getOnlinePlayers()) { |
| if (other != player && other.getUniqueId().equals(player.getUniqueId())) { |
| other.getSession().disconnect("You logged in from another location.", true); |
| break; |
| } |
| } |
| |
| player.getWorld().getRawPlayers().add(player); |
| |
| GlowServer.logger.info(player.getName() + " [" + address + "] connected, UUID: " + player.getUniqueId()); |
| |
| // message and user list |
| String message = EventFactory.onPlayerJoin(player).getJoinMessage(); |
| if (message != null && !message.isEmpty()) { |
| server.broadcastMessage(message); |
| } |
| |
| // todo: display names are included in the outgoing messages here, but |
| // don't show up on the client. A workaround or proper fix is needed. |
| Message addMessage = new UserListItemMessage(UserListItemMessage.Action.ADD_PLAYER, player.getUserListEntry()); |
| List<UserListItemMessage.Entry> entries = new ArrayList<>(); |
| for (GlowPlayer other : server.getOnlinePlayers()) { |
| if (other != player && other.canSee(player)) { |
| other.getSession().send(addMessage); |
| } |
| if (player.canSee(other)) { |
| entries.add(other.getUserListEntry()); |
| } |
| } |
| send(new UserListItemMessage(UserListItemMessage.Action.ADD_PLAYER, entries)); |
| } |
| |
| @Override |
| public ChannelFuture sendWithFuture(Message message) { |
| writeTimeoutCounter = 0; |
| if (!isActive()) { |
| // discard messages sent if we're closed, since this happens a lot |
| return null; |
| } |
| return super.sendWithFuture(message); |
| } |
| |
| @Override |
| @Deprecated |
| public void disconnect() { |
| disconnect("No reason specified."); |
| } |
| |
| /** |
| * Disconnects the session with the specified reason. This causes a |
| * KickMessage to be sent. When it has been delivered, the channel |
| * is closed. |
| * @param reason The reason for disconnection. |
| */ |
| public void disconnect(String reason) { |
| disconnect(reason, false); |
| } |
| |
| /** |
| * Disconnects the session with the specified reason. This causes a |
| * KickMessage to be sent. When it has been delivered, the channel |
| * is closed. |
| * @param reason The reason for disconnection. |
| * @param overrideKick Whether to skip the kick event. |
| */ |
| public void disconnect(String reason, boolean overrideKick) { |
| if (player != null && !overrideKick) { |
| PlayerKickEvent event = EventFactory.onPlayerKick(player, reason); |
| if (event.isCancelled()) { |
| return; |
| } |
| |
| reason = event.getReason(); |
| |
| if (event.getLeaveMessage() != null) { |
| server.broadcastMessage(event.getLeaveMessage()); |
| } |
| } |
| |
| // log that the player was kicked |
| if (player != null) { |
| GlowServer.logger.info(player.getName() + " kicked: " + reason); |
| } else { |
| GlowServer.logger.info("[" + address + "] kicked: " + reason); |
| } |
| |
| if (quitReason == null) { |
| quitReason = "kicked"; |
| } |
| |
| // perform the kick, sending a kick message if possible |
| if (isActive() && (getProtocol() instanceof LoginProtocol || getProtocol() instanceof PlayProtocol)) { |
| // channel is both currently connected and in a protocol state allowing kicks |
| sendWithFuture(new KickMessage(reason)).addListener(ChannelFutureListener.CLOSE); |
| } else { |
| getChannel().close(); |
| } |
| } |
| |
| /** |
| * Pulse this session, performing any updates needed. |
| */ |
| void pulse() { |
| readTimeoutCounter++; |
| writeTimeoutCounter++; |
| |
| // drop the previous placement if needed |
| if (previousPlacementTicks > 0 && --previousPlacementTicks == 0) { |
| previousPlacement = null; |
| } |
| |
| // process messages |
| Message message; |
| while ((message = messageQueue.poll()) != null) { |
| if (getProtocol() instanceof PlayProtocol && player == null) { |
| // player has been unset, we are just seeing extra messages now |
| continue; |
| } |
| |
| super.messageReceived(message); |
| readTimeoutCounter = 0; |
| } |
| |
| // let us know if the client has timed out yet |
| if (readTimeoutCounter >= TIMEOUT_TICKS) { |
| if (pingMessageId == 0 && getProtocol() instanceof PlayProtocol) { |
| pingMessageId = random.nextInt(); |
| send(new PingMessage(pingMessageId)); |
| } else { |
| disconnect("Timed out"); |
| } |
| readTimeoutCounter = 0; |
| } |
| |
| // let the client know we haven't timed out yet |
| if (writeTimeoutCounter >= TIMEOUT_TICKS && getProtocol() instanceof PlayProtocol) { |
| pingMessageId = random.nextInt(); |
| send(new PingMessage(pingMessageId)); |
| } |
| } |
| |
| //////////////////////////////////////////////////////////////////////////// |
| // Pipeline management |
| |
| public void setProtocol(ProtocolType protocol) { |
| getChannel().flush(); |
| |
| GlowProtocol proto = protocol.getProtocol(); |
| updatePipeline("codecs", new CodecsHandler(proto)); |
| super.setProtocol(proto); |
| } |
| |
| public void enableEncryption(SecretKey sharedSecret) { |
| updatePipeline("encryption", new EncryptionHandler(sharedSecret)); |
| } |
| |
| public void enableCompression(int threshold) { |
| send(new SetCompressionMessage(threshold)); |
| updatePipeline("compression", new CompressionHandler(threshold)); |
| } |
| |
| public void disableCompression() { |
| send(new SetCompressionMessage(-1)); |
| updatePipeline("compression", NoopHandler.INSTANCE); |
| } |
| |
| private void updatePipeline(String key, ChannelHandler handler) { |
| getChannel().pipeline().replace(key, key, handler); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////// |
| // Handler overrides |
| |
| @Override |
| public void onDisconnect() { |
| if (player == null) { |
| return; |
| } |
| |
| player.remove(); |
| |
| Message userListMessage = UserListItemMessage.removeOne(player.getUniqueId()); |
| for (GlowPlayer player : server.getOnlinePlayers()) { |
| if (player.canSee(this.player)) { |
| player.getSession().send(userListMessage); |
| } else { |
| player.stopHidingDisconnectedPlayer(this.player); |
| } |
| } |
| |
| GlowServer.logger.info(player.getName() + " [" + address + "] lost connection"); |
| |
| if (player.isSleeping()) { |
| player.leaveBed(false); |
| } |
| |
| final String text = EventFactory.onPlayerQuit(player).getQuitMessage(); |
| if (text != null && !text.isEmpty()) { |
| server.broadcastMessage(text); |
| } |
| |
| player = null; // in case we are disposed twice |
| } |
| |
| @Override |
| public void messageReceived(Message message) { |
| if (message instanceof AsyncableMessage && ((AsyncableMessage) message).isAsync()) { |
| // async messages get their handlers called immediately |
| super.messageReceived(message); |
| } else { |
| messageQueue.add(message); |
| } |
| } |
| |
| @Override |
| public void onInboundThrowable(Throwable t) { |
| if (t instanceof CodecException) { |
| // generated by the pipeline, not a network error |
| GlowServer.logger.log(Level.SEVERE, "Error in network input", t); |
| } else { |
| // probably a network-level error - consider the client gone |
| if (quitReason == null) { |
| quitReason = "read error: " + t; |
| } |
| getChannel().close(); |
| } |
| } |
| |
| @Override |
| public void onOutboundThrowable(Throwable t) { |
| if (t instanceof CodecException) { |
| // generated by the pipeline, not a network error |
| GlowServer.logger.log(Level.SEVERE, "Error in network output", t); |
| } else { |
| // probably a network-level error - consider the client gone |
| if (quitReason == null) { |
| quitReason = "write error: " + t; |
| } |
| getChannel().close(); |
| } |
| } |
| |
| @Override |
| public void onHandlerThrowable(Message message, MessageHandler<?, ?> handle, Throwable t) { |
| // can be safely logged and the connection maintained |
| GlowServer.logger.log(Level.SEVERE, "Error while handling " + message + " (handler: " + handle.getClass().getSimpleName() + ")", t); |
| } |
| |
| @Override |
| public String toString() { |
| if (player != null) { |
| return player.getName() + "[" + address + "]"; |
| } else { |
| return "[" + address + "]"; |
| } |
| } |
| } |