on master help sign in
Merge branch 'push'
commit3a21f0b4e188463fedef1b8f225bb07666319a9d authorKazu Yamamoto <kazu@iij.ad.jp> dateMon 4 Jul 2016, 13:09 (10 years ago) parent47996f95af708c
12 files changed+684−143
wai-http2-extra/LICENSE+20 wai-http2-extra/Network/Wai/Middleware/Push/Referer.hs+183 wai-http2-extra/test/doctests.hs+4 wai-http2-extra/wai-http2-extra.cabal+35 warp/Network/Wai/Handler/Warp.hs+29−3 warp/Network/Wai/Handler/Warp/HTTP2/HPACK.hs+32−18 warp/Network/Wai/Handler/Warp/HTTP2/Receiver.hs+66−55 warp/Network/Wai/Handler/Warp/HTTP2/Request.hs+41−2 warp/Network/Wai/Handler/Warp/HTTP2/Sender.hs+88−36 warp/Network/Wai/Handler/Warp/HTTP2/Types.hs+85−10 warp/Network/Wai/Handler/Warp/HTTP2/Worker.hs+96−19 warp/Network/Wai/Handler/Warp/Settings.hs+5
wai-http2-extra/LICENSEnew file+20
@@ -0,0 +1,20 @@
1+Copyright (c) 2016 Kazu Yamamoto, http://www.yesodweb.com/
2+
3+Permission is hereby granted, free of charge, to any person obtaining
4+a copy of this software and associated documentation files (the
5+"Software"), to deal in the Software without restriction, including
6+without limitation the rights to use, copy, modify, merge, publish,
7+distribute, sublicense, and/or sell copies of the Software, and to
8+permit persons to whom the Software is furnished to do so, subject to
9+the following conditions:
10+
11+The above copyright notice and this permission notice shall be
12+included in all copies or substantial portions of the Software.
13+
14+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
wai-http2-extra/Network/Wai/Middleware/Push/Referer.hsnew file+183
@@ -0,0 +1,183 @@
1+{-# LANGUAGE BangPatterns, OverloadedStrings #-}
2+
3+module Network.Wai.Middleware.Push.Referer (
4+ pushOnReferer
5+ , defaultMakePushPromise
6+ ) where
7+
8+import Control.Monad (when)
9+import Control.Reaper
10+import Data.ByteString (ByteString)
11+import qualified Data.ByteString as BS
12+import Data.ByteString.Internal (ByteString(..), memchr)
13+import Data.Map (Map)
14+import qualified Data.Map.Strict as M
15+import Data.Maybe (isNothing)
16+import Data.Set (Set)
17+import qualified Data.Set as S
18+import Data.Word (Word8)
19+import Data.Word8
20+import Foreign.ForeignPtr (withForeignPtr, ForeignPtr)
21+import Foreign.Ptr (Ptr, plusPtr, minusPtr, nullPtr)
22+import Foreign.Storable (peek)
23+import Network.Wai
24+import Network.Wai.Handler.Warp
25+import Network.Wai.Internal (Response(..))
26+import System.IO.Unsafe (unsafePerformIO)
27+
28+-- $setup
29+-- >>> :set -XOverloadedStrings
30+
31+type URLPath = ByteString
32+
33+type Cache = Map URLPath (Set PushPromise)
34+
35+emptyCache :: Cache
36+emptyCache = M.empty
37+
38+cacheReaper :: Reaper Cache (URLPath,PushPromise)
39+cacheReaper = unsafePerformIO $ mkReaper settings
40+{-# NOINLINE cacheReaper #-}
41+
42+settings :: ReaperSettings Cache (URLPath,PushPromise)
43+settings = defaultReaperSettings {
44+ reaperAction = \_ -> return (\_ -> emptyCache)
45+ , reaperCons = insert
46+ , reaperNull = M.null
47+ , reaperEmpty = emptyCache
48+ }
49+
50+insert :: (URLPath,PushPromise) -> Cache -> Cache
51+insert (path,pp) m = M.alter ins path m
52+ where
53+ ins Nothing = Just $ S.singleton pp
54+ ins (Just set) = Just $ S.insert pp set
55+
56+pushOnReferer :: (URLPath -> URLPath -> FilePath -> IO (Maybe PushPromise))
57+ -> Middleware
58+pushOnReferer func app req sendResponse = app req $ \res -> do
59+ let !path = rawPathInfo req
60+ m <- reaperRead cacheReaper
61+ case M.lookup path m of
62+ Nothing -> case requestHeaderReferer req of
63+ Nothing -> return ()
64+ Just referer -> case res of
65+ ResponseFile _ _ file Nothing -> do
66+ (mauth,refPath) <- parseUrl referer
67+ when (isNothing mauth
68+ || requestHeaderHost req == mauth) $ do
69+ when (path /= refPath) $ do -- just in case
70+ mpp <- func refPath path file
71+ case mpp of
72+ Nothing -> return ()
73+ Just pp -> reaperAdd cacheReaper (refPath,pp)
74+ _ -> return ()
75+ Just pset -> do
76+ let !ps = S.toList pset
77+ !h2d = defaultHTTP2Data { http2dataPushPromise = ps}
78+ setHTTP2Data req (Just h2d)
79+ sendResponse res
80+
81+defaultMakePushPromise :: URLPath -- ^ path in referer
82+ -> URLPath -- ^ path to be pushed
83+ -> FilePath -- ^ file to be pushed
84+ -> IO (Maybe PushPromise)
85+defaultMakePushPromise refPath path file
86+ | isHTML refPath = case getCT path of
87+ Nothing -> return Nothing
88+ Just ct -> do
89+ let pp = defaultPushPromise {
90+ promisedPath = path
91+ , promisedFile = file
92+ , promisedResponseHeaders = [("content-type", ct)
93+ ,("x-http2-push", refPath)]
94+ }
95+ return $ Just pp
96+ | otherwise = return Nothing
97+
98+getCT :: URLPath -> Maybe ByteString
99+getCT p
100+ | ".js" `BS.isSuffixOf` p = Just "application/javascript"
101+ | ".css" `BS.isSuffixOf` p = Just "text/css"
102+ | otherwise = Nothing
103+
104+isHTML :: URLPath -> Bool
105+isHTML p = ("/" `BS.isSuffixOf` p)
106+ || (".html" `BS.isSuffixOf` p)
107+ || (".htm" `BS.isSuffixOf` p)
108+
109+-- |
110+--
111+-- >>> parseUrl ""
112+-- (Nothing,"")
113+-- >>> parseUrl "/"
114+-- (Nothing,"/")
115+-- >>> parseUrl "ht"
116+-- (Nothing,"")
117+-- >>> parseUrl "http://example.com/foo/bar/"
118+-- (Just "example.com","/foo/bar/")
119+-- >>> parseUrl "https://www.example.com/path/to/dir/"
120+-- (Just "www.example.com","/path/to/dir/")
121+-- >>> parseUrl "http://www.example.com:8080/path/to/dir/"
122+-- (Just "www.example.com:8080","/path/to/dir/")
123+-- >>> parseUrl "//www.example.com:8080/path/to/dir/"
124+-- (Just "www.example.com:8080","/path/to/dir/")
125+-- >>> parseUrl "/path/to/dir/"
126+-- (Nothing,"/path/to/dir/")
127+
128+parseUrl :: ByteString -> IO (Maybe ByteString, URLPath)
129+parseUrl bs@(PS fptr0 off len)
130+ | len == 0 = return (Nothing, "")
131+ | len == 1 = return (Nothing, bs)
132+ | otherwise = withForeignPtr fptr0 $ \ptr0 -> do
133+ let begptr = ptr0 `plusPtr` off
134+ limptr = begptr `plusPtr` len
135+ parseUrl' fptr0 ptr0 begptr limptr len
136+
137+parseUrl' :: ForeignPtr Word8 -> Ptr Word8 -> Ptr Word8 -> Ptr Word8 -> Int
138+ -> IO (Maybe ByteString, URLPath)
139+parseUrl' fptr0 ptr0 begptr limptr len0 = do
140+ w0 <- peek begptr
141+ if w0 == _slash then do
142+ w1 <- peek $ begptr `plusPtr` 1
143+ if w1 == _slash then
144+ doubleSlashed begptr len0
145+ else
146+ slashed begptr len0 Nothing
147+ else do
148+ colonptr <- memchr begptr _colon $ fromIntegral len0
149+ if colonptr == nullPtr then
150+ return (Nothing, "")
151+ else do
152+ let !authptr = colonptr `plusPtr` 1
153+ doubleSlashed authptr (limptr `minusPtr` authptr)
154+ where
155+ -- // / ?
156+ doubleSlashed :: Ptr Word8 -> Int -> IO (Maybe ByteString, URLPath)
157+ doubleSlashed ptr len
158+ | len < 2 = return (Nothing, "")
159+ | otherwise = do
160+ let ptr1 = ptr `plusPtr` 2
161+ pathptr <- memchr ptr1 _slash $ fromIntegral len
162+ if pathptr == nullPtr then
163+ return (Nothing, "")
164+ else do
165+ let !auth = bs ptr0 ptr1 pathptr
166+ slashed pathptr (limptr `minusPtr` pathptr) (Just auth)
167+
168+ -- / ?
169+ slashed :: Ptr Word8 -> Int -> Maybe ByteString -> IO (Maybe ByteString, URLPath)
170+ slashed ptr len mauth = do
171+ questionptr <- memchr ptr _question $ fromIntegral len
172+ if questionptr == nullPtr then do
173+ let !path = bs ptr0 ptr limptr
174+ return (mauth, path)
175+ else do
176+ let !path = bs ptr0 ptr questionptr
177+ return (mauth, path)
178+ bs p0 p1 p2 = path
179+ where
180+ !off = p1 `minusPtr` p0
181+ !siz = p2 `minusPtr` p1
182+ !path = PS fptr0 off siz
183+
wai-http2-extra/test/doctests.hsnew file+4
@@ -0,0 +1,4 @@
1+import Test.DocTest
2+
3+main :: IO ()
4+main = doctest ["Network"]
wai-http2-extra/wai-http2-extra.cabalnew file+35
@@ -0,0 +1,35 @@
1+Name: wai-http2-extra
2+Version: 0.0.0
3+Synopsis: WAI utilities for HTTP/2
4+License: MIT
5+License-file: LICENSE
6+Author: Kazu Yamamoto
7+Maintainer: kazu@iij.ad.jp
8+Homepage: http://github.com/yesodweb/wai
9+Category: Web
10+Build-Type: Simple
11+Cabal-Version: >=1.8
12+Stability: Stable
13+Description: WAI utilities for HTTP/2
14+
15+Library
16+ Build-Depends: base >= 3 && < 5
17+ , auto-update >= 0.1.3 && < 0.2
18+ , bytestring
19+ , containers
20+ , wai
21+ , warp
22+ , word8
23+ Exposed-modules: Network.Wai.Middleware.Push.Referer
24+ Ghc-Options: -Wall
25+
26+Test-Suite doctest
27+ Type: exitcode-stdio-1.0
28+ HS-Source-Dirs: test
29+ Ghc-Options: -threaded -Wall
30+ Main-Is: doctests.hs
31+ Build-Depends: base
32+ , doctest >= 0.10.1
33+Source-Repository head
34+ Type: git
35+ Location: git://github.com/yesodweb/wai.git
warp/Network/Wai/Handler/Warp.hs+29−3
@@ -70,6 +70,7 @@ module Network.Wai.Handler.Warp (
7070 , setSlowlorisSize
7171 , setHTTP2Disabled
7272 , setLogger
73+ , setServerPushLogger
7374 , setGracefulShutdownTimeout
7475 -- ** Getters
7576 , getPort
@@ -99,6 +100,20 @@ module Network.Wai.Handler.Warp (
99100 , openFreePort
100101 -- * Version
101102 , warpVersion
103+ -- * HTTP/2
104+ -- ** HTTP2 data
105+ , HTTP2Data
106+ , http2dataPushPromise
107+ , defaultHTTP2Data
108+ , getHTTP2Data
109+ , setHTTP2Data
110+ -- ** Push promise
111+ , PushPromise
112+ , promisedPath
113+ , promisedFile
114+ , promisedResponseHeaders
115+ , promisedWeight
116+ , defaultPushPromise
102117 ) where
103118
104119import Control.Exception (SomeException, throwIO)
@@ -114,6 +129,8 @@ import Network.Wai.Handler.Warp.Request
114129import Network.Wai.Handler.Warp.Response (warpVersion)
115130import Network.Wai.Handler.Warp.Run
116131import Network.Wai.Handler.Warp.Settings
132+import Network.Wai.Handler.Warp.HTTP2.Request (getHTTP2Data, setHTTP2Data)
133+import Network.Wai.Handler.Warp.HTTP2.Types
117134import Network.Wai.Handler.Warp.Timeout
118135import Network.Wai.Handler.Warp.Types hiding (getFileInfo)
119136import Network.Wai.Handler.Warp.WithApplication
@@ -357,13 +374,22 @@ setSlowlorisSize x y = y { settingsSlowlorisSize = x }
357374setHTTP2Disabled :: Settings -> Settings
358375setHTTP2Disabled y = y { settingsHTTP2Enabled = False }
359376
360−-- | Setting a log function. `Integer` is the body length of a response.
377+-- | Setting a log function.
361378--
362379-- Since 3.X.X
363−setLogger :: (Request -> H.Status -> Maybe Integer -> IO ())
364− -> Settings -> Settings
380+setLogger :: (Request -> H.Status -> Maybe Integer -> IO ()) -- ^ request, status, maybe file-size
381+ -> Settings
382+ -> Settings
365383setLogger lgr y = y { settingsLogger = lgr }
366384
385+-- | Setting a log function for HTTP/2 server push.
386+--
387+-- Since: 3.2.7
388+setServerPushLogger :: (Request -> ByteString -> Integer -> IO ()) -- ^ request, path, file-size
389+ -> Settings
390+ -> Settings
391+setServerPushLogger lgr y = y { settingsServerPushLogger = lgr }
392+
367393-- | Set the graceful shutdown timeout. A timeout of `Nothing' will
368394-- wait indefinitely, and a number, if provided, will be treated as seconds
369395-- to wait for requests to finish, before shutting down the server entirely.
warp/Network/Wai/Handler/Warp/HTTP2/HPACK.hs+32−18
@@ -6,6 +6,7 @@ module Network.Wai.Handler.Warp.HTTP2.HPACK (
66 , hpackEncodeHeaderLoop
77 , hpackDecodeHeader
88 , just
9+ , addNecessaryHeaders
910 ) where
1011
1112import qualified Control.Exception as E
@@ -13,7 +14,6 @@ import Control.Monad (unless)
1314import Data.ByteString (ByteString)
1415import Network.HPACK hiding (Buffer)
1516import Network.HPACK.Token
16−import qualified Network.HTTP.Types as H
1717import Network.HTTP2
1818import Network.Wai.Handler.Warp.HTTP2.Types
1919import Network.Wai.Handler.Warp.PackInt
@@ -23,31 +23,45 @@ import Network.Wai.Handler.Warp.Types
2323-- $setup
2424-- >>> :set -XOverloadedStrings
2525
26+----------------------------------------------------------------
27+
28+{-# INLINE addHeader #-}
29+addHeader :: Token -> ByteString -> ValueTable -> TokenHeaderList -> TokenHeaderList
30+addHeader t v tbl ths = case getHeaderValue t tbl of
31+ Nothing -> (t,v) : ths
32+ Just _ -> ths
33+
34+addNecessaryHeaders :: Context
35+ -> Rspn
36+ -> InternalInfo
37+ -> S.Settings
38+ -> IO TokenHeaderList
39+addNecessaryHeaders Context{..} rspn ii settings = do
40+ date <- getDate ii
41+ let !s = rspnStatus rspn
42+ !status = packStatus s
43+ !defServer = S.settingsServerName settings
44+ (!ths0,tbl) = rspnHeaders rspn
45+ !ths1 = addHeader tokenServer defServer tbl ths0
46+ !ths2 = addHeader tokenDate date tbl ths1
47+ !ths3 = (tokenStatus, status) : ths2
48+ return ths3
49+
50+----------------------------------------------------------------
51+
2652strategy :: EncodeStrategy
2753strategy = EncodeStrategy { compressionAlgo = Linear, useHuffman = False }
2854
2955-- Set-Cookie: contains only one cookie value.
3056-- So, we don't need to split it.
3157hpackEncodeHeader :: Context -> Buffer -> BufSize
32− -> InternalInfo -> S.Settings
33− -> H.Status -> (TokenHeaderList,ValueTable)
58+ -> TokenHeaderList
3459 -> IO (TokenHeaderList, Int)
35−hpackEncodeHeader Context{..} buf siz ii settings s (ths0,tbl) = do
36− let !defServer = S.settingsServerName settings
37− !ths1 = addHeader tokenServer defServer tbl ths0
38− date <- getDate ii
39− let !ths2 = addHeader tokenDate date tbl ths1
40− !status = packStatus s
41− !ths3 = (tokenStatus, status) : ths2
42− encodeTokenHeader buf siz strategy True encodeDynamicTable ths3
43−
44−{-# INLINE addHeader #-}
45−addHeader :: Token -> ByteString -> ValueTable -> TokenHeaderList -> TokenHeaderList
46−addHeader t v tbl ths = case getHeaderValue t tbl of
47− Nothing -> (t,v) : ths
48− Just _ -> ths
60+hpackEncodeHeader Context{..} buf siz ths =
61+ encodeTokenHeader buf siz strategy True encodeDynamicTable ths
4962
50−hpackEncodeHeaderLoop :: Context -> Buffer -> BufSize -> TokenHeaderList
63+hpackEncodeHeaderLoop :: Context -> Buffer -> BufSize
64+ -> TokenHeaderList
5165 -> IO (TokenHeaderList, Int)
5266hpackEncodeHeaderLoop Context{..} buf siz hs =
5367 encodeTokenHeader buf siz strategy False encodeDynamicTable hs
warp/Network/Wai/Handler/Warp/HTTP2/Receiver.hs+66−55
@@ -36,13 +36,13 @@ frameReceiver ctx mkreq recvN = loop 0 `E.catch` sendGoaway
3636 , streamTable
3737 , concurrency
3838 , continued
39− , currentStreamId
39+ , clientStreamId
4040 , inputQ
4141 , controlQ
4242 } = ctx
4343 sendGoaway e
4444 | Just (ConnectionError err msg) <- E.fromException e = do
45− csid <- readIORef currentStreamId
45+ csid <- readIORef clientStreamId
4646 let !frame = goawayFrame csid err msg
4747 enqueueControl controlQ $ CGoaway frame
4848 | otherwise = return ()
@@ -64,8 +64,10 @@ frameReceiver ctx mkreq recvN = loop 0 `E.catch` sendGoaway
6464 cont <- processStreamGuardingError $ decodeFrameHeader hd
6565 when cont $ loop (n + 1)
6666
67− processStreamGuardingError (_, FrameHeader{streamId})
68− | isResponse streamId = E.throwIO $ ConnectionError ProtocolError "stream id should be odd"
67+ processStreamGuardingError (fid, FrameHeader{streamId})
68+ | isResponse streamId &&
69+ (fid `notElem` [FramePriority,FrameRSTStream,FrameWindowUpdate]) =
70+ E.throwIO $ ConnectionError ProtocolError "stream id should be odd"
6971 processStreamGuardingError (FrameUnknown _, FrameHeader{payloadLength}) = do
7072 mx <- readIORef continued
7173 case mx of
@@ -100,38 +102,46 @@ frameReceiver ctx mkreq recvN = loop 0 `E.catch` sendGoaway
100102 control ftyp header pl ctx
101103 | otherwise = do
102104 checkContinued
103− !strm@Stream{streamState,streamPrecedence} <- getStream
105+ !mstrm <- getStream
104106 pl <- recvN payloadLength
105− state <- readIORef streamState
106− state' <- stream ftyp header pl ctx state strm
107− case state' of
108− Open (NoBody tbl@(_,reqvt) pri) -> do
109− resetContinued
110− let mcl = readInt <$> getHeaderValue tokenContentLength reqvt
111− when (just mcl (== (0 :: Int))) $
112− E.throwIO $ StreamError ProtocolError streamId
113− writeIORef streamPrecedence $ toPrecedence pri
114− writeIORef streamState HalfClosed
115− let (!req, !ii) = mkreq tbl (return "")
116− atomically $ writeTQueue inputQ $ Input strm req reqvt ii
117− Open (HasBody tbl@(_,reqvt) pri) -> do
118− resetContinued
119− q <- newTQueueIO
120− let !mcl = readInt <$> getHeaderValue tokenContentLength reqvt
121− writeIORef streamPrecedence $ toPrecedence pri
122− bodyLength <- newIORef 0
123− writeIORef streamState $ Open (Body q mcl bodyLength)
124− readQ <- newReadBody q
125− bodySource <- mkSource readQ
126− let (!req, !ii) = mkreq tbl (readSource bodySource)
127− atomically $ writeTQueue inputQ $ Input strm req reqvt ii
128− s@(Open Continued{}) -> do
129− setContinued
130− writeIORef streamState s
131− s -> do -- Idle, Open Body, HalfClosed, Closed
132− resetContinued
133− writeIORef streamState s
134− return True
107+ case mstrm of
108+ Nothing -> do
109+ -- for h2spec only
110+ when (ftyp == FramePriority) $ do
111+ PriorityFrame newpri <- guardIt $ decodePriorityFrame header pl
112+ checkPriority newpri streamId
113+ return True -- just ignore this frame
114+ Just strm@Stream{streamState,streamPrecedence} -> do
115+ state <- readIORef streamState
116+ state' <- stream ftyp header pl ctx state strm
117+ case state' of
118+ Open (NoBody tbl@(_,reqvt) pri) -> do
119+ resetContinued
120+ let mcl = readInt <$> getHeaderValue tokenContentLength reqvt
121+ when (just mcl (== (0 :: Int))) $
122+ E.throwIO $ StreamError ProtocolError streamId
123+ writeIORef streamPrecedence $ toPrecedence pri
124+ writeIORef streamState HalfClosed
125+ (!req, !ii) <- mkreq tbl (return "")
126+ atomically $ writeTQueue inputQ $ Input strm req reqvt ii
127+ Open (HasBody tbl@(_,reqvt) pri) -> do
128+ resetContinued
129+ q <- newTQueueIO
130+ let !mcl = readInt <$> getHeaderValue tokenContentLength reqvt
131+ writeIORef streamPrecedence $ toPrecedence pri
132+ bodyLength <- newIORef 0
133+ writeIORef streamState $ Open (Body q mcl bodyLength)
134+ readQ <- newReadBody q
135+ bodySource <- mkSource readQ
136+ (!req, !ii) <- mkreq tbl (readSource bodySource)
137+ atomically $ writeTQueue inputQ $ Input strm req reqvt ii
138+ s@(Open Continued{}) -> do
139+ setContinued
140+ writeIORef streamState s
141+ s -> do -- Idle, Open Body, HalfClosed, Closed
142+ resetContinued
143+ writeIORef streamState s
144+ return True
135145 where
136146 setContinued = writeIORef continued (Just streamId)
137147 resetContinued = writeIORef continued Nothing
@@ -145,29 +155,30 @@ frameReceiver ctx mkreq recvN = loop 0 `E.catch` sendGoaway
145155 getStream = do
146156 mstrm0 <- search streamTable streamId
147157 case mstrm0 of
148− Just strm0 -> do
158+ js@(Just strm0) -> do
149159 when (ftyp == FrameHeaders) $ do
150160 st <- readIORef $ streamState strm0
151161 when (isHalfClosed st) $ E.throwIO $ ConnectionError StreamClosed "header must not be sent to half closed"
152− return strm0
153− Nothing -> do
154− -- checkme
155− when (ftyp `notElem` [FrameHeaders,FramePriority]) $
156− E.throwIO $ ConnectionError ProtocolError "this frame is not allowed in an idel stream"
157− csid <- readIORef currentStreamId
158− when (streamId <= csid) $
159− E.throwIO $ ConnectionError ProtocolError "stream identifier must not decrease"
160− when (ftyp == FrameHeaders) $ do
161− writeIORef currentStreamId streamId
162− cnt <- readIORef concurrency
163− -- Checking the limitation of concurrency
164− when (cnt >= maxConcurrency) $
165− E.throwIO $ StreamError RefusedStream streamId
166− ws <- initialWindowSize <$> readIORef http2settings
167− newstrm <- newStream streamId (fromIntegral ws)
168− when (ftyp == FrameHeaders) $ opened ctx newstrm
169− insert streamTable streamId newstrm
170− return newstrm
162+ return js
163+ Nothing
164+ | isResponse streamId -> return Nothing
165+ | otherwise -> do
166+ when (ftyp `notElem` [FrameHeaders,FramePriority]) $
167+ E.throwIO $ ConnectionError ProtocolError "this frame is not allowed in an idel stream"
168+ csid <- readIORef clientStreamId
169+ when (streamId <= csid) $
170+ E.throwIO $ ConnectionError ProtocolError "stream identifier must not decrease"
171+ when (ftyp == FrameHeaders) $ do
172+ writeIORef clientStreamId streamId
173+ cnt <- readIORef concurrency
174+ -- Checking the limitation of concurrency
175+ when (cnt >= maxConcurrency) $
176+ E.throwIO $ StreamError RefusedStream streamId
177+ ws <- initialWindowSize <$> readIORef http2settings
178+ newstrm <- newStream streamId (fromIntegral ws)
179+ when (ftyp == FrameHeaders) $ opened ctx newstrm
180+ insert streamTable streamId newstrm
181+ return $ Just newstrm
171182
172183 consume = void . recvN
173184
warp/Network/Wai/Handler/Warp/HTTP2/Request.hs+41−2
@@ -4,6 +4,8 @@
44module Network.Wai.Handler.Warp.HTTP2.Request (
55 mkRequest
66 , MkReq
7+ , getHTTP2Data
8+ , setHTTP2Data
79 ) where
810
911import Control.Applicative ((<|>))
@@ -19,16 +21,25 @@ import Network.Socket (SockAddr)
1921import Network.Wai
2022import Network.Wai.Handler.Warp.HTTP2.Types
2123import Network.Wai.Handler.Warp.HashMap (hashByteString)
24+import Network.Wai.Handler.Warp.IORef
2225import Network.Wai.Handler.Warp.Request (pauseTimeoutKey, getFileInfoKey)
2326import qualified Network.Wai.Handler.Warp.Settings as S (Settings, settingsNoParsePath)
2427import qualified Network.Wai.Handler.Warp.Timeout as Timeout
2528import Network.Wai.Handler.Warp.Types
2629import Network.Wai.Internal (Request(..))
30+import System.IO.Unsafe (unsafePerformIO)
2731
28−type MkReq = (TokenHeaderList,ValueTable) -> IO ByteString -> (Request,InternalInfo)
32+type MkReq = (TokenHeaderList,ValueTable) -> IO ByteString -> IO (Request,InternalInfo)
2933
3034mkRequest :: InternalInfo1 -> S.Settings -> SockAddr -> MkReq
31−mkRequest ii1 settings addr (reqths,reqvt) body = (req,ii)
35+mkRequest ii1 settings addr (reqths,reqvt) body = do
36+ ref <- newIORef Nothing
37+ mkRequest' ii1 settings addr ref (reqths,reqvt) body
38+
39+mkRequest' :: InternalInfo1 -> S.Settings -> SockAddr
40+ -> IORef (Maybe HTTP2Data)
41+ -> MkReq
42+mkRequest' ii1 settings addr ref (reqths,reqvt) body = return (req,ii)
3243 where
3344 !req = Request {
3445 requestMethod = colonMethod
@@ -70,4 +81,32 @@ mkRequest ii1 settings addr (reqths,reqvt) body = (req,ii)
7081 !th = threadHandle ii
7182 !vaultValue = Vault.insert pauseTimeoutKey (Timeout.pause th)
7283 $ Vault.insert getFileInfoKey (getFileInfo ii)
84+ $ Vault.insert getHTTP2DataKey (readIORef ref)
85+ $ Vault.insert setHTTP2DataKey (writeIORef ref)
7386 Vault.empty
87+
88+getHTTP2DataKey :: Vault.Key (IO (Maybe HTTP2Data))
89+getHTTP2DataKey = unsafePerformIO Vault.newKey
90+{-# NOINLINE getHTTP2Data #-}
91+
92+-- | Getting 'HTTP2Data' through vault of the request.
93+-- Warp uses this to receive 'HTTP2Data' from 'Middleware'.
94+--
95+-- Since: 3.2.7
96+getHTTP2Data :: Request -> IO (Maybe HTTP2Data)
97+getHTTP2Data req = case Vault.lookup getHTTP2DataKey (vault req) of
98+ Nothing -> return Nothing
99+ Just getter -> getter
100+
101+setHTTP2DataKey :: Vault.Key (Maybe HTTP2Data -> IO ())
102+setHTTP2DataKey = unsafePerformIO Vault.newKey
103+{-# NOINLINE setHTTP2Data #-}
104+
105+-- | Setting 'HTTP2Data' through vault of the request.
106+-- 'Middleware' should use this.
107+--
108+-- Since: 3.2.7
109+setHTTP2Data :: Request -> Maybe HTTP2Data -> IO ()
110+setHTTP2Data req mh2d = case Vault.lookup setHTTP2DataKey (vault req) of
111+ Nothing -> return ()
112+ Just setter -> setter mh2d
warp/Network/Wai/Handler/Warp/HTTP2/Sender.hs+88−36
@@ -10,11 +10,14 @@ import Control.Applicative
1010import Control.Concurrent.STM
1111import qualified Control.Exception as E
1212import Control.Monad (void, when)
13+import Data.Bits
1314import qualified Data.ByteString as BS
1415import Data.ByteString.Builder (Builder)
1516import qualified Data.ByteString.Builder.Extra as B
1617import Data.Maybe (isNothing)
17−import Foreign.Ptr
18+import Data.Word (Word8, Word32)
19+import Foreign.Ptr (Ptr, plusPtr)
20+import Foreign.Storable (poke)
1821import Network.HPACK (setLimitForEncoding)
1922import Network.HTTP2
2023import Network.HTTP2.Priority (isEmptySTM, dequeueSTM, Precedence)
@@ -49,14 +52,14 @@ getStreamWindowSize :: Stream -> IO WindowSize
4952getStreamWindowSize Stream{streamWindow} = atomically $ readTVar streamWindow
5053
5154{-# INLINE waitStreamWindowSize #-}
52−waitStreamWindowSize :: Stream -> STM ()
53−waitStreamWindowSize Stream{streamWindow} = do
55+waitStreamWindowSize :: Stream -> IO ()
56+waitStreamWindowSize Stream{streamWindow} = atomically $ do
5457 w <- readTVar streamWindow
5558 check (w > 0)
5659
5760{-# INLINE waitStreaming #-}
58−waitStreaming :: TBQueue a -> STM ()
59−waitStreaming tbq = do
61+waitStreaming :: TBQueue a -> IO ()
62+waitStreaming tbq = atomically $ do
6063 isEmpty <- isEmptyTBQueue tbq
6164 check (isEmpty == False)
6265
@@ -119,22 +122,23 @@ frameSender ctx@Context{outputQ,controlQ,connectionWindow,encodeDynamicTable}
119122 Nothing -> return ()
120123 Just siz -> setLimitForEncoding siz encodeDynamicTable
121124
122− output (ONext strm curr mtbq) off0 lim = do
125+ output (ONext strm curr mtbq tell) off0 lim = do
123126 -- Data frame payload
124127 let !buf = connWriteBuffer `plusPtr` off0
125128 !siz = connBufferSize - off0
126129 Next datPayloadLen mnext <- curr buf siz lim
127− off <- fillDataHeader strm off0 datPayloadLen mnext
128− maybeEnqueueNext strm mtbq mnext
130+ off <- fillDataHeader strm off0 datPayloadLen mnext tell
131+ maybeEnqueueNext strm mtbq mnext tell
129132 return off
130133
131− output (ORspn strm rspn ii) off0 lim = do
134+ output (ORspn strm rspn ii tell) off0 lim = do
132135 -- Header frame and Continuation frame
133− let sid = streamNumber strm
134− endOfStream = case rspn of
136+ let !sid = streamNumber strm
137+ !endOfStream = case rspn of
135138 RspnNobody _ _ -> True
136139 _ -> False
137− kvlen <- headerContinue sid rspn endOfStream off0 ii
140+ ths <- addNecessaryHeaders ctx rspn ii settings
141+ kvlen <- headerContinue sid ths endOfStream off0
138142 off <- sendHeadersIfNecessary $ off0 + frameHeaderLength + kvlen
139143 case rspn of
140144 RspnNobody _ _ -> do
@@ -145,32 +149,48 @@ frameSender ctx@Context{outputQ,controlQ,connectionWindow,encodeDynamicTable}
145149 let payloadOff = off + frameHeaderLength
146150 Next datPayloadLen mnext <-
147151 fillFileBodyGetNext conn ii payloadOff lim path mpart
148− off' <- fillDataHeader strm off datPayloadLen mnext
149− maybeEnqueueNext strm Nothing mnext
152+ off' <- fillDataHeader strm off datPayloadLen mnext tell
153+ maybeEnqueueNext strm Nothing mnext tell
150154 return off'
151155 RspnBuilder _ _ builder -> do
152156 -- Data frame payload
153157 let payloadOff = off + frameHeaderLength
154158 Next datPayloadLen mnext <-
155159 fillBuilderBodyGetNext conn ii payloadOff lim builder
156− off' <- fillDataHeader strm off datPayloadLen mnext
157− maybeEnqueueNext strm Nothing mnext
160+ off' <- fillDataHeader strm off datPayloadLen mnext tell
161+ maybeEnqueueNext strm Nothing mnext tell
158162 return off'
159163 RspnStreaming _ _ tbq -> do
160164 let payloadOff = off + frameHeaderLength
161165 Next datPayloadLen mnext <-
162166 fillStreamBodyGetNext conn payloadOff lim tbq strm
163− off' <- fillDataHeader strm off datPayloadLen mnext
164− maybeEnqueueNext strm (Just tbq) mnext
167+ off' <- fillDataHeader strm off datPayloadLen mnext tell
168+ maybeEnqueueNext strm (Just tbq) mnext tell
165169 return off'
166170
171+ output (OPush strm ths rspn ii tell pid) off0 lim = do
172+ -- Creating a push promise header
173+ -- Frame id should be associated stream id from the client.
174+ let !sid = streamNumber strm
175+ len <- pushPromise pid sid ths off0
176+ off <- sendHeadersIfNecessary $ off0 + frameHeaderLength + len
177+ output (ORspn strm rspn ii tell) off lim
178+
179+ output _ _ _ = undefined -- never reach
180+
167181 outputOrEnqueueAgain out off = E.handle resetStream $ do
168182 state <- readIORef $ streamState strm
169183 if isClosed state then
170184 return off
171− else case mtbq of
172− Just tbq -> checkStreaming tbq
173− _ -> checkStreamWindowSize
185+ else case out of
186+ OWait strm' rsp ii wait -> do
187+ -- Checking if all push are done.
188+ let out' = ORspn strm' rsp ii (return ())
189+ forkAndEnqueueWhenReady wait outputQ out' mgr
190+ return off
191+ _ -> case mtbq of
192+ Just tbq -> checkStreaming tbq
193+ _ -> checkStreamWindowSize
174194 where
175195 strm = outputStream out
176196 mtbq = outputMaybeTBQueue out
@@ -202,13 +222,11 @@ frameSender ctx@Context{outputQ,controlQ,connectionWindow,encodeDynamicTable}
202222 flushN :: Int -> IO ()
203223 flushN n = bufferIO connWriteBuffer n connSendAll
204224
205− headerContinue sid rspn endOfStream off ii = do
206− let !s = rspnStatus rspn
207− !h = rspnHeaders rspn
225+ headerContinue sid ths endOfStream off = do
208226 let !offkv = off + frameHeaderLength
209227 let !bufkv = connWriteBuffer `plusPtr` offkv
210228 !limkv = connBufferSize - offkv
211− (hs,kvlen) <- hpackEncodeHeader ctx bufkv limkv ii settings s h
229+ (hs,kvlen) <- hpackEncodeHeader ctx bufkv limkv ths
212230 let flag0 = case hs of
213231 [] -> setEndHeader defaultFlags
214232 _ -> defaultFlags
@@ -221,24 +239,25 @@ frameSender ctx@Context{outputQ,controlQ,connectionWindow,encodeDynamicTable}
221239 !headerPayloadLim = connBufferSize - frameHeaderLength
222240
223241 continue _ kvlen [] = return kvlen
224− continue sid kvlen hs = do
242+ continue sid kvlen ths = do
225243 flushN $ kvlen + frameHeaderLength
226244 -- Now off is 0
227− (hs', kvlen') <- hpackEncodeHeaderLoop ctx bufHeaderPayload headerPayloadLim hs
228− when (hs == hs') $ E.throwIO $ ConnectionError CompressionError "cannot compress the header"
229− let flag = case hs' of
245+ (ths', kvlen') <- hpackEncodeHeaderLoop ctx bufHeaderPayload headerPayloadLim ths
246+ when (ths == ths') $ E.throwIO $ ConnectionError CompressionError "cannot compress the header"
247+ let flag = case ths' of
230248 [] -> setEndHeader defaultFlags
231249 _ -> defaultFlags
232250 fillFrameHeader FrameContinuation kvlen' sid flag connWriteBuffer
233− continue sid kvlen' hs'
251+ continue sid kvlen' ths'
234252
235253 {-# INLINE maybeEnqueueNext #-}
236254 -- Re-enqueue the stream in the output queue.
237− maybeEnqueueNext :: Stream -> Maybe (TBQueue Sequence) -> Maybe DynaNext -> IO ()
238− maybeEnqueueNext _ _ Nothing = return ()
239− maybeEnqueueNext strm mtbq (Just next) = enqueueOutput outputQ out
255+ maybeEnqueueNext :: Stream -> Maybe (TBQueue Sequence)
256+ -> Maybe DynaNext -> IO () -> IO ()
257+ maybeEnqueueNext _ _ Nothing _ = return ()
258+ maybeEnqueueNext strm mtbq (Just next) tell = enqueueOutput outputQ out
240259 where
241− !out = ONext strm next mtbq
260+ !out = ONext strm next mtbq tell
242261
243262 {-# INLINE sendHeadersIfNecessary #-}
244263 -- Send headers if there is not room for a 1-byte data frame, and return
@@ -250,7 +269,7 @@ frameSender ctx@Context{outputQ,controlQ,connectionWindow,encodeDynamicTable}
250269 flushN off
251270 return 0
252271
253− fillDataHeader strm off datPayloadLen mnext = do
272+ fillDataHeader strm off datPayloadLen mnext tell = do
254273 -- Data frame header
255274 let !sid = streamNumber strm
256275 !buf = connWriteBuffer `plusPtr` off
@@ -259,11 +278,27 @@ frameSender ctx@Context{outputQ,controlQ,connectionWindow,encodeDynamicTable}
259278 flag | done = setEndStream defaultFlags
260279 | otherwise = defaultFlags
261280 fillFrameHeader FrameData datPayloadLen sid flag buf
262− when done $ closed ctx strm Finished
281+ when done $ do
282+ void $ tell
283+ closed ctx strm Finished
263284 atomically $ modifyTVar' connectionWindow (subtract datPayloadLen)
264285 atomically $ modifyTVar' (streamWindow strm) (subtract datPayloadLen)
265286 return off'
266287
288+ pushPromise pid sid ths off = do
289+ let !offsid = off + frameHeaderLength
290+ !bufsid = connWriteBuffer `plusPtr` offsid
291+ poke32 bufsid $ fromIntegral sid
292+ let !offkv = offsid + 4
293+ !bufkv = connWriteBuffer `plusPtr` offkv
294+ !limkv = connBufferSize - offkv
295+ (_,kvlen) <- hpackEncodeHeader ctx bufkv limkv ths
296+ let !flag = setEndHeader defaultFlags -- No EndStream flag
297+ !buf = connWriteBuffer `plusPtr` off
298+ !len = kvlen + 4
299+ fillFrameHeader FramePushPromise len pid flag buf
300+ return len
301+
267302 {-# INLINE fillFrameHeader #-}
268303 fillFrameHeader ftyp len sid flag buf = encodeFrameHeaderBuf ftyp hinfo buf
269304 where
@@ -470,3 +505,20 @@ mini :: Int -> Integer -> Int
470505mini i n
471506 | fromIntegral i < n = i
472507 | otherwise = fromIntegral n
508+
509+
510+----------------------------------------------------------------
511+
512+poke32 :: Ptr Word8 -> Word32 -> IO ()
513+poke32 ptr i = do
514+ poke ptr w0
515+ poke8 ptr 1 w1
516+ poke8 ptr 2 w2
517+ poke8 ptr 3 w3
518+ where
519+ w0 = fromIntegral ((i `shiftR` 24) .&. 0xff)
520+ w1 = fromIntegral ((i `shiftR` 16) .&. 0xff)
521+ w2 = fromIntegral ((i `shiftR` 8) .&. 0xff)
522+ w3 = fromIntegral (i .&. 0xff)
523+ poke8 :: Ptr Word8 -> Int -> Word8 -> IO ()
524+ poke8 ptr0 n w = poke (ptr0 `plusPtr` n) w
warp/Network/Wai/Handler/Warp/HTTP2/Types.hs+85−10
@@ -68,17 +68,27 @@ rspnHeaders (RspnStreaming _ t _) = t
6868rspnHeaders (RspnBuilder _ t _) = t
6969rspnHeaders (RspnFile _ t _ _ ) = t
7070
71−data Output = ORspn !Stream !Rspn !InternalInfo
72− | ONext !Stream !DynaNext !(Maybe (TBQueue Sequence))
71+data Output = ORspn !Stream !Rspn !InternalInfo (IO ()) -- done
72+ | OWait !Stream !Rspn !InternalInfo (IO ()) -- done
73+ | OPush !Stream -- stream for this push from this server
74+ TokenHeaderList
75+ !Rspn {- RspnFile only-}
76+ !InternalInfo (IO ()) -- wait for done
77+ !StreamId -- associated stream id from client
78+ | ONext !Stream !DynaNext !(Maybe (TBQueue Sequence)) (IO ()) -- done
7379
7480outputStream :: Output -> Stream
75−outputStream (ORspn strm _ _) = strm
76−outputStream (ONext strm _ _) = strm
81+outputStream (ORspn strm _ _ _) = strm
82+outputStream (OPush strm _ _ _ _ _) = strm
83+outputStream (OWait strm _ _ _) = strm
84+outputStream (ONext strm _ _ _) = strm
7785
7886outputMaybeTBQueue :: Output -> Maybe (TBQueue Sequence)
79−outputMaybeTBQueue (ORspn _ (RspnStreaming _ _ tbq) _) = Just tbq
80−outputMaybeTBQueue (ORspn _ _ _) = Nothing
81−outputMaybeTBQueue (ONext _ _ mtbq) = mtbq
87+outputMaybeTBQueue (ORspn _ (RspnStreaming _ _ tbq) _ _) = Just tbq
88+outputMaybeTBQueue (ORspn _ _ _ _) = Nothing
89+outputMaybeTBQueue (OPush _ _ _ _ _ _) = Nothing
90+outputMaybeTBQueue (OWait _ _ _ _) = Nothing
91+outputMaybeTBQueue (ONext _ _ mtbq _) = mtbq
8292
8393data Control = CFinish
8494 | CGoaway !ByteString
@@ -107,7 +117,8 @@ data Context = Context {
107117 -- frames that might follow". This field is used to implement
108118 -- this requirement.
109119 , continued :: !(IORef (Maybe StreamId))
110− , currentStreamId :: !(IORef StreamId)
120+ , clientStreamId :: !(IORef StreamId)
121+ , serverStreamId :: !(IORef StreamId)
111122 , inputQ :: !(TQueue Input)
112123 , outputQ :: !(PriorityTree Output)
113124 , controlQ :: !(TQueue Control)
@@ -127,6 +138,7 @@ newContext = Context <$> newIORef defaultSettings
127138 <*> newIORef 0
128139 <*> newIORef Nothing
129140 <*> newIORef 0
141+ <*> newIORef 0
130142 <*> newTQueueIO
131143 <*> newPriorityTree
132144 <*> newTQueueIO
@@ -164,6 +176,7 @@ data StreamState =
164176 | Open !OpenState
165177 | HalfClosed
166178 | Closed !ClosedCode
179+ | Reserved
167180
168181isIdle :: StreamState -> Bool
169182isIdle Idle = True
@@ -186,6 +199,7 @@ instance Show StreamState where
186199 show Open{} = "Open"
187200 show HalfClosed = "HalfClosed"
188201 show (Closed e) = "Closed: " ++ show e
202+ show Reserved = "Reserved"
189203
190204----------------------------------------------------------------
191205
@@ -204,6 +218,15 @@ newStream sid win = Stream sid <$> newIORef Idle
204218 <*> newTVarIO win
205219 <*> newIORef defaultPrecedence
206220
221+newPushStream :: Context -> WindowSize -> Precedence -> IO Stream
222+newPushStream Context{serverStreamId} win pre = do
223+ sid <- atomicModifyIORef' serverStreamId inc2
224+ Stream sid <$> newIORef Reserved
225+ <*> newTVarIO win
226+ <*> newIORef pre
227+ where
228+ inc2 x = let !x' = x + 2 in (x', x')
229+
207230----------------------------------------------------------------
208231
209232opened :: Context -> Stream -> IO ()
@@ -238,10 +261,10 @@ search :: StreamTable -> M.Key -> IO (Maybe Stream)
238261search (StreamTable ref) k = M.lookup k <$> readIORef ref
239262
240263{-# INLINE forkAndEnqueueWhenReady #-}
241−forkAndEnqueueWhenReady :: STM () -> PriorityTree Output -> Output -> Manager -> IO ()
264+forkAndEnqueueWhenReady :: IO () -> PriorityTree Output -> Output -> Manager -> IO ()
242265forkAndEnqueueWhenReady wait outQ out mgr = bracket setup teardown $ \_ ->
243266 void . forkIO $ do
244− atomically wait
267+ wait
245268 enqueueOutput outQ out
246269 where
247270 setup = addMyId mgr
@@ -257,3 +280,55 @@ enqueueOutput outQ out = do
257280{-# INLINE enqueueControl #-}
258281enqueueControl :: TQueue Control -> Control -> IO ()
259282enqueueControl ctlQ ctl = atomically $ writeTQueue ctlQ ctl
283+
284+----------------------------------------------------------------
285+
286+-- | HTTP/2 specific data.
287+--
288+-- Since: 3.2.7
289+newtype HTTP2Data = HTTP2Data {
290+ -- | Accessor for 'PushPromise' in 'HTTP2Data'.
291+ --
292+ -- Since: 3.2.7
293+ http2dataPushPromise :: [PushPromise]
294+ } deriving (Eq,Show)
295+
296+-- | Default HTTP/2 specific data.
297+--
298+-- Since: 3.2.7
299+defaultHTTP2Data :: HTTP2Data
300+defaultHTTP2Data = HTTP2Data []
301+
302+-- | HTTP/2 push promise or sever push.
303+--
304+-- Since: 3.2.7
305+data PushPromise = PushPromise {
306+ -- | Accessor for a URL path in 'PushPromise'.
307+ -- E.g. \"\/style\/default.css\".
308+ --
309+ -- Since: 3.2.7
310+ promisedPath :: ByteString
311+ -- | Accessor for 'FilePath' in 'PushPromise'.
312+ -- E.g. \"FILE_PATH/default.css\".
313+ --
314+ -- Since: 3.2.7
315+ , promisedFile :: FilePath
316+ -- | Accessor for 'H.ResponseHeaders' in 'PushPromise'
317+ -- \"content-type\" must be specified.
318+ -- Default value: [].
319+ --
320+ --
321+ -- Since: 3.2.7
322+ , promisedResponseHeaders :: H.ResponseHeaders
323+ -- | Accessor for 'Weight' in 'PushPromise'.
324+ -- Default value: 16.
325+ --
326+ -- Since: 3.2.7
327+ , promisedWeight :: Weight
328+ } deriving (Eq,Ord,Show)
329+
330+-- | Default push promise.
331+--
332+-- Since: 3.2.7
333+defaultPushPromise :: PushPromise
334+defaultPushPromise = PushPromise "" "" [] 16
warp/Network/Wai/Handler/Warp/HTTP2/Worker.hs+96−19
@@ -15,6 +15,8 @@ module Network.Wai.Handler.Warp.HTTP2.Worker (
1515import Control.Applicative
1616import Data.Monoid (mempty)
1717#endif
18+import Control.Applicative ((<|>))
19+import Data.Maybe (fromJust)
1820import Control.Concurrent.STM
1921import Control.Exception (SomeException(..), AsyncException(..))
2022import qualified Control.Exception as E
@@ -22,13 +24,16 @@ import Control.Monad (when)
2224import Data.ByteString.Builder (byteString)
2325import qualified Network.HTTP.Types as H
2426import Network.HTTP2
27+import Network.HTTP2.Priority
2528import Network.HPACK
29+import Network.HPACK.Token
2630import Network.Wai
2731import Network.Wai.Handler.Warp.FileInfoCache
2832import Network.Wai.Handler.Warp.HTTP2.EncodeFrame
2933import Network.Wai.Handler.Warp.HTTP2.File
3034import Network.Wai.Handler.Warp.HTTP2.Manager
3135import Network.Wai.Handler.Warp.HTTP2.Types
36+import Network.Wai.Handler.Warp.HTTP2.Request
3237import Network.Wai.Handler.Warp.IORef
3338import qualified Network.Wai.Handler.Warp.Response as R
3439import qualified Network.Wai.Handler.Warp.Settings as S
@@ -41,32 +46,104 @@ import Network.Wai.Internal (Response(..), ResponseReceived(..), ResponseReceive
4146-- | The wai definition is 'type Application = Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived'.
4247-- This type implements the second argument (Response -> IO ResponseReceived)
4348-- with extra arguments.
44−type Responder = InternalInfo -> ValueTable ->
45− ThreadContinue -> Stream -> Request ->
46− Response -> IO ResponseReceived
49+type Responder = InternalInfo
50+ -> ValueTable -- for Request
51+ -> ThreadContinue
52+ -> Stream
53+ -> Request
54+ -> Response
55+ -> IO ResponseReceived
56+
57+pushStream :: Context -> S.Settings
58+ -> StreamId -> ValueTable -> Request -> InternalInfo
59+ -> Maybe HTTP2Data
60+ -> IO (Stream -> Rspn -> InternalInfo -> IO () -> Output, IO ())
61+pushStream _ _ _ _ _ _ Nothing = return (ORspn, return ())
62+pushStream ctx@Context{http2settings,outputQ,streamTable}
63+ settings pid reqvt req ii (Just h2d)
64+ | len == 0 = return (ORspn, return ())
65+ | otherwise = do
66+ pushable <- enablePush <$> readIORef http2settings
67+ if pushable then do
68+ tvar <- newTVarIO 0
69+ lim <- push tvar pps0 0
70+ if lim == 0 then
71+ return (ORspn, return ())
72+ else
73+ return (OWait, waiter lim tvar)
74+ else
75+ return (ORspn, return ())
76+ where
77+ !pps0 = http2dataPushPromise h2d
78+ !len = length pps0
79+ !pushLogger = S.settingsServerPushLogger settings
80+ increment tvar = atomically $ modifyTVar' tvar (+1)
81+ waiter lim tvar = atomically $ do
82+ n <- readTVar tvar
83+ check (n >= lim)
84+ push _ [] !n = return (n :: Int)
85+ push tvar (pp:pps) !n = do
86+ let !file = promisedFile pp
87+ efinfo <- E.try $ getFileInfo ii file
88+ case efinfo of
89+ Left (_ex :: E.IOException) -> push tvar pps n
90+ Right (FileInfo _ size _ date) -> do
91+ ws <- initialWindowSize <$> readIORef http2settings
92+ let !w = promisedWeight pp
93+ !pri = defaultPriority { weight = w }
94+ !pre = toPrecedence pri
95+ strm <- newPushStream ctx ws pre
96+ let !sid = streamNumber strm
97+ insert streamTable sid strm
98+ (ths0, vt) <- toHeaderTable (promisedResponseHeaders pp)
99+ let !scheme = fromJust $ getHeaderValue tokenScheme reqvt
100+ -- fixme: this value can be Nothing
101+ !auth = fromJust (getHeaderValue tokenHost reqvt
102+ <|> getHeaderValue tokenAuthority reqvt)
103+ !path = promisedPath pp
104+ !promisedRequest = [(tokenMethod, H.methodGet)
105+ ,(tokenScheme, scheme)
106+ ,(tokenAuthority, auth)
107+ ,(tokenPath, path)]
108+ !part = FilePart 0 size size
109+ !rsp = RspnFile H.ok200 (ths,vt) file (Just part)
110+ !ths = (tokenLastModified,date) :
111+ addContentHeadersForFilePart ths0 part
112+ pushLogger req path size
113+ let out = OPush strm promisedRequest rsp ii (increment tvar) pid
114+ enqueueOutput outputQ out
115+ push tvar pps (n + 1)
116+
47117
48118-- | This function is passed to workers.
49119-- They also pass 'Response's from 'Application's to this function.
50120-- This function enqueues commands for the HTTP/2 sender.
51121response :: S.Settings -> Context -> Manager -> Responder
52−response settings Context{outputQ} mgr ii reqvt tconf strm req rsp = case rsp of
122+response settings ctx@Context{outputQ} mgr ii reqvt tconf strm req rsp = case rsp of
53123 ResponseStream s0 hs0 strmbdy
54124 | noBody s0 -> responseNoBody s0 hs0
55125 | isHead -> responseNoBody s0 hs0
56− | otherwise -> responseStreaming s0 hs0 strmbdy
126+ | otherwise -> getHTTP2Data req
127+ >>= pushStream ctx settings sid reqvt req ii
128+ >>= responseStreaming s0 hs0 strmbdy
57129 ResponseBuilder s0 hs0 b
58130 | noBody s0 -> responseNoBody s0 hs0
59131 | isHead -> responseNoBody s0 hs0
60− | otherwise -> responseBuilderBody s0 hs0 b
132+ | otherwise -> getHTTP2Data req
133+ >>= pushStream ctx settings sid reqvt req ii
134+ >>= responseBuilderBody s0 hs0 b
61135 ResponseFile s0 hs0 p mp
62136 | noBody s0 -> responseNoBody s0 hs0
63− | otherwise -> responseFileXXX s0 hs0 p mp
137+ | otherwise -> getHTTP2Data req
138+ >>= pushStream ctx settings sid reqvt req ii
139+ >>= responseFileXXX s0 hs0 p mp
64140 ResponseRaw _ _ -> error "HTTP/2 does not support ResponseRaw"
65141 where
66142 noBody = not . R.hasBody
67143 !isHead = requestMethod req == H.methodHead
68144 !logger = S.settingsLogger settings
69145 !th = threadHandle ii
146+ sid = streamNumber strm
70147
71148 -- Ideally, log messages should be written when responses are
72149 -- actually sent. But there is no way to keep good memory usage
@@ -80,20 +157,20 @@ response settings Context{outputQ} mgr ii reqvt tconf strm req rsp = case rsp of
80157 logger req s Nothing
81158 setThreadContinue tconf True
82159 let rspn = RspnNobody s tbl
83− out = ORspn strm rspn ii
160+ out = ORspn strm rspn ii (return ())
84161 enqueueOutput outputQ out
85162 return ResponseReceived
86163
87− responseBuilderBody s hs0 bdy = do
164+ responseBuilderBody s hs0 bdy (rspnOrWait,tell) = do
88165 logger req s Nothing
89166 setThreadContinue tconf True
90167 tbl <- toHeaderTable hs0
91168 let rspn = RspnBuilder s tbl bdy
92− out = ORspn strm rspn ii
169+ out = rspnOrWait strm rspn ii tell
93170 enqueueOutput outputQ out
94171 return ResponseReceived
95172
96− responseFileXXX _ hs0 path Nothing = do
173+ responseFileXXX _ hs0 path Nothing aux = do
97174 efinfo <- E.try $ getFileInfo ii path
98175 case efinfo of
99176 Left (_ex :: E.IOException) -> response404 hs0
@@ -101,13 +178,13 @@ response settings Context{outputQ} mgr ii reqvt tconf strm req rsp = case rsp of
101178 (rspths0,vt) <- toHeaderTable hs0
102179 case conditionalRequest finfo rspths0 reqvt of
103180 WithoutBody s -> responseNoBody s hs0
104− WithBody s rspths beg len -> responseFile2XX s (rspths,vt) path (Just (FilePart beg len (fileInfoSize finfo)))
181+ WithBody s rspths beg len -> responseFile2XX s (rspths,vt) path (Just (FilePart beg len (fileInfoSize finfo))) aux
105182
106− responseFileXXX s0 hs0 path mpart = do
183+ responseFileXXX s0 hs0 path mpart aux = do
107184 tbl <- toHeaderTable hs0
108− responseFile2XX s0 tbl path mpart
185+ responseFile2XX s0 tbl path mpart aux
109186
110− responseFile2XX s tbl path mpart
187+ responseFile2XX s tbl path mpart (rspnOrWait,tell)
111188 | isHead = do
112189 logger req s Nothing
113190 responseNoBody' s tbl
@@ -115,17 +192,17 @@ response settings Context{outputQ} mgr ii reqvt tconf strm req rsp = case rsp of
115192 logger req s (filePartByteCount <$> mpart)
116193 setThreadContinue tconf True
117194 let rspn = RspnFile s tbl path mpart
118− out = ORspn strm rspn ii
195+ out = rspnOrWait strm rspn ii tell
119196 enqueueOutput outputQ out
120197 return ResponseReceived
121198
122− response404 hs0 = responseBuilderBody s hs body
199+ response404 hs0 = responseBuilderBody s hs body (ORspn, return ())
123200 where
124201 s = H.notFound404
125202 hs = R.replaceHeader H.hContentType "text/plain; charset=utf-8" hs0
126203 body = byteString "File not found"
127204
128− responseStreaming s0 hs0 strmbdy = do
205+ responseStreaming s0 hs0 strmbdy (rspnOrWait,tell) = do
129206 logger req s0 Nothing
130207 -- We must not exit this WAI application.
131208 -- If the application exits, streaming would be also closed.
@@ -141,7 +218,7 @@ response settings Context{outputQ} mgr ii reqvt tconf strm req rsp = case rsp of
141218 tbq <- newTBQueueIO 10 -- fixme: hard coding: 10
142219 tbl <- toHeaderTable hs0
143220 let rspn = RspnStreaming s0 tbl tbq
144− out = ORspn strm rspn ii
221+ out = rspnOrWait strm rspn ii tell
145222 enqueueOutput outputQ out
146223 let push b = do
147224 atomically $ writeTBQueue tbq (SBuilder b)
warp/Network/Wai/Handler/Warp/Settings.hs+5
@@ -101,6 +101,10 @@ data Settings = Settings
101101 -- ^ A log function. Default: no action.
102102 --
103103 -- Since 3.X.X.
104+ , settingsServerPushLogger :: Request -> ByteString -> Integer -> IO ()
105+ -- ^ A HTTP/2 server push log function. Default: no action.
106+ --
107+ -- Since 3.X.X.
104108 , settingsGracefulShutdownTimeout :: Maybe Int
105109 -- ^ An optional timeout to limit the time (in seconds) waiting for
106110 -- a graceful shutdown of the web server.
@@ -140,6 +144,7 @@ defaultSettings = Settings
140144 , settingsSlowlorisSize = 2048
141145 , settingsHTTP2Enabled = True
142146 , settingsLogger = \_ _ _ -> return ()
147+ , settingsServerPushLogger = \_ _ _ -> return ()
143148 , settingsGracefulShutdownTimeout = Nothing
144149 }
145150