Skip to content

Repository files navigation

java-gripcontrol

Author: Konstantin Bokarius kon@fanout.io

A GRIP library for Java.

License

java-gripcontrol is offered under the MIT license. See the LICENSE file.

Installation

java-gripcontrol is compatible with JDK 6 and above.

Maven:

<dependency>
<groupId>org.fanout</groupId>
<artifactId>gripcontrol</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>org.fanout</groupId>
<artifactId>pubcontrol</artifactId>
<version>1.0.10</version>
</dependency>

HTTPS Publishing

Note that on some operating systems Java may require you to add the root CA certificate of the publishing server to the key store. This is particularly the case with OSX. Follow the steps outlined in this article to address the issue: http://nodsw.com/blog/leeland/2006/12/06-no-more-unable-find-valid-certification-path-requested-target

Also, if using Java 6 you may run into SNI issues. If this occurs we recommend HTTP-only publishing or upgrading to Java 7 or above.

Usage

Examples for how to publish HTTP response and HTTP stream messages to GRIP proxy endpoints via the GripPubControl class.

importorg.fanout.gripcontrol.*;
importorg.fanout.pubcontrol.*;
importjavax.xml.bind.DatatypeConverter;
importjava.util.*;
publicclassGripPubControlExample {
privatestaticclassCallbackimplementsPublishCallback {
publicvoidcompleted(booleanresult, Stringmessage) {
if (result)
System.out.println("Publish successful");
elseSystem.out.println("Publish failed with message: " + message);
}
}
publicstaticvoidmain(String[] args) {
// PubControl can be initialized with or without an endpoint configuration.// Each endpoint can include optional JWT authentication info.// Multiple endpoints can be included in a single configuration.// Initialize PubControl with a single endpoint:List<Map<String, Object>> config = newArrayList<Map<String, Object>>();
Map<String, Object> entry = newHashMap<String, Object>();
entry.put("control_uri", "https://api.fastly.com/service/<service-id>");
// The API token needs to have purge permissionentry.put("key", "<fastly-api-token>");
config.add(entry);
GripPubControlpub = newGripPubControl(config);
// Add new endpoints by applying an endpoint configuration:config = newArrayList<Map<String, Object>>();
HashMap<String, Object> entry1 = newHashMap<String, Object>();
entry1.put("control_uri", "<myendpoint_uri_1>");
config.add(entry1);
HashMap<String, Object> entry2 = newHashMap<String, Object>();
entry2.put("control_uri", "<myendpoint_uri_2>");
config.add(entry2);
pub.applyConfig(config);
// Remove all configured endpoints:pub.removeAllClients();
// Explicitly add an endpoint as a PubControlClient instance:PubControlClientpubClient = newPubControlClient("<myendpoint_uri");
// Optionally set JWT auth: pubClient.setAuthJwt(<claims>, '<key>')// Optionally set basic auth: pubClient.setAuthBasic('<user>', '<password>')pub.addClient(pubClient);
// Publish across all configured endpoints:List<String> channels = newArrayList<String>();
channels.add("<channel>");
try {
pub.publishHttpResponse(channels, "Test publish!");
pub.publishHttpStream(channels, "Test publish!");
} catch (PublishFailedExceptionexception) {
System.out.println(exception.getMessage());
exception.printStackTrace();
}
pub.publishHttpResponseAsync(channels, "Test publish!", newCallback());
pub.publishHttpStreamAsync(channels, "Test publish!", newCallback());
// Wait for all async publish calls to complete:pub.finish();
}
}

Validate the Grip-Sig request header from incoming GRIP messages. This ensures that the message was sent from a valid source and is not expired. When using Pushpin the key is configurable in Pushpin's settings. The key is passed in base64 encoded format (Fastly Fanout support is forthcoming).

booleanisValid = GripControl.validateSig(headers["Grip-Sig"], "<key>");

Long polling example via response headers using the NanoHTTPD web server. The client connects to a GRIP proxy over HTTP and the proxy forwards the request to the origin. The origin subscribes the client to a channel and instructs it to long poll via the response headers. Note that with the recent versions of Apache it's not possible to send a 304 response containing custom headers, in which case the response body should be used instead (next usage example below).

