package org.geoserver.rest.proxy;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.geoserver.proxy.ProxyConfig;
import org.geoserver.proxy.ProxyConfig.Mode;
import org.geoserver.rest.RestletException;
import org.geoserver.security.PropertyFileWatcher;
import org.restlet.Context;
import org.restlet.Restlet;
import org.restlet.data.Form;
import org.restlet.data.MediaType;
import org.restlet.data.Method;
import org.restlet.data.Request;
import org.restlet.data.Response;
import org.restlet.data.Status;
import org.restlet.resource.StreamRepresentation;
/**
* The ProxyRestlet implements a simple REST service that forwards HTTP requests on to a third-party
* webserver.
*
* @author David Winslow <cdwinslow@opengeo.org>
* @author Alan Gerber <agerber@openplans.org>
*/
public class ProxyRestlet extends Restlet {
private static Logger LOGGER = org.geotools.util.logging.Logging.getLogger(ProxyRestlet.class);
private ProxyConfig config;
private boolean watcherWorks;
private PropertyFileWatcher configWatcher;
/*
* Initialize the proxy
*/
public ProxyRestlet()
{
super();
init();
}
/*
* Initialize the proxy with context to call parent with
*/
public ProxyRestlet(Context context)
{
super(context);
init();
}
/*
* Prepares the proxy's environment.
*/
private void init()
{
try{
configWatcher = new PropertyFileWatcher(ProxyConfig.getConfigFile());
watcherWorks = true;
LOGGER.log(Level.INFO, "Proxy init'd pretty much ok.");
}
catch(Exception e){
LOGGER.log(Level.WARNING, "Proxy could not create configuration watcher. Proxy will not be able to update its configuration when it is modified. Exception:", e);
watcherWorks = false;
}
/*Load up the proxy configuration*/
config = ProxyConfig.loadConfFromDisk();
}
/*
* Forwards a request if it is permitted by the proxy's rules as configured.
*/
@Override
public void handle(Request request, Response response) {
/* Check the proxy's config has been modified if the watcher was created correctly*/
if (watcherWorks && configWatcher.isStale()) {
//reload the config if it's stale
config = ProxyConfig.loadConfFromDisk();
}
/* Grab the argument */
Form f = request.getResourceRef().getQueryAsForm();
/* The first argument should be the request for a URL to grab by proxy */
String url = f.getFirstValue("url");
try {
/*Construct the connection to the server*/
URL resolved = new URL(url);
final HttpURLConnection connection = (HttpURLConnection) resolved.openConnection();;
/*Set appropriately whether the connection does output.*/
if (request.getMethod().equals(Method.PUT)
|| request.getMethod().equals(Method.POST)) {
connection.setDoOutput(true);
}
//connection
connection.setRequestMethod(request.getMethod().toString());
/*Check if this request is permitted to be forwarded*/
if (checkPermission(resolved, connection.getContentType()) != true) {
throw new RestletException("Request for nonpermitted content type or hostname",
Status.CLIENT_ERROR_BAD_REQUEST);
}
/*Appropriately forward the message*/
if (request.getMethod().equals(Method.PUT)
|| request.getMethod().equals(Method.POST)) {
//connection.setDoOutput(true);
copyStream(request.getEntity().getStream(), connection.getOutputStream());
}
response.setEntity(new StreamRepresentation(new MediaType(connection
.getContentType())) {
@Override
public void write(OutputStream out) throws IOException {
copyStream(connection.getInputStream(), out);
}
@Override
public InputStream getStream() throws IOException {
throw new UnsupportedOperationException();
}
});
/*If the request is broken, offer appropriate notice*/
} catch (MalformedURLException e) {
LOGGER.log(Level.WARNING, "Invalid proxy request. ", e);
throw new RestletException("Invalid proxy request", Status.CLIENT_ERROR_BAD_REQUEST);
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Couldn't connect to proxied service", e);
throw new RestletException("Couldn't connect to proxied service",
Status.SERVER_ERROR_BAD_GATEWAY);
}
}
private boolean checkPermission(URL locator, String contentType) {
/* Check that the correct protocol is being used */
if (locator.getProtocol().equals("http")) {
boolean hostnameOk = false, mimetypeOk = false;
/* Check hostname and mimetype as appropriate to mode */
if (config.mode == Mode.HOSTNAME || config.mode == Mode.HOSTNAMEANDMIMETYPE
|| config.mode == Mode.HOSTNAMEORMIMETYPE) {
hostnameOk = checkHostnamePermission(locator);
}
if (config.mode == Mode.MIMETYPE || config.mode == Mode.HOSTNAMEANDMIMETYPE
|| config.mode == Mode.HOSTNAMEORMIMETYPE) {
mimetypeOk = checkContentType(contentType);
}
/* Return whether an action is permitted based on how proxy is configured */
switch (config.mode) {
case MIMETYPE:
return mimetypeOk;
case HOSTNAME:
return hostnameOk;
case HOSTNAMEANDMIMETYPE:
return hostnameOk && mimetypeOk;
case HOSTNAMEORMIMETYPE:
return hostnameOk || mimetypeOk;
}
}
/* The request is not permitted to go through. */
return false;
}
/*
* Checks a URL for whether its hostname is permitted
* @param locator A URL to check the permission status of
* @return true if the hostname is permitted; otherwise false
*/
private boolean checkHostnamePermission(URL locator) {
/* Check the whitelist of hosts */
if (config.hostnameWhitelist.contains(locator.getHost())) {
return true;
}
//otherwise say no
return false;
}
/*
* Checks whether the content-type of a request is permitted by the proxy
* @param contentType A content type
* @return true if the content-type is permitted; otherwise false
*/
private boolean checkContentType(String contentType) {
//Trim off extraneous information
String firstType = contentType.split(";")[0];
//Check off the content type provided vs. permitted content types
if (config.mimetypeWhitelist.contains(firstType)) {
return true;
}
//otherwise say no
return false;
}
@Override
public Logger getLogger()
{
return LOGGER;
}
private void copyStream(InputStream in, OutputStream out) throws IOException {
byte[] buff = new byte[4096];
int length = 0;
while ((length = in.read(buff)) != -1) {
out.write(buff, 0, length);
}
}
}