module Happstack.Server.S3
( newS3
, closeS3
, createBucket
, createObject
, getObject
, deleteBucket
, deleteObject
, listObjects
, sendRequest
, sendRequest_
, BucketId, ObjectId, AccessKey, SecretKey
, amazonURI
) where
import Happstack.Crypto.HMAC ( hmacSHA1 )
import Happstack.Server.HTTPClient.HTTP
import qualified Happstack.Server.HTTPClient.Stream as Stream
import Network.URI hiding (path)
import Control.Concurrent ( newMVar, modifyMVar, swapMVar
, modifyMVar_, MVar )
import Data.Maybe ( fromJust, fromMaybe )
import Data.List ( intersperse )
import System.Time ( getClockTime, toCalendarTime
, formatCalendarTime )
import System.Locale ( defaultTimeLocale, rfc822DateFormat )
import Text.XML.HaXml ( xmlParse, Document(..), Content(..) )
import Text.XML.HaXml.Xtract.Parse ( xtract )
type BucketId = String
type ObjectId = String
type AccessKey = String
type SecretKey = String
data S3
= S3
{ s3AccessKey :: AccessKey
, s3SecretKey :: SecretKey
, s3URI :: URI
, s3Conn :: MVar (Maybe Connection)
}
signRequest :: S3 -> Request -> IO Request
signRequest s3
= let akey = s3AccessKey s3
skey = s3SecretKey s3
in signRequest' akey skey
signRequest' :: AccessKey -> SecretKey -> Request -> IO Request
signRequest' akey skey request
= do now <- getClockTime
cal <- toCalendarTime now
let isoDate = formatCalendarTime defaultTimeLocale rfc822DateFormat cal
auth = fromJust (uriAuthority (rqURI request))
let dat = concat $ intersperse "\n"
[show (rqMethod request)
,lookupHeader HdrContentMD5
,lookupHeader HdrContentType
,isoDate
,uriPath (rqURI request)]
authorization = Header HdrAuthorization $ "AWS " ++ akey ++ ":" ++ signature
signature = hmacSHA1 skey dat
lookupHeader hn = fromMaybe "" (findHeader hn request)
dateHdr = Header HdrDate isoDate
lengthHdr = Header HdrContentLength (show $ length (rqBody request))
connHdr = Header HdrConnection "Keep-Alive"
hostHdr = Header HdrHost (uriRegName auth)
return $ request
{ rqHeaders = hostHdr:connHdr:lengthHdr:dateHdr:
authorization:rqHeaders request
, rqURI = (rqURI request) { uriScheme = ""
, uriAuthority = Nothing}}
getConnection :: S3 -> IO Connection
getConnection s3
= modifyMVar (s3Conn s3) $ \mbConn ->
case mbConn of
Just conn -> return (mbConn,conn)
Nothing -> do print (uriRegName auth, uriPort auth)
c <- openTCPPort (uriRegName auth) (if null $ uriPort auth then 80 else read$ uriPort auth)
return (Just c,c)
where auth = fromJust (uriAuthority (s3URI s3))
createRequest :: S3 -> RequestMethod -> String -> String -> Request
createRequest _s3 method path body
= Request uri method [] body
where uri = localhost { uriPath = '/':escapeURIString isAllowedInURI path }
sendRequest :: S3 -> Request -> IO String
sendRequest s3 request
= loop =<< signRequest s3 request
where loop request'
= do c <- getConnection s3
ret <- sendHTTP c request'
case ret of
Left ErrorClosed
-> do putStrLn "Connection closed."
swapMVar (s3Conn s3) Nothing
loop request'
Left err -> error ("Failed to connect: " ++ show err)
Right res
| (2,_,_) <- rspCode res -> return (rspBody res)
| otherwise -> error ("Server error: " ++ rspReason res)
sendRequest_ :: S3 -> Request -> IO ()
sendRequest_ s3 request
= do sendRequest s3 request
return ()
newS3 :: AccessKey -> SecretKey -> URI -> IO S3
newS3 akey skey uri
= do conn <- newMVar Nothing
return $ S3 { s3AccessKey = akey
, s3SecretKey = skey
, s3URI = uri
, s3Conn = conn
}
closeS3 :: S3 -> IO ()
closeS3 s3
= modifyMVar_ (s3Conn s3) $ \mbConn ->
case mbConn of
Nothing -> return Nothing
Just conn -> do Stream.close conn
return Nothing
createBucket :: S3 -> BucketId -> Request
createBucket s3 bucket
= createRequest s3 PUT bucket ""
createObject :: S3 -> BucketId -> ObjectId -> String -> Request
createObject s3 bucket object
= createRequest s3 PUT (bucket ++ "/" ++ object)
getObject :: S3 -> BucketId -> ObjectId -> Request
getObject s3 bucket object
= createRequest s3 GET (bucket ++ "/" ++ object) ""
deleteBucket :: S3 -> BucketId -> Request
deleteBucket s3 bucket
= createRequest s3 DELETE bucket ""
deleteObject :: S3 -> BucketId -> ObjectId -> Request
deleteObject s3 bucket object
= createRequest s3 DELETE (bucket ++ "/" ++ object) ""
listObjects :: S3 -> BucketId -> IO [String]
listObjects s3 bucket
= do lst <- sendRequest s3 (createRequest s3 GET bucket "")
return $ ppContent . auxFilter . getContent . xmlParse bucket $ lst
where auxFilter = xtract "*/Key/-"
getContent (Document _ _ e _) = CElem e
ppContent xs = [ s | CString _ s <- xs ]
amazonURI :: URI
amazonURI = fromJust $ parseURI "http://s3.amazonaws.com/"
localhost :: URI
localhost = fromJust $ parseURI "http://localhost/"