Skip to content

Instantly share code, notes, and snippets.

@marklister
Created September 12, 2012 21:28
Show Gist options
  • Save marklister/3710059 to your computer and use it in GitHub Desktop.
Save marklister/3710059 to your computer and use it in GitHub Desktop.
Scala Amazon Web Service Signed Request Generator

####This class allows you to assemble and sign an Amazon Web Services Request. It was rewritten because I didn't like the original api. And the original was trying to do to much networking for my application.

AwsRequest is immutable.

Prerequisites

You need Apache Commons Codec in your classpath.

#####Usage:

val base= AwsRequest("HSGHGSHGDHWYIU","your-secret-key")
//base now contains your credentials and can be used to build specific requests

val req1=base.params(Map("Name"->"MyTopic","Action"->"CreateTopic")).toUrl
// Perform request ...
val req2=base.params(Map("Name"->"MyTopic","Action"->"DeleteTopic")).toUrl
//Perform request ...

#####More detailed assembly:

val base= AwsRequest("HSGHGSHGDHWYIU","your-secret-key")
val req1=base.param("Name"->"MyTopic")
             .param("Action"->"CreateTopic")
             .server("sns.us-east-1.amazonaws.com")
             .uri("/")
             .toUrl

#####You can also use the constructor to construct the complete request if you prefer.

val req1= new AwsRequest("HSGHGSHGDHWYIU",
                         "your-secret-key",
                         Map("Name"->"MyTopic","Action"->"CreateTopic"),
                         "sns.us-east-1.amazonaws.com").toUrl

#####Or using named parameters:

val req1= new AwsRequest(awsPublicKey="HSGHGSHGDHWYIU",
                         awsSecretKey="your-secret-key",
                         params=Map("Name"->"MyTopic","Action"->"CreateTopic"),
                         server="sns.us-east-1.amazonaws.com").toUrl

#####And you get the case class copy construct for free:

val base= AwsRequest("HSGHGSHGDHWYIU","your-secret-key")
val req1= base.copy(server="sns.us-west-1.amazonaws.com", params=Map("Action"->"DeleteTopic"))
//Perform request

So this is an api experiment as much as anything...

#####Notes

  • Setting the same parameter twice will override the first version.
  • The SignatureVersion, SignatureMethod and Timestamp are hard coded. If you need to change anything here you'll need to get into the source.
package org.catch22.amazonws
/**
* This class derives from https://github.com/keplar/scalapac
* It's released under the Apache licence V2.0
* Copyright (c) 2012 Orderly Ltd. All rights reserved.
* Copyright (c) 2012 Mark Lister
*
*/
import java.net.{ URL, URLEncoder }
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import org.apache.commons.codec.binary.Base64
import java.util.{ TimeZone, Calendar }
import java.text.SimpleDateFormat
import scala.collection.immutable.TreeMap
/**
* An AWS request represents a request to Amazon Web Services.
* The primary reason you would use this class is to sign the request
*/
case class AwsRequest(awsPublicKey: String,
awsSecretKey: String,
params: Map[String, String] = TreeMap.empty,
server: String = "sns.us-east-1.amazonaws.com",
uri: String = "/",
httpMethod: String = "GET") {
private val digest = "HmacSHA256"
private val utf = "UTF-8"
/*
* Make things easy to 'assemble'
*/
def param(me: Tuple2[String, String]): AwsRequest = copy(params = params + me)
def params(p: Map[String, String]): AwsRequest = copy(params = params ++ p)
def server(s: String): AwsRequest = copy(server = s)
def uri(s: String): AwsRequest = copy(uri = s)
def httpMethod(m:String):AwsRequest = copy(httpMethod=httpMethod)
/*
* return a copy of this awsrequest as an url String that you can execute at your pleasure
*/
def toUrl: URL = new URL("http://" + server + uri + "?" +
(basics ++ params + ("Signature" -> signature)).map(p => escape(p._1) +
"=" + escape(p._2)).mkString("&"))
private def escape(s: String) = URLEncoder.encode(s, utf).replace("+", "%20")
.replace("*", "%2A").replace("%7E", "~")
//We insert these if they're not in the map already. And the TreeMap sorts the data.
private def basics = TreeMap("SignatureVersion" -> "2",
"SignatureMethod" -> digest,
"Timestamp" -> timeStamp,
"AWSAccessKeyId" -> awsPublicKey)
private def processedParams = {
(basics ++ params).map(x => escape(x._1) + "=" + escape(x._2)).mkString("&")
}
private def urlEncode(s: String) =
URLEncoder.encode(s, "UTF-8").replace("+", "%20")
.replace("*", "%2A").replace("%7E", "~")
//Should be private but access is handy if you are debugging
def stringToSign = List(httpMethod, server, uri, processedParams).mkString("\n")
private def signature: String = {
val mac = Mac.getInstance(digest)
val secretKeySpec = new SecretKeySpec(awsSecretKey.getBytes(utf), digest)
mac.init(secretKeySpec)
val data = stringToSign.getBytes(utf)
val rawHmac = mac.doFinal(data)
Base64.encodeBase64String(rawHmac).trim()
}
private def timeStamp: String = {
val dfm = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")
dfm.setTimeZone(TimeZone.getTimeZone("GMT"))
dfm.format(Calendar.getInstance.getTime)
}
//Avoid dumping our secret key in the logs
override def toString = {
val t = AwsRequest.unapply(this).get.productIterator.toList
"AwsRequest(" + (t.head::"[secret key not shown]"::t.tail.drop(1)).mkString(",") + ")"
}
}
object AwsRequest {
def apply(awsPublicKey: String, awsSecretKey: String) =
new AwsRequest(awsPublicKey, awsSecretKey)
def apply(awsPublicKey: String, awsSecretKey: String, params: Map[String, String]) =
new AwsRequest(awsPublicKey, awsSecretKey, params)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment