summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Network/SocketLike.hs104
-rw-r--r--src/Network/StreamServer.hs143
2 files changed, 247 insertions, 0 deletions
diff --git a/src/Network/SocketLike.hs b/src/Network/SocketLike.hs
new file mode 100644
index 00000000..2aa78e3e
--- /dev/null
+++ b/src/Network/SocketLike.hs
@@ -0,0 +1,104 @@
1{-# LANGUAGE GeneralizedNewtypeDeriving #-}
2-- |
3--
4-- A socket could be used indirectly via a 'System.IO.Handle' or a conduit from
5-- Michael Snoyman's conduit package. But doing so presents an encapsulation
6-- problem. Do we allow access to the underlying socket and trust that it wont
7-- be used in an unsafe way? Or do we protect it at the higher level and deny
8-- access to various state information?
9--
10-- The 'SocketLike' class enables the approach that provides a safe wrapper to
11-- the underlying socket and gives access to various state information without
12-- enabling direct reads or writes.
13module Network.SocketLike
14 ( SocketLike(..)
15 , RestrictedSocket
16 , restrictSocket
17 , restrictHandleSocket
18 -- * Re-exports
19 --
20 -- | To make the 'SocketLike' methods less awkward to use, the types
21 -- 'CUInt', 'SockAddr', and 'PortNumber' are re-exported.
22 , CUInt
23 , PortNumber
24 , SockAddr(..)
25 ) where
26
27import Network.Socket
28 ( PortNumber
29 , SockAddr
30 )
31import Foreign.C.Types ( CUInt )
32
33import qualified Network.Socket as NS
34import System.IO (Handle,hClose,hIsOpen)
35
36-- | A safe (mostly read-only) interface to a 'NS.Socket'. Note that despite
37-- how this class is named, it provides no access to typical 'NS.Socket' uses
38-- like sending or receiving network packets.
39class SocketLike sock where
40 -- | See 'NS.getSocketName'
41 getSocketName :: sock -> IO SockAddr
42 -- | See 'NS.getPeerName'
43 getPeerName :: sock -> IO SockAddr
44 -- | See 'NS.getPeerCred'
45 getPeerCred :: sock -> IO (CUInt, CUInt, CUInt)
46 -- | See 'NS.socketPort'
47 socketPort :: sock -> IO PortNumber
48 -- | See 'NS.sIsConnected'
49 --
50 -- __Warning__: Don't rely on this method if it's possible the socket was
51 -- converted into a 'Handle'.
52 sIsConnected :: sock -> IO Bool
53 -- | See 'NS.sIsBound'
54 sIsBound :: sock -> IO Bool
55 -- | See 'NS.sIsListening'
56 sIsListening :: sock -> IO Bool
57 -- | See 'NS.sIsReadable'
58 sIsReadable :: sock -> IO Bool
59 -- | See 'NS.sIsWritable'
60 sIsWritable :: sock -> IO Bool
61
62 -- | This is the only exposed write-access method to the
63 -- underlying state. Usually implemented by 'NS.close'
64 sClose :: sock -> IO ()
65
66instance SocketLike NS.Socket where
67 getSocketName = NS.getSocketName
68 getPeerName = NS.getPeerName
69 getPeerCred = NS.getPeerCred
70 socketPort = NS.socketPort
71 sIsConnected = NS.sIsConnected -- warning: this is always False if the socket
72 -- was converted to a Handle
73 sIsBound = NS.sIsBound
74 sIsListening = NS.sIsListening
75 sIsReadable = NS.sIsReadable
76 sIsWritable = NS.sIsWritable
77
78 sClose = NS.sClose
79
80-- | An encapsulated socket. Data reads and writes are not possible.
81data RestrictedSocket = Restricted (Maybe Handle) NS.Socket deriving Show
82
83instance SocketLike RestrictedSocket where
84 getSocketName (Restricted mb sock) = NS.getSocketName sock
85 getPeerName (Restricted mb sock) = NS.getPeerName sock
86 getPeerCred (Restricted mb sock) = NS.getPeerCred sock
87 socketPort (Restricted mb sock) = NS.socketPort sock
88 sIsConnected (Restricted mb sock) = maybe (NS.sIsConnected sock) (hIsOpen) mb
89 sIsBound (Restricted mb sock) = NS.sIsBound sock
90 sIsListening (Restricted mb sock) = NS.sIsListening sock
91 sIsReadable (Restricted mb sock) = NS.sIsReadable sock
92 sIsWritable (Restricted mb sock) = NS.sIsWritable sock
93 sClose (Restricted mb sock) = maybe (NS.sClose sock) (\h -> hClose h >> NS.sClose sock) mb
94
95-- | Create a 'RestrictedSocket' that explicitly disallows sending or
96-- receiving data.
97restrictSocket :: NS.Socket -> RestrictedSocket
98restrictSocket socket = Restricted Nothing socket
99
100-- | Build a 'RestrictedSocket' for which 'sClose' will close the given
101-- 'Handle'. It is intended that this 'Handle' was obtained via
102-- 'NS.socketToHandle'.
103restrictHandleSocket :: Handle -> NS.Socket -> RestrictedSocket
104restrictHandleSocket h socket = Restricted (Just h) socket
diff --git a/src/Network/StreamServer.hs b/src/Network/StreamServer.hs
new file mode 100644
index 00000000..a6cead0e
--- /dev/null
+++ b/src/Network/StreamServer.hs
@@ -0,0 +1,143 @@
1-- | This module implements a bare-bones TCP or Unix socket server.
2{-# LANGUAGE TypeFamilies #-}
3{-# LANGUAGE TypeOperators #-}
4{-# LANGUAGE OverloadedStrings #-}
5{-# LANGUAGE RankNTypes #-}
6module Network.StreamServer
7 ( streamServer
8 , ServerHandle
9 , ServerConfig(..)
10 , withSession
11 , quitListening
12 , dummyServerHandle
13 ) where
14
15import Data.Monoid
16import Network.Socket as Socket
17import Data.ByteString.Char8
18 ( hGetNonBlocking
19 )
20import qualified Data.ByteString.Char8 as S
21 ( hPutStrLn
22 )
23import System.Directory (removeFile)
24import System.IO
25 ( IOMode(..)
26 , hSetBuffering
27 , BufferMode(..)
28 , hWaitForInput
29 , hClose
30 , hIsEOF
31 , hPutStrLn
32 , stderr
33 , hFlush
34 )
35import Control.Monad
36import Control.Monad.Fix (fix)
37import Control.Concurrent (forkIO, threadDelay, ThreadId, mkWeakThreadId)
38import Control.Exception (catch,handle,try,finally)
39import System.IO.Error (tryIOError)
40import System.Mem.Weak
41import System.IO.Error
42
43-- import Data.Conduit
44import Control.Monad.IO.Class (MonadIO (liftIO))
45import qualified Data.ByteString as S (ByteString)
46import System.IO (Handle)
47import Control.Concurrent.MVar (newMVar)
48
49import Network.SocketLike
50
51data ServerHandle = ServerHandle Socket (Weak ThreadId)
52
53
54-- | Create a useless do-nothing 'ServerHandle'.
55dummyServerHandle :: IO ServerHandle
56dummyServerHandle = do
57 mvar <- newMVar Closed
58 let sock = MkSocket 0 AF_UNSPEC NoSocketType 0 mvar
59 thread <- mkWeakThreadId <=< forkIO $ return ()
60 return (ServerHandle sock thread)
61
62removeSocketFile :: SockAddr -> IO ()
63removeSocketFile (SockAddrUnix fname) = removeFile fname
64removeSocketFile _ = return ()
65
66-- | Terminate the server accept-loop. Call this to shut down the server.
67quitListening :: ServerHandle -> IO ()
68quitListening (ServerHandle socket _) =
69 finally (Socket.getSocketName socket >>= removeSocketFile)
70 (Socket.close socket)
71
72
73-- | It's 'bshow' instead of 'show' to enable swapping in a 'ByteString'
74-- variation. (This is not exported.)
75bshow :: Show a => a -> String
76bshow e = show e
77
78-- | Send a string to stderr. Not exported. Default 'serverWarn' when
79-- 'withSession' is used to configure the server.
80warnStderr :: String -> IO ()
81warnStderr str = hPutStrLn stderr str >> hFlush stderr
82
83data ServerConfig = ServerConfig
84 { serverWarn :: String -> IO ()
85 -- ^ Action to report warnings and errors.
86 , serverSession :: RestrictedSocket -> Int -> Handle -> IO ()
87 -- ^ Action to handle interaction with a client
88 }
89
90-- | Initialize a 'ServerConfig' using the provided session handler.
91withSession :: (RestrictedSocket -> Int -> Handle -> IO ()) -> ServerConfig
92withSession session = ServerConfig warnStderr session
93
94-- | Launch a thread to listen at the given bind address and dispatch
95-- to session handler threads on every incomming connection. Supports
96-- IPv4 and IPv6, TCP and unix sockets.
97--
98-- The returned handle can be used with 'quitListening' to terminate the
99-- thread and prevent any new sessions from starting. Currently active
100-- session threads will not be terminated or signaled in any way.
101streamServer :: ServerConfig -> SockAddr -> IO ServerHandle
102streamServer cfg addr = do
103 let warn = serverWarn cfg
104 family = case addr of
105 SockAddrInet {} -> AF_INET
106 SockAddrInet6 {} -> AF_INET6
107 SockAddrUnix {} -> AF_UNIX
108 sock <- socket family Stream 0
109 setSocketOption sock ReuseAddr 1
110 fix $ \loop ->
111 tryIOError (removeSocketFile addr) >> bind sock addr
112 `catchIOError` \e -> do warn $ "bind-error: " <> bshow addr <> " " <> bshow e
113 threadDelay 5000000
114 loop
115 listen sock maxListenQueue
116 thread <- mkWeakThreadId <=< forkIO $ acceptLoop cfg sock 0
117 return (ServerHandle sock thread)
118
119-- | Not exported. This, combined with 'acceptException' form a mutually recursive
120-- loop that handles incomming connections. To quit the loop, the socket must be
121-- closed by 'quitListening'.
122acceptLoop :: ServerConfig -> Socket -> Int -> IO ()
123acceptLoop cfg sock n = handle (acceptException cfg n sock) $ do
124 con <- accept sock
125 let conkey = n + 1
126 h <- socketToHandle (fst con) ReadWriteMode
127 forkIO $ serverSession cfg (restrictHandleSocket h (fst con)) conkey h
128 acceptLoop cfg sock (n + 1)
129
130acceptException :: ServerConfig -> Int -> Socket -> IOError -> IO ()
131acceptException cfg n sock ioerror = do
132 Socket.close sock
133 case show (ioeGetErrorType ioerror) of
134 "resource exhausted" -> do -- try again
135 serverWarn cfg $ ("acceptLoop: resource exhasted")
136 threadDelay 500000
137 acceptLoop cfg sock (n + 1)
138 "invalid argument" -> do -- quit on closed socket
139 return ()
140 message -> do -- unexpected exception
141 serverWarn cfg $ ("acceptLoop: "<>bshow message)
142 return ()
143