/****************************************************************************** * Copyright © 2013-2016 The Nxt Core Developers. * * * * See the AUTHORS.txt, DEVELOPER-AGREEMENT.txt and LICENSE.txt files at * * the top-level directory of this distribution for the individual copyright * * holder information and the developer policies on copyright and licensing. * * * * Unless otherwise agreed in a custom licensing agreement, no part of the * * Nxt software, including this file, may be copied, modified, propagated, * * or distributed except according to the terms contained in the LICENSE.txt * * file. * * * * Removal or modification of this copyright notice is prohibited. * * * ******************************************************************************/ package nxt.http; import nxt.Constants; import nxt.Nxt; import nxt.util.Logger; import nxt.util.ThreadPool; import nxt.util.UPnP; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.SecureRequestCustomizer; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.SslConnectionFactory; import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.server.handler.DefaultHandler; import org.eclipse.jetty.server.handler.HandlerList; import org.eclipse.jetty.server.handler.ResourceHandler; import org.eclipse.jetty.server.handler.gzip.GzipHandler; import org.eclipse.jetty.servlet.DefaultServlet; import org.eclipse.jetty.servlet.FilterHolder; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.servlets.CrossOriginFilter; import org.eclipse.jetty.util.ssl.SslContextFactory; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.MultipartConfigElement; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.math.BigInteger; import java.net.Inet4Address; import java.net.InetAddress; import java.net.URI; import java.net.URISyntaxException; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import static nxt.http.JSONResponses.INCORRECT_ADMIN_PASSWORD; import static nxt.http.JSONResponses.NO_PASSWORD_IN_CONFIG; public final class API { public static final int TESTNET_API_PORT = 6876; public static final int TESTNET_API_SSLPORT = 6877; public static final int openAPIPort; public static final int openAPISSLPort; private static final Set<String> allowedBotHosts; private static final List<NetworkAddress> allowedBotNets; static final String adminPassword = Nxt.getStringProperty("nxt.adminPassword", "", true); static final boolean disableAdminPassword; static final int maxRecords = Nxt.getIntProperty("nxt.maxAPIRecords"); static final boolean enableAPIUPnP = Nxt.getBooleanProperty("nxt.enableAPIUPnP"); private static final Server apiServer; private static URI browserUri; static { List<String> allowedBotHostsList = Nxt.getStringListProperty("nxt.allowedBotHosts"); if (! allowedBotHostsList.contains("*")) { Set<String> hosts = new HashSet<>(); List<NetworkAddress> nets = new ArrayList<>(); for (String host : allowedBotHostsList) { if (host.contains("/")) { try { nets.add(new NetworkAddress(host)); } catch (UnknownHostException e) { Logger.logErrorMessage("Unknown network " + host, e); throw new RuntimeException(e.toString(), e); } } else { hosts.add(host); } } allowedBotHosts = Collections.unmodifiableSet(hosts); allowedBotNets = Collections.unmodifiableList(nets); } else { allowedBotHosts = null; allowedBotNets = null; } boolean enableAPIServer = Nxt.getBooleanProperty("nxt.enableAPIServer"); if (enableAPIServer) { final int port = Constants.isTestnet ? TESTNET_API_PORT : Nxt.getIntProperty("nxt.apiServerPort"); final int sslPort = Constants.isTestnet ? TESTNET_API_SSLPORT : Nxt.getIntProperty("nxt.apiServerSSLPort"); final String host = Nxt.getStringProperty("nxt.apiServerHost"); disableAdminPassword = Nxt.getBooleanProperty("nxt.disableAdminPassword") || ("127.0.0.1".equals(host) && adminPassword.isEmpty()); apiServer = new Server(); ServerConnector connector; boolean enableSSL = Nxt.getBooleanProperty("nxt.apiSSL"); // // Create the HTTP connector // if (!enableSSL || port != sslPort) { connector = new ServerConnector(apiServer); connector.setPort(port); connector.setHost(host); connector.setIdleTimeout(Nxt.getIntProperty("nxt.apiServerIdleTimeout")); connector.setReuseAddress(true); apiServer.addConnector(connector); Logger.logMessage("API server using HTTP port " + port); } // // Create the HTTPS connector // if (enableSSL) { HttpConfiguration https_config = new HttpConfiguration(); https_config.setSecureScheme("https"); https_config.setSecurePort(sslPort); https_config.addCustomizer(new SecureRequestCustomizer()); SslContextFactory sslContextFactory = new SslContextFactory(); sslContextFactory.setKeyStorePath(Nxt.getStringProperty("nxt.keyStorePath")); sslContextFactory.setKeyStorePassword(Nxt.getStringProperty("nxt.keyStorePassword", null, true)); sslContextFactory.setExcludeCipherSuites("SSL_RSA_WITH_DES_CBC_SHA", "SSL_DHE_RSA_WITH_DES_CBC_SHA", "SSL_DHE_DSS_WITH_DES_CBC_SHA", "SSL_RSA_EXPORT_WITH_RC4_40_MD5", "SSL_RSA_EXPORT_WITH_DES40_CBC_SHA", "SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA", "SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA"); sslContextFactory.setExcludeProtocols("SSLv3"); connector = new ServerConnector(apiServer, new SslConnectionFactory(sslContextFactory, "http/1.1"), new HttpConnectionFactory(https_config)); connector.setPort(sslPort); connector.setHost(host); connector.setIdleTimeout(Nxt.getIntProperty("nxt.apiServerIdleTimeout")); connector.setReuseAddress(true); apiServer.addConnector(connector); Logger.logMessage("API server using HTTPS port " + sslPort); } try { browserUri = new URI(enableSSL ? "https" : "http", null, "localhost", enableSSL ? sslPort : port, "/index.html", null, null); } catch (URISyntaxException e) { Logger.logInfoMessage("Cannot resolve browser URI", e); } openAPIPort = "0.0.0.0".equals(host) && allowedBotHosts == null && (!enableSSL || port != sslPort) ? port : 0; openAPISSLPort = "0.0.0.0".equals(host) && allowedBotHosts == null && enableSSL ? sslPort : 0; HandlerList apiHandlers = new HandlerList(); ServletContextHandler apiHandler = new ServletContextHandler(); String apiResourceBase = Nxt.getStringProperty("nxt.apiResourceBase"); if (apiResourceBase != null) { ServletHolder defaultServletHolder = new ServletHolder(new DefaultServlet()); defaultServletHolder.setInitParameter("dirAllowed", "false"); defaultServletHolder.setInitParameter("resourceBase", apiResourceBase); defaultServletHolder.setInitParameter("welcomeServlets", "true"); defaultServletHolder.setInitParameter("redirectWelcome", "true"); defaultServletHolder.setInitParameter("gzip", "true"); defaultServletHolder.setInitParameter("etags", "true"); apiHandler.addServlet(defaultServletHolder, "/*"); apiHandler.setWelcomeFiles(new String[]{Nxt.getStringProperty("nxt.apiWelcomeFile")}); } String javadocResourceBase = Nxt.getStringProperty("nxt.javadocResourceBase"); if (javadocResourceBase != null) { ContextHandler contextHandler = new ContextHandler("/doc"); ResourceHandler docFileHandler = new ResourceHandler(); docFileHandler.setDirectoriesListed(false); docFileHandler.setWelcomeFiles(new String[]{"index.html"}); docFileHandler.setResourceBase(javadocResourceBase); contextHandler.setHandler(docFileHandler); apiHandlers.addHandler(contextHandler); } ServletHolder servletHolder = apiHandler.addServlet(APIServlet.class, "/nxt"); servletHolder.getRegistration().setMultipartConfig(new MultipartConfigElement( null, Math.max(Nxt.getIntProperty("nxt.maxUploadFileSize"), Constants.MAX_TAGGED_DATA_DATA_LENGTH), -1L, 0)); GzipHandler gzipHandler = new GzipHandler(); if (!Nxt.getBooleanProperty("nxt.enableAPIServerGZIPFilter")) { gzipHandler.setExcludedPaths("/nxt"); } gzipHandler.setIncludedMethods("GET", "POST"); gzipHandler.setMinGzipSize(nxt.peer.Peers.MIN_COMPRESS_SIZE); apiHandler.setGzipHandler(gzipHandler); apiHandler.addServlet(APITestServlet.class, "/test"); apiHandler.addServlet(DbShellServlet.class, "/dbshell"); if (Nxt.getBooleanProperty("nxt.apiServerCORS")) { FilterHolder filterHolder = apiHandler.addFilter(CrossOriginFilter.class, "/*", null); filterHolder.setInitParameter("allowedHeaders", "*"); filterHolder.setAsyncSupported(true); } if (Nxt.getBooleanProperty("nxt.apiFrameOptionsSameOrigin")) { FilterHolder filterHolder = apiHandler.addFilter(XFrameOptionsFilter.class, "/*", null); filterHolder.setAsyncSupported(true); } apiHandlers.addHandler(apiHandler); apiHandlers.addHandler(new DefaultHandler()); apiServer.setHandler(apiHandlers); apiServer.setStopAtShutdown(true); ThreadPool.runBeforeStart(() -> { try { if (enableAPIUPnP) { Connector[] apiConnectors = apiServer.getConnectors(); for (Connector apiConnector : apiConnectors) { if (apiConnector instanceof ServerConnector) UPnP.addPort(((ServerConnector)apiConnector).getPort()); } } apiServer.start(); Logger.logMessage("Started API server at " + host + ":" + port + (enableSSL && port != sslPort ? ", " + host + ":" + sslPort : "")); } catch (Exception e) { Logger.logErrorMessage("Failed to start API server", e); throw new RuntimeException(e.toString(), e); } }, true); } else { apiServer = null; disableAdminPassword = false; openAPIPort = 0; openAPISSLPort = 0; Logger.logMessage("API server not enabled"); } } public static void init() {} public static void shutdown() { if (apiServer != null) { try { apiServer.stop(); if (enableAPIUPnP) { Connector[] apiConnectors = apiServer.getConnectors(); for (Connector apiConnector : apiConnectors) { if (apiConnector instanceof ServerConnector) UPnP.deletePort(((ServerConnector)apiConnector).getPort()); } } } catch (Exception e) { Logger.logShutdownMessage("Failed to stop API server", e); } } } static void verifyPassword(HttpServletRequest req) throws ParameterException { if (API.disableAdminPassword) { return; } if (API.adminPassword.isEmpty()) { throw new ParameterException(NO_PASSWORD_IN_CONFIG); } else if (!API.adminPassword.equals(req.getParameter("adminPassword"))) { Logger.logWarningMessage("Incorrect adminPassword"); throw new ParameterException(INCORRECT_ADMIN_PASSWORD); } } static boolean checkPassword(HttpServletRequest req) { return (API.disableAdminPassword || (!API.adminPassword.isEmpty() && API.adminPassword.equals(req.getParameter("adminPassword")))); } static boolean isAllowed(String remoteHost) { if (API.allowedBotHosts == null || API.allowedBotHosts.contains(remoteHost)) { return true; } try { BigInteger hostAddressToCheck = new BigInteger(InetAddress.getByName(remoteHost).getAddress()); for (NetworkAddress network : API.allowedBotNets) { if (network.contains(hostAddressToCheck)) { return true; } } } catch (UnknownHostException e) { // can't resolve, disallow Logger.logMessage("Unknown remote host " + remoteHost); } return false; } private static class NetworkAddress { private BigInteger netAddress; private BigInteger netMask; private NetworkAddress(String address) throws UnknownHostException { String[] addressParts = address.split("/"); if (addressParts.length == 2) { InetAddress targetHostAddress = InetAddress.getByName(addressParts[0]); byte[] srcBytes = targetHostAddress.getAddress(); netAddress = new BigInteger(1, srcBytes); int maskBitLength = Integer.valueOf(addressParts[1]); int addressBitLength = (targetHostAddress instanceof Inet4Address) ? 32 : 128; netMask = BigInteger.ZERO .setBit(addressBitLength) .subtract(BigInteger.ONE) .subtract(BigInteger.ZERO.setBit(addressBitLength - maskBitLength).subtract(BigInteger.ONE)); } else { throw new IllegalArgumentException("Invalid address: " + address); } } private boolean contains(BigInteger hostAddressToCheck) { return hostAddressToCheck.and(netMask).equals(netAddress); } } public static final class XFrameOptionsFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { ((HttpServletResponse) response).setHeader("X-FRAME-OPTIONS", "SAMEORIGIN"); chain.doFilter(request, response); } @Override public void destroy() { } } public static URI getBrowserUri() { return browserUri; } private API() {} // never }