diff --git a/CHANGELOG.md b/CHANGELOG.md index e216039..2be43b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,31 @@ ## 0.12.6 [unreleased] * Remove `array` dependency +* Add parsing and rendering functions for `Status` +* Add pattern synonyms for `HttpVersion` + * `Http09` + * `Http10` + * `Http11` + * `Http20` + * `Http30` +* Add pattern synonyms for `Status` + * all of format `StatusXXX` +* Add more constant headers: + * `hAcceptPatch` + * `hAccessControlAllowCredentials` + * `hAccessControlAllowHeaders` + * `hAccessControlAllowMethods` + * `hAccessControlAllowOrigin` + * `hAccessControlExposeHeaders` + * `hAccessControlMaxAge` + * `hAccessControlRequestMethod` + * `hAltSvc` + * `hContentDigest` + * `hContentSecurityPolicy` + * `hContentSecurityPolicyReportOnly` + * `hForwarded` + * `hLink` + * `hStrictTransportSecurity` ## 0.12.5 [2026-05-31] diff --git a/Network/HTTP/Types.hs b/Network/HTTP/Types.hs index e474b0d..a3bc1b5 100644 --- a/Network/HTTP/Types.hs +++ b/Network/HTTP/Types.hs @@ -2,7 +2,6 @@ module Network.HTTP.Types ( -- * Methods -- | __For more information__: "Network.HTTP.Types.Method" - Method, -- ** Constants @@ -25,7 +24,6 @@ module Network.HTTP.Types ( -- * Versions -- | __For more information__: "Network.HTTP.Types.Version" - HttpVersion (..), http09, http10, @@ -36,11 +34,20 @@ module Network.HTTP.Types ( -- * Status -- | __For more information__: "Network.HTTP.Types.Status" - Status (..), + mkStatus, + + -- ** Parsing and Rendering + parseStatusCode, + renderStatusCode, + parseFullStatus, + renderFullStatus, + + -- ** Low level functions + renderStatusCodeToPtr, + renderFullStatusToPtr, -- ** Constants - mkStatus, status100, continue100, status101, @@ -139,6 +146,8 @@ module Network.HTTP.Types ( httpVersionNotSupported505, status511, networkAuthenticationRequired511, + + -- *** Category checks statusIsInformational, statusIsSuccessful, statusIsRedirection, @@ -155,17 +164,27 @@ module Network.HTTP.Types ( RequestHeaders, ResponseHeaders, - -- ** Header constants + -- ** Constants hAccept, hAcceptCharset, hAcceptEncoding, hAcceptLanguage, + hAcceptPatch, hAcceptRanges, + hAccessControlAllowCredentials, + hAccessControlAllowHeaders, + hAccessControlAllowMethods, + hAccessControlAllowOrigin, + hAccessControlExposeHeaders, + hAccessControlMaxAge, + hAccessControlRequestMethod, hAge, hAllow, + hAltSvc, hAuthorization, hCacheControl, hConnection, + hContentDigest, hContentDisposition, hContentEncoding, hContentLanguage, @@ -173,12 +192,15 @@ module Network.HTTP.Types ( hContentLocation, hContentMD5, hContentRange, + hContentSecurityPolicy, + hContentSecurityPolicyReportOnly, hContentType, hCookie, hDate, hETag, hExpect, hExpires, + hForwarded, hFrom, hHost, hIfMatch, @@ -187,6 +209,7 @@ module Network.HTTP.Types ( hIfRange, hIfUnmodifiedSince, hLastModified, + hLink, hLocation, hMaxForwards, hMIMEVersion, @@ -201,6 +224,7 @@ module Network.HTTP.Types ( hRetryAfter, hServer, hSetCookie, + hStrictTransportSecurity, hTE, hTrailer, hTransferEncoding, diff --git a/Network/HTTP/Types/Header.hs b/Network/HTTP/Types/Header.hs index f2a5add..cf586ed 100644 --- a/Network/HTTP/Types/Header.hs +++ b/Network/HTTP/Types/Header.hs @@ -23,12 +23,22 @@ module Network.HTTP.Types.Header ( hAcceptCharset, hAcceptEncoding, hAcceptLanguage, + hAcceptPatch, hAcceptRanges, + hAccessControlAllowCredentials, + hAccessControlAllowHeaders, + hAccessControlAllowMethods, + hAccessControlAllowOrigin, + hAccessControlExposeHeaders, + hAccessControlMaxAge, + hAccessControlRequestMethod, hAge, hAllow, + hAltSvc, hAuthorization, hCacheControl, hConnection, + hContentDigest, hContentDisposition, hContentEncoding, hContentLanguage, @@ -36,12 +46,15 @@ module Network.HTTP.Types.Header ( hContentLocation, hContentMD5, hContentRange, + hContentSecurityPolicy, + hContentSecurityPolicyReportOnly, hContentType, hCookie, hDate, hETag, hExpect, hExpires, + hForwarded, hFrom, hHost, hIfMatch, @@ -50,6 +63,7 @@ module Network.HTTP.Types.Header ( hIfRange, hIfUnmodifiedSince, hLastModified, + hLink, hLocation, hMaxForwards, hMIMEVersion, @@ -64,6 +78,7 @@ module Network.HTTP.Types.Header ( hRetryAfter, hServer, hSetCookie, + hStrictTransportSecurity, hTE, hTrailer, hTransferEncoding, @@ -89,13 +104,21 @@ module Network.HTTP.Types.Header ( ) where +import Control.Monad (guard) import qualified Data.ByteString as B -import qualified Data.ByteString.Builder as B +import qualified Data.ByteString.Builder as B ( + Builder, + byteString, + integerDec, + toLazyByteString, + word8, + ) import qualified Data.ByteString.Char8 as B8 import qualified Data.ByteString.Lazy as BL import qualified Data.CaseInsensitive as CI import Data.Data (Data) import Data.List (intersperse) +import Data.Word (Word8) import GHC.Generics (Generic) -- | A full HTTP header field with the name and value separated. @@ -136,6 +159,48 @@ hAcceptCharset = "Accept-Charset" hAcceptEncoding :: HeaderName hAcceptEncoding = "Accept-Encoding" +-- | [Access-Control-Allow-Credentials](https://www.w3.org/TR/2014/REC-cors-20140116/#access-control-allow-origin-response-header) +-- +-- @since 0.12.6 +hAccessControlAllowCredentials :: HeaderName +hAccessControlAllowCredentials = "Access-Control-Allow-Credentials" + +-- | [Access-Control-Allow-Headers](https://www.w3.org/TR/2014/REC-cors-20140116/#access-control-allow-headers-response-header) +-- +-- @since 0.12.6 +hAccessControlAllowHeaders :: HeaderName +hAccessControlAllowHeaders = "Access-Control-Allow-Headers" + +-- | [Access-Control-Allow-Methods](https://www.w3.org/TR/2014/REC-cors-20140116/#access-control-allow-methods-response-header) +-- +-- @since 0.12.6 +hAccessControlAllowMethods :: HeaderName +hAccessControlAllowMethods = "Access-Control-Allow-Methods" + +-- | [Access-Control-Allow-Origin](https://www.w3.org/TR/2014/REC-cors-20140116/#access-control-allow-origin-response-header) +-- +-- @since 0.12.6 +hAccessControlAllowOrigin :: HeaderName +hAccessControlAllowOrigin = "Access-Control-Allow-Origin" + +-- | [Access-Control-Expose-Headers](https://www.w3.org/TR/2014/REC-cors-20140116/#access-control-expose-headers-response-header) +-- +-- @since 0.12.6 +hAccessControlExposeHeaders :: HeaderName +hAccessControlExposeHeaders = "Access-Control-Expose-Headers" + +-- | [Access-Control-Max-Age](https://www.w3.org/TR/2014/REC-cors-20140116/#access-control-max-age-response-header) +-- +-- @since 0.12.6 +hAccessControlMaxAge :: HeaderName +hAccessControlMaxAge = "Access-Control-Max-Age" + +-- | [Access-Control-Request-Method](https://www.w3.org/TR/2014/REC-cors-20140116/#access-control-request-method-request-header) +-- +-- @since 0.12.6 +hAccessControlRequestMethod :: HeaderName +hAccessControlRequestMethod = "Access-Control-Request-Method" + -- | [Accept-Language](https://www.rfc-editor.org/rfc/rfc9110.html#name-accept-language) -- -- @since 0.7.0 @@ -406,17 +471,41 @@ hWWWAuthenticate = "WWW-Authenticate" hWarning :: HeaderName hWarning = "Warning" +-- | [Accept-Patch](https://www.rfc-editor.org/rfc/rfc5789.html#section-3.1) +-- +-- @since 0.12.6 +hAcceptPatch :: HeaderName +hAcceptPatch = "Accept-Patch" + +-- | [Alt-Svc](https://www.rfc-editor.org/rfc/rfc7838.html#section-3) +-- +-- @since 0.12.6 +hAltSvc :: HeaderName +hAltSvc = "Alt-Svc" + -- | [Content-Disposition](https://www.rfc-editor.org/rfc/rfc6266.html) -- -- @since 0.10 hContentDisposition :: HeaderName hContentDisposition = "Content-Disposition" --- | [MIME-Version](https://www.rfc-editor.org/rfc/rfc2616.html#section-19.4.1) +-- | [Content-Digest](https://www.rfc-editor.org/rfc/rfc9530.html#section-2) -- --- @since 0.10 -hMIMEVersion :: HeaderName -hMIMEVersion = "MIME-Version" +-- @since 0.12.6 +hContentDigest :: HeaderName +hContentDigest = "Content-Digest" + +-- | [Content-Security-Policy](https://www.w3.org/TR/CSP3/#csp-header) +-- +-- @since 0.12.6 +hContentSecurityPolicy :: HeaderName +hContentSecurityPolicy = "Content-Security-Policy" + +-- | [Content-Security-Policy-Report-Only](https://www.w3.org/TR/CSP3/#cspro-header) +-- +-- @since 0.12.6 +hContentSecurityPolicyReportOnly :: HeaderName +hContentSecurityPolicyReportOnly = "Content-Security-Policy-Report-Only" -- | [Cookie](https://www.rfc-editor.org/rfc/rfc6265.html#section-4.2) -- @@ -424,11 +513,23 @@ hMIMEVersion = "MIME-Version" hCookie :: HeaderName hCookie = "Cookie" --- | [Set-Cookie](https://www.rfc-editor.org/rfc/rfc6265.html#section-4.1) +-- | [Forwarded](https://www.rfc-editor.org/rfc/rfc7239.html) +-- +-- @since 0.12.6 +hForwarded :: HeaderName +hForwarded = "Forwarded" + +-- | [Link](https://www.rfc-editor.org/rfc/rfc8288.html#section-3) +-- +-- @since 0.12.6 +hLink :: HeaderName +hLink = "Link" + +-- | [MIME-Version](https://www.rfc-editor.org/rfc/rfc2616.html#section-19.4.1) -- -- @since 0.10 -hSetCookie :: HeaderName -hSetCookie = "Set-Cookie" +hMIMEVersion :: HeaderName +hMIMEVersion = "MIME-Version" -- | [Origin](https://www.rfc-editor.org/rfc/rfc6454.html#section-7) -- @@ -448,6 +549,18 @@ hPrefer = "Prefer" hPreferenceApplied :: HeaderName hPreferenceApplied = "Preference-Applied" +-- | [Set-Cookie](https://www.rfc-editor.org/rfc/rfc6265.html#section-4.1) +-- +-- @since 0.10 +hSetCookie :: HeaderName +hSetCookie = "Set-Cookie" + +-- | [Strict-Transport-Security](https://www.rfc-editor.org/rfc/rfc6797.html#section-6.1) +-- +-- @since 0.12.6 +hStrictTransportSecurity :: HeaderName +hStrictTransportSecurity = "Strict-Transport-Security" + -- | An individual byte range. Used in @Range@ request headers. -- This type and its accompanying functions are /NOT/ compatible with the -- @Content-Range@ response header. @@ -476,9 +589,9 @@ data ByteRange -- -- @since 0.6.11 renderByteRangeBuilder :: ByteRange -> B.Builder -renderByteRangeBuilder (ByteRangeFrom from) = B.integerDec from `mappend` B.char7 '-' -renderByteRangeBuilder (ByteRangeFromTo from to) = B.integerDec from `mappend` B.char7 '-' `mappend` B.integerDec to -renderByteRangeBuilder (ByteRangeSuffix suffix) = B.char7 '-' `mappend` B.integerDec suffix +renderByteRangeBuilder (ByteRangeFrom from) = B.integerDec from `mappend` B.word8 _hyphen +renderByteRangeBuilder (ByteRangeFromTo from to) = B.integerDec from `mappend` B.word8 _hyphen `mappend` B.integerDec to +renderByteRangeBuilder (ByteRangeSuffix suffix) = B.word8 _hyphen `mappend` B.integerDec suffix -- | Renders a byte range into a 'B.ByteString'. -- @@ -500,7 +613,7 @@ type ByteRanges = [ByteRange] renderByteRangesBuilder :: ByteRanges -> B.Builder renderByteRangesBuilder xs = B.byteString "bytes=" - `mappend` mconcat (intersperse (B.char7 ',') $ map renderByteRangeBuilder xs) + `mappend` mconcat (intersperse (B.word8 _comma) $ map renderByteRangeBuilder xs) -- | Renders a list of byte ranges into a 'B.ByteString'. -- @@ -533,33 +646,35 @@ renderByteRanges = BL.toStrict . B.toLazyByteString . renderByteRangesBuilder -- @since 0.9.1 parseByteRanges :: B.ByteString -> Maybe ByteRanges parseByteRanges bs1 = do - bs2 <- stripPrefixB "bytes=" bs1 - (r, bs3) <- range bs2 - ranges (r :) bs3 + (r, bs2) <- stripBytes >>= range + ranges (r :) bs2 where + stripBytes = do + let prefix = "bytes=" + prefixLen = B.length prefix + (pre, post) = B.splitAt prefixLen bs1 + guard $ pre == prefix + Just post range bs2 = do (i, bs3) <- B8.readInteger bs2 if i < 0 -- has prefix "-" ("-0" is not valid, but here treated as "0-") then Just (ByteRangeSuffix (negate i), bs3) else do - bs4 <- stripPrefixB "-" bs3 + bs4 <- pop _hyphen bs3 case B8.readInteger bs4 of Just (j, bs5) | j >= i -> Just (ByteRangeFromTo i j, bs5) _ -> Just (ByteRangeFrom i, bs4) ranges front bs3 | B.null bs3 = Just (front []) | otherwise = do - bs4 <- stripPrefixB "," bs3 + bs4 <- pop _comma bs3 (r, bs5) <- range bs4 ranges (front . (r :)) bs5 - -stripPrefixB :: B.ByteString -> B.ByteString -> Maybe B.ByteString -#if !MIN_VERSION_bytestring(0,10,8) --- FIXME: Use 'stripPrefix' from the 'bytestring' package. --- Might have to update the dependency constraints though. -stripPrefixB x y - | x `B.isPrefixOf` y = Just (B.drop (B.length x) y) - | otherwise = Nothing -#else -stripPrefixB = B.stripPrefix -#endif + pop w8 bs = do + (b, rest) <- B.uncons bs + guard $ b == w8 + Just rest + +_comma, _hyphen :: Word8 +_comma = 0x2C +_hyphen = 0x2D diff --git a/Network/HTTP/Types/QueryLike.hs b/Network/HTTP/Types/QueryLike.hs index 1c24843..fc46ca5 100644 --- a/Network/HTTP/Types/QueryLike.hs +++ b/Network/HTTP/Types/QueryLike.hs @@ -9,8 +9,8 @@ module Network.HTTP.Types.QueryLike ( where import Control.Arrow ((***)) -import Data.ByteString as B (ByteString, concat) -import Data.ByteString.Lazy as L (ByteString, toChunks) +import Data.ByteString as B (ByteString) +import Data.ByteString.Lazy as L (ByteString, toStrict) import Data.Maybe (catMaybes) import Data.Text as T (Text, pack) import Data.Text.Encoding as T (encodeUtf8) @@ -48,13 +48,13 @@ instance (QueryKeyLike k, QueryValueLike v) => QueryLike [Maybe (k, v)] where toQuery = toQuery . catMaybes instance QueryKeyLike B.ByteString where toQueryKey = id -instance QueryKeyLike L.ByteString where toQueryKey = B.concat . L.toChunks +instance QueryKeyLike L.ByteString where toQueryKey = L.toStrict instance QueryKeyLike T.Text where toQueryKey = T.encodeUtf8 instance QueryKeyLike [Char] where toQueryKey = T.encodeUtf8 . T.pack instance QueryValueLike B.ByteString where toQueryValue = Just -instance QueryValueLike L.ByteString where toQueryValue = Just . B.concat . L.toChunks +instance QueryValueLike L.ByteString where toQueryValue = Just . L.toStrict instance QueryValueLike T.Text where toQueryValue = Just . T.encodeUtf8 instance QueryValueLike [Char] where toQueryValue = Just . T.encodeUtf8 . T.pack -instance QueryValueLike a => QueryValueLike (Maybe a) where +instance (QueryValueLike a) => QueryValueLike (Maybe a) where toQueryValue mVal = mVal >>= toQueryValue diff --git a/Network/HTTP/Types/Status.hs b/Network/HTTP/Types/Status.hs index a939a15..0abf1c7 100644 --- a/Network/HTTP/Types/Status.hs +++ b/Network/HTTP/Types/Status.hs @@ -1,23 +1,133 @@ +{-# LANGUAGE CPP #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE PatternSynonyms #-} -- | Types and constants to describe HTTP status codes. -- --- At the bottom are some functions to check if a given 'Status' is from a certain category. (i.e. @1XX@, @2XX@, etc.) +-- At the bottom are some functions to check if a given t'Status' is from a certain category. (i.e. @1XX@, @2XX@, etc.) module Network.HTTP.Types.Status ( -- * HTTP Status - - -- If we ever want to deprecate the 'Status' data constructor: - -- #if __GLASGOW_HASKELL__ >= 908 - -- {-# DEPRECATED "Use 'mkStatus' when constructing a 'Status'" #-} Status(Status) - -- #else - Status (Status), - -- #endif +#if __GLASGOW_HASKELL__ >= 800 + Status ( + Status, + Status100, + Status101, + Status200, + Status201, + Status202, + Status203, + Status204, + Status205, + Status206, + Status300, + Status301, + Status302, + Status303, + Status304, + Status305, + Status307, + Status308, + Status400, + Status401, + Status402, + Status403, + Status404, + Status405, + Status407, + Status408, + Status409, + Status410, + Status411, + Status412, + Status413, + Status414, + Status415, + Status416, + Status417, + Status418, + Status422, + Status426, + Status428, + Status429, + Status431, + Status451, + Status500, + Status501, + Status502, + Status503, + Status504, + Status505, + Status511 + ), +#else + Status(Status), + pattern Status100, + pattern Status101, + pattern Status200, + pattern Status201, + pattern Status202, + pattern Status203, + pattern Status204, + pattern Status205, + pattern Status206, + pattern Status300, + pattern Status301, + pattern Status302, + pattern Status303, + pattern Status304, + pattern Status305, + pattern Status307, + pattern Status308, + pattern Status400, + pattern Status401, + pattern Status402, + pattern Status403, + pattern Status404, + pattern Status405, + pattern Status407, + pattern Status408, + pattern Status409, + pattern Status410, + pattern Status411, + pattern Status412, + pattern Status413, + pattern Status414, + pattern Status415, + pattern Status416, + pattern Status417, + pattern Status418, + pattern Status422, + pattern Status426, + pattern Status428, + pattern Status429, + pattern Status431, + pattern Status451, + pattern Status500, + pattern Status501, + pattern Status502, + pattern Status503, + pattern Status504, + pattern Status505, + pattern Status511, +#endif statusCode, statusMessage, mkStatus, + -- ** Parsing and Rendering + + -- | These functions are quicker and more efficient than doing it yourself. + parseStatusCode, + renderStatusCode, + parseFullStatus, + renderFullStatus, + + -- ** Low level functions + renderStatusCodeToPtr, + renderFullStatusToPtr, + -- * Common statuses status100, continue100, @@ -126,8 +236,23 @@ module Network.HTTP.Types.Status ( statusIsServerError, ) where -import Data.ByteString as B (ByteString, empty) +import Control.Monad (guard) +import Data.Bits ((.&.), (.|.)) +import Data.ByteString as B (ByteString, drop, empty, length, uncons) +import qualified Data.ByteString.Char8 as B8 +import Data.ByteString.Internal as B ( + ByteString (..), + accursedUnutterablePerformIO, + unsafeCreate, + ) import Data.Data (Data) +import Data.Function (on) +import Foreign (Ptr, Word8, copyBytes, peek, plusPtr, poke) +#if !MIN_VERSION_base(4,15,0) +import Foreign.ForeignPtr (ForeignPtr, withForeignPtr) +#else +import GHC.ForeignPtr (unsafeWithForeignPtr) +#endif import GHC.Generics (Generic) -- | HTTP Status. @@ -140,11 +265,11 @@ import GHC.Generics (Generic) -- Note that the 'Show' instance is only for debugging. data Status = Status { statusCode :: Int - -- ^ The 3-digit code of a 'Status' + -- ^ The 3-digit code of a t'Status' -- -- For example: "200" in a @200 OK@ status , statusMessage :: B.ByteString - -- ^ The textual message of a 'Status' + -- ^ The textual message of a t'Status' -- -- For example: "Not Found" in a @404 Not Found@ status } @@ -156,26 +281,18 @@ data Status = Status Generic ) --- FIXME: If the data constructor of 'Status' is ever deprecated, we should define --- a pattern synonym to minimize any breakage. This also involves changing the --- name of the constructor, so that it doesn't clash with the new pattern synonym --- that's replacing it. --- --- > data Status = MkStatus ... --- > pattern Status code msg = MkStatus code msg - --- | A 'Status' is equal to another 'Status' if the status codes are equal. +-- | A t'Status' is equal to another t'Status' if the status codes are equal. instance Eq Status where - Status{statusCode = a} == Status{statusCode = b} = a == b + (==) = (==) `on` statusCode --- | 'Status'es are ordered according to their status codes only. +-- | t'Status'es are ordered according to their status codes only. instance Ord Status where - compare Status{statusCode = a} Status{statusCode = b} = a `compare` b + compare = compare `on` statusCode --- | Be advised, that when using the \"enumFrom*\" family of methods or +-- | Be advised, that when using the @enumFrom*@ family of methods or -- ranges in lists, it will generate all possible status codes. -- --- E.g. @[status100 .. status200]@ generates 'Status'es of @100, 101, 102 .. 198, 199, 200@ +-- E.g. @[status100 .. status200]@ generates t'Status'es of @100, 101, 102 .. 198, 199, 200@ -- -- The statuses not included in this library will have an empty message. -- @@ -238,7 +355,7 @@ instance Bounded Status where minBound = status100 maxBound = status511 --- | Create a 'Status' from a status code and message. +-- | Create a t'Status' from a status code and message. -- -- @since 0.7.3 mkStatus :: Int -> B.ByteString -> Status @@ -268,6 +385,14 @@ status101 = mkStatus 101 "Switching Protocols" switchingProtocols101 :: Status switchingProtocols101 = status101 +-- | @since 0.12.6 +pattern Status100 :: Status +pattern Status100 <- Status 100 _ + +-- | @since 0.12.6 +pattern Status101 :: Status +pattern Status101 <- Status 101 _ + -- | OK 200 status200 :: Status status200 = mkStatus 200 "OK" @@ -344,6 +469,34 @@ status206 = mkStatus 206 "Partial Content" partialContent206 :: Status partialContent206 = status206 +-- | @since 0.12.6 +pattern Status200 :: Status +pattern Status200 <- Status 200 _ + +-- | @since 0.12.6 +pattern Status201 :: Status +pattern Status201 <- Status 201 _ + +-- | @since 0.12.6 +pattern Status202 :: Status +pattern Status202 <- Status 202 _ + +-- | @since 0.12.6 +pattern Status203 :: Status +pattern Status203 <- Status 203 _ + +-- | @since 0.12.6 +pattern Status204 :: Status +pattern Status204 <- Status 204 _ + +-- | @since 0.12.6 +pattern Status205 :: Status +pattern Status205 <- Status 205 _ + +-- | @since 0.12.6 +pattern Status206 :: Status +pattern Status206 <- Status 206 _ + -- | Multiple Choices 300 status300 :: Status status300 = mkStatus 300 "Multiple Choices" @@ -424,6 +577,39 @@ status308 = mkStatus 308 "Permanent Redirect" permanentRedirect308 :: Status permanentRedirect308 = status308 + +-- | @since 0.12.6 +pattern Status300 :: Status +pattern Status300 <- Status 300 _ + +-- | @since 0.12.6 +pattern Status301 :: Status +pattern Status301 <- Status 301 _ + +-- | @since 0.12.6 +pattern Status302 :: Status +pattern Status302 <- Status 302 _ + +-- | @since 0.12.6 +pattern Status303 :: Status +pattern Status303 <- Status 303 _ + +-- | @since 0.12.6 +pattern Status304 :: Status +pattern Status304 <- Status 304 _ + +-- | @since 0.12.6 +pattern Status305 :: Status +pattern Status305 <- Status 305 _ + +-- | @since 0.12.6 +pattern Status307 :: Status +pattern Status307 <- Status 307 _ + +-- | @since 0.12.6 +pattern Status308 :: Status +pattern Status308 <- Status 308 _ + -- | Bad Request 400 status400 :: Status status400 = mkStatus 400 "Bad Request" @@ -719,6 +905,102 @@ status451 = mkStatus 451 "Unavailable For Legal Reasons" unavailableForLegalReasons451 :: Status unavailableForLegalReasons451 = status451 +-- | @since 0.12.6 +pattern Status400 :: Status +pattern Status400 <- Status 400 _ + +-- | @since 0.12.6 +pattern Status401 :: Status +pattern Status401 <- Status 401 _ + +-- | @since 0.12.6 +pattern Status402 :: Status +pattern Status402 <- Status 402 _ + +-- | @since 0.12.6 +pattern Status403 :: Status +pattern Status403 <- Status 403 _ + +-- | @since 0.12.6 +pattern Status404 :: Status +pattern Status404 <- Status 404 _ + +-- | @since 0.12.6 +pattern Status405 :: Status +pattern Status405 <- Status 405 _ + +-- | @since 0.12.6 +pattern Status407 :: Status +pattern Status407 <- Status 407 _ + +-- | @since 0.12.6 +pattern Status408 :: Status +pattern Status408 <- Status 408 _ + +-- | @since 0.12.6 +pattern Status409 :: Status +pattern Status409 <- Status 409 _ + +-- | @since 0.12.6 +pattern Status410 :: Status +pattern Status410 <- Status 410 _ + +-- | @since 0.12.6 +pattern Status411 :: Status +pattern Status411 <- Status 411 _ + +-- | @since 0.12.6 +pattern Status412 :: Status +pattern Status412 <- Status 412 _ + +-- | @since 0.12.6 +pattern Status413 :: Status +pattern Status413 <- Status 413 _ + +-- | @since 0.12.6 +pattern Status414 :: Status +pattern Status414 <- Status 414 _ + +-- | @since 0.12.6 +pattern Status415 :: Status +pattern Status415 <- Status 415 _ + +-- | @since 0.12.6 +pattern Status416 :: Status +pattern Status416 <- Status 416 _ + +-- | @since 0.12.6 +pattern Status417 :: Status +pattern Status417 <- Status 417 _ + +-- | @since 0.12.6 +pattern Status418 :: Status +pattern Status418 <- Status 418 _ + +-- | @since 0.12.6 +pattern Status422 :: Status +pattern Status422 <- Status 422 _ + +-- | @since 0.12.6 +pattern Status426 :: Status +pattern Status426 <- Status 426 _ + +-- | @since 0.12.6 +pattern Status428 :: Status +pattern Status428 <- Status 428 _ + +-- | @since 0.12.6 +pattern Status429 :: Status +pattern Status429 <- Status 429 _ + +-- | @since 0.12.6 +pattern Status431 :: Status +pattern Status431 <- Status 431 _ + +-- | @since 0.12.6 +pattern Status451 :: Status +pattern Status451 <- Status 451 _ + -- | Internal Server Error 500 status500 :: Status status500 = mkStatus 500 "Internal Server Error" @@ -801,6 +1083,34 @@ status511 = mkStatus 511 "Network Authentication Required" networkAuthenticationRequired511 :: Status networkAuthenticationRequired511 = status511 +-- | @since 0.12.6 +pattern Status500 :: Status +pattern Status500 <- Status 500 _ + +-- | @since 0.12.6 +pattern Status501 :: Status +pattern Status501 <- Status 501 _ + +-- | @since 0.12.6 +pattern Status502 :: Status +pattern Status502 <- Status 502 _ + +-- | @since 0.12.6 +pattern Status503 :: Status +pattern Status503 <- Status 503 _ + +-- | @since 0.12.6 +pattern Status504 :: Status +pattern Status504 <- Status 504 _ + +-- | @since 0.12.6 +pattern Status505 :: Status +pattern Status505 <- Status 505 _ + +-- | @since 0.12.6 +pattern Status511 :: Status +pattern Status511 <- Status 511 _ + -- | Informational class -- -- Checks if the status is in the 1XX range. @@ -840,3 +1150,132 @@ statusIsClientError (Status{statusCode = code}) = code >= 400 && code < 500 -- @since 0.8.0 statusIsServerError :: Status -> Bool statusIsServerError (Status{statusCode = code}) = code >= 500 && code < 600 + +-- | Write the 3 digit t'Status' code to the provided 'Ptr'. +-- +-- /N.B. This function assumes @statusCode < 1000@!/ +-- /If it is @>= 1000@, the first byte will not be a digit./ +-- +-- @since 0.12.6 +renderStatusCodeToPtr :: Status -> Ptr Word8 -> IO () +renderStatusCodeToPtr (Status code _) ptr = do + poke ptr $ toByte h + poke (ptr `plusPtr` 1) $ toByte t + poke (ptr `plusPtr` 2) $ toByte i + where + (h, rest) = code `divMod` 100 + (t, i) = rest `divMod` 10 + toByte :: Int -> Word8 + toByte x = fromIntegral x .|. 0x30 + +-- | Render the 3 digit t'Status' code into a 'ByteString'. +-- +-- @since 0.12.6 +renderStatusCode :: Status -> ByteString +renderStatusCode s@(Status code _) + | code >= 1000 = B8.pack $ show s + | otherwise = + unsafeCreate 3 $ renderStatusCodeToPtr s + +-- | Writes the full t'Status' code to the provided 'Ptr'. +-- +-- /N.B. Same caveat from 'renderStatusCodeToPtr' applies./ +-- +-- @since 0.12.6 +renderFullStatusToPtr :: Status -> Ptr Word8 -> IO () +renderFullStatusToPtr s@(Status _ (PS fptr offset len)) ptr = do + renderStatusCodeToPtr s ptr + poke (ptr `plusPtr` 3) (0x20 :: Word8) + unsafeWithForeignPtr fptr $ \src -> + copyBytes (ptr `plusPtr` 4) (src `plusPtr` offset) len + +-- | Render the full t'Status' code with status message into a 'ByteString'. +-- +-- @since 0.12.6 +renderFullStatus :: Status -> ByteString +renderFullStatus s@(Status code msg) + | code >= 1000 = + B8.pack (show code) `mappend` " " `mappend` msg + | otherwise = + unsafeCreate (4 + len) $ renderFullStatusToPtr s + where + len = B.length msg + +-- | Parses the first three characters as digits and converts them to an 'Int'. +-- +-- If the first 3 characters are not digits (i.e. @0-9@), or the 'ByteString' +-- is less than 3 bytes long, the result will be 'Nothing'. +-- +-- When successful, it will return the parsed status code and the remainder of +-- the 'ByteString'. +-- +-- >>> parseStatusCode "307" +-- Just (307,"") +-- +-- >>> parseStatusCode "404 Not Found" +-- Just (404," Not Found") +-- +-- >>> parseStatusCode "No Digits" +-- Nothing +-- +-- >>> parseStatusCode "12 Is Not Enough Digits" +-- Nothing +parseStatusCode :: ByteString -> Maybe (Int, ByteString) +parseStatusCode bs@(PS fptr offset len) + | len < 3 = Nothing + | otherwise = + accursedUnutterablePerformIO $ + unsafeWithForeignPtr fptr $ \ptr' -> do + let ptr = ptr' `plusPtr` offset + w1 <- peek ptr + w2 <- peek (ptr `plusPtr` 1) + w3 <- peek (ptr `plusPtr` 2) + pure $ do + h <- toNumber w1 + t <- toNumber w2 + i <- toNumber w3 + Just (h * 100 + t * 10 + i, B.drop 3 bs) + where + toNumber :: Word8 -> Maybe Int + toNumber w = do + guard $ 0x30 <= w && w <= 0x39 + Just . fromIntegral $ w .&. 0x0F + +-- | Assumes the provided 'ByteString' is either: +-- +-- * only 3 digits, or +-- * 3 digits, a space, and the rest of the status message +-- +-- /N.B. this function does not check for newlines, it puts everything/ +-- /after the code and space into the 'statusMessage'./ +-- +-- >>> parseFullStatus "307" +-- Just (Status {statusCode = 307, statusMessage = ""}) +-- +-- >>> parseFullStatus "404 Not Found" +-- Just (Status {statusCode = 404, statusMessage = "Not Found"}) +-- +-- >>> parseFullStatus "500 Someone Forgot To\r\nBreak At The Newline" +-- Just (Status {statusCode = 500, statusMessage = "Someone Forgot To\r\nBreak At The Newline"}) +-- +-- >>> parseFullStatus "1337 Is A Bad Status Code" +-- Nothing +-- +-- >>> parseFullStatus "101Still Needs A Space" +-- Nothing +-- +-- >>> parseFullStatus "No Digits" +-- Nothing +parseFullStatus :: ByteString -> Maybe Status +parseFullStatus bs = do + (code, rest) <- parseStatusCode bs + case B.uncons rest of + Nothing -> Just $ mkStatus code "" + Just (w, ws) + | w == 0x20 -> Just $ mkStatus code ws + | otherwise -> Nothing + +#if !MIN_VERSION_base(4,15,0) +unsafeWithForeignPtr :: ForeignPtr a -> (Ptr a -> IO b) -> IO b +unsafeWithForeignPtr = withForeignPtr +#endif diff --git a/Network/HTTP/Types/URI.hs b/Network/HTTP/Types/URI.hs index 8d34f18..f920f19 100644 --- a/Network/HTTP/Types/URI.hs +++ b/Network/HTTP/Types/URI.hs @@ -84,7 +84,7 @@ module Network.HTTP.Types.URI ( where import Control.Arrow (second, (***)) -import Data.Bits (shiftL, (.|.)) +import Data.Bits (shiftL, shiftR, (.&.), (.|.)) import qualified Data.ByteString as B import qualified Data.ByteString.Builder as B import qualified Data.ByteString.Lazy as BL @@ -114,6 +114,9 @@ import Data.Word (Word8) type QueryItem = (B.ByteString, Maybe B.ByteString) -- | A sequence of 'QueryItem's. +-- +-- General form: @a=b&c=d@, but if for example the value of @a@ is 'Nothing' +-- instead of @'Just' "b"@, it becomes @a&c=d@. type Query = [QueryItem] -- | Like Query, but with 'Text' instead of 'B.ByteString' (UTF8-encoded). @@ -174,21 +177,17 @@ simpleQueryToQuery = map (second Just) renderQueryBuilder :: Bool -> Query -> B.Builder renderQueryBuilder _ [] = mempty renderQueryBuilder qmark' (p : ps) = - -- FIXME: replace mconcat + map with foldr - mconcat $ - go (if qmark' then qmark else mempty) p - : map (go amp) ps + mconcat $ go qmark p : map (go amp) ps where - qmark = B.byteString "?" - amp = B.byteString "&" - equal = B.byteString "=" + qmark = if qmark' then B.word8 _question else mempty + amp = B.word8 _ampersand go sep (k, mv) = mconcat [ sep , urlEncodeBuilder True k , case mv of Nothing -> mempty - Just v -> equal `mappend` urlEncodeBuilder True v + Just v -> B.word8 _equal `mappend` urlEncodeBuilder True v ] -- | Renders the given 'Query' into a 'B.ByteString'. @@ -234,30 +233,29 @@ parseQueryReplacePlus replacePlus bs = parseQueryString' $ dropQuestion bs where dropQuestion q = case B.uncons q of - Just (63, q') -> q' + -- 0x3F == _question + Just (0x3F, q') -> q' _ -> q parseQueryString' q | B.null q = [] parseQueryString' q = let (x, xs) = breakDiscard queryStringSeparators q in parsePair x : parseQueryString' xs where + queryStringSeparators :: B.ByteString + queryStringSeparators = "&;" parsePair x = - let (k, v) = B.break (== 61) x -- equal sign + let (k, v) = B.break (== _equal) x v'' = case B.uncons v of Just (_, v') -> Just $ urlDecode replacePlus v' _ -> Nothing in (urlDecode replacePlus k, v'') - -queryStringSeparators :: B.ByteString -queryStringSeparators = B.pack [38, 59] -- ampersand, semicolon - --- | Break the second bytestring at the first occurrence of any bytes from --- the first bytestring, discarding that byte. -breakDiscard :: B.ByteString -> B.ByteString -> (B.ByteString, B.ByteString) -breakDiscard seps s = - let (x, y) = B.break (`B.elem` seps) s - in (x, B.drop 1 y) + -- Break the second bytestring at the first occurrence of any bytes from + -- the first bytestring, discarding that byte. + breakDiscard :: B.ByteString -> B.ByteString -> (B.ByteString, B.ByteString) + breakDiscard seps s = + let (x, y) = B.break (`B.elem` seps) s + in (x, B.drop 1 y) -- | Parse 'SimpleQuery' from a 'B.ByteString'. -- @@ -300,19 +298,22 @@ urlEncodeBuilder' extraUnreserved = | unreserved ch = B.word8 ch | otherwise = h2 ch - unreserved ch - | ch >= 65 && ch <= 90 = True -- A-Z - | ch >= 97 && ch <= 122 = True -- a-z - | ch >= 48 && ch <= 57 = True -- 0-9 - unreserved c = c `elem` extraUnreserved + -- The order is optimized from most expected to least expected + unreserved ch = + -- FIXME: could be one index lookup + (ch >= 0x61 && ch <= 0x7A) -- a-z + || (ch >= 0x30 && ch <= 0x39) -- 0-9 + || (ch >= 0x41 && ch <= 0x5A) -- A-Z + || (ch `elem` extraUnreserved) -- must be upper-case - h2 v = B.word8 37 `mappend` B.word8 (h a) `mappend` B.word8 (h b) -- 37 = % + h2 v = B.word8 _percent `mappend` B.word8 (h a) `mappend` B.word8 (h b) where - (a, b) = v `divMod` 16 + a = v `shiftR` 4 + b = v .&. 0x0F h i - | i < 10 = 48 + i -- zero (0) - | otherwise = 65 + i - 10 -- 65: A + | i < 10 = 0x30 + i -- digits (0x30 == '0') + | otherwise = 0x37 + i -- A-F (0x41 - 10; 0x41 == 'A') -- | Percent-encoding for URLs. -- @@ -359,20 +360,23 @@ urlDecode replacePlus z = fst $ B.unfoldrN (B.length z) go z case B.uncons bs of Nothing -> Nothing -- plus to space - Just (43, ws) | replacePlus -> Just (32, ws) + Just (0x2B, ws) | replacePlus -> Just (0x20, ws) -- percent - Just (37, ws) -> Just $ fromMaybe (37, ws) $ do + Just tup@(0x25, ws) -> Just $ fromMaybe tup $ do (x, xs) <- B.uncons ws - x' <- hexVal x (y, ys) <- B.uncons xs - y' <- hexVal y - Just (combine x' y', ys) - Just (w, ws) -> Just (w, ws) + a <- hexVal x + b <- hexVal y + Just (a `combine` b, ys) + Just other -> Just other hexVal w - | 48 <= w && w <= 57 = Just $ w - 48 -- 0 - 9 - | 65 <= w && w <= 70 = Just $ w - 55 -- A - F - | 97 <= w && w <= 102 = Just $ w - 87 -- a - f + -- FIXME: could be one index lookup + | 0x30 <= w && w <= 0x39 = Just result -- 0 - 9 + | 0x41 <= w && w <= 0x46 = Just (result + 9) -- A - F + | 0x61 <= w && w <= 0x66 = Just (result + 9) -- a - f | otherwise = Nothing + where + result = w .&. 0x0F combine :: Word8 -> Word8 -> Word8 combine a b = shiftL a 4 .|. b @@ -409,13 +413,13 @@ urlDecode replacePlus z = fst $ B.unfoldrN (B.length z) go z -- -- @since 0.5 encodePathSegments :: [Text] -> B.Builder -encodePathSegments = foldr (\x -> mappend (B.byteString "/" `mappend` encodePathSegment x)) mempty +encodePathSegments = foldr (\x -> mappend (B.word8 _slash `mappend` encodePathSegment x)) mempty -- | Like 'encodePathSegments', but without the initial slash. -- -- @since 0.6.10 encodePathSegmentsRelative :: [Text] -> B.Builder -encodePathSegmentsRelative xs = mconcat $ intersperse (B.byteString "/") (map encodePathSegment xs) +encodePathSegmentsRelative xs = mconcat $ intersperse (B.word8 _slash) (map encodePathSegment xs) encodePathSegment :: Text -> B.Builder encodePathSegment = urlEncodeBuilder False . encodeUtf8 @@ -433,10 +437,11 @@ decodePathSegments a = where drop1Slash bs = case B.uncons bs of - Just (47, bs') -> bs' -- 47 == / + -- 0x2F == _slash + Just (0x2F, bs') -> bs' _ -> bs go bs = - let (x, y) = B.break (== 47) bs + let (x, y) = B.break (== _slash) bs in decodePathSegment x : if B.null y then [] @@ -474,11 +479,17 @@ decodePathSegment = decodeUtf8With lenientDecode . urlDecode False extractPath :: B.ByteString -> B.ByteString extractPath = ensureNonEmpty . extract where - extract path - | "http://" `B.isPrefixOf` path = (snd . breakOnSlash . B.drop 7) path - | "https://" `B.isPrefixOf` path = (snd . breakOnSlash . B.drop 8) path - | otherwise = path - breakOnSlash = B.break (== 47) + extract path = + case prefix of + "http://" -> fromSlash rest + "https:/" + -- we need one more _slash for it to be a correct protocol prefix + | Just (0x2F, more) <- B.uncons rest -> + fromSlash more + _ -> path + where + (prefix, rest) = B.splitAt 7 path + fromSlash = B.dropWhile (/= _slash) ensureNonEmpty "" = "/" ensureNonEmpty p = p @@ -494,7 +505,7 @@ encodePath x y = encodePathSegments x `mappend` renderQueryBuilder True y -- @since 0.5 decodePath :: B.ByteString -> ([Text], Query) decodePath b = - let (x, y) = B.break (== 63) b -- question mark + let (x, y) = B.break (== _question) b in (decodePathSegments x, parseQuery y) ----------------------------------------------------------------------------------------- @@ -545,22 +556,25 @@ renderQueryPartialEscape qm = -- @since 0.12.1 renderQueryBuilderPartialEscape :: Bool -> PartialEscapeQuery -> B.Builder renderQueryBuilderPartialEscape _ [] = mempty --- FIXME: replace mconcat + map with foldr renderQueryBuilderPartialEscape qmark' (p : ps) = - mconcat $ - go (if qmark' then qmark else mempty) p - : map (go amp) ps + mconcat $ go qmark p : map (go amp) ps where - qmark = B.byteString "?" - amp = B.byteString "&" - equal = B.byteString "=" + qmark = if qmark' then B.word8 _question else mempty + amp = B.word8 _ampersand go sep (k, mv) = mconcat [ sep , urlEncodeBuilder True k , case mv of [] -> mempty - vs -> equal `mappend` mconcat (map encode vs) + vs -> B.word8 _equal `mappend` mconcat (map encode vs) ] encode (QE v) = urlEncodeBuilder True v encode (QN v) = B.byteString v + +_percent, _ampersand, _slash, _equal, _question :: Word8 +_percent = 0x25 +_ampersand = 0x26 +_slash = 0x2F +_equal = 0x3D +_question = 0x3F diff --git a/Network/HTTP/Types/Version.hs b/Network/HTTP/Types/Version.hs index 6a68731..78ddeaf 100644 --- a/Network/HTTP/Types/Version.hs +++ b/Network/HTTP/Types/Version.hs @@ -1,9 +1,35 @@ +{-# LANGUAGE CPP #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE PatternSynonyms #-} -- | Types and constants to describe the HTTP version. +-- +-- There are no parsing functions, as the formats are fairly different +-- per version of HTTP. And seeing as there are only a handful of versions, +-- it is easier to manually parse the version ad hoc. +-- +-- For example, when you are expecting HTTP v1, try to match @HTTP/1.1@ or +-- @HTTP/1.0@. If you're expecting HTTP v2 or v3, you'd be using ALPN tokens, +-- which would be @http/1.1@, @h2@ or @h3@. module Network.HTTP.Types.Version ( +#if __GLASGOW_HASKELL__ >= 800 + HttpVersion ( + .., + Http09, + Http10, + Http11, + Http20, + Http30 + ), +#else HttpVersion (..), + pattern Http09, + pattern Http10, + pattern Http11, + pattern Http20, + pattern Http30, +#endif http09, http10, http11, @@ -32,6 +58,11 @@ data HttpVersion = HttpVersion -- | >>> show http11 -- "HTTP/1.1" +-- >>> show http20 +-- "HTTP/2.0" +-- +-- This should not be used to render the HTTP version, as different versions +-- have different ways of rendering (i.e. HTTP v2 uses @"h2"@ instead of @"HTTP/2.0"@) instance Show HttpVersion where show (HttpVersion major minor) = "HTTP/" ++ show major ++ "." ++ show minor @@ -58,3 +89,29 @@ http20 = HttpVersion 2 0 -- @since 0.12.5 http30 :: HttpVersion http30 = HttpVersion 3 0 + +---------------------- +-- Pattern Synonyms -- +---------------------- + +-- DO NOT put these on one line with commas, as GHC 7.10.3 doesn't parse that +pattern Http09 :: HttpVersion +pattern Http10 :: HttpVersion +pattern Http11 :: HttpVersion +pattern Http20 :: HttpVersion +pattern Http30 :: HttpVersion + +-- | @since 0.12.6 +pattern Http09 = HttpVersion 0 9 + +-- | @since 0.12.6 +pattern Http10 = HttpVersion 1 0 + +-- | @since 0.12.6 +pattern Http11 = HttpVersion 1 1 + +-- | @since 0.12.6 +pattern Http20 = HttpVersion 2 0 + +-- | @since 0.12.6 +pattern Http30 = HttpVersion 3 0 diff --git a/http-types.cabal b/http-types.cabal index 81ad2a4..2055dcb 100644 --- a/http-types.cabal +++ b/http-types.cabal @@ -7,7 +7,7 @@ Description: Types and functions to describe and handle HTTP concepts. Homepage: https://github.com/Vlix/http-types License: BSD-3-Clause License-file: LICENSE -Author: Aristid Breitkreuz, Michael Snoyman +Author: Felix Paulusma, Aristid Breitkreuz, Michael Snoyman Maintainer: felix.paulusma@gmail.com Copyright: (C) 2011 Aristid Breitkreuz, (C) 2023 Felix Paulusma Category: Network, Web @@ -24,7 +24,7 @@ Extra-doc-files: Source-repository this type: git location: https://github.com/Vlix/http-types.git - tag: v0.12.5 + tag: v0.12.6 Source-repository head type: git diff --git a/test/Network/HTTP/Types/HeaderSpec.hs b/test/Network/HTTP/Types/HeaderSpec.hs index 86a290e..5349a35 100644 --- a/test/Network/HTTP/Types/HeaderSpec.hs +++ b/test/Network/HTTP/Types/HeaderSpec.hs @@ -46,12 +46,22 @@ allHeaders = , (hAcceptCharset, "Accept-Charset") , (hAcceptEncoding, "Accept-Encoding") , (hAcceptLanguage, "Accept-Language") + , (hAcceptPatch, "Accept-Patch") , (hAcceptRanges, "Accept-Ranges") + , (hAccessControlAllowCredentials, "Access-Control-Allow-Credentials") + , (hAccessControlAllowHeaders, "Access-Control-Allow-Headers") + , (hAccessControlAllowMethods, "Access-Control-Allow-Methods") + , (hAccessControlAllowOrigin, "Access-Control-Allow-Origin") + , (hAccessControlExposeHeaders, "Access-Control-Expose-Headers") + , (hAccessControlMaxAge, "Access-Control-Max-Age") + , (hAccessControlRequestMethod, "Access-Control-Request-Method") , (hAge, "Age") , (hAllow, "Allow") + , (hAltSvc, "Alt-Svc") , (hAuthorization, "Authorization") , (hCacheControl, "Cache-Control") , (hConnection, "Connection") + , (hContentDigest, "Content-Digest") , (hContentDisposition, "Content-Disposition") , (hContentEncoding, "Content-Encoding") , (hContentLanguage, "Content-Language") @@ -59,12 +69,15 @@ allHeaders = , (hContentLocation, "Content-Location") , (hContentMD5, "Content-MD5") , (hContentRange, "Content-Range") + , (hContentSecurityPolicy, "Content-Security-Policy") + , (hContentSecurityPolicyReportOnly, "Content-Security-Policy-Report-Only") , (hContentType, "Content-Type") , (hCookie, "Cookie") , (hDate, "Date") , (hETag, "ETag") , (hExpect, "Expect") , (hExpires, "Expires") + , (hForwarded, "Forwarded") , (hFrom, "From") , (hHost, "Host") , (hIfMatch, "If-Match") @@ -73,6 +86,7 @@ allHeaders = , (hIfRange, "If-Range") , (hIfUnmodifiedSince, "If-Unmodified-Since") , (hLastModified, "Last-Modified") + , (hLink, "Link") , (hLocation, "Location") , (hMaxForwards, "Max-Forwards") , (hMIMEVersion, "MIME-Version") @@ -87,6 +101,7 @@ allHeaders = , (hRetryAfter, "Retry-After") , (hServer, "Server") , (hSetCookie, "Set-Cookie") + , (hStrictTransportSecurity, "Strict-Transport-Security") , (hTE, "TE") , (hTrailer, "Trailer") , (hTransferEncoding, "Transfer-Encoding") diff --git a/test/Network/HTTP/Types/StatusSpec.hs b/test/Network/HTTP/Types/StatusSpec.hs index 7f8f700..86fb681 100644 --- a/test/Network/HTTP/Types/StatusSpec.hs +++ b/test/Network/HTTP/Types/StatusSpec.hs @@ -1,4 +1,5 @@ {-# LANGUAGE OverloadedStrings #-} +{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} {-# OPTIONS_GHC -Wno-orphans #-} module Network.HTTP.Types.StatusSpec (main, spec) where @@ -34,6 +35,92 @@ spec = do it "only orders on 'statusCode'" $ property $ \st1 st2 -> (st1 < st2) == ((<) `on` statusCode) st1 st2 + describe "Render functions" $ do + it "renders the code" $ do + renderStatusCode notFound404 `shouldBe` "404" + renderStatusCode continue100 `shouldBe` "100" + renderStatusCode (mkStatus 12 "") `shouldBe` "012" + renderStatusCode (mkStatus 987 "") `shouldBe` "987" + it "renders the message" $ do + renderFullStatus notFound404 `shouldBe` "404 Not Found" + renderFullStatus continue100 `shouldBe` "100 Continue" + renderFullStatus (mkStatus 12 "Short") `shouldBe` "012 Short" + renderFullStatus (mkStatus 987 "Pretty Long If I May Say So Myself") + `shouldBe` "987 Pretty Long If I May Say So Myself" + describe "Parsing functions" $ do + it "parses the code" $ do + parseStatusCode "307" `shouldBe` Just (307, "") + parseStatusCode "404 Not Found" `shouldBe` Just (404, " Not Found") + parseStatusCode "1337 Works Still" `shouldBe` Just (133, "7 Works Still") + parseStatusCode "No Digits" `shouldBe` Nothing + parseStatusCode "12 Is Not Enough" `shouldBe` Nothing + it "parses the message" $ do + parseFullStatus "307" `shouldBe` Just (mkStatus 307 "") + parseFullStatus "404 Not Found" `shouldBe` Just (mkStatus 404 "Not Found") + parseFullStatus "500 Someone Forgot To\r\nBreak At The Newline" + `shouldBe` Just (mkStatus 500 "Someone Forgot To\r\nBreak At The Newline") + parseFullStatus "1337 Works Still" `shouldBe` Nothing + parseFullStatus "No Digits" `shouldBe` Nothing + it "round trips" $ do + parseFullStatus (renderFullStatus notFound404) `shouldBe` Just notFound404 + -- I know this is under "round trips" and loses the message, but + -- it's about the status code here. + parseStatusCode (renderStatusCode notFound404) `shouldBe` Just (404, "") + describe "Patterns" $ do + patternMatch status100 (\Status100 -> pure ()) + patternMatch status101 (\Status101 -> pure ()) + patternMatch status200 (\Status200 -> pure ()) + patternMatch status201 (\Status201 -> pure ()) + patternMatch status202 (\Status202 -> pure ()) + patternMatch status203 (\Status203 -> pure ()) + patternMatch status204 (\Status204 -> pure ()) + patternMatch status205 (\Status205 -> pure ()) + patternMatch status206 (\Status206 -> pure ()) + patternMatch status300 (\Status300 -> pure ()) + patternMatch status301 (\Status301 -> pure ()) + patternMatch status302 (\Status302 -> pure ()) + patternMatch status303 (\Status303 -> pure ()) + patternMatch status304 (\Status304 -> pure ()) + patternMatch status305 (\Status305 -> pure ()) + patternMatch status307 (\Status307 -> pure ()) + patternMatch status308 (\Status308 -> pure ()) + patternMatch status400 (\Status400 -> pure ()) + patternMatch status401 (\Status401 -> pure ()) + patternMatch status402 (\Status402 -> pure ()) + patternMatch status403 (\Status403 -> pure ()) + patternMatch status404 (\Status404 -> pure ()) + patternMatch status405 (\Status405 -> pure ()) + patternMatch status407 (\Status407 -> pure ()) + patternMatch status408 (\Status408 -> pure ()) + patternMatch status409 (\Status409 -> pure ()) + patternMatch status410 (\Status410 -> pure ()) + patternMatch status411 (\Status411 -> pure ()) + patternMatch status412 (\Status412 -> pure ()) + patternMatch status413 (\Status413 -> pure ()) + patternMatch status414 (\Status414 -> pure ()) + patternMatch status415 (\Status415 -> pure ()) + patternMatch status416 (\Status416 -> pure ()) + patternMatch status417 (\Status417 -> pure ()) + patternMatch status418 (\Status418 -> pure ()) + patternMatch status422 (\Status422 -> pure ()) + patternMatch status426 (\Status426 -> pure ()) + patternMatch status428 (\Status428 -> pure ()) + patternMatch status429 (\Status429 -> pure ()) + patternMatch status431 (\Status431 -> pure ()) + patternMatch status451 (\Status451 -> pure ()) + patternMatch status500 (\Status500 -> pure ()) + patternMatch status501 (\Status501 -> pure ()) + patternMatch status502 (\Status502 -> pure ()) + patternMatch status503 (\Status503 -> pure ()) + patternMatch status504 (\Status504 -> pure ()) + patternMatch status505 (\Status505 -> pure ()) + patternMatch status511 (\Status511 -> pure ()) + +patternMatch :: Status -> (Status -> IO ()) -> SpecWith () +patternMatch s f = + it name $ f s `shouldReturn` () + where + name = "match the status " <> show (statusCode s) categoryCheck :: String -> (Status -> Bool) -> [StatusTuple] -> Spec categoryCheck name p shoulds = do diff --git a/test/Network/HTTP/Types/URISpec.hs b/test/Network/HTTP/Types/URISpec.hs index 5e5591c..cb901c4 100644 --- a/test/Network/HTTP/Types/URISpec.hs +++ b/test/Network/HTTP/Types/URISpec.hs @@ -7,10 +7,12 @@ module Network.HTTP.Types.URISpec (main, spec) where +import Data.Bits ((.|.)) import qualified Data.ByteString as B import qualified Data.ByteString.Builder as BB import qualified Data.ByteString.Char8 as B8 import qualified Data.ByteString.Lazy as BL +import qualified Data.Char as C import Data.Maybe (fromMaybe) import Data.Text as T (Text, null) import Debug.Trace (traceShow) @@ -25,6 +27,7 @@ import Test.QuickCheck ( property, suchThat, (.&&.), + (===), (==>), ) import Test.QuickCheck.Instances () @@ -42,7 +45,7 @@ spec = do it "does not escape period and dash" $ toStrictBS (encodePath ["foo-bar.baz"] []) `shouldBe` "/foo-bar.baz" - -- FIXME: needs more path tests + -- FIXME: needs more path tests describe "encode/decode query" $ do it "is identity to encode and then decode" $ @@ -85,6 +88,13 @@ spec = do it "still encodes the same (query)" $ mkGoldenFile "urlEncode-query" $ urlEncode True asciis + it "decodes lower case" $ + property $ \bs -> + -- force all bytes to be above the ASCII range + let onlyPercent = B.map (.|. 0x80) bs + -- Should only be percent encoded and then all "A-F -> a-f" + lowerCaseEncoded = B8.map C.toLower $ urlEncode True onlyPercent + in urlDecode True lowerCaseEncoded === onlyPercent describe "decodePathSegments" $ do it "is inverse to encodePathSegments" $ @@ -150,15 +160,15 @@ goldenDir = "test" ".golden" mkGoldenFile :: String -> B.ByteString -> Golden B.ByteString mkGoldenFile name content = - Golden { - output = content, - encodePretty = B8.unpack, - writeToFile = B.writeFile, - readFromFile = B.readFile, - goldenFile = goldenDir name "golden", - actualFile = Just (goldenDir name "actual"), - failFirstTime = False - } + Golden + { output = content + , encodePretty = B8.unpack + , writeToFile = B.writeFile + , readFromFile = B.readFile + , goldenFile = goldenDir name "golden" + , actualFile = Just (goldenDir name "actual") + , failFirstTime = False + } propEncodeDecodePath :: ([Text], QueryGen B.ByteString) -> Bool propEncodeDecodePath (p', QueryGen b) = @@ -209,15 +219,15 @@ propDecodeSimpleQuery (QueryGen q) = where rq = renderQuery True q -propEncodeDecodeQuerySimple :: QueryGen B.ByteString -> Bool -> Bool +propEncodeDecodeQuerySimple :: QueryGen B.ByteString -> Bool -> Property propEncodeDecodeQuerySimple (QueryGen q') b = - q == (parseSimpleQuery . renderSimpleQuery b) q + q === (parseSimpleQuery . renderSimpleQuery b) q where q = fmap (fmap $ fromMaybe "") q' -propEncodeDecodeURL :: B.ByteString -> Bool -> Bool -> Bool +propEncodeDecodeURL :: B.ByteString -> Bool -> Bool -> Property propEncodeDecodeURL bs b1 b2 = - bs == urlDecode b1 (urlEncode b3 bs) + bs === urlDecode b1 (urlEncode b3 bs) where b3 = b1 || b2 diff --git a/test/Network/HTTP/Types/VersionSpec.hs b/test/Network/HTTP/Types/VersionSpec.hs index 1a80923..e5ef90d 100644 --- a/test/Network/HTTP/Types/VersionSpec.hs +++ b/test/Network/HTTP/Types/VersionSpec.hs @@ -1,3 +1,5 @@ +{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} + module Network.HTTP.Types.VersionSpec (main, spec) where import Test.Hspec @@ -8,9 +10,21 @@ main :: IO () main = hspec spec spec :: Spec -spec = +spec = do describe "Regression tests" $ mapM_ checkVersion allVersions + describe "Patterns" $ do + patternMatch http09 (\Http09 -> pure ()) + patternMatch http10 (\Http10 -> pure ()) + patternMatch http11 (\Http11 -> pure ()) + patternMatch http20 (\Http20 -> pure ()) + patternMatch http30 (\Http30 -> pure ()) + +patternMatch :: HttpVersion -> (HttpVersion -> IO ()) -> SpecWith () +patternMatch hv f = + it name $ f hv `shouldReturn` () + where + name = "match the version " <> show hv -- | [("Rendered", {constant}, {literal})] allVersions :: [(String, HttpVersion, HttpVersion)]