root / src / Ganeti / UDSServer.hs @ e181c8cd
History | View | Annotate | Download (16.1 kB)
1 |
{-# LANGUAGE TemplateHaskell #-} |
---|---|
2 |
{-# LANGUAGE FlexibleContexts #-} |
3 |
|
4 |
{-| Implementation of the Ganeti Unix Domain Socket JSON server interface. |
5 |
|
6 |
-} |
7 |
|
8 |
{- |
9 |
|
10 |
Copyright (C) 2013 Google Inc. |
11 |
|
12 |
This program is free software; you can redistribute it and/or modify |
13 |
it under the terms of the GNU General Public License as published by |
14 |
the Free Software Foundation; either version 2 of the License, or |
15 |
(at your option) any later version. |
16 |
|
17 |
This program is distributed in the hope that it will be useful, but |
18 |
WITHOUT ANY WARRANTY; without even the implied warranty of |
19 |
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
20 |
General Public License for more details. |
21 |
|
22 |
You should have received a copy of the GNU General Public License |
23 |
along with this program; if not, write to the Free Software |
24 |
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA |
25 |
02110-1301, USA. |
26 |
|
27 |
-} |
28 |
|
29 |
module Ganeti.UDSServer |
30 |
( ConnectConfig(..) |
31 |
, ServerConfig(..) |
32 |
, Client |
33 |
, Server |
34 |
, RecvResult(..) |
35 |
, MsgKeys(..) |
36 |
, strOfKey |
37 |
-- * Unix sockets |
38 |
, openClientSocket |
39 |
, closeClientSocket |
40 |
, openServerSocket |
41 |
, closeServerSocket |
42 |
, acceptSocket |
43 |
-- * Client and server |
44 |
, connectClient |
45 |
, connectServer |
46 |
, pipeClient |
47 |
, acceptClient |
48 |
, closeClient |
49 |
, clientToFd |
50 |
, closeServer |
51 |
, buildResponse |
52 |
, parseResponse |
53 |
, buildCall |
54 |
, parseCall |
55 |
, recvMsg |
56 |
, recvMsgExt |
57 |
, sendMsg |
58 |
-- * Client handler |
59 |
, Handler(..) |
60 |
, HandlerResult |
61 |
, listener |
62 |
) where |
63 |
|
64 |
import Control.Applicative |
65 |
import Control.Concurrent.Lifted (fork) |
66 |
import Control.Monad.Base |
67 |
import Control.Monad.Trans.Control |
68 |
import Control.Exception (catch) |
69 |
import Control.Monad |
70 |
import qualified Data.ByteString as B |
71 |
import qualified Data.ByteString.Lazy as BL |
72 |
import qualified Data.ByteString.UTF8 as UTF8 |
73 |
import qualified Data.ByteString.Lazy.UTF8 as UTF8L |
74 |
import Data.IORef |
75 |
import Data.List |
76 |
import Data.Word (Word8) |
77 |
import qualified Network.Socket as S |
78 |
import System.Directory (removeFile) |
79 |
import System.IO (hClose, hFlush, hWaitForInput, Handle, IOMode(..)) |
80 |
import System.IO.Error (isEOFError) |
81 |
import System.Posix.Types (Fd) |
82 |
import System.Posix.IO (createPipe, fdToHandle, handleToFd) |
83 |
import System.Timeout |
84 |
import Text.JSON (encodeStrict, decodeStrict) |
85 |
import qualified Text.JSON as J |
86 |
import Text.JSON.Types |
87 |
|
88 |
import Ganeti.BasicTypes |
89 |
import Ganeti.Errors (GanetiException(..), ErrorResult) |
90 |
import Ganeti.JSON |
91 |
import Ganeti.Logging |
92 |
import Ganeti.Runtime (GanetiDaemon(..), MiscGroup(..), GanetiGroup(..)) |
93 |
import Ganeti.THH |
94 |
import Ganeti.Utils |
95 |
import Ganeti.Constants (privateParametersBlacklist) |
96 |
|
97 |
-- * Utility functions |
98 |
|
99 |
-- | Wrapper over System.Timeout.timeout that fails in the IO monad. |
100 |
withTimeout :: Int -> String -> IO a -> IO a |
101 |
withTimeout secs descr action = do |
102 |
result <- timeout (secs * 1000000) action |
103 |
case result of |
104 |
Nothing -> fail $ "Timeout in " ++ descr |
105 |
Just v -> return v |
106 |
|
107 |
|
108 |
-- * Generic protocol functionality |
109 |
|
110 |
-- | Result of receiving a message from the socket. |
111 |
data RecvResult = RecvConnClosed -- ^ Connection closed |
112 |
| RecvError String -- ^ Any other error |
113 |
| RecvOk String -- ^ Successfull receive |
114 |
deriving (Show, Eq) |
115 |
|
116 |
|
117 |
-- | The end-of-message separator. |
118 |
eOM :: Word8 |
119 |
eOM = 3 |
120 |
|
121 |
-- | The end-of-message encoded as a ByteString. |
122 |
bEOM :: B.ByteString |
123 |
bEOM = B.singleton eOM |
124 |
|
125 |
-- | Valid keys in the requests and responses. |
126 |
data MsgKeys = Method |
127 |
| Args |
128 |
| Success |
129 |
| Result |
130 |
|
131 |
-- | The serialisation of MsgKeys into strings in messages. |
132 |
$(genStrOfKey ''MsgKeys "strOfKey") |
133 |
|
134 |
|
135 |
-- Information required for creating a server connection. |
136 |
data ServerConfig = ServerConfig |
137 |
{ connDaemon :: GanetiDaemon |
138 |
, connConfig :: ConnectConfig |
139 |
} |
140 |
|
141 |
-- Information required for creating a client or server connection. |
142 |
data ConnectConfig = ConnectConfig |
143 |
{ recvTmo :: Int |
144 |
, sendTmo :: Int |
145 |
} |
146 |
|
147 |
-- | A client encapsulation. Note that it has separate read and write handle. |
148 |
-- For sockets it is the same handle. It is required for bi-directional |
149 |
-- inter-process pipes though. |
150 |
data Client = Client { rsocket :: Handle -- ^ The read part of |
151 |
-- the client socket |
152 |
, wsocket :: Handle -- ^ The write part of |
153 |
-- the client socket |
154 |
, rbuf :: IORef B.ByteString -- ^ Already received buffer |
155 |
, clientConfig :: ConnectConfig |
156 |
} |
157 |
|
158 |
-- | A server encapsulation. |
159 |
data Server = Server { sSocket :: S.Socket -- ^ The bound server socket |
160 |
, sPath :: FilePath -- ^ The scoket's path |
161 |
, serverConfig :: ConnectConfig |
162 |
} |
163 |
|
164 |
-- * Unix sockets |
165 |
|
166 |
-- | Creates a Unix socket and connects it to the specified @path@, |
167 |
-- where @timeout@ specifies the connection timeout. |
168 |
openClientSocket |
169 |
:: Int -- ^ connection timeout |
170 |
-> FilePath -- ^ socket path |
171 |
-> IO Handle |
172 |
openClientSocket tmo path = do |
173 |
sock <- S.socket S.AF_UNIX S.Stream S.defaultProtocol |
174 |
withTimeout tmo "creating a connection" $ |
175 |
S.connect sock (S.SockAddrUnix path) |
176 |
S.socketToHandle sock ReadWriteMode |
177 |
|
178 |
closeClientSocket :: Handle -> IO () |
179 |
closeClientSocket = hClose |
180 |
|
181 |
-- | Creates a Unix socket and binds it to the specified @path@. |
182 |
openServerSocket :: FilePath -> IO S.Socket |
183 |
openServerSocket path = do |
184 |
sock <- S.socket S.AF_UNIX S.Stream S.defaultProtocol |
185 |
S.bindSocket sock (S.SockAddrUnix path) |
186 |
return sock |
187 |
|
188 |
closeServerSocket :: S.Socket -> FilePath -> IO () |
189 |
closeServerSocket sock path = do |
190 |
S.sClose sock |
191 |
removeFile path |
192 |
|
193 |
acceptSocket :: S.Socket -> IO Handle |
194 |
acceptSocket sock = do |
195 |
-- ignore client socket address |
196 |
(clientSock, _) <- S.accept sock |
197 |
S.socketToHandle clientSock ReadWriteMode |
198 |
|
199 |
-- * Client and server |
200 |
|
201 |
-- | Connects to the master daemon and returns a Client. |
202 |
connectClient |
203 |
:: ConnectConfig -- ^ configuration for the client |
204 |
-> Int -- ^ connection timeout |
205 |
-> FilePath -- ^ socket path |
206 |
-> IO Client |
207 |
connectClient conf tmo path = do |
208 |
h <- openClientSocket tmo path |
209 |
rf <- newIORef B.empty |
210 |
return Client { rsocket=h, wsocket=h, rbuf=rf, clientConfig=conf } |
211 |
|
212 |
-- | Creates and returns a server endpoint. |
213 |
connectServer :: ServerConfig -> Bool -> FilePath -> IO Server |
214 |
connectServer sconf setOwner path = do |
215 |
s <- openServerSocket path |
216 |
when setOwner . setOwnerAndGroupFromNames path (connDaemon sconf) $ |
217 |
ExtraGroup DaemonsGroup |
218 |
S.listen s 5 -- 5 is the max backlog |
219 |
return Server { sSocket = s, sPath = path, serverConfig = connConfig sconf } |
220 |
|
221 |
-- | Creates a new bi-directional client pipe. The two returned clients |
222 |
-- talk to each other through the pipe. |
223 |
pipeClient :: ConnectConfig -> IO (Client, Client) |
224 |
pipeClient conf = |
225 |
let newClient r w = do |
226 |
rf <- newIORef B.empty |
227 |
rh <- fdToHandle r |
228 |
wh <- fdToHandle w |
229 |
return Client { rsocket = rh, wsocket = wh |
230 |
, rbuf = rf, clientConfig = conf } |
231 |
in do |
232 |
(r1, w1) <- createPipe |
233 |
(r2, w2) <- createPipe |
234 |
(,) <$> newClient r1 w2 <*> newClient r2 w1 |
235 |
|
236 |
-- | Closes a server endpoint. |
237 |
closeServer :: (MonadBase IO m) => Server -> m () |
238 |
closeServer server = |
239 |
liftBase $ closeServerSocket (sSocket server) (sPath server) |
240 |
|
241 |
-- | Accepts a client |
242 |
acceptClient :: Server -> IO Client |
243 |
acceptClient s = do |
244 |
handle <- acceptSocket (sSocket s) |
245 |
new_buffer <- newIORef B.empty |
246 |
return Client { rsocket=handle |
247 |
, wsocket=handle |
248 |
, rbuf=new_buffer |
249 |
, clientConfig=serverConfig s |
250 |
} |
251 |
|
252 |
-- | Closes the client socket. |
253 |
closeClient :: Client -> IO () |
254 |
closeClient client = do |
255 |
closeClientSocket . wsocket $ client |
256 |
closeClientSocket . rsocket $ client |
257 |
|
258 |
-- | Extracts the read (the first) and the write (the second) file descriptor |
259 |
-- of a client. This closes the underlying 'Handle's, therefore the original |
260 |
-- client is closed and unusable after the call. |
261 |
-- |
262 |
-- The purpose of this function is to keep the communication channel open, |
263 |
-- while replacing a 'Client' with some other means. |
264 |
clientToFd :: Client -> IO (Fd, Fd) |
265 |
clientToFd client | rh == wh = join (,) <$> handleToFd rh |
266 |
| otherwise = (,) <$> handleToFd rh <*> handleToFd wh |
267 |
where |
268 |
rh = rsocket client |
269 |
wh = wsocket client |
270 |
|
271 |
-- | Sends a message over a transport. |
272 |
sendMsg :: Client -> String -> IO () |
273 |
sendMsg s buf = withTimeout (sendTmo $ clientConfig s) "sending a message" $ do |
274 |
let encoded = UTF8L.fromString buf |
275 |
handle = wsocket s |
276 |
BL.hPut handle encoded |
277 |
B.hPut handle bEOM |
278 |
hFlush handle |
279 |
|
280 |
-- | Given a current buffer and the handle, it will read from the |
281 |
-- network until we get a full message, and it will return that |
282 |
-- message and the leftover buffer contents. |
283 |
recvUpdate :: ConnectConfig -> Handle -> B.ByteString |
284 |
-> IO (B.ByteString, B.ByteString) |
285 |
recvUpdate conf handle obuf = do |
286 |
nbuf <- withTimeout (recvTmo conf) "reading a response" $ do |
287 |
_ <- hWaitForInput handle (-1) |
288 |
B.hGetNonBlocking handle 4096 |
289 |
let (msg, remaining) = B.break (eOM ==) nbuf |
290 |
newbuf = B.append obuf msg |
291 |
if B.null remaining |
292 |
then recvUpdate conf handle newbuf |
293 |
else return (newbuf, B.tail remaining) |
294 |
|
295 |
-- | Waits for a message over a transport. |
296 |
recvMsg :: Client -> IO String |
297 |
recvMsg s = do |
298 |
cbuf <- readIORef $ rbuf s |
299 |
let (imsg, ibuf) = B.break (eOM ==) cbuf |
300 |
(msg, nbuf) <- |
301 |
if B.null ibuf -- if old buffer didn't contain a full message |
302 |
-- then we read from network: |
303 |
then recvUpdate (clientConfig s) (rsocket s) cbuf |
304 |
else return (imsg, B.tail ibuf) -- else we return data from our buffer |
305 |
writeIORef (rbuf s) nbuf |
306 |
return $ UTF8.toString msg |
307 |
|
308 |
-- | Extended wrapper over recvMsg. |
309 |
recvMsgExt :: Client -> IO RecvResult |
310 |
recvMsgExt s = |
311 |
Control.Exception.catch (liftM RecvOk (recvMsg s)) $ \e -> |
312 |
return $ if isEOFError e |
313 |
then RecvConnClosed |
314 |
else RecvError (show e) |
315 |
|
316 |
|
317 |
-- | Serialize a request to String. |
318 |
buildCall :: (J.JSON mth, J.JSON args) |
319 |
=> mth -- ^ The method |
320 |
-> args -- ^ The arguments |
321 |
-> String -- ^ The serialized form |
322 |
buildCall mth args = |
323 |
let keyToObj :: (J.JSON a) => MsgKeys -> a -> (String, J.JSValue) |
324 |
keyToObj k v = (strOfKey k, J.showJSON v) |
325 |
in encodeStrict $ toJSObject [ keyToObj Method mth, keyToObj Args args ] |
326 |
|
327 |
-- | Parse the required keys out of a call. |
328 |
parseCall :: (J.JSON mth, J.JSON args) => String -> Result (mth, args) |
329 |
parseCall s = do |
330 |
arr <- fromJResult "parsing top-level JSON message" $ |
331 |
decodeStrict s :: Result (JSObject JSValue) |
332 |
let keyFromObj :: (J.JSON a) => MsgKeys -> Result a |
333 |
keyFromObj = fromObj (fromJSObject arr) . strOfKey |
334 |
(,) <$> keyFromObj Method <*> keyFromObj Args |
335 |
|
336 |
|
337 |
-- | Serialize the response to String. |
338 |
buildResponse :: Bool -- ^ Success |
339 |
-> JSValue -- ^ The arguments |
340 |
-> String -- ^ The serialized form |
341 |
buildResponse success args = |
342 |
let ja = [ (strOfKey Success, JSBool success) |
343 |
, (strOfKey Result, args)] |
344 |
jo = toJSObject ja |
345 |
in encodeStrict jo |
346 |
|
347 |
-- | Try to decode an error from the server response. This function |
348 |
-- will always fail, since it's called only on the error path (when |
349 |
-- status is False). |
350 |
decodeError :: JSValue -> ErrorResult JSValue |
351 |
decodeError val = |
352 |
case fromJVal val of |
353 |
Ok e -> Bad e |
354 |
Bad msg -> Bad $ GenericError msg |
355 |
|
356 |
-- | Check that luxi responses contain the required keys and that the |
357 |
-- call was successful. |
358 |
parseResponse :: String -> ErrorResult JSValue |
359 |
parseResponse s = do |
360 |
when (UTF8.replacement_char `elem` s) $ |
361 |
failError "Failed to decode UTF-8,\ |
362 |
\ detected replacement char after decoding" |
363 |
oarr <- fromJResultE "Parsing LUXI response" (decodeStrict s) |
364 |
let arr = J.fromJSObject oarr |
365 |
status <- fromObj arr (strOfKey Success) |
366 |
result <- fromObj arr (strOfKey Result) |
367 |
if status |
368 |
then return result |
369 |
else decodeError result |
370 |
|
371 |
-- | Logs an outgoing message. |
372 |
logMsg |
373 |
:: (Show e, J.JSON e, MonadLog m) |
374 |
=> Handler i m o |
375 |
-> i -- ^ the received request (used for logging) |
376 |
-> GenericResult e J.JSValue -- ^ A message to be sent |
377 |
-> m () |
378 |
logMsg handler req (Bad err) = |
379 |
logWarning $ "Failed to execute request " ++ hInputLogLong handler req ++ ": " |
380 |
++ show err |
381 |
logMsg handler req (Ok result) = do |
382 |
-- only log the first 2,000 chars of the result |
383 |
logDebug $ "Result (truncated): " ++ take 2000 (J.encode result) |
384 |
logInfo $ "Successfully handled " ++ hInputLogShort handler req |
385 |
|
386 |
-- | Prepares an outgoing message. |
387 |
prepareMsg |
388 |
:: (J.JSON e) |
389 |
=> GenericResult e J.JSValue -- ^ A message to be sent |
390 |
-> (Bool, J.JSValue) |
391 |
prepareMsg (Bad err) = (False, J.showJSON err) |
392 |
prepareMsg (Ok result) = (True, result) |
393 |
|
394 |
|
395 |
-- * Processing client requests |
396 |
|
397 |
type HandlerResult m o = m (Bool, GenericResult GanetiException o) |
398 |
|
399 |
data Handler i m o = Handler |
400 |
{ hParse :: J.JSValue -> J.JSValue -> Result i |
401 |
-- ^ parses method and its arguments into the input type |
402 |
, hInputLogShort :: i -> String |
403 |
-- ^ short description of an input, for the INFO logging level |
404 |
, hInputLogLong :: i -> String |
405 |
-- ^ long description of an input, for the DEBUG logging level |
406 |
, hExec :: i -> HandlerResult m o |
407 |
-- ^ executes the handler on an input |
408 |
} |
409 |
|
410 |
|
411 |
handleJsonMessage |
412 |
:: (J.JSON o, Monad m) |
413 |
=> Handler i m o -- ^ handler |
414 |
-> i -- ^ parsed input |
415 |
-> HandlerResult m J.JSValue |
416 |
handleJsonMessage handler req = do |
417 |
(close, call_result) <- hExec handler req |
418 |
return (close, fmap J.showJSON call_result) |
419 |
|
420 |
-- | Takes a request as a 'String', parses it, passes it to a handler and |
421 |
-- formats its response. |
422 |
handleRawMessage |
423 |
:: (J.JSON o, MonadLog m) |
424 |
=> Handler i m o -- ^ handler |
425 |
-> String -- ^ raw unparsed input |
426 |
-> m (Bool, String) |
427 |
handleRawMessage handler payload = |
428 |
case parseCall payload >>= uncurry (hParse handler) of |
429 |
Bad err -> do |
430 |
let errmsg = "Failed to parse request: " ++ err |
431 |
logWarning errmsg |
432 |
return (False, buildResponse False (J.showJSON errmsg)) |
433 |
Ok req -> do |
434 |
logDebug $ "Request: " ++ hInputLogLong handler req |
435 |
(close, call_result_json) <- handleJsonMessage handler req |
436 |
logMsg handler req call_result_json |
437 |
let (status, response) = prepareMsg call_result_json |
438 |
return (close, buildResponse status response) |
439 |
|
440 |
isRisky :: RecvResult -> Bool |
441 |
isRisky msg = case msg of |
442 |
RecvOk payload -> any (`isInfixOf` payload) privateParametersBlacklist |
443 |
_ -> False |
444 |
|
445 |
-- | Reads a request, passes it to a handler and sends a response back to the |
446 |
-- client. |
447 |
handleClient |
448 |
:: (J.JSON o, MonadBase IO m, MonadLog m) |
449 |
=> Handler i m o |
450 |
-> Client |
451 |
-> m Bool |
452 |
handleClient handler client = do |
453 |
msg <- liftBase $ recvMsgExt client |
454 |
|
455 |
debugMode <- liftBase isDebugMode |
456 |
when (debugMode && isRisky msg) $ |
457 |
logAlert "POSSIBLE LEAKING OF CONFIDENTIAL PARAMETERS. \ |
458 |
\Daemon is running in debug mode. \ |
459 |
\The text of the request has been logged." |
460 |
logDebug $ "Received message (truncated): " ++ take 500 (show msg) |
461 |
|
462 |
case msg of |
463 |
RecvConnClosed -> logDebug "Connection closed" >> |
464 |
return False |
465 |
RecvError err -> logWarning ("Error during message receiving: " ++ err) >> |
466 |
return False |
467 |
RecvOk payload -> do |
468 |
(close, outMsg) <- handleRawMessage handler payload |
469 |
liftBase $ sendMsg client outMsg |
470 |
return close |
471 |
|
472 |
|
473 |
-- | Main client loop: runs one loop of 'handleClient', and if that |
474 |
-- doesn't report a finished (closed) connection, restarts itself. |
475 |
clientLoop |
476 |
:: (J.JSON o, MonadBase IO m, MonadLog m) |
477 |
=> Handler i m o |
478 |
-> Client |
479 |
-> m () |
480 |
clientLoop handler client = do |
481 |
result <- handleClient handler client |
482 |
if result |
483 |
then clientLoop handler client |
484 |
else liftBase $ closeClient client |
485 |
|
486 |
-- | Main listener loop: accepts clients, forks an I/O thread to handle |
487 |
-- that client. |
488 |
listener |
489 |
:: (J.JSON o, MonadBaseControl IO m, MonadLog m) |
490 |
=> Handler i m o |
491 |
-> Server |
492 |
-> m () |
493 |
listener handler server = do |
494 |
client <- liftBase $ acceptClient server |
495 |
_ <- fork $ clientLoop handler client |
496 |
return () |