// Copyright 2012 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.collide.server.fe;
import com.google.collide.dto.shared.JsonFieldConstants;
import org.apache.commons.httpclient.HttpStatus;
import org.jboss.netty.handler.codec.http.QueryStringDecoder;
import org.vertx.java.busmods.BusModBase;
import org.vertx.java.core.Handler;
import org.vertx.java.core.buffer.Buffer;
import org.vertx.java.core.eventbus.Message;
import org.vertx.java.core.http.HttpServer;
import org.vertx.java.core.http.HttpServerRequest;
import org.vertx.java.core.http.HttpServerResponse;
import org.vertx.java.core.json.JsonArray;
import org.vertx.java.core.json.JsonObject;
import org.vertx.java.core.sockjs.SockJSServer;
import java.io.File;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Map;
/**
* A simple web server module that can serve static files bundled with the webserver, as well as
* serve files from the directory that the webserver was launched in via simple URL path prefixes.
*
* This web server can also bridge event bus messages to/from client side JavaScript and the server
* side event bus.
*
* (Implementation based on the stock WebServer module bundled with the Vert.x distribution)
*/
public class WebFE extends BusModBase implements Handler<HttpServerRequest> {
private static final String WEBROOT_PATH = "/res/";
private static final String BUNDLED_STATIC_FILES_PATH = "/static/";
private static final String AUTH_PATH = "/_auth";
private static final String AUTH_COOKIE_NAME = "_COLLIDE_SESSIONID";
/**
* The directory that we will be serving our bundled web application client form. We serve content
* from here when the URL matches {@link #WEBROOT_PATH}
*/
private String bundledStaticFilesPrefix;
/**
* The directory that we are serving files from as the "web root". We serve content from here when
* the URL matches {@link #BUNDLED_STATIC_FILES_PATH}
*/
private String webRootPrefix;
@Override
public void start() {
super.start();
HttpServer server = vertx.createHttpServer();
server.requestHandler(this);
// Configure SSL.
if (getOptionalBooleanConfig("ssl", false)) {
server.setSSL(true)
.setKeyStorePassword(getOptionalStringConfig("keyStorePassword", "password"))
.setKeyStorePath(getOptionalStringConfig("keyStorePath", "server-keystore.jks"));
}
// Configure the event bus bridge.
boolean bridge = getOptionalBooleanConfig("bridge", false);
if (bridge) {
SockJSServer sjsServer = vertx.createSockJSServer(server);
JsonArray inboundPermitted = getOptionalArrayConfig("in_permitted", new JsonArray());
JsonArray outboundPermitted = getOptionalArrayConfig("out_permitted", new JsonArray());
sjsServer.bridge(
getOptionalObjectConfig("sjs_config", new JsonObject().putString("prefix", "/eventbus")),
inboundPermitted, outboundPermitted, getOptionalLongConfig("auth_timeout", 5 * 60 * 1000),
getOptionalStringConfig("auth_address", "participants.authorise"));
}
String bundledStaticFiles = getMandatoryStringConfig("staticFiles");
String webRoot = getMandatoryStringConfig("webRoot");
bundledStaticFilesPrefix = bundledStaticFiles + File.separator;
webRootPrefix = webRoot + File.separator;
server.listen(getOptionalIntConfig("port", 8080), getOptionalStringConfig("host", "127.0.0.1"));
}
/**
* Routes HTTP requests to the Collide web server.
*/
@Override
public void handle(HttpServerRequest req) {
String path = req.path;
if (path.equals("/")) {
// This is a request for the client. Serve it.
authAndWriteHostPage(req);
} else if (path.contains("..")) {
// This is an attempt to escape the directory jail. Deny it.
sendStatusCode(req, 404);
} else if (path.startsWith(WEBROOT_PATH) && (webRootPrefix != null)) {
// This is a request for content in the directory the user started the server in.
req.response.sendFile(webRootPrefix + path.substring(WEBROOT_PATH.length()));
} else if (path.startsWith(BUNDLED_STATIC_FILES_PATH) && (bundledStaticFilesPrefix != null)) {
// This is a request for static content bundled with the client.
req.response.sendFile(bundledStaticFilesPrefix
+ path.substring(BUNDLED_STATIC_FILES_PATH.length()));
} else if (path.startsWith(AUTH_PATH)) {
// This is an attempt to install the session cookie.
writeSessionCookie(req);
} else {
// Otherwise, we don't know what you are looking for.
sendStatusCode(req, HttpStatus.SC_NOT_FOUND);
}
}
private void authAndWriteHostPage(HttpServerRequest req) {
Cookie cookie = Cookie.getCookie(AUTH_COOKIE_NAME, req);
if (cookie != null) {
// We found a session ID. Lets go ahead and serve up the host page.
doAuthAndWriteHostPage(req, cookie.value);
return;
}
// We did not find the session ID. Lets go ahead and serve up the login page that should take
// care of installing a cookie and reloading the page.
sendRedirect(req, "/static/login.html");
}
// TODO: If we want to make this secure, setup SSL and set this as a Secure cookie.
// Also probably want to resurrect XsrfTokens and the whole nine yards. But for now, this is
// purely a tracking cookie.
/**
* Writes cookies for the session ID that is posted to it.
*/
private void writeSessionCookie(final HttpServerRequest req) {
if (!"POST".equals(req.method)) {
sendStatusCode(req, HttpStatus.SC_METHOD_NOT_ALLOWED);
return;
}
// Extract the post data.
req.bodyHandler(new Handler<Buffer>() {
@Override
public void handle(Buffer buff) {
String contentType = req.headers().get("Content-Type");
if ("application/x-www-form-urlencoded".equals(contentType)) {
QueryStringDecoder qsd = new QueryStringDecoder(buff.toString(), false);
Map<String, List<String>> params = qsd.getParameters();
List<String> loginSessionIdList = params.get(JsonFieldConstants.SESSION_USER_ID);
List<String> usernameList = params.get(JsonFieldConstants.SESSION_USERNAME);
if (loginSessionIdList == null || loginSessionIdList.size() == 0 ||
usernameList == null || usernameList.size() == 0) {
sendStatusCode(req, 400);
return;
}
final String sessionId = loginSessionIdList.get(0);
final String username = usernameList.get(0);
vertx.eventBus().send("participants.authorise",
new JsonObject().putString("sessionID", sessionId).putString("username", username),
new Handler<Message<JsonObject>>() {
@Override
public void handle(Message<JsonObject> event) {
if ("ok".equals(event.body.getString("status"))) {
req.response.headers().put("Set-Cookie",
AUTH_COOKIE_NAME + "=" + sessionId + "__" + username + "; HttpOnly");
sendStatusCode(req, HttpStatus.SC_OK);
} else {
sendStatusCode(req, HttpStatus.SC_FORBIDDEN);
}
}
});
} else {
sendRedirect(req, "/static/login.html");
}
}
});
}
private void doAuthAndWriteHostPage(final HttpServerRequest req, String authCookie) {
String[] cookieParts = authCookie.split("__");
if (cookieParts.length != 2) {
sendRedirect(req, "/static/login.html");
return;
}
final String sessionId = cookieParts[0];
String username = cookieParts[1];
final HttpServerResponse response = req.response;
vertx.eventBus().send("participants.authorise", new JsonObject().putString(
"sessionID", sessionId).putString("username", username).putBoolean("createClient", true),
new Handler<Message<JsonObject>>() {
@Override
public void handle(Message<JsonObject> event) {
if ("ok".equals(event.body.getString("status"))) {
String activeClientId = event.body.getString("activeClient");
String username = event.body.getString("username");
if (activeClientId == null || username == null) {
sendStatusCode(req, HttpStatus.SC_INTERNAL_SERVER_ERROR);
return;
}
String responseText = getHostPage(sessionId, username, activeClientId);
response.statusCode = HttpStatus.SC_OK;
byte[] page = responseText.getBytes(Charset.forName("UTF-8"));
response.putHeader("Content-Length", page.length);
response.putHeader("Content-Type", "text/html");
response.end(new Buffer(page));
} else {
sendRedirect(req, "/static/login.html");
}
}
});
}
/**
* Generate the header for the host page that includes the client bootstrap information as well as
* relevant script includes.
*/
private String getHostPage(String userId, String username, String activeClientId) {
StringBuilder sb = new StringBuilder();
sb.append("<html>\n");
sb.append(" <head>\n");
// Include Javascript dependencies.
sb.append("<script src=\"/static/sockjs-0.2.1.min.js\"></script>\n");
sb.append("<script src=\"/static/vertxbus.js\"></script>\n");
sb.append("<script src=\"/static/com.google.collide.client.Collide/com.google.collide.client.Collide." +
"nocache.js\"></script>\n");
// Embed the bootstrap session object.
emitBootstrapJson(sb, userId, username, activeClientId);
emitDefaultStyles(sb);
sb.append(" </head>\n<body><div id='gwt_root'></div></body>\n</html>");
return sb.toString();
}
private void emitDefaultStyles(StringBuilder sb) {
sb.append("<style>\n#gwt_root {\n")
.append("position: absolute;\n")
.append("top: 0;\n")
.append("left: 0;\n")
.append("bottom: 0;\n")
.append("right: 0;\n")
.append("}\n</style>");
}
private void emitBootstrapJson(
StringBuilder sb, String userId, String username, String activeClientId) {
sb.append("<script>\n").append("window['__session'] = {\n")
.append(JsonFieldConstants.SESSION_USER_ID).append(": \"").append(userId).append("\",\n")
.append(JsonFieldConstants.SESSION_ACTIVE_ID).append(": \"").append(activeClientId)
.append("\",\n").append(JsonFieldConstants.SESSION_USERNAME).append(": \"")
.append(username).append("\"\n}\n").append("</script>");
}
private void sendRedirect(HttpServerRequest req, String url) {
req.response.putHeader("Location", url);
sendStatusCode(req, HttpStatus.SC_MOVED_TEMPORARILY);
}
private void sendStatusCode(HttpServerRequest req, int statusCode) {
req.response.statusCode = statusCode;
req.response.end();
}
}