package org.namelessrom.devicecontrol.net;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.BatteryManager;
import android.os.Environment;
import android.text.TextUtils;
import android.util.Base64;
import com.google.gson.Gson;
import com.koushikdutta.async.AsyncServerSocket;
import com.koushikdutta.async.callback.CompletedCallback;
import com.koushikdutta.async.http.WebSocket;
import com.koushikdutta.async.http.server.AsyncHttpServer;
import com.koushikdutta.async.http.server.AsyncHttpServerRequest;
import com.koushikdutta.async.http.server.AsyncHttpServerResponse;
import com.koushikdutta.async.http.server.HttpServerRequestCallback;
import org.namelessrom.devicecontrol.App;
import org.namelessrom.devicecontrol.models.WebServerConfig;
import org.namelessrom.devicecontrol.services.WebServerService;
import org.namelessrom.devicecontrol.utils.ContentTypes;
import org.namelessrom.devicecontrol.utils.HtmlHelper;
import org.namelessrom.devicecontrol.utils.SortHelper;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import at.amartinz.hardware.device.Device;
import at.amartinz.hardware.utils.HwIoUtils;
import timber.log.Timber;
/**
* A wrapper for the AsyncHttpServer
*/
public class ServerWrapper {
public static final String ACTION_CONNECTED = "---CONNECTED---";
public static final String ACTION_TERMINATING = "---TERMINATING---";
public boolean isStopped = false;
private static final ArrayList<WebSocket> _sockets = new ArrayList<>();
private AsyncHttpServer mServer;
private AsyncServerSocket mServerSocket;
private final WebServerService mService;
private WebServerConfig webServerConfig;
public ServerWrapper(final WebServerService service) {
mService = service;
}
public void stopServer() {
unregisterReceivers();
if (mServer != null) {
mServer.stop();
mServer = null;
}
if (mServerSocket != null) {
mServerSocket.stop();
mServerSocket = null;
}
for (final WebSocket socket : _sockets) {
if (socket == null) { continue; }
socket.send(ACTION_TERMINATING);
socket.close();
}
_sockets.clear();
isStopped = true;
}
public void createServer() {
/*Thread thread = new Thread(new Runnable() {
@Override public void run() {
createServerAsync();
}
});
thread.start();*/
createServerAsync();
}
private void createServerAsync() {
if (mServer != null) {
return;
}
webServerConfig = WebServerConfig.get();
mServer = new AsyncHttpServer();
Timber.v("[!] Server created");
setupFonts();
Timber.v("[!] Setup fonts");
setupWebSockets();
Timber.v("[!] Setup websockets");
setupApi();
Timber.v("[!] Setup api");
mServer.directory(App.get(), "/license", "license.html");
Timber.v("[!] Setup route: /license");
mServer.get("/files", new HttpServerRequestCallback() {
@Override public void onRequest(final AsyncHttpServerRequest req, final AsyncHttpServerResponse res) {
res.redirect("/files/");
}
});
mServer.get("/files/(?s).*", filesCallback);
Timber.v("[!] Setup route: /files/(?s).*");
mServer.get("/information", informationCallback);
Timber.v("[!] Setup route: /information");
// should be always the last, matches anything that the stuff above did not
mServer.get("/(?s).*", mainCallback);
Timber.v("[!] Setup route: /");
mServerSocket = mServer.listen(WebServerConfig.get().port);
mService.setNotification(null);
registerReceivers();
}
private final HttpServerRequestCallback mainCallback = new HttpServerRequestCallback() {
@Override public void onRequest(final AsyncHttpServerRequest req, final AsyncHttpServerResponse res) {
if (!shouldPass(req, res)) {
return;
}
Timber.v("[+] Received connection from: %s", req.getHeaders().get("User-Agent"));
final String path = remapPath(req.getPath());
res.getHeaders().set("Content-Type", ContentTypes.getInstance().getContentType(path));
final InputStream is = HtmlHelper.loadPath(path);
if (is != null) {
try {
res.sendStream(is, is.available());
return;
} catch (IOException ioe) {
Timber.e(ioe, "Error!");
} finally {
HwIoUtils.closeQuietly(is);
}
}
res.send(HtmlHelper.loadPathAsString(path));
}
};
private final HttpServerRequestCallback filesCallback = new HttpServerRequestCallback() {
@Override public void onRequest(final AsyncHttpServerRequest req, final AsyncHttpServerResponse res) {
if (!shouldPass(req, res)) {
return;
}
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
res.send("sdcard not mounted!");
return;
}
boolean isDirectory = true;
final String filePath = HtmlHelper.urlDecode(req.getPath()).replace("/files/", "");
Timber.v("req.getPath(): %s", req.getPath());
Timber.v("filePath: %s", filePath);
File file;
String sdRoot;
if (webServerConfig.root) {
file = new File("/");
sdRoot = "";
} else {
file = Environment.getExternalStorageDirectory();
sdRoot = file.getAbsolutePath();
}
if (filePath != null && !filePath.isEmpty()) {
file = new File(file, filePath);
if (file.exists()) {
isDirectory = file.isDirectory();
} else {
res.send("File or directory does not exist!");
return;
}
}
if (isDirectory) {
final File[] fs = file.listFiles();
if (fs == null) {
res.send("An error occured!");
return;
}
final List<File> directories = new ArrayList<>();
final List<File> files = new ArrayList<>();
for (final File f : fs) {
if (f.exists()) {
if (f.isDirectory()) {
directories.add(f);
} else {
files.add(f);
}
}
}
final ArrayList<FileEntry> fileEntries = new ArrayList<>();
if (directories.size() > 0) {
Collections.sort(directories, SortHelper.sFileComparator);
for (final File f : directories) {
fileEntries.add(new FileEntry(f.getName(), f.getAbsolutePath().replace(sdRoot, ""), true));
}
}
if (files.size() > 0) {
Collections.sort(files, SortHelper.sFileComparator);
for (final File f : files) {
fileEntries.add(new FileEntry(f.getName(), f.getAbsolutePath().replace(sdRoot, ""), false));
}
}
res.send(new Gson().toJson(fileEntries));
} else {
final String contentType = ContentTypes.getInstance().getContentType(file.getAbsolutePath());
Timber.v("Requested file: %s", file.getName());
Timber.v("Content-Type: %s", contentType);
res.setContentType(contentType);
res.sendFile(file);
}
}
};
private static final HttpServerRequestCallback informationCallback = new HttpServerRequestCallback() {
@Override public void onRequest(final AsyncHttpServerRequest req, final AsyncHttpServerResponse res) {
}
};
private boolean shouldPass(final AsyncHttpServerRequest req, final AsyncHttpServerResponse res) {
if (isStopped) {
res.code(404);
res.end();
return false;
}
if (!isAuthenticated(req)) {
res.getHeaders().add("WWW-Authenticate", "Basic realm=\"DeviceControl\"");
res.code(401);
res.end();
return false;
}
return true;
}
private String remapPath(final String path) {
if (TextUtils.equals("/", path)) {
return "index.html";
}
return path;
}
private void setupFonts() {
final Context context = mService.getApplicationContext();
// Bootstrap glyphicons
mServer.directory(context, "/fonts/glyphicons-halflings-regular.eot", "fonts/glyphicons-halflings-regular.eot");
mServer.directory(context, "/fonts/glyphicons-halflings-regular.svg", "fonts/glyphicons-halflings-regular.svg");
mServer.directory(context, "/fonts/glyphicons-halflings-regular.ttf", "fonts/glyphicons-halflings-regular.ttf");
mServer.directory(context, "/fonts/glyphicons-halflings-regular.woff", "fonts/glyphicons-halflings-regular.woff");
// FontAwesome
mServer.directory(context, "/fonts/FontAwesome.otf", "fonts/FontAwesome.otf");
mServer.directory(context, "/fonts/fontawesome-webfont.eot", "fonts/fontawesome-webfont.eot");
mServer.directory(context, "/fonts/fontawesome-webfont.svg", "fonts/fontawesome-webfont.svg");
mServer.directory(context, "/fonts/fontawesome-webfont.ttf", "fonts/fontawesome-webfont.ttf");
mServer.directory(context, "/fonts/fontawesome-webfont.woff", "fonts/fontawesome-webfont.woff");
}
private void setupWebSockets() {
mServer.websocket("/live", new AsyncHttpServer.WebSocketRequestCallback() {
@Override public void onConnected(final WebSocket socket, AsyncHttpServerRequest req) {
_sockets.add(socket);
if (_sockets.size() == 1) {
// first client connected, register receivers
registerReceivers();
}
socket.setClosedCallback(new CompletedCallback() {
@Override public void onCompleted(final Exception ex) {
_sockets.remove(socket);
if (_sockets.size() == 0) {
// No client left, unregister to save battery
unregisterReceivers();
}
}
});
socket.setStringCallback(new WebSocket.StringCallback() {
@Override public void onStringAvailable(final String s) {
Timber.v(s);
//noinspection StatementWithEmptyBody
if (ACTION_CONNECTED.equals(s)) {
//TODO: initializing
}
}
});
socket.send(ACTION_CONNECTED);
}
});
}
private void setupApi() {
mServer.get("/api", new HttpServerRequestCallback() {
@Override public void onRequest(final AsyncHttpServerRequest req,
final AsyncHttpServerResponse res) {
res.redirect("/api/device");
}
});
mServer.get("/api/device", new HttpServerRequestCallback() {
@Override public void onRequest(final AsyncHttpServerRequest req, final AsyncHttpServerResponse res) {
final String result = Device.get(mService).update().toString();
Timber.v(result);
res.send(result);
}
});
}
private boolean isAuthenticated(final AsyncHttpServerRequest req) {
final boolean isAuth = !webServerConfig.useAuth;
final String authHeader = req.getHeaders().get("Authorization");
if (!isAuth && !TextUtils.isEmpty(authHeader)) {
final String[] parts = new String(Base64.decode(authHeader.replace("Basic", "").trim(), Base64.DEFAULT)).split(":");
return parts[0] != null
&& parts[1] != null
&& parts[0].equals(webServerConfig.username)
&& parts[1].equals(webServerConfig.password);
}
return isAuth;
}
private void registerReceivers() {
if (mService == null) {
Timber.wtf("mService is null!");
return;
}
final Intent sticky = mService.registerReceiver(mBatteryReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
// try to preload battery level
if (sticky != null) {
mBatteryReceiver.onReceive(mService, sticky);
}
}
private void unregisterReceivers() {
if (mService == null) {
Timber.wtf("mService is null!");
return;
}
try {
mService.unregisterReceiver(mBatteryReceiver);
} catch (Exception ignored) { }
}
private final BroadcastReceiver mBatteryReceiver = new BroadcastReceiver() {
@Override public void onReceive(Context context, Intent intent) {
final String level = String.format("batteryLevel|%s", intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 1));
final String charging = String.format("batteryCharging|%s",
intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0 ? "1" : "0");
for (final WebSocket socket : _sockets) {
socket.send(level);
socket.send(charging);
}
}
};
public AsyncHttpServer getServer() { return mServer; }
public AsyncServerSocket getServerSocket() { return mServerSocket; }
private class FileEntry {
public final String name;
public final String path;
public final boolean isDirectory;
public FileEntry(final String name, final String path, final boolean isDirectory) {
this.name = name;
this.path = path;
this.isDirectory = isDirectory;
}
}
}