org.apache.river.tool.ClassServer Maven / Gradle / Ivy
Show all versions of classserver Show documentation
/*
* 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:
*
* - {@linkplain #main Command line options}
*
- Logging
*
- Examples for running ClassServer
*
*
*
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);
}
}
}