Skip to content
This repository has been archived by the owner on May 15, 2023. It is now read-only.

Commit

Permalink
Add basic foundation of Service class
Browse files Browse the repository at this point in the history
It works, but has lots of TODOs and needs more fleshing out.
  • Loading branch information
ejona86 committed Mar 10, 2014
1 parent 55aed62 commit efcf210
Show file tree
Hide file tree
Showing 4 changed files with 339 additions and 5 deletions.
9 changes: 4 additions & 5 deletions src/com/google/enterprise/adaptor/Application.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
*/
public final class Application {
private static final String SLEEP_PATH = "/sleep";
private static final String DEFAULT_CONFIG_FILE
static final String DEFAULT_CONFIG_FILE
= "adaptor-config.properties";

private static final Logger log
Expand Down Expand Up @@ -215,7 +215,7 @@ synchronized void daemonDestroy(long time, TimeUnit unit) {
dashboardServer = null;
}

private static void httpServerShutdown(HttpServer server, long time,
static void httpServerShutdown(HttpServer server, long time,
TimeUnit unit) {
// Workaround Java Bug 7105369.
SleepHandler sleepHandler = new SleepHandler(100 /* millis */);
Expand Down Expand Up @@ -280,7 +280,7 @@ public void run() {
}).start();
}

private static HttpServer createHttpServer(Config config) throws IOException {
static HttpServer createHttpServer(Config config) throws IOException {
HttpServer server;
if (!config.isServerSecure()) {
server = HttpServer.create();
Expand Down Expand Up @@ -321,7 +321,7 @@ public void configure(HttpsParameters params) {
return server;
}

private static HttpServer createDashboardHttpServer(Config config)
static HttpServer createDashboardHttpServer(Config config)
throws IOException {
boolean secure = config.isServerSecure();
HttpServer server;
Expand Down Expand Up @@ -374,7 +374,6 @@ public Config getConfig() {
* @return unused command line arguments
* @throws IllegalStateException when not all configuration keys have values
*/
@VisibleForTesting
static String[] autoConfig(Config config, String[] args, File configFile) {
int i;
for (i = 0; i < args.length; i++) {
Expand Down
52 changes: 52 additions & 0 deletions src/com/google/enterprise/adaptor/ReverseProxyHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ public ReverseProxyHandler(URI destinationBase) {
throw new NullPointerException();
}
this.destinationBase = destinationBase;
if (destinationBase.getScheme() == null || destinationBase.getHost() == null
|| destinationBase.getPath() == null) {
throw new IllegalArgumentException(
"destinationBase must contain a scheme, host, and path");
}
}

@Override
Expand Down Expand Up @@ -206,6 +211,48 @@ private URI computeProxyDestination(HttpExchange ex) {
}
}

/**
* Compute the equivalent URI to this proxy provided a possible URI pointing
* to the destination server.
*
* <p>Redirects and similar require absolute URIs, so the destination server
* is forced to expose its hostname and IP. We could pass the original
* Host in the request (a la Apache's ProxyPreserveHost), but that is not
* sufficent because it can't specify the path. So instead, we rewrite
* such response headers when they match the destinationUri so they point to
* the proxy instead of the destination (a la Apache's ProxyPassReverse).
*/
private String computeReverseProxyDestination(HttpExchange ex,
String location) {
URI uri = null;
try {
uri = new URI(location);
} catch (URISyntaxException e) {
log.log(Level.INFO, "Could not parse value from header: " + location, e);
return location;
}
URI base = destinationBase;
// Check if the value starts with destinationBase. Use URI so that we
// can correctly take case into consideration as appropriate.
if (!(base.getScheme().equalsIgnoreCase(uri.getScheme())
&& base.getHost().equalsIgnoreCase(uri.getHost())
&& base.getPort() == uri.getPort()
&& uri.getPath() != null
// This is correctly case-sensitive.
&& uri.getPath().startsWith(base.getPath()))) {
return location;
}
String lastPartOfPath = uri.getPath().substring(base.getPath().length());
URI requestUri = HttpExchanges.getRequestUri(ex);
try {
return new URI(requestUri.getScheme(), requestUri.getAuthority(),
ex.getHttpContext().getPath() + lastPartOfPath, uri.getQuery(),
uri.getFragment()).toASCIIString();
} catch (URISyntaxException e) {
throw new AssertionError(e);
}
}

private void copyRequestHeaders(HttpExchange from, HttpURLConnection to) {
Set<String> requestHopByHopHeaders
= getHopByHopHeaders(from.getRequestHeaders().get("Connection"));
Expand Down Expand Up @@ -239,6 +286,11 @@ private void copyResponseHeaders(HttpURLConnection from, HttpExchange to) {
if (responseHopByHopHeaders.contains(key)) {
continue;
}
if ("Location".equalsIgnoreCase(key)
|| "Content-Location".equalsIgnoreCase(key)
|| "URI".equalsIgnoreCase(key)) {
value = computeReverseProxyDestination(to, value);
}
to.getResponseHeaders().add(key, value);
}
}
Expand Down
260 changes: 260 additions & 0 deletions src/com/google/enterprise/adaptor/Service.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
// Copyright 2014 Google Inc. All Rights Reserved.
//
// Licensed 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.google.enterprise.adaptor;

import com.sun.net.httpserver.HttpContext;
import com.sun.net.httpserver.HttpServer;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.util.Arrays;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
* Provides environment for running multiple adaptors on the same port and
* having each instance be managed.
*/
// TODO(ejona): improve locking
// TODO(ejona): improve shutdown while starting
// TODO(ejona): improve handling of time limits
// TODO(ejona): improve state pre-condition checking
public final class Service {
private static final Logger log = Logger.getLogger(Service.class.getName());

private final Config config;
private final HttpServer server;
private final HttpServer dashboardServer;
private final ConcurrentMap<String, Instance> instances
= new ConcurrentHashMap<String, Instance>();
private final Thread shutdownHook
= new Thread(new ShutdownHook(), "service-shutdown");
private int index;

private Service(Config config) throws IOException {
this.config = config;
this.server = Application.createHttpServer(config);
this.dashboardServer = Application.createDashboardHttpServer(config);
}

public synchronized Instance createInstance(String name, File jar,
File workingDir) {
Instance instance = new Instance(name, jar, workingDir, index++);
if (instances.putIfAbsent(name, instance) != null) {
throw new IllegalArgumentException("Instance by name already present: "
+ name);
}
instance.install();
return instance;
}

public synchronized void deleteInstance(String name, long time,
TimeUnit unit) {
Instance instance = instances.get(name);
if (instance == null) {
throw new IllegalArgumentException("No instance with name: " + name);
}
instance.stop(time, unit);
instance.uninstall();
instances.remove(name, instance);
}

public synchronized void start() throws IOException {
daemonInit();
// The shutdown hook is purposefully not part of the daemon methods,
// because it should only be done when running from the command line.
Runtime.getRuntime().addShutdownHook(shutdownHook);
daemonStart();
}

synchronized void daemonInit() {
server.start();
dashboardServer.start();
}

synchronized void daemonStart() {
for (Instance instance : instances.values()) {
instance.start();
}
}

public synchronized void stop(long time, TimeUnit unit) {
daemonStop(time, unit);
daemonDestroy(time, unit);
try {
Runtime.getRuntime().removeShutdownHook(shutdownHook);
} catch (IllegalStateException ex) {
// Already executing hook.
}
}

synchronized void daemonStop(long time, TimeUnit unit) {
for (Instance instance : instances.values()) {
instance.stop(time, unit);
}
}

synchronized void daemonDestroy(long time, TimeUnit unit) {
Application.httpServerShutdown(server, time, unit);
Application.httpServerShutdown(dashboardServer, time, unit);
}

static Service daemonMain(String[] args) throws IOException {
Config config = new Config();
Application.autoConfig(config, args,
new File(Application.DEFAULT_CONFIG_FILE));
return new Service(config);
}

public static void main(String[] args) throws IOException {
Service service = daemonMain(args);
// TODO(ejona): decide if we want the JAR to be relative to the parent
// process or the child process (right now it is relative to the child).
service.createInstance("adaptor1", new File("../AdaptorTemplate.jar"),
new File("adaptor1"));
service.createInstance("adaptor2", new File("../AdaptorTemplate.jar"),
new File("adaptor2"));
service.start();
}

/**
* Maybe use this as an interface to allow disabling/enabling and
* starting/stopping of particular instances?
*/
public final class Instance {
private final String name;
private final File jar;
private final File workingDir;
private final int index;
private Thread running;
private ShutdownWaiter waiter;

private Instance(String name, File jar, File workingDir, int index) {
if (name == null) {
throw new NullPointerException();
}
if (name.contains("/") || name.startsWith(".")) {
// Prevent '.', '..', and things containing '/' from being names.
throw new IllegalArgumentException(
"Name must not contain / or start with .");
}
if (jar == null) {
throw new NullPointerException();
}
if (index < 0 || index > 10000) {
throw new IllegalArgumentException("Index too large or small: "
+ index);
}
this.name = name;
this.jar = jar;
this.workingDir = workingDir;
this.index = index;
}

private void install() {
// TODO(ejona): add disable support, where we return Service Unavailable
// instead of Not Found.
}

private void start() {
if (running != null) {
throw new IllegalStateException();
}
final int port = config.getServerPort() + 2 * (index + 1);
final int dashboardPort = config.getServerDashboardPort()
+ 2 * (index + 1);
final String scheme = config.isServerSecure() ? "https" : "http";
waiter = new ShutdownWaiter();
running = new Thread(new Runnable() {
@Override
public void run() {
try {
// TODO(ejona): Figure out how much configuration to share.
int ret = JavaExec.exec(jar, workingDir, Arrays.<String>asList(
"-Dserver.port=" + port,
"-Dserver.dashboardPort=" + dashboardPort,
// TODO(ejona): use same security for child
"-Dserver.secure=false",
// TODO(ejona): REMOVE THIS HACK. WE NEED TO DO PROPER SECURITY
// COMMUNICATION AND HANDLE X-Forwarded-For AND SIMILAR. THIS
// DISABLES ALL SECURITY.
"-Dserver.fullAccessHosts=127.0.0.1",
"-Dgsa.hostname=" + config.getGsaHostname(),
"-Dserver.reverseProxyProtocol=" + scheme,
"-Dserver.reverseProxyPort=" + config.getServerPort()));
if (ret != 0) {
log.log(Level.WARNING, "Error response code from child: " + ret);
}
} catch (IOException ex) {
// TODO(ejona): add more info as to which one failed.
log.log(Level.WARNING, "IOException in subprocess", ex);
} catch (InterruptedException ex) {
log.log(Level.WARNING, "Forced shutdown of child", ex);
}
}
});
running.start();

HttpContext context = server.createContext(
"/" + name + "/", new ReverseProxyHandler(
URI.create("http://127.0.0.1:" + port + "/")));
context.getFilters().add(waiter.filter());

// TODO(ejona): When you end up visiting the dashboard, it redirects you
// to its port. It would be nice to fix RedirectHandler to deal with that,
// although it will require additional config parameters.
HttpContext dashboardContext = dashboardServer.createContext(
"/" + name + "/", new ReverseProxyHandler(
URI.create("http://127.0.0.1:" + dashboardPort + "/")));
dashboardContext.getFilters().add(waiter.filter());
}

private void stop(long time, TimeUnit unit) {
if (running == null) {
throw new IllegalStateException();
}
server.removeContext("/" + name);
dashboardServer.removeContext("/" + name);
try {
waiter.shutdown(time, unit);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
// TODO(ejona): Send graceful shutdown request and join with running
// thread, only interrupting if it times out.
running.interrupt();
try {
running.join(unit.toMillis(time));
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
running = null;
}

private void uninstall() {
}
}

private class ShutdownHook implements Runnable {
@Override
public void run() {
stop(3, TimeUnit.SECONDS);
}
}
}
Loading

0 comments on commit efcf210

Please sign in to comment.