s3-signer (empty) → 0.1.0.0
raw patch · 9 files changed
+389/−0 lines, 9 filesdep +basedep +base64-bytestringdep +cryptohashsetup-changed
Dependencies added: base, base64-bytestring, cryptohash, http-types, time, utf8-string
Files
- LICENSE +26/−0
- README.md +165/−0
- Setup.hs +2/−0
- s3-signer.cabal +53/−0
- src/Network/S3.hs +30/−0
- src/Network/S3/Sign.hs +11/−0
- src/Network/S3/Time.hs +18/−0
- src/Network/S3/Types.hs +31/−0
- src/Network/S3/URL.hs +53/−0
+ LICENSE view
@@ -0,0 +1,26 @@+Copyright (c) 2014, David Johnson+All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions+are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,165 @@+s3-signer+======+s3-signer is intended to be an aid in building secure cloud-based services with+AWS. This library generates cryptographically secure URLs that+expire at a user-defined interval. These URLs can be used to offload+the process of uploading and downloading large files, freeing your+webserver to focus on other things. ++### Features+ - Minimal depedencies+ - Web framework agnostic+ - Reduced web server load+ - Ideal for AJAX direct-to-s3 upload scenarios++### Documentation+[S3 Query String Request Authentication](http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html#RESTAuthenticationQueryStringAuth)++### Implementation++> AWS Specification++```shell+Signature = URL-Encode( Base64( HMAC-SHA1( YourSecretAccessKeyID,UTF-8-Encoding-Of( StringToSign ) ) ) );+```+> Haskell Implementation++```haskell+module Network.S3.Sign ( sign ) where++import Crypto.Hash.SHA1 (hash)+import Crypto.MAC.HMAC (hmac)+import qualified Data.ByteString.Base64 as B64+import Data.ByteString.UTF8 (ByteString)+import Network.HTTP.Types.URI (urlEncode)++-- | HMAC-SHA1 Encrypted Signature+sign :: ByteString -> ByteString -> ByteString+sign secretKey url = urlEncode True . B64.encode $ hmac hash 64 secretKey url+```++### Use Case+```haskell+{-# LANGUAGE OverloadedStrings #-}++module Main where++import Network.S3++main :: IO ()+main = print =<< generateS3URL credentials request+ where+ credentials = S3Keys "<public-key-goes-here>" "<secret-key-goes-here>"+ request = S3Request S3GET "bucket-name" "file-name.extension" 3 -- 3 secs until expired+```+### Result+```haskell+S3URL {+ signedRequest =+ "https://bucket-name.s3.amazonaws.com/file-name.extension?AWSAccessKeyId=<public-key-goes-here>&Expires=1402346638&Signature=1XraY%2Bhp117I5CTKNKPc6%2BiihRA%3D"+ }+```++### Snap integration - Downloads+```haskell+-- Quick and dirty example+type FileID = ByteString++makeS3URL :: FileID -> IO S3URL+makeS3URL fileId = generateS3URL credentials request+ where+ credentials = S3Keys "<public-key-goes-here>" "<secret-key-goes-here>"+ request = S3Request S3GET "bucket-name" (fileId <> ".zip") 3 ++downloadFile :: Handler App (AuthManager App) ()+downloadFile = method POST $ currentUserId >>= maybe the404 handleDownload+ where handleDownload uid = do+ Just fileId <- getParam "fileId"+ -- Ensure file being requested belongs to user else 403...+ S3URL url <- liftIO $ makeS3URL fileId+ redirect' url 302+```+### Direct to S3 AJAX Uploads+ - Configure S3 Bucket CORS Policy settings+ - [CORS Docs](http://docs.aws.amazon.com/AmazonS3/latest/dev/cors.html#how-do-i-enable-cors)++```xml+<?xml version="1.0" encoding="UTF-8"?>+<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">+ <CORSRule>+ <AllowedOrigin>https://my-url-goes-here.com</AllowedOrigin>+ <AllowedMethod>PUT</AllowedMethod>+ <AllowedHeader>*</AllowedHeader>+ </CORSRule>+</CORSConfiguration>+```+ - Retrieve PUT Request URL via AJAX ++```haskell+type FileID = ByteString++makeS3URL :: FileID -> IO S3URL+makeS3URL fileId = generateS3URL credentials request+ where+ credentials = S3Keys "<public-key-goes-here>" "<secret-key-goes-here>"+ request = S3Request S3PUT "bucket-name" (fileId <> ".zip") 3 ++instance ToJSON S3URL++getUploadURL :: Handler App (AuthManager App) ()+getUploadURL = method POST $ currentUserId >>= maybe the404 handleDownload+ where handleDownload _ = do+ Just fileId <- getParam "fileId"+ writeJSON =<< liftIO (makeS3URL fileId)+```+ - Embed FileReader blob data to request+ - Send upload request++```javascript+var xhr = new XMLHttpRequest();+xhr.open('PUT', url /* S3-URL generated from server */);+xhr.setRequestHeader('Content-Type', 'application/zip'); /* whatever http-content-type makes sense */+xhr.setRequestHeader('x-amz-acl', 'public-read');++/* upload completion check */+xhr.onreadystatechange = function(e) {+ if (this.readyState === 4 && this.status === 200) + console.log('upload complete');+};++/* Amazon gives you progress information on AJAX Uploads */+xhr.upload.addEventListener("progress", function(evt) {+ if (evt.lengthComputable) {+ var v = (evt.loaded / evt.total) * 100,+ val = Math.round(v) + '%',+ console.log('Completed: ' + val);+ }+}, false);++/* error handling */+xhr.upload.addEventListener("error", function(evt) {+ console.log("There has been an error :(");+}, false);++/* Commence upload */+xhr.send(file); // file here is a blob from the file reader API+```+### File Reader Info+[How to read file data from the browser](https://developer.mozilla.org/en-US/docs/Web/API/FileReader)++### Troubleshoooting+- Why do I keep getting 403 forbidden when I attempt to upload or download from a pre-signed URL?+ * Ask yourself the following:+ - Are my keys specified correctly?+ - Did I configure the CORS settings on my bucket properly?+ - Still trouble? [Make an issue](https://github.com/dmjio/s3-signer/issues)+- Why are my URLs expiring faster than the specified time?+ * Ask yourself the following:+ - Is my server's clock synchronized with AWS? [See wiki for NTP info](https://github.com/dmjio/s3-signer/wiki/If-URLs-expire-too-quickly)++### FAQ+- Why didn't you use HMAC-SHA256?+ * It's 30% slower, and no less secure than HMAC-SHA1+ ++
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ s3-signer.cabal view
@@ -0,0 +1,53 @@+name: s3-signer+version: 0.1.0.0+homepage: https://github.com/dmjio/s3-signer+bug-reports: https://github.com/dmjio/s3-signer/issues+synopsis: Pre-signed Amazon S3 URLs+description: + .+ s3-signer creates cryptographically secure Amazon S3 URLs that expire within a user-defined + period. It allows uploading and downloading of content from Amazon S3. + Ideal for AJAX direct-to-s3 uploads via CORS and secure downloads. + Web framework agnostic with minimal dependencies.+ .+ > module Main where+ > import Network.S3+ > main :: IO ()+ > main = print =<< generateS3URL credentials request+ > where+ > credentials = S3Creds "<public-key-goes-here>" "<secret-key-goes-here>"+ > request = S3Request S3GET "bucket-name" "file-name.extension" 3 -- three seconds until expiration+ . + Result+ .+ > S3URL "https://bucket-name.s3.amazonaws.com/file-name.extension?AWSAccessKeyId=<public-key-goes-here>&Expires=1402346638&Signature=1XraY%2Bhp117I5CTKNKPc6%2BiihRA%3D"++license: BSD3+license-file: LICENSE+author: David Johnson <djohnson.m@gmail.com>+maintainer: David Johnson <djohnson.m@gmail.com>+copyright: 2014 David Johnson+category: Network+build-type: Simple+cabal-version: >=1.18+extra-source-files: README.md ++library+ build-depends: base >=4.7 && <4.8+ , base64-bytestring+ , utf8-string+ , http-types+ , cryptohash+ , time+ hs-source-dirs: src+ default-language: Haskell2010+ ghc-options: -Wall+ exposed-modules: Network.S3+ , Network.S3.Types+ other-modules: Network.S3.Sign+ , Network.S3.Time+ , Network.S3.URL++source-repository head+ type: git+ location: https://github.com/dmjio/s3-signer
+ src/Network/S3.hs view
@@ -0,0 +1,30 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++module Network.S3+ ( -- * Create pre-signed AWS S3 URL+ generateS3URL+ -- * Types+ , module Network.S3.Types+ ) where++import Network.S3.Sign+import Network.S3.Time+import Network.S3.Types+import Network.S3.URL++-- | Generates a cryptographically secure URL that expires within a+-- user defined period+--+-- See README for use cases and examples: <https://github.com/dmjio/s3-signer/blob/master/README.md>+generateS3URL :: S3Keys -- ^ Amazon S3 Keys+ -> S3Request -- ^ Amazon S3 Request information+ -> IO S3URL -- ^ Generated URL+generateS3URL S3Keys{..} S3Request{..} = do+ time <- getExpirationTimeStamp secondsToExpire+ let url = case s3method of+ S3GET -> getURL bucketName objectName time+ S3PUT -> putURL bucketName objectName time+ req = s3URL bucketName objectName publicKey time (sign secretKey url)+ return (S3URL req)+
+ src/Network/S3/Sign.hs view
@@ -0,0 +1,11 @@+module Network.S3.Sign ( sign ) where++import Crypto.Hash.SHA1 (hash)+import Crypto.MAC.HMAC (hmac)+import qualified Data.ByteString.Base64 as B64+import Data.ByteString.UTF8 (ByteString)+import Network.HTTP.Types.URI (urlEncode)++-- | SHA1 Encrypted Signature+sign :: ByteString -> ByteString -> ByteString+sign secretKey url = urlEncode True . B64.encode $ hmac hash 64 secretKey url
+ src/Network/S3/Time.hs view
@@ -0,0 +1,18 @@+module Network.S3.Time+ ( getExpirationTimeStamp+ , utcTimeToEpochTime+ ) where++import Control.Applicative ((<$>))+import Data.ByteString.UTF8 (ByteString, fromString)+import Data.Time (UTCTime (..), getCurrentTime)+import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)++getExpirationTimeStamp :: Integer -> IO ByteString+getExpirationTimeStamp secs = fromString . show . (+secs) . utcTimeToEpochTime <$> getCurrentTime++utcTimeToEpochTime :: UTCTime -> Integer+utcTimeToEpochTime = fromIntegral . toSecs+ where+ toSecs :: UTCTime -> Int+ toSecs = round . utcTimeToPOSIXSeconds
+ src/Network/S3/Types.hs view
@@ -0,0 +1,31 @@+{-# LANGUAGE DeriveGeneric #-}++module Network.S3.Types+ ( S3URL (..)+ , S3Keys (..)+ , S3Method (..)+ , S3Request (..)+ ) where++import Data.ByteString.UTF8 (ByteString)+import GHC.Generics (Generic)++newtype S3URL = S3URL {+ signedRequest :: ByteString -- ^ Generated URL+ } deriving (Generic, Show)++data S3Keys = S3Keys {+ publicKey :: ByteString -- ^ AWS Public Key+ , secretKey :: ByteString -- ^ AWS Private Key+ } deriving (Generic, Show)++data S3Method = S3GET -- ^ GET Request+ | S3PUT -- ^ PUT Request+ deriving (Generic, Show)++data S3Request = S3Request {+ s3method :: S3Method -- ^ Type of HTTP Method+ , bucketName :: ByteString -- ^ Name of Amazon S3 Bucket+ , objectName :: ByteString -- ^ Name of Amazon S3 File+ , secondsToExpire :: Integer -- ^ Number of seconds until expiration+ } deriving (Generic, Show)
+ src/Network/S3/URL.hs view
@@ -0,0 +1,53 @@+{-# LANGUAGE OverloadedStrings #-}++module Network.S3.URL+ ( putURL+ , getURL+ , s3URL+ ) where++import Data.Monoid+import Data.String++-- | S3 Upload URL Template+putURL :: (Monoid m, IsString m) => m -> m -> m -> m+putURL bucket object expires =+ mconcat [ "PUT\n\napplication/zip\n"+ , expires+ , "\nx-amz-acl:public-read\n/"+ , bucket+ , "/"+ , object+ ]++-- | S3 Download URL Template+getURL :: (Monoid m, IsString m) => m -> m -> m -> m+getURL bucket object expires =+ mconcat [ "GET\n\n\n"+ , expires+ , "\n/"+ , bucket+ , "/"+ , object+ ]++-- | Amazon S3 URL Template+baseUrl :: (Monoid m, IsString m) => m -> m -> m+baseUrl bucket object =+ mconcat [ "https://"+ , bucket+ , ".s3.amazonaws.com/"+ , object+ ]++s3URL :: (Monoid m, IsString m) =>+ m -> m -> m -> m -> m -> m+s3URL bucket object publicKey expires sig+ = mconcat [ baseUrl bucket object+ , "?AWSAccessKeyId="+ , publicKey+ , "&Expires="+ , expires+ , "&Signature="+ , sig+ ]