scala-s3

aws s3 client in scala

git clone https://9o.is/git/scala-s3.git

AmazonS3.scala

(5930B)


      1 package inc.pyc
      2 package aws
      3 package s3
      4 
      5 import dispatch._
      6 import com.ning.http._
      7 import client._
      8 import util._
      9 
     10 import java.net.URLEncoder._
     11 import java.util.{Date, Locale, SimpleTimeZone}
     12 import java.text.SimpleDateFormat
     13 import javax.crypto
     14 
     15 /**
     16  * Handles the signing, authentication and requests for S3.
     17  */
     18 private[s3] object AmazonS3 {
     19   
     20   /**
     21    * S3 root domain
     22    */
     23   lazy val Root = "s3.amazonaws.com"
     24 
     25   /**
     26    * Date Format: http://www.ietf.org/rfc/rfc0822.txt
     27    */
     28   object rfc822DateParser extends SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US) {
     29     this.setTimeZone(new SimpleTimeZone(0, "GMT"))
     30   }
     31 
     32   def trim(s: String): String = s.dropWhile(_ == ' ').reverse.dropWhile(_ == ' ').reverse.toString
     33 
     34   /**
     35    * MD5 hash of bytes.
     36    */
     37   def md5(bytes: Array[Byte]) = {
     38     import java.security.MessageDigest
     39 
     40     val r = MessageDigest.getInstance("MD5")
     41     r.reset()
     42     r.update(bytes)
     43     Base64.encode(r.digest)
     44   }
     45 
     46   /**
     47    * MD5 hash of InputStream.
     48    */
     49   def md5(stream: java.io.InputStream) = {
     50     import java.security.MessageDigest
     51 
     52     val buffer = new Array[Byte](1024)
     53     val r = MessageDigest.getInstance("MD5")
     54     var numRead: Int = 0
     55     do {
     56       numRead = stream.read(buffer)
     57       if (numRead > 0) {
     58         r.update(buffer, 0, numRead)
     59       }
     60     } while (numRead != -1)
     61 
     62     stream.close()
     63     Base64.encode(r.digest())
     64   }
     65 
     66   /**
     67    * Signing of information to be transfered to S3 servers with the secret key.
     68    */
     69   def sign(method: String, path: String, secretKey: String, date: Date,
     70            contentType: Option[String], contentMd5: Option[String], amzHeaders: Map[String, Set[String]]): String = {
     71     sign(method, path, secretKey, Left(date), contentType, contentMd5, amzHeaders)
     72   }
     73 
     74   /**
     75    * Signing of information to be transfered to S3 servers with the secret key.
     76    */
     77   def sign(method: String, path: String, secretKey: String, dateOrExpires: Either[Date, Long],
     78            contentType: Option[String], contentMd5: Option[String], amzHeaders: Map[String, Set[String]]) = {
     79     val SHA1 = "HmacSHA1"
     80     val message = canonicalString(method, path, dateOrExpires, contentType, contentMd5, amzHeaders)
     81     val sig = {
     82       val mac = crypto.Mac.getInstance(SHA1)
     83       val key = new crypto.spec.SecretKeySpec(bytes(secretKey), SHA1)
     84       mac.init(key)
     85       Base64.encode(mac.doFinal(bytes(message)))
     86     }
     87     sig
     88   }
     89 
     90   def signedUri(accessKey: String, secretKey: String, method: String, path: String, amzHeaders: Map[String, Set[String]],
     91                 expires: Long = defaultExpiryTime,
     92                 contentType: Option[String] = None, contentMd5: Option[String] = None) = {
     93     val signed = encode(sign(method, path, secretKey, Right(expires), contentType, contentMd5, amzHeaders), "UTF-8")
     94     "%s?Signature=%s&Expires=%s&AWSAccessKeyId=%s".format(path, signed, expires, accessKey)
     95   }
     96 
     97   def defaultExpiryTime = System.currentTimeMillis() / 1000 + 600
     98 
     99   /**
    100    * @return the canonical request string that needs to be signed for authentication
    101    */
    102   def canonicalString(method: String, path: String, dateOrExpires: Either[Date, Long], contentType: Option[String],
    103                       contentMd5: Option[String], amzHeaders: Map[String, Set[String]]) = {
    104     val amzString = amzHeaders.toList.sortWith(_._1.toLowerCase < _._1.toLowerCase).map {
    105       case (k, v) => "%s:%s".format(k.toLowerCase, v.map(trim).mkString(","))
    106     }
    107     val dateExpiresString = dateOrExpires match {
    108       case Left(date) => rfc822DateParser.format(date)
    109       case Right(expires) => expires.toString
    110     }
    111     (method :: contentMd5.getOrElse("") :: contentType.getOrElse("") :: dateExpiresString :: Nil) ++ amzString ++ List(path) mkString "\n"
    112   }
    113 
    114   /**
    115    * Bytes of string in UTF-8 encoding.
    116    */
    117   def bytes(s: String) = s.getBytes("UTF-8")
    118 
    119   
    120   implicit def Request2S3RequestSigner(r: RequestBuilder) = new S3RequestSigner(r)
    121   implicit def Request2S3RequestSigner(r: String) = new S3RequestSigner(new RequestBuilder().setUrl(r))
    122 
    123   /**
    124    * Signing of S3 requests.
    125    */
    126   class S3RequestSigner(r: RequestBuilder) {
    127 
    128     import scala.collection.JavaConverters._
    129 
    130     protected def path = RawUri(r.build.getUrl).path.getOrElse("")
    131 
    132     /**
    133      * Handler for authentication information, access and secret key, for signing.
    134      */
    135     def <@(accessKey: String, secretKey: String) = {
    136       val req = r.build
    137       val contentStream = req.getStreamData
    138       val contentMd5 = if (req.getContentLength <= 0) None else Some(md5(contentStream))
    139 
    140       for (cmd5 <- contentMd5)
    141         r.addHeader("Content-MD5", cmd5)
    142 
    143       val headers = req.getHeaders
    144       val contentType = headers.keySet.asScala.find {
    145         _.toLowerCase == "content-type"
    146       }.map {
    147         headers.get(_).asScala.head
    148       }
    149 
    150       val d = new Date
    151       r.addHeader("Authorization", "AWS %s:%s".format(accessKey, sign(req.getMethod, path, secretKey, d, contentType, contentMd5, amazonHeaders)))
    152       r.addHeader("Date", rfc822DateParser.format(d))
    153       r
    154     }
    155 
    156     def signed(accessKey: String, secretKey: String, expires: Long = defaultExpiryTime,
    157                contentType: Option[String] = None, contentMd5: Option[String] = None): RequestBuilder = {
    158       val req = r.build
    159       val path = RawUri(req.getUrl).path.getOrElse("")
    160       val uri = signedUri(accessKey, secretKey, req.getMethod, path, amazonHeaders, expires, contentType, contentMd5)
    161       val requestHeaders = for {
    162         (key, Some(value)) <- Map("Content-Type" -> contentType, "Content-Md5" -> contentMd5)
    163       } yield key -> value
    164       (:/(Root) / uri.substring(1)).setMethod(req.getMethod).setHeaders(req.getHeaders).secure <:< requestHeaders
    165     }
    166 
    167     private def amazonHeaders = {
    168       val headers = r.build.getHeaders
    169       headers.keySet.asScala.filter {
    170         _.toLowerCase.startsWith("x-amz")
    171       }
    172         .map(name => name -> headers.get(name).asScala.toSet).toMap
    173     }
    174   }
    175 
    176 }