All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.river.tool.ClassServer Maven / Gradle / Ivy

The newest version!
/*
 * 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 org.apache.river.tool;

import org.apache.river.config.LocalHostLookup;
import org.apache.river.logging.Levels;
import org.apache.river.start.lifecycle.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.BindException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
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;
import org.apache.river.api.util.Startable;

/**
 * A simple HTTP server, for serving up JAR and class files.
 * 

* The following items are discussed below: *

*

*

Logging

*

* * This implementation uses the {@link Logger} named * org.apache.river.tool.ClassServer to log information at the * following logging levels: *

*

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* org.apache.river.tool.ClassServer
Level Description
{@link Level#SEVERE SEVERE}failure to accept an incoming connection
{@link Level#WARNING WARNING}failure to read the contents of a requested file, * failure to find the message resource bundle, failure while * executing the -stop option *
{@link Level#INFO INFO}server startup and termination
{@link Level#CONFIG CONFIG}the JAR files being used for -trees
{@link Levels#HANDLED HANDLED}failure reading an HTTP request or writing a response
{@link Level#FINE FINE}bad HTTP requests, HTTP requests for nonexistent files
{@link Level#FINER FINER}good HTTP requests
* *

*

Examples for running ClassServer

*

* * This server can be run directly from the * {@linkplain #main command line} * or as a nonactivatable service under the * org.apache.river.start.ServiceStarter. *

* An example of running directly from the command line is: *

 * % java -jar install_dir/lib/classserver.jar \
 *        -port 8081 -dir install_dir/lib-dl -verbose
 * 
* where install_dir * is the directory where the JGDMS 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 * install_dir/lib-dl. The -verbose option * also causes download attempts to be logged. *

* An example of running under the Service Starter is: *

 * % java -Djava.security.policy=start_policy \
 *        -jar install_dir/lib/start.jar \
 *        httpd.config
 * 
*

* where start_policy is the name of a security * policy file (not provided), and httpd.config is the * following configuration file: * *

 * import org.apache.river.start.NonActivatableServiceDescriptor;
 * import org.apache.river.start.ServiceDescriptor;
 * 
 * org.apache.river.start {
 * 
 *   serviceDescriptors = new ServiceDescriptor[]{
 *     new NonActivatableServiceDescriptor(
 *       "",
 *       "httpd_policy",
 *       "install_dir/lib/classserver.jar",
 *       "org.apache.river.tool.ClassServer",
 *       new String[]{"-port", "8081", "-dir", "install_dir/lib-dl", "-verbose"})
 *     };
 * }
 * 
* where httpd_policy is the name of a security * policy file (not provided). * * @author Sun Microsystems, Inc. * */ public class ClassServer extends Thread implements Startable { /** 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("org.apache.river.tool.ClassServer"); /** Server socket to accept connections on */ private final ServerSocket server; /** Directories to serve from */ private final String[] dirs; /** Map from String (JAR root) to JarFile[] (JAR class path) */ private final Map map; /** Verbosity flag */ private volatile boolean verbose; /** Stoppable flag */ private final boolean stoppable; /** Read permission on dir and all subdirs, for each dir in dirs */ private final FilePermission[] perms; /** Life cycle control */ private final 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 true if files within JAR files should be * served up * @param verbose true if downloads should be logged * @throws IOException if the server socket cannot be created * @throws NullPointerException if dir is null */ public ClassServer(int port, String dirlist, boolean trees, boolean verbose) throws IOException { this(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 true if files within JAR files should be * served up * @param verbose true if downloads should be logged * @param stoppable true if network shutdown from the * local host should be supported * @throws IOException if the server socket cannot be created * @throws NullPointerException if dir is null */ public ClassServer(int port, String dirlist, boolean trees, boolean verbose, boolean stoppable) throws IOException { this(port, dirlist, trees, verbose, stoppable, null); } private static class Initializer { int port; String dirlist; boolean trees; boolean verbose; boolean stoppable; LifeCycle lifeCycle; Initializer(LifeCycle lifeCycle, String[] args){ port = DEFAULT_PORT; dirlist = DEFAULT_DIR; if (File.separatorChar == '\\') dirlist = DEFAULT_WIN_DIR; trees = false; verbose = false; 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); } } } } private ClassServer(Initializer init) throws IOException { this(init.port, init.dirlist, init.trees, init.verbose, init.stoppable, init.lifeCycle); } /** * Do the real work of the constructor. */ private ClassServer(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(); server.setReuseAddress(true); server.setSoTimeout(300000); // 5 minutes try { server.bind(new InetSocketAddress(port)); } catch( BindException be ) { throw new IOException( "failure to bind to port: "+port, be ); } if (!trees) { map = null; 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 server, accepting the same command line options * supported by {@link #main main}, except for the -stop * option. * * If constructed by org.apache.river.start.ServiceStarter , * {@link Startable#start() }, is called automatically, otherwise {@link #start()} * must be called manually after construction. * * @param args command line options * @param lifeCycle life cycle control object, or null * @throws IOException if the server socket cannot be created * @throws IllegalArgumentException if a command line option is not * understood * @throws NullPointerException if args or any element * of args is null * @see Startable */ public ClassServer(String[] args, LifeCycle lifeCycle) throws IOException { this(new Initializer(lifeCycle, args)); } /** Add transitive Class-Path JARs to jflist. */ private void addJar(String jar, List jflist, Map jfmap) throws IOException { JarFile jf = 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 (!isInterrupted()) { try { new Task(server.accept()).start(); } catch (SocketTimeoutException e){ // This happens every 5 minutes, it allows ClassServer to // be interrupted if necessary. } catch ( SecurityException e){ logger.log(Level.SEVERE, "Permission denied: ", e); interrupt(); } } } catch (IOException e) { if (verbose) { e.printStackTrace(); } if (!server.isClosed()) logger.log(Level.SEVERE, "accepting connection", e); } finally { 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("org.apache.river.tool.resources.classserver"); } catch (MissingResourceException e) { logger.log(Level.WARNING, "missing resource bundle {0}", "org.apache.river.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: *
     * [-port port] [-dir dirlist] [-dirs dirlist] [-stoppable] [-verbose] [-trees]
     * 
* The default port is 8080; the default can be overridden with * the -port option. The default directory on Windows is * J: and the default on other systems is * /vob/jive/lib-dl; the default can be overridden with the * -dir or -dirs 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 path relative to one of the top-level directories, then * the file can be downloaded using the URL *
     * http://host:port/path
     * 
* If a relative path 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.

* * If the -stoppable option is given, the HTTP server can be * shut down with a custom HTTP SHUTDOWN request originating * from the local host. The command line options for stopping an existing * HTTP server are: *

     * [-port port] -stop
     * 
*

* * If the -verbose option is given, then all attempts to * download files are output.

* * The -trees 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 * name.jar or name.zip, * then any individual file named file within it (or within the * JAR or zip files referenced transitively in Class-Path * manifest attributes), can be downloaded using a URL of the form: *

     * http://host:port/name/file
     * 
* If multiple top-level directories have JAR or zip files with the same * name, the file under the first top-level directory with a * match is used. If a Class-Path 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(LocalHostLookup.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); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy