diff options
author | Sam T <pxqr.sta@gmail.com> | 2013-05-11 13:40:58 +0400 |
---|---|---|
committer | Sam T <pxqr.sta@gmail.com> | 2013-05-11 13:40:58 +0400 |
commit | b2a81b581db7f328e0ec345104fb2fea1cae1296 (patch) | |
tree | 738da87100d190af4ee34edff603a74eb7103f45 /src |
Initial commit.
Diffstat (limited to 'src')
-rw-r--r-- | src/Remote/KRPC.hs | 132 | ||||
-rw-r--r-- | src/Remote/KRPC/Method.hs | 76 | ||||
-rw-r--r-- | src/Remote/KRPC/Protocol.hs | 207 |
3 files changed, 415 insertions, 0 deletions
diff --git a/src/Remote/KRPC.hs b/src/Remote/KRPC.hs new file mode 100644 index 00000000..a6318ccd --- /dev/null +++ b/src/Remote/KRPC.hs | |||
@@ -0,0 +1,132 @@ | |||
1 | -- | | ||
2 | -- Copyright : (c) Sam T. 2013 | ||
3 | -- License : MIT | ||
4 | -- Maintainer : pxqr.sta@gmail.com | ||
5 | -- Stability : experimental | ||
6 | -- Portability : portable | ||
7 | -- | ||
8 | -- This module provides remote procedure call. | ||
9 | -- | ||
10 | {-# LANGUAGE OverloadedStrings #-} | ||
11 | {-# LANGUAGE FlexibleContexts, DeriveDataTypeable #-} | ||
12 | {-# LANGUAGE ExplicitForAll, KindSignatures #-} | ||
13 | module Remote.KRPC | ||
14 | ( module Remote.KRPC.Method, RemoteAddr | ||
15 | |||
16 | -- * Client | ||
17 | , call, async, await | ||
18 | |||
19 | -- * Server | ||
20 | , handler, server | ||
21 | ) where | ||
22 | |||
23 | import Control.Exception | ||
24 | import Control.Monad | ||
25 | import Control.Monad.Trans.Control | ||
26 | import Control.Monad.IO.Class | ||
27 | import Data.BEncode | ||
28 | import Data.List as L | ||
29 | import Data.Map as M | ||
30 | import Data.Text as T | ||
31 | import Data.Typeable | ||
32 | import Network | ||
33 | |||
34 | import Remote.KRPC.Protocol | ||
35 | import Remote.KRPC.Method | ||
36 | |||
37 | |||
38 | data RPCException = RPCException KError | ||
39 | deriving (Show, Eq, Typeable) | ||
40 | |||
41 | instance Exception RPCException | ||
42 | |||
43 | |||
44 | type RemoteAddr = KRemoteAddr | ||
45 | |||
46 | |||
47 | queryCall :: BEncodable param | ||
48 | => KRemote -> KRemoteAddr | ||
49 | -> Method remote param result -> param -> IO () | ||
50 | queryCall sock addr m arg = sendMessage q addr sock | ||
51 | where | ||
52 | q = kquery (L.head (methodName m)) [(L.head (methodParams m), toBEncode arg)] | ||
53 | |||
54 | getResult :: BEncodable result | ||
55 | => KRemote -> KRemoteAddr | ||
56 | -> Method remote param result -> IO result | ||
57 | getResult sock addr m = do | ||
58 | resp <- recvResponse addr sock | ||
59 | case resp of | ||
60 | Left e -> throw (RPCException e) | ||
61 | Right (KResponse dict) -> do | ||
62 | let valName = L.head (methodVals m) | ||
63 | case M.lookup valName dict of | ||
64 | Just val | Right res <- fromBEncode val -> return res | ||
65 | Nothing -> throw (RPCException (ProtocolError msg)) | ||
66 | where | ||
67 | msg = T.concat | ||
68 | [ "Unable to find return value: ", T.pack (show valName), "\n" | ||
69 | , "in response: ", T.pack (show dict) | ||
70 | ] | ||
71 | |||
72 | -- TODO async call | ||
73 | -- | Makes remote procedure call. Throws RPCException if server | ||
74 | -- returns error or decode error occurred. | ||
75 | -- | ||
76 | call :: (MonadBaseControl IO host, MonadIO host) | ||
77 | => (BEncodable param, BEncodable result) | ||
78 | => RemoteAddr | ||
79 | -> Method remote param result | ||
80 | -> param | ||
81 | -> host result | ||
82 | call addr m arg = liftIO $ withRemote $ \sock -> do | ||
83 | queryCall sock addr m arg | ||
84 | getResult sock addr m | ||
85 | |||
86 | |||
87 | newtype Async result = Async { waitResult :: IO result } | ||
88 | |||
89 | async :: MonadIO host | ||
90 | => (BEncodable param, BEncodable result) | ||
91 | => RemoteAddr | ||
92 | -> Method remote param result | ||
93 | -> param | ||
94 | -> host (Async result) | ||
95 | async addr m arg = do | ||
96 | liftIO $ withRemote $ \sock -> | ||
97 | queryCall sock addr m arg | ||
98 | return $ Async $ withRemote $ \sock -> | ||
99 | getResult sock addr m | ||
100 | |||
101 | await :: MonadIO host => Async result -> host result | ||
102 | await = liftIO . waitResult | ||
103 | |||
104 | -- TODO better name | ||
105 | type MHandler remote = Method remote BEncode (Result BEncode) | ||
106 | |||
107 | handler :: forall (remote :: * -> *) (param :: *) (result :: *). | ||
108 | (BEncodable param, BEncodable result) | ||
109 | => Monad remote | ||
110 | => Method remote param result | ||
111 | -> Method remote BEncode (Result BEncode) | ||
112 | handler m = m { methodBody = \x -> do | ||
113 | case fromBEncode x of | ||
114 | Right a -> liftM (Right . toBEncode) (methodBody m a) | ||
115 | Left e -> return (Left e) | ||
116 | } | ||
117 | |||
118 | -- TODO: allow forkIO | ||
119 | server :: (MonadBaseControl IO remote, MonadIO remote) | ||
120 | => PortNumber | ||
121 | -> [MHandler remote] | ||
122 | -> remote () | ||
123 | server servport ms = remoteServer servport $ \_ q -> do | ||
124 | let name = queryMethod q | ||
125 | let args = queryArgs q | ||
126 | let m = L.head ms | ||
127 | res <- methodBody m (snd (L.head (M.toList args))) | ||
128 | case res of | ||
129 | Left r -> return (Left (ProtocolError (T.pack r))) | ||
130 | Right r -> do | ||
131 | let retName = L.head (methodVals m) | ||
132 | return (Right (kresponse [(retName, r)])) | ||
diff --git a/src/Remote/KRPC/Method.hs b/src/Remote/KRPC/Method.hs new file mode 100644 index 00000000..f4b0bb9a --- /dev/null +++ b/src/Remote/KRPC/Method.hs | |||
@@ -0,0 +1,76 @@ | |||
1 | {-# LANGUAGE OverloadedStrings #-} | ||
2 | module Remote.KRPC.Method | ||
3 | ( Method(methodName, methodParams, methodVals, methodBody) | ||
4 | |||
5 | -- * Construction | ||
6 | , method | ||
7 | |||
8 | -- * Predefined methods | ||
9 | , idM, composeM, concatM | ||
10 | ) where | ||
11 | |||
12 | import Prelude hiding ((.), id) | ||
13 | import Control.Category | ||
14 | import Control.Monad | ||
15 | |||
16 | import Remote.KRPC.Protocol | ||
17 | |||
18 | -- | The | ||
19 | -- | ||
20 | -- * argument: type of method parameter | ||
21 | -- | ||
22 | -- * remote: A monad used by server-side. | ||
23 | -- | ||
24 | -- * result: type of return value of the method. | ||
25 | -- | ||
26 | data Method remote param result = Method { | ||
27 | -- | Name used in query and | ||
28 | methodName :: [MethodName] | ||
29 | |||
30 | -- | Description of each method parameter in right to left order. | ||
31 | , methodParams :: [ParamName] | ||
32 | |||
33 | -- | Description of each method return value in right to left order. | ||
34 | , methodVals :: [ValName] | ||
35 | |||
36 | -- | Description of method body. | ||
37 | , methodBody :: param -> remote result | ||
38 | } | ||
39 | |||
40 | instance Monad remote => Category (Method remote) where | ||
41 | id = idM | ||
42 | (.) = composeM | ||
43 | |||
44 | -- | Remote identity function. Could be used for echo servers for example. | ||
45 | -- | ||
46 | -- idM = method "id" ["x"] ["y"] return | ||
47 | -- | ||
48 | idM :: Monad m => Method m a a | ||
49 | idM = method "id" ["x"] ["y"] return | ||
50 | |||
51 | -- | Pipelining of two or more methods. | ||
52 | -- | ||
53 | -- NOTE: composed methods will work only with this implementation of | ||
54 | -- KRPC, so both server and client should use this implementation, | ||
55 | -- otherwise you more likely get the 'ProtocolError'. | ||
56 | -- | ||
57 | composeM :: Monad m => Method m b c -> Method m a b -> Method m a c | ||
58 | composeM g h = Method (methodName g ++ methodName h) | ||
59 | (methodParams h) | ||
60 | (methodVals g) | ||
61 | (methodBody h >=> methodBody g) | ||
62 | |||
63 | -- | Concat list of list. Could be used for performance tests. | ||
64 | -- | ||
65 | -- concatM = method "concat" ["xxs"] ["xs"] $ return . Prelude.concat | ||
66 | -- | ||
67 | concatM :: Monad m => Method m [[a]] [a] | ||
68 | concatM = method "concat" ["xxs"] ["xs"] $ return . Prelude.concat | ||
69 | |||
70 | |||
71 | method :: MethodName | ||
72 | -> [ParamName] | ||
73 | -> [ValName] | ||
74 | -> (param -> remote result) | ||
75 | -> Method remote param result | ||
76 | method name = Method [name] | ||
diff --git a/src/Remote/KRPC/Protocol.hs b/src/Remote/KRPC/Protocol.hs new file mode 100644 index 00000000..0aa7e100 --- /dev/null +++ b/src/Remote/KRPC/Protocol.hs | |||
@@ -0,0 +1,207 @@ | |||
1 | -- | | ||
2 | -- Copyright : (c) Sam T. 2013 | ||
3 | -- License : MIT | ||
4 | -- Maintainer : pxqr.sta@gmail.com | ||
5 | -- Stability : experimental | ||
6 | -- Portability : portable | ||
7 | -- | ||
8 | -- This module provides straightforward implementation of KRPC | ||
9 | -- protocol. In many situations Network.KRPC should be prefered | ||
10 | -- since it gives more safe, convenient and high level api. | ||
11 | -- | ||
12 | -- > See http://www.bittorrent.org/beps/bep_0005.html#krpc-protocol | ||
13 | -- | ||
14 | {-# LANGUAGE OverloadedStrings, FlexibleContexts #-} | ||
15 | module Remote.KRPC.Protocol | ||
16 | ( | ||
17 | -- * Error | ||
18 | KError(..), errorCode, mkKError | ||
19 | |||
20 | -- * Query | ||
21 | , KQuery(..), MethodName, ParamName, kquery | ||
22 | |||
23 | -- * Response | ||
24 | , KResponse(..), ValName, kresponse | ||
25 | , sendMessage, recvResponse | ||
26 | |||
27 | -- * Remote | ||
28 | , KRemote, KRemoteAddr, withRemote, remoteServer | ||
29 | |||
30 | -- * Re-exports | ||
31 | , encode, encoded, decode, decoded, toBEncode, fromBEncode | ||
32 | ) where | ||
33 | |||
34 | import Control.Applicative | ||
35 | import Control.Exception.Lifted | ||
36 | import Control.Monad | ||
37 | import Control.Monad.IO.Class | ||
38 | import Control.Monad.Trans.Control | ||
39 | import Data.BEncode | ||
40 | import Data.ByteString as B | ||
41 | import qualified Data.ByteString.Lazy as LB | ||
42 | import Data.Map as M | ||
43 | import Data.Text as T | ||
44 | import Network.Socket hiding (recvFrom) | ||
45 | import Network.Socket.ByteString | ||
46 | |||
47 | |||
48 | data KError | ||
49 | = GenericError { errorMessage :: Text } | ||
50 | | ServerError { errorMessage :: Text } | ||
51 | -- | Malformed packet, invalid arguments or bad token. | ||
52 | | ProtocolError { errorMessage :: Text } | ||
53 | | MethodUnknown { errorMessage :: Text } | ||
54 | deriving (Show, Read, Eq, Ord) | ||
55 | |||
56 | instance BEncodable KError where | ||
57 | toBEncode e = fromAssocs | ||
58 | [ "y" --> ("e" :: ByteString) | ||
59 | , "e" --> (errorCode e, errorMessage e) | ||
60 | ] | ||
61 | |||
62 | fromBEncode (BDict d) | ||
63 | | M.lookup "y" d == Just (BString "e") = | ||
64 | uncurry mkKError <$> d >-- "e" | ||
65 | |||
66 | fromBEncode _ = decodingError "KError" | ||
67 | |||
68 | type ErrorCode = Int | ||
69 | |||
70 | errorCode :: KError -> ErrorCode | ||
71 | errorCode (GenericError _) = 201 | ||
72 | errorCode (ServerError _) = 202 | ||
73 | errorCode (ProtocolError _) = 203 | ||
74 | errorCode (MethodUnknown _) = 204 | ||
75 | |||
76 | mkKError :: ErrorCode -> Text -> KError | ||
77 | mkKError 201 = GenericError | ||
78 | mkKError 202 = ServerError | ||
79 | mkKError 203 = ProtocolError | ||
80 | mkKError 204 = MethodUnknown | ||
81 | mkKError _ = GenericError | ||
82 | |||
83 | |||
84 | |||
85 | type MethodName = ByteString | ||
86 | type ParamName = ByteString | ||
87 | |||
88 | data KQuery = KQuery { | ||
89 | queryMethod :: MethodName | ||
90 | , queryArgs :: Map ParamName BEncode | ||
91 | } deriving (Show, Read, Eq, Ord) | ||
92 | |||
93 | instance BEncodable KQuery where | ||
94 | toBEncode (KQuery m args) = fromAssocs | ||
95 | [ "y" --> ("q" :: ByteString) | ||
96 | , "q" --> m | ||
97 | , "a" --> BDict args | ||
98 | ] | ||
99 | |||
100 | fromBEncode (BDict d) | ||
101 | | M.lookup "y" d == Just (BString "q") = | ||
102 | KQuery <$> d >-- "q" | ||
103 | <*> d >-- "a" | ||
104 | |||
105 | fromBEncode _ = decodingError "KQuery" | ||
106 | |||
107 | kquery :: MethodName -> [(ParamName, BEncode)] -> KQuery | ||
108 | kquery name args = KQuery name (M.fromList args) | ||
109 | |||
110 | |||
111 | |||
112 | |||
113 | type ValName = ByteString | ||
114 | |||
115 | newtype KResponse = KResponse (Map ValName BEncode) | ||
116 | deriving (Show, Read, Eq, Ord) | ||
117 | |||
118 | instance BEncodable KResponse where | ||
119 | toBEncode (KResponse vals) = fromAssocs | ||
120 | [ "y" --> ("r" :: ByteString) | ||
121 | , "r" --> vals | ||
122 | ] | ||
123 | |||
124 | |||
125 | fromBEncode (BDict d) | ||
126 | | M.lookup "y" d == Just (BString "r") = | ||
127 | KResponse <$> d >-- "r" | ||
128 | |||
129 | fromBEncode _ = decodingError "KDict" | ||
130 | |||
131 | kresponse :: [(ValName, BEncode)] -> KResponse | ||
132 | kresponse = KResponse . M.fromList | ||
133 | |||
134 | |||
135 | type KRemoteAddr = (HostAddress, PortNumber) | ||
136 | |||
137 | remoteAddr :: KRemoteAddr -> SockAddr | ||
138 | remoteAddr = SockAddrInet <$> snd <*> fst | ||
139 | |||
140 | type KRemote = Socket | ||
141 | |||
142 | withRemote :: (MonadBaseControl IO m, MonadIO m) => (KRemote -> m a) -> m a | ||
143 | withRemote = bracket (liftIO (socket AF_INET Datagram defaultProtocol)) | ||
144 | (liftIO . sClose) | ||
145 | |||
146 | maxMsgSize :: Int | ||
147 | maxMsgSize = 16 * 1024 | ||
148 | |||
149 | -- TODO eliminate toStrict | ||
150 | sendMessage :: BEncodable msg => msg -> KRemoteAddr -> KRemote -> IO () | ||
151 | sendMessage msg (host, port) sock = | ||
152 | sendAllTo sock (LB.toStrict (encoded msg)) (SockAddrInet port host) | ||
153 | |||
154 | recvResponse :: KRemoteAddr -> KRemote -> IO (Either KError KResponse) | ||
155 | recvResponse addr sock = do | ||
156 | connect sock (remoteAddr addr) | ||
157 | (raw, _) <- recvFrom sock maxMsgSize | ||
158 | return $ case decoded raw of | ||
159 | Right resp -> Right resp | ||
160 | Left decE -> Left $ case decoded raw of | ||
161 | Right kerror -> kerror | ||
162 | _ -> ProtocolError (T.pack decE) | ||
163 | |||
164 | remoteServer :: (MonadBaseControl IO remote, MonadIO remote) | ||
165 | => PortNumber | ||
166 | -> (KRemoteAddr -> KQuery -> remote (Either KError KResponse)) | ||
167 | -> remote () | ||
168 | remoteServer servport action = bracket (liftIO bind) (liftIO . sClose) loop | ||
169 | where | ||
170 | bind = do | ||
171 | sock <- socket AF_INET Datagram defaultProtocol | ||
172 | bindSocket sock (SockAddrInet servport iNADDR_ANY) | ||
173 | return sock | ||
174 | |||
175 | loop sock = forever $ do | ||
176 | (bs, addr) <- liftIO $ recvFrom sock maxMsgSize | ||
177 | |||
178 | case addr of | ||
179 | SockAddrInet port host -> | ||
180 | case decoded bs of | ||
181 | Right query -> do | ||
182 | res <- action (host, port) query | ||
183 | case res of | ||
184 | Right resp -> liftIO $ sendMessage resp (host, port) sock | ||
185 | Left err -> liftIO $ sendMessage err (host, port) sock | ||
186 | |||
187 | Left decodeE -> liftIO $ sendMessage rpcE (host, port) sock | ||
188 | where | ||
189 | rpcE = ProtocolError $ T.concat | ||
190 | ["Unable to decode query: ", T.pack (show bs), "\n" | ||
191 | ,"Specifically: ", T.pack decodeE | ||
192 | ] | ||
193 | _ -> return () | ||
194 | |||
195 | |||
196 | |||
197 | -- TODO to bencodable | ||
198 | instance (BEncodable a, BEncodable b) => BEncodable (a, b) where | ||
199 | {-# SPECIALIZE instance (BEncodable a, BEncodable b) => BEncodable (a, b) #-} | ||
200 | toBEncode (a, b) = BList [toBEncode a, toBEncode b] | ||
201 | {-# INLINE toBEncode #-} | ||
202 | |||
203 | fromBEncode be = case fromBEncode be of | ||
204 | Right [a, b] -> (,) <$> fromBEncode a <*> fromBEncode b | ||
205 | Right _ -> decodingError "Unable to decode a pair." | ||
206 | Left e -> Left e | ||
207 | {-# INLINE fromBEncode #-} \ No newline at end of file | ||