packages feed

salvia-0.0.4: src/Network/Protocol/Uri.hs

module Network.Protocol.Uri (

    URI (..)
  , Scheme
  , IPv4
  , Label
  , Domain
  , RegName
  , Port
  , Query
  , Fragment
  , Hash
  , UserInfo
  , PathSegment
  , Parameters
  , Path (..)
  , Host (..)
  , Authority (..)

  , encode
  , decode

  , mkURI
  , mkScheme
  , mkPath
  , mkAuthority
  , mkQuery
  , mkFragment
  , mkUserinfo
  , mkHost
  , mkPort

  , scheme
  , authority
  , path
  , query
  , fragment
  , userinfo
  , host
  , port
  , domains
  , pathSegments

  , setScheme
  , setAuthority
  , setPath
  , setQuery
  , setFragment
  , setUserinfo
  , setHost
  , setPort

  , modScheme
  , modAuthority
  , modPath
  , modQuery
  , modUserinfo
  , modHost
  , modPort

  , parseURI
  , parseAbsoluteURI
  , parseAuthority
  , parsePath
  , parseHost

  , pUriReference
  , pAbsoluteURI
  , pAuthority
  , pPath
  , pHost

  , parseQueryParams
  , queryParams

  , extension
  , setExtension
  , modExtension

  , relative
  , mimetype
  , normalize
  , jail
  , (/+)

  ) where

import Control.Applicative
import Data.Bits
import Data.Char (ord, chr, isDigit, isAlphaNum, intToDigit, isHexDigit)
import Data.List (intercalate, isPrefixOf)
import Data.Maybe (mapMaybe, fromJust)
import Text.ParserCombinators.Parsec hiding (many, optional, (<|>))
import System.FilePath.Posix

import Network.Protocol.Mime
import Misc.Misc

-------[ data type definition of URIs ]----------------------------------------

type Scheme      = String
type IPv4        = [Int] -- actually 4-tupel
type Label       = String
type Domain      = [Label]
type RegName     = String
type Port        = Int
type Query       = String
type Fragment    = String
type Hash        = String
type UserInfo    = String
type PathSegment = String
type Parameters  = [(String, Maybe String)]

data Path = Path {
    _absolute :: Bool
  , _segments :: [PathSegment]
  }
  deriving Eq

data Host =
    Hostname { _domain  :: Domain }
  | RegName  { _regname :: String }
  | IPv4     { _ipv4    :: IPv4   }
  deriving Eq

data Authority = Authority {
    _userinfo :: UserInfo
  , _host     :: Host
  , _port     :: Port
  }
  deriving Eq

data URI = URI {
    _relative  :: Bool
  , _scheme    :: Scheme
  , _authority :: Authority
  , _path      :: Path
  , _query     :: Query
  , _fragment  :: Fragment
  }
  deriving Eq

{-
testUri0 :: URI
testUri0 = fromJust $ parseURI "http://sebas@hs.spugium.net:8080/wiki/first%20section/chapter-2;pdf?additional=vars"

testUri1 :: URI
testUri1 = fromJust $ parseURI "http://cs.uu.nl/docs/../docs/vakken/../vakken/"
-}

-------[ creating, selection and modifying URIs ]------------------------------

