Java tutorial
/* * * Copyright (c) 2015 University of Massachusetts * * Licensed under the Apache License, Version 2.0 (the "License"); you * may not use this file except in compliance with the License. You * may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or * implied. See the License for the specific language governing * permissions and limitations under the License. * * Initial developer(s): Westy * */ package edu.umass.cs.gnsserver.httpserver; /** * * @author westy */ import com.sun.net.httpserver.Headers; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; import com.sun.net.httpserver.HttpsExchange; import edu.umass.cs.gnsclient.client.GNSClient; import edu.umass.cs.gnsclient.client.GNSClientConfig; import edu.umass.cs.gnscommon.CommandType; import edu.umass.cs.gnsserver.main.GNSConfig; import java.io.IOException; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.InetSocketAddress; import java.net.URI; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.concurrent.Executors; import edu.umass.cs.gnscommon.ResponseCode; import static edu.umass.cs.gnsserver.httpserver.Defs.QUERYPREFIX; import edu.umass.cs.gnsserver.gnsapp.clientCommandProcessor.ClientRequestHandlerInterface; import edu.umass.cs.gnsserver.gnsapp.clientCommandProcessor.commandSupport.CommandHandler; import edu.umass.cs.gnsserver.gnsapp.clientCommandProcessor.commands.CommandModule; import edu.umass.cs.gnsserver.gnsapp.clientCommandProcessor.commands.AbstractCommand; import edu.umass.cs.gnscommon.exceptions.client.ClientException; import edu.umass.cs.gnscommon.exceptions.server.InternalRequestException; import edu.umass.cs.gnscommon.packets.CommandPacket; import edu.umass.cs.gnscommon.utils.Base64; import edu.umass.cs.gnscommon.utils.CanonicalJSON; import edu.umass.cs.gnscommon.utils.Format; import edu.umass.cs.gnsserver.gnsapp.clientCommandProcessor.commandSupport.CommandResponse; import edu.umass.cs.gnsserver.main.GNSConfig.GNSC; import edu.umass.cs.gnsserver.utils.Util; import edu.umass.cs.nio.JSONPacket; import edu.umass.cs.reconfiguration.ReconfigurationConfig; import edu.umass.cs.utils.Config; import java.util.Date; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.lang.time.DurationFormatUtils; import org.json.JSONException; import org.json.JSONObject; import edu.umass.cs.gnscommon.GNSProtocol; import java.io.UnsupportedEncodingException; /** * * * @author westy */ public class GNSHttpServer { /** * */ protected static final String GNS_PATH = Config.getGlobalString(GNSConfig.GNSC.HTTP_SERVER_GNS_URL_PATH); private HttpServer httpServer = null; // handles command processing private final CommandModule commandModule; // newer handles command processing private GNSClient client = null; /** * */ protected final ClientRequestHandlerInterface requestHandler; private final Date serverStartDate = new Date(); private final static Logger LOGGER = Logger.getLogger(GNSHttpServer.class.getName()); /** * * @param port * @param requestHandler */ public GNSHttpServer(int port, ClientRequestHandlerInterface requestHandler) { this.commandModule = new CommandModule(); this.requestHandler = requestHandler; try { this.client = new GNSClient() { @Override public String getLabel() { return GNSHttpServer.class.getSimpleName(); } }; } catch (IOException e) { LOGGER.log(Level.SEVERE, "Unable to start GNS client:{0}", e.getMessage()); } runServer(port); } /** * Start the server. * * @param startingPort */ public final void runServer(int startingPort) { int cnt = 0; do { // Find the first port after starting port that actually works. // Usually if 8080 is busy we can get 8081. if (tryPort(startingPort + cnt)) { break; } edu.umass.cs.utils.Util.suicide(GNSConfig.getLogger(), "Unable to start GNS HTTP server; exiting"); } while (cnt++ < 100); } /** * Stop everything. */ public void stop() { if (httpServer != null) { httpServer.stop(0); } } /** * Try to start the http server at the port. * * @param port * @return true if it was started */ public boolean tryPort(int port) { try { InetSocketAddress addr = new InetSocketAddress(port); httpServer = HttpServer.create(addr, 0); httpServer.createContext("/", new EchoHttpHandler()); httpServer.createContext("/" + GNS_PATH, new DefaultHttpHandler()); httpServer.setExecutor(Executors.newCachedThreadPool()); httpServer.start(); // Need to do this for the places where we expose the insecure http service to the user requestHandler.setHttpServerPort(port); LOGGER.log(Level.INFO, "HTTP server is listening on port {0}", port); return true; } catch (IOException e) { LOGGER.log(Level.FINE, "HTTP server failed to start on port {0} due to {1}", new Object[] { port, e.getMessage() }); return false; } } /** * The default handler. */ protected class DefaultHttpHandler implements HttpHandler { /** * * @param exchange */ @Override public void handle(HttpExchange exchange) { try { String requestMethod = exchange.getRequestMethod(); if (requestMethod.equalsIgnoreCase("GET")) { Headers requestHeaders = exchange.getRequestHeaders(); String host = requestHeaders.getFirst("Host"); Headers responseHeaders = exchange.getResponseHeaders(); responseHeaders.set("Content-Type", "text/plain"); if (Config.getGlobalBoolean(GNSClientConfig.GNSCC.ENABLE_CROSS_ORIGIN_REQUESTS)) { responseHeaders.set("Access-Control-Allow-Origin", "*"); } exchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, 0); try (OutputStream responseBody = exchange.getResponseBody()) { URI uri = exchange.getRequestURI(); LOGGER.log(Level.FINE, "HTTP SERVER REQUEST FROM {0}: {1}", new Object[] { exchange.getRemoteAddress().getHostName(), uri.toString() }); String path = uri.getPath(); String query = uri.getQuery() != null ? uri.getQuery() : ""; // stupidly it returns null for empty query String commandName = path.replaceFirst("/" + GNS_PATH + "/", ""); CommandResponse response; if (!commandName.isEmpty()) { LOGGER.log(Level.FINE, "Action: {0} Query:{1}", new Object[] { commandName, query }); boolean secureServer = exchange instanceof HttpsExchange; response = processQuery(host, commandName, query, secureServer); } else { response = new CommandResponse(ResponseCode.OPERATION_NOT_SUPPORTED, GNSProtocol.BAD_RESPONSE.toString() + " " + GNSProtocol.OPERATION_NOT_SUPPORTED.toString() + " Don't understand " + commandName + " " + query); } LOGGER.log(Level.FINER, "Response: {0}", response); // FIXME: This totally ignores the error code. responseBody.write(response.getReturnValue().getBytes()); } } } catch (Exception e) { LOGGER.log(Level.SEVERE, "Error: {0}", e.getMessage()); e.printStackTrace(); try { String response = GNSProtocol.BAD_RESPONSE.toString() + " " + GNSProtocol.QUERY_PROCESSING_ERROR.toString() + " " + e; try (OutputStream responseBody = exchange.getResponseBody()) { responseBody.write(response.getBytes()); } } catch (Exception f) { // at this point screw it } } } } /* * Process queries for the http service. Converts the URI of e the HTTP query into * the JSON Object format that is used by the CommandModeule class, then finds * executes the matching command. * * @throws InternalRequestException */ private CommandResponse processQuery(String host, String commandName, String queryString, boolean secureServer) throws InternalRequestException { // Convert the URI into a JSONObject, stuffing in some extra relevant fields like // the signature, and the message signed. try { // Note that the commandName is not part of the queryString string here so // it doesn't end up in the jsonCommand. Also see below where we put the // command integer into the jsonCommand. JSONObject jsonCommand = Util.parseURIQueryStringIntoJSONObject(queryString); // If the signature exists it is Base64 encoded so decode it now. if (jsonCommand.has(GNSProtocol.SIGNATURE.toString())) { jsonCommand.put(GNSProtocol.SIGNATURE.toString(), new String(Base64.decode(jsonCommand.getString(GNSProtocol.SIGNATURE.toString())), GNSProtocol.CHARSET.toString())); } // getCommandForHttp allows for "dump" as well as "Dump" CommandType commandType = CommandType.getCommandForHttp(commandName); if (commandType == null) { return new CommandResponse(ResponseCode.OPERATION_NOT_SUPPORTED, GNSProtocol.BAD_RESPONSE.toString() + " " + GNSProtocol.OPERATION_NOT_SUPPORTED.toString() + " Sorry, don't understand " + commandName + QUERYPREFIX + queryString); } //Only allow mutual auth commands if we're on a secure (HTTPS) server if (commandType.isMutualAuth() && !secureServer) { return new CommandResponse(ResponseCode.OPERATION_NOT_SUPPORTED, GNSProtocol.BAD_RESPONSE.toString() + " " + GNSProtocol.OPERATION_NOT_SUPPORTED.toString() + " Not authorized to execute " + commandName + QUERYPREFIX + queryString); } // The client currently just uses the command name (which is not part of the // query string above) so we need to stuff // in the Command integer for the signature check and execution. jsonCommand.put(GNSProtocol.COMMAND_INT.toString(), commandType.getInt()); // Optionally does some sanity checking on the message if that was enabled at the client. // This makes necessary changes to the jsonCommand so don't remove this call // unless you know what you're doing and also change the code in the HTTP client. sanityCheckMessage(jsonCommand); // Hair below is to handle some commands locally (creates, delets, selects, admin) // and the rest by invoking the GNS client and sending them out. // Client will be null if GNSC.DISABLE_MULTI_SERVER_HTTP (see above) // is true (or there was a problem). if (client == null || commandType.isLocallyHandled()) { // EXECUTE IT LOCALLY AbstractCommand command; try { command = commandModule.lookupCommand(commandType); // Do some work to get the signature and message into the command for // signature checking that happens later on. // This only happens for local execution because remote handling (in the // other side of the if) already does this. processSignature(jsonCommand); if (command != null) { return CommandHandler.executeCommand(command, new CommandPacket((long) (Math.random() * Long.MAX_VALUE), jsonCommand, false), requestHandler); } LOGGER.log(Level.FINE, "lookupCommand returned null for {0}", commandName); } catch (IllegalArgumentException e) { LOGGER.log(Level.FINE, "lookupCommand failed for {0}", commandName); } return new CommandResponse(ResponseCode.OPERATION_NOT_SUPPORTED, GNSProtocol.BAD_RESPONSE.toString() + " " + GNSProtocol.OPERATION_NOT_SUPPORTED.toString() + " Sorry, don't understand " + commandName + QUERYPREFIX + queryString); } else { // Send the command remotely using a client try { LOGGER.log(Level.FINE, "Sending command out to a remote server: {0}", jsonCommand); CommandPacket commandResponsePacket = getResponseUsingGNSClient(client, jsonCommand); return new CommandResponse(ResponseCode.NO_ERROR, // Some crap here to make single field reads return just the value for backward compatibility // There is similar code to this other places. specialCaseSingleFieldRead(commandResponsePacket.getResultString(), commandType, jsonCommand)); } catch (IOException | ClientException e) { return new CommandResponse(ResponseCode.UNSPECIFIED_ERROR, GNSProtocol.BAD_RESPONSE.toString() + " " + GNSProtocol.UNSPECIFIED_ERROR.toString() + " " + e.toString()); // } catch (ClientException e) { // return new CommandResponse(ResponseCode.GNSProtocol.UNSPECIFIED_ERROR.toString(), // GNSProtocol.BAD_RESPONSE.toString() + " " + GNSProtocol.OPERATION_NOT_SUPPORTED.toString() // + " Sorry, don't understand " + commandName + QUERYPREFIX + queryString); } } } catch (JSONException | UnsupportedEncodingException e) { return new CommandResponse(ResponseCode.UNSPECIFIED_ERROR, GNSProtocol.BAD_RESPONSE.toString() + " " + GNSProtocol.UNSPECIFIED_ERROR.toString() + " " + e.toString()); } } private static void sanityCheckMessage(JSONObject jsonCommand) throws JSONException, UnsupportedEncodingException { if (jsonCommand.has("originalMessageBase64")) { String originalMessage = new String(Base64.decode(jsonCommand.getString("originalMessageBase64")), GNSProtocol.CHARSET.toString()); jsonCommand.remove("originalMessageBase64"); String commandSansSignature = CanonicalJSON.getCanonicalForm(jsonCommand); if (!originalMessage.equals(commandSansSignature)) { LOGGER.log(Level.SEVERE, "signature message mismatch! original: {0} computed for signature: {1}", new Object[] { originalMessage, commandSansSignature }); } else { LOGGER.log(Level.FINE, "######## original: {0}", originalMessage); } } } private static void processSignature(JSONObject jsonCommand) throws JSONException { if (jsonCommand.has(GNSProtocol.SIGNATURE.toString())) { // Squirrel away the signature. String signature = jsonCommand.getString(GNSProtocol.SIGNATURE.toString()); // Pull it out of the command because we don't want to have it there when we check the message. jsonCommand.remove(GNSProtocol.SIGNATURE.toString()); // Convert it to a conanical string (the message) that we can use later to check against the signature. String commandSansSignature = CanonicalJSON.getCanonicalForm(jsonCommand); // Put the decoded signature back as well as the message that we're going to // later compare the signature against. jsonCommand.put(GNSProtocol.SIGNATURE.toString(), signature) .put(GNSProtocol.SIGNATUREFULLMESSAGE.toString(), commandSansSignature); } } //make single field reads return just the value for backward compatibility private static String specialCaseSingleFieldRead(String response, CommandType commandType, JSONObject jsonFormattedArguments) { try { if (commandType.isRead() && jsonFormattedArguments.has(GNSProtocol.FIELD.toString()) && !jsonFormattedArguments.getString(GNSProtocol.FIELD.toString()) .equals(GNSProtocol.ENTIRE_RECORD.toString()) && JSONPacket.couldBeJSON(response) && response.startsWith("{")) { String key = jsonFormattedArguments.getString(GNSProtocol.FIELD.toString()); JSONObject json = new JSONObject(response); return json.getString(key); } } catch (JSONException e) { LOGGER.log(Level.SEVERE, "Problem getting single key reponse for : {0}", e.getMessage()); // just return the response if there is some issue } return response; } private CommandPacket getResponseUsingGNSClient(GNSClient client, JSONObject jsonFormattedArguments) throws ClientException, IOException, JSONException { LOGGER.log(Level.FINE, "jsonFormattedCommand ={0}", jsonFormattedArguments.toString()); CommandPacket outgoingPacket = new CommandPacket((long) (Math.random() * Long.MAX_VALUE), jsonFormattedArguments, false); //GNSCommand.createGNSCommandFromJSONObject(jsonFormattedArguments); LOGGER.log(Level.FINE, "outgoingPacket ={0}", outgoingPacket.toString()); CommandPacket returnPacket = client.execute(outgoingPacket); LOGGER.log(Level.FINE, "returnPacket ={0}", returnPacket.toString()); /** * Can also invoke getResponse(), getResponseString(), getResponseJSONObject() * etc. on {@link CommandPacket} as documented in {@link GNSCommand}. */ return returnPacket; } /** * Returns info about the server. */ protected class EchoHttpHandler implements HttpHandler { /** * * @param exchange * @throws IOException */ @Override public void handle(HttpExchange exchange) throws IOException { String requestMethod = exchange.getRequestMethod(); if (requestMethod.equalsIgnoreCase("GET")) { Headers responseHeaders = exchange.getResponseHeaders(); responseHeaders.set("Content-Type", "text/HTML"); exchange.sendResponseHeaders(200, 0); try (OutputStream responseBody = exchange.getResponseBody()) { Headers requestHeaders = exchange.getRequestHeaders(); Set<String> keySet = requestHeaders.keySet(); Iterator<String> iter = keySet.iterator(); String buildVersion = GNSConfig.readBuildVersion(); String buildVersionInfo = "Build Version: Unable to lookup!"; if (buildVersion != null) { buildVersionInfo = "Build Version: " + buildVersion; } String responsePreamble = "<html><head><title>GNS Server Status</title></head><body><p>"; String responsePostamble = "</p></body></html>"; String serverStartDateString = "Server start time: " + Format.formatDualDate(serverStartDate); String serverUpTimeString = "Server uptime: " + DurationFormatUtils .formatDurationWords(new Date().getTime() - serverStartDate.getTime(), true, true); String serverSSLMode = "Server SSL mode: " + ReconfigurationConfig.getServerSSLMode().toString(); String clientSSLMode = "Client SSL mode: " + ReconfigurationConfig.getClientSSLMode().toString(); String reconAddresses = "Recon addresses: " + ReconfigurationConfig.getReconfiguratorAddresses().toString(); String numberOfNameServers = "Server count: " + requestHandler.getGnsNodeConfig().getNumberOfNodes(); String recordsClass = "Records Class: " + GNSConfig.GNSC.getNoSqlRecordsClass(); String secureString = exchange instanceof HttpsExchange ? "Security: Secure" : "Security: Open"; // Build the response responseBody.write(responsePreamble.getBytes()); responseBody.write(buildVersionInfo.getBytes()); responseBody.write("<br>".getBytes()); responseBody.write(serverStartDateString.getBytes()); responseBody.write("<br>".getBytes()); responseBody.write(serverUpTimeString.getBytes()); responseBody.write("<br>".getBytes()); responseBody.write(numberOfNameServers.getBytes()); responseBody.write("<br>".getBytes()); responseBody.write(reconAddresses.getBytes()); responseBody.write("<br>".getBytes()); responseBody.write(serverSSLMode.getBytes()); responseBody.write("<br>".getBytes()); responseBody.write(clientSSLMode.getBytes()); responseBody.write("<br>".getBytes()); responseBody.write(secureString.getBytes()); responseBody.write("<br>".getBytes()); responseBody.write(recordsClass.getBytes()); responseBody.write("<br>".getBytes()); responseBody.write("<br>".getBytes()); responseBody.write("Request Headers:".getBytes()); responseBody.write("<br>".getBytes()); while (iter.hasNext()) { String key = iter.next(); List<String> values = requestHeaders.get(key); String s = key + " = " + values.toString() + "\n"; responseBody.write(s.getBytes()); responseBody.write("<br>".getBytes()); } responseBody.write(responsePostamble.getBytes()); } } } } }