de.cubeisland.engine.core.webapi.ApiServer.java Source code

Java tutorial

Introduction

Here is the source code for de.cubeisland.engine.core.webapi.ApiServer.java

Source

/**
 * This file is part of CubeEngine.
 * CubeEngine is licensed under the GNU General Public License Version 3.
 *
 * CubeEngine is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * CubeEngine is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with CubeEngine.  If not, see <http://www.gnu.org/licenses/>.
 */
package de.cubeisland.engine.core.webapi;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import com.fasterxml.jackson.databind.node.ObjectNode;
import de.cubeisland.engine.core.Core;
import de.cubeisland.engine.core.logging.LoggingUtil;
import de.cubeisland.engine.core.module.Module;
import de.cubeisland.engine.core.permission.PermDefault;
import de.cubeisland.engine.core.util.StringUtils;
import de.cubeisland.engine.core.webapi.exception.ApiStartupException;
import de.cubeisland.engine.logscribe.Log;
import de.cubeisland.engine.logscribe.target.file.AsyncFileTarget;
import de.cubeisland.engine.logscribe.target.proxy.LogProxyTarget;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import org.apache.commons.lang.Validate;

import static de.cubeisland.engine.core.contract.Contract.expectNotNull;
import static java.util.Locale.ENGLISH;

/**
 * This class represents the API server and provides methods to configure and control it
 */
public class ApiServer {
    private final Core core;
    private final Log log;
    private final AtomicInteger maxContentLength = new AtomicInteger(1048576);
    private final AtomicBoolean compress = new AtomicBoolean(false);
    private final AtomicInteger compressionLevel = new AtomicInteger(9);
    private final AtomicInteger windowBits = new AtomicInteger(15);
    private final AtomicInteger memoryLevel = new AtomicInteger(9);
    private final AtomicReference<InetAddress> bindAddress = new AtomicReference<>(null);
    private final AtomicInteger port = new AtomicInteger(6561);
    private final AtomicReference<ServerBootstrap> bootstrap = new AtomicReference<>(null);
    private final AtomicReference<EventLoopGroup> eventLoopGroup = new AtomicReference<>(null);
    private final AtomicReference<Channel> channel = new AtomicReference<>(null);
    private final AtomicInteger maxThreads = new AtomicInteger(2);
    private final Set<String> disabledRoutes = new CopyOnWriteArraySet<>();
    private final AtomicBoolean enableWhitelist = new AtomicBoolean(false);
    private final Set<InetAddress> whitelist = new CopyOnWriteArraySet<>();
    private final AtomicBoolean enableBlacklist = new AtomicBoolean(false);
    private final Set<InetAddress> blacklist = new CopyOnWriteArraySet<>();
    private final AtomicBoolean enableAuthorizedList = new AtomicBoolean(false);
    private final Set<InetAddress> authorizedList = new CopyOnWriteArraySet<>();

    private final ConcurrentMap<String, ApiHandler> handlers = new ConcurrentHashMap<>();
    private final ConcurrentMap<String, Set<WebSocketRequestHandler>> subscriptions = new ConcurrentHashMap<>();
    private final AtomicInteger maxConnectionCount = new AtomicInteger(1);

    public ApiServer(Core core) {
        this.core = core;
        this.log = core.getLogFactory().getLog(Core.class, "WebAPI");
        this.log.addTarget(
                new AsyncFileTarget(LoggingUtil.getLogFile(core, "WebAPI"), LoggingUtil.getFileFormat(true, true),
                        true, LoggingUtil.getCycler(), core.getTaskManager().getThreadFactory()));
        this.log.addTarget(new LogProxyTarget(core.getLogFactory().getParent()));
        try {
            this.bindAddress.set(InetAddress.getLocalHost());
        } catch (UnknownHostException ignored) {
            this.log.warn("Failed to get the localhost!");
        }
    }

    public Log getLog() {
        return this.log;
    }

    public void configure(final ApiConfig config) {
        expectNotNull(config, "The config must not be null!");

        try {
            this.setBindAddress(config.network.address);
        } catch (UnknownHostException ignored) {
            this.log.warn("Failed to resolve the host {}, ignoring the value...");
        }
        this.setPort(config.network.port);
        this.setMaxThreads(config.network.maxThreads);
        this.setMaxConnectionCount(config.network.maxConnectionPerIp);

        this.setCompressionEnabled(config.compression.enable);
        this.setCompressionLevel(config.compression.level);
        this.setCompressionWindowBits(config.compression.windowBits);
        this.setCompressionMemoryLevel(config.compression.memoryLevel);

        this.setWhitelistEnabled(config.whitelist.enable);
        this.setWhitelist(config.whitelist.ips);

        this.setBlacklistEnabled(config.blacklist.enable);
        this.setBlacklist(config.blacklist.ips);

        this.setAuthorizedListEnabled(config.authorizedList.enable);
        this.setAuthorizedList(config.authorizedList.ips);
    }

