1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178
|
{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_GHC -fno-warn-missing-signatures #-}
-- TBD: basic-auth, gzip
module HttpBin.Server (serve) where
import Control.Applicative ((<$>))
import Control.Monad.IO.Class (liftIO)
import Data.Aeson (Value(..), eitherDecode, object, toJSON)
import Data.Aeson.Encode.Pretty (Config(..), Indent(Spaces), defConfig, encodePretty')
import Data.Aeson.Key (fromText)
import Data.ByteString.Char8 (pack)
import Data.CaseInsensitive (original)
import Data.Maybe (catMaybes, fromMaybe)
import Data.Monoid ((<>))
import Data.Text.Encoding (decodeUtf8)
import Data.Text.Read (decimal)
import Data.Time.Clock (UTCTime(..))
import Data.UUID (toASCIIBytes)
import Data.UUID.V4 (nextRandom)
import Snap.Core
import Snap.Http.Server as Snap
import Snap.Util.GZip (withCompression)
import System.PosixCompat.Time (epochTime)
import qualified Data.ByteString.Base64 as B64
import qualified Data.ByteString.Char8 as B
import qualified Data.Map as Map
import qualified Data.Text.Lazy.Encoding as Lazy
get = respond return
post = respond $ \obj -> do
body <- readRequestBody 65536
return $ obj <> [("data", toJSON (Lazy.decodeUtf8 body))] <>
case eitherDecode body of
Left _ -> [("json", Null)]
Right val -> [("json", val)]
put = post
delete = respond return
status = do
val <- (fromMaybe 200 . rqIntParam "val") <$> getRequest
let code | val >= 200 && val <= 505 = val
| otherwise = 400
modifyResponse $ setResponseCode code
gzip =
localRequest (setHeader "Accept-Encoding" "gzip") . withCompression .
respond $ \obj -> return $ obj <> [("gzipped", Bool True)]
deleteCookies = do
req <- getRequest
let expire name = Cookie name "" (Just mcfly) Nothing (Just "/") False False
mcfly = UTCTime (read "1985-10-26") 4800
modifyResponse . foldr (.) id $ [
addResponseCookie (expire name) . deleteResponseCookie name
| name <- Map.keys (rqQueryParams req)
]
redirect "/cookies"
setCookies = do
params <- rqQueryParams <$> getRequest
modifyResponse . foldr (.) id . map addResponseCookie $
[Cookie k v Nothing Nothing (Just "/") False False
| (k,vs) <- Map.toList params, v <- vs]
redirect "/cookies"
listCookies = do
cks <- rqCookies <$> getRequest
let cs = [(fromText(decodeUtf8 (cookieName c)),
toJSON (decodeUtf8 (cookieValue c))) | c <- cks]
respond $ \obj -> return $ obj <> [("cookies", object cs)]
redirect_ = do
req <- getRequest
let n = fromMaybe (-1::Int) . rqIntParam "n" $ req
prefix = B.reverse . B.dropWhile (/='/') . B.reverse . rqURI $ req
case undefined of
_| n > 1 -> redirect $ prefix <> pack (show (n-1))
| n == 1 -> redirect "/get"
| otherwise -> modifyResponse $ setResponseCode 400
unauthorized = modifyResponse $
setHeader "WWW-Authenticate" "Basic realm=\"Fake Realm\"" .
setResponseCode 401
simpleAuth expect = do
req <- getRequest
case expect req of
Nothing -> modifyResponse $ setResponseCode 400
Just (expected, resp) ->
case getHeader "Authorization" (headers req) of
Nothing -> unauthorized
Just auth | auth == expected -> writeJSON $
resp <> [("authenticated", Bool True)]
| otherwise -> unauthorized
basicAuth = simpleAuth $ \req ->
case (rqParam "user" req, rqParam "pass" req) of
(Just [user], Just [passwd]) | not (':' `B.elem` user) ->
Just ("Basic " <> B64.encode (user <> ":" <> passwd),
[("user", toJSON (B.unpack user))])
_ -> Nothing
oauth2token = simpleAuth $ \req ->
case (rqParam "kind" req, rqParam "token" req) of
(Just [kind], Just [token]) ->
Just (kind <> " " <> token,
[("token", toJSON (B.unpack token))])
_ -> Nothing
cache = do
hdrs <- headers <$> getRequest
let cond = not . null . catMaybes . map (flip getHeader hdrs) $
["If-Modified-Since", "If-None-Match"]
if cond
then modifyResponse $ setResponseCode 304
else do
now <- liftIO $ formatHttpTime =<< epochTime
uuid <- liftIO nextRandom
modifyResponse $ setHeader "Last-Modified" now .
setHeader "ETag" (toASCIIBytes uuid)
respond return
rqIntParam name req =
case rqParam name req of
Just (str:_) -> case decimal (decodeUtf8 str) of
Right (n, "") -> Just n
_ -> Nothing
_ -> Nothing
writeJSON obj = do
modifyResponse $ setContentType "application/json"
writeLBS . (<> "\n") . encodePretty' defConfig { confIndent = Spaces 2, confCompare = compare } . object $ obj
respond act = do
req <- getRequest
let step m k v = Map.insert (decodeUtf8 k) (decodeUtf8 (head v)) m
params = Map.foldlWithKey' step Map.empty .
rqQueryParams $ req
wibble (k,v) = (decodeUtf8 (original k), decodeUtf8 v)
rqHdrs = headers req
hdrs = Map.fromList . map wibble . listHeaders $ rqHdrs
url = case getHeader "Host" rqHdrs of
Nothing -> []
Just host -> [("url", toJSON . decodeUtf8 $
"http://" <> host <> rqURI req)]
writeJSON =<< act ([ ("args", toJSON params)
, ("headers", toJSON hdrs)
, ("origin", toJSON . decodeUtf8 . rqClientAddr $ req)
] <> url)
meths ms h = methods ms (path "" h)
meth m h = method m (path "" h)
serve mkConfig = do
cfg <- mkConfig
. setAccessLog ConfigNoLog
. setErrorLog ConfigNoLog
$ defaultConfig
httpServe cfg $ route [
("/get", meths [GET,HEAD] get)
, ("/post", meth POST post)
, ("/put", meth PUT put)
, ("/delete", meth DELETE delete)
, ("/redirect/:n", redirect_)
, ("/status/:val", status)
, ("/gzip", meths [GET,HEAD] gzip)
, ("/cookies/delete", meths [GET,HEAD] deleteCookies)
, ("/cookies/set", meths [GET,HEAD] setCookies)
, ("/cookies", meths [GET,HEAD] listCookies)
, ("/basic-auth/:user/:pass", meths [GET,HEAD] basicAuth)
, ("/oauth2/:kind/:token", meths [GET,HEAD] oauth2token)
, ("/cache", meths [GET,HEAD] cache)
]
|