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.gnsclient.client.http; /** * @author arun */ import com.sun.net.httpserver.*; import edu.umass.cs.gnsclient.client.CommandUtils; import edu.umass.cs.gnsclient.client.GNSClient; import edu.umass.cs.gnsclient.client.GNSClientConfig; import edu.umass.cs.gnsclient.client.GNSCommand; import edu.umass.cs.gnsclient.client.util.EnvUtils; import edu.umass.cs.gnsclient.client.util.GuidEntry; import edu.umass.cs.gnsclient.client.util.GuidUtils; import edu.umass.cs.gnsclient.client.util.KeyPairUtils; import edu.umass.cs.gnscommon.CommandType; import edu.umass.cs.gnscommon.GNSProtocol; import edu.umass.cs.gnscommon.ResponseCode; import edu.umass.cs.gnscommon.exceptions.client.ClientException; import edu.umass.cs.gnscommon.exceptions.client.EncryptionException; 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; 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 org.apache.commons.lang.time.DurationFormatUtils; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.net.HttpURLConnection; import java.net.InetSocketAddress; import java.net.URI; import java.security.NoSuchAlgorithmException; import java.util.*; import java.util.concurrent.Executors; import java.util.logging.Level; import java.util.logging.Logger; import static edu.umass.cs.gnsserver.httpserver.Defs.QUERYPREFIX; public class GNSHTTPProxy { /** * */ protected static final String GNS_PATH = Config.getGlobalString(GNSConfig.GNSC.HTTP_SERVER_GNS_URL_PATH); private final boolean isProxyClient; private final HttpServer httpServer; // handles command processing private final GNSClient client; /** * */ private final Date serverStartDate = new Date(); private final static Logger LOGGER = Logger.getLogger(GNSHTTPProxy.class.getName()); /** * @param port */ public GNSHTTPProxy(int port, boolean isProxyClient, String clientKeyDBDir) throws IOException { if (isProxyClient) { ensureDirExists(clientKeyDBDir); } this.isProxyClient = isProxyClient; this.client = new GNSClient() { @Override public String getLabel() { return GNSHTTPProxy.class.getSimpleName(); } }; this.httpServer = tryPort(port); } public GNSHTTPProxy(int port, boolean isProxyClient) throws IOException { this(port, isProxyClient, EnvUtils.GNS_HOME); } public GNSHTTPProxy(int port) throws IOException { this(port, false); } /** * 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 HttpServer tryPort(int port) throws IOException { try { InetSocketAddress addr = new InetSocketAddress(port); HttpServer server = HttpServer.create(addr, 0); server.createContext("/", new EchoHttpHandler()); server.createContext("/" + GNS_PATH, new DefaultHttpHandler()); server.setExecutor(Executors.newCachedThreadPool()); server.start(); LOGGER.log(Level.INFO, "HTTP server is listening on port {0}", port); return server; } catch (IOException e) { LOGGER.log(Level.WARNING, "HTTP server failed to start on port " + "{0}" + " " + "due to {1}", new Object[] { port, e.getMessage() }); throw e; } } /** * The default handler. */ protected class DefaultHttpHandler implements HttpHandler { /** * @param exchange */ @Override public void handle(HttpExchange exchange) { CommandResponse response = null; 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", "*"); } int statusCode = HttpURLConnection.HTTP_OK; //default 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() : ""; String commandName = path.replaceFirst("/" + GNS_PATH + "/", ""); if (!commandName.isEmpty()) { LOGGER.log(Level.FINE, "Action: {0} Query:{1}", new Object[] { commandName, query }); // process commandName query response = processQuery(host, commandName, query, exchange instanceof HttpsExchange); if (response.getExceptionOrErrorCode() != ResponseCode.NO_ERROR) statusCode = translateToHTTPStatusCode(response.getExceptionOrErrorCode()); } else { statusCode = HttpURLConnection.HTTP_BAD_REQUEST; 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); exchange.sendResponseHeaders(statusCode, 0); responseBody.write(response.getReturnValue().getBytes()); } } } catch (IOException e) { LOGGER.log(Level.WARNING, "IOException: {0}", e.getMessage()); e.printStackTrace(); // FIXME: unclear why retrying write makes sense try { String defaultResponse = GNSProtocol.BAD_RESPONSE.toString() + " " + GNSProtocol.QUERY_PROCESSING_ERROR.toString() + " " + e; OutputStream responseBody = exchange.getResponseBody(); responseBody.write( response != null ? response.getReturnValue().getBytes() : defaultResponse.getBytes()); } catch (Exception f) { // at this point screw it } } } } private static int translateToHTTPStatusCode(ResponseCode code) { // FIXME: systematically distinguish between client and server errors return HttpURLConnection.HTTP_BAD_REQUEST; } /* * Process queries for the http service. Converts the URI of 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) { // 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()) .put(GNSProtocol.COMMANDNAME.toString(), commandType.toString()); // 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). return defaultHandleCommand(host, commandName, queryString, jsonCommand, secureServer); } // FIXME: translate to appropriate HTTP + GNS response code catch (JSONException | UnsupportedEncodingException | NoSuchAlgorithmException e) { return new CommandResponse(ResponseCode.UNSPECIFIED_ERROR, GNSProtocol.BAD_RESPONSE.toString() + " " + GNSProtocol.UNSPECIFIED_ERROR.toString() + " " + e.toString()); } } /** * Implementations may extend this method. * * @param host * @param commandName * @param queryString * @param jsonCommand * @param secureServer * @return * @throws JSONException */ protected CommandResponse defaultHandleCommand(String host, String commandName, String queryString, JSONObject jsonCommand, boolean secureServer) throws JSONException, NoSuchAlgorithmException { // 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.getCommandType(jsonCommand.getInt(GNSProtocol.COMMAND_INT.toString())), jsonCommand)); } catch (IOException | ClientException 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(GNSProtocol.ORIGINAL_MESSAGE_BASE64.toString())) { String originalMessage = new String( Base64.decode(jsonCommand.getString(GNSProtocol.ORIGINAL_MESSAGE_BASE64.toString())), GNSProtocol.CHARSET.toString()); jsonCommand.remove(GNSProtocol.ORIGINAL_MESSAGE_BASE64.toString()); 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.WARNING, "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, NoSuchAlgorithmException { CommandType type = CommandType .getCommandType(jsonFormattedArguments.getInt(GNSProtocol.COMMAND_INT.toString())); GuidEntry querier = getQuerier(client, type, jsonFormattedArguments); CommandPacket outgoingPacket = this.isProxyClient ? // sign command GNSCommand.getCommand(type, querier, getKeysAndValuesAsArray(addMissingParams(client, querier, type, jsonFormattedArguments))) : GNSCommand.getCommand(type, querier, jsonFormattedArguments); LOGGER.log(Level.FINE, "{0} sending request {1}", new Object[] { this, outgoingPacket }); CommandPacket returnPacket = client.execute(outgoingPacket); LOGGER.log(Level.FINE, "{0} received response {1}", new Object[] { this, returnPacket }); /** * Can also invoke getResponse(), getResponseString(), * getResponseJSONObject() * etc. on {@link CommandPacket} as documented in {@link GNSCommand}. */ return returnPacket; } public String toString() { return this.getClass().getSimpleName(); } private static JSONObject addMissingParams(GNSClient client, GuidEntry querier, CommandType type, JSONObject jsonFormattedArguments) throws JSONException, ClientException, NoSuchAlgorithmException { //if (querier == null) return jsonFormattedArguments; /** * Add missing arguments like public key for account or GUID * creation; default field for read, etc. */ switch (type) { case RegisterAccount: case RegisterAccountSecured: jsonFormattedArguments.put(GNSProtocol.PUBLIC_KEY.toString(), querier.getPublicKeyString()); break; case AddMultipleGuids: JSONArray names = jsonFormattedArguments.getJSONArray(GNSProtocol.NAMES.toString()); ArrayList<String> publicKeys = new ArrayList<String>(); for (int i = 0; i < names.length(); i++) { GuidEntry entry = GuidUtils.lookupOrCreateGuidEntry((String) names.get(i), client.getGNSProvider()); publicKeys.add(entry.getPublicKeyString()); } jsonFormattedArguments.put(GNSProtocol.PUBLIC_KEYS.toString(), publicKeys); break; case Read: if (!jsonFormattedArguments.has(GNSProtocol.FIELD.toString())) jsonFormattedArguments.put(GNSProtocol.FIELD.toString(), GNSProtocol.ENTIRE_RECORD.toString()); default: } // if command requires and lacks signature, insert signature if (type.requiresSignature() && !jsonFormattedArguments.has(GNSProtocol.SIGNATURE.toString()) && querier != null) { if (type.isRead() || type.isSelect()) jsonFormattedArguments.put(GNSProtocol.READER.toString(), querier.getGuid()); else if (type.isUpdate()) jsonFormattedArguments.put(GNSProtocol.WRITER.toString(), querier.getGuid()); } return jsonFormattedArguments; } private static boolean allRequiredParamsPresent(CommandType type, JSONObject jsonFormattedArguments) throws JSONException { for (String param : type.getCommandRequiredParameters()) if (!jsonFormattedArguments.has(param)) return false; return true; } private static CommandType getCommandType(JSONObject json) throws JSONException { return CommandType.getCommandType(json.getInt(GNSProtocol.COMMAND_INT.toString())); } private static GuidEntry getImplicitSigner(GNSClient client, CommandType type, JSONObject jsonFormattedArguments) throws JSONException, EncryptionException, NoSuchAlgorithmException { GuidEntry querierGUIDEntry = null; if (querierGUIDEntry == null) querierGUIDEntry = getGUIDEntry(client, jsonFormattedArguments, GNSProtocol.READER.toString()); if (querierGUIDEntry == null) querierGUIDEntry = getGUIDEntry(client, jsonFormattedArguments, GNSProtocol.WRITER.toString()); // if (type.isCreateDelete()) switch (type) { case RegisterAccount: case RegisterAccountSecured: case RegisterAccountWithCertificate: // keypair creation necessary querierGUIDEntry = GuidUtils.lookupOrCreateGuidEntry( jsonFormattedArguments.getString(GNSProtocol.NAME.toString()), client.getGNSProvider()); break; default: querierGUIDEntry = getGUIDEntry(client, jsonFormattedArguments, GNSProtocol.GUID.toString()); } return querierGUIDEntry; } private static GuidEntry getGUIDEntry(GNSClient client, JSONObject json, String querierKey) throws JSONException { if (!json.has(querierKey)) return null; // else String name = KeyPairUtils.getName(client.getGNSProvider(), json.getString(querierKey)); GuidEntry entry = GuidUtils.lookupGuidEntryFromDatabase(client, name); return entry; } private static GuidEntry getQuerier(GNSClient client, CommandType type, JSONObject jsonFormattedArguments) throws JSONException, EncryptionException, NoSuchAlgorithmException { String querier = null; // no-op if all required arguments are present return allRequiredParamsPresent(type, jsonFormattedArguments) ? null : // else implicit signer GUID getImplicitSigner(client, type, jsonFormattedArguments); } /** * Converts a JSONObject to an array of key-value pairs wherein every * odd element is a key and the successive even element is the * corresponding value. * * @param json * @return * @throws JSONException */ public static Object[] getKeysAndValuesAsArray(JSONObject json) throws JSONException { ArrayList<Object> arrayList = new ArrayList<Object>(); JSONArray jsonArray = json.names(); for (int i = 0; i < jsonArray.length(); i++) { String key = (String) jsonArray.get(i); arrayList.add(key); arrayList.add(json.get(key)); } return arrayList.toArray(); } /** * 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()); } } } } private static String ensureDirExists(String dir) { new File(dir).mkdirs(); return dir; } private static void processArgs(String[] args) { } public static void main(String[] args) throws IOException { new GNSHTTPProxy(5678, true); } }