-
Notifications
You must be signed in to change notification settings - Fork 113
/
Server.hs
235 lines (199 loc) · 8.95 KB
/
Server.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
--------------------------------------------------------------------------------
-- | This provides a simple stand-alone server for 'WebSockets' applications.
-- Note that in production you want to use a real webserver such as snap or
-- warp.
{-# LANGUAGE OverloadedStrings #-}
module Network.WebSockets.Server
( ServerApp
, runServer
, ServerOptions (..)
, defaultServerOptions
, runServerWithOptions
, runServerWith
, makeListenSocket
, makePendingConnection
, makePendingConnectionFromStream
, PongTimeout
) where
--------------------------------------------------------------------------------
import Control.Concurrent (threadDelay)
import qualified Control.Concurrent.Async as Async
import Control.Exception (Exception, allowInterrupt,
bracket, bracketOnError,
finally, mask_, throwIO)
import Control.Monad (forever, void, when)
import qualified Data.IORef as IORef
import Data.Maybe (isJust)
import Network.Socket (Socket)
import qualified Network.Socket as S
import qualified System.Clock as Clock
--------------------------------------------------------------------------------
import Network.WebSockets.Connection
import Network.WebSockets.Http
import qualified Network.WebSockets.Stream as Stream
import Network.WebSockets.Types
--------------------------------------------------------------------------------
-- | WebSockets application that can be ran by a server. Once this 'IO' action
-- finishes, the underlying socket is closed automatically.
type ServerApp = PendingConnection -> IO ()
--------------------------------------------------------------------------------
-- | Provides a simple server. This function blocks forever. Note that this
-- is merely provided for quick-and-dirty or internal applications, but for real
-- applications, you should use a real server.
--
-- For example:
--
-- * Performance is reasonable under load, but:
-- * No protection against DoS attacks is provided.
-- * No logging is performed.
-- * ...
--
-- Glue for using this package with real servers is provided by:
--
-- * <https://hackage.haskell.org/package/wai-websockets>
--
-- * <https://hackage.haskell.org/package/websockets-snap>
runServer :: String -- ^ Address to bind
-> Int -- ^ Port to listen on
-> ServerApp -- ^ Application
-> IO () -- ^ Never returns
runServer host port app = runServerWith host port defaultConnectionOptions app
--------------------------------------------------------------------------------
-- | A version of 'runServer' which allows you to customize some options.
runServerWith :: String -> Int -> ConnectionOptions -> ServerApp -> IO ()
runServerWith host port opts = runServerWithOptions defaultServerOptions
{ serverHost = host
, serverPort = port
, serverConnectionOptions = opts
}
{-# DEPRECATED runServerWith "Use 'runServerWithOptions' instead" #-}
--------------------------------------------------------------------------------
data ServerOptions = ServerOptions
{ serverHost :: String
, serverPort :: Int
, serverConnectionOptions :: ConnectionOptions
-- | Require a pong from the client every N seconds; otherwise kill the
-- connection. If you use this, you should also use 'withPingThread' to
-- send a ping at a smaller interval; for example N/2.
, serverRequirePong :: Maybe Int
}
--------------------------------------------------------------------------------
defaultServerOptions :: ServerOptions
defaultServerOptions = ServerOptions
{ serverHost = "127.0.0.1"
, serverPort = 8080
, serverConnectionOptions = defaultConnectionOptions
, serverRequirePong = Nothing
}
--------------------------------------------------------------------------------
-- | Customizable version of 'runServer'. Never returns until killed.
--
-- Please use the 'defaultServerOptions' combined with record updates to set the
-- fields you want. This way your code is unlikely to break on future changes.
runServerWithOptions :: ServerOptions -> ServerApp -> IO a
runServerWithOptions opts app = S.withSocketsDo $
bracket
(makeListenSocket host port)
S.close $ \sock -> mask_ $ forever $ do
allowInterrupt
(conn, _) <- S.accept sock
-- This IORef holds a time at which the thread may be killed. This time
-- can be extended by calling 'tickle'.
killRef <- IORef.newIORef =<< (+ killDelay) <$> getSecs
let tickle = IORef.writeIORef killRef =<< (+ killDelay) <$> getSecs
-- Update the connection options to call 'tickle' whenever a pong is
-- received.
let connOpts'
| not useKiller = connOpts
| otherwise = connOpts
{ connectionOnPong = tickle >> connectionOnPong connOpts
}
-- Run the application.
appAsync <- Async.asyncWithUnmask $ \unmask ->
(unmask $ do
runApp conn connOpts' app) `finally`
(S.close conn)
-- Install the killer if required.
when useKiller $ void $ Async.async (killer killRef appAsync)
where
host = serverHost opts
port = serverPort opts
connOpts = serverConnectionOptions opts
-- Get the current number of seconds on some clock.
getSecs = Clock.sec <$> Clock.getTime Clock.Monotonic
-- Parse the 'serverRequirePong' options.
useKiller = isJust $ serverRequirePong opts
killDelay = maybe 0 fromIntegral (serverRequirePong opts)
-- Thread that reads the killRef, and kills the application if enough time
-- has passed.
killer killRef appAsync = do
killAt <- IORef.readIORef killRef
now <- getSecs
appState <- Async.poll appAsync
case appState of
-- Already finished/killed/crashed, we can give up.
Just _ -> return ()
-- Should not be killed yet. Wait and try again.
Nothing | now < killAt -> do
threadDelay (fromIntegral killDelay * 1000 * 1000)
killer killRef appAsync
-- Time to kill.
_ -> Async.cancelWith appAsync PongTimeout
--------------------------------------------------------------------------------
-- | Create a standardized socket on which you can listen for incomming
-- connections. Should only be used for a quick and dirty solution! Should be
-- preceded by the call 'Network.Socket.withSocketsDo'.
makeListenSocket :: String -> Int -> IO Socket
makeListenSocket host port = do
addr:_ <- S.getAddrInfo (Just hints) (Just host) (Just (show port))
bracketOnError
(S.socket (S.addrFamily addr) S.Stream S.defaultProtocol)
S.close
(\sock -> do
_ <- S.setSocketOption sock S.ReuseAddr 1
_ <- S.setSocketOption sock S.NoDelay 1
S.bind sock (S.addrAddress addr)
S.listen sock 5
return sock
)
where
hints = S.defaultHints { S.addrSocketType = S.Stream }
--------------------------------------------------------------------------------
runApp :: Socket
-> ConnectionOptions
-> ServerApp
-> IO ()
runApp socket opts app =
bracket
(makePendingConnection socket opts)
(Stream.close . pendingStream)
app
--------------------------------------------------------------------------------
-- | Turns a socket, connected to some client, into a 'PendingConnection'. The
-- 'PendingConnection' should be closed using 'Stream.close' later.
makePendingConnection
:: Socket -> ConnectionOptions -> IO PendingConnection
makePendingConnection socket opts = do
stream <- Stream.makeSocketStream socket
makePendingConnectionFromStream stream opts
-- | More general version of 'makePendingConnection' for 'Stream.Stream'
-- instead of a 'Socket'.
makePendingConnectionFromStream
:: Stream.Stream -> ConnectionOptions -> IO PendingConnection
makePendingConnectionFromStream stream opts = do
-- TODO: we probably want to send a 40x if the request is bad?
mbRequest <- Stream.parse stream (decodeRequestHead False)
case mbRequest of
Nothing -> throwIO ConnectionClosed
Just request -> return PendingConnection
{ pendingOptions = opts
, pendingRequest = request
, pendingOnAccept = \_ -> return ()
, pendingStream = stream
}
--------------------------------------------------------------------------------
-- | Internally used exception type used to kill connections if there
-- is a pong timeout.
data PongTimeout = PongTimeout deriving Show
--------------------------------------------------------------------------------
instance Exception PongTimeout