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);
}
}
}