We added support for doing things that you would normally do in a
ServletFilter
or its ilk. We had the hook there already and the Predicate
already allowed you to chainPredicates
but this did not address that we started to use Predicate
s to wire in health checks and stat check endpoints. We added a mechanism to create chains of predicates. The first one that returns false, the chain stops processing.See https://github.com/ advantageous/qbit/pull/357/ files for the pull request.https://github.com/ advantageous/qbit/issues/355 for issue
The HTTP server allows you to pass a predicate.
setShouldContinueHttpRequest(Predicate<HttpRequest> predicate)
Predicate<HttpRequest> predicate.
The predicate allows for things like security interception. Look for an auth header. Reject request if auth header is not in place.
Predicates are nest-able.
It is often the case, that you will want to run more than one predicate.
To support this, we added
addShouldContinueHttpReq uestPredicate(final Predicate<HttpRequest> predicate)
to the HttpServerBuilder.The HttpServerBuilder will keep a list of predicates, and register them with the HttpServer when it builds the http server.
You can add your own predicates or replace the default predicate mechanism.
HttpServerBuilder
privateRequestContinuePredicate requestContinuePredicate =null;
publicRequestContinuePredicate getRequestContinuePredicate() {
if (requestContinuePredicate ==null) {
requestContinuePredicate =newRequestContinuePredicate();
}
return requestContinuePredicate;
}
publicHttpServerBuilder setRequestContinuePredicate(finalRequestContinuePredicate requestContinuePredicate) {
this.requestContinuePredicate = requestContinuePredicate;
returnthis;
}
publicHttpServerBuilder addShouldContinueHttpRequestPredicate(finalPredicate<HttpRequest> predicate) {
getRequestContinuePredicate().add(predicate);
returnthis;
}
publicclassRequestContinuePredicateimplementsPredicate<HttpRequest>{
privatefinalCopyOnWriteArrayList<Predicate<HttpRequest>> predicates =newCopyOnWriteArrayList<>();
publicRequestContinuePredicateadd(Predicate<HttpRequest>predicate) {
predicates.add(predicate);
returnthis;
}
@Override
publicbooleantest(finalHttpRequesthttpRequest) {
boolean shouldContinue;
for (Predicate<HttpRequest> shouldContinuePredicate : predicates) {
shouldContinue = shouldContinuePredicate.test(httpRequest);
if (!shouldContinue) {
returnfalse;
}
}
returntrue;
}
}
We added a bunch of unit tests to make sure this actually works. :)
We created an example to show how this works.
packagecom.mammatustech;
importio.advantageous.qbit.admin.ManagedServiceBuilder;
importio.advantageous.qbit.annotation.RequestMapping;
importio.advantageous.qbit.http.request.HttpRequest;
importio.advantageous.qbit.http.server.HttpServerBuilder;
importjava.util.function.Predicate;
/**
* Default port for admin is 7777.
* Default port for main endpoint is 8080.
*
* <pre>
* <code>
*
* Access the service:
*
* $ curl http://localhost:8080/root/hello/hello
*
* This above will respond "shove off".
*
* $ curl --header "X-SECURITY-TOKEN: shibboleth"http://localhost:8080/root/hello/hello
*
* This will get your hello message.
*
* To see swagger file for this service:
*
* $ curl http://localhost:7777/__admin/meta/
*
* To see health for this service:
*
* $ curl http://localhost:8080/__health
* Returns "ok" if all registered health systems are healthy.
*
* OR if same port endpoint health is disabled then:
*
* $ curl http://localhost:7777/__admin/ok
* Returns "true" if all registered health systems are healthy.
*
*
* A node is a service, service bundle, queue, or server endpoint that is being monitored.
*
* List all service nodes or endpoints
*
* $ curl http://localhost:7777/__admin/all-nodes/
*
*
* List healthy nodes by name:
*
* $ curl http://localhost:7777/__admin/healthy-nodes/
*
* List complete node information:
*
* $ curl http://localhost:7777/__admin/load-nodes/
*
*
* Show service stats and metrics
*
* $ curl http://localhost:8080/__stats/instance
* </code>
* </pre>
*/
@RequestMapping("/hello")
publicclassHelloWorldService {
@RequestMapping("/hello")
publicStringhello() {
return"hello "+System.currentTimeMillis();
}
publicstaticvoidmain(finalString... args) {
finalManagedServiceBuilder managedServiceBuilder =
ManagedServiceBuilder.managedServiceBuilder().setRootURI("/ root");
finalHttpServerBuilder httpServerBuilder = managedServiceBuilder.getHttpServerBuilder();
/** We can register our security token checker here. */
httpServerBuilder.addShouldContinueHttpRequestPredicate(Hell oWorldService::checkAuth);
/* Start the service. */
managedServiceBuilder.addEndpointService(newHelloWorldService())
.getEndpointServerBuilder()
.build().startServer();
/* Start the admin builder which exposes health end-points and meta data. */
managedServiceBuilder.getAdminBuilder().build().startServer( );
System.out.println("Servers started");
}
/**
* Checks to see if the header <code>X-SECURITY-TOKEN</code> is set to "shibboleth".
* @param httpRequest http request
* @return true if we should continue, i.e., auth passed, false otherwise.
*/
privatestaticbooleancheckAuth(finalHttpRequesthttpRequest) {
/* Only check uri's that start with /root/hello. */
if (httpRequest.getUri().startsWith("/root/hello")) {
finalString x_security_token = httpRequest.headers().getFirst("X-SECURITY-TOKEN");
/* If the security token is set to "shibboleth" then continue processing the request. */
if ("shibboleth".equals(x_security_token)) {
returntrue;
} else {
/* Security token was not what we expected so send a 401 auth failed. */
httpRequest.getReceiver().response(401, "application/json", "\"shove off\"");
returnfalse;
}
}
returntrue;
}
}
To exercise this and show that it is working, let's use curl.
Pass the header token
$ curl --header "X-SECURITY-TOKEN: shibboleth"http://localhost:8080/root/hello/hello
"hello 1440012093122"
No header token
$ curl http://localhost:8080/root/hello/hello
"shove off"
Health request
$ curl http://localhost:8080/__health
"ok"
You may wonder why/how health comes up in this conversation. It is clear really. and configure the health system as a should you continue
EndpointServerBuilder
ManagedServiceBuilder
Predicate
as well.EndpointServerBuilder
publicclassEndpointServerBuilder {
publicEndpointServerBuildersetupHealthAndStats(finalHttpServerBuilderhttpServerBuilder) {
if (isEnableStatEndpoint() || isEnableHealthEndpoint()) {
finalboolean healthEnabled = isEnableHealthEndpoint();
finalboolean statsEnabled = isEnableStatEndpoint();
finalHealthServiceAsync healthServiceAsync = healthEnabled ? getHealthService() :null;
finalStatCollection statCollection = statsEnabled ? getStatsCollection() :null;
httpServerBuilder.addShouldContinueHttpRequestPredicate(
newEndPointHealthPredicate(healthEnabled, statsEnabled,
healthServiceAsync, statCollection));
}
returnthis;
}
package io.advantageous.qbit.server;
import io.advantageous.boon.json.JsonFactory;
import io.advantageous.qbit.http.request.HttpRequest;
import io.advantageous.qbit.service.health.HealthServiceAsync;
import io.advantageous.qbit.service.stats.StatCollection;
import java.util.function.Predicate;
publicclassEndPointHealthPredicateimplementsPredicate<HttpRequest> {
privatefinalboolean healthEnabled;
privatefinalboolean statsEnabled;
privatefinalHealthServiceAsync healthServiceAsync;
privatefinalStatCollection statCollection;
publicEndPointHealthPredicate(booleanhealthEnabled, booleanstatsEnabled,
HealthServiceAsynchealthServiceAsync, StatCollectionstatCollection
) {
this.healthEnabled = healthEnabled;
this.statsEnabled = statsEnabled;
this.healthServiceAsync = healthServiceAsync;
this.statCollection = statCollection;
}
@Override
publicbooleantest(finalHttpRequesthttpRequest) {
boolean shouldContinue =true;
if (healthEnabled && httpRequest.getUri().startsWith("/__health")) {
healthServiceAsync.ok(ok -> {
if (ok) {
httpRequest.getReceiver().respondOK("\"ok\"");
} else {
httpRequest.getReceiver().error("\"fail\"");
}
});
shouldContinue =false;
} elseif (statsEnabled && httpRequest.getUri().startsWith("/__stats")) {
if (httpRequest.getUri().equals("/__stats/instance")) {
if (statCollection !=null) {
statCollection.collect(stats -> {
String json =JsonFactory.toJson(stats);
httpRequest.getReceiver().respondOK(json);
});
} else {
httpRequest.getReceiver().error("\"failed to load stats collector\"");
}
} elseif (httpRequest.getUri().equals("/__stats/global")) {
/* We don't support global stats, yet. */
httpRequest.getReceiver().respondOK("{\"version\":1}");
} else {
httpRequest.getReceiver().notFound();
}
shouldContinue =false;
}
return shouldContinue;
}
}