-- pseudo constructors for `empty values'

mkURI :: URI
mkURI = URI False mkScheme mkAuthority mkPath mkQuery mkFragment

mkScheme :: Scheme
mkScheme = ""

mkPath :: Path
mkPath = Path False []

mkAuthority :: Authority
mkAuthority = Authority "" mkHost mkPort

mkQuery :: Query
mkQuery = ""

mkFragment :: Fragment
mkFragment = ""

mkUserinfo :: UserInfo
mkUserinfo = ""

mkHost :: Host
mkHost = Hostname []

mkPort :: Port
mkPort = (-1)

-- getter functions
scheme :: URI -> Scheme
scheme = _scheme

authority :: URI -> String
authority = show . _authority

path :: URI -> FilePath
path = decode . show . _path

query :: URI -> Query
query = decode . _query

fragment :: URI -> Fragment
fragment = _fragment

userinfo :: URI -> UserInfo
userinfo = _userinfo . _authority

host :: URI -> String
host = show . _host . _authority

port :: URI -> Port
port = _port . _authority

domains :: URI -> Domain
domains = _domain . _host . _authority

pathSegments :: URI -> [PathSegment]
pathSegments = _segments . _path 

-- setter functions
setScheme :: Scheme -> URI -> URI
setScheme s u = u { _scheme = s }

setAuthority :: String -> URI -> URI
setAuthority a u = u { _authority = fromJust $ parseAuthority a }

setPath :: String -> URI -> URI
setPath p u = u { _path = fromJust $ parsePath p }

setQuery :: Query -> URI -> URI
setQuery q u = u { _query = q }

setFragment :: Fragment -> URI -> URI
setFragment q u = u { _fragment  = q }

setUserinfo :: UserInfo -> URI -> URI
setUserinfo i u = u { _authority = (_authority u) { _userinfo = i }}

setHost :: String -> URI -> URI
setHost h u = u { _authority = (_authority u) { _host = fromJust $ parseHost h }}

setPort :: Port -> URI -> URI
setPort p u = u { _authority = (_authority u) { _port = p }}

-- modifier functions
modScheme :: (Scheme -> Scheme) -> URI -> URI
modScheme f u = setScheme (f $ scheme u) u

modAuthority :: (String -> String) -> URI -> URI
modAuthority f u = setAuthority (f $ authority u) u

modPath :: (String -> String) -> URI -> URI
modPath f u = setPath (f $ encode $ path u) u

modQuery :: (Query -> Query) -> URI -> URI
modQuery f u = setQuery (f $ query u) u

modUserinfo :: (UserInfo -> UserInfo) -> URI -> URI
modUserinfo f u = setUserinfo  (f $ userinfo u) u

modHost :: (String -> String) -> URI -> URI
modHost f u = setHost (f $ host u) u

modPort :: (Port -> Port) -> URI -> URI
modPort f u = setPort (f $ port u) u

-------[ path encoding and decoding ]------------------------------------------

encode :: String -> String
encode = concatMap encodeChr
  where
    encodeChr c
      | unreserved c || genDelims c || subDelims c = [c]
      | otherwise = '%' :
          intToDigit (shiftR (ord c) 4) :
          intToDigit ((ord c) .&. 0x0F) : []

decode :: String -> String
decode [] = []
decode ('%':d:e:ds) | isHexDigit d && isHexDigit e = f d e : decode ds
  where f a b = chr $ (ord a-ord '0') * 16 + (ord b - ord '0')
decode (d:ds) = d : decode ds

-------[ show instance for URIs ]----------------------------------------------

-- TODO: cleanup and test this mess: QuickCheck property for testing:
-- parse $ show u == u /\ show $ parse u' = u'

-- TODO: ShowS

instance Show Path where
  show (Path a s) =
       (if a then "/" else "")
    ++ (intercalate "/" s)

instance Show Host where
  show (Hostname d) = if null d then "" else intercalate "." d
  show (IPv4     a) = intercalate "." $ map show a
  show (RegName  r) = r

instance Show Authority where
  show (Authority u h p) =
       (if hst h then "//" else "")
    ++ (if null u then "" else u ++ "@")
    ++  show h
    ++ (if p == -1 then "" else ":" ++ show p)
    where hst (Hostname d) = not $ null d
          hst (RegName  d) = not $ null d
          hst _            = True

instance Show URI where
  show (URI r s a p q f) =
       (if not r then (if null s then "" else s ++ ":") else "")
    ++ (show a)
    ++ (show p)
    ++ (if null q then "" else "?" ++ q)
    ++ (if null f then "" else "#" ++ f)

-------[ global URI parse interface ]------------------------------------------

-- Top level parsers.
parseURI :: String -> Maybe URI
parseURI = eitherToMaybe . parse pUriReference ""

parseAbsoluteURI :: String -> Maybe URI
parseAbsoluteURI = eitherToMaybe . parse pAbsoluteURI ""

parseAuthority :: String -> Maybe Authority
parseAuthority = eitherToMaybe . parse pAuthority ""

parsePath :: String -> Maybe Path
parsePath = eitherToMaybe . parse pPath ""

parseHost :: String -> Maybe Host
parseHost = eitherToMaybe . parse pHost ""

-------[ parsing URIs according to rfc2396 ]-----------------------------------

-- D.2.  Modifications
pAlpha, pDigit, pAlphanum :: GenParser Char st Char

pAlpha    = letter
pDigit    = digit
pAlphanum = alphaNum

-- 2.3.  Unreserved Characters
unreserved :: Char -> Bool
unreserved c = isAlphaNum c || elem c "-._~"

pUnreserved :: GenParser Char st Char
pUnreserved  = pAlphanum <|> oneOf "-._~"

-- 2.2.  Reserved Characters
genDelims :: Char -> Bool
genDelims = flip elem ":/?#[]@"

subDelims :: Char -> Bool
subDelims = flip elem "!$&'()*+,;="

{-
pReserved :: GenParser Char st Char
pReserved  = pGenDelims <|> pSubDelims
pGenDelims :: GenParser Char st Char
pGenDelims = oneOf ":/?#[]@"
-}

pSubDelims :: GenParser Char st Char
pSubDelims = oneOf "!$&'()*+,;="

-- 2.1.  Percent-Encoding
pPctEncoded :: GenParser Char st String
pPctEncoded = (:) <$> char '%' <*> pHex

pHex :: GenParser Char st String
pHex = (\a b -> a:b:[])
    <$> hexDigit
    <*> hexDigit

-- 3.  Syntax Components

-- With the hier-part integrated.
pUri :: GenParser Char st URI
pUri = (\a (b,c) d e -> URI False a b c d e)
  <$> (pScheme <* string ":")
  <*> (ap <|> p)
  <*> option "" (string "?" *> pQuery)
  <*> option "" (string "#" *> pFragment)
  where
    ap = (,) <$> (string "//" *> pAuthority) <*> pPathAbempty
    p  = ((,) mkAuthority) <$> (pPathAbsolute <|> pPathRootless {-<|> pPathEmpty-})

-- 3.1.  Scheme
pScheme :: GenParser Char st Scheme
pScheme = (:) <$> pAlpha <*> many (pAlphanum <|> oneOf "+_.")

-- 3.2.  Authority
pAuthority :: GenParser Char st Authority
pAuthority = Authority
  <$> option mkUserinfo (try (pUserinfo <* string "@"))
  <*> pHost
  <*> option mkPort (string ":" *> pPort)

-- 3.2.1.  User Information
pUserinfo :: GenParser Char st String
pUserinfo = concat <$> many (
      (pure <$> pUnreserved)
  <|> (         pPctEncoded)
  <|> (pure <$> pSubDelims)
  <|> (pure <$> oneOf ":")
  )

-- 3.2.2.  Host
pHost :: GenParser Char st Host
pHost = diff <$> pRegName -- <|> RegName <$> pRegName
  where
    diff  a = maybe (RegName a) sep (pHostname @@ a)
    sep   a = bool (Hostname a) (ipreg a) $ hst a
    ipreg a = bool (IPv4 $ map read a) (RegName $ intercalate "." a) $ ip a
    hst     = not . all isDigit . head . dropWhile null . reverse
    ip    a = length a == 4 && length (mapMaybe (pDecOctet @@) a) == 4

{-
pfff, ipv6 is sooo not gonna make it..

pIPLiteral = "[" ( IPv6address <|> IPvFuture  ) "]"

pIPvFuture = "v" 1*HEXDIG "." 1*( unreserved <|> subDelims <|> ":" )

pIPv6address =
                                 6( h16 ":" ) ls32
  <|>                       "::" 5( h16 ":" ) ls32
  <|> [               h16 ] "::" 4( h16 ":" ) ls32
  <|> [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32
  <|> [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32
  <|> [ *3( h16 ":" ) h16 ] "::"    h16 ":"   ls32
  <|> [ *4( h16 ":" ) h16 ] "::"              ls32
  <|> [ *5( h16 ":" ) h16 ] "::"              h16
  <|> [ *6( h16 ":" ) h16 ] "::"

pH16           = 1*4HEXDIG
pLs32          = ( h16 ":" h16 ) <|> IPv4address
-}

{-
pIPv4address :: GenParser Char st [Int]
pIPv4address = (:) <$> pDecOctet <*> (count 3 $ char '.' *> pDecOctet)
-}

pDecOctet :: GenParser Char st Int
pDecOctet = read <$> choice [
    try ((\a b c -> [a,b,c]) <$> char '2' <*> char '5'      <*> oneOf "012345")
  , try ((\a b c -> [a,b,c]) <$> char '2' <*> oneOf "01234" <*> digit)
  , try ((\a b c -> [a,b,c]) <$> char '1' <*> digit         <*> digit)
  , try ((\a b   -> [a,b])   <$>              digit         <*> digit)
  ,     (pure                <$>                                digit)
  ]

pRegName :: GenParser Char st Label
pRegName = concat <$> many1 (
      (pure <$> pUnreserved)
  <|>           pPctEncoded
  <|> (pure <$> pSubDelims))

-- Not actually part of the rfc3986, but comptability with the rfc2396.
-- This information can be useful, so why throw away.
pHostname :: GenParser Char st Domain
pHostname = sepBy (option "" pDomainlabel) (string ".")

pDomainlabel :: GenParser Char st Label
pDomainlabel = intercalate "-" <$> sepBy1 (some pAlphanum) (string "-")

-- 3.2.3.  Port
pPort :: GenParser Char st Port
pPort = read <$> some pDigit

-- 3.4.  Query
pQuery :: GenParser Char st String
pQuery = concat <$> many (pPchar <|> pure <$> oneOf "/?")

-- 3.5.  Fragment
pFragment :: GenParser Char st String
pFragment = concat <$> many (pPchar <|> pure <$> oneOf "/?" )

-- 3.3.  Path

pPath, pPathAbempty, pPathAbsolute, pPathNoscheme, pPathRootless, pPathEmpty
  :: GenParser Char st Path

pPath =
      try pPathAbsolute -- begins with "/" but not "//"
  <|> try pPathNoscheme -- begins with a nonColon segment
  <|> try pPathRootless -- begins with a segment
  <|> pPathEmpty        -- zero characters

pPathAbempty  = (Path True) <$> _pSlashSegments
pPathAbsolute = (char '/' *>) $ (Path True) <$> (option [] $ (:) <$> pSegmentNz <*> _pSlashSegments)
pPathNoscheme = (Path False) <$> ((:) <$> pSegmentNzNc <*> _pSlashSegments)
pPathRootless = (Path False) <$> ((:) <$> pSegmentNz    <*> _pSlashSegments)
pPathEmpty    = (Path False []) <$ string ""

pSegment, pSegmentNz, pSegmentNzNc :: GenParser Char st String
pSegment     = concat <$> many pPchar
pSegmentNz   = concat <$> some pPchar
pSegmentNzNc = concat <$> some (
      (pure <$> pUnreserved)
  <|>           pPctEncoded
  <|> (pure <$> pSubDelims)
  <|> (pure <$> oneOf "@" ))

_pSlashSegments :: GenParser Char st [PathSegment]
_pSlashSegments = (many $ (:) <$> char '/' *> pSegment)

pPchar :: GenParser Char st String
pPchar = choice [
    pure <$> pUnreserved
  , pPctEncoded
  , pure <$> pSubDelims
  , pure <$> oneOf ":@"
  ]

-- 4.1.  URI Reference

pUriReference :: GenParser Char st URI
pUriReference = try pAbsoluteURI <|> pRelativeRef

-- 4.2.  Relative Reference

-- With the relative-part integrated.
pRelativeRef :: GenParser Char st URI
pRelativeRef = ($)
  <$> (try pRelativePart
  <|> ((URI True mkScheme mkAuthority)
  <$> (pPathAbsolute <|> pPathRootless <|> pPathEmpty)))
  <*> option "" (string "?" *> pQuery)
  <*> option "" (string "#" *> pFragment)

pRelativePart :: GenParser Char st (Query -> Fragment -> URI)
pRelativePart = (URI True mkScheme) <$> (string "//" *> pAuthority) <*> pPathAbempty

-- 4.3.  Absolute URI

pAbsoluteURI :: GenParser Char st URI
pAbsoluteURI = pUri

-------[ parsing query parameters ]--------------------------------------------

-- Parse a pre-decoded query string into key value pairs parameters.

pQueryParams :: GenParser Char st Parameters
pQueryParams = 
      filter (not . null . fst)
  <$> sepBy ((,)
  <$> many (noneOf "=&")
  <*> pMaybe (char '='
   *> many (noneOf "&")))
      (char '&')

parseQueryParams :: String -> Maybe Parameters
parseQueryParams = eitherToMaybe . parse pQueryParams  ""

queryParams :: URI -> Parameters
queryParams = maybe [] id . parseQueryParams . query

-------[ filename path utilities ]---------------------------------------------

extension :: URI -> Maybe String
extension u = do
  lst <- safeLast . _segments . _path $ u
  guardMaybe (elem '.' lst)
  return $ withReverse (takeWhile (/='.')) lst

setExtension :: Maybe String -> URI -> URI
setExtension e u =
  let ex = maybe "" ('.':) e in
  (\f -> modPath (\p -> f p ++ ex) u)
  $ maybe id (const $ withReverse (tail . dropWhile (/='.')))
    (extension u)

modExtension :: (String -> String) -> URI -> URI
modExtension f u = setExtension (f `fmap` extension u) u

relative :: URI -> URI
relative = modPath (dropWhile (=='/'))

-- Try to guess the 'correct' mime type for the input file based on the file
-- extension.

mimetype :: URI -> Maybe String
mimetype u = extension u >>= mime

-- Normalize a path by removing or merging all dot or dot-dot segments and
-- double slashes. Todo: cleanup, remove fixp.

normalize :: FilePath -> FilePath
normalize [] = []
normalize p = fixAbs absolute $ intercalate "/" $ norm
  where
    fixAbs True          = ('/':)
    fixAbs False         = id
    absolute             = head p == '/'
    norm                 = fixp False $ split '/' p
    fixp False xs        = let ys = merge xs in fixp (xs == ys) ys
    fixp True  xs        = xs
    merge ("":xs)        = merge xs
    merge (".":xs)       = merge xs
    merge ("..":"..":xs) = ".." : ".." : merge xs
    merge (_:"..":xs)    = merge xs
    merge (x:xs)         = x : merge xs
    merge xs             = xs

jail :: FilePath -> FilePath -> Maybe FilePath
jail jailDir p =
  let nj = normalize jailDir
      np = normalize p in
  if nj `isPrefixOf` np -- && not (".." `isPrefixOf` np)
    then Just np
    else Nothing

(/+) :: FilePath -> FilePath -> FilePath
a /+ b = normalize (a ++ "/" ++ b)