sssp (empty) → 1.0.0
raw patch · 8 files changed
+798/−0 lines, 8 filesdep +attemptdep +attoparsecdep +awssetup-changed
Dependencies added: attempt, attoparsec, aws, base, blaze-builder, bytestring, case-insensitive, conduit, containers, data-default, http-conduit, http-types, mtl, network-conduit, text, wai, wai-extra, warp
Files
- Aws/SSSP.hs +346/−0
- Aws/SSSP/App.hs +40/−0
- Aws/SSSP/Configuration.hs +147/−0
- LICENSE +28/−0
- README +133/−0
- Setup.hs +4/−0
- sssp.cabal +93/−0
- sssp.hs +7/−0
+ Aws/SSSP.hs view
@@ -0,0 +1,346 @@+{-# LANGUAGE OverloadedStrings+ , ParallelListComp+ , RecordWildCards+ , StandaloneDeriving+ , PatternGuards+ #-}+module Aws.SSSP where++import Control.Applicative+import Control.Arrow ((***))+import Control.Monad+import Data.Char+import Data.Either+import Data.ByteString (ByteString)+import qualified Data.ByteString.Char8 as Bytes+import qualified Data.List as List+import Data.Maybe+import Data.Monoid+import Data.Ord+import qualified Data.Set as Set+import Data.Word++import qualified Aws as Aws+import qualified Aws.Core as Aws+import qualified Aws.S3 as Aws+import qualified Blaze.ByteString.Builder as Blaze+import qualified Blaze.ByteString.Builder.Char.Utf8 as Blaze+import Control.Monad.Trans+import Data.Attempt+import Data.Attoparsec.Text (Parser)+import qualified Data.Attoparsec.Text as Atto+import qualified Data.Conduit as Conduit+import Data.Text (Text)+import qualified Data.Text as Text+import qualified Data.Text.Read as Text+import qualified Network.Wai as WWW+import qualified Network.HTTP.Conduit as Conduit+import qualified Network.HTTP.Types as HTTP++import qualified Aws.SSSP.WWW as WWW+++wai :: Ctx -> WWW.Application+wai ctx@Ctx{..} req@WWW.Request{..} = do+ resolved <- task ctx requestMethod (resource req)+ maybe (return badTask) id $ do+ task <- resolved+ Just $ case task of+ Retrieve t -> do+ maybe (return badTime) id $ do+ seconds <- timeParam+ Just $ do+ sigInfo <- liftIO $ sigData (fromIntegral seconds)+ let q = Aws.getObject bucket t+ s = Aws.queryToUri (Aws.signQuery q s3 sigInfo)+ b = Blaze.fromByteString (s `Bytes.snoc` '\n')+ m = "max-age=" ++ show (seconds - 1)+ headers = [("Cache-Control", Bytes.pack m)+ ,("Content-Type", "text/plain")+ ,("Location", s)]+ if direct then WWW.proxied manager (Bytes.unpack s)+ else return $ WWW.ResponseBuilder status307 headers b+ Listing ts -> do+ return $ WWW.ResponseBuilder+ HTTP.status200 [("Content-Type", "text/plain")]+ (mconcat (plusNL . s3Encode <$> ts))+ Remove ts -> do+ let deletes = [ Aws.DeleteObject t bucket | t <- ts ]+ responses <- mapM (Aws.aws aws s3 manager) deletes+ let attempts = [ attempt | Aws.Response _meta attempt <- responses ]+ d = [ mappend `uncurry` case a of+ Success _ -> ("deleted: ", t)+ Failure _ -> ("failed: ", t) | a <- attempts | t <- ts ]+ status | all isSuccess attempts = HTTP.status200+ | otherwise = HTTP.status500+ return $ WWW.ResponseBuilder+ status [("Content-Type", "text/plain")]+ (Blaze.fromText . Text.unlines $ d)+ Write t -> do+ let len = join $ listToMaybe+ [ fst <$> (listToMaybe . reads . Bytes.unpack) v+ | (k, v) <- requestHeaders, k == "Content-Length" ]+ maybe (return noLength) id $ do+ n <- len+ Just . maybe (return badTime) id $ do+ seconds <- timeParam+ Just $ do+ sigInfo <- liftIO $ sigData (fromIntegral seconds)+ let q = Aws.putObject bucket t (blazeBody n)+ r = WWW.addHeaders q requestHeaders+ s = Aws.queryToUri (Aws.signQuery r s3 sigInfo)+ b = Blaze.fromByteString (s `Bytes.snoc` '\n')+ m = "max-age=" ++ show (defaultSeconds - 1)+ headers = [("Cache-Control", Bytes.pack m)+ ,("Content-Type", "text/plain")+ ,("Location", s)]+ return $ WWW.ResponseBuilder status307 headers b+ where+ defaultSeconds = 10+ status307 = HTTP.Status 307 "Temporary Redirect"+ sigData n = Aws.signatureData (Aws.ExpiresIn n) (Aws.credentials aws)+ badTask = WWW.ResponseBuilder+ HTTP.status400 [("Content-Type", "text/plain")]+ (Blaze.fromByteString "Malformed task.\n")+ noLength = WWW.ResponseBuilder+ HTTP.status400 [("Content-Type", "text/plain")]+ (Blaze.fromByteString "No Content-Length header.\n")+ badTime = WWW.ResponseBuilder+ HTTP.status400 [("Content-Type", "text/plain")]+ (Blaze.fromByteString "Give time as t=2..40000000\n")+ blazeBody len = Conduit.RequestBodySource len+ . Conduit.mapOutput Blaze.fromByteString $ requestBody+ timeParam = (maybe defaultSeconds id . listToMaybe)+ <$> sequence [ f v | (k, Just v) <- queryString, k == "t" ]+ where+ f :: ByteString -> Maybe Integer+ f = (validate =<<) . (fst <$>) . listToMaybe . reads . Bytes.unpack+ validate i = guard (notTooMany i) >> Just i+ notTooMany i = i >= 2 && i <= 40000000 -- About 16 months+ direct = any ((=="direct") . fst) queryString++task :: Ctx -> HTTP.Method -> Resource -> Conduit.ResourceT IO (Maybe Task)+task ctx method resource = do+ urls <- fromAttempt <$> resolve ctx resource+ return $ case (method, resource) of+ ("GET", Singular _) -> Retrieve <$> (listToMaybe =<< urls)+ ("GET", Plural _) -> Listing <$> urls+ ("DELETE", _) -> Remove <$> urls+ ("PUT", Singular _) -> Write <$> (listToMaybe =<< urls)+ _ -> Nothing++data Task = Retrieve Text+ | Listing [Text]+ | Remove [Text]+ | Write Text -- (Conduit.RequestBody IO)+ deriving (Eq, Ord, Show)++-- | Resources are either singular or plural in character. URLs ending ending+-- in @/@ or containing set wildcards specify plural resources; all other+-- URLs indicate singular resources. A singular resource results in a+-- redirect while a plural resource results in a newline-separated list of+-- URLs (themselves singular in character).+data Resource = Singular [Either Text Wildcard]+ | Plural [Either (Either Text Wildcard) SetWildcard]+ deriving (Eq, Ord, Show)++data Ctx = Ctx { bucket :: Aws.Bucket+ , aws :: Aws.Configuration+ , s3 :: Aws.S3Configuration Aws.NormalQuery+ , manager :: Conduit.Manager }+instance Show Ctx where+ show Ctx{..} = mconcat [ "Ctx { bucket=", show bucket+ , ", aws=..., s3=", show s3, " }" ]++data Order = ASCII | SemVer deriving (Eq, Ord, Show)+data Wildcard = Hi Order | Lo Order deriving (Eq, Ord, Show)+data SetWildcard = Include Word Wildcard | Exclude Word Wildcard+ deriving (Eq, Ord, Show)++-- | Interpret a request URL as a resource, expanding wildcards as needed. By+-- default, wildcards are expanded with @\@@ as the meta-character (@\@hi@,+-- @\@lo.semver5@) but the meta-character can be changed with a query+-- parameter so we pass the whole request here.+--+-- The meta-character is in leading position in wildcard path components and+-- escapes itself in leading position, in a simple way: leading runs are+-- shortened by one character. Some examples of path components and their+-- interpretation are helpful:+--+-- > hi -> The string "hi".+-- > @hi -> The hi.semver wildcard.+-- > @@hi -> The string "@hi".+-- > @@@hi -> The string "@@hi".+-- > ...and so on...+--+-- Sending @meta=_@ as a query parameter changes the meta-character to an+-- underscore. The meta-character may be any single character; empty or+-- overlong @meta@ parameters are ignored.+resource :: WWW.Request -> Resource+resource WWW.Request{..} = url metaChar pathInfo+ where+ metaParams = [ b | Just (b, _) <- culled ] :: [Char]+ where culled = [ Bytes.uncons v | (k, Just v) <- queryString, k == "meta" ]+ metaChar = List.head (metaParams ++ ['@'])++url :: Char -> [Text] -> Resource+url _ [] = Plural []+url meta texts | singular = Singular (lefts components)+ | otherwise = Plural components+ where+ (empty, full) = List.break (/= "") . List.reverse $ texts+ empty' = if empty /= [] then [""] else []+ components = parse <$> List.reverse (empty' ++ full)+ singular = empty == [] && rights components == []+ -- Parser is total but just to be on the safe side...+ parse t = either (const . Left . Left $ t) id+ (Atto.parseOnly (component meta) t)++-- | Parse a single path component.+component :: Char -> Parser (Either (Either Text Wildcard) SetWildcard)+component meta = eitherRotate <$> Atto.eitherP+ (Atto.eitherP (setWildcard meta) (wildcard meta) <* Atto.endOfInput)+ (plain meta)+ where+ eitherRotate :: Either (Either SetWildcard Wildcard) Text+ -> Either (Either Text Wildcard) SetWildcard+ eitherRotate (Left (Right wc)) = Left (Right wc)+ eitherRotate (Left (Left set)) = Right set+ eitherRotate (Right text) = Left (Left text)++-- | Parse a plain string, shrinking leading runs of the metacharacter by one.+plain :: Char -> Parser Text+plain c = mappend <$> (Text.drop 1 <$> Atto.takeWhile (== c)) <*> Atto.takeText++-- | Match a simple, singular wildcard.+wildcard :: Char -> Parser Wildcard+wildcard meta = Atto.char meta *> Atto.choice matchers+ where+ matcher (t, w) = Atto.string t *> pure w+ matchers = matcher <$> wildcards++-- | Match a wildcard set, ending with a count (if it is inclusive) or an+-- optional count and a final tilde (if it is exclusive).+setWildcard :: Char -> Parser SetWildcard+setWildcard meta = star meta <|> wildcard meta <**> (exclude <|> include)+ where+ include = Include <$> Atto.decimal+ exclude = Exclude <$> Atto.option 1 Atto.decimal <* Atto.char '~'++star :: Char -> Parser SetWildcard+star meta = Atto.char meta *> Atto.char '*'+ *> (Exclude 0 . Lo <$> Atto.option SemVer ordering)+ where+ ordering :: Parser Order+ ordering = (Atto.string ".ascii" *> pure ASCII)+ <|> (Atto.string ".semver" *> pure SemVer)++-- | Wildcards and their textual representations.+wildcards :: [(Text, Wildcard)]+wildcards = [( "hi.ascii", Hi ASCII) ,( "lo.ascii", Lo ASCII)+ ,("hi.semver", Hi SemVer) ,("lo.semver", Lo SemVer)+ ,( "hi", Hi SemVer) ,( "lo", Lo SemVer)]+-- The order of these matters when they are translated to alternative+-- Attoparsec parsers, which is unfortunate and seemingly contrary to the+-- documentation. In lieu of left-factoring, we put the longer prefixes last.+++-- | Translate a resource in to a listing of objects. While intermediate S3+-- prefixes (directories) are traversed, the final match is always on keys+-- for objects.+resolve :: Ctx -> Resource -> Conduit.ResourceT IO (Attempt [Text])+resolve Ctx{..} res = case res of+ Plural [ ] -> (fst <$>) <$> listing Ctx{..} ""+ Plural components -> resolve' "" (simplify <$> components)+ Singular components -> resolve' "" (pluralize <$> components)+ where+ pluralize :: Either Text Wildcard -> Either Text SetWildcard+ pluralize = either Left (Right . Include 1)+ simplify :: Either (Either Text Wildcard) SetWildcard+ -> Either Text SetWildcard+ simplify = either pluralize Right+ resolve' prefix [ ] = return (Success [prefix])+ resolve' prefix (h:t) = case h of+ Left "" -> (fst <$>) <$> listing Ctx{..} prefix+ Left text -> resolve' (prefix -/- text) t+ Right set -> do+ attempt <- listing Ctx{..} prefix+ case names <$> attempt of+ Success texts -> (List.concat <$>) . sequence <$> mapM recurse texts+ Failure e -> return (Failure e)+ where+ recurse text = resolve' text t+ names (objects, prefixes) = expand set $ case t of [ ] -> objects+ _:_ -> prefixes++listing :: Ctx -> Text -> Conduit.ResourceT IO (Attempt ([Text],[Text]))+listing Ctx{..} prefix =+ ((concat *** concat) . unzip <$>) <$> listing' Nothing []+ where+ listing' mark acc = do+ Aws.Response _meta attempt <- Aws.aws aws s3 manager gb{Aws.gbMarker=mark}+ case attempt of+ Success Aws.GetBucketResponse{..} -> do+ let (keys, pres) = (Aws.objectKey <$> gbrContents, gbrCommonPrefixes)+ if length keys < 1000 -- TODO: Use truncation flag.+ then (return . Success . reverse) ((keys, pres):acc)+ else listing' (Just (last keys)) ((keys, pres):acc)+ Failure e -> return (Failure e)+ gb = Aws.GetBucket { Aws.gbBucket = bucket+ , Aws.gbPrefix = Just (prefix -/- "")+ , Aws.gbDelimiter = Just "/"+ , Aws.gbMaxKeys = Just 1000 -- The Amazon maximum.+ , Aws.gbMarker = Nothing }++expand :: SetWildcard -> [Text] -> [Text]+expand set texts+ | complement && count == 0 = ordered texts -- Special case for "all" wilcard.+ | complement = complemented matching+ | otherwise = matching+ where+ matching = (selected . ordered) texts+ uniq = Set.fromList texts+ complemented = ordered . Set.toList . Set.difference uniq . Set.fromList+ (count, wc, complement) = case set of+ Include count wc -> (fromIntegral count, wc, False)+ Exclude count wc -> (fromIntegral count, wc, True)+ (ordered, selected) = case wc of+ Hi o -> (order o, List.reverse . List.take count . List.reverse)+ Lo o -> (order o, List.take count)++a -/- b | a == "" = mappend a b+ | "/" `Text.isSuffixOf` a = mappend a b+ | otherwise = mconcat [a, "/", b]++-- | Split a URL into components, placing the balance of slashes in a slash+-- run to the left of the last slash. This is what all the Amazon APIs --+-- including the HTTP interface -- seem to expect, based on experiment.+-- This function exists so that we can split a URL retrieved from S3, by way+-- of list bucket, for example, into pieces for later escaping.+s3Pieces :: Text -> [Text]+s3Pieces text = reverse . uncurry (:) $ List.foldl' f (leading', []) rest'+ where+ (leading, rest) = List.span (=="") (Text.split (=='/') text)+ leading'' = Text.pack [ '/' | _ <- leading ]+ (leading', rest') | h:t <- rest = (mappend leading'' h, t)+ | otherwise = (leading'', [])+ f (piece, pieces) "" = (piece `Text.snoc` '/', pieces)+ f (piece, pieces) s = (s, piece:pieces)++-- | Encode an S3 path to a URL, splitting on slashes but preserving slash+-- runs as appropriate.+s3Encode :: Text -> Blaze.Builder+s3Encode = HTTP.encodePathSegmentsRelative . s3Pieces++plusNL :: Blaze.Builder -> Blaze.Builder+plusNL = (`mappend` Blaze.fromChar '\n')++order :: Order -> [Text] -> [Text]+order ASCII = id -- Amazon returns them sorted anyways...+order SemVer = List.sortBy (comparing textSemVer)++textSemVer :: Text -> [Integer]+textSemVer = (fst <$>) . rights . (Text.decimal <$>) . digitalPieces+ where+ digitalPieces = List.filter (/= "") . Text.split (not . isDigit)+
+ Aws/SSSP/App.hs view
@@ -0,0 +1,40 @@+{-# LANGUAGE OverloadedStrings+ #-}+module Aws.SSSP.App where++import Control.Applicative+import qualified Data.ByteString.Char8 as Bytes+import Data.Monoid+import System.Environment+import System.Exit+import System.IO++import qualified Network.Wai.Handler.Warp as WWW+import qualified Network.Wai.Middleware.RequestLogger as WWW++import Aws.SSSP.Configuration+import Aws.SSSP+++app = args =<< getArgs++args [ ] = web+args ["web"] = web+args args = argumentError args++web = do+ res <- conf+ case res of Left map -> err (misconfigured map)+ Right (ctx, www) -> WWW.runSettings www (WWW.logStdout (wai ctx))+ where+ misconfigured map = mappend "!!! Misconfigured for web; please check:\n"+ (render map)++argumentError args = (err . Bytes.unlines)+ ("Not able to interpret these command line arguments:" : (fmt <$> args))+ where+ fmt = (mappend " ") . Bytes.pack++msg s = Bytes.hPutStrLn stderr s+err s = msg s >> exitFailure +
+ Aws/SSSP/Configuration.hs view
@@ -0,0 +1,147 @@+{-# LANGUAGE OverloadedStrings+ , TupleSections #-}+-- | Utilities for determining the server configuration from environment+-- variables and file input.+module Aws.SSSP.Configuration where++import Control.Applicative+import Control.Exception+import Control.Monad+import Data.ByteString.Char8 (ByteString)+import qualified Data.ByteString.Char8 as Bytes+import Data.Map (Map)+import qualified Data.Map as Map+import Data.Maybe+import Data.Monoid+import System.Environment+import System.IO+import System.IO.Error++import qualified Aws as Aws+import qualified Aws.S3 as Aws+import Data.Attoparsec.Char8+import qualified Data.Conduit.Network as Conduit+import Data.Default+import qualified Data.Text.Encoding as Text+import qualified Data.Text.Encoding.Error as Text+import qualified Network.HTTP.Conduit as Conduit+import qualified Network.Wai.Handler.Warp as WWW++import Aws.SSSP (Ctx(..))+++variables = [ "AWS_ACCESS_KEY", "AWS_SECRET_KEY",+ "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY",+ "AWS_REGION", "SSSP_BUCKET", "SSSP_CONN", "PORT" ]++fromEnv :: IO (Map ByteString ByteString)+fromEnv = Map.fromList . catMaybes <$> mapM paired variables+ where+ paired k = ((k,) <$>) <$> maybeGetEnv k++maybeGetEnv :: ByteString -> IO (Maybe ByteString)+maybeGetEnv k = catchJust ((>> Just ()) . guard . isDoesNotExistError)+ (Just . Bytes.pack <$> (getEnv . Bytes.unpack $ k))+ (const (return Nothing))++fromBytes :: ByteString -> Map ByteString ByteString+fromBytes bytes = Map.fromList+ [ (k, v) | Right (k, v) <- parseOnly line <$> Bytes.lines bytes ]++-- | Recognizes a parseable @k = v@ or @k: v@ style line. It's relatively+-- flexible on input but rejects lines that might have shell interpolations+-- in them -- lines containing one of @$`{}@ -- as well as lines with shell+-- quotes (@'\"@). This allows the file input parser to skip over such values+-- when a raw rc file is loaded.+line :: Parser (ByteString, ByteString)+line = optional (string "export") *> skipSpace *> do+ (,) <$> choice (string <$> variables) <*> (copula *> chopped)+ where+ copula = skipWhile (==' ') *> (char '=' <|> char ':') <* skipWhile (==' ')+ chopped = simple =<< fst . Bytes.spanEnd isSpace+ <$> takeWhile1 (notInClass "\n\r")+ simple s = guard (and [ Bytes.notElem c s | c <- "${}`\"'" ]) >> return s++fromEnvAndSTDIN = do+ env <- fromEnv+ prune <$> do go <- checkForInput+ if go then (`Map.union` env) . fromBytes <$> Bytes.getContents+ else return env+ where+ checkForInput = catchJust ((>> Just ()) . guard . isEOFError)+ (hReady stdin)+ (const (return False))+++conf :: IO (Either (Map ByteString ByteString) (Ctx, WWW.Settings))+conf = do+ map <- fromEnvAndSTDIN+ ctx' <- createCtx map+ return $ maybe (Left map) Right ((,) <$> ctx' <*> createSettings map)++createSettings :: Map ByteString ByteString -> Maybe WWW.Settings+createSettings map = do+ (host,port) <- (splitConn =<< read "SSSP_CONN") <|>+ ("127.0.0.1",) <$> (read "PORT" <|> Just "8000")+ host <- parseHost host+ port <- parsePort port+ Just WWW.defaultSettings{WWW.settingsPort=port,WWW.settingsHost=host}+ where+ read k = Map.lookup k map+ splitConn bs = case Bytes.split ':' bs of [host,port] -> Just (host, port)+ _ -> Nothing+ parsePort :: ByteString -> Maybe Int+ parsePort = (fst <$>) . listToMaybe . reads . Bytes.unpack+ parseHost :: ByteString -> Maybe Conduit.HostPreference+ parseHost "*" = Just Conduit.HostAny+ parseHost "*4" = Just Conduit.HostIPv4+ parseHost "*6" = Just Conduit.HostIPv6+ parseHost bs = Just (Conduit.Host (Bytes.unpack bs))++createCtx :: Map ByteString ByteString -> IO (Maybe Ctx)+createCtx map = do+ manager <- Conduit.newManager def+ return $ do+ aws <- aws <$> (read "AWS_ACCESS_KEY" <|> read "AWS_ACCESS_KEY_ID")+ <*> (read "AWS_SECRET_KEY" <|> read "AWS_SECRET_ACCESS_KEY")+ s3 <- s3Configured <|> Just defS3+ bucket <- utf8 <$> read "SSSP_BUCKET"+ Just Ctx{bucket=bucket, aws=aws, s3=s3{Aws.s3UseUri=True}, manager=manager}+ where+ read k = Map.lookup k map+ aws id key = Aws.Configuration { Aws.timeInfo = Aws.Timestamp+ , Aws.credentials = Aws.Credentials id key+ , Aws.logger = Aws.defaultLog Aws.Warning }+ s3Configured :: Maybe (Aws.S3Configuration Aws.NormalQuery)+ s3Configured = do region <- read "AWS_REGION"+ url <- endpoint region+ Just defS3{Aws.s3Endpoint=url}+ utf8 = Text.decodeUtf8With Text.lenientDecode+ defS3 = Aws.defServiceConfig :: Aws.S3Configuration Aws.NormalQuery++validate :: ByteString -> ByteString -> Maybe ByteString+validate "AWS_REGION" r = endpoint r+validate _ s = guard (s /= "") >> Just s++prune :: Map ByteString ByteString -> Map ByteString ByteString+prune = Map.mapMaybeWithKey validate++-- | Interpret a region name, like @us-west-1@, in accord with the Amazon's+-- documentation for endpoint lcoations.+-- <http://docs.amazonwebservices.com/general/latest/gr/rande.html>+endpoint :: ByteString -> Maybe ByteString+endpoint "classic" = Just "s3.amazonaws.com"+endpoint "us-east-1" = Just "s3.amazonaws.com"+endpoint "us-west-2" = Just "s3-us-west-2.amazonaws.com"+endpoint "us-west-1" = Just "s3-us-west-1.amazonaws.com"+endpoint "eu-west-1" = Just "s3-eu-west-1.amazonaws.com"+endpoint "ap-southeast-1" = Just "s3-ap-southeast-1.amazonaws.com"+endpoint "ap-northeast-1" = Just "s3-ap-northeast-1.amazonaws.com"+endpoint "sa-east-1" = Just "s3-sa-east-1.amazonaws.com"+endpoint _ = Nothing++render :: Map ByteString ByteString -> ByteString+render = Bytes.unlines . (kv <$>) . Map.toAscList+ where+ kv (k, v) = mconcat [k, ": ", v]+
+ LICENSE view
@@ -0,0 +1,28 @@++ ©2012 Airbnb, Inc.++ 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.++ . Names of the contributors to this software may not be used to endorse or+ promote products derived from this software without specific prior written+ permission.++ This software is provided by the 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 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 view
@@ -0,0 +1,133 @@+SYNOPSIS+ sssp web?++DESCRIPTION+ SSSP is an HTTP proxy for S3 that can generate short-lived, signed URLs+ for stored objects. By providing a server separate from S3 that can be+ placed behind an authenticating proxy or firewall, SSSP allows a vari-+ ety of common security mechanisms to be used to limit access to S3+ objects over HTTP while taking advantage of S3's considerable bandwidth+ and parallelism.++ Use-cases for SSSP include:++ o sharing of large files within an organization,++ o media service for public facing web applications,++ o distribution of internal software.++ SSSP supports configuration via environment variables or STDIN.++CONFIGURATION+ These settings can be passed as environment variables or fed to the+ server on STDIN in colon separated format. Both the new and old forms+ of the AWS credential environment variables are supported.++ # AWS Settings+ AWS_ACCESS_KEY = account access key+ AWS_ACCESS_KEY_ID = account access key+ AWS_SECRET_KEY = secret+ AWS_SECRET_ACCESS_KEY = secret+ AWS_REGION = eu-west-1, classic, us-east-1, ...++ # Storage settings+ SSSP_BUCKET = DNS friendly bucket name++ # Server settings+ SSSP_CONN = <ip>:<port> pair+ PORT = port to connect to, on localhost++ SSSP is fairly liberal when parsing STDIN. In fact, Bourne shell .rc+ files, like the follow example, are parsed without error:++ export SSSP_BUCKET=dist+ export SSSP_CONN=*:6000++ However, SSSP skips over lines that contain quotes ("') or that appear+ to require shell interpolation for their correct interpolation (lines+ containing $`{}).++REST INTERFACE+ URLs in SSSP point to one of two objects: an item or a listing. Items+ correspond to S3 objects; a GET retrieves a signed redirect to the+ object. Listings are a sequence of URLs, in ascending order; a GET+ retrieves the listing as a plaintext document, one URL per line.++ GET http://sssp.io/p/a/t/h # Signed for the default time (10s).++ A PUT to an item sets the item's content. DELETEs can be singular or+ plural. A plural DELETE removes only the objects generated by a list-+ ing.++ URLs are divided syntactically in to listings and items. A URL ending+ with a slash is always a listing.++ GET http://sssp.io/dist # Signed redirect to an object called dist.+ GET http://sssp.io/dist/ # Listing of items below the key `dist'.++ To make it easier to work with versioned or timestamped assets, SSSP+ supports the @hi and @lo meta-paths. These correspond to the names that+ sort highest and lowest according to semantic version sort, where+ non-digit chars serve to delimit arrays of numbers. For common forms of+ dates, these have the same effect as ASCII sort. (ASCII sort may speci-+ fied, as well; please the section WILDCARDS, below.)++ GET http://sssp.io/dist/x/x-0.1.1.tgz+ GET http://sssp.io/dist/x/x-0.1.4.tgz+ GET http://sssp.io/dist/x/x-0.2.11.tgz+ GET http://sssp.io/dist/x/x-0.2.9.tgz++ # Retrieval with @hi and @lo.+ GET http://sssp.io/dist/x/@hi -307-> http://sssp.io/dist/x/x-0.2.11.tgz+ GET http://sssp.io/dist/x/@lo -307-> http://sssp.io/dist/x/x-0.1.1.tgz++ Wildcards @hi and @lo used together with a count specify a set wild-+ card; the result is a listing:++ GET http://sssp.io/dist/x/@lo2 -200-> dist/x/x-0.1.1.tgz+ dist/x/x-0.1.4.tgz++ Counts are the natural numbers starting at 0. The wildcard @* refers to+ "all the items".++ A counted wildcard, like @hi2, can be suffixed with a tilde to form its+ complement -- so @hi2~ is everything but the highest two items. This+ can be useful for bulk deletion of old/new things.++WILDCARDS+ @hi.semver, @lo.semver+ Key with highest or lowest version, according to a liberal-+ ized form of "semantic versioning", where version components+ are delimited by any non-digit characters.++ @hi.ascii, @lo.ascii+ Keys sorted ASCIIbetically, in the C locale (sorted purely by+ byte value).++ @hi, @lo+ The default sort, which is semantic version sort.++ @*, @*.semver, @*.ascii+ All the items, in the default order (semantic version) or in+ a specified order.++ ASCII sort can be substantially more performant than semantic version+ sort, because S3 returns data in ASCII order and thus no real sorting+ is necessary.++EXAMPLES+ # Start web application.+ sssp < conf++ # Start web application with configuration provided by the environment.+ export AWS_ACCESS_KEY_ID=...+ export AWS_SECRET_ACCESS_KEY=...+ sssp <<CONF+ SSSP_BUCKET: dist+ CONF++BUGS+ Listing results should really be URLs. The time to sign should really+ be configurable; or at least settable with a query parameter.+
+ Setup.hs view
@@ -0,0 +1,4 @@+import Distribution.Simple++main = defaultMain+
+ sssp.cabal view
@@ -0,0 +1,93 @@+name : sssp+version : 1.0.0+category : Text+license : BSD3+license-file : LICENSE+author : Jason Dusek+maintainer : oss@solidsnack.be+homepage : http://github.com/erudify/sssp/+synopsis : HTTP proxy for S3.+description :+ An HTTP proxy for S3 that generates signed URLs for GETs and PUTs and+ proxies DELETEs. A very limited form of range queries, using semantic+ version sort and ASCII set, are supported.++cabal-version : >= 1.10+build-type : Simple+extra-source-files : README+ , LICENSE++source-repository head+ type : git+ location : http://github.com/airbnb/sssp.git+++flag no-cli+ description : Disable command line tool.+ default : False+++library+ default-language : Haskell98+ build-depends : base >= 2 && <= 5+ , bytestring >= 0.9+ , containers+ , attempt >= 0.4+ , attoparsec >= 0.10+ , aws >= 0.7 && < 0.8+ , blaze-builder >= 0.3+ , case-insensitive >= 0.4+ , conduit >= 0.5+ , data-default >= 0.4+ , http-conduit >= 1.5+ , http-types >= 0.6+ , mtl >= 2+ , network-conduit >= 0.5+ , text >= 0.11+ , wai >= 1.3+ , wai-extra >= 1.3+ , warp >= 1.3++ exposed-modules : Aws.SSSP+ Aws.SSSP.App+ Aws.SSSP.Configuration+ default-extensions : OverloadedStrings+ ParallelListComp+ PatternGuards+ RecordWildCards+ StandaloneDeriving+ TupleSections+++executable sssp+ default-language : Haskell98+ main-is : sssp.hs+ if flag(no-cli)+ buildable : False+ else+ buildable : True++ build-depends : base >= 2 && <= 5+ , bytestring >= 0.9+ , containers+ , attempt >= 0.4+ , attoparsec >= 0.10+ , aws >= 0.7 && < 0.8+ , blaze-builder >= 0.3+ , case-insensitive >= 0.4+ , conduit >= 0.5+ , data-default >= 0.4+ , http-conduit >= 1.5+ , http-types >= 0.6+ , mtl >= 2+ , network-conduit >= 0.5+ , text >= 0.11+ , wai >= 1.3+ , wai-extra >= 1.3+ , warp >= 1.3+ default-extensions : OverloadedStrings+ ParallelListComp+ PatternGuards+ RecordWildCards+ StandaloneDeriving+ TupleSections
+ sssp.hs view
@@ -0,0 +1,7 @@+#!/usr/bin/env runhaskell+{-# LANGUAGE OverloadedStrings+ #-}+import Aws.SSSP.App++main = web+