packagecom.example;
importjava.util.*;
importjava.io.IOException;
importfi.iki.elonen.NanoHTTPD;
importorg.fanout.gripcontrol.*;
importorg.fanout.pubcontrol.*;
publicclassAppextendsNanoHTTPD {
publicApp() throwsIOException {
super(80);
start();
}
publicstaticvoidmain(String[] args) {
try {
newApp();
}
catch (IOExceptionexception) { }
}
@OverridepublicResponseserve(IHTTPSessionsession) {
// Validate the Grip-Sig header:if (!GripControl.validateSig(session.getHeaders().get("grip-sig"), "<key>"))
returnnewFixedLengthResponse(Response.Status.UNAUTHORIZED, null,
"invalid grip-sig token");
// Instruct the client to long poll via the response headers:List<Channel> channels = Arrays.asList(newChannel("<channel>"));
Responseresponse = newFixedLengthResponse(Response.Status.OK, null, null);
response.addHeader("grip-hold", "response");
response.addHeader("grip-channel", GripControl.createGripChannelHeader(channels));
// To optionally set a timeout value in seconds:// session.addHeader("grip-timeout", "<timeout_value>");returnresponse;
}
}

Long polling example via response body using the NanoHTTPD web server. The client connects to a GRIP proxy over HTTP and the proxy forwards the request to the origin. The origin subscribes the client to a channel and instructs it to long poll via the response body.

packagecom.example;
importjava.util.*;
importjava.io.IOException;
importfi.iki.elonen.NanoHTTPD;
importorg.fanout.gripcontrol.*;
importorg.fanout.pubcontrol.*;
publicclassAppextendsNanoHTTPD {
publicApp() throwsIOException {
super(80);
start();
}
publicstaticvoidmain(String[] args) {
try {
newApp();
}
catch (IOExceptionexception) { }
}
@OverridepublicResponseserve(IHTTPSessionsession) {
// Validate the Grip-Sig header with a base64 encoded key:if (!GripControl.validateSig(session.getHeaders().get("grip-sig"), "<key>"))
returnnewFixedLengthResponse(Response.Status.UNAUTHORIZED, null,
"invalid grip-sig token");
// Instruct the client to long poll via the response body:List<Channel> channels = Arrays.asList(newChannel("<channel>"));
StringholdResponse = GripControl.createHoldResponse(channels);
// To optionally set a timeout value in seconds:// holdResponse = GripControl.createHoldResponse(channels, null, <timeout_value>);Responseresponse = newFixedLengthResponse(Response.Status.OK, null, holdResponse);
response.addHeader("content-type", "application/grip-instruct");
returnresponse;
}
}

WebSocket example using the NanoHTTPD web server. A client connects to a GRIP proxy via WebSockets and the proxy forward the request to the origin. The origin accepts the connection over a WebSocket and responds with a control message indicating that the client should be subscribed to a channel. Note that in order for the GRIP proxy to properly interpret the control messages, the origin must provide a 'grip' extension in the 'Sec-WebSocket-Extensions' header. This is accomplished in the NanoHTTPD server by overriding the serve(IHTTPSession) method.

packagecom.example;
importjava.util.*;
importjava.io.IOException;
importfi.iki.elonen.*;
importfi.iki.elonen.NanoWSD.WebSocketFrame.*;
importorg.fanout.gripcontrol.*;
importorg.fanout.pubcontrol.*;
importjava.lang.Thread;
importjava.lang.InterruptedException;
publicclassAppextendsNanoWSD {
publicApp() throwsIOException {
super(80);
start();
}
publicstaticvoidmain(String[] args) {
try {
newApp();
}
catch (IOExceptionexception) { }
}
// Override the serve method to ensure that the 'Sec-WebSocket-Extensions' header is sent:@OverridepublicResponseserve(finalIHTTPSessionsession) {
Responseresponse = super.serve(session);
response.addHeader("Sec-WebSocket-Extensions", "grip; message-prefix=\"\"");
returnresponse;
}
@OverrideprotectedWebSocketopenWebSocket(IHTTPSessionhandshake) {
returnnewGripWebSocket(this, handshake);
}
privatestaticclassGripWebSocketextendsWebSocket {
privatefinalAppserver;
publicGripWebSocket(Appserver, IHTTPSessionhandshakeRequest) {
super(handshakeRequest);
this.server = server;
}
@OverrideprotectedvoidonOpen() {
// Create channel hash map:Map<String, Object> channel = newHashMap<String, Object>();
channel.put("channel", "<channel>");
// Subscribe the WebSocket to a channel:try {
send("c:" + GripControl.webSocketControlMessage("subscribe", channel));
} catch (IOExceptionexception) { }
try {
Thread.sleep(1000);
}
catch (InterruptedExceptionexception) { }
// Publish a message to the subscribed channel:Map<String, Object> entry = newHashMap<String, Object>();
entry.put("control_uri", "<myendpoint_url>");
List<Map<String, Object>> config = Arrays.asList(entry);
GripPubControlpub = newGripPubControl(config);
List<String> channels = Arrays.asList("<channel>");
List<Format> formats = Arrays.asList((Format)newWebSocketMessageFormat("WebSocket test publish!"));
try {
pub.publish(channels, newItem(formats, null, null));
} catch (PublishFailedExceptionexception) {
System.err.println("Publish failed: " + exception);
}
}
@OverrideprotectedvoidonClose(CloseCodecode, Stringreason, booleaninitiatedByRemote) { }
@OverrideprotectedvoidonMessage(WebSocketFramemessage) { }
@OverrideprotectedvoidonPong(WebSocketFramepong) { }
@OverrideprotectedvoidonException(IOExceptionexception) { }
}
}

