packages feed

http-date-0.0.13: Network/HTTP/Date/Parser.hs

{-# LANGUAGE OverloadedStrings #-}

module Network.HTTP.Date.Parser (parseHTTPDate) where

import Control.Applicative
import Control.Monad
import Data.Attoparsec.ByteString.Char8
import Data.ByteString (ByteString)
import Data.Char
import Data.Maybe (isJust)
import Network.HTTP.Date.Converter (httpDateToUTCValid)
import Network.HTTP.Date.Types

----------------------------------------------------------------

-- |
-- Parsing HTTP Date. The three formats of RFC 9110 are supported:
-- IMF-fixdate (RFC 1123), the obsolete RFC 850 format and asctime.
-- Dates which do not exist are rejected while the day of the week is
-- not checked against the date.
--
-- >>> :set -XOverloadedStrings
-- >>> parseHTTPDate "Tue, 15 Nov 1994 08:12:31 GMT"
-- Just (HTTPDate {hdYear = 1994, hdMonth = 11, hdDay = 15, hdHour = 8, hdMinute = 12, hdSecond = 31, hdWkday = 2})
-- >>> parseHTTPDate "Tuesday, 15-Nov-94 08:12:31 GMT"
-- Just (HTTPDate {hdYear = 1994, hdMonth = 11, hdDay = 15, hdHour = 8, hdMinute = 12, hdSecond = 31, hdWkday = 2})
-- >>> parseHTTPDate "Tue Nov 15 08:12:31 1994"
-- Just (HTTPDate {hdYear = 1994, hdMonth = 11, hdDay = 15, hdHour = 8, hdMinute = 12, hdSecond = 31, hdWkday = 2})
-- >>> parseHTTPDate "Tue, 31 Feb 1994 08:12:31 GMT"
-- Nothing
--
-- A two-digit year of the RFC 850 format is taken as 1970-2069.
parseHTTPDate :: ByteString -> Maybe HTTPDate
parseHTTPDate bs = case parseOnly httpDate bs of
    Right hd -> Just hd
    _ -> Nothing

httpDate :: Parser HTTPDate
httpDate = do
    -- Trailing OWS is not part of a field value (RFC 9110 Section 5.5)
    -- but some HTTP parsers leave it there.
    hd <- rfc1123Date <|> rfc850Date <|> asctimeDate
    skipWhile (\c -> c == ' ' || c == '\t')
    endOfInput
    guard $ isJust $ httpDateToUTCValid hd
    return hd

mkHTTPDate :: Int -> (Int, Int, Int) -> (Int, Int, Int) -> HTTPDate
mkHTTPDate w (y, m, d) (h, n, s) =
    defaultHTTPDate
        { hdYear = y
        , hdMonth = m
        , hdDay = d
        , hdHour = h
        , hdMinute = n
        , hdSecond = s
        , hdWkday = w
        }

-- Sun, 06 Nov 1994 08:49:37 GMT
rfc1123Date :: Parser HTTPDate
rfc1123Date = do
    w <- wkday
    void $ string ", "
    d <- digit2
    sp
    m <- month
    sp
    y <- digit4
    sp
    t <- time
    sp
    gmt
    return $ mkHTTPDate w (y, m, d) t

-- Sunday, 06-Nov-94 08:49:37 GMT
rfc850Date :: Parser HTTPDate
rfc850Date = do
    w <- weekday
    void $ string ", "
    d <- digit2
    void $ char '-'
    m <- month
    void $ char '-'
    yy <- digit2
    sp
    t <- time
    sp
    gmt
    let y = if yy < 70 then 2000 + yy else 1900 + yy
    return $ mkHTTPDate w (y, m, d) t

-- Sun Nov  6 08:49:37 1994
asctimeDate :: Parser HTTPDate
asctimeDate = do
    w <- wkday
    sp
    m <- month
    sp
    d <- digit2 <|> (sp *> digit1)
    sp
    t <- time
    sp
    y <- digit4
    return $ mkHTTPDate w (y, m, d) t

-- RFC 9110 defines GMT only but there are actually ill-formed ones such
-- as "+0000" and "UTC" in the wild.
gmt :: Parser ()
gmt = void $ string "GMT" <|> string "+0000" <|> string "UTC"

wkday :: Parser Int
wkday =
    1 <$ string "Mon"
        <|> 2 <$ string "Tue"
        <|> 3 <$ string "Wed"
        <|> 4 <$ string "Thu"
        <|> 5 <$ string "Fri"
        <|> 6 <$ string "Sat"
        <|> 7 <$ string "Sun"

weekday :: Parser Int
weekday =
    1 <$ string "Monday"
        <|> 2 <$ string "Tuesday"
        <|> 3 <$ string "Wednesday"
        <|> 4 <$ string "Thursday"
        <|> 5 <$ string "Friday"
        <|> 6 <$ string "Saturday"
        <|> 7 <$ string "Sunday"

sp :: Parser ()
sp = () <$ char ' '

time :: Parser (Int, Int, Int)
time = do
    h <- digit2
    void $ char ':'
    m <- digit2
    void $ char ':'
    s <- digit2
    return (h, m, s)

month :: Parser Int
month =
    1 <$ string "Jan"
        <|> 2 <$ string "Feb"
        <|> 3 <$ string "Mar"
        <|> 4 <$ string "Apr"
        <|> 5 <$ string "May"
        <|> 6 <$ string "Jun"
        <|> 7 <$ string "Jul"
        <|> 8 <$ string "Aug"
        <|> 9 <$ string "Sep"
        <|> 10 <$ string "Oct"
        <|> 11 <$ string "Nov"
        <|> 12 <$ string "Dec"

digit1 :: Parser Int
digit1 = toInt <$> digit

digit2 :: Parser Int
digit2 = do
    x1 <- digit1
    x2 <- digit1
    return $ x1 * 10 + x2

digit4 :: Parser Int
digit4 = do
    x1 <- digit2
    x2 <- digit2
    return $ x1 * 100 + x2

toInt :: Char -> Int
toInt c = ord c - ord '0'