Permalink
Please sign in to comment.
Showing
with
389 additions
and 0 deletions.
- +262 −0 Pusher.java
- +89 −0 PusherRequest.java
- +38 −0 README.md
262
Pusher.java
@@ -0,0 +1,262 @@ | ||
+ | ||
+ | ||
+import java.io.IOException; | ||
+import java.io.UnsupportedEncodingException; | ||
+import java.math.BigInteger; | ||
+import java.net.MalformedURLException; | ||
+import java.net.URL; | ||
+import java.security.InvalidKeyException; | ||
+import java.security.MessageDigest; | ||
+import java.security.NoSuchAlgorithmException; | ||
+ | ||
+import javax.crypto.Mac; | ||
+import javax.crypto.spec.SecretKeySpec; | ||
+ | ||
+import org.mortbay.log.Log; | ||
+ | ||
+import com.google.appengine.api.urlfetch.HTTPHeader; | ||
+import com.google.appengine.api.urlfetch.HTTPMethod; | ||
+import com.google.appengine.api.urlfetch.HTTPRequest; | ||
+import com.google.appengine.api.urlfetch.HTTPResponse; | ||
+import com.google.appengine.api.urlfetch.URLFetchService; | ||
+import com.google.appengine.api.urlfetch.URLFetchServiceFactory; | ||
+ | ||
+/** | ||
+ * Static class to send messages to Pusher's REST API. | ||
+ * | ||
+ * Please set pusherApplicationId, pusherApplicationKey, pusherApplicationSecret accordingly | ||
+ * before sending any request. | ||
+ * | ||
+ * @author Stephan Scheuermann | ||
+ * Copyright 2010. Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php | ||
+ */ | ||
+public class Pusher { | ||
+ | ||
+ /** | ||
+ * Pusher Host name | ||
+ */ | ||
+ private final static String pusherHost = "api.pusherapp.com"; | ||
+ | ||
+ /** | ||
+ * Pusher Application Identifier | ||
+ */ | ||
+ private final static String pusherApplicationId = ""; | ||
+ | ||
+ /** | ||
+ * Pusher Application Key | ||
+ */ | ||
+ private final static String pusherApplicationKey = ""; | ||
+ | ||
+ /** | ||
+ * Pusher Secret | ||
+ */ | ||
+ private final static String pusherApplicationSecret = ""; | ||
+ | ||
+ /** | ||
+ * Converts a byte array to a string representation | ||
+ * @param data | ||
+ * @return | ||
+ */ | ||
+ private static String byteArrayToString(byte[] data){ | ||
+ BigInteger bigInteger = new BigInteger(1,data); | ||
+ String hash = bigInteger.toString(16); | ||
+ // Zero pad it | ||
+ while(hash.length() < 32 ){ | ||
+ hash = "0" + hash; | ||
+ } | ||
+ return hash; | ||
+ } | ||
+ | ||
+ /** | ||
+ * Returns a md5 representation of the given string | ||
+ * @param data | ||
+ * @return | ||
+ */ | ||
+ private static String md5Representation(String data) { | ||
+ try { | ||
+ //Get MD5 MessageDigest | ||
+ MessageDigest messageDigest = MessageDigest.getInstance("MD5"); | ||
+ byte[] digest = messageDigest.digest(data.getBytes("US-ASCII")); | ||
+ return byteArrayToString(digest); | ||
+ } catch (NoSuchAlgorithmException nsae) { | ||
+ //We should never come here, because GAE has a MD5 algorithm | ||
+ throw new RuntimeException("No MD5 algorithm"); | ||
+ } catch (UnsupportedEncodingException e) { | ||
+ //We should never come here, because UTF-8 should be available | ||
+ throw new RuntimeException("No UTF-8"); | ||
+ } | ||
+ } | ||
+ | ||
+ /** | ||
+ * Returns a HMAC/SHA256 representation of the given string | ||
+ * @param data | ||
+ * @return | ||
+ */ | ||
+ private static String hmacsha256Representation(String data) { | ||
+ try { | ||
+ // Create the HMAC/SHA256 key from application secret | ||
+ final SecretKeySpec signingKey = new SecretKeySpec( pusherApplicationSecret.getBytes(), "HmacSHA256"); | ||
+ | ||
+ // Create the message authentication code (MAC) | ||
+ final Mac mac = Mac.getInstance("HmacSHA256"); | ||
+ mac.init(signingKey); | ||
+ | ||
+ //Process and return data | ||
+ byte[] digest = mac.doFinal(data.getBytes("UTF-8")); | ||
+ digest = mac.doFinal(data.getBytes()); | ||
+ return byteArrayToString(digest); | ||
+ } catch (NoSuchAlgorithmException nsae) { | ||
+ //We should never come here, because GAE has HMac SHA256 | ||
+ throw new RuntimeException("No HMac SHA256 algorithm"); | ||
+ } catch (UnsupportedEncodingException e) { | ||
+ //We should never come here, because UTF-8 should be available | ||
+ throw new RuntimeException("No UTF-8"); | ||
+ } catch (InvalidKeyException e) { | ||
+ throw new RuntimeException("Invalid key exception while converting to HMac SHA256"); | ||
+ } | ||
+ } | ||
+ | ||
+ /** | ||
+ * Build query string that will be appended to the URI and HMAC/SHA256 encoded | ||
+ * @param eventName | ||
+ * @param jsonData | ||
+ * @return | ||
+ */ | ||
+ private static String buildQuery(String eventName, String jsonData, String socketID){ | ||
+ StringBuffer buffer = new StringBuffer(); | ||
+ //Auth_Key | ||
+ buffer.append("auth_key="); | ||
+ buffer.append(pusherApplicationKey); | ||
+ //Timestamp | ||
+ buffer.append("&auth_timestamp="); | ||
+ buffer.append(System.currentTimeMillis() / 1000); | ||
+ //Auth_version | ||
+ buffer.append("&auth_version=1.0"); | ||
+ //MD5 body | ||
+ buffer.append("&body_md5="); | ||
+ buffer.append(md5Representation(jsonData)); | ||
+ //Event Name | ||
+ buffer.append("&name="); | ||
+ buffer.append(eventName); | ||
+ //Append socket id if set | ||
+ if (!socketID.isEmpty()) { | ||
+ buffer.append("&socket_id="); | ||
+ buffer.append(socketID); | ||
+ } | ||
+ //Return content of buffer | ||
+ return buffer.toString(); | ||
+ } | ||
+ | ||
+ /** | ||
+ * Build path of the URI that is also required for Authentication | ||
+ * @return | ||
+ */ | ||
+ private static String buildURIPath(String channelName){ | ||
+ StringBuffer buffer = new StringBuffer(); | ||
+ //Application ID | ||
+ buffer.append("/apps/"); | ||
+ buffer.append(pusherApplicationId); | ||
+ //Channel name | ||
+ buffer.append("/channels/"); | ||
+ buffer.append(channelName); | ||
+ //Event | ||
+ buffer.append("/events"); | ||
+ //Return content of buffer | ||
+ return buffer.toString(); | ||
+ } | ||
+ | ||
+ /** | ||
+ * Build authentication signature to assure that our event is recognized by Pusher | ||
+ * @param uriPath | ||
+ * @param query | ||
+ * @return | ||
+ */ | ||
+ private static String buildAuthenticationSignature(String uriPath, String query){ | ||
+ StringBuffer buffer = new StringBuffer(); | ||
+ //request method | ||
+ buffer.append("POST\n"); | ||
+ //URI Path | ||
+ buffer.append(uriPath); | ||
+ buffer.append("\n"); | ||
+ //Query string | ||
+ buffer.append(query); | ||
+ //Encode data | ||
+ String h = buffer.toString(); | ||
+ return hmacsha256Representation(h); | ||
+ } | ||
+ | ||
+ /** | ||
+ * Build URI where request is send to | ||
+ * @param uriPath | ||
+ * @param query | ||
+ * @param signature | ||
+ * @return | ||
+ */ | ||
+ private static URL buildURI(String uriPath, String query, String signature){ | ||
+ StringBuffer buffer = new StringBuffer(); | ||
+ //Protocol | ||
+ buffer.append("http://"); | ||
+ //Host | ||
+ buffer.append(pusherHost); | ||
+ //URI Path | ||
+ buffer.append(uriPath); | ||
+ //Query string | ||
+ buffer.append("?"); | ||
+ buffer.append(query); | ||
+ //Authentication signature | ||
+ buffer.append("&auth_signature="); | ||
+ buffer.append(signature); | ||
+ //Build URI | ||
+ try { | ||
+ return new URL(buffer.toString()); | ||
+ } catch (MalformedURLException e) { | ||
+ throw new RuntimeException("Malformed URI"); | ||
+ } | ||
+ } | ||
+ | ||
+ /** | ||
+ * Delivers a message to the Pusher API without providing a socket_id | ||
+ * @param channel | ||
+ * @param event | ||
+ * @param jsonData | ||
+ * @return | ||
+ */ | ||
+ public static HTTPResponse triggerPush(String channel, String event, String jsonData){ | ||
+ return triggerPush(channel, event, jsonData, ""); | ||
+ } | ||
+ | ||
+ /** | ||
+ * Delivers a message to the Pusher API | ||
+ * @param channel | ||
+ * @param event | ||
+ * @param jsonData | ||
+ * @param socketId | ||
+ * @return | ||
+ */ | ||
+ public static HTTPResponse triggerPush(String channel, String event, String jsonData, String socketId){ | ||
+ //Build URI path | ||
+ String uriPath = buildURIPath(channel); | ||
+ //Build query | ||
+ String query = buildQuery(event, jsonData, socketId); | ||
+ //Generate signature | ||
+ String signature = buildAuthenticationSignature(uriPath, query); | ||
+ //Build URI | ||
+ URL url = buildURI(uriPath, query, signature); | ||
+ | ||
+ //Create Google APP Engine Fetch URL service and request | ||
+ URLFetchService urlFetchService = URLFetchServiceFactory.getURLFetchService(); | ||
+ HTTPRequest request = new HTTPRequest(url, HTTPMethod.POST); | ||
+ request.addHeader(new HTTPHeader("Content-Type", "application/json")); | ||
+ request.setPayload(jsonData.getBytes()); | ||
+ | ||
+ //Start request | ||
+ try { | ||
+ return urlFetchService.fetch(request); | ||
+ } catch (IOException e) { | ||
+ //Log warning | ||
+ Log.warn("Pusher request could not be send to the following URI " + url.toString()); | ||
+ return null; | ||
+ } | ||
+ } | ||
+ | ||
+} |
@@ -0,0 +1,89 @@ | ||
+ | ||
+ | ||
+import com.google.appengine.api.urlfetch.HTTPResponse; | ||
+ | ||
+/** | ||
+ * An instance of this class can be used to prepopulate properties (event or channel name) with default values | ||
+ * | ||
+ * @author Stephan Scheuermann | ||
+ * Copyright 2010. Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php | ||
+ */ | ||
+public class PusherRequest { | ||
+ | ||
+ /** | ||
+ * Name of the event | ||
+ */ | ||
+ private String eventName; | ||
+ | ||
+ /** | ||
+ * Channel identifier | ||
+ */ | ||
+ private String channelName; | ||
+ | ||
+ /** | ||
+ * Create an instance and assign a channelName that is used for each request | ||
+ * @param channelName | ||
+ */ | ||
+ public PusherRequest(String channelName) { | ||
+ super(); | ||
+ this.channelName = channelName; | ||
+ } | ||
+ | ||
+ /** | ||
+ * Create an instance and assign both channelName and eventName that are used for each request | ||
+ * @param eventName | ||
+ * @param channelName | ||
+ */ | ||
+ public PusherRequest(String channelName, String eventName) { | ||
+ super(); | ||
+ this.channelName = channelName; | ||
+ this.eventName = eventName; | ||
+ } | ||
+ | ||
+ /** | ||
+ * Triggers a new push with default channelName, eventName properties | ||
+ * @param jsonData | ||
+ * @return | ||
+ */ | ||
+ public HTTPResponse triggerPush(String jsonData){ | ||
+ return Pusher.triggerPush(channelName, eventName, jsonData); | ||
+ } | ||
+ | ||
+ /** | ||
+ * Triggers a new push with default channelName property | ||
+ * @param jsonData | ||
+ * @param eventName | ||
+ * @return | ||
+ */ | ||
+ public HTTPResponse triggerPush(String jsonData, String eventName){ | ||
+ return Pusher.triggerPush(channelName, eventName, jsonData); | ||
+ } | ||
+ | ||
+ /** | ||
+ * Triggers a new push with default channelName property | ||
+ * @param jsonData | ||
+ * @param eventName | ||
+ * @param socketId | ||
+ * @return | ||
+ */ | ||
+ public HTTPResponse triggerPush(String jsonData, String eventName, String socketId){ | ||
+ return Pusher.triggerPush(channelName, eventName, jsonData, socketId); | ||
+ } | ||
+ | ||
+ public void setEventName(String eventName) { | ||
+ this.eventName = eventName; | ||
+ } | ||
+ | ||
+ public String getEventName() { | ||
+ return eventName; | ||
+ } | ||
+ | ||
+ public void setChannelName(String channelName) { | ||
+ this.channelName = channelName; | ||
+ } | ||
+ | ||
+ public String getChannelName() { | ||
+ return channelName; | ||
+ } | ||
+ | ||
+} |
@@ -0,0 +1,38 @@ | ||
+Pusher Java classes for Google App Engine | ||
+========================================= | ||
+ | ||
+This Java classes can be used to communicate very easily with the Pusher REST API (http://www.pusherapp.com) from any Google App engine Web application. | ||
+ | ||
+Get Started | ||
+----------- | ||
+Simply replace the Pusher specific constants in Pusher.java: | ||
+ | ||
+ private final static String pusherApplicationId = ""; | ||
+ | ||
+ private final static String pusherApplicationKey = ""; | ||
+ | ||
+ private final static String pusherApplicationSecret = ""; | ||
+ | ||
+Call one of the two available static methods called "triggerPush" and pass channel name, event name and the message body (JSON encoded data) as parameters: | ||
+ | ||
+ Pusher.triggerPush("test_channel", "my_event", jsonData); | ||
+ | ||
+The second "triggerPush" method provides an additional parameter for the socket_id: | ||
+ | ||
+ Pusher.triggerPush("test_channel", "my_event", jsonData, socketId); | ||
+ | ||
+That's it. | ||
+ | ||
+Default values | ||
+-------------- | ||
+Sometimes it can be very convenient to prepulate a PusherRequest instance with default channel and/or event name: | ||
+ | ||
+ PusherRequest p = new PusherRequest("test_channel","my_event"); | ||
+ | ||
+To send a request to the Pusher API just call "triggerPush" on this instance: | ||
+ | ||
+ p.triggerPush(jsondata); | ||
+ | ||
+License | ||
+------- | ||
+Copyright 2010, Stephan Scheuermann. Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php |
0 comments on commit
b27a916