/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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.sun.jini.tool; import com.sun.jini.logging.Levels; import com.sun.jini.start.LifeCycle; import java.io.BufferedInputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FilePermission; import java.io.IOException; import java.io.InputStream; import java.net.InetAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.URL; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.MissingResourceException; import java.util.ResourceBundle; import java.util.StringTokenizer; import java.util.jar.Attributes; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.jar.Manifest; import java.util.logging.Level; import java.util.logging.Logger; /** * A simple HTTP server, for serving up JAR and class files. * <p> * The following items are discussed below: * <ul> * <li>{@linkplain #main Command line options} * <li><a href="#logging">Logging</a> * <li><a href="#running">Examples for running ClassServer</a> * </ul> * <p> * <a name="logging"><h3>Logging</h3></a> * <p> * * This implementation uses the {@link Logger} named * <code>com.sun.jini.tool.ClassServer</code> to log information at the * following logging levels: * <p> * <table border="1" cellpadding="5" * summary="Describes logging performed by ClassServer at different * logging levels"> * <caption halign="center" valign="top"><b><code> * com.sun.jini.tool.ClassServer</code></b></caption> * * <tr> <th scope="col">Level</th> <th scope="col">Description</th> </tr> * <tr> * <td>{@link Level#SEVERE SEVERE}</td> * <td>failure to accept an incoming connection</td> * </tr> * <tr> * <td>{@link Level#WARNING WARNING}</td> * <td>failure to read the contents of a requested file, * failure to find the message resource bundle, failure while * executing the <code>-stop</code> option * </td> * </tr> * <tr> * <td>{@link Level#INFO INFO}</td> * <td>server startup and termination</td> * </tr> * <tr> * <td>{@link Level#CONFIG CONFIG}</td> * <td>the JAR files being used for <code>-trees</code></td> * </tr> * <tr> * <td>{@link Levels#HANDLED HANDLED}</td> * <td>failure reading an HTTP request or writing a response</td> * </tr> * <tr> * <td>{@link Level#FINE FINE}</td> * <td>bad HTTP requests, HTTP requests for nonexistent files</td> * </tr> * <tr> * <td>{@link Level#FINER FINER}</td> * <td>good HTTP requests</td> * </tr> * </table> * * <p> * <a name="running"><h3>Examples for running ClassServer</h3></a> * <p> * * This server can be run directly from the * {@linkplain #main command line} * or as a nonactivatable service under the * {@linkplain com.sun.jini.start Service Starter}. * <p> * An example of running directly from the command line is: * <blockquote><pre> * % java -jar <var><b>install_dir</b></var>/lib/classserver.jar * -port 8081 -dir <var><b>install_dir</b></var>/lib-dl -verbose * </pre></blockquote> * where <var><b>install_dir</b></var> * is the directory where the Apache River release is installed. * This command places the class server on the (non-default) port * 8081, which serves out the files under the (non-default) directory * <var><b>install_dir</b></var>/lib-dl. The <code>-verbose</code> option * also causes download attempts to be logged. * <p> * An example of running under the Service Starter is: * <blockquote><pre> * % java -Djava.security.policy=<var><b>start_policy</b></var> * -jar <var><b>install_dir</b></var>/lib/start.jar * <a href="#config">httpd.config</a> * </pre></blockquote> * <p> * where <var><b>start_policy</b></var> is the name of a security * policy file (not provided), and <code>httpd.config</code> is the * following configuration file: * <a name="config"></a> * <blockquote><pre> * import com.sun.jini.start.NonActivatableServiceDescriptor; * import com.sun.jini.start.ServiceDescriptor; * * com.sun.jini.start { * * serviceDescriptors = new ServiceDescriptor[]{ * new NonActivatableServiceDescriptor( * "", * "<var><b>httpd_policy</b></var>", * "<var><b>install_dir</b></var>/lib/classserver.jar", * "com.sun.jini.tool.ClassServer", * new String[]{"-port", "8081", "-dir", "<var><b>install_dir</b></var>/lib-dl", "-verbose"}) * }; * } * </pre></blockquote> * where <var><b>httpd_policy</b></var> is the name of a security * policy file (not provided). * * @author Sun Microsystems, Inc. * */ public class ClassServer extends Thread { /** Default HTTP port */ private static int DEFAULT_PORT = 8080; /** Default directory to serve files from on non-Windows OS */ private static String DEFAULT_DIR = "/vob/jive/lib-dl"; /** Default directory to serve files from on Windows */ private static String DEFAULT_WIN_DIR = "J:"; private static Logger logger = Logger.getLogger("com.sun.jini.tool.ClassServer"); /** Server socket to accept connections on */ private ServerSocket server; /** Directories to serve from */ private String[] dirs; /** Map from String (JAR root) to JarFile[] (JAR class path) */ private Map map; /** Verbosity flag */ private boolean verbose; /** Stoppable flag */ private boolean stoppable; /** Read permission on dir and all subdirs, for each dir in dirs */ private FilePermission[] perms; /** Life cycle control */ private LifeCycle lifeCycle; /** * Construct a server that does not support network shutdown. * Use the {@link #start start} method to run it. * * @param port the port to use * @param dirlist the list of directories to serve files from, with entries * separated by the {@linkplain File#pathSeparatorChar path-separator * character} * @param trees <code>true</code> if files within JAR files should be * served up * @param verbose <code>true</code> if downloads should be logged * @throws IOException if the server socket cannot be created * @throws NullPointerException if <code>dir</code> is <code>null</code> */ public ClassServer(int port, String dirlist, boolean trees, boolean verbose) throws IOException { init(port, dirlist, trees, verbose, false, null); } /** * Construct a server. Use the {@link #start start} method to run it. * * @param port the port to use * @param dirlist the list of directories to serve files from, with entries * separated by the {@linkplain File#pathSeparatorChar path-separator * character} * @param trees <code>true</code> if files within JAR files should be * served up * @param verbose <code>true</code> if downloads should be logged * @param stoppable <code>true</code> if network shutdown from the * local host should be supported * @throws IOException if the server socket cannot be created * @throws NullPointerException if <code>dir</code> is <code>null</code> */ public ClassServer(int port, String dirlist, boolean trees, boolean verbose, boolean stoppable) throws IOException { init(port, dirlist, trees, verbose, stoppable, null); } /** * Do the real work of the constructor. */ private void init(int port, String dirlist, boolean trees, boolean verbose, boolean stoppable, LifeCycle lifeCycle) throws IOException { StringTokenizer st = new StringTokenizer(dirlist, File.pathSeparator); dirs = new String[st.countTokens()]; perms = new FilePermission[dirs.length]; for (int i = 0; st.hasMoreTokens(); i++) { String dir = st.nextToken(); if (!dir.endsWith(File.separator)) dir = dir + File.separatorChar; dirs[i] = dir; perms[i] = new FilePermission(dir + '-', "read"); } this.verbose = verbose; this.stoppable = stoppable; this.lifeCycle = lifeCycle; server = new ServerSocket(port); if (!trees) return; map = new HashMap(); Map jfmap = new HashMap(); for (int i = 0; i < dirs.length; i++) { String[] files = new File(dirs[i]).list(); if (files == null) continue; for (int j = 0; j < files.length; j++) { String jar = files[j]; if (!jar.endsWith(".jar") && !jar.endsWith(".zip")) continue; String name = jar.substring(0, jar.length() - 4); if (map.containsKey(name)) continue; List jflist = new ArrayList(1); addJar(jar, jflist, jfmap); map.put(name, jflist.toArray(new JarFile[jflist.size()])); } } } /** * Construct a running server, accepting the same command line options * supported by {@link #main main}, except for the <code>-stop</code> * option. * * @param args command line options * @param lifeCycle life cycle control object, or <code>null</code> * @throws IOException if the server socket cannot be created * @throws IllegalArgumentException if a command line option is not * understood * @throws NullPointerException if <code>args</code> or any element * of <code>args</code> is <code>null</code> */ public ClassServer(String[] args, LifeCycle lifeCycle) throws IOException { int port = DEFAULT_PORT; String dirlist = DEFAULT_DIR; if (File.separatorChar == '\\') dirlist = DEFAULT_WIN_DIR; boolean trees = false; boolean verbose = false; boolean stoppable = false; for (int i = 0; i < args.length ; i++ ) { String arg = args[i]; if (arg.equals("-port")) { i++; port = Integer.parseInt(args[i]); } else if (arg.equals("-dir") || arg.equals("-dirs")) { i++; dirlist = args[i]; } else if (arg.equals("-verbose")) { verbose = true; } else if (arg.equals("-trees")) { trees = true; } else if (arg.equals("-stoppable")) { stoppable = true; } else { throw new IllegalArgumentException(arg); } } init(port, dirlist, trees, verbose, stoppable, lifeCycle); start(); } /** Add transitive Class-Path JARs to jflist. */ private void addJar(String jar, List jflist, Map jfmap) throws IOException { JarFile jf = (JarFile) jfmap.get(jar); if (jf != null) { if (jflist.contains(jf)) { return; } } else { for (int i = 0; i < dirs.length; i++) { File f = new File(dirs[i] + jar).getCanonicalFile(); if (f.exists()) { jf = new JarFile(f); jfmap.put(jar, jf); if (verbose) print("classserver.jar", f.getPath()); logger.config(f.getPath()); break; } } if (jf == null) { if (verbose) print("classserver.notfound", jar); logger.log(Level.CONFIG, "{0} not found", jar); return; } } jflist.add(jf); Manifest man = jf.getManifest(); if (man == null) return; Attributes attrs = man.getMainAttributes(); if (attrs == null) return; String val = attrs.getValue(Attributes.Name.CLASS_PATH); if (val == null) return; for (StringTokenizer st = new StringTokenizer(val); st.hasMoreTokens(); ) { String elt = st.nextToken(); String path = decode(elt); if (path == null) { if (verbose) print("classserver.notfound", elt); logger.log(Level.CONFIG, "{0} not found", elt); } if ('/' != File.separatorChar) { path = path.replace('/', File.separatorChar); } addJar(path, jflist, jfmap); } } /** Just keep looping, spawning a new thread for each incoming request. */ public void run() { logger.log(Level.INFO, "ClassServer started [{0}, port {1}]", new Object[]{Arrays.asList(dirs), Integer.toString(getPort())}); try { while (true) { new Task(server.accept()).start(); } } catch (IOException e) { synchronized (this) { if (verbose) { e.printStackTrace(); } if (!server.isClosed()) logger.log(Level.SEVERE, "accepting connection", e); terminate(); } } } /** Close the server socket, causing the thread to terminate. */ public synchronized void terminate() { verbose = false; try { server.close(); } catch (IOException e) { } if (lifeCycle != null) lifeCycle.unregister(this); logger.log(Level.INFO, "ClassServer terminated [port {0}]", Integer.toString(getPort())); } /** Returns the port on which this server is listening. */ public int getPort() { return server.getLocalPort(); } /** Read up to CRLF, return false if EOF */ private static boolean readLine(InputStream in, StringBuffer buf) throws IOException { while (true) { int c = in.read(); if (c < 0) return buf.length() > 0; if (c == '\r') { in.mark(1); c = in.read(); if (c != '\n') in.reset(); return true; } if (c == '\n') return true; buf.append((char) c); } } /** Parse % HEX HEX from s starting at i */ private static char decode(String s, int i) { return (char) Integer.parseInt(s.substring(i + 1, i + 3), 16); } /** Decode escape sequences */ private static String decode(String path) { try { for (int i = path.indexOf('%'); i >= 0; i = path.indexOf('%', i + 1)) { char c = decode(path, i); int n = 3; if ((c & 0x80) != 0) { switch (c >> 4) { case 0xC: case 0xD: n = 6; c = (char)(((c & 0x1F) << 6) | (decode(path, i + 3) & 0x3F)); break; case 0xE: n = 9; c = (char)(((c & 0x0f) << 12) | ((decode(path, i + 3) & 0x3F) << 6) | (decode(path, i + 6) & 0x3F)); break; default: return null; } } path = path.substring(0, i) + c + path.substring(i + n); } } catch (Exception e) { return null; } return path; } /** Read the request/response and return the initial line. */ private static String getInput(Socket sock, boolean isRequest) throws IOException { BufferedInputStream in = new BufferedInputStream(sock.getInputStream(), 256); StringBuffer buf = new StringBuffer(80); do { if (!readLine(in, buf)) return null; } while (isRequest && buf.length() == 0); String initial = buf.toString(); do { buf.setLength(0); } while (readLine(in, buf) && buf.length() > 0); return initial; } /** Simple daemon task thread */ private class Task extends Thread { /** Socket for the incoming request */ private Socket sock; /** Simple constructor */ public Task(Socket sock) { this.sock = sock; setDaemon(true); } /** Read specified number of bytes and always close the stream. */ private byte[] getBytes(InputStream in, long length) throws IOException { DataInputStream din = new DataInputStream(in); byte[] bytes = new byte[(int)length]; try { din.readFully(bytes); } finally { din.close(); } return bytes; } /** Canonicalize the path */ private String canon(String path) { if (path.regionMatches(true, 0, "http://", 0, 7)) { int i = path.indexOf('/', 7); if (i < 0) path = "/"; else path = path.substring(i); } path = decode(path); if (path == null || path.length() == 0 || path.charAt(0) != '/') return null; return path.substring(1); } /** Return the bytes of the requested file, or null if not found. */ private byte[] getBytes(String path) throws IOException { if (map != null) { int i = path.indexOf('/'); if (i > 0) { JarFile[] jfs = (JarFile[])map.get(path.substring(0, i)); if (jfs != null) { String jpath = path.substring(i + 1); for (i = 0; i < jfs.length; i++) { JarEntry je = jfs[i].getJarEntry(jpath); if (je != null) return getBytes(jfs[i].getInputStream(je), je.getSize()); } } } } if ('/' != File.separatorChar) { path = path.replace('/', File.separatorChar); } for (int i = 0; i < dirs.length; i++) { File f = new File(dirs[i] + path); if (perms[i].implies(new FilePermission(f.getPath(), "read"))) { try { return getBytes(new FileInputStream(f), f.length()); } catch (FileNotFoundException e) { } } } return null; } /** Process the request */ public void run() { try { DataOutputStream out = new DataOutputStream(sock.getOutputStream()); String req; try { req = getInput(sock, true); } catch (Exception e) { if (verbose) { print("classserver.inputerror", new String[]{sock.getInetAddress().getHostName(), Integer.toString(sock.getPort())}); e.printStackTrace(); } logger.log(Levels.HANDLED, "reading request", e); return; } if (req == null) return; if (req.startsWith("SHUTDOWN *")) { if (verbose) print("classserver.shutdown", new String[]{sock.getInetAddress().getHostName(), Integer.toString(sock.getPort())}); boolean ok = stoppable; try { new ServerSocket(0, 1, sock.getInetAddress()); } catch (IOException e) { ok = false; } if (!ok) { out.writeBytes("HTTP/1.0 403 Forbidden\r\n\r\n"); out.flush(); return; } try { out.writeBytes("HTTP/1.0 200 OK\r\n\r\n"); out.flush(); } catch (Exception e) { if (verbose) e.printStackTrace(); logger.log(Levels.HANDLED, "writing response", e); } terminate(); return; } String[] args = null; if (verbose || logger.isLoggable(Level.FINE)) args = new String[]{req, sock.getInetAddress().getHostName(), Integer.toString(sock.getPort())}; boolean get = req.startsWith("GET "); if (!get && !req.startsWith("HEAD ")) { if (verbose) print("classserver.badrequest", args); logger.log(Level.FINE, "bad request \"{0}\" from {1}:{2}", args); out.writeBytes("HTTP/1.0 400 Bad Request\r\n\r\n"); out.flush(); return; } String path = req.substring(get ? 4 : 5); int i = path.indexOf(' '); if (i > 0) path = path.substring(0, i); path = canon(path); if (path == null) { if (verbose) print("classserver.badrequest", args); logger.log(Level.FINE, "bad request \"{0}\" from {1}:{2}", args); out.writeBytes("HTTP/1.0 400 Bad Request\r\n\r\n"); out.flush(); return; } if (args != null) args[0] = path; if (verbose) { print(get ? "classserver.request" : "classserver.probe", args); } logger.log(Level.FINER, get ? "{0} requested from {1}:{2}" : "{0} probed from {1}:{2}", args); byte[] bytes; try { bytes = getBytes(path); } catch (Exception e) { if (verbose) e.printStackTrace(); logger.log(Level.WARNING, "getting bytes", e); out.writeBytes("HTTP/1.0 500 Internal Error\r\n\r\n"); out.flush(); return; } if (bytes == null) { if (verbose) print("classserver.notfound", path); logger.log(Level.FINE, "{0} not found", path); out.writeBytes("HTTP/1.0 404 Not Found\r\n\r\n"); out.flush(); return; } out.writeBytes("HTTP/1.0 200 OK\r\n"); out.writeBytes("Content-Length: " + bytes.length + "\r\n"); out.writeBytes("Content-Type: application/java\r\n\r\n"); if (get) out.write(bytes); out.flush(); if (get) fileDownloaded(path, sock.getInetAddress()); } catch (Exception e) { if (verbose) e.printStackTrace(); logger.log(Levels.HANDLED, "writing response", e); } finally { try { sock.close(); } catch (IOException e) { } } } } private static ResourceBundle resources; private static boolean resinit = false; private static synchronized String getString(String key) { if (!resinit) { resinit = true; try { resources = ResourceBundle.getBundle("com.sun.jini.tool.resources.classserver"); } catch (MissingResourceException e) { logger.log(Level.WARNING, "missing resource bundle {0}", "com.sun.jini.tool.resources.classserver"); } } if (resources != null) { try { return resources.getString(key); } catch (MissingResourceException e) { } } return null; } private static void print(String key, String val) { String fmt = getString(key); if (fmt == null) fmt = "no text found: \"" + key + "\" {0}"; System.out.println(MessageFormat.format(fmt, new String[]{val})); } private static void print(String key, String[] vals) { String fmt = getString(key); if (fmt == null) fmt = "no text found: \"" + key + "\" {0} {1} {2}"; System.out.println(MessageFormat.format(fmt, vals)); } /** * This method provides a way for subclasses to be notified when a * file has been completely downloaded. * * @param fp The path to the file that was downloaded. */ protected void fileDownloaded(String fp, InetAddress addr) { } /** * Command line interface for creating an HTTP server. * The command line options are: * <pre> * [-port <var>port</var>] [-dir <var>dirlist</var>] [-dirs <var>dirlist</var>] [-stoppable] [-verbose] [-trees] * </pre> * The default port is 8080; the default can be overridden with * the <code>-port</code> option. The default directory on Windows is * <code>J:</code> and the default on other systems is * <code>/vob/jive/lib-dl</code>; the default can be overridden with the * <code>-dir</code> or <code>-dirs</code> option, providing one or more * directories separated by the {@linkplain File#pathSeparatorChar * path-separator character}. All files under these directories (including * all subdirectories) are served up via HTTP. If the pathname of a file * is <var>path</var> relative to one of the top-level directories, then * the file can be downloaded using the URL * <pre> * http://<var>host</var>:<var>port</var>/<var>path</var> * </pre> * If a relative <var>path</var> matches a file under more than one * top-level directory, the file under the first top-level directory * with a match is used. No caching of directory contents or file contents * is performed. <p> * * If the <code>-stoppable</code> option is given, the HTTP server can be * shut down with a custom HTTP <code>SHUTDOWN</code> request originating * from the local host. The command line options for stopping an existing * HTTP server are: * <pre> * [-port <var>port</var>] -stop * </pre> * <p> * * If the <code>-verbose</code> option is given, then all attempts to * download files are output. <p> * * The <code>-trees</code> option can be used to serve up individual files * stored within JAR and zip files in addition to the files that are * served up as described above. If the option is used, the server finds * all JAR and zip files in the top-level directories (not in * subdirectories). If the name of the JAR or zip file is * <var>name</var><code>.jar</code> or <var>name</var><code>.zip</code>, * then any individual file named <var>file</var> within it (or within the * JAR or zip files referenced transitively in <code>Class-Path</code> * manifest attributes), can be downloaded using a URL of the form: * <pre> * http://<var>host</var>:<var>port</var>/<var>name</var>/<var>file</var> * </pre> * If multiple top-level directories have JAR or zip files with the same * <var>name</var>, the file under the first top-level directory with a * match is used. If a <code>Class-Path</code> element matches a file under * more than one top-level directory, the file under the first top-level * directory with a match is used. When this option is used, an open file * descriptor and cached information is held for each JAR or zip file, for * the life of the process. */ public static void main(String[] args) { int port = DEFAULT_PORT; String dirlist = DEFAULT_DIR; if (File.separatorChar == '\\') dirlist = DEFAULT_WIN_DIR; boolean trees = false; boolean verbose = false; boolean stoppable = false; boolean stop = false; for (int i = 0; i < args.length ; i++ ) { String arg = args[i]; if (arg.equals("-port")) { i++; port = Integer.parseInt(args[i]); } else if (arg.equals("-dir") || arg.equals("-dirs")) { i++; dirlist = args[i]; } else if (arg.equals("-verbose")) { verbose = true; } else if (arg.equals("-trees")) { trees = true; } else if (arg.equals("-stoppable")) { stoppable = true; } else if (arg.equals("-stop")) { stop = true; } else { print("classserver.usage", (String)null); return; } } try { if (stop) { Socket sock = new Socket(InetAddress.getLocalHost(), port); try { DataOutputStream out = new DataOutputStream(sock.getOutputStream()); out.writeBytes("SHUTDOWN *\r\n\r\n"); out.flush(); String status = getInput(sock, false); if (status != null && status.startsWith("HTTP/")) { status = status.substring(status.indexOf(' ') + 1); if (status.startsWith("403 ")) { print("classserver.forbidden", status); } else if (!status.startsWith("200 ") && status.indexOf(' ') == 3) { print("classserver.status", new String[]{status.substring(0, 3), status.substring(4)}); } } } finally { try { sock.close(); } catch (IOException e) { } } } else { new ClassServer(port, dirlist, trees, verbose, stoppable).start(); } } catch (IOException e) { logger.log(Level.WARNING, "requesting shutdown", e); } } }