/*
* Protocoder
* A prototyping platform for Android devices
*
* Victor Diaz Barrales victormdb@gmail.com
*
* Copyright (C) 2014 Victor Diaz
* Copyright (C) 2013 Motorola Mobility LLC
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the Software
* is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
package org.protocoder.network;
import android.content.Context;
import android.content.res.AssetManager;
import org.apache.commons.lang3.StringEscapeUtils;
import org.json.JSONArray;
import org.json.JSONObject;
import org.protocoderrunner.apidoc.APIManager;
import org.protocoderrunner.apprunner.api.PApp;
import org.protocoderrunner.apprunner.api.PBoards;
import org.protocoderrunner.apprunner.api.PConsole;
import org.protocoderrunner.apprunner.api.PDashboard;
import org.protocoderrunner.apprunner.api.PDevice;
import org.protocoderrunner.apprunner.api.PFileIO;
import org.protocoderrunner.apprunner.api.PMedia;
import org.protocoderrunner.apprunner.api.PNetwork;
import org.protocoderrunner.apprunner.api.PProtocoder;
import org.protocoderrunner.apprunner.api.PSensors;
import org.protocoderrunner.apprunner.api.PUI;
import org.protocoderrunner.apprunner.api.PUtil;
import org.protocoderrunner.apprunner.api.boards.PArduino;
import org.protocoderrunner.apprunner.api.boards.PIOIO;
import org.protocoderrunner.apprunner.api.boards.PSerial;
import org.protocoderrunner.apprunner.api.dashboard.PDashboardBackground;
import org.protocoderrunner.apprunner.api.dashboard.PDashboardButton;
import org.protocoderrunner.apprunner.api.dashboard.PDashboardCustomWidget;
import org.protocoderrunner.apprunner.api.dashboard.PDashboardHTML;
import org.protocoderrunner.apprunner.api.dashboard.PDashboardImage;
import org.protocoderrunner.apprunner.api.dashboard.PDashboardInput;
import org.protocoderrunner.apprunner.api.dashboard.PDashboardPlot;
import org.protocoderrunner.apprunner.api.dashboard.PDashboardSlider;
import org.protocoderrunner.apprunner.api.dashboard.PDashboardText;
import org.protocoderrunner.apprunner.api.dashboard.PDashboardVideoCamera;
import org.protocoderrunner.apprunner.api.other.PCamera;
import org.protocoderrunner.apprunner.api.other.PDeviceEditor;
import org.protocoderrunner.apprunner.api.other.PEvents;
import org.protocoderrunner.apprunner.api.other.PLiveCodingFeedback;
import org.protocoderrunner.apprunner.api.other.PMidi;
import org.protocoderrunner.apprunner.api.other.PProcessing;
import org.protocoderrunner.apprunner.api.other.PPureData;
import org.protocoderrunner.apprunner.api.other.PSimpleHttpServer;
import org.protocoderrunner.apprunner.api.other.PSocketIOClient;
import org.protocoderrunner.apprunner.api.other.PSqLite;
import org.protocoderrunner.apprunner.api.other.PVideo;
import org.protocoderrunner.apprunner.api.other.PWebEditor;
import org.protocoderrunner.apprunner.api.widgets.PAbsoluteLayout;
import org.protocoderrunner.apprunner.api.widgets.PButton;
import org.protocoderrunner.apprunner.api.widgets.PCanvasView;
import org.protocoderrunner.apprunner.api.widgets.PCard;
import org.protocoderrunner.apprunner.api.widgets.PCheckBox;
import org.protocoderrunner.apprunner.api.widgets.PEditText;
import org.protocoderrunner.apprunner.api.widgets.PGrid;
import org.protocoderrunner.apprunner.api.widgets.PGridRow;
import org.protocoderrunner.apprunner.api.widgets.PImageButton;
import org.protocoderrunner.apprunner.api.widgets.PImageView;
import org.protocoderrunner.apprunner.api.widgets.PList;
import org.protocoderrunner.apprunner.api.widgets.PListItem;
import org.protocoderrunner.apprunner.api.widgets.PMap;
import org.protocoderrunner.apprunner.api.widgets.PNumberPicker;
import org.protocoderrunner.apprunner.api.widgets.PPadView;
import org.protocoderrunner.apprunner.api.widgets.PPlotView;
import org.protocoderrunner.apprunner.api.widgets.PPopupCustomFragment;
import org.protocoderrunner.apprunner.api.widgets.PProgressBar;
import org.protocoderrunner.apprunner.api.widgets.PRadioButton;
import org.protocoderrunner.apprunner.api.widgets.PRow;
import org.protocoderrunner.apprunner.api.widgets.PScrollView;
import org.protocoderrunner.apprunner.api.widgets.PSlider;
import org.protocoderrunner.apprunner.api.widgets.PSpinner;
import org.protocoderrunner.apprunner.api.widgets.PSwitch;
import org.protocoderrunner.apprunner.api.widgets.PTextView;
import org.protocoderrunner.apprunner.api.widgets.PToggleButton;
import org.protocoderrunner.apprunner.api.widgets.PVerticalSeekbar;
import org.protocoderrunner.apprunner.api.widgets.PWebView;
import org.protocoderrunner.apprunner.api.widgets.PWindow;
import org.protocoderrunner.events.Events;
import org.protocoderrunner.events.Events.ProjectEvent;
import org.protocoderrunner.network.NanoHTTPD;
import org.protocoderrunner.network.NetworkUtils;
import org.protocoderrunner.project.Project;
import org.protocoderrunner.project.ProjectManager;
import org.protocoderrunner.utils.FileIO;
import org.protocoderrunner.utils.MLog;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import de.greenrobot.event.EventBus;
/**
* An example of subclassing NanoHTTPD to make a custom HTTP server.
*/
public class ProtocoderHttpServer extends NanoHTTPD {
public static final String TAG = "myHTTPServer";
private final WeakReference<Context> ctx;
private final String WEBAPP_DIR = "webapp/";
String projectURLPrefix = "/apps";
private static final Map<String, String> MIME_TYPES = new HashMap<String, String>() {
{
put("css", "text/css");
put("htm", "text/html");
put("html", "text/html");
put("xml", "text/xml");
put("txt", "text/plain");
put("asc", "text/plain");
put("gif", "image/gif");
put("jpg", "image/jpeg");
put("jpeg", "image/jpeg");
put("png", "image/png");
put("mp3", "audio/mpeg");
put("m3u", "audio/mpeg-url");
put("mp4", "video/mp4");
put("ogv", "video/ogg");
put("flv", "video/x-flv");
put("mov", "video/quicktime");
put("swf", "application/x-shockwave-flash");
put("js", "application/javascript");
put("pdf", "application/pdf");
put("doc", "application/msword");
put("ogg", "application/x-ogg");
put("zip", "application/octet-stream");
put("exe", "application/octet-stream");
put("class", "application/octet-stream");
}
};
private static ProtocoderHttpServer instance;
public static ProtocoderHttpServer getInstance(Context aCtx, int port) {
MLog.d(TAG, "launching web server...");
if (instance == null) {
try {
MLog.d(TAG, "ok...");
instance = new ProtocoderHttpServer(aCtx, port);
} catch (IOException e) {
MLog.d(TAG, "nop :(...");
e.printStackTrace();
}
}
return instance;
}
public ProtocoderHttpServer(Context aCtx, int port) throws IOException {
super(port);
ctx = new WeakReference<Context>(aCtx);
String ip = NetworkUtils.getLocalIpAddress(aCtx);
if (ip == null) {
MLog.d(TAG, "No IP found. Please connect to a newwork and try again");
} else {
MLog.d(TAG, "Launched server at http://" + ip.toString() + ":" + port);
}
}
@Override
public Response serve(String uri, String method, Properties header, Properties parms, Properties files) {
Response res = null;
try {
MLog.d(TAG, "received String" + uri + " " + method + " " + header + " " + " " + parms + " " + files);
if (uri.startsWith(projectURLPrefix)) {
// checking if we are inside the directory so we sandbox the app
// TODO its pretty hack so this deserves coding it again
Project p = ProjectManager.getInstance().getCurrentProject();
String projectFolder = "/" + p.getFolder() + "/" + p.getName();
// MLog.d("qq", "project folder is " + projectFolder);
if (uri.replace(projectURLPrefix, "").contains(projectFolder)) {
// MLog.d("qq", "inside project");
return serveFile(uri.substring(uri.lastIndexOf('/') + 1, uri.length()), header,
new File(p.getStoragePath()), false);
} else {
// MLog.d("qq", "outside project");
new Response(HTTP_NOTFOUND, MIME_HTML, "resource not found");
}
}
// file upload
if (!files.isEmpty()) {
String name = parms.getProperty("name").toString();
String folder = parms.getProperty("fileType").toString();
Project p = ProjectManager.getInstance().get(folder, name);
File src = new File(files.getProperty("pic").toString());
File dst = new File(p.getStoragePath() + "/" + parms.getProperty("pic").toString());
FileIO.copyFile(src, dst);
JSONObject data = new JSONObject();
data.put("result", "OK");
return new Response("200", MIME_TYPES.get("txt"), data.toString());
}
// webapi
JSONObject data = new JSONObject();
// splitting the string into command and parameters
String[] m = uri.split("=");
String userCmd = m[0];
String params = "";
if (m.length > 1) {
params = m[1];
}
MLog.d(TAG, "cmd " + userCmd);
if (userCmd.contains("cmd")) {
JSONObject obj = new JSONObject(params);
Project foundProject;
String name;
String url;
String newCode;
String folder;
MLog.d(TAG, "params " + obj.toString(2));
String cmd = obj.getString("cmd");
// fetch code
if (cmd.equals("fetch_code")) {
MLog.d(TAG, "--> fetch code");
name = obj.getString("name");
folder = obj.getString("type");
Project p = ProjectManager.getInstance().get(folder, name);
// TODO add type
data.put("code", ProjectManager.getInstance().getCode(p));
// list apps
} else if (cmd.equals("list_apps")) {
MLog.d(TAG, "--> list apps");
folder = obj.getString("filter");
ArrayList<Project> projects = ProjectManager.getInstance().list(folder, false);
JSONArray projectsArray = new JSONArray();
for (Project project : projects) {
projectsArray.put(ProjectManager.getInstance().toJson(project));
}
data.put("projects", projectsArray);
// run app
} else if (cmd.equals("run_app")) {
MLog.d(TAG, "--> run app");
name = obj.getString("name");
folder = obj.getString("type");
Project p = ProjectManager.getInstance().get(folder, name);
ProjectManager.getInstance().setRemoteIP(obj.getString("remoteIP"));
ProjectEvent evt = new ProjectEvent(p, "run");
EventBus.getDefault().post(evt);
MLog.i(TAG, "Running...");
// execute app
} else if (cmd.equals("execute_code")) {
MLog.d(TAG, "--> execute code");
// Save and run
String code = parms.get("codeToSend").toString();
Events.ExecuteCodeEvent evt = new Events.ExecuteCodeEvent(code);
EventBus.getDefault().post(evt);
MLog.i(TAG, "Execute...");
// save_code
} else if (cmd.equals("push_code")) {
MLog.d(TAG, "--> push code " + method + " " + header);
name = parms.get("name").toString();
String fileName = parms.get("fileName").toString();
newCode = parms.get("code").toString();
MLog.d(TAG, "fileName -> " + fileName);
// MLog.d(TAG, "code -> " + newCode);
folder = parms.get("type").toString();
// add type
Project p = ProjectManager.getInstance().get(folder, name);
ProjectManager.getInstance().writeNewCode(p, newCode, fileName);
data.put("project", ProjectManager.getInstance().toJson(p));
ProjectEvent evt = new ProjectEvent(p, "save");
EventBus.getDefault().post(evt);
MLog.i(TAG, "Saved");
// list files in project
} else if (cmd.equals("list_files_in_project")) {
MLog.d(TAG, "--> create new project");
name = obj.getString("name");
folder = obj.getString("type");
Project p = new Project(folder, name);
JSONArray array = ProjectManager.getInstance().listFilesInProjectJSON(p);
data.put("files", array);
// ProjectEvent evt = new ProjectEvent(p, "new");
// EventBus.getDefault().post(evt);
} else if (cmd.equals("create_new_project")) {
MLog.d(TAG, "--> create new project");
name = obj.getString("name");
Project p = new Project(ProjectManager.FOLDER_USER_PROJECTS, name);
ProjectEvent evt = new ProjectEvent(p, "new");
EventBus.getDefault().post(evt);
Project newProject = ProjectManager.getInstance().addNewProject(ctx.get(), name, ProjectManager.FOLDER_USER_PROJECTS, name);
// remove app
} else if (cmd.equals("remove_app")) {
MLog.d(TAG, "--> remove app");
name = obj.getString("name");
folder = obj.getString("type");
Project p = new Project(folder, name);
ProjectManager.getInstance().deleteProject(p);
ProjectEvent evt = new ProjectEvent(p, "update");
EventBus.getDefault().post(evt);
// rename app
} else if (cmd.equals("rename_app")) {
MLog.d(TAG, "--> rename app");
// get help
} else if (cmd.equals("get_documentation")) {
MLog.d(TAG, "--> get documentation");
// TODO do it automatically
//main objects
APIManager.getInstance().clear();
APIManager.getInstance().addClass(PApp.class, true);
APIManager.getInstance().addClass(PBoards.class, true);
APIManager.getInstance().addClass(PConsole.class, true);
APIManager.getInstance().addClass(PDashboard.class, true);
APIManager.getInstance().addClass(PDevice.class, true);
APIManager.getInstance().addClass(PFileIO.class, true);
APIManager.getInstance().addClass(PMedia.class, true);
APIManager.getInstance().addClass(PNetwork.class, true);
APIManager.getInstance().addClass(PProtocoder.class, true);
APIManager.getInstance().addClass(PSensors.class, true);
APIManager.getInstance().addClass(PUI.class, true);
APIManager.getInstance().addClass(PUtil.class, true);
//boards
APIManager.getInstance().addClass(PArduino.class, false);
APIManager.getInstance().addClass(PSerial.class, false);
APIManager.getInstance().addClass(PIOIO.class, false);
//other
APIManager.getInstance().addClass(PCamera.class, false);
APIManager.getInstance().addClass(PDeviceEditor.class, false);
APIManager.getInstance().addClass(PEvents.class, false);
APIManager.getInstance().addClass(PMidi.class, false);
APIManager.getInstance().addClass(PProcessing.class, false);
APIManager.getInstance().addClass(PLiveCodingFeedback.class, false);
APIManager.getInstance().addClass(PPureData.class, false);
APIManager.getInstance().addClass(PSimpleHttpServer.class, false);
APIManager.getInstance().addClass(PSocketIOClient.class, false);
APIManager.getInstance().addClass(PSqLite.class, false);
APIManager.getInstance().addClass(PVideo.class, false);
APIManager.getInstance().addClass(PWebEditor.class, false);
//widgets
APIManager.getInstance().addClass(PAbsoluteLayout.class, false);
APIManager.getInstance().addClass(PButton.class, false);
APIManager.getInstance().addClass(PCanvasView.class, false);
APIManager.getInstance().addClass(PCard.class, false);
APIManager.getInstance().addClass(PCheckBox.class, false);
APIManager.getInstance().addClass(PEditText.class, false);
APIManager.getInstance().addClass(PGrid.class, false);
APIManager.getInstance().addClass(PGridRow.class, false);
APIManager.getInstance().addClass(PImageButton.class, false);
APIManager.getInstance().addClass(PImageView.class, false);
// TODO add plist item
APIManager.getInstance().addClass(PList.class, false);
APIManager.getInstance().addClass(PListItem.class, false);
APIManager.getInstance().addClass(PMap.class, false);
APIManager.getInstance().addClass(PNumberPicker.class, false);
APIManager.getInstance().addClass(PPadView.class, false);
APIManager.getInstance().addClass(PPlotView.class, false);
APIManager.getInstance().addClass(PPopupCustomFragment.class, false);
APIManager.getInstance().addClass(PProgressBar.class, false);
APIManager.getInstance().addClass(PRadioButton.class, false);
APIManager.getInstance().addClass(PRow.class, false);
APIManager.getInstance().addClass(PScrollView.class, false);
APIManager.getInstance().addClass(PSlider.class, false);
APIManager.getInstance().addClass(PSpinner.class, false);
APIManager.getInstance().addClass(PSwitch.class, false);
APIManager.getInstance().addClass(PTextView.class, false);
APIManager.getInstance().addClass(PToggleButton.class, false);
APIManager.getInstance().addClass(PVerticalSeekbar.class, false);
APIManager.getInstance().addClass(PWebView.class, false);
APIManager.getInstance().addClass(PWindow.class, false);
//dashboard
APIManager.getInstance().addClass(PDashboardBackground.class, false);
APIManager.getInstance().addClass(PDashboardButton.class, false);
APIManager.getInstance().addClass(PDashboardCustomWidget.class, false);
APIManager.getInstance().addClass(PDashboardHTML.class, false);
APIManager.getInstance().addClass(PDashboardImage.class, false);
APIManager.getInstance().addClass(PDashboardInput.class, false);
APIManager.getInstance().addClass(PDashboardPlot.class, false);
APIManager.getInstance().addClass(PDashboardSlider.class, false);
APIManager.getInstance().addClass(PDashboardText.class, false);
APIManager.getInstance().addClass(PDashboardVideoCamera.class, false);
data.put("api", APIManager.getInstance().getDocumentation());
}
res = new Response("200", MIME_TYPES.get("txt"), data.toString());
} else if (uri.contains("apps")) {
String[] u = uri.split("/");
// server webui
} else {
res = sendWebAppFile(uri, method, header, parms, files);
}
} catch (Exception e) {
MLog.d(TAG, "response error " + e.toString());
}
// return serveFile(uri, header, servingFolder, true);
return res;
}
private Response sendProjectFile(String uri, String method, Properties header, Properties parms, Properties files) {
Response res = null;
// Clean up uri
uri = uri.trim().replace(File.separatorChar, '/');
MLog.d(TAG, uri);
// have the object build the directory structure, if needed.
AssetManager am = ctx.get().getAssets();
try {
MLog.d(TAG, WEBAPP_DIR + uri);
InputStream fi = am.open(WEBAPP_DIR + uri);
// Get MIME type from file name extension, if possible
String mime = null;
int dot = uri.lastIndexOf('.');
if (dot >= 0) {
mime = MIME_TYPES.get(uri.substring(dot + 1).toLowerCase());
}
if (mime == null) {
mime = NanoHTTPD.MIME_DEFAULT_BINARY;
}
res = new Response(HTTP_OK, mime, fi);
} catch (IOException e) {
e.printStackTrace();
MLog.d(TAG, e.getStackTrace().toString());
res = new Response(HTTP_INTERNALERROR, "text/html", "ERROR: " + e.getMessage());
}
return res;
}
private Response sendWebAppFile(String uri, String method, Properties header, Properties parms, Properties files) {
Response res = null;
MLog.d(TAG, "" + method + " '" + uri + " " + /* header + */" " + parms);
String escapedCode = parms.getProperty("code");
String unescapedCode = StringEscapeUtils.unescapeEcmaScript(escapedCode);
MLog.d("HTTP Code", "" + escapedCode);
MLog.d("HTTP Code", "" + unescapedCode);
// Clean up uri
uri = uri.trim().replace(File.separatorChar, '/');
if (uri.indexOf('?') >= 0) {
uri = uri.substring(0, uri.indexOf('?'));
}
// We never want to request just the '/'
if (uri.length() == 1) {
uri = "index.html";
}
// We're using assets, so we can't have a leading '/'
if (uri.charAt(0) == '/') {
uri = uri.substring(1, uri.length());
}
// have the object build the directory structure, if needed.
AssetManager am = ctx.get().getAssets();
try {
MLog.d(TAG, WEBAPP_DIR + uri);
InputStream fi = am.open(WEBAPP_DIR + uri);
// Get MIME type from file name extension, if possible
String mime = null;
int dot = uri.lastIndexOf('.');
if (dot >= 0) {
mime = MIME_TYPES.get(uri.substring(dot + 1).toLowerCase());
}
if (mime == null) {
mime = NanoHTTPD.MIME_DEFAULT_BINARY;
}
res = new Response(HTTP_OK, mime, fi);
} catch (IOException e) {
e.printStackTrace();
MLog.d(TAG, e.getStackTrace().toString());
res = new Response(HTTP_INTERNALERROR, "text/html", "ERROR: " + e.getMessage());
}
return res;
}
public void close() {
stop();
instance = null;
}
}