    /**
     * Starts the server
     *
     * @return fluent interface
     */
    public ApiServer start() throws ApiStartupException {
        if (!this.isRunning()) {
            final ServerBootstrap serverBootstrap = new ServerBootstrap();

            try {
                this.eventLoopGroup.set(new NioEventLoopGroup(this.maxThreads.get(),
                        this.core.getTaskManager().getThreadFactory()));
                serverBootstrap.group(this.eventLoopGroup.get()).channel(NioServerSocketChannel.class)
                        .childHandler(new ApiServerInitializer(this.core, this))
                        .localAddress(this.bindAddress.get(), this.port.get());

                this.bootstrap.set(serverBootstrap);
                this.channel.set(serverBootstrap.bind().sync().channel());
            } catch (Exception e) {
                this.bootstrap.set(null);
                this.channel.set(null);
                this.eventLoopGroup.getAndSet(null).shutdownGracefully(2, 5, TimeUnit.SECONDS);
                throw new ApiStartupException("The API server failed to start!", e);
            }
        }
        return this;
    }

    /**
     * Stops the server
     *
     * @return fluent interface
     */
    public ApiServer stop() {
        if (this.isRunning()) {
            this.bootstrap.set(null);
            this.channel.set(null);
            this.eventLoopGroup.getAndSet(null).shutdownGracefully(2, 5, TimeUnit.SECONDS);
        }
        return this;
    }

    /**
     * Returns whether the server is running or not
     *
     * @return true if it is running
     */
    public boolean isRunning() {
        return (this.channel.get() != null && this.channel.get().isOpen());
    }

    public ApiHandler getApiHandler(String route) {
        if (route == null) {
            return null;
        }
        return this.handlers.get(route);
    }

    public void registerApiHandlers(final Module owner, final Object holder) {
        expectNotNull(holder, "The API holder must not be null!");

        for (Method method : holder.getClass().getDeclaredMethods()) {
            Action aAction = method.getAnnotation(Action.class);
            if (aAction != null) {
                String route = aAction.value();
                if (route.isEmpty()) {
                    route = StringUtils.deCamelCase(method.getName(), "/");
                }
                route = owner.getId() + "/" + route;
                route = HttpRequestHandler.normalizePath(route);
                de.cubeisland.engine.core.permission.Permission perm = null;
                if (aAction.needsAuth()) {
                    perm = owner.getBasePermission().childWildcard("webapi");
                    if (method.isAnnotationPresent(ApiPermission.class)) {
                        ApiPermission apiPerm = method.getAnnotation(ApiPermission.class);
                        if (apiPerm.value().isEmpty()) {
                            perm = perm.child(route, apiPerm.permDefault());
                        } else {
                            perm = perm.child(apiPerm.value(), apiPerm.permDefault());
                        }
                    } else {
                        perm = perm.child(route, PermDefault.DEFAULT);
                    }
                }
                LinkedHashMap<String, Class> params = new LinkedHashMap<>();
                Class<?>[] types = method.getParameterTypes();
                Annotation[][] paramAnnotations = method.getParameterAnnotations();
                for (int i = 1; i < types.length; i++) {
                    Class<?> type = types[i];
                    Value val = null;
                    for (Annotation annotation : paramAnnotations[i]) {
                        if (annotation instanceof Value) {
                            val = (Value) annotation;
                            break;
                        }
                    }
                    if (val == null) {
                        throw new IllegalArgumentException("Missing Value Annotation for Additional Parameters");
                    }
                    if (params.put(val.value(), type) != null) {
                        throw new IllegalArgumentException("Duplicate value in Value Annotation");
                    }
                }
                RequestMethod reqMethod = RequestMethod.GET;
                if (method.isAnnotationPresent(de.cubeisland.engine.core.webapi.Method.class)) {
                    reqMethod = method.getAnnotation(de.cubeisland.engine.core.webapi.Method.class).value();
                }
                this.handlers.put(route,
                        new ReflectedApiHandler(owner, route, perm, params, reqMethod, method, holder));
            }
        }
    }

    public void unregisterApiHandler(String route) {
        this.handlers.remove(route);
    }

    public void unregisterApiHandlers(Module module) {
        Iterator<Map.Entry<String, ApiHandler>> iter = this.handlers.entrySet().iterator();

        ApiHandler handler;
        while (iter.hasNext()) {
            handler = iter.next().getValue();
            if (handler.getModule() == module) {
                iter.remove();
            }
        }
    }

