package net.pms.remote; import com.sun.net.httpserver.*; import java.io.*; import java.net.InetAddress; import java.net.InetSocketAddress; import java.security.AccessController; import java.security.GeneralSecurityException; import java.security.KeyStore; import java.security.PrivilegedAction; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.Executors; import javax.net.ssl.*; import net.pms.PMS; import net.pms.configuration.PmsConfiguration; import net.pms.configuration.RendererConfiguration; import net.pms.configuration.WebRender; import net.pms.dlna.DLNAResource; import net.pms.dlna.DLNAThumbnailInputStream; import net.pms.dlna.RealFile; import net.pms.dlna.RootFolder; import net.pms.image.ImageFormat; import net.pms.network.HTTPResource; import net.pms.newgui.DbgPacker; import net.pms.util.FullyPlayed; import org.apache.commons.configuration.ConfigurationException; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @SuppressWarnings("restriction") public class RemoteWeb { private static final Logger LOGGER = LoggerFactory.getLogger(RemoteWeb.class); private KeyStore ks; private KeyManagerFactory kmf; private TrustManagerFactory tmf; private HttpServer server; private SSLContext sslContext; private Map<String, RootFolder> roots; private RemoteUtil.ResourceManager resources; private static final PmsConfiguration configuration = PMS.getConfiguration(); private static final int defaultPort = configuration.getWebPort(); public RemoteWeb() throws IOException { this(defaultPort); } public RemoteWeb(int port) throws IOException { if (port <= 0) { port = defaultPort; } roots = new HashMap<>(); // Add "classpaths" for resolving web resources resources = AccessController.doPrivileged(new PrivilegedAction<RemoteUtil.ResourceManager>() { public RemoteUtil.ResourceManager run() { return new RemoteUtil.ResourceManager( "file:" + configuration.getProfileDirectory() + "/web/", "jar:file:" + configuration.getProfileDirectory() + "/web.zip!/", "file:" + configuration.getWebPath() + "/" ); } }); //readCred(); // Setup the socket address InetSocketAddress address = new InetSocketAddress(InetAddress.getByName("0.0.0.0"), port); // Initialize the HTTP(S) server if (configuration.getWebHttps()) { try { server = httpsServer(address); } catch (IOException e) { LOGGER.error("Failed to start WEB interface on HTTPS: {}", e.getMessage()); LOGGER.trace("", e); if (e.getMessage().contains("UMS.jks")) { LOGGER.info("To enable HTTPS please generate a self-signed keystore file called \"UMS.jks\" using the java 'keytool' commandline utility."); } } catch (GeneralSecurityException e) { LOGGER.error("Failed to start WEB interface on HTTPS due to a security error: {}", e.getMessage()); LOGGER.trace("", e); } } else { server = HttpServer.create(address, 0); } if (server != null) { int threads = configuration.getWebThreads(); // Add context handlers addCtx("/", new RemoteStartHandler(this)); addCtx("/browse", new RemoteBrowseHandler(this)); RemotePlayHandler playHandler = new RemotePlayHandler(this); addCtx("/play", playHandler); addCtx("/playstatus", playHandler); addCtx("/playlist", playHandler); addCtx("/media", new RemoteMediaHandler(this)); addCtx("/fmedia", new RemoteMediaHandler(this, true)); addCtx("/thumb", new RemoteThumbHandler(this)); addCtx("/raw", new RemoteRawHandler(this)); addCtx("/files", new RemoteFileHandler(this)); addCtx("/doc", new RemoteDocHandler(this)); addCtx("/poll", new RemotePollHandler(this)); server.setExecutor(Executors.newFixedThreadPool(threads)); server.start(); } } private HttpServer httpsServer(InetSocketAddress address) throws IOException, GeneralSecurityException { // Initialize the keystore char[] password = "umsums".toCharArray(); ks = KeyStore.getInstance("JKS"); try (FileInputStream fis = new FileInputStream("UMS.jks")) { ks.load(fis, password); } // Setup the key manager factory kmf = KeyManagerFactory.getInstance("SunX509"); kmf.init(ks, password); // Setup the trust manager factory tmf = TrustManagerFactory.getInstance("SunX509"); tmf.init(ks); HttpsServer server = HttpsServer.create(address, 0); sslContext = SSLContext.getInstance("TLS"); sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); server.setHttpsConfigurator(new HttpsConfigurator(sslContext) { @Override public void configure(HttpsParameters params) { try { // initialise the SSL context SSLContext c = SSLContext.getDefault(); SSLEngine engine = c.createSSLEngine(); params.setNeedClientAuth(true); params.setCipherSuites(engine.getEnabledCipherSuites()); params.setProtocols(engine.getEnabledProtocols()); // get the default parameters SSLParameters defaultSSLParameters = c.getDefaultSSLParameters(); params.setSSLParameters(defaultSSLParameters); } catch (Exception e) { LOGGER.debug("https configure error " + e); } } }); return server; } public String getTag(String user) { String tag = PMS.getCredTag("web", user); //tags.get(user); if (tag == null) { return user; } return tag; } public String getAddress() { return PMS.get().getServer().getHost() + ":" + server.getAddress().getPort(); } public RootFolder getRoot(String user, HttpExchange t) { return getRoot(user, false, t); } public RootFolder getRoot(String user, boolean create, HttpExchange t) { String groupTag = getTag(user); String cookie = RemoteUtil.getCookie("UMS", t); RootFolder root; synchronized (roots) { root = roots.get(cookie); if (root == null) { // Double-check for cookie errors WebRender valid = RemoteUtil.matchRenderer(user, t); if (valid != null) { // A browser of the same type and user is already connected at // this ip but for some reason we didn't get a cookie match. RootFolder validRoot = valid.getRootFolder(); // Do a reverse lookup to see if it's been registered for (Map.Entry<String, RootFolder> entry : roots.entrySet()) { if (entry.getValue() == validRoot) { // Found root = validRoot; cookie = entry.getKey(); LOGGER.debug("Allowing browser connection without cookie match: {}: {}", valid.getRendererName(), t.getRemoteAddress().getAddress()); break; } } } } if (!create || (root != null)) { t.getResponseHeaders().add("Set-Cookie", "UMS=" + cookie + ";Path=/"); return root; } ArrayList<String> tag = new ArrayList<>(); tag.add(user); if (!groupTag.equals(user)) { tag.add(groupTag); } tag.add(t.getRemoteAddress().getHostString()); tag.add("web"); root = new RootFolder(tag); try { WebRender render = new WebRender(user); root.setDefaultRenderer(render); render.setRootFolder(root); render.associateIP(t.getRemoteAddress().getAddress()); render.associatePort(t.getRemoteAddress().getPort()); if (configuration.useWebSubLang()) { render.setSubLang(StringUtils.join(RemoteUtil.getLangs(t), ",")); } // render.setUA(t.getRequestHeaders().getFirst("User-agent")); render.setBrowserInfo(RemoteUtil.getCookie("UMSINFO", t), t.getRequestHeaders().getFirst("User-agent")); PMS.get().setRendererFound(render); } catch (ConfigurationException e) { root.setDefaultRenderer(RendererConfiguration.getDefaultConf()); } //root.setDefaultRenderer(RendererConfiguration.getRendererConfigurationByName("web")); root.discoverChildren(); cookie = UUID.randomUUID().toString(); t.getResponseHeaders().add("Set-Cookie", "UMS=" + cookie + ";Path=/"); roots.put(cookie, root); } return root; } public void associate(HttpExchange t, WebRender webRenderer) { webRenderer.associateIP(t.getRemoteAddress().getAddress()); webRenderer.associatePort(t.getRemoteAddress().getPort()); } private void addCtx(String path, HttpHandler h) { HttpContext ctx = server.createContext(path, h); if (configuration.isWebAuthenticate()) { ctx.setAuthenticator(new BasicAuthenticator("") { @Override public boolean checkCredentials(String user, String pwd) { LOGGER.debug("authenticate " + user); return PMS.verifyCred("web", PMS.getCredTag("web", user), user, pwd); //return pwd.equals(users.get(user)); //return true; } }); } } public HttpServer getServer() { return server; } static class RemoteThumbHandler implements HttpHandler { private RemoteWeb parent; public RemoteThumbHandler(RemoteWeb parent) { this.parent = parent; } @Override public void handle(HttpExchange t) throws IOException { if (RemoteUtil.deny(t)) { throw new IOException("Access denied"); } String id = RemoteUtil.getId("thumb/", t); LOGGER.trace("web thumb req " + id); if (id.contains("logo")) { RemoteUtil.sendLogo(t); return; } RootFolder root = parent.getRoot(RemoteUtil.userName(t), t); if (root == null) { LOGGER.debug("weird root in thumb req"); throw new IOException("Unknown root"); } final DLNAResource r = root.getDLNAResource(id, root.getDefaultRenderer()); if (r == null) { // another error LOGGER.debug("media unknown"); throw new IOException("Bad id"); } DLNAThumbnailInputStream in; if (!configuration.isShowCodeThumbs() && !r.isCodeValid(r)) { // we shouldn't show the thumbs for coded objects // unless the code is entered in = r.getGenericThumbnailInputStream(null); } else { r.checkThumbnail(); in = r.fetchThumbnailInputStream(); } if (r instanceof RealFile && FullyPlayed.isFullyPlayedThumbnail(((RealFile) r).getFile())) { in = FullyPlayed.addFullyPlayedOverlay(in); } Headers hdr = t.getResponseHeaders(); hdr.add("Content-Type", ImageFormat.PNG.equals(in.getFormat()) ? HTTPResource.PNG_TYPEMIME : HTTPResource.JPEG_TYPEMIME); hdr.add("Accept-Ranges", "bytes"); hdr.add("Connection", "keep-alive"); t.sendResponseHeaders(200, in.getSize()); OutputStream os = t.getResponseBody(); LOGGER.trace("Web thumbnail: Input is {} output is {}", in, os); RemoteUtil.dump(in, os); } } static class RemoteFileHandler implements HttpHandler { private RemoteWeb parent; public RemoteFileHandler(RemoteWeb parent) { this.parent = parent; } @Override public void handle(HttpExchange t) throws IOException { LOGGER.debug("Handling web interface file request \"{}\"", t.getRequestURI()); String path = t.getRequestURI().getPath(); String response = null; String mime = null; int status = 200; if (path.contains("crossdomain.xml")) { response = "<?xml version=\"1.0\"?>" + "<!-- http://www.bitsontherun.com/crossdomain.xml -->" + "<cross-domain-policy>" + "<allow-access-from domain=\"*\" />" + "</cross-domain-policy>"; mime = "text/xml"; } else if (path.startsWith("/files/log/")) { String filename = path.substring(11); if (filename.equals("info")) { String log = PMS.get().getFrame().getLog(); log = log.replaceAll("\n", "<br>"); String fullLink = "<br><a href=\"/files/log/full\">Full log</a><br><br>"; String x = fullLink + log; if (StringUtils.isNotEmpty(log)) { x = x + fullLink; } response = "<html><title>UMS LOG</title><body>" + x + "</body></html>"; } else { File file = parent.getResources().getFile(filename); if (file != null) { filename = file.getName(); HashMap<String, Object> vars = new HashMap<>(); vars.put("title", filename); vars.put("brush", filename.endsWith("debug.log") ? "debug_log" : filename.endsWith(".log") ? "log" : "conf"); vars.put("log", RemoteUtil.read(file).replace("<", "<")); response = parent.getResources().getTemplate("util/log.html").execute(vars); } else { status = 404; } } mime = "text/html"; } else if (parent.getResources().write(path.substring(7), t)) { // The resource manager found and sent the file, all done. return; } else { status = 404; } if (status == 404 && response == null) { response = "<html><body>404 - File Not Found: " + path + "</body></html>"; mime = "text/html"; } RemoteUtil.respond(t, response, status, mime); } } static class RemoteStartHandler implements HttpHandler { private static final Logger LOGGER = LoggerFactory.getLogger(RemoteStartHandler.class); @SuppressWarnings("unused") private final static String CRLF = "\r\n"; private RemoteWeb parent; public RemoteStartHandler(RemoteWeb parent) { this.parent = parent; } @Override public void handle(HttpExchange t) throws IOException { LOGGER.debug("root req " + t.getRequestURI()); if (RemoteUtil.deny(t)) { throw new IOException("Access denied"); } if (t.getRequestURI().getPath().contains("favicon")) { RemoteUtil.sendLogo(t); return; } HashMap<String, Object> vars = new HashMap<>(); vars.put("serverName", configuration.getServerDisplayName()); String response = parent.getResources().getTemplate("start.html").execute(vars); RemoteUtil.respond(t, response, 200, "text/html"); } } static class RemoteDocHandler implements HttpHandler { private static final Logger LOGGER = LoggerFactory.getLogger(RemoteDocHandler.class); @SuppressWarnings("unused") private final static String CRLF = "\r\n"; private RemoteWeb parent; public RemoteDocHandler(RemoteWeb parent) { this.parent = parent; // Make sure logs are available right away getLogs(false); } @Override public void handle(HttpExchange t) throws IOException { LOGGER.debug("root req " + t.getRequestURI()); if (RemoteUtil.deny(t)) { throw new IOException("Access denied"); } if (t.getRequestURI().getPath().contains("favicon")) { RemoteUtil.sendLogo(t); return; } HashMap<String, Object> vars = new HashMap<>(); vars.put("logs", getLogs(true)); if (configuration.getUseCache()) { vars.put("cache", "http://" + PMS.get().getServer().getHost() + ":" + PMS.get().getServer().getPort() + "/console/home"); } String response = parent.getResources().getTemplate("doc.html").execute(vars); RemoteUtil.respond(t, response, 200, "text/html"); } private ArrayList<HashMap<String, String>> getLogs(boolean asList) { Set<File> files = new DbgPacker().getItems(); ArrayList<HashMap<String, String>> logs = asList ? new ArrayList<HashMap<String, String>>() : null; for (File f : files) { if (f.exists()) { String id = String.valueOf(parent.getResources().add(f)); if (asList) { HashMap<String, String> item = new HashMap<>(); item.put("filename", f.getName()); item.put("id", id); logs.add(item); } } } return logs; } } public RemoteUtil.ResourceManager getResources() { return resources; } public String getUrl() { if (server != null) { return (server instanceof HttpsServer ? "https://" : "http://") + PMS.get().getServer().getHost() + ":" + server.getAddress().getPort(); } return null; } static class RemotePollHandler implements HttpHandler { @SuppressWarnings("unused") private static final Logger LOGGER = LoggerFactory.getLogger(RemotePollHandler.class); @SuppressWarnings("unused") private final static String CRLF = "\r\n"; private RemoteWeb parent; public RemotePollHandler(RemoteWeb parent) { this.parent = parent; } @Override public void handle(HttpExchange t) throws IOException { //LOGGER.debug("poll req " + t.getRequestURI()); if (RemoteUtil.deny(t)) { throw new IOException("Access denied"); } RootFolder root = parent.getRoot(RemoteUtil.userName(t), t); WebRender renderer = (WebRender) root.getDefaultRenderer(); String json = renderer.getPushData(); RemoteUtil.respond(t, json, 200, "text"); } } }