/*
* Copyright (c) 2014 Globo.com - ATeam
* All rights reserved.
*
* This source is subject to the Apache License, Version 2.0.
* Please see the LICENSE file for more information.
*
* Authors: See AUTHORS file
*
* 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.globo.galeb.request;
import org.vertx.java.core.Handler;
import org.vertx.java.core.MultiMap;
import org.vertx.java.core.Vertx;
import org.vertx.java.core.VoidHandler;
import org.vertx.java.core.http.CaseInsensitiveMultiMap;
import org.vertx.java.core.http.HttpClient;
import org.vertx.java.core.http.HttpClientRequest;
import org.vertx.java.core.http.HttpHeaders;
import org.vertx.java.core.http.HttpServerRequest;
import org.vertx.java.core.http.HttpVersion;
import org.vertx.java.core.json.JsonObject;
import com.globo.galeb.bus.IQueueService;
import com.globo.galeb.bus.NullQueueService;
import com.globo.galeb.entity.impl.Farm;
import com.globo.galeb.entity.impl.backend.BackendPool;
import com.globo.galeb.entity.impl.backend.IBackend;
import com.globo.galeb.entity.impl.backend.NullBackend;
import com.globo.galeb.entity.impl.frontend.Rule;
import com.globo.galeb.entity.impl.frontend.Virtualhost;
import com.globo.galeb.exceptions.NotFoundException;
import com.globo.galeb.exceptions.ServiceUnavailableException;
import com.globo.galeb.handlers.ClientRequestExceptionHandler;
import com.globo.galeb.handlers.GatewayTimeoutTaskHandler;
import com.globo.galeb.handlers.RouterResponseHandler;
import com.globo.galeb.logger.SafeLogger;
import com.globo.galeb.metrics.CounterConsoleOut;
import com.globo.galeb.metrics.ICounter;
import com.globo.galeb.rulereturn.HttpCode;
import com.globo.galeb.rulereturn.IRuleReturn;
import com.globo.galeb.scheduler.IScheduler;
import com.globo.galeb.scheduler.ISchedulerHandler;
import com.globo.galeb.scheduler.impl.NullScheduler;
import com.globo.galeb.scheduler.impl.VertxDelayScheduler;
import com.globo.galeb.server.ServerResponse;
import com.globo.galeb.streams.Pump;
/**
* Class RouterRequest.
*
* @author See AUTHORS file.
* @version 1.0.0, Nov 20, 2014.
*/
public class RouterRequest {
/** The http header connection. */
public static final String HTTP_HEADER_CONNECTION = HttpHeaders.CONNECTION.toString();
/** The http header host. */
public static final String HTTP_HEADER_HOST = HttpHeaders.HOST.toString();
/** The http server request. */
private final HttpServerRequest httpServerRequest;
/** The httpClientRequest request. */
private HttpClientRequest httpClientRequest = null;
/** The serverResponse response. */
private ServerResponse serverResponse = null;
/** The schedulerTimeout. */
private IScheduler schedulerTimeOut = new NullScheduler();
/** The json conf. */
private JsonObject conf = new JsonObject();
/** The farm. */
private Farm farm = null;
/** The queue service. */
private IQueueService queueService = new NullQueueService();
/** The counter. */
private ICounter counter = new CounterConsoleOut();
/** The log. */
private SafeLogger log = null;
/** The plataform. */
private Object plataform = null;
/** The http headers. */
private MultiMap headers = new CaseInsensitiveMultiMap();
/** The enable chuncked. */
private boolean enableChuncked = true;
/** The enable access log. */
private boolean enableAccessLog = false;
/** The virtualhost. */
private Virtualhost virtualhost = null;
/** The remote user. */
private RemoteUser remoteUser = new RemoteUser();
/** The connection keepalive. */
private boolean connectionKeepalive = true;
/** The http version. */
private HttpVersion httpVersion = HttpVersion.HTTP_1_1;
/** The backend. */
private IBackend backend = new NullBackend();
/** The request ended flag. */
private boolean requestEnded = false;
/**
* Instantiates a new router request.
*
* @param httpServerRequest the http server request
*/
public RouterRequest(final HttpServerRequest httpServerRequest) {
this.httpServerRequest = httpServerRequest;
}
/**
* Sets the conf.
*
* @param conf the conf
* @return this
*/
public RouterRequest setConf(final JsonObject conf) {
this.conf = conf;
return this;
}
/**
* Sets the farm.
*
* @param farm the farm
* @return this
*/
public RouterRequest setFarm(final Farm farm) {
this.farm = farm;
return this;
}
/**
* Sets the queue service.
*
* @param queueService the queue service
* @return this
*/
public RouterRequest setQueueService(final IQueueService queueService) {
this.queueService = queueService;
return this;
}
/**
* Sets the counter.
*
* @param counter the counter
* @return this
*/
public RouterRequest setCounter(final ICounter counter) {
this.counter = counter;
return this;
}
/**
* Sets the log.
*
* @param log the log
* @return this
*/
public RouterRequest setLog(final SafeLogger log) {
this.log = log;
return this;
}
/**
* Sets the plataform.
*
* @param plataform the plataform
* @return this
*/
public RouterRequest setPlataform(final Object plataform) {
this.plataform = plataform;
return this;
}
/**
* Start RouterRequest.
*/
public void start() {
setUpHeadersAndVersion();
remoteUser = new RemoteUser(httpServerRequest.remoteAddress());
connectionKeepalive = isHttpKeepAlive();
serverResponse = new ServerResponse(httpServerRequest)
.setCounter(counter)
.setLog(log);
defineLoggerIfNecessary();
httpServerRequest.exceptionHandler(new Handler<Throwable>() {
@Override
public void handle(Throwable event) {
log.error("HttpServerRequest fail");
serverResponse.showErrorAndClose(event);
}
});
log.debug(String.format("Received request for host %s '%s %s'", headers.get(RouterRequest.HTTP_HEADER_HOST),
httpServerRequest.method(),
httpServerRequest.absoluteURI().toString()));
virtualhost = farm.getCriterion().when(httpServerRequest).thenGetResult();
if (virtualhost==null) {
serverResponse.showErrorAndClose(new NotFoundException());
return;
}
enableChuncked = virtualhost.getProperties().getBoolean(Virtualhost.ENABLE_CHUNKED_FIELDNAME, true);
enableAccessLog = virtualhost.getProperties().getBoolean(Virtualhost.ENABLE_ACCESSLOG_FIELDNAME, false);
serverResponse.setEnableAccessLog(enableAccessLog)
.setChunked(enableChuncked);
choiceBackend();
if (requestEnded) {
return;
}
if (backend==null || backend instanceof NullBackend) {
log.error("Backend is null");
serverResponse.showErrorAndClose(new ServiceUnavailableException());
return;
}
final HttpClient httpClient = backend.connect(remoteUser);
if (httpClient==null) {
log.error("HttpClient is null");
serverResponse.showErrorAndClose(new ServiceUnavailableException());
return;
}
startSchedulerTimeout(conf.getLong(Farm.REQUEST_TIMEOUT_FIELDNAME, 5000L));
final RouterResponseHandler handlerHttpClientResponse = new RouterResponseHandler();
handlerHttpClientResponse.setScheduler(schedulerTimeOut)
.setQueueService(queueService)
.setLog(log)
.setHttpServerResponse(httpServerRequest.response())
.setsResponse(serverResponse)
.setBackend(backend)
.setRemoteUser(remoteUser)
.setCounter(counter)
.setConnectionKeepalive(connectionKeepalive)
.setHeaderHost(httpServerRequest.headers().get(RouterRequest.HTTP_HEADER_HOST))
.setInitialRequestTime(System.currentTimeMillis());
httpClientRequest = httpClient.request(httpServerRequest.method(), httpServerRequest.uri(), handlerHttpClientResponse);
if (httpClientRequest==null) {
schedulerTimeOut.cancel();
log.error("FAIL: HttpClientRequest is null");
serverResponse.showErrorAndClose(new ServiceUnavailableException());
return;
}
httpClientRequest.setChunked(enableChuncked);
updateRequestHeaders();
pumpStream();
}
private void defineLoggerIfNecessary() {
if (log==null) {
log = new SafeLogger();
}
}
public RouterRequest setUpHeadersAndVersion() {
headers = httpServerRequest.headers();
httpVersion = httpServerRequest.version();
return this;
}
/**
* Pump stream.
*/
private void pumpStream() {
final Pump pump = new Pump(httpServerRequest, httpClientRequest);
defineLoggerIfNecessary();
pump.exceptionHandler(new Handler<Throwable>() {
@Override
public void handle(Throwable event) {
schedulerTimeOut.cancel();
log.error(String.format("FAIL: RouterRequest.pump with %s", event.getMessage()));
serverResponse.showErrorAndClose(new ServiceUnavailableException());
}
});
pump.writeHandler(new Handler<Void>() {
@Override
public void handle(Void v) {
schedulerTimeOut.cancel();
pump.writeHandler(null);
}
});
pump.start();
httpClientRequest.exceptionHandler(new ClientRequestExceptionHandler()
.setQueueService(queueService)
.setLog(log)
.setsResponse(serverResponse)
.setBackendJson(backend.toJson())
.setScheduler(schedulerTimeOut));
httpServerRequest.endHandler(new VoidHandler() {
@Override
public void handle() {
log.debug("sRequest endHandler");
httpClientRequest.end();
}
});
}
/**
* Start scheduler timeout.
*
* @param requestTimeout the request timeout
* @return the i scheduler
*/
private void startSchedulerTimeout(Long requestTimeout) {
defineLoggerIfNecessary();
if (plataform instanceof Vertx) {
schedulerTimeOut = new VertxDelayScheduler((Vertx) plataform);
ISchedulerHandler handler = new GatewayTimeoutTaskHandler(serverResponse, backend.toString());
schedulerTimeOut.setPeriod(requestTimeout)
.setHandler(handler)
.cancelHandler(new Handler<Void>() {
@Override
public void handle(Void event) {
log.debug("scheduler canceled");
}
})
.cancelFailedHandler(new Handler<Void>() {
@Override
public void handle(Void event) {
log.debug("FAIL: scheduler NOT canceled");
}
})
.start();
log.debug("Scheduler started");
}
}
/**
* Choice backend.
*/
private void choiceBackend() {
Rule ruleChosen = virtualhost.getCriterion().when(httpServerRequest).thenGetResult();
IRuleReturn ruleReturn = null;
if (ruleChosen!=null) {
ruleReturn = ruleChosen.getRuleReturn();
} else {
serverResponse.showErrorAndClose(new ServiceUnavailableException());
}
if (ruleReturn instanceof HttpCode) {
serverResponse.setStatusCode(Integer.parseInt(ruleReturn.getReturnId()));
serverResponse.setMessage(((HttpCode)ruleReturn).getMessage());
serverResponse.endResponse();
this.requestEnded = true;
return;
}
BackendPool backendPool = null;
if (ruleReturn instanceof BackendPool) {
backendPool = (BackendPool)ruleReturn;
} else {
serverResponse.showErrorAndClose(new ServiceUnavailableException());
return;
}
defineLoggerIfNecessary();
if (backendPool.getEntities().isEmpty()) {
log.warn(String.format("Pool '%s' without backends", backendPool));
serverResponse.showErrorAndClose(new ServiceUnavailableException());
return;
}
backend = backendPool.getChoice(new RequestData(httpServerRequest));
if (backend!=null && virtualhost!=null) {
backend.setMetricPrefix(virtualhost.getId());
serverResponse.setBackendId(backend.toString());
log.debug(String.format("GetChoice >> Virtualhost: %s, Backend: %s", virtualhost, backend));
}
}
/**
* Update headers xff.
*/
private void updateRequestHeaders() {
final String httpHeaderXRealIp = "X-Real-IP";
final String httpHeaderXForwardedFor = "X-Forwarded-For";
final String httpHeaderforwardedFor = "Forwarded-For";
final String httpHeaderXForwardedHost = "X-Forwarded-Host";
final String httpHeaderXForwardedProto = "X-Forwarded-Proto";
String remote = remoteUser.getRemoteIP();
String headerHost = headers.get(RouterRequest.HTTP_HEADER_HOST).split(":")[0];
httpClientRequest.headers().set(headers);
httpClientRequest.headers().set(RouterRequest.HTTP_HEADER_CONNECTION, "keep-alive");
if (!headers.contains(httpHeaderXRealIp)) {
headers.set(httpHeaderXRealIp, remote);
}
String xff;
if (headers.contains(httpHeaderXForwardedFor)) {
xff = String.format("%s, %s", headers.get(httpHeaderXForwardedFor),remote);
headers.remove(httpHeaderXForwardedFor);
} else {
xff = remote;
}
headers.set(httpHeaderXForwardedFor, xff);
if (headers.contains(httpHeaderforwardedFor)) {
xff = String.format("%s, %s" , headers.get(httpHeaderforwardedFor), remote);
headers.remove(httpHeaderforwardedFor);
} else {
xff = remote;
}
headers.set(httpHeaderforwardedFor, xff);
if (!headers.contains(httpHeaderXForwardedHost)) {
headers.set(httpHeaderXForwardedHost, headerHost);
}
if (!headers.contains(httpHeaderXForwardedProto)) {
headers.set(httpHeaderXForwardedProto, "http");
}
}
/**
* Checks if is http keep alive.
*
* @return true, if is http keep alive
*/
public boolean isHttpKeepAlive() {
return headers.contains(RouterRequest.HTTP_HEADER_CONNECTION) ?
!"close".equalsIgnoreCase(headers.get(RouterRequest.HTTP_HEADER_CONNECTION)) :
httpVersion.equals(HttpVersion.HTTP_1_1);
}
}