1 /*
   2  * Copyright (c) 2010, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 import java.io.*;
  27 import java.nio.file.*;
  28 import java.nio.file.attribute.*;
  29 import java.net.*;
  30 import java.text.DateFormat;
  31 import java.text.SimpleDateFormat;
  32 import java.util.*;
  33 import java.util.concurrent.*;
  34 import com.sun.net.httpserver.*;
  35 
  36 import static java.lang.System.out;
  37 import static java.net.HttpURLConnection.*;
  38 import static java.util.concurrent.TimeUnit.*;
  39 
  40 
  41 public class TrivialWebServer {
  42     
  43     private boolean debug = System.getenv("TWS_DEBUG") != null;
  44 
  45     private final PrintStream log;
  46     private final Handler handler;
  47 
  48     private TrivialWebServer(Path rpath, PrintStream l) {
  49         log = l;
  50         handler = new Handler(rpath);
  51     }
  52 
  53     private void dump(String t, Headers hs) {
  54         log.format("%s headers%n", t);
  55         for (Map.Entry<String,List<String>> e : hs.entrySet()) {
  56             log.format("  %s : %s%n", e.getKey(), e.getValue());
  57         }
  58     }
  59     
  60     private static final String pattern = "EEE, dd MMM yyyy HH:mm:ss zzz";
  61     private static final TimeZone gmtTZ = TimeZone.getTimeZone("GMT");
  62     private static final ThreadLocal<DateFormat> HTTP_DATE =
  63          new ThreadLocal<DateFormat>() {
  64             @Override protected DateFormat initialValue() {
  65                 DateFormat df = new SimpleDateFormat(pattern, Locale.US);
  66                 df.setTimeZone(gmtTZ);
  67                 return df;
  68             }
  69         };
  70 
  71     private class Handler
  72         implements HttpHandler
  73     {
  74 
  75         private URI root;
  76         private URI BARE_ROOT = URI.create("/");
  77 
  78         Handler(Path rpath) {
  79             root = rpath.toAbsolutePath().toUri().normalize();
  80             if (debug)
  81                 log.format("root %s%n", root);
  82         }
  83 
  84         private void notFound(HttpExchange hx, URI hxu)
  85             throws IOException
  86         {
  87             if (debug)
  88                 log.format("HTTP_NOT_FOUND");
  89             byte[] err
  90                 = ("<b>Not found: " + hxu + "</b>").getBytes("ASCII");
  91             hx.sendResponseHeaders(HTTP_NOT_FOUND, err.length);
  92             OutputStream os = hx.getResponseBody();
  93             os.write(err);
  94         }
  95 
  96         private String etag(Object ob) {
  97             if (ob == null)
  98                 return null;
  99             return '"' + Integer.toHexString(ob.hashCode()) + '"';
 100         }
 101 
 102         public void handle(HttpExchange hx) throws IOException {
 103             try {
 104 
 105                 URI hxu = hx.getRequestURI();
 106                 URI u = root.resolve(BARE_ROOT.relativize(hxu));
 107                 Path p = Paths.get(u);
 108                 if (debug) {
 109                     log.format("%s %s --> %s%n", hx.getRequestMethod(), hxu, p);
 110                     dump("req", hx.getRequestHeaders());
 111                 }
 112                 if (!Files.exists(p)) {
 113                     notFound(hx, hxu);
 114                     return;
 115                 }
 116                 BasicFileAttributes ba
 117                     = Files.readAttributes(p, BasicFileAttributes.class);
 118 
 119                 // Directory -> index.html
 120                 //
 121                 if (ba.isDirectory()) {
 122                     String us = hxu.toString();
 123                     if (!us.endsWith("/")) {
 124                         Headers ahs = hx.getResponseHeaders();
 125                         ahs.put("Location", Arrays.asList(us + "/"));
 126                         if (debug)
 127                             dump("HTTP_MOVED_PERM", ahs);
 128                         hx.sendResponseHeaders(HTTP_MOVED_PERM, -1);
 129                         return;
 130                     }
 131                     p = p.resolve("index.html");
 132                     if (!Files.exists(p)) {
 133                         notFound(hx, hxu);
 134                         return;
 135                     }
 136                     ba = Files.readAttributes(p, BasicFileAttributes.class);
 137                 }
 138                 if (debug)
 139                     log.format("%s --> %s%n", hxu, p);
 140 
 141                 // Check Last-Modified/ETag headers
 142                 //
 143                 DateFormat df = HTTP_DATE.get();
 144                 long mtime = ba.lastModifiedTime().to(TimeUnit.SECONDS);
 145                 String etag = etag(ba.fileKey());
 146                 Headers rhs = hx.getRequestHeaders();
 147                 String rmtime = rhs.getFirst("If-Modified-Since");
 148                 boolean condget = false;
 149                 boolean sendit = false;
 150                 if (rmtime != null) {
 151                     condget = true;
 152                     long rmt = SECONDS.convert(df.parse(rmtime).getTime(),
 153                                                MILLISECONDS);
 154                     sendit = mtime > rmt;
 155                 }
 156                 String retag = rhs.getFirst("If-None-Match");
 157                 if (retag != null) {
 158                     condget = true;
 159                     sendit = sendit || !retag.equals(etag);
 160                 }
 161                 if (condget && !sendit) {
 162                     if (debug)
 163                         out.format("HTTP_NOT_MODIFIED%n");
 164                     hx.sendResponseHeaders(HTTP_NOT_MODIFIED, -1);
 165                     return;
 166                 }
 167 
 168                 // Send content
 169                 //
 170                 Headers ahs = hx.getResponseHeaders();
 171                 ahs.set("Content-Type", "application/octet-stream");
 172                 ahs.set("Last-Modified",
 173                         df.format(new Date(MILLISECONDS.convert(mtime, SECONDS))));
 174                 if (etag != null)
 175                     ahs.set("ETag", etag);
 176                 if (debug)
 177                     dump("HTTP_OK", ahs);
 178                 if ("HEAD".equalsIgnoreCase(hx.getRequestMethod())) {
 179                     // HEAD: manually set the c-l and write no response body
 180                     ahs.set("Content-length", Long.toString(ba.size()));
 181                     hx.sendResponseHeaders(HTTP_OK, -1);
 182                 } else {
 183                     hx.sendResponseHeaders(HTTP_OK, ba.size());
 184                     Files.copy(p, hx.getResponseBody());
 185                 }
 186             } catch (Exception x) {
 187                 x.printStackTrace(out);
 188             } finally {
 189                 hx.close();
 190             }
 191         }
 192 
 193     }
 194 
 195     private HttpServer server = null;
 196 
 197     private void bind(int port) throws IOException {
 198         server = HttpServer.create(new InetSocketAddress(port), 10);
 199         server.createContext("/", handler);
 200         server.setExecutor(Executors.newCachedThreadPool());
 201     }
 202 
 203     private void start() throws IOException {
 204         server.start();
 205     }
 206 
 207     public void stop() throws IOException {
 208         server.stop(0);
 209         ((ExecutorService)server.getExecutor()).shutdown();
 210     }
 211 
 212     public int port() {
 213         if (server == null)
 214             throw new IllegalStateException();
 215         return server.getAddress().getPort();
 216     }
 217 
 218     public static TrivialWebServer create(Path root, PrintStream log)
 219         throws IOException
 220     {
 221         TrivialWebServer tws = new TrivialWebServer(root, log);
 222         Random r = new Random();
 223         for (;;) {
 224             int p = r.nextInt((1 << 16) - 1024) + 1024;
 225             try {
 226                 tws.bind(p);
 227                 break;
 228             } catch (BindException x) {
 229                 continue;
 230             }
 231         }
 232         tws.start();
 233         return tws;
 234     }
 235 
 236     public static TrivialWebServer create(Path root, int port, PrintStream log)
 237         throws IOException
 238     {
 239         if (port == -1)
 240             return create(root, log);
 241         TrivialWebServer tws = new TrivialWebServer(root, log);
 242         tws.bind(port);
 243         tws.start();
 244         return tws;
 245     }
 246 
 247     public static void main(String[] args) throws IOException {
 248         int port = 8081;
 249         Path root = Paths.get(".");
 250         Iterator<String> ai = Arrays.asList(args).iterator();
 251         while (ai.hasNext()) {
 252             String a = ai.next();
 253             if (a.matches("\\d+")) {
 254                 port = Integer.parseInt(a);
 255                 continue;
 256             }
 257             if (a.equals("-r")) {
 258                 port = -1;
 259                 continue;
 260             }
 261             root = Paths.get(a);
 262         }
 263         TrivialWebServer tws
 264             = TrivialWebServer.create(root, port, System.out);
 265         System.out.format("Serving %s on port %d%n", root, tws.port());
 266     }
 267 
 268 }