001/*
002 * Trident - A Multithreaded Server Alternative
003 * Copyright 2014 The TridentSDK Team
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *    http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package net.tridentsdk.server.netty;
019
020import io.netty.buffer.ByteBuf;
021import io.netty.buffer.Unpooled;
022import io.netty.channel.Channel;
023import io.netty.channel.ChannelHandlerAdapter;
024import io.netty.channel.ChannelHandlerContext;
025import net.tridentsdk.docs.InternalUseOnly;
026import net.tridentsdk.entity.living.Player;
027import net.tridentsdk.server.netty.packet.Packet;
028import net.tridentsdk.server.netty.protocol.Protocol;
029import net.tridentsdk.server.packets.login.LoginHandler;
030import net.tridentsdk.server.packets.login.PacketLoginOutDisconnect;
031import net.tridentsdk.server.packets.login.PacketLoginOutSetCompression;
032import net.tridentsdk.server.packets.play.out.PacketPlayOutDisconnect;
033import net.tridentsdk.server.player.PlayerConnection;
034import net.tridentsdk.server.player.TridentPlayer;
035import net.tridentsdk.util.TridentLogger;
036
037import javax.crypto.Cipher;
038import javax.crypto.SecretKey;
039import javax.crypto.spec.IvParameterSpec;
040import javax.crypto.spec.SecretKeySpec;
041import java.net.InetSocketAddress;
042import java.nio.ByteBuffer;
043import java.security.InvalidAlgorithmParameterException;
044import java.security.InvalidKeyException;
045import java.security.KeyPair;
046import java.security.SecureRandom;
047import java.util.UUID;
048import java.util.concurrent.ConcurrentHashMap;
049import java.util.concurrent.ConcurrentMap;
050
051/**
052 * Handles the connection of a client upon joining
053 *
054 * @author The TridentSDK Team
055 */
056public class ClientConnection {
057    /**
058     * Map of client connections registered
059     */
060    protected static final ConcurrentMap<InetSocketAddress, ClientConnection> clientData =
061            new ConcurrentHashMap<>();
062
063    /**
064     * Random for generating the verification token
065     */
066    protected static final SecureRandom SR = new SecureRandom();
067    /**
068     * The RSA cipher used to encrypt client data
069     */
070    protected final ThreadLocal<Cipher> encryptCipher = new ThreadLocal<Cipher>() {
071        @Override
072        protected Cipher initialValue() {
073            return getCipher();
074        }
075    };
076    protected final ThreadLocal<Cipher> decryptCipher = new ThreadLocal<Cipher>() {
077        @Override
078        protected Cipher initialValue() {
079            return getCipher();
080        }
081    };
082
083    /* Network fields */
084    private final Object BARRIER;
085
086    /* Encryption and client data fields */
087    /**
088     * The client's connection address
089     */
090    protected InetSocketAddress address;
091    /**
092     * The data channel
093     */
094    protected Channel channel;
095    /**
096     * The login key pair
097     */
098    protected volatile KeyPair loginKeyPair;
099    /**
100     * The client stage during login
101     */
102    protected volatile Protocol.ClientStage stage;
103    /**
104     * Whether or not encryption is enabled for the client
105     */
106    protected volatile boolean encryptionEnabled;
107    /**
108     * The secret key shared between the client and server
109     */
110    protected volatile SecretKey sharedSecret;
111    /**
112     * The verification token
113     */
114    protected volatile byte[] verificationToken; // DO NOT WRITE INDIVIDUAL ELEMENTS TO IT. Consult AgentTroll
115    /**
116     * Whether or not encryption is enabled
117     */
118    protected volatile boolean compressionEnabled = false;
119    private volatile UUID uuid;
120    private volatile IvParameterSpec ivSpec;
121
122    /**
123     * Creates a new connection handler for the joining channel stream
124     */
125    protected ClientConnection(Channel channel) {
126        this.address = (InetSocketAddress) channel.remoteAddress();
127        this.channel = channel;
128        BARRIER = new Object();
129
130        this.encryptionEnabled = false;
131        this.stage = Protocol.ClientStage.HANDSHAKE;
132        channel.closeFuture().addListener(future -> logout());
133        channel.pipeline().addLast(new ChannelHandlerAdapter() {
134            @Override
135            public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
136                if (cause.getMessage().toLowerCase().contains("connection reset")) {
137                    logout();
138                    return;
139                }
140
141                super.exceptionCaught(ctx, cause);
142            }
143        });
144    }
145
146    protected ClientConnection() {
147        BARRIER = new Object();
148    }
149
150    private static Cipher getCipher() {
151        try {
152            return Cipher.getInstance("AES/CFB8/NoPadding");
153        } catch (Exception ex) {
154            TridentLogger.get().error(ex);
155        }
156
157        return null;
158    }
159
160    /**
161     * Checks if an IP address is logged into the server
162     *
163     * @param address the address to check if online
164     * @return {@code true} if the IP is on the server, {@code false} if not
165     */
166    public static boolean isLoggedIn(InetSocketAddress address) {
167        return clientData.keySet().contains(address);
168    }
169
170    /**
171     * Gets the connection by the IP address
172     *
173     * @param address the IP to lookup the connection handler
174     * @return the instance of the client handler associated with the IP, or {@code null} if not registered
175     */
176    public static ClientConnection getConnection(InetSocketAddress address) {
177        return clientData.get(address);
178    }
179
180    /**
181     * Gets the connection of a channel handler context
182     *
183     * @param chx the context of which to find the client from
184     * @return the client connection given the handler context, or {@code null} if not registered
185     */
186    public static ClientConnection connection(ChannelHandlerContext chx) {
187        return getConnection((InetSocketAddress) chx.channel().remoteAddress());
188    }
189
190    /**
191     * Registers the client channel with a protocol connection wrapper
192     *
193     * @param channel the channel of which the player is connected by
194     * @return the client connection that was registered
195     */
196    public static ClientConnection registerConnection(final Channel channel) {
197        return clientData.computeIfAbsent((InetSocketAddress) channel.remoteAddress(),
198                (k) -> new ClientConnection(channel));
199    }
200
201    /**
202     * Sends protocol data through the client stream
203     *
204     * @param packet the packet to send, encoded and written to the stream
205     */
206    public void sendPacket(Packet packet) {
207        // Create new ByteBuf
208        ByteBuf buffer = this.channel.alloc().buffer();
209
210        Codec.writeVarInt32(buffer, packet.id());
211        packet.encode(buffer);
212        TridentLogger.get().debug(packet.getClass().getSimpleName() + " sent");
213
214        // Write the packet and flush it
215        this.channel.write(buffer);
216        this.channel.flush();
217
218        if (packet instanceof PacketPlayOutDisconnect
219                || packet instanceof PacketLoginOutDisconnect) {
220            logout();
221        }
222    }
223
224    /**
225     * Encrypts the given {@code byte} data
226     *
227     * @param data the data to encrypt
228     * @return the encrypted data
229     * @throws Exception if something wrong occurs
230     */
231    public ByteBuf encrypt(ByteBuf data) throws Exception {
232        ByteBuffer out = ByteBuffer.allocate(data.readableBytes());
233
234        encryptCipher.get().update(data.nioBuffer(), out);
235        out.flip();
236
237        return Unpooled.wrappedBuffer(out);
238    }
239
240    /**
241     * Decrypts the given {@code byte} encryption data
242     *
243     * @param data the data to decrypt
244     * @return the decrypted data
245     * @throws Exception if something wrong occurs
246     */
247    public ByteBuf decrypt(ByteBuf data) throws Exception {
248        ByteBuffer out = ByteBuffer.allocate(data.readableBytes());
249
250        decryptCipher.get().update(data.nioBuffer(), out);
251        out.flip();
252
253        return Unpooled.wrappedBuffer(out);
254    }
255
256    /**
257     * Generates the client token and stores it in the {@link #verificationToken}
258     */
259    public void generateToken() {
260        byte[] localToken = new byte[4];
261        SR.nextBytes(localToken);
262        this.verificationToken = localToken;
263    }
264
265    /**
266     * Enables client data encryption
267     *
268     * @param secret the client secret to encrypt data with
269     */
270    public void enableEncryption(byte... secret) {
271        //Makes sure the secret is only set once
272        if (!this.encryptionEnabled) {
273            this.sharedSecret = new SecretKeySpec(secret, "AES");
274            this.ivSpec = new IvParameterSpec(this.sharedSecret.getEncoded());
275            this.encryptionEnabled = true;
276
277            try {
278                encryptCipher.get().init(Cipher.ENCRYPT_MODE, sharedSecret, ivSpec);
279                decryptCipher.get().init(Cipher.DECRYPT_MODE, sharedSecret, ivSpec);
280            } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
281                TridentLogger.get().error(e);
282            }
283        }
284    }
285
286    /**
287     * Allows compression on the server and client
288     */
289    public void enableCompression() {
290        if (compressionEnabled) {
291            TridentLogger.get().error(new UnsupportedOperationException("Compression is already enabled!"));
292        }
293
294        if (stage != Protocol.ClientStage.LOGIN) {
295            TridentLogger.get().error(new UnsupportedOperationException());
296            return;
297        }
298
299        sendPacket(new PacketLoginOutSetCompression());
300        compressionEnabled = true;
301    }
302
303    /**
304     * Sets the UUID of the connection
305     *
306     * @param uuid the uuid of the connection
307     */
308    @InternalUseOnly
309    public void setUuid(UUID uuid) {
310        this.uuid = uuid;
311    }
312
313    /**
314     * Gets the channel context for the connection stream
315     *
316     * @return the netty channel wrapped by the handler
317     */
318    public Channel channel() {
319        return this.channel;
320    }
321
322    /**
323     * The IP address of the client handled by this connection wrapper
324     *
325     * @return the handled IP address
326     */
327    public InetSocketAddress address() {
328        return this.address;
329    }
330
331    /**
332     * Gets the current state of the connection
333     *
334     * @return the current state of the protocol for the client
335     */
336    public Protocol.ClientStage stage() {
337        return this.stage;
338    }
339
340    /**
341     * Sets the client state, should only be used by the ClientConnectionHandler
342     *
343     * @param stage the state to set the client to
344     */
345    public void setStage(Protocol.ClientStage stage) {
346        this.stage = stage;
347    }
348
349    /**
350     * Gets the client verification token
351     *
352     * @return the token of which to verify the client
353     */
354    public byte[] verificationToken() {
355        return this.verificationToken;
356    }
357
358    /**
359     * Whether or not encryption is enabled
360     *
361     * @return {@code true} if encryption is enabled, {@code false} if it is not
362     */
363    public boolean isEncryptionEnabled() {
364        return this.encryptionEnabled;
365    }
366
367    public boolean isCompressionEnabled() {
368        return this.compressionEnabled;
369    }
370
371    /**
372     * Gets the key pair for client login
373     *
374     * @return the {@link java.security.KeyPair} for the client
375     */
376    public KeyPair loginKeyPair() {
377        return this.loginKeyPair;
378    }
379
380    /**
381     * Sets the client login key pair
382     *
383     * @param keyPair the key pair used by the client
384     */
385    public void setLoginKeyPair(KeyPair keyPair) {
386        this.loginKeyPair = keyPair;
387    }
388
389    /**
390     * The protocol login encryption secret
391     *
392     * @return the {@link javax.crypto.SecretKey} shared between the server and client
393     */
394    public SecretKey sharedSecret() {
395        return this.sharedSecret;
396    }
397
398    /**
399     * Removes the client's server side client handler
400     */
401    public void logout() {
402        // Don't change the order of this, it is important for thread-safety
403        ClientConnection connection = clientData.remove(this.address);
404        LoginHandler.getInstance().finish(address()); // In case they errored while logging in
405
406        if (connection == null) return;
407
408        Player p = null;
409        if (this instanceof PlayerConnection) {
410            p = ((PlayerConnection) this).player();
411        } else if (uuid != null) {
412            p = TridentPlayer.getPlayer(uuid);
413        }
414
415        if (p == null) {
416            return;
417        }
418
419        this.channel.close();
420
421        p.remove();
422    }
423}