From 0a4f73cddec1120ae601453fce94be14b9614149 Mon Sep 17 00:00:00 2001 From: zypeh <zypeh.geek@gmail.com> Date: Thu, 19 Aug 2021 23:49:07 +0800 Subject: [PATCH 01/13] parser fixed; remove redundant imports --- Z/Data/HTTP/Request.hs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Z/Data/HTTP/Request.hs b/Z/Data/HTTP/Request.hs index f0bbe89..0769a83 100644 --- a/Z/Data/HTTP/Request.hs +++ b/Z/Data/HTTP/Request.hs @@ -5,7 +5,6 @@ import Data.IORef import qualified Data.CaseInsensitive as CI import qualified Z.Data.Builder as B import qualified Z.Data.Parser as P -import qualified Z.Data.Parser as P import qualified Z.Data.Text as T import qualified Z.Data.Vector as V import qualified Z.Data.Vector.Base as V @@ -102,10 +101,10 @@ requestLineParser = do -- version vbs <- P.bytes "HTTP/" - majv <- P.satisfy isDigit + majv <- P.digit P.word8 DOT - minv <- P.satisfy isDigit - let !version = Version (fromIntegral $ majv - DIGIT_0) (fromIntegral $ minv - DIGIT_0) + minv <- P.digit + let !version = Version majv minv -- request line end P.word8 CARRIAGE_RETURN From a838d3c6f42f035f742e286c9c1c45e2bfd0cb2c Mon Sep 17 00:00:00 2001 From: zypeh <zypeh.geek@gmail.com> Date: Thu, 19 Aug 2021 23:49:44 +0800 Subject: [PATCH 02/13] WIP Z.Data.Client.Request for request builder --- Z/Data/HTTP/Client/Request.hs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 Z/Data/HTTP/Client/Request.hs diff --git a/Z/Data/HTTP/Client/Request.hs b/Z/Data/HTTP/Client/Request.hs new file mode 100644 index 0000000..596305d --- /dev/null +++ b/Z/Data/HTTP/Client/Request.hs @@ -0,0 +1,24 @@ +module Z.Data.HTTP.Client.Request where + +import Z.Data.HTTP.Request +import qualified Z.Data.Vector as V +import Z.IO (Source) +import Z.IO.Network + +type Host = (HostName, Maybe PortNumber) + +initialRequest :: Request +initialRequest = Request + { requestHost = V.empty + , requestSecure = False + , requestRemote = SocketAddrIPv4 ipv4Any 80 -- default http port number + + , requestMethod = GET + , requestVersion = Version 1 0 + , requestPathRaw = V.empty + , requestPathQuery = ([], V.empty) + + , requestHeaders = V.empty + + , requestBody = Left V.empty + } From 1e9b2b3f36dc721a88cfe974c332d415b24ccb69 Mon Sep 17 00:00:00 2001 From: zypeh <zypeh.geek@gmail.com> Date: Fri, 20 Aug 2021 00:39:26 +0800 Subject: [PATCH 03/13] WIP host parser --- Z/HTTP/Client.hs | 27 +++++++++++++++++++++++++++ z-http.cabal | 1 + 2 files changed, 28 insertions(+) create mode 100644 Z/HTTP/Client.hs diff --git a/Z/HTTP/Client.hs b/Z/HTTP/Client.hs new file mode 100644 index 0000000..beba853 --- /dev/null +++ b/Z/HTTP/Client.hs @@ -0,0 +1,27 @@ +module Z.HTTP.Client where + +import Z.Data.HTTP.Request +import Z.Data.HTTP.Client.Request +import Z.IO.Network + +import Z.Data.Text (Text) +import Z.Data.Parser (Parser) +import Z.Data.CBytes + +import qualified Z.Data.Parser as P +import qualified Z.Data.ASCII as C + +fromUrl :: Text -> Request +fromUrl = undefined + +-- fromHost :: Text -> Either P.ParseError Host +-- fromHost host = P.parse' parseHost (T) + +-- This should parse "www.google.com:80" but not "http://www.google.com:80" +parseHost :: Parser Host +parseHost = do + hostName <- P.takeTill (== C.COLON) + w <- P.peek + if w == C.COLON + then P.skipWord8 >> ((,) (fromBytes hostName) <$> (Just . PortNumber <$> P.int)) + else pure (fromBytes hostName, Nothing) diff --git a/z-http.cabal b/z-http.cabal index 41ef6b0..961e0e3 100644 --- a/z-http.cabal +++ b/z-http.cabal @@ -22,6 +22,7 @@ extra-source-files: CHANGELOG.md library exposed-modules: Z.Data.HTTP.Request Z.HTTP.Server + Z.HTTP.Client -- Modules included in this library but not exported. -- other-modules: From ea537345e084e1988226abd4bc11f6856b5fc9fa Mon Sep 17 00:00:00 2001 From: zypeh <zypeh.geek@gmail.com> Date: Sun, 22 Aug 2021 15:27:36 +0800 Subject: [PATCH 04/13] bump version --- z-http.cabal | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/z-http.cabal b/z-http.cabal index 961e0e3..ae50ac5 100644 --- a/z-http.cabal +++ b/z-http.cabal @@ -29,8 +29,8 @@ library build-depends: base >=4.14 , case-insensitive == 1.* - , Z-Data == 0.7.* - , Z-IO == 0.7.* + , Z-Data == 1.0.* + , Z-IO == 1.0.* -- Directories containing source files. hs-source-dirs: . From 630cbe3a395d7574e0d4bcd770fe6ed71d944d12 Mon Sep 17 00:00:00 2001 From: zypeh <zypeh.geek@gmail.com> Date: Sun, 22 Aug 2021 15:27:58 +0800 Subject: [PATCH 05/13] delete unused file --- Z/Data/HTTP/Client/Request.hs | 24 ------------------------ 1 file changed, 24 deletions(-) delete mode 100644 Z/Data/HTTP/Client/Request.hs diff --git a/Z/Data/HTTP/Client/Request.hs b/Z/Data/HTTP/Client/Request.hs deleted file mode 100644 index 596305d..0000000 --- a/Z/Data/HTTP/Client/Request.hs +++ /dev/null @@ -1,24 +0,0 @@ -module Z.Data.HTTP.Client.Request where - -import Z.Data.HTTP.Request -import qualified Z.Data.Vector as V -import Z.IO (Source) -import Z.IO.Network - -type Host = (HostName, Maybe PortNumber) - -initialRequest :: Request -initialRequest = Request - { requestHost = V.empty - , requestSecure = False - , requestRemote = SocketAddrIPv4 ipv4Any 80 -- default http port number - - , requestMethod = GET - , requestVersion = Version 1 0 - , requestPathRaw = V.empty - , requestPathQuery = ([], V.empty) - - , requestHeaders = V.empty - - , requestBody = Left V.empty - } From 73dc010a7de16d23e5c093fd2bfdee46c551d08d Mon Sep 17 00:00:00 2001 From: zypeh <zypeh.geek@gmail.com> Date: Sun, 22 Aug 2021 15:28:12 +0800 Subject: [PATCH 06/13] sendRequest --- Z/Data/HTTP/Request.hs | 6 +-- Z/HTTP/Client.hs | 111 +++++++++++++++++++++++++++++++++++++---- 2 files changed, 103 insertions(+), 14 deletions(-) diff --git a/Z/Data/HTTP/Request.hs b/Z/Data/HTTP/Request.hs index 0769a83..de618e6 100644 --- a/Z/Data/HTTP/Request.hs +++ b/Z/Data/HTTP/Request.hs @@ -13,7 +13,7 @@ import Z.Data.ASCII import Z.Data.PrimRef import Z.IO import Z.IO.Network - +import Z.IO.BIO (Source) data HTTPException = BadHeaderLine V.Bytes @@ -132,7 +132,7 @@ readRequest remoteAddr secure bi = do host <- readIORef hostRef when (V.null host) $ throwIO NoHostHeader - contentLen <- readPrimIORef contentLenRef + contentLen <- readPrimRef contentLenRef transferEncoding <- readIORef transferEncodingRef keepAlive <- readIORef connectionRef @@ -170,7 +170,7 @@ readRequest remoteAddr secure bi = do when (hdrK == "content-length") $ case P.parse' P.uint hdrV of - Right l -> writePrimIORef contentLenRef l + Right l -> writePrimRef contentLenRef l _ -> throwIO (BadHeaderLine hdr) when (hdrK == "transfer-encoding") $ diff --git a/Z/HTTP/Client.hs b/Z/HTTP/Client.hs index beba853..6d1c1bb 100644 --- a/Z/HTTP/Client.hs +++ b/Z/HTTP/Client.hs @@ -1,27 +1,116 @@ module Z.HTTP.Client where -import Z.Data.HTTP.Request -import Z.Data.HTTP.Client.Request import Z.IO.Network - +import Z.IO.Buffered +import Z.IO (withResource) +import Z.Data.HTTP.Request (Method (..)) import Z.Data.Text (Text) import Z.Data.Parser (Parser) -import Z.Data.CBytes - +import Z.Data.CBytes (fromBytes, buildCBytes) +import GHC.Word import qualified Z.Data.Parser as P import qualified Z.Data.ASCII as C +import qualified Z.Data.Vector as V +import qualified Z.Data.Builder as B + +type Path = V.Bytes + +data Request = Request + { reqMethod :: !Method + , reqPath :: !Path + , reqHost :: V.Bytes + , reqHeaders :: [(V.Bytes, V.Bytes)] + } -fromUrl :: Text -> Request -fromUrl = undefined +defaultRequest :: Request +defaultRequest = Request + { reqMethod = GET + , reqPath = V.empty + , reqHost = V.empty + , reqHeaders = [] + } --- fromHost :: Text -> Either P.ParseError Host --- fromHost host = P.parse' parseHost (T) +type Host = (HostName, Maybe PortNumber) -- This should parse "www.google.com:80" but not "http://www.google.com:80" -parseHost :: Parser Host -parseHost = do +parseHost' :: Parser Host +parseHost' = do hostName <- P.takeTill (== C.COLON) w <- P.peek if w == C.COLON then P.skipWord8 >> ((,) (fromBytes hostName) <$> (Just . PortNumber <$> P.int)) else pure (fromBytes hostName, Nothing) + +---------------------------- + +-- http-client +-- Builder model +-- https://github.com/snoyberg/http-client/blob/master/TUTORIAL.md#request-building + +-- * Record +-- Simple +-- https://hackage.haskell.org/package/http-conduit-2.3.8/docs/Network-HTTP-Conduit.html + +resolveDNS :: Host -> IO AddrInfo +resolveDNS (hostName, Just portNumber) = head <$> getAddrInfo Nothing hostName (buildCBytes . B.int $ portNumber) +resolveDNS (hostName, Nothing) = head <$> getAddrInfo Nothing hostName "http" + +pattern CRLF :: V.Bytes +pattern CRLF = "\r\n" + +requestToBytes :: Request -> V.Bytes +requestToBytes req = mconcat [method, " ", path, " ", version, CRLF, headers, CRLF] + where + method :: V.Bytes = "GET" -- TODO: find a way to serialise HTTP method from enum + path :: V.Bytes = reqPath req + version :: V.Bytes = "HTTP/1.1" + headers :: V.Bytes = "" + +data Response = Response + { responseVersion :: HttpVersion + , responseCode :: Word16 -- smallest unit that can contain 3 digits int + , responseMessage :: V.Bytes + , responseHeaders :: [(V.Bytes, V.Bytes)] + } deriving (Show) + +-- TODO: user defined chunksize? +sendRequest :: Request -> IO V.Bytes +sendRequest req = do + addr <- resolveDNS (fromBytes $ reqHost req, Nothing) + withResource (initTCPClient defaultTCPClientConfig { tcpRemoteAddr = addrAddress addr }) $ \tcp -> do + (i, o) <- newBufferedIO tcp + writeBuffer' o (requestToBytes req) + readBuffer i + +data HttpVersion = HttpVersion Int Int deriving (Show) + +httpParser :: Parser Response +httpParser = do + P.bytes "HTTP/" + maj <- P.digit + P.word8 C.DOT + min <- P.digit + P.skipSpaces + httpCode <- P.uint + P.skipSpaces + httpMsg <- P.takeWhile (/= C.CARRIAGE_RETURN) + P.bytes CRLF + !headers <- headersLoop [] + return $ Response (HttpVersion maj min) httpCode httpMsg (reverse headers) + + where + headersLoop :: [(V.Bytes, V.Bytes)] -> Parser [(V.Bytes, V.Bytes)] + headersLoop acc = do + w <- P.peek + case w of + C.CARRIAGE_RETURN -> do + P.bytes CRLF + return acc + _ -> do + headerKey <- P.takeWhile (/= C.COLON) + P.word8 C.COLON + P.skipSpaces + headerVal <- P.takeWhile (/= C.CARRIAGE_RETURN) + P.bytes CRLF + headersLoop $ (headerKey, headerVal) : acc -- Don't forget to reverse + From 0a4fecc398d1056784544b01e7c15a64d0900992 Mon Sep 17 00:00:00 2001 From: zypeh <zypeh.geek@gmail.com> Date: Sun, 22 Aug 2021 17:34:01 +0800 Subject: [PATCH 07/13] Using FlatMap as http header IR --- Z/Data/HTTP/Request.hs | 4 ++-- Z/HTTP/Client.hs | 45 +++++++++++++++++++++++++----------------- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/Z/Data/HTTP/Request.hs b/Z/Data/HTTP/Request.hs index de618e6..db989e6 100644 --- a/Z/Data/HTTP/Request.hs +++ b/Z/Data/HTTP/Request.hs @@ -37,7 +37,7 @@ data Method | OPTIONS | PATCH | CUSTOM_METHOD V.Bytes - deriving (Eq, Ord) + deriving (Eq, Ord, Show) instance T.Print Method where toUTF8BuilderP _ GET = "GET" @@ -53,7 +53,7 @@ instance T.Print Method where data Version = Version {-# UNPACK #-} !Int {-# UNPACK #-} !Int - deriving (Eq, Ord) + deriving (Eq, Ord, Show) instance T.Print Version where toUTF8BuilderP _ (Version maj min) = do diff --git a/Z/HTTP/Client.hs b/Z/HTTP/Client.hs index 6d1c1bb..f633fdd 100644 --- a/Z/HTTP/Client.hs +++ b/Z/HTTP/Client.hs @@ -1,17 +1,21 @@ module Z.HTTP.Client where import Z.IO.Network -import Z.IO.Buffered +import Z.IO.Buffered (newBufferedIO, readBuffer, writeBuffer') import Z.IO (withResource) -import Z.Data.HTTP.Request (Method (..)) +import Z.Data.HTTP.Request (Method (..), Version(..)) import Z.Data.Text (Text) import Z.Data.Parser (Parser) import Z.Data.CBytes (fromBytes, buildCBytes) -import GHC.Word +import GHC.Word (Word16) import qualified Z.Data.Parser as P import qualified Z.Data.ASCII as C import qualified Z.Data.Vector as V import qualified Z.Data.Builder as B +import qualified Z.Data.Text as T + +import Z.Data.Vector.FlatMap (FlatMap) +import qualified Z.Data.Vector.FlatMap as FlatMap type Path = V.Bytes @@ -64,25 +68,31 @@ requestToBytes req = mconcat [method, " ", path, " ", version, CRLF, headers, CR method :: V.Bytes = "GET" -- TODO: find a way to serialise HTTP method from enum path :: V.Bytes = reqPath req version :: V.Bytes = "HTTP/1.1" - headers :: V.Bytes = "" + headers :: V.Bytes = "" -- TODO: find a way to serialise Flatmap to V.Bytes + +type Headers = FlatMap V.Bytes V.Bytes + +emptyHeaders :: Headers +emptyHeaders = FlatMap.empty data Response = Response - { responseVersion :: HttpVersion + { responseVersion :: Version , responseCode :: Word16 -- smallest unit that can contain 3 digits int , responseMessage :: V.Bytes - , responseHeaders :: [(V.Bytes, V.Bytes)] - } deriving (Show) + , responseHeaders :: Headers + } deriving Show -- TODO: user defined chunksize? -sendRequest :: Request -> IO V.Bytes +sendRequest :: Request -> IO Response sendRequest req = do addr <- resolveDNS (fromBytes $ reqHost req, Nothing) withResource (initTCPClient defaultTCPClientConfig { tcpRemoteAddr = addrAddress addr }) $ \tcp -> do (i, o) <- newBufferedIO tcp writeBuffer' o (requestToBytes req) - readBuffer i - -data HttpVersion = HttpVersion Int Int deriving (Show) + buf <- readBuffer i + case P.parse' httpParser buf of + Left _ -> undefined + Right res -> pure res httpParser :: Parser Response httpParser = do @@ -95,11 +105,11 @@ httpParser = do P.skipSpaces httpMsg <- P.takeWhile (/= C.CARRIAGE_RETURN) P.bytes CRLF - !headers <- headersLoop [] - return $ Response (HttpVersion maj min) httpCode httpMsg (reverse headers) + !headers <- headersLoop emptyHeaders + return $ Response (Version maj min) httpCode httpMsg headers where - headersLoop :: [(V.Bytes, V.Bytes)] -> Parser [(V.Bytes, V.Bytes)] + headersLoop :: Headers -> Parser Headers headersLoop acc = do w <- P.peek case w of @@ -107,10 +117,9 @@ httpParser = do P.bytes CRLF return acc _ -> do - headerKey <- P.takeWhile (/= C.COLON) + key <- P.takeWhile (/= C.COLON) P.word8 C.COLON P.skipSpaces - headerVal <- P.takeWhile (/= C.CARRIAGE_RETURN) + val <- P.takeWhile (/= C.CARRIAGE_RETURN) P.bytes CRLF - headersLoop $ (headerKey, headerVal) : acc -- Don't forget to reverse - + headersLoop $ FlatMap.insert key val acc From fd073016ce37ef6c011da00966cb37a9756fc752 Mon Sep 17 00:00:00 2001 From: zypeh <zypeh.geek@gmail.com> Date: Wed, 25 Aug 2021 20:07:12 +0800 Subject: [PATCH 08/13] HTTP/major.minor version defined https://datatracker.ietf.org/doc/html/rfc2616#section-3.1 --- Z/Data/HTTP/Request.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Z/Data/HTTP/Request.hs b/Z/Data/HTTP/Request.hs index db989e6..8060f05 100644 --- a/Z/Data/HTTP/Request.hs +++ b/Z/Data/HTTP/Request.hs @@ -37,7 +37,7 @@ data Method | OPTIONS | PATCH | CUSTOM_METHOD V.Bytes - deriving (Eq, Ord, Show) + deriving (Eq, Ord) instance T.Print Method where toUTF8BuilderP _ GET = "GET" @@ -57,7 +57,7 @@ data Version = Version {-# UNPACK #-} !Int {-# UNPACK #-} !Int instance T.Print Version where toUTF8BuilderP _ (Version maj min) = do - "HTTP" + "HTTP/" B.int maj B.encodePrim DOT B.int min From b3b819c08080263469d2141c86ca5f7db87652d9 Mon Sep 17 00:00:00 2001 From: zypeh <zypeh.geek@gmail.com> Date: Wed, 25 Aug 2021 20:08:05 +0800 Subject: [PATCH 09/13] Bytes request builder --- Z/HTTP/Client.hs | 38 ++++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/Z/HTTP/Client.hs b/Z/HTTP/Client.hs index f633fdd..fc7c921 100644 --- a/Z/HTTP/Client.hs +++ b/Z/HTTP/Client.hs @@ -20,17 +20,19 @@ import qualified Z.Data.Vector.FlatMap as FlatMap type Path = V.Bytes data Request = Request - { reqMethod :: !Method - , reqPath :: !Path + { reqMethod :: Method + , reqPath :: Path , reqHost :: V.Bytes + , reqVersion :: Version , reqHeaders :: [(V.Bytes, V.Bytes)] } defaultRequest :: Request defaultRequest = Request { reqMethod = GET - , reqPath = V.empty + , reqPath = "/" , reqHost = V.empty + , reqVersion = Version 1 1 , reqHeaders = [] } @@ -62,13 +64,33 @@ resolveDNS (hostName, Nothing) = head <$> getAddrInfo Nothing hostName "http" pattern CRLF :: V.Bytes pattern CRLF = "\r\n" +-- build lazily +buildHeaders :: [(V.Bytes, V.Bytes)] -> B.Builder () +buildHeaders = foldl buildHeader "" + where + buildHeader :: B.Builder () -> (V.Bytes, V.Bytes) -> B.Builder () + buildHeader b (headerKey, headerVal) = B.append b $ do + B.bytes headerKey + B.word8 C.COLON + B.word8 C.SPACE + B.bytes headerVal + B.bytes CRLF + requestToBytes :: Request -> V.Bytes -requestToBytes req = mconcat [method, " ", path, " ", version, CRLF, headers, CRLF] +requestToBytes req = B.build $ do + B.bytes method + B.encodePrim C.SPACE + B.bytes path + B.encodePrim C.SPACE + B.bytes version + B.bytes CRLF + headers + B.bytes CRLF where - method :: V.Bytes = "GET" -- TODO: find a way to serialise HTTP method from enum - path :: V.Bytes = reqPath req - version :: V.Bytes = "HTTP/1.1" - headers :: V.Bytes = "" -- TODO: find a way to serialise Flatmap to V.Bytes + method :: V.Bytes = T.toUTF8Bytes (reqMethod req) + path :: V.Bytes = reqPath req + version :: V.Bytes = T.toUTF8Bytes (reqVersion req) + headers = buildHeaders $ ("Host", reqHost req) : reqHeaders req type Headers = FlatMap V.Bytes V.Bytes From a9a0f84a8353204b6fdd0b2ec9cee6fe62642818 Mon Sep 17 00:00:00 2001 From: zypeh <zypeh.geek@gmail.com> Date: Wed, 25 Aug 2021 21:02:46 +0800 Subject: [PATCH 10/13] add some strictness in buildHeaders function --- Z/HTTP/Client.hs | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/Z/HTTP/Client.hs b/Z/HTTP/Client.hs index fc7c921..5f367bc 100644 --- a/Z/HTTP/Client.hs +++ b/Z/HTTP/Client.hs @@ -1,18 +1,27 @@ module Z.HTTP.Client where +import GHC.Word (Word16) +import Data.Foldable (foldl') import Z.IO.Network + ( getAddrInfo + , defaultTCPClientConfig + , initTCPClient + , AddrInfo(addrAddress) + , HostName + , PortNumber(..) + , TCPClientConfig(tcpRemoteAddr) + ) import Z.IO.Buffered (newBufferedIO, readBuffer, writeBuffer') import Z.IO (withResource) import Z.Data.HTTP.Request (Method (..), Version(..)) import Z.Data.Text (Text) import Z.Data.Parser (Parser) import Z.Data.CBytes (fromBytes, buildCBytes) -import GHC.Word (Word16) -import qualified Z.Data.Parser as P -import qualified Z.Data.ASCII as C -import qualified Z.Data.Vector as V +import qualified Z.Data.Parser as P +import qualified Z.Data.ASCII as C +import qualified Z.Data.Vector as V import qualified Z.Data.Builder as B -import qualified Z.Data.Text as T +import qualified Z.Data.Text as T import Z.Data.Vector.FlatMap (FlatMap) import qualified Z.Data.Vector.FlatMap as FlatMap @@ -66,7 +75,7 @@ pattern CRLF = "\r\n" -- build lazily buildHeaders :: [(V.Bytes, V.Bytes)] -> B.Builder () -buildHeaders = foldl buildHeader "" +buildHeaders = foldl' buildHeader "" where buildHeader :: B.Builder () -> (V.Bytes, V.Bytes) -> B.Builder () buildHeader b (headerKey, headerVal) = B.append b $ do From f33388f2aedb460a1ef72e9b85ac922d22798102 Mon Sep 17 00:00:00 2001 From: zypeh <zypeh.geek@gmail.com> Date: Mon, 30 Aug 2021 17:53:01 +0800 Subject: [PATCH 11/13] builder pattern --- Z/HTTP/Client.hs | 99 ++++++++++++++++++++++++++++++------------------ 1 file changed, 62 insertions(+), 37 deletions(-) diff --git a/Z/HTTP/Client.hs b/Z/HTTP/Client.hs index 5f367bc..89f88af 100644 --- a/Z/HTTP/Client.hs +++ b/Z/HTTP/Client.hs @@ -1,7 +1,13 @@ +{-# LANGUAGE StrictData #-} -- Give an error instead of warnings in case + -- user forgets to initialize some fields +{-# LANGUAGE RecordWildCards #-} + module Z.HTTP.Client where import GHC.Word (Word16) -import Data.Foldable (foldl') +import Data.Foldable (foldr) +import Data.Functor.Identity (Identity) +import Data.Function ((&)) import Z.IO.Network ( getAddrInfo , defaultTCPClientConfig @@ -16,7 +22,7 @@ import Z.IO (withResource) import Z.Data.HTTP.Request (Method (..), Version(..)) import Z.Data.Text (Text) import Z.Data.Parser (Parser) -import Z.Data.CBytes (fromBytes, buildCBytes) +import Z.Data.CBytes (fromBytes, buildCBytes, toBytes) import qualified Z.Data.Parser as P import qualified Z.Data.ASCII as C import qualified Z.Data.Vector as V @@ -36,6 +42,18 @@ data Request = Request , reqHeaders :: [(V.Bytes, V.Bytes)] } +type Headers = FlatMap V.Bytes V.Bytes + +emptyHeaders :: Headers +emptyHeaders = FlatMap.empty + +data Response = Response + { responseVersion :: Version + , responseCode :: Word16 -- smallest unit that can contain 3 digits int + , responseMessage :: V.Bytes + , responseHeaders :: Headers + } deriving Show + defaultRequest :: Request defaultRequest = Request { reqMethod = GET @@ -47,25 +65,6 @@ defaultRequest = Request type Host = (HostName, Maybe PortNumber) --- This should parse "www.google.com:80" but not "http://www.google.com:80" -parseHost' :: Parser Host -parseHost' = do - hostName <- P.takeTill (== C.COLON) - w <- P.peek - if w == C.COLON - then P.skipWord8 >> ((,) (fromBytes hostName) <$> (Just . PortNumber <$> P.int)) - else pure (fromBytes hostName, Nothing) - ----------------------------- - --- http-client --- Builder model --- https://github.com/snoyberg/http-client/blob/master/TUTORIAL.md#request-building - --- * Record --- Simple --- https://hackage.haskell.org/package/http-conduit-2.3.8/docs/Network-HTTP-Conduit.html - resolveDNS :: Host -> IO AddrInfo resolveDNS (hostName, Just portNumber) = head <$> getAddrInfo Nothing hostName (buildCBytes . B.int $ portNumber) resolveDNS (hostName, Nothing) = head <$> getAddrInfo Nothing hostName "http" @@ -75,13 +74,12 @@ pattern CRLF = "\r\n" -- build lazily buildHeaders :: [(V.Bytes, V.Bytes)] -> B.Builder () -buildHeaders = foldl' buildHeader "" +buildHeaders = foldr buildHeader "" where - buildHeader :: B.Builder () -> (V.Bytes, V.Bytes) -> B.Builder () - buildHeader b (headerKey, headerVal) = B.append b $ do + buildHeader :: (V.Bytes, V.Bytes) -> B.Builder () -> B.Builder () + buildHeader (headerKey, headerVal) b = B.append b $ do B.bytes headerKey B.word8 C.COLON - B.word8 C.SPACE B.bytes headerVal B.bytes CRLF @@ -101,18 +99,6 @@ requestToBytes req = B.build $ do version :: V.Bytes = T.toUTF8Bytes (reqVersion req) headers = buildHeaders $ ("Host", reqHost req) : reqHeaders req -type Headers = FlatMap V.Bytes V.Bytes - -emptyHeaders :: Headers -emptyHeaders = FlatMap.empty - -data Response = Response - { responseVersion :: Version - , responseCode :: Word16 -- smallest unit that can contain 3 digits int - , responseMessage :: V.Bytes - , responseHeaders :: Headers - } deriving Show - -- TODO: user defined chunksize? sendRequest :: Request -> IO Response sendRequest req = do @@ -154,3 +140,42 @@ httpParser = do val <- P.takeWhile (/= C.CARRIAGE_RETURN) P.bytes CRLF headersLoop $ FlatMap.insert key val acc + +-- This should parse "www.google.com:80" but not "http://www.google.com:80" +parseHost :: Parser Host +parseHost = do + hostName <- P.takeTill (== C.COLON) + w <- P.peek + if w == C.COLON + then P.skipWord8 >> ((,) (fromBytes hostName) <$> (Just . PortNumber <$> P.int)) + else pure (fromBytes hostName, Nothing) + +fromHost :: Text -> Request +fromHost host = let host' = T.toUTF8Bytes host in + case P.parse' parseHost (T.toUTF8Bytes host) of + Left err -> error (show err) + Right (hostName, Nothing) -> defaultRequest { reqHost = toBytes hostName } + Right (hostName, Just port) -> defaultRequest { reqHost = toBytes hostName <> ":" <> T.toUTF8Bytes port } + +fromIpAddr :: Text -> Request +fromIpAddr = undefined + +f :: Request +f = fromHost "www.google.com" + & setPath "/url" + & setHeadears [("X-Powered-By", "Z-HTTP-Client")] + +setMethod :: Method -> Request -> Request +setMethod method req@Request{..} = req { reqMethod = method } + +setPath :: V.Bytes -> Request -> Request +setPath path req@Request{..} = req { reqPath = path } + +-- setHost :: V.Bytes -> Request -> Request +-- setHost host req@Request{..} = req { reqHost = host } + +setVersion :: Version -> Request -> Request +setVersion version req@Request{..} = req { reqVersion = version } + +setHeadears :: [(V.Bytes, V.Bytes)] -> Request -> Request +setHeadears headers req@Request{..} = req { reqHeaders = headers } From 61253a9edc4f7c5749d6f38d3715d845ff14c4bb Mon Sep 17 00:00:00 2001 From: zypeh <zypeh.geek@gmail.com> Date: Mon, 6 Sep 2021 10:22:32 +0800 Subject: [PATCH 12/13] misc changes --- Z/HTTP/Client.hs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Z/HTTP/Client.hs b/Z/HTTP/Client.hs index 89f88af..b61e4ca 100644 --- a/Z/HTTP/Client.hs +++ b/Z/HTTP/Client.hs @@ -160,10 +160,10 @@ fromHost host = let host' = T.toUTF8Bytes host in fromIpAddr :: Text -> Request fromIpAddr = undefined -f :: Request -f = fromHost "www.google.com" - & setPath "/url" - & setHeadears [("X-Powered-By", "Z-HTTP-Client")] +-- | f :: Request +-- | f = fromHost "www.google.com" +-- | & setPath "/url" +-- | & setHeadears [("X-Powered-By", "Z-HTTP-Client")] setMethod :: Method -> Request -> Request setMethod method req@Request{..} = req { reqMethod = method } @@ -171,9 +171,6 @@ setMethod method req@Request{..} = req { reqMethod = method } setPath :: V.Bytes -> Request -> Request setPath path req@Request{..} = req { reqPath = path } --- setHost :: V.Bytes -> Request -> Request --- setHost host req@Request{..} = req { reqHost = host } - setVersion :: Version -> Request -> Request setVersion version req@Request{..} = req { reqVersion = version } From efad1716b2a6b8d6ef3ae40dfc374ca9958aebd8 Mon Sep 17 00:00:00 2001 From: zypeh <zypeh.geek@gmail.com> Date: Sat, 18 Sep 2021 01:13:58 +0800 Subject: [PATCH 13/13] parseUri --- Z/HTTP/Client.hs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Z/HTTP/Client.hs b/Z/HTTP/Client.hs index b61e4ca..a9744a3 100644 --- a/Z/HTTP/Client.hs +++ b/Z/HTTP/Client.hs @@ -42,6 +42,8 @@ data Request = Request , reqHeaders :: [(V.Bytes, V.Bytes)] } +data Protocol = HTTP | HTTPS deriving Show + type Headers = FlatMap V.Bytes V.Bytes emptyHeaders :: Headers @@ -141,6 +143,14 @@ httpParser = do P.bytes CRLF headersLoop $ FlatMap.insert key val acc +parseUri :: Parser (Protocol, Host) +parseUri = do + P.bytes "http" + proto <- P.peek + if proto == C.LETTER_S + then P.skipWord8 >> P.bytes "://" >> ((,) HTTPS <$> parseHost) + else P.bytes "://" >> ((,) HTTP <$> parseHost) + -- This should parse "www.google.com:80" but not "http://www.google.com:80" parseHost :: Parser Host parseHost = do