hedis-0.16.0: src/Database/Redis/URL.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE ViewPatterns #-}
module Database.Redis.URL
( parseConnectInfo
) where
#if __GLASGOW_HASKELL__ < 710
import Control.Applicative ((<$>))
#endif
import qualified Data.ByteString.Char8 as C8
import Control.Error.Util (note)
#if __GLASGOW_HASKELL__ < 808
import Data.Monoid ((<>))
#endif
import Data.String (fromString)
import Database.Redis.Connection (ConnectInfo(..), defaultConnectInfo)
import qualified Database.Redis.ConnectionContext as CC
import Network.HTTP.Base
import Network.URI (parseURI, uriPath, uriScheme, uriQuery, URI)
import Network.TLS (defaultParamsClient)
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import Network.HTTP.Types (parseSimpleQuery)
import Text.Read (readMaybe)
-- | Parse a @'ConnectInfo'@ from a URL according to the Rules in Redis client
--
-- Standalone Redis:
--
-- @
-- redis :// [[username :] password@] host [:port][/database]
-- @
--
-- >>> parseConnectInfo "redis://username:password@host:42/2"
-- Right (ConnInfo {connectAddr = ConnectAddrHostPort "host" 42, connectAuth = Just "password", connectUsername = Just "username", connectDatabase = 2, connectMaxConnections = 50, connectNumStripes = Just 1, connectMaxIdleTime = 30s, connectTimeout = Nothing, connectTLSParams = Nothing, connectHooks = Hooks {...}, connectPoolLabel = ""})
--
-- >>> parseConnectInfo "redis://password@host:42/2"
-- Right (ConnInfo {connectAddr = ConnectAddrHostPort "host" 42, connectAuth = Just "password", connectUsername = Nothing, connectDatabase = 2, connectMaxConnections = 50, connectNumStripes = Just 1, connectMaxIdleTime = 30s, connectTimeout = Nothing, connectTLSParams = Nothing, connectHooks = Hooks {...}, connectPoolLabel = ""})
--
-- TLS-enabled Redis:
--
-- @
-- rediss :// [[username :] password@] host [: port][/database]
-- @
--
-- Unix socket Redis:
--
-- @
-- redis-socket :// [[username :] password@]path [? [&database=database]
-- @
--
-- >>> parseConnectInfo "redis-socket://password@/tmp/redis.sock?database=2"
-- Right (ConnInfo {connectAddr = ConnectAddrUnixSocket "/tmp/redis.sock", connectAuth = Just "password", connectUsername = Nothing, connectDatabase = 2, connectMaxConnections = 50, connectNumStripes = Just 1, connectMaxIdleTime = 30s, connectTimeout = Nothing, connectTLSParams = Nothing, connectHooks = Hooks {...}, connectPoolLabel = ""})
--
-- >>> parseConnectInfo "redis://username:password@host:42/db"
-- Left "Invalid port: db"
--
-- The scheme is validated, to prevent mixing up configurations:
--
-- >>> parseConnectInfo "postgres://"
-- Left "Wrong scheme postgres:"
--
-- Beyond that, all values are optional. Omitted values are taken from
-- @'defaultConnectInfo'@:
--
-- >>> parseConnectInfo "rediss://"
-- Right (ConnInfo {connectAddr = ConnectAddrHostPort "localhost" 6379, connectAuth = Nothing, connectUsername = Nothing, connectDatabase = 0, connectMaxConnections = 50, connectNumStripes = Just 1, connectMaxIdleTime = 30s, connectTimeout = Nothing, connectTLSParams = Just (ClientParams ...), connectHooks = Hooks {...}, connectPoolLabel = ""})
--
parseConnectInfo :: String -> Either String ConnectInfo
parseConnectInfo url = do
uri <- note "Invalid URI" $ parseURI url
let userScheme = uriScheme uri
case userScheme of
"redis:" -> parseSocket False uri
"rediss:" -> parseSocket True uri
"redis-socket:" -> parseUnix uri
x -> Left ("Wrong scheme " ++ x)
where
parseSocket :: Bool -> URI -> Either String ConnectInfo
parseSocket isSecure uri = do
uriAuth <- note "Missing or invalid Authority"
$ parseURIAuthority
$ uriToAuthorityString uri
let h = host uriAuth
dbNumPart = dropWhile (== '/') (uriPath uri)
db <- if null dbNumPart
then return $ connectDatabase defaultConnectInfo
else note ("Invalid port: " <> dbNumPart) $ readMaybe dbNumPart
let finalHost = if null h
then case connectAddr defaultConnectInfo of
CC.ConnectAddrHostPort defaultHost _ -> defaultHost
CC.ConnectAddrUnixSocket _ -> "localhost"
else h
let (finalUser, finalAuth) = case (T.pack <$> user uriAuth, T.pack <$> password uriAuth) of
(p, Nothing) -> (Nothing, p)
(p, fmap T.strip -> Just "") -> (Nothing, p)
(u, p) -> (u, p)
return defaultConnectInfo
{ connectAddr =
CC.ConnectAddrHostPort
finalHost
(maybe defaultPort fromIntegral (port uriAuth))
, connectAuth = T.encodeUtf8 <$> finalAuth
, connectUsername = T.encodeUtf8 <$> finalUser
, connectDatabase = db
, connectTLSParams = case isSecure of
False -> Nothing
True -> Just $ defaultParamsClient finalHost ""
}
where
defaultPort = case connectAddr defaultConnectInfo of
CC.ConnectAddrHostPort _ portNum -> portNum
CC.ConnectAddrUnixSocket _ -> 6379
parseUnix :: URI -> Either String ConnectInfo
parseUnix uri = do
auth <- note "Missing or invalid Authority"
$ parseURIAuthority
$ uriToAuthorityString uri
db <- case lookup "database" query of
Nothing -> return $ connectDatabase defaultConnectInfo
Just dbNumPart ->
note "Invalid database" $ readMaybe @Integer . T.unpack $ T.decodeUtf8 dbNumPart
return defaultConnectInfo
{ connectAddr = CC.ConnectAddrUnixSocket (mkPath auth)
, connectAuth = C8.pack <$> (user auth)
, connectDatabase = (db :: Integer)
}
where
mkPath auth =
case host auth <> uriPath uri of
('/':_) -> host auth <> uriPath uri
_ -> '/' : host auth <> uriPath uri
query = parseSimpleQuery (T.encodeUtf8 $ fromString $ uriQuery uri)