packages feed

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 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+              ]