/*
* Copyright (c) 2013-2017 Cinchapi Inc.
*
* 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.cinchapi.concourse.server.http;
import java.lang.reflect.Method;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import org.reflections.Reflections;
import org.slf4j.LoggerFactory;
import spark.Request;
import spark.Response;
import spark.Route;
import spark.Spark;
import ch.qos.logback.classic.Level;
import com.cinchapi.common.reflect.Reflection;
import com.cinchapi.concourse.server.ConcourseServer;
import com.cinchapi.concourse.server.GlobalState;
import com.cinchapi.concourse.server.http.errors.HttpError;
import com.cinchapi.concourse.thrift.AccessToken;
import com.cinchapi.concourse.thrift.TransactionToken;
import com.cinchapi.concourse.util.Logger;
import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.common.collect.Sets;
import com.google.common.primitives.Longs;
import com.google.gson.JsonObject;
/**
* An server that can handle HTTP requests and delegate calls to a
* {@link ConcourseServer} instance.
*
* @author Jeff Nelson
*/
public class HttpServer {
/**
* Return an {@link HttpServer} that listens on the specified {@code port}
* and serves static files from the default location.
*
* @param concourseServer reference to the ConcourseServer
* @param port
* @return the HttpServer
*/
public static HttpServer create(ConcourseServer concourseServer, int port) {
return new HttpServer(concourseServer, port, "/public");
}
/**
* Return an {@link HttpServer} that listens on the specified {@code port}
* and serves static files from the {@link staticFileLocation}.
*
* @param concourseServer - reference to the ConcourseServer
* @param port
* @param staticFileLocation
* @return the HttpServer
*/
public static HttpServer create(ConcourseServer concourseServer, int port,
String staticFileLocation) {
return new HttpServer(concourseServer, port, staticFileLocation);
}
/**
* Return an {@link HttpServer} that doesn't do anything.
*
* @return the HttpServer
*/
public static HttpServer disabled() {
return new HttpServer(null, 0, "") {
@Override
public void start() {
Logger.info("HTTP Server disabled");
};
@Override
public void stop() {};
};
}
// Turn off logging from third-party code
static {
Reflections.log = null;
((ch.qos.logback.classic.Logger) LoggerFactory
.getLogger("org.eclipse.jetty")).setLevel(Level.OFF);
}
/**
* The port on which the HTTP/S server listens.
*/
private final int port;
/**
* A flag that indicates if the HttpServer is running or not.
*/
private final AtomicBoolean running = new AtomicBoolean(false);
/**
* The location of any static files (i.e. templates, images, etc) that are
* referenced by any Views.
*/
private final String staticFileLocation;
/**
* A reference to the {@link ConcourseServer backend} that is associated
* with this {@link HttpServer}. Typically, the runtime embeds the server.
*/
private final ConcourseServer concourse;
/**
* Construct a new instance.
*
* @param port
* @param staticFileLocation
*/
private HttpServer(ConcourseServer concourse, int port,
String staticFileLocation) {
this.port = port;
this.staticFileLocation = staticFileLocation;
this.concourse = concourse;
}
/**
* Initialize a {@link EndpointContainer container} by registering all of
* its
* endpoints.
*
* @param container the {@link EndpointContainer} to initialize
*/
private static void initialize(EndpointContainer container) {
for (final Endpoint endpoint : container.endpoints()) {
String action = endpoint.getAction();
Route route = new Route(endpoint.getPath()) {
@Override
public Object handle(Request request, Response response) {
response.type(endpoint.getContentType().toString());
// The HttpRequests preprocessor assigns attributes to the
// request in order for the Endpoint to make calls into
// ConcourseServer.
AccessToken creds = (AccessToken) request
.attribute(GlobalState.HTTP_ACCESS_TOKEN_ATTRIBUTE);
String environment = MoreObjects
.firstNonNull(
(String) request
.attribute(GlobalState.HTTP_ENVIRONMENT_ATTRIBUTE),
GlobalState.DEFAULT_ENVIRONMENT);
String fingerprint = (String) request
.attribute(GlobalState.HTTP_FINGERPRINT_ATTRIBUTE);
// Check basic authentication: is an AccessToken present and
// does the fingerprint match?
if((boolean) request
.attribute(GlobalState.HTTP_REQUIRE_AUTH_ATTRIBUTE)
&& creds == null) {
halt(401);
}
if(!Strings.isNullOrEmpty(fingerprint)
&& !fingerprint.equals(HttpRequests
.getFingerprint(request))) {
Logger.warn(
"Request made with mismatching fingerprint. Expecting {} but got {}",
HttpRequests.getFingerprint(request),
fingerprint);
halt(401);
}
TransactionToken transaction = null;
try {
Long timestamp = Longs
.tryParse((String) request
.attribute(GlobalState.HTTP_TRANSACTION_TOKEN_ATTRIBUTE));
transaction = creds != null && timestamp != null ? new TransactionToken(
creds, timestamp) : transaction;
}
catch (NullPointerException e) {}
try {
return endpoint.serve(request, response, creds,
transaction, environment);
}
catch (Exception e) {
if(e instanceof HttpError) {
response.status(((HttpError) e).getCode());
}
else if(e instanceof SecurityException
|| e instanceof java.lang.SecurityException) {
response.removeCookie(GlobalState.HTTP_AUTH_TOKEN_COOKIE);
response.status(401);
}
else if(e instanceof IllegalArgumentException) {
response.status(400);
}
else {
response.status(500);
Logger.error("", e);
}
JsonObject json = new JsonObject();
json.addProperty("error", e.getMessage());
return json.toString();
}
}
};
if(action.equals("get")) {
Spark.get(route);
}
else if(action.equals("post")) {
Spark.post(route);
}
else if(action.equals("put")) {
Spark.put(route);
}
else if(action.equals("delete")) {
Spark.delete(route);
}
else if(action.equals("upsert")) {
Spark.post(route);
Spark.put(route);
}
else if(action.equals("options")) {
Spark.options(route);
}
}
}
/**
* Start the server.
*/
public void start() {
if(running.compareAndSet(false, true)) {
Spark.setPort(port);
Spark.staticFileLocation(staticFileLocation);
// Register all the EndpointContainers and listen for any requests
Reflections reflections = new Reflections(
"com.cinchapi.concourse.server.http.router");
Set<EndpointContainer> weighted = Sets.newTreeSet();
for (Class<? extends EndpointContainer> container : reflections
.getSubTypesOf(EndpointContainer.class)) {
EndpointContainer instance = Reflection.newInstance(container,
concourse);
weighted.add(instance);
}
for (EndpointContainer instance : weighted) {
initialize(instance);
}
Logger.info("HTTP Server enabled on port {}", port);
}
}
/**
* Stop the server
*/
public void stop() {
if(running.compareAndSet(true, false)) {
try {
Method stop = Spark.class.getDeclaredMethod("stop");
Method clearRoutes = Spark.class
.getDeclaredMethod("clearRoutes");
stop.setAccessible(true);
clearRoutes.setAccessible(true);
stop.invoke(null);
clearRoutes.invoke(null);
}
catch (ReflectiveOperationException e) {
throw Throwables.propagate(e);
}
}
}
}