    public void unregisterApiHandlers(Object holder) {
        Iterator<Map.Entry<String, ApiHandler>> iter = this.handlers.entrySet().iterator();

        ApiHandler handler;
        while (iter.hasNext()) {
            handler = iter.next().getValue();
            if (handler instanceof ReflectedApiHandler) {
                if (((ReflectedApiHandler) handler).getHolder() == holder) {
                    iter.remove();
                }
            }
        }
    }

    public void unregisterApiHandlers() {
        this.handlers.clear();
    }

    public void setBindAddress(String address) throws UnknownHostException {
        this.setBindAddress(InetAddress.getByName(address));
    }

    /**
     * Sets the address the server will bind to on the next start
     *
     * @param address the address
     */
    public void setBindAddress(InetAddress address) {
        expectNotNull(address, "The address must not be null!");

        this.bindAddress.set(address);
    }

    /**
     * Returns the address the server is bound/will bind to
     *
     * @return the address
     */
    public InetAddress getBindAddress() {
        return this.bindAddress.get();
    }

    public InetAddress getBoundAddress() {
        if (this.isRunning()) {
            return ((InetSocketAddress) this.channel.get().localAddress()).getAddress();
        }
        return null;
    }

    public void setPort(short port) {
        this.port.set(port);
    }

    /**
     * Returns the port the server is/will be listening on
     *
     * @return the post
     */
    public int getPort() {
        return this.port.get();
    }

    public short getBoundPort() {
        if (this.isRunning()) {
            return (short) ((InetSocketAddress) this.channel.get().localAddress()).getPort();
        }
        return -1;
    }

    public void setMaxThreads(int maxThreads) {
        this.maxThreads.set(maxThreads);
    }

    public int getMaxThreads() {
        return this.maxThreads.get();
    }

    public void setMaxContentLength(int mcl) {
        this.maxContentLength.set(mcl);
    }

    /**
     * Returns the maximum content length the client may send
     *
     * @return the maximum content length
     */
    public int getMaxContentLength() {
        return this.maxContentLength.get();
    }

    public void setCompressionEnabled(boolean state) {
        this.compress.set(state);
    }

    public boolean isCompressionEnabled() {
        return this.compress.get();
    }

    public void setCompressionLevel(int level) {
        this.compressionLevel.set(Math.max(1, Math.min(9, level)));
    }

    public int getCompressionLevel() {
        return this.compressionLevel.get();
    }

    public void setCompressionWindowBits(int bits) {
        this.windowBits.set(Math.max(9, Math.min(15, bits)));
    }

    public int getCompressionMemoryLevel() {
        return this.memoryLevel.get();
    }

    public void setCompressionMemoryLevel(int level) {
        this.memoryLevel.set(Math.max(1, Math.min(9, level)));
    }

    public int getCompressionWindowBits() {
        return this.windowBits.get();
    }

    /**
     * Returns whether whitelisting is enabled
     *
     * @return true if enabled
     */
    public boolean isWhitelistEnabled() {
        return this.enableWhitelist.get();
    }

    /**
     * Sets the enabled state of the whitelisting
     *
     * @param state true to enable, false to disable
     */
    public void setWhitelistEnabled(boolean state) {
        this.enableWhitelist.set(state);
    }

    public void whitelistAddress(InetAddress address) {
        this.whitelist.add(address);
    }

    public void whitelistAddress(InetSocketAddress address) {
        this.whitelistAddress(address.getAddress());
    }

    public void unWhitelistAddress(InetAddress address) {
        this.whitelist.remove(address);
    }

    public void unWhitelistAddress(InetSocketAddress address) {
        this.unWhitelistAddress(address.getAddress());
    }

    public void setWhitelist(Set<InetAddress> newWhitelist) {
        expectNotNull(newWhitelist, "The whitelist must not be null!");
        Validate.noNullElements(newWhitelist, "The whitelist must not contain null values!");

        this.whitelist.clear();
        this.whitelist.addAll(newWhitelist);
    }

    /**
     * Checks whether an InetAddress is whitelisted
     *
     * @param ip the IP
     * @return true if it is
     */
    public boolean isWhitelisted(InetAddress ip) {
        return !this.enableWhitelist.get() || this.whitelist.contains(ip);
    }

    public boolean isAuthorized(InetAddress ip) {
        return !this.enableAuthorizedList.get() || this.authorizedList.contains(ip);
    }

    /**
     * Checks whether an InetSocketAddress is whitelisted
     *
     * @param ip the IP
     * @return true if it is
     */
    public boolean isWhitelisted(InetSocketAddress ip) {
        return this.isWhitelisted(ip.getAddress());
    }

    /**
     * Sets the enabled state of the blacklisting
     *
     * @param state true to enable, false to disable
     */
    public void setBlacklistEnabled(boolean state) {
        this.enableBlacklist.set(state);
    }

    public void blacklistAddress(InetAddress address) {
        this.blacklist.add(address);
    }

