From 6d295275cae74cc529f1fecb375fc29e152d244d Mon Sep 17 00:00:00 2001 From: Ben Fry Date: Sun, 8 Aug 2021 08:22:20 -0400 Subject: [PATCH] move Worker to inner class, sort extensions --- app/src/processing/app/WebServer.java | 514 +++++++++++++------------- 1 file changed, 255 insertions(+), 259 deletions(-) diff --git a/app/src/processing/app/WebServer.java b/app/src/processing/app/WebServer.java index abd9f5786..354649454 100644 --- a/app/src/processing/app/WebServer.java +++ b/app/src/processing/app/WebServer.java @@ -18,18 +18,29 @@ import java.util.zip.*; public class WebServer { /** where worker threads stand idle */ - static final Vector threads = new Vector<>(); + static final Vector threads = new Vector<>(); /** max # worker threads */ static final int WORKERS = 5; - /** port to use, if there are complaints, move to preferences.txt */ + /** P5 in decimal; if there are complaints, move to preferences.txt */ static final int PORT = 8053; + static final int HTTP_OK = 200; + static final int HTTP_NOT_FOUND = 404; + static final int HTTP_BAD_METHOD = 405; - static public int launch(String zipPath) throws IOException { - final ZipFile zip = new ZipFile(zipPath); - final Map entries = new HashMap<>(); + ZipFile zip; + Map entries; + + static final int BUFFER_SIZE = 2048; + + static final byte[] EOL = { (byte) '\r', (byte) '\n' }; + + + public WebServer(String zipPath) throws IOException { + zip = new ZipFile(zipPath); + entries = new HashMap<>(); Enumeration en = zip.entries(); while (en.hasMoreElements()) { ZipEntry entry = en.nextElement(); @@ -38,7 +49,7 @@ public class WebServer { // start worker threads for (int i = 0; i < WORKERS; ++i) { - WebServerWorker w = new WebServerWorker(zip, entries); + Worker w = new Worker(); Thread t = new Thread(w, "Web Server Worker #" + i); t.start(); threads.addElement(w); @@ -51,11 +62,11 @@ public class WebServer { Socket s = ss.accept(); synchronized (threads) { if (threads.isEmpty()) { - WebServerWorker ws = new WebServerWorker(zip, entries); + Worker ws = new Worker(); ws.setSocket(s); (new Thread(ws, "additional worker")).start(); } else { - WebServerWorker w = threads.elementAt(0); + Worker w = threads.elementAt(0); threads.removeElementAt(0); w.setSocket(s); } @@ -65,14 +76,248 @@ public class WebServer { e.printStackTrace(); } }).start(); - return PORT; + } + + + class Worker implements Runnable { + // buffer to use for requests + byte[] buffer; + private Socket socket; + + Worker() { + buffer = new byte[BUFFER_SIZE]; + socket = null; + } + + synchronized void setSocket(Socket s) { + this.socket = s; + notify(); + } + + public synchronized void run() { + while (true) { + if (socket == null) { + try { + wait(); + } catch (InterruptedException e) { + continue; + } + } + try { + handleClient(); + } catch (Exception e) { + e.printStackTrace(); + } + // go back in wait queue if there's fewer + // than numHandler connections. + socket = null; + synchronized (WebServer.threads) { + if (WebServer.threads.size() >= WebServer.WORKERS) { + // too many threads, exit this one + return; + } else { + WebServer.threads.addElement(this); + } + } + } + } + + + void handleClient() throws IOException { + InputStream is = new BufferedInputStream(socket.getInputStream()); + PrintStream ps = new PrintStream(socket.getOutputStream()); + // we will only block in read for this many milliseconds + // before we fail with java.io.InterruptedIOException, + // at which point we will abandon the connection. + socket.setSoTimeout(10000); + socket.setTcpNoDelay(true); + // zero out the buffer from last time + for (int i = 0; i < BUFFER_SIZE; i++) { + buffer[i] = 0; + } + try { + // We only support HTTP GET/HEAD, and don't support any fancy HTTP + // options, so we're only interested really in the first line. + int length = 0; + + outerLoop: + while (length < BUFFER_SIZE) { + int r = is.read(buffer, length, BUFFER_SIZE - length); + if (r == -1) { + return; // EOF + } + int i = length; + length += r; + for (; i < length; i++) { + if (buffer[i] == (byte) '\n' || buffer[i] == (byte) '\r') { + break outerLoop; // read one line + } + } + } + + // are we doing a GET or just a HEAD + boolean doingGet; + // beginning of file name + int index; + if (buffer[0] == (byte) 'G' && + buffer[1] == (byte) 'E' && + buffer[2] == (byte) 'T' && + buffer[3] == (byte) ' ') { + doingGet = true; + index = 4; + } else if (buffer[0] == (byte) 'H' && + buffer[1] == (byte) 'E' && + buffer[2] == (byte) 'A' && + buffer[3] == (byte) 'D' && + buffer[4] == (byte) ' ') { + doingGet = false; + index = 5; + } else { + // we don't support this method + ps.print("HTTP/1.0 " + HTTP_BAD_METHOD + " unsupported method type: "); + ps.write(buffer, 0, 5); + ps.write(EOL); + ps.flush(); + socket.close(); + return; + } + + int i; + // find the file name, from: + // GET /foo/bar.html HTTP/1.0 + // extract "/foo/bar.html" + for (i = index; i < length; i++) { + if (buffer[i] == (byte) ' ') { + break; + } + } + + String filename = new String(buffer, index, i - index); + // get the zip entry, remove the front slash + ZipEntry entry = entries.get(filename.substring(1)); + boolean ok = printHeaders(entry, ps); + if (entry != null) { + InputStream stream = zip.getInputStream(entry); + if (doingGet && ok) { + sendFile(stream, ps); + } + } else { + send404(ps); + } + } finally { + socket.close(); + } + } + + + boolean printHeaders(ZipEntry entry, PrintStream ps) throws IOException { + boolean ret; + int rCode; + if (entry == null) { + rCode = HTTP_NOT_FOUND; + ps.print("HTTP/1.0 " + HTTP_NOT_FOUND + " Not Found"); + ps.write(EOL); + ret = false; + } else { + rCode = HTTP_OK; + ps.print("HTTP/1.0 " + HTTP_OK + " OK"); + ps.write(EOL); + ret = true; + } + if (entry != null) { + Messages.log("From " + socket.getInetAddress().getHostAddress() + ": GET " + entry.getName() + " --> " + rCode); + } + ps.print("Server: Processing Documentation Server"); + ps.write(EOL); + ps.print("Date: " + new Date()); + ps.write(EOL); + if (ret) { + if (!entry.isDirectory()) { + ps.print("Content-length: " + entry.getSize()); + ps.write(EOL); + ps.print("Last Modified: " + new Date(entry.getTime())); + ps.write(EOL); + String name = entry.getName(); + int ind = name.lastIndexOf('.'); + String ct = null; + if (ind > 0) { + ct = map.get(name.substring(ind)); + } + if (ct == null) { + //System.err.println("unknown content type " + name.substring(ind)); + ct = "application/x-unknown-content-type"; + } + ps.print("Content-type: " + ct); + } else { + ps.print("Content-type: text/html"); + } + ps.write(EOL); + } + ps.write(EOL); // adding another newline here [fry] + return ret; + } + + + void send404(PrintStream ps) throws IOException { + ps.write(EOL); + ps.write(EOL); + ps.print("

404 Not Found

"); + ps.print("The requested resource was not found."); + ps.write(EOL); + ps.write(EOL); + } + + + void sendFile(InputStream is, PrintStream ps) throws IOException { + try (is) { + int n; + while ((n = is.read(buffer)) > 0) { + ps.write(buffer, 0, n); + } + } + } + } + + + /** mapping of file extensions to content-types */ + static final Map map = new ConcurrentHashMap<>(); + + // get list of extensions to support (https://superuser.com/a/232101) + // find . -type f | sed -En 's|.*/[^/]+\.([^/.]+)$|\1|p' | sort -u + // -E is for macOS, use -r on Linux + static { + map.put("", "content/unknown"); + + map.put(".au", "audio/basic"); + map.put(".c", "text/plain"); + map.put(".c++", "text/plain"); + map.put(".cc", "text/plain"); + map.put(".css", "text/css"); + map.put(".exe", "application/octet-stream"); + map.put(".gif", "image/gif"); + map.put(".h", "text/plain"); + map.put(".htm", "text/html"); + map.put(".html", "text/html"); + map.put(".java", "text/plain"); + map.put(".jpeg", "image/jpeg"); + map.put(".jpg", "image/jpeg"); + map.put(".js", "text/javascript"); + map.put(".pl", "text/plain"); + map.put(".ps", "application/postscript"); + map.put(".sh", "application/x-shar"); + map.put(".snd", "audio/basic"); + map.put(".tar", "application/x-tar"); + map.put(".txt", "text/plain"); + map.put(".uu", "application/octet-stream"); + map.put(".wav", "audio/x-wav"); + map.put(".zip", "application/zip"); } // main method for testing static public void main(String[] args) { try { - launch(args[0]); + new WebServer(args[0]); } catch (IOException e) { e.printStackTrace(); } @@ -80,253 +325,4 @@ public class WebServer { } -class WebServerWorker implements Runnable { - static final int HTTP_OK = 200; - static final int HTTP_NOT_FOUND = 404; - static final int HTTP_BAD_METHOD = 405; - - private final ZipFile zip; - private final Map entries; - - final static int BUF_SIZE = 2048; - - static final byte[] EOL = { (byte)'\r', (byte)'\n' }; - - /* buffer to use for requests */ - byte[] buf; - private Socket s; - - WebServerWorker(ZipFile zip, Map entries) { - this.entries = entries; - this.zip = zip; - - buf = new byte[BUF_SIZE]; - s = null; - } - - synchronized void setSocket(Socket s) { - this.s = s; - notify(); - } - - public synchronized void run() { - while (true) { - if (s == null) { - try { - wait(); - } catch (InterruptedException e) { - continue; - } - } - try { - handleClient(); - } catch (Exception e) { - e.printStackTrace(); - } - // go back in wait queue if there's fewer - // than numHandler connections. - s = null; - synchronized (WebServer.threads) { - if (WebServer.threads.size() >= WebServer.WORKERS) { - // too many threads, exit this one - return; - } else { - WebServer.threads.addElement(this); - } - } - } - } - - - void handleClient() throws IOException { - InputStream is = new BufferedInputStream(s.getInputStream()); - PrintStream ps = new PrintStream(s.getOutputStream()); - // we will only block in read for this many milliseconds - // before we fail with java.io.InterruptedIOException, - // at which point we will abandon the connection. - s.setSoTimeout(10000); - s.setTcpNoDelay(true); - // zero out the buffer from last time - for (int i = 0; i < BUF_SIZE; i++) { - buf[i] = 0; - } - try { - // We only support HTTP GET/HEAD, and don't support any fancy HTTP - // options, so we're only interested really in the first line. - int length = 0; - - outerLoop: - while (length < BUF_SIZE) { - int r = is.read(buf, length, BUF_SIZE - length); - if (r == -1) { - return; // EOF - } - int i = length; - length += r; - for (; i < length; i++) { - if (buf[i] == (byte) '\n' || buf[i] == (byte) '\r') { - break outerLoop; // read one line - } - } - } - - // are we doing a GET or just a HEAD - boolean doingGet; - // beginning of file name - int index; - if (buf[0] == (byte) 'G' && - buf[1] == (byte) 'E' && - buf[2] == (byte) 'T' && - buf[3] == (byte) ' ') { - doingGet = true; - index = 4; - } else if (buf[0] == (byte) 'H' && - buf[1] == (byte) 'E' && - buf[2] == (byte) 'A' && - buf[3] == (byte) 'D' && - buf[4] == (byte) ' ') { - doingGet = false; - index = 5; - } else { - // we don't support this method - ps.print("HTTP/1.0 " + HTTP_BAD_METHOD + " unsupported method type: "); - ps.write(buf, 0, 5); - ps.write(EOL); - ps.flush(); - s.close(); - return; - } - - int i; - // find the file name, from: - // GET /foo/bar.html HTTP/1.0 - // extract "/foo/bar.html" - for (i = index; i < length; i++) { - if (buf[i] == (byte) ' ') { - break; - } - } - - String filename = new String(buf, index, i - index); - // get the zip entry, remove the front slash - ZipEntry entry = entries.get(filename.substring(1)); - boolean ok = printHeaders(entry, ps); - if (entry != null) { - InputStream stream = zip.getInputStream(entry); - if (doingGet && ok) { - sendFile(stream, ps); - } - } else { - send404(ps); - } - } finally { - s.close(); - } - } - - - boolean printHeaders(ZipEntry entry, PrintStream ps) throws IOException { - boolean ret; - int rCode; - if (entry == null) { - rCode = HTTP_NOT_FOUND; - ps.print("HTTP/1.0 " + HTTP_NOT_FOUND + " Not Found"); - ps.write(EOL); - ret = false; - } else { - rCode = HTTP_OK; - ps.print("HTTP/1.0 " + HTTP_OK + " OK"); - ps.write(EOL); - ret = true; - } - if (entry != null) { - Messages.log("From " + s.getInetAddress().getHostAddress() + ": GET " + entry.getName() + " --> " + rCode); - } - ps.print("Server: Processing Documentation Server"); - ps.write(EOL); - ps.print("Date: " + new Date()); - ps.write(EOL); - if (ret) { - if (!entry.isDirectory()) { - ps.print("Content-length: " + entry.getSize()); - ps.write(EOL); - ps.print("Last Modified: " + new Date(entry.getTime())); - ps.write(EOL); - String name = entry.getName(); - int ind = name.lastIndexOf('.'); - String ct = null; - if (ind > 0) { - ct = map.get(name.substring(ind)); - } - if (ct == null) { - //System.err.println("unknown content type " + name.substring(ind)); - ct = "application/x-unknown-content-type"; - } - ps.print("Content-type: " + ct); - } else { - ps.print("Content-type: text/html"); - } - ps.write(EOL); - } - ps.write(EOL); // adding another newline here [fry] - return ret; - } - - - void send404(PrintStream ps) throws IOException { - ps.write(EOL); - ps.write(EOL); - ps.print("

404 Not Found

" + - "The requested resource was not found."); - ps.write(EOL); - ps.write(EOL); - } - - - void sendFile(InputStream is, PrintStream ps) throws IOException { - try (is) { - int n; - while ((n = is.read(buf)) > 0) { - ps.write(buf, 0, n); - } - } - } - - - /** mapping of file extensions to content-types */ - static Map map = new ConcurrentHashMap<>(); - - static { - map.put("", "content/unknown"); - - map.put(".uu", "application/octet-stream"); - map.put(".exe", "application/octet-stream"); - map.put(".ps", "application/postscript"); - map.put(".zip", "application/zip"); - map.put(".sh", "application/x-shar"); - map.put(".tar", "application/x-tar"); - map.put(".snd", "audio/basic"); - map.put(".au", "audio/basic"); - map.put(".wav", "audio/x-wav"); - - map.put(".gif", "image/gif"); - map.put(".jpg", "image/jpeg"); - map.put(".jpeg", "image/jpeg"); - - map.put(".htm", "text/html"); - map.put(".html", "text/html"); - map.put(".css", "text/css"); - map.put(".js", "text/javascript"); - - map.put(".txt", "text/plain"); - map.put(".java", "text/plain"); - - map.put(".c", "text/plain"); - map.put(".cc", "text/plain"); - map.put(".c++", "text/plain"); - map.put(".h", "text/plain"); - map.put(".pl", "text/plain"); - } -} -