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 }