/* * Copyright (C) 2014 Michell Bak * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.miz.smbstreamer; import android.net.Uri; import android.util.Log; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.PrintWriter; import java.net.ServerSocket; import java.net.Socket; import java.util.Date; import java.util.Enumeration; import java.util.Locale; import java.util.Properties; import java.util.StringTokenizer; import java.util.TimeZone; import static com.miz.smbstreamer.Response.HTTP_BADREQUEST; import static com.miz.smbstreamer.Response.HTTP_INTERNALERROR; public abstract class StreamServer { public static final String MIME_PLAINTEXT = "text/plain"; private int mTcpPort; private final ServerSocket mServerSocket; private Thread mServerThread; private int mBufferSize = 8192 * 2; private static java.text.SimpleDateFormat sGmtFormat; static { sGmtFormat = new java.text.SimpleDateFormat( "E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US); sGmtFormat.setTimeZone(TimeZone.getTimeZone("GMT")); } public void setBufferSize(int size) { mBufferSize = size; } /** * Override this to customize the server.<p> * * (By default, this delegates to serveFile() and allows directory listing.) * * @param uri Percent-decoded URI without parameters, for example "/index.cgi" * @param method "GET", "POST" etc. * @param parms Parsed, percent decoded parameters from URI and, in case of POST, data. * @param header Header entries, percent decoded * @return HTTP response, see class Response for details */ public abstract Response serve(String uri, String method, Properties header, Properties parms, Properties files); // ================================================== // Socket & server code // ================================================== /** * Starts a HTTP server to given port.<p> * Throws an IOException if the socket is already in use */ public StreamServer( int port, File wwwroot ) throws IOException { mTcpPort = port; mServerSocket = new ServerSocket(mTcpPort); mServerThread = new Thread(new Runnable() { public void run() { try { while (true) { Socket accept = mServerSocket.accept(); new HTTPSession(accept); } } catch (IOException ioe) {} } }); mServerThread.setDaemon(true); mServerThread.setPriority(Thread.MAX_PRIORITY); mServerThread.start(); } /** * Stops the server. */ public void stop() { try { mServerSocket.close(); mServerThread.join(); } catch (Exception e) {} } /** * Handles one session, i.e. parses the HTTP request * and returns the response. */ private class HTTPSession implements Runnable { private InputStream is; private final Socket socket; public HTTPSession(Socket s) { socket = s; Thread t = new Thread(this); t.setDaemon(true); t.setPriority(Thread.MAX_PRIORITY); t.start(); } public void run() { try { handleResponse(socket); } finally { if (is != null) { try { is.close(); socket.close(); } catch(IOException e) {} } } } private void handleResponse(Socket socket) { try { is = socket.getInputStream(); if ( is == null) return; byte[] buf = new byte[mBufferSize]; int rlen = is.read(buf, 0, mBufferSize); if (rlen <= 0) return; // Create a BufferedReader for parsing the header. ByteArrayInputStream hbis = new ByteArrayInputStream(buf, 0, rlen); BufferedReader hin = new BufferedReader( new InputStreamReader( hbis , "utf-8"), mBufferSize); Properties pre = new Properties(); Properties params = new Properties(); Properties header = new Properties(); Properties files = new Properties(); // Decode the header into params and header java properties decodeHeader(hin, pre, params, header); // Logging! Log.d("Streamer", pre.toString()); Log.d("Streamer", "Params: " + params.toString()); Log.d("Streamer", "Header: " + header.toString()); String method = pre.getProperty("method"); String uri = pre.getProperty("uri"); // Ok, now do the serve() Response r = serve(uri, method, header, params, files); if (r == null) sendError(socket, HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: Serve() returned a null response." ); else sendResponse(socket, r.status, r.mimeType, r.header, r.data ); } catch (IOException ioe) { try { sendError(socket, HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); } catch (Throwable t) {} } catch (InterruptedException ie) {} // Thrown by sendError, ignore and exit the thread. } /** * Decodes the sent headers and loads the data into * java Properties' key - value pairs **/ private void decodeHeader(BufferedReader in, Properties pre, Properties params, Properties header) throws InterruptedException { try { // Read the request line String inLine = in.readLine(); if (inLine == null) return; StringTokenizer st = new StringTokenizer( inLine ); if (!st.hasMoreTokens()) sendError(socket, HTTP_BADREQUEST, "BAD REQUEST: Syntax error. Usage: GET /example/file.html" ); String method = st.nextToken(); pre.put("method", method); if (!st.hasMoreTokens()) sendError(socket, HTTP_BADREQUEST, "BAD REQUEST: Missing URI. Usage: GET /example/file.html" ); String uri = st.nextToken(); // Decode parameters from the URI int qmi = uri.indexOf('?'); if (qmi >= 0) { decodeParams(uri.substring(qmi + 1), params); uri = decodePercent(uri.substring(0, qmi)); } else uri = Uri.decode(uri);//decodePercent(uri); // If there's another token, it's protocol version, // followed by HTTP headers. Ignore version but parse headers. // NOTE: this now forces header names lowercase since they are // case insensitive and vary by client. if (st.hasMoreTokens()) { String line = in.readLine(); while (line != null && line.trim().length() > 0) { int p = line.indexOf(':'); if (p >= 0) header.put( line.substring(0,p).trim().toLowerCase(Locale.ENGLISH), line.substring(p + 1).trim()); line = in.readLine(); } } pre.put("uri", uri); } catch (IOException ioe) { sendError(socket, HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); } } /** * Decodes the percent encoding scheme. <br/> * For example: "an+example%20string" -> "an example string" */ private String decodePercent(String str) throws InterruptedException { try { StringBuffer sb = new StringBuffer(); for(int i = 0; i < str.length(); i++) { char c = str.charAt(i); switch (c) { case '+': sb.append(' '); break; case '%': sb.append((char) Integer.parseInt(str.substring(i + 1, i + 3), 16)); i += 2; break; default: sb.append(c); break; } } return sb.toString(); } catch( Exception e ) { sendError(socket, HTTP_BADREQUEST, "BAD REQUEST: Bad percent-encoding."); return null; } } /** * Decodes parameters in percent-encoded URI-format * ( e.g. "name=Jack%20Daniels&pass=Single%20Malt" ) and * adds them to given Properties. NOTE: this doesn't support multiple * identical keys due to the simplicity of Properties -- if you need multiples, * you might want to replace the Properties with a Hashtable of Vectors or such. */ private void decodeParams(String params, Properties p) throws InterruptedException { if (params == null) return; StringTokenizer st = new StringTokenizer(params, "&" ); while (st.hasMoreTokens()) { String e = st.nextToken(); int sep = e.indexOf( '=' ); if (sep >= 0) p.put(decodePercent(e.substring(0, sep)).trim(), decodePercent(e.substring(sep + 1))); } } /** * Returns an error message as a HTTP response and * throws InterruptedException to stop further request processing. */ private void sendError(Socket socket, String status, String msg) throws InterruptedException { sendResponse(socket, status, MIME_PLAINTEXT, null, null); throw new InterruptedException(); } /** * Sends given response to the socket. */ private void sendResponse(Socket socket, String status, String mime, Properties header, StreamSource data) { try { if (status == null) throw new Error("sendResponse(): Status can't be null."); OutputStream out = socket.getOutputStream(); PrintWriter pw = new PrintWriter(out); pw.print("HTTP/1.0 " + status + "\r\n"); pw.print("Content-Type: video/*\r\n"); if (header == null || header.getProperty("Date") == null) pw.print("Date: " + sGmtFormat.format(new Date()) + "\r\n"); if (header != null) { Enumeration<Object> e = header.keys(); while (e.hasMoreElements()) { String key = (String)e.nextElement(); String value = header.getProperty(key); pw.print(key + ": " + value + "\r\n"); } } pw.print("\r\n"); pw.flush(); if (data != null) { data.open(); byte[] buff = new byte[mBufferSize]; int read = 0; while ((read = data.read(buff)) > 0) { out.write( buff, 0, read ); } } out.flush(); out.close(); if (data != null) data.close(); } catch (IOException ioe) { // Couldn't write? No can do. try { socket.close(); } catch (Throwable t) {} } } } }