/* * RESTServer.java - Copyright(c) 2014 Joe Pasqua * Provided under the MIT License. See the LICENSE file for details. * Created: May 24, 2014 */ package org.noroomattheinn.visibletesla.rest; import com.sun.net.httpserver.BasicAuthenticator; import com.sun.net.httpserver.HttpContext; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.InetSocketAddress; import java.net.URL; import java.util.HashMap; import java.util.Map; import javafx.beans.property.BooleanProperty; import javafx.beans.property.IntegerProperty; import javafx.beans.property.StringProperty; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import static org.noroomattheinn.tesla.Tesla.logger; import org.noroomattheinn.utils.Utils; import org.noroomattheinn.visibletesla.MessageTemplate; import org.noroomattheinn.utils.LRUMap; import org.noroomattheinn.utils.ThreadManager; import org.noroomattheinn.visibletesla.AppAPI; import org.noroomattheinn.visibletesla.vehicle.VTVehicle; /** * RESTServer: Provide minimal external services. * * @author Joe Pasqua <joe at NoRoomAtTheInn dot org> */ public class RESTServer implements ThreadManager.Stoppable { /*------------------------------------------------------------------------------ * * Constants and Enums * *----------------------------------------------------------------------------*/ private static final Map<String,AppAPI.Mode> toAppMode = Utils.newHashMap("sleep", AppAPI.Mode.AllowSleeping, "wakeup", AppAPI.Mode.StayAwake, "produce", AppAPI.Mode.StayAwake); /*------------------------------------------------------------------------------ * * Internal State * *----------------------------------------------------------------------------*/ private final AppAPI api; private final VTVehicle vtVehicle; private final BasicAuthenticator authenticator; private final BooleanProperty restEnabled; private final IntegerProperty restPort; private final StringProperty urlSource; private HttpServer server; private boolean launched = false; /*============================================================================== * ------- ------- * ------- Public Interface To This Class ------- * ------- ------- *============================================================================*/ public RESTServer( AppAPI api, VTVehicle v, BasicAuthenticator authenticator, BooleanProperty restEnabled, IntegerProperty restPort, StringProperty urlSource) { this.server = null; this.api = api; this.vtVehicle = v; this.restEnabled = restEnabled; this.urlSource = urlSource; this.restPort = restPort; this.authenticator = authenticator; ThreadManager.get().addStoppable((ThreadManager.Stoppable)this); watch(v); } @Override public synchronized void stop() { if (server != null) { server.stop(0); server = null; } } /*------------------------------------------------------------------------------ * * Methods to launch the server when a vehicle comes into being * *----------------------------------------------------------------------------*/ private void watch(final VTVehicle v) { v.vehicle.addTracker(new Runnable() { @Override public void run() { if (v.vehicle.get() != null && !launched) { launch(); launched = true; } } }); } private synchronized void launch() { if (!restEnabled.get()) { logger.info("REST Services are disabled"); return; } try { server = HttpServer.create(new InetSocketAddress(restPort.get()), 0); HttpContext cc; cc = server.createContext("/v1/action/activity", activityRequest); cc.setAuthenticator(authenticator); cc = server.createContext("/v1/action/info", infoRequest); cc.setAuthenticator(authenticator); cc = server.createContext("/", staticPageRequest); cc.setAuthenticator(authenticator); server.setExecutor(null); // creates a default executor server.start(); } catch (IOException ex) { logger.severe("Unable to start RESTServer: " + ex.getMessage()); } } /*------------------------------------------------------------------------------ * * PRIVATE - Request Handlers * *----------------------------------------------------------------------------*/ private HttpHandler activityRequest = new HttpHandler() { @Override public void handle(HttpExchange exchange) throws IOException { if (!exchange.getRequestMethod().equals("GET")) { sendResponse(exchange, 400, "GET only on activity endpoint\n"); return; } String path = StringUtils.stripEnd(exchange.getRequestURI().getPath(), "/"); String mode = StringUtils.substringAfterLast(path, "/"); if (mode.equals("activity")) { sendResponse(exchange, 403, "403 (Forbidden)\n"); return; } AppAPI.Mode requestedMode = toAppMode.get(mode); if (requestedMode == null) { logger.warning("Unknown app mode: " + mode + "\n"); sendResponse(exchange, 400, "Unknown app mode"); return; } logger.info("Requested app mode: " + mode); if (requestedMode == AppAPI.Mode.AllowSleeping) api.allowSleeping(); else api.stayAwake(); sendResponse(exchange, 200, "Requested mode: " + mode + "\n"); } }; private HttpHandler infoRequest = new HttpHandler() { @Override public void handle(HttpExchange exchange) throws IOException { if (!exchange.getRequestMethod().equals("GET")) { sendResponse(exchange, 400, "GET only on info endpoint\n"); return; } String path = StringUtils.stripEnd(exchange.getRequestURI().getPath(), "/"); String infoType = StringUtils.substringAfterLast(path, "/"); if (infoType.equals("info")) { sendResponse(exchange, 403, "403 (Forbidden)\n"); return; } logger.info("Requested info type: " + infoType); String response; switch (infoType) { case "car_state": response = vtVehicle.carStateAsJSON(); break; case "car_details": response = vtVehicle.carDetailsAsJSON(); break; case "inactivity_mode": response = String.format("{ \"mode\": \"%s\" }", api.mode.get().name()); break; case "dbg_sar": Map<String,String> params = getParams(exchange.getRequestURI().getQuery()); response = params.get("p1"); if (response == null) { response = "DBG_SAR"; } api.fakeSchedulerActivity(response); break; default: logger.warning("Unknown info request: " + infoType + "\n"); sendResponse(exchange, 400, "Unknown info request " + infoType); return; } exchange.getResponseHeaders().add("Content-Type", "application/json"); sendResponse(exchange, 200, response); } }; private Map<String, String> getParams(String query) { Map<String, String> params = new HashMap<>(); if (query != null) { for (String param : query.split("&")) { String pair[] = param.split("="); if (pair.length > 1) { params.put(pair[0], pair[1]); } else { params.put(pair[0], ""); } } } return params; } private HttpHandler staticPageRequest = new HttpHandler() { LRUMap<String,byte[]> cache = new LRUMap<>(10); @Override public void handle(HttpExchange exchange) throws IOException { // TO DO: Check for path traversal attack! String path = StringUtils.stripEnd(exchange.getRequestURI().getPath(), "/"); path = StringUtils.stripStart(path, "/"); try { byte[] content = cache.get(path); if (content == null) { InputStream is; if (path.startsWith("custom/")) { String cPath = path.substring(7); is = new URL(urlSource.get()+cPath).openStream(); } else if (path.startsWith("TeslaResources/")) { path = "org/noroomattheinn/" + path; is = getClass().getClassLoader().getResourceAsStream(path); } else { is = getClass().getResourceAsStream(path); } if (is == null) { sendResponse(exchange, 404, "404 (Not Found)\n"); return; } else { content = IOUtils.toByteArray(is); if (!path.startsWith("custom/_nc_")) cache.put(path, content); } } String type = getMimeType(StringUtils.substringAfterLast(path, ".")); if (type.equalsIgnoreCase("text/html")) { MessageTemplate mt = new MessageTemplate(new String(content, "UTF-8")); content = mt.getMessage(api, vtVehicle, null).getBytes(); } else if (cacheOnClient(type)) { exchange.getResponseHeaders().add("Cache-Control", "max-age=2592000"); } exchange.getResponseHeaders().add("Content-Type", type); sendResponse(exchange, 200, content); } catch (IOException ex) { logger.severe("Error reading requested file: " + ex.getMessage()); sendResponse(exchange, 404, "404 (Not Found)\n"); } } }; /*------------------------------------------------------------------------------ * * PRIVATE - Utility Methods * *----------------------------------------------------------------------------*/ private boolean cacheOnClient(String type) { return (!type.equals("text/html")); } private void sendResponse(HttpExchange exchange, int code, String response) throws IOException { exchange.sendResponseHeaders(code, response.length()); OutputStream os = exchange.getResponseBody(); os.write(response.getBytes()); os.close(); } private void sendResponse(HttpExchange exchange, int code, byte[] response) throws IOException { exchange.sendResponseHeaders(code, response.length); OutputStream os = exchange.getResponseBody(); os.write(response); os.close(); } private String getMimeType(String type) { if (type != null) { switch (type) { case "css": return "text/css"; case "htm": case "html": return "text/html"; case "js": return "application/javascript"; case "png": return "image/png"; case "gif": return "image/gif"; case "jpg": case "jpeg": return "image/jpeg"; } } return "text/plain"; } }