    public void blacklistAddress(InetSocketAddress address) {
        this.blacklistAddress(address.getAddress());
    }

    public void unBlacklistAddress(InetAddress address) {
        this.blacklist.remove(address);
    }

    public void unBlacklistAddress(InetSocketAddress address) {
        this.unBlacklistAddress(address.getAddress());
    }

    public void setBlacklist(Set<InetAddress> newBlacklist) {
        expectNotNull(newBlacklist, "The blacklist must not be null!");
        Validate.noNullElements(newBlacklist, "The blacklist must not contain null values!");

        this.blacklist.clear();
        this.blacklist.addAll(newBlacklist);
    }

    /**
     * Returns whether blacklisting is enabled
     *
     * @return true if it is
     */
    public boolean isBlacklistEnabled() {
        return this.enableBlacklist.get();
    }

    /**
     * Checks whether an InetSocketAddress is blacklisted
     *
     * @param ip the IP
     * @return true if it is
     */
    public boolean isBlacklisted(InetSocketAddress ip) {
        return this.isBlacklisted(ip.getAddress());
    }

    /**
     * Checks whether an InetAddress is blacklisted
     *
     * @param ip the IP
     * @return true if it is
     */
    public boolean isBlacklisted(InetAddress ip) {
        return this.enableBlacklist.get() && this.blacklist.contains(ip);
    }

    public void setAuthorizedListEnabled(boolean enable) {
        this.enableAuthorizedList.set(enable);
    }

    public void setAuthorizedList(Set<InetAddress> newAuthorizedlist) {
        expectNotNull(newAuthorizedlist, "The autorizedlist must not be null!");
        Validate.noNullElements(newAuthorizedlist, "The autorizedlist must not contain null values!");

        this.authorizedList.clear();
        this.authorizedList.addAll(newAuthorizedlist);
    }

    public boolean isRouteDisabled(String route) {
        return this.disabledRoutes.contains(route);
    }

    public void disableRoute(String route) {
        this.disabledRoutes.add(route);
    }

    public void reenableRoute(String route) {
        this.disabledRoutes.remove(route);
    }

    public void reenableRoutes(String... routes) {
        for (String route : routes) {
            this.reenableRoute(route);
        }
    }

    public boolean isAddressAccepted(InetAddress address) {
        return this.isWhitelisted(address) && !this.isBlacklisted(address);
    }

    public void subscribe(String event, WebSocketRequestHandler requestHandler) {
        expectNotNull(event, "The event name must not be null!");
        expectNotNull(requestHandler, "The request handler must not be null!");
        event = event.toLowerCase(ENGLISH);

        Set<WebSocketRequestHandler> subscribedHandlers = this.subscriptions.get(event);
        if (subscribedHandlers == null) {
            this.subscriptions.put(event, subscribedHandlers = new CopyOnWriteArraySet<>());
        }
        subscribedHandlers.add(requestHandler);
    }

    public void unsubscribe(String event, WebSocketRequestHandler requestHandler) {
        expectNotNull(event, "The event name must not be null!");
        expectNotNull(requestHandler, "The request handler must not be null!");
        event = event.toLowerCase(ENGLISH);

        Set<WebSocketRequestHandler> subscribedHandlers = this.subscriptions.get(event);
        if (subscribedHandlers != null) {
            subscribedHandlers.remove(requestHandler);
            if (subscribedHandlers.isEmpty()) {
                this.subscriptions.remove(event);
            }
        }
    }

    public void unsubscribe(String event) {
        expectNotNull(event, "The event name must not be null!");
        event = event.toLowerCase(ENGLISH);

        this.subscriptions.remove(event);
    }

    public void unsubscribe(WebSocketRequestHandler handler) {
        expectNotNull(handler, "The event name must not be null!");

        Iterator<Map.Entry<String, Set<WebSocketRequestHandler>>> iter = this.subscriptions.entrySet().iterator();

        Set<WebSocketRequestHandler> handlers;
        while (iter.hasNext()) {
            handlers = iter.next().getValue();
            handlers.remove(handler);
            if (handlers.isEmpty()) {
                iter.remove();
            }
        }
    }

    public void fireEvent(String event, ObjectNode data) {
        expectNotNull(event, "The event name must not be null!");
        event = event.toLowerCase(ENGLISH);

        Set<WebSocketRequestHandler> subscribedHandlers = this.subscriptions.get(event);
        if (subscribedHandlers != null) {
            for (WebSocketRequestHandler handler : subscribedHandlers) {
                handler.handleEvent(event, data);
            }
        }
    }

    public void setMaxConnectionCount(int maxConnectionCount) {
        this.maxConnectionCount.set(maxConnectionCount);
    }

    public int getMaxConnectionCount() {
        return maxConnectionCount.get();
    }
}