package com.alecgorge.minecraft.jsonapi; import java.io.BufferedReader; import java.io.File; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.io.StringWriter; import java.lang.reflect.InvocationTargetException; import java.net.URL; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.logging.Logger; import org.bukkit.event.player.AsyncPlayerChatEvent; import org.bukkit.event.player.PlayerEggThrowEvent; import org.json.simpleForBukkit.JSONArray; import org.json.simpleForBukkit.JSONObject; import org.json.simpleForBukkit.parser.JSONParser; import org.json.simpleForBukkit.parser.ParseException; import com.alecgorge.java.http.MutableHttpRequest; import com.alecgorge.minecraft.jsonapi.api.v2.APIv2Handler; import com.alecgorge.minecraft.jsonapi.api.v2.EssentialsAPIMethods; import com.alecgorge.minecraft.jsonapi.api.v2.StandardAPIMethods; import com.alecgorge.minecraft.jsonapi.config.UsersConfig; import com.alecgorge.minecraft.jsonapi.dynamic.APIWrapperMethods; import com.alecgorge.minecraft.jsonapi.dynamic.Caller; import com.alecgorge.minecraft.jsonapi.streams.ChatMessage; import com.alecgorge.minecraft.jsonapi.streams.ChatStream; import com.alecgorge.minecraft.jsonapi.streams.EggMessage; import com.alecgorge.minecraft.jsonapi.streams.EggStream; import com.alecgorge.minecraft.jsonapi.streams.ConnectionMessage; import com.alecgorge.minecraft.jsonapi.streams.ConnectionStream; import com.alecgorge.minecraft.jsonapi.streams.ConsoleMessage; import com.alecgorge.minecraft.jsonapi.streams.ConsoleStream; import com.alecgorge.minecraft.jsonapi.streams.FormattedChatMessage; import com.alecgorge.minecraft.jsonapi.streams.FormattedChatStream; import com.alecgorge.minecraft.jsonapi.streams.PerformanceStream; import com.alecgorge.minecraft.jsonapi.streams.StreamingResponse; public class JSONServer extends NanoHTTPD { public UsersConfig logins; private JSONAPI inst; private Logger outLog = JSONAPI.instance.outLog; private Caller caller; public ChatStream chat = new ChatStream("chat"); public EggStream eggStream = new EggStream("egg"); public FormattedChatStream formattedChat = new FormattedChatStream("formatted_chat"); public ConsoleStream console = new ConsoleStream("console"); public ConnectionStream connections = new ConnectionStream("connections"); public PerformanceStream performance = new PerformanceStream("performance"); private static boolean initted = false; public JSONServer(UsersConfig auth, final JSONAPI plugin, final long startupDelay) throws IOException { super(plugin.port, plugin.bindAddress); inst = plugin; caller = new Caller(inst); caller.loadFile(new File(inst.getDataFolder() + File.separator + "methods.json"), false); outLog.info("[JSONAPI] Loaded methods.json."); (new Thread(new Runnable() { @Override public void run() { float seconds = startupDelay / 1000; outLog.info("[JSONAPI] Waiting " + String.format("%2.3f", seconds) + " seconds to load methods so that all the other plugins load..."); outLog.info("[JSONAPI] Any requests in this time will not work..."); try { if (!initted) { Thread.sleep(startupDelay); initted = true; } } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } File[] files = (new File(inst.getDataFolder() + File.separator + "methods" + File.separator)).listFiles(new FilenameFilter() { @Override public boolean accept(File dir, String name) { return name.endsWith(".json"); } }); if (files != null && files.length > 0) { for (File f : files) { caller.loadFile(f, false); } } String[] methodsFiles = new String[] { "chat.json", "dynmap.json", "econ.json", "fs.json", "permissions.json", "players.json", "plugins.json", "remotetoolkit.json", "server.json", "streams.json", "system.json", "worlds.json", "jsonapi.json" }; for(String m : methodsFiles) { caller.loadInputStream(inst.getResource("jsonapi4/methods/" + m), true); } caller.registerMethods(APIWrapperMethods.getInstance()); new EssentialsAPIMethods(inst); new StandardAPIMethods(inst); outLog.info("[JSONAPI] " + caller.methodCount + " methods loaded in " + caller.methods.size() + " namespaces."); connectionInfo(); } })).start(); this.logins = auth; } void connectionInfo() { outLog.info("[JSONAPI] ------[Connection information]-------"); outLog.info("[JSONAPI] JSON Server listening on " + inst.port); outLog.info("[JSONAPI] JSON Stream Server listening on " + (inst.port + 1)); outLog.info("[JSONAPI] JSON WebSocket Stream Server listening on " + (inst.port + 2)); if(inst.sslJsonWebSocketServer != null){ outLog.info("[JSONAPI] JSON WebSocket Secure Stream Server listening on " + (inst.port + 3)); } outLog.info("[JSONAPI] Active and listening for requests."); try { URL whatismyip = new URL("http://tools.alecgorge.com/ip.php"); BufferedReader in = new BufferedReader(new InputStreamReader(whatismyip.openStream())); String ip = in.readLine(); URL checkURL = new URL("http://tools.alecgorge.com/port_check.php"); outLog.info("[JSONAPI] External IP: " + ip); for(int i : new int[] { inst.port, inst.port + 1, inst.port + 2 }) { MutableHttpRequest reqReg = new MutableHttpRequest(checkURL); reqReg.addGetValue("host", ip); reqReg.addGetValue("port", String.valueOf(i)); if(reqReg.get().getStatusCode() == 200) { outLog.info("[JSONAPI] Port " + i + " is properly forwarded and is externally accessible."); } else { outLog.info("[JSONAPI] Port " + i + " is not properly forwarded."); } } } catch (Exception e) { e.printStackTrace(); } outLog.info("[JSONAPI] -------------------------------------"); } public UsersConfig getLogins() { return logins; } public Caller getCaller() { return caller; } public JSONAPI getInstance() { return inst; } public void logChat(AsyncPlayerChatEvent e) { if(inst.isEnabled()) { chat.addMessage(new ChatMessage(e)); formattedChat.addMessage(new FormattedChatMessage(e)); } } public void logChat(String player, String message) { if(inst.isEnabled()) { chat.addMessage(new ChatMessage(player, message)); } } public void logConsole(String line) { if(inst.isEnabled()) { console.addMessage(new ConsoleMessage(line)); } } public void logConnected(String player) { if(inst.isEnabled()) { connections.addMessage(new ConnectionMessage(player, true)); } } public void logDisconnected(String player) { if(inst.isEnabled()) { connections.addMessage(new ConnectionMessage(player, false)); } } public void logEggThrow(PlayerEggThrowEvent e) { if(inst.isEnabled()) { eggStream.addMessage(new EggMessage(e)); } } public boolean testLogin(String method, String hash) { try { boolean valid = false; for (Map<String, Object> user : logins.getUsers()) { String thishash = JSONAPI.SHA256(user.get("username") + method + user.get("password") + inst.salt); String saltless = JSONAPI.SHA256(user.get("username") + method + user.get("password")); if (thishash.equals(hash) || saltless.equals(hash)) { valid = true; break; } } return valid; } catch (Exception e) { return false; } } public static String callback(String callback, String json) { if (callback == null || callback.equals("")) return json; return callback.concat("(").concat(json).concat(")"); } public void info(final String log) { if (inst.logging || !inst.logFile.equals("false")) { outLog.info("[JSONAPI] " + log); } } public void warning(final String log) { if (inst.logging || !inst.logFile.equals("false")) { outLog.warning("[JSONAPI] " + log); } } private void setLastRequestParms(Properties parms) { synchronized (parms) { this.lastRequestParms = parms; } } private Properties getLastRequestParms() { synchronized (lastRequestParms) { return lastRequestParms; } } private Properties lastRequestParms = null; @SuppressWarnings("unchecked") @Override public Response serve(String uri, String method, Properties header, Properties parms) { if(method.equals("OPTIONS")) { Response r = new NanoHTTPD.Response(HTTP_OK, MIME_HTML, ""); r.addHeader("Access-Control-Allow-Origin", "*"); r.addHeader("Access-Control-Allow-Methods", "GET, POST"); return r; } if(uri.startsWith("/api/2/") || inst.useGroups) { APIv2Handler handler = new APIv2Handler(uri, method, header, parms, this); return handler.serve(); } String callback = parms.getProperty("callback"); setLastRequestParms(parms); if (inst.whitelist.size() > 0 && !inst.whitelist.contains(header.get("X-REMOTE-ADDR"))) { outLog.warning("[JSONAPI] An API call from " + header.get("X-REMOTE-ADDR") + " was blocked because " + header.get("X-REMOTE-ADDR") + " is not on the whitelist."); return jsonRespone(returnAPIError("", "You are not allowed to make API calls."), callback, HTTP_FORBIDDEN); } if (uri.equals("/api/subscribe")) { String source = parms.getProperty("source"); String sources = parms.getProperty("sources"); String key = parms.getProperty("key"); Object prev = parms.getProperty("show_previous"); boolean showOlder; if (prev == null) { showOlder = true; } else { if (prev.equals("false")) { showOlder = false; } else { showOlder = true; } } List<String> sourceList = new ArrayList<String>(); if (source != null) { if (!testLogin(source, key)) { info("[Streaming API] " + header.get("X-REMOTE-ADDR") + ": Invalid API Key."); return jsonRespone(returnAPIError(source, "Invalid API key."), callback, HTTP_FORBIDDEN); } if (source.equals("all")) { sourceList = new ArrayList<String>(JSONAPI.instance.getStreamManager().getStreams().keySet()); } else { sourceList.add(source); } } else if (sources != null) { if (!testLogin(sources, key)) { info("[Streaming API] " + header.get("X-REMOTE-ADDR") + ": Invalid API Key."); return jsonRespone(returnAPIError(source, "Invalid API key."), callback, HTTP_FORBIDDEN); } JSONParser p = new JSONParser(); try { for (Object o : (JSONArray) p.parse(sources)) { sourceList.add(o.toString()); } } catch (ParseException e) { // TODO Auto-generated catch block e.printStackTrace(); } } info("[Streaming API] " + header.get("X-REMOTE-ADDR") + ": source=" + sourceList.toString()); StreamingResponse out = new StreamingResponse(inst, sourceList, callback, showOlder, parms.containsKey("tag") ? parms.getProperty("tag") : null); Response r = new NanoHTTPD.Response(HTTP_OK, MIME_PLAINTEXT, out); r.addHeader("Access-Control-Allow-Origin", "*"); return r; } else if (!uri.equals("/api/call") && !uri.equals("/api/call-multiple")) { Response r = new NanoHTTPD.Response(HTTP_NOTFOUND, MIME_PLAINTEXT, "File not found."); r.addHeader("Access-Control-Allow-Origin", "*"); return r; } Object args = parms.getProperty("args", "[]"); String calledMethod = (String) parms.getProperty("method"); if (calledMethod == null) { info("[API Call] " + header.get("X-REMOTE-ADDR") + ": Parameter 'method' was not defined."); return jsonRespone(returnAPIError("", "Parameter 'method' was not defined."), callback, HTTP_NOTFOUND); } String key = parms.getProperty("key"); if (!inst.method_noauth_whitelist.contains(calledMethod) && !testLogin(calledMethod, key)) { info("[API Call] " + header.get("X-REMOTE-ADDR") + ": Invalid API Key."); return jsonRespone(returnAPIError(calledMethod, "Invalid API key."), callback, HTTP_FORBIDDEN); } info("[API Call] " + header.get("X-REMOTE-ADDR") + ": method=" + parms.getProperty("method").concat("?args=").concat((String) args)); if (args == null || calledMethod == null) { return jsonRespone(returnAPIError(calledMethod, "You need to pass a method and an array of arguments."), callback, HTTP_NOTFOUND); } else { try { JSONParser parse = new JSONParser(); args = parse.parse((String) args); if (uri.equals("/api/call-multiple")) { List<String> methods = new ArrayList<String>(); List<Object> arguments = new ArrayList<Object>(); Object o = parse.parse(calledMethod); if (o instanceof List<?> && args instanceof List<?>) { methods = (List<String>) o; arguments = (List<Object>) args; } else { return jsonRespone(returnAPIException(calledMethod, new Exception("method and args both need to be arrays for /api/call-multiple")), callback); } int size = methods.size(); JSONArray arr = new JSONArray(); for (int i = 0; i < size; i++) { arr.add(serveAPICall(methods.get(i), (arguments.size() - 1 >= i ? arguments.get(i) : new ArrayList<Object>()))); } return jsonRespone(returnAPISuccess(o, arr), callback); } else { // work around because Adminium 2.1.1 doesn't parse the version correctly // it says that 4.0.1 < 3.4.5. Always return 3.6.7 for that. // :sadface: if(calledMethod.equals("getPluginVersion") && args instanceof List<?> && ((List<Object>) args).get(0).toString().equals("JSONAPI") && header.getProperty("user-agent").startsWith("Adminium 2.1.1")) { calledMethod = "polyfill_getPluginVersion"; } return jsonRespone(serveAPICall(calledMethod, args), callback); } } catch (Exception e) { return jsonRespone(returnAPIException(calledMethod, e), callback); } } } public JSONObject returnAPIException(Object calledMethod, Throwable e) { JSONObject r = new JSONObject(); r.put("result", "error"); StringWriter pw = new StringWriter(); e.printStackTrace(new PrintWriter(pw)); e.printStackTrace(); r.put("source", calledMethod); r.put("error", "Caught exception: " + pw.toString().replaceAll("\\n", "\n").replaceAll("\\r", "\r")); return r; } public JSONObject returnAPIError(Object calledMethod, String error) { JSONObject r = new JSONObject(); r.put("result", "error"); r.put("source", calledMethod); r.put("error", error); return r; } public JSONObject returnAPISuccess(Object calledMethod, Object result) { JSONObject r = new JSONObject(); r.put("result", "success"); r.put("source", calledMethod); r.put("success", result); return r; } public NanoHTTPD.Response jsonRespone(JSONObject o, String callback, String code) { Properties p = getLastRequestParms(); if (p != null && p.containsKey("tag")) { o.put("tag", p.get("tag")); } NanoHTTPD.Response r = new NanoHTTPD.Response(code, MIME_JSON, callback(callback, o.toJSONString())); r.addHeader("Access-Control-Allow-Origin", "*"); return r; } public NanoHTTPD.Response jsonRespone(JSONObject o, String callback) { return jsonRespone(o, callback, HTTP_OK); } @SuppressWarnings("unchecked") public JSONObject serveAPICall(String calledMethod, Object args) { try { if (caller.methodExists(calledMethod)) { if (!(args instanceof JSONArray)) { args = new JSONArray(); } Object result = caller.call(calledMethod, (Object[]) ((ArrayList<Object>) args).toArray(new Object[((ArrayList<Object>) args).size()])); return returnAPISuccess(calledMethod, result); } else { warning("The method '" + calledMethod + "' does not exist!"); return returnAPIError(calledMethod, "The method '" + calledMethod + "' does not exist!"); } } catch (APIException e) { return returnAPIError(calledMethod, e.getMessage()); } catch (InvocationTargetException e) { if (e.getCause() instanceof APIException) { return returnAPIError(calledMethod, e.getCause().getMessage()); } return returnAPIException(calledMethod, e); } catch (NullPointerException e) { return returnAPIError(calledMethod, "The server is offline right now. Try again in 2 seconds."); } catch (Throwable e) { return returnAPIException(calledMethod, e); } } }