WebSocket over HTTP example using the NanoHTTPD web server. In this case, a client connects to a GRIP proxy via WebSockets and the GRIP proxy communicates with the origin via HTTP.

packagecom.example;
importjava.util.*;
importjava.io.IOException;
importfi.iki.elonen.*;
importorg.fanout.gripcontrol.*;
importorg.fanout.pubcontrol.*;
importjava.lang.Thread;
publicclassAppextendsNanoHTTPD {
publicApp() throwsIOException {
super(80);
start();
}
publicstaticvoidmain(String[] args) {
try {
newApp();
}
catch (IOExceptionexception) { }
}
// A helper class for publishing a message on a separate thread:privateclassPublishMessageimplementsRunnable {
publicvoidrun() {
Thread.sleep(1000);
Map<String, Object> entry = newHashMap<String, Object>();
entry.put("control_uri", "<myendpoint_uri>");
List<Map<String, Object>> config = Arrays.asList(entry);
GripPubControlpub = newGripPubControl(config);
List<String> channels = Arrays.asList("<channel>");
List<Format> formats = Arrays.asList(
(Format)newWebSocketMessageFormat("WebSocket test publish!"));
try {
pub.publish(channels, newItem(formats, null, null));
} catch (PublishFailedExceptionexception) {
System.err.println("Publish failed: " + exception);
}
}
}
@OverridepublicResponseserve(IHTTPSessionsession) {
// Validate the Grip-Sig header with a base64 encoded key:if (!GripControl.validateSig(session.getHeaders().get("grip-sig"), "<key>"))
returnnewFixedLengthResponse(Response.Status.UNAUTHORIZED, null, "invalid grip-sig token");
// Only allow the POST method:Methodmethod = session.getMethod();
if (!Method.POST.equals(method))
returnnewFixedLengthResponse(Response.Status.METHOD_NOT_ALLOWED, null, null);
// Parse the request body:Map<String, String> body = newHashMap<String, String>();
try {
session.parseBody(body);
} catch (IOExceptionexception) {
returnnewFixedLengthResponse(Response.Status.INTERNAL_ERROR, null, null);
} catch (ResponseExceptionexception) {
returnnewFixedLengthResponse(Response.Status.INTERNAL_ERROR, null, null);
}
// Decode the WebSocket events.// Note: appending a new line is required because NanoHTTPD automatically// trims whitespace from the post data.List<WebSocketEvent> inEvents = GripControl.decodeWebSocketEvents(body.get("postData") + "\r\n");
StringresponseBody = "";
if (inEvents != null && inEvents.size() > 0 && inEvents.get(0).type.equals("OPEN")) {
// Create channel hash map:Map<String, Object> channel = newHashMap<String, Object>();
channel.put("channel", "<channel>");
// Open the WebSocket and subscribe it to a channel:List<WebSocketEvent> outEvents = newArrayList<WebSocketEvent>();
outEvents.add(newWebSocketEvent("OPEN"));
outEvents.add(newWebSocketEvent("TEXT", "c:" +
GripControl.webSocketControlMessage("subscribe", channel)));
responseBody = GripControl.encodeWebSocketEvents(outEvents);
// Publish a message to the subscribed channel:newThread(newPublishMessage()).start();
}
Responseresponse = newFixedLengthResponse(Response.Status.OK, null, responseBody);
// Set the headers required by the GRIP proxy:response.addHeader("content-type", "application/websocket-events");
response.addHeader("sec-websocket-extensions", "grip; message-prefix=\"\"");
returnresponse;
}
}

Parse a GRIP URI to extract the URI, ISS, and key values. The values will be returned in a map containing 'control_uri', 'control_iss', and 'key' keys.

Map<String, Object> config = GripControl.parseGripUri(
"https://api.fastly.com/service/<my-service>?key=<fastly-api-token>")

About

A GRIP library for Java.

Resources

Stars

2 stars

Watchers

4 watching

Forks

Releases

Packages

Used by

Contributors

Languages