package net.pms.remote; import com.samskivert.mustache.Mustache; import com.samskivert.mustache.Template; import com.sun.net.httpserver.Headers; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpPrincipal; import java.io.*; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.net.URLConnection; import java.nio.charset.Charset; import java.util.*; import net.pms.Messages; import net.pms.PMS; import net.pms.configuration.IpFilter; import net.pms.configuration.RendererConfiguration; import net.pms.configuration.WebRender; import net.pms.dlna.DLNAMediaInfo; import net.pms.dlna.Range; import net.pms.newgui.LooksFrame; import net.pms.util.FileWatcher; import net.pms.util.Languages; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class RemoteUtil { private static final Logger LOGGER = LoggerFactory.getLogger(RemoteUtil.class); public static final String MIME_MP4 = "video/mp4"; public static final String MIME_OGG = "video/ogg"; public static final String MIME_WEBM = "video/webm"; //public static final String MIME_TRANS = MIME_MP4; public static final String MIME_TRANS = MIME_OGG; //public static final String MIME_TRANS = MIME_WEBM; public static final String MIME_MP3 = "audio/mpeg"; public static final String MIME_WAV = "audio/wav"; public static final String MIME_PNG = "image/png"; public static final String MIME_JPG = "image/jpeg"; public static void respond(HttpExchange t, String response, int status, String mime) { if (response != null) { respond(t, response.getBytes(), status, mime); } } public static void respond(HttpExchange t, byte[] response, int status, String mime) { if (response != null) { if (mime != null) { Headers hdr = t.getResponseHeaders(); hdr.add("Content-Type", mime); } try (OutputStream os = t.getResponseBody()) { t.sendResponseHeaders(status, response.length); os.write(response); os.close(); } catch (Exception e) { LOGGER.debug("Error sending response: " + e); } } } public static void dumpFile(String file, HttpExchange t) throws IOException { File f = new File(file); dumpFile(f, t); } public static void dumpFile(File f, HttpExchange t) throws IOException { LOGGER.debug("file " + f + " " + f.length()); if (!f.exists()) { throw new IOException("no file"); } t.sendResponseHeaders(200, f.length()); dump(new FileInputStream(f), t.getResponseBody()); LOGGER.debug("dump of " + f.getName() + " done"); } public static void dump(final InputStream in, final OutputStream os) { dump(in, os, null); } public static void dump(final InputStream in, final OutputStream os, final WebRender renderer) { Runnable r = new Runnable() { @Override public void run() { byte[] buffer = new byte[32 * 1024]; int bytes; int sendBytes = 0; try { while ((bytes = in.read(buffer)) != -1) { sendBytes += bytes; os.write(buffer, 0, bytes); os.flush(); } } catch (IOException e) { LOGGER.trace("Sending stream with premature end: " + sendBytes + " bytes. Reason: " + e.getMessage()); } finally { try { in.close(); } catch (IOException e) { } } try { os.close(); } catch (IOException e) { } if (renderer != null) { renderer.stop(); } } }; new Thread(r).start(); } public static String read(File f) { try { return FileUtils.readFileToString(f, Charset.forName("UTF-8")); } catch (IOException e) { LOGGER.debug("Error reading file: " + e); } return null; } public static String getId(String path, HttpExchange t) { String id = "0"; int pos = t.getRequestURI().getPath().indexOf(path); if (pos != -1) { id = t.getRequestURI().getPath().substring(pos + path.length()); } return id; } public static String strip(String id) { int pos = id.lastIndexOf('.'); if (pos != -1) { return id.substring(0, pos); } return id; } public static boolean deny(HttpExchange t) { return !PMS.getConfiguration().getIpFiltering().allowed(t.getRemoteAddress().getAddress()) || !PMS.isReady(); } private static Range.Byte nullRange(long len) { return new Range.Byte(0L, len); } public static Range.Byte parseRange(Headers hdr, long len) { if (hdr == null) { return nullRange(len); } List<String> r = hdr.get("Range"); if (r == null) { // no range return nullRange(len); } // assume only one String range = r.get(0); String[] tmp = range.split("=")[1].split("-"); long start = Long.parseLong(tmp[0]); long end = tmp.length == 1 ? len : Long.parseLong(tmp[1]); return new Range.Byte(start, end); } public static void sendLogo(HttpExchange t) throws IOException { InputStream in = LooksFrame.class.getResourceAsStream("/resources/images/logo.png"); t.sendResponseHeaders(200, 0); OutputStream os = t.getResponseBody(); dump(in, os); } public static boolean directmime(String mime) { if ( mime != null && ( mime.equals(MIME_MP4) || mime.equals(MIME_WEBM) || mime.equals(MIME_OGG) || mime.equals(MIME_MP3) || mime.equals(MIME_PNG) || mime.equals(MIME_JPG) ) ) { return true; } return false; } public static String userName(HttpExchange t) { HttpPrincipal p = t.getPrincipal(); if (p == null) { return ""; } return p.getUsername(); } public static String getQueryVars(String query, String var) { if (StringUtils.isEmpty(query)) { return null; } for (String p : query.split("&")) { String[] pair = p.split("="); if (pair[0].equalsIgnoreCase(var)) { if (pair.length > 1 && StringUtils.isNotEmpty(pair[1])) { return pair[1]; } } } return null; } public static WebRender matchRenderer(String user, HttpExchange t) { int browser = WebRender.getBrowser(t.getRequestHeaders().getFirst("User-agent")); String confName = WebRender.getBrowserName(browser); RendererConfiguration r = RendererConfiguration.find(confName, t.getRemoteAddress().getAddress()); return ((r instanceof WebRender) && (StringUtils.isBlank(user) || user.equals(((WebRender)r).getUser()))) ? (WebRender) r : null; } public static String getCookie(String name, HttpExchange t) { String cstr = t.getRequestHeaders().getFirst("Cookie"); if (!StringUtils.isEmpty(cstr)) { name += "="; for (String str : cstr.trim().split("\\s*;\\s*")) { if (str.startsWith(name)) { return str.substring(name.length()); } } } LOGGER.debug("Cookie '{}' not found: {}", name, t.getRequestHeaders().get("Cookie")); return null; } private static final int WIDTH = 0; private static final int HEIGHT = 1; private static final int DEFAULT_WIDTH = 720; private static final int DEFAULT_HEIGHT = 404; private static int getHW(int cfgVal, int id, int def) { if (cfgVal != 0) { // if we have a value cfg return that return cfgVal; } String s = PMS.getConfiguration().getWebSize(); if (StringUtils.isEmpty(s)) { // no size string return default return def; } String[] tmp = s.split("x", 2); if (tmp.length < 2) { // bad format resort to default return def; } try { // pick whatever we got return Integer.parseInt(tmp[id]); } catch (NumberFormatException e) { // bad format (again) resort to default return def; } } public static int getHeight() { return getHW(PMS.getConfiguration().getWebHeight(), HEIGHT, DEFAULT_HEIGHT); } public static int getWidth() { return getHW(PMS.getConfiguration().getWebWidth(), WIDTH, DEFAULT_WIDTH); } public static boolean transMp4(String mime, DLNAMediaInfo media) { LOGGER.debug("mp4 profile " + media.getH264Profile()); return mime.equals(MIME_MP4) && (PMS.getConfiguration().isWebMp4Trans() || media.getAvcAsInt() >= 40); } private static IpFilter bumpFilter = null; public static boolean bumpAllowed(HttpExchange t) { if (bumpFilter == null) { bumpFilter = new IpFilter(PMS.getConfiguration().getBumpAllowedIps()); } return bumpFilter.allowed(t.getRemoteAddress().getAddress()); } public static String transMime() { return MIME_TRANS; } public static String getContentType(String filename) { return filename.endsWith(".html") ? "text/html" : filename.endsWith(".css") ? "text/css" : filename.endsWith(".js") ? "text/javascript" : URLConnection.guessContentTypeFromName(filename); } public static Template compile(InputStream stream) { try { return Mustache.compiler().escapeHTML(false).compile(new InputStreamReader(stream)); } catch (Exception e) { LOGGER.debug("Error compiling mustache template: " + e); } return null; } public static LinkedHashSet<String> getLangs(HttpExchange t) { String hdr = t.getRequestHeaders().getFirst("Accept-language"); LinkedHashSet<String> result = new LinkedHashSet<>(); if (StringUtils.isEmpty(hdr)) { return result; } String[] tmp = hdr.split(","); for (String language : tmp) { String[] l1 = language.split(";"); result.add(l1[0]); } return result; } public static String getFirstSupportedLanguage(HttpExchange t) { LinkedHashSet<String> languages = getLangs(t); for (String language : languages) { String code = Languages.toLanguageTag(language); if (code != null) { return code; } } return ""; } public static String getMsgString(String key, HttpExchange t) { if (PMS.getConfiguration().useWebLang()) { String lang = getFirstSupportedLanguage(t); if (!lang.isEmpty()) { return Messages.getString(key, Locale.forLanguageTag(lang)); } } return Messages.getString(key); } /** * A web resource manager to act as: * * - A resource finder with native java classpath search behaviour (including in zip files) * to allow flexible customizing/skinning of the web interface. * * - A file manager to control access to arbitrary non-web resources, i.e. subtitles, * logs, etc. * * - A template manager. */ public static class ResourceManager extends URLClassLoader { private HashSet<File> files; private HashMap<String, Template> templates; public ResourceManager(String... urls) { super(new URL[]{}, null); try { for (String url : urls) { addURL(new URL(url)); } } catch (MalformedURLException e) { LOGGER.debug("Error adding resource url: " + e); } files = new HashSet<>(); templates = new HashMap<>(); } public InputStream getInputStream(String filename) { InputStream stream = getResourceAsStream(filename); if (stream == null) { File file = getFile(filename); if (file != null && file.exists()) { try { stream = new FileInputStream(file); } catch (Exception e) { LOGGER.debug("Error opening stream: " + e); } } } return stream; } @Override public URL getResource(String name) { URL url = super.getResource(name); if (url != null) { LOGGER.debug("Using resource: " + url); } return url; } /** * Register a file as servable. * * @return its hashcode (for use as a 'filename' in an http path) */ public int add(File f) { files.add(f); return f.hashCode(); } /** * Retrieve a servable file by its hashcode. */ public File getFile(String hash) { try { int h = Integer.valueOf(hash); for (File f : files) { if (f.hashCode() == h) { return f; } } } catch (NumberFormatException e) { } return null; } public String read(String filename) { try { return IOUtils.toString(getInputStream(filename), "UTF-8"); } catch (IOException e) { LOGGER.debug("Error reading resource {}: {}", filename, e); } return null; } /** * Write the given resource as an http response body. */ public boolean write(String filename, HttpExchange t) throws IOException { InputStream stream = getInputStream(filename); if (stream != null) { Headers headers = t.getResponseHeaders(); if (!headers.containsKey("Content-Type")) { String mime = getContentType(filename); if (mime != null) { headers.add("Content-Type", mime); } } // Note: available() isn't officially guaranteed to return the full // stream length but effectively seems to do so in our context. t.sendResponseHeaders(200, stream.available()); dump(stream, t.getResponseBody()); return true; } return false; } /** * Retrieve the given mustache template, compiling as necessary. */ public Template getTemplate(String filename) { Template t = null; if (templates.containsKey(filename)) { t = templates.get(filename); } else { URL url = findResource(filename); if (url != null) { t = compile(getInputStream(filename)); templates.put(filename, t); PMS.getFileWatcher().add(new FileWatcher.Watch(url.getFile(), recompiler)); } } return t; } /** * Automatic recompiling */ FileWatcher.Listener recompiler = new FileWatcher.Listener() { @Override public void notify(String filename, String event, FileWatcher.Watch watch, boolean isDir) { String path = watch.fspec.startsWith("web/") ? watch.fspec.substring(4) : watch.fspec; if (templates.containsKey(path)) { templates.put(path, compile(getInputStream(path))); LOGGER.info("Recompiling template: {}", path); } } }; } }