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