| {-# LANGUAGE TemplateHaskell #-} |
| {-# LANGUAGE FlexibleContexts #-} |
| |
| {-| Implementation of the Ganeti Unix Domain Socket JSON server interface. |
| |
| -} |
| |
| {- |
| |
| Copyright (C) 2013 Google Inc. |
| All rights reserved. |
| |
| Redistribution and use in source and binary forms, with or without |
| modification, are permitted provided that the following conditions are |
| met: |
| |
| 1. Redistributions of source code must retain the above copyright notice, |
| this list of conditions and the following disclaimer. |
| |
| 2. Redistributions in binary form must reproduce the above copyright |
| notice, this list of conditions and the following disclaimer in the |
| documentation and/or other materials provided with the distribution. |
| |
| THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS |
| IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
| TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR |
| CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
| EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
| PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
| PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
| LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
| NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
| SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| -} |
| |
| module Ganeti.UDSServer |
| ( ConnectConfig(..) |
| , ServerConfig(..) |
| , Client |
| , Server |
| , RecvResult(..) |
| , MsgKeys(..) |
| , strOfKey |
| -- * Unix sockets |
| , openClientSocket |
| , closeClientSocket |
| , openServerSocket |
| , closeServerSocket |
| , acceptSocket |
| -- * Client and server |
| , connectClient |
| , connectServer |
| , pipeClient |
| , acceptClient |
| , closeClient |
| , clientToFd |
| , closeServer |
| , buildResponse |
| , parseResponse |
| , buildCall |
| , parseCall |
| , recvMsg |
| , recvMsgExt |
| , sendMsg |
| -- * Client handler |
| , Handler(..) |
| , HandlerResult |
| , listener |
| ) where |
| |
| import Prelude () |
| import Ganeti.Prelude |
| |
| import Control.Concurrent.Lifted (fork, yield) |
| import Control.Monad.Base |
| import Control.Monad.Trans.Control |
| import Control.Exception (catch) |
| import Control.Monad |
| import qualified Data.ByteString as B |
| import qualified Data.ByteString.UTF8 as UTF8 |
| import Data.IORef |
| import Data.List (isInfixOf) |
| import Data.Word (Word8) |
| import qualified Network.Socket as S |
| import System.Directory (removeFile) |
| import System.IO ( hClose, hFlush, hPutStr, hWaitForInput, Handle, IOMode(..) |
| , hSetBuffering, BufferMode(..)) |
| import System.IO.Error (isEOFError) |
| import System.Posix.Types (Fd) |
| import System.Posix.IO (createPipe, fdToHandle, handleToFd) |
| import System.Timeout |
| import Text.JSON (encodeStrict, decodeStrict) |
| import qualified Text.JSON as J |
| import Text.JSON.Types |
| |
| import Ganeti.BasicTypes |
| import Ganeti.Errors (GanetiException(..), ErrorResult) |
| import Ganeti.JSON (fromJResult, fromJVal, fromJResultE, fromObj) |
| import Ganeti.Logging |
| import Ganeti.THH |
| import Ganeti.Utils |
| import Ganeti.Constants (privateParametersBlacklist) |
| |
| -- * Utility functions |
| |
| -- | Wrapper over System.Timeout.timeout that fails in the IO monad. |
| withTimeout :: Int -> String -> IO a -> IO a |
| withTimeout secs descr action = do |
| result <- timeout (secs * 1000000) action |
| case result of |
| Nothing -> fail $ "Timeout in " ++ descr |
| Just v -> return v |
| |
| |
| -- * Generic protocol functionality |
| |
| -- | Result of receiving a message from the socket. |
| data RecvResult = RecvConnClosed -- ^ Connection closed |
| | RecvError String -- ^ Any other error |
| | RecvOk String -- ^ Successfull receive |
| deriving (Show, Eq) |
| |
| |
| -- | The end-of-message separator. |
| eOM :: Word8 |
| eOM = 3 |
| |
| -- | The end-of-message encoded as a ByteString. |
| bEOM :: B.ByteString |
| bEOM = B.singleton eOM |
| |
| -- | Valid keys in the requests and responses. |
| data MsgKeys = Method |
| | Args |
| | Success |
| | Result |
| |
| -- | The serialisation of MsgKeys into strings in messages. |
| $(genStrOfKey ''MsgKeys "strOfKey") |
| |
| |
| -- Information required for creating a server connection. |
| data ServerConfig = ServerConfig |
| { connPermissions :: FilePermissions |
| , connConfig :: ConnectConfig |
| } |
| |
| -- Information required for creating a client or server connection. |
| data ConnectConfig = ConnectConfig |
| { recvTmo :: Int |
| , sendTmo :: Int |
| } |
| |
| -- | A client encapsulation. Note that it has separate read and write handle. |
| -- For sockets it is the same handle. It is required for bi-directional |
| -- inter-process pipes though. |
| data Client = Client { rsocket :: Handle -- ^ The read part of |
| -- the client socket |
| , wsocket :: Handle -- ^ The write part of |
| -- the client socket |
| , rbuf :: IORef B.ByteString -- ^ Already received buffer |
| , clientConfig :: ConnectConfig |
| } |
| |
| -- | A server encapsulation. |
| data Server = Server { sSocket :: S.Socket -- ^ The bound server socket |
| , sPath :: FilePath -- ^ The scoket's path |
| , serverConfig :: ConnectConfig |
| } |
| |
| -- * Unix sockets |
| |
| -- | Creates a Unix socket and connects it to the specified @path@, |
| -- where @timeout@ specifies the connection timeout. |
| openClientSocket |
| :: Int -- ^ connection timeout |
| -> FilePath -- ^ socket path |
| -> IO Handle |
| openClientSocket tmo path = do |
| sock <- S.socket S.AF_UNIX S.Stream S.defaultProtocol |
| withTimeout tmo "creating a connection" $ |
| S.connect sock (S.SockAddrUnix path) |
| S.socketToHandle sock ReadWriteMode |
| |
| -- | Closes the handle. |
| -- Performing the operation on a handle that has already been closed has no |
| -- effect; doing so is not an error. |
| -- All other operations on a closed handle will fail. |
| closeClientSocket :: Handle -> IO () |
| closeClientSocket = hClose |
| |
| -- | Creates a Unix socket and binds it to the specified @path@. |
| openServerSocket :: FilePath -> IO S.Socket |
| openServerSocket path = do |
| sock <- S.socket S.AF_UNIX S.Stream S.defaultProtocol |
| S.bindSocket sock (S.SockAddrUnix path) |
| return sock |
| |
| closeServerSocket :: S.Socket -> FilePath -> IO () |
| closeServerSocket sock path = do |
| S.sClose sock |
| removeFile path |
| |
| acceptSocket :: S.Socket -> IO Handle |
| acceptSocket sock = do |
| -- ignore client socket address |
| (clientSock, _) <- S.accept sock |
| S.socketToHandle clientSock ReadWriteMode |
| |
| -- * Client and server |
| |
| -- | Connects to the master daemon and returns a Client. |
| connectClient |
| :: ConnectConfig -- ^ configuration for the client |
| -> Int -- ^ connection timeout |
| -> FilePath -- ^ socket path |
| -> IO Client |
| connectClient conf tmo path = do |
| h <- openClientSocket tmo path |
| rf <- newIORef B.empty |
| return Client { rsocket=h, wsocket=h, rbuf=rf, clientConfig=conf } |
| |
| -- | Creates and returns a server endpoint. |
| connectServer :: ServerConfig -> Bool -> FilePath -> IO Server |
| connectServer sconf setOwner path = do |
| s <- openServerSocket path |
| when setOwner $ do |
| res <- ensurePermissions path (connPermissions sconf) |
| exitIfBad "Error - could not set socket properties" res |
| |
| S.listen s 5 -- 5 is the max backlog |
| return Server { sSocket = s, sPath = path, serverConfig = connConfig sconf } |
| |
| -- | Creates a new bi-directional client pipe. The two returned clients |
| -- talk to each other through the pipe. |
| pipeClient :: ConnectConfig -> IO (Client, Client) |
| pipeClient conf = |
| let newClient r w = do |
| rf <- newIORef B.empty |
| rh <- fdToHandle r |
| wh <- fdToHandle w |
| return Client { rsocket = rh, wsocket = wh |
| , rbuf = rf, clientConfig = conf } |
| in do |
| (r1, w1) <- createPipe |
| (r2, w2) <- createPipe |
| (,) <$> newClient r1 w2 <*> newClient r2 w1 |
| |
| -- | Closes a server endpoint. |
| closeServer :: (MonadBase IO m) => Server -> m () |
| closeServer server = |
| liftBase $ closeServerSocket (sSocket server) (sPath server) |
| |
| -- | Accepts a client |
| acceptClient :: Server -> IO Client |
| acceptClient s = do |
| handle <- acceptSocket (sSocket s) |
| new_buffer <- newIORef B.empty |
| return Client { rsocket=handle |
| , wsocket=handle |
| , rbuf=new_buffer |
| , clientConfig=serverConfig s |
| } |
| |
| -- | Closes the client socket. |
| -- Performing the operation on a client that has already been closed has no |
| -- effect; doing so is not an error. |
| -- All other operations on a closed client will fail with an exception. |
| closeClient :: Client -> IO () |
| closeClient client = do |
| closeClientSocket . wsocket $ client |
| closeClientSocket . rsocket $ client |
| |
| -- | Extracts the read (the first) and the write (the second) file descriptor |
| -- of a client. This closes the underlying 'Handle's, therefore the original |
| -- client is closed and unusable after the call. |
| -- |
| -- The purpose of this function is to keep the communication channel open, |
| -- while replacing a 'Client' with some other means. |
| clientToFd :: Client -> IO (Fd, Fd) |
| clientToFd client | rh == wh = join (,) <$> handleToFd rh |
| | otherwise = (,) <$> handleToFd rh <*> handleToFd wh |
| where |
| rh = rsocket client |
| wh = wsocket client |
| |
| -- | Sends a message over a transport. |
| sendMsg :: Client -> String -> IO () |
| sendMsg s buf = withTimeout (sendTmo $ clientConfig s) "sending a message" $ do |
| t1 <- getCurrentTimeUSec |
| let handle = wsocket s |
| -- Allow buffering (up to 1MiB) when writing to the socket. Note that |
| -- otherwise we get the default of sending each byte in a separate |
| -- system call, resulting in very poor performance. |
| hSetBuffering handle (BlockBuffering . Just $ 1024 * 1024) |
| hPutStr handle buf |
| B.hPut handle bEOM |
| hFlush handle |
| t2 <- getCurrentTimeUSec |
| logDebug $ "sendMsg: " ++ show ((t2 - t1) `div` 1000) ++ "ms" |
| |
| -- | Given a current buffer and the handle, it will read from the |
| -- network until we get a full message, and it will return that |
| -- message and the leftover buffer contents. |
| recvUpdate :: ConnectConfig -> Handle -> B.ByteString |
| -> IO (B.ByteString, B.ByteString) |
| recvUpdate conf handle obuf = do |
| nbuf <- withTimeout (recvTmo conf) "reading a response" $ do |
| _ <- hWaitForInput handle (-1) |
| B.hGetNonBlocking handle 4096 |
| let (msg, remaining) = B.break (eOM ==) nbuf |
| newbuf = B.append obuf msg |
| if B.null remaining |
| then recvUpdate conf handle newbuf |
| else return (newbuf, B.copy (B.tail remaining)) |
| |
| -- | Waits for a message over a transport. |
| recvMsg :: Client -> IO String |
| recvMsg s = do |
| cbuf <- readIORef $ rbuf s |
| let (imsg, ibuf) = B.break (eOM ==) cbuf |
| (msg, nbuf) <- |
| if B.null ibuf -- if old buffer didn't contain a full message |
| -- then we read from network: |
| then recvUpdate (clientConfig s) (rsocket s) cbuf |
| -- else we return data from our buffer, copying it so that the whole |
| -- message isn't retained and can be garbage collected |
| else return (imsg, B.copy (B.tail ibuf)) |
| writeIORef (rbuf s) nbuf |
| return $ UTF8.toString msg |
| |
| -- | Extended wrapper over recvMsg. |
| recvMsgExt :: Client -> IO RecvResult |
| recvMsgExt s = |
| Control.Exception.catch (liftM RecvOk (recvMsg s)) $ \e -> |
| return $ if isEOFError e |
| then RecvConnClosed |
| else RecvError (show e) |
| |
| |
| -- | Serialize a request to String. |
| buildCall :: (J.JSON mth, J.JSON args) |
| => mth -- ^ The method |
| -> args -- ^ The arguments |
| -> String -- ^ The serialized form |
| buildCall mth args = |
| let keyToObj :: (J.JSON a) => MsgKeys -> a -> (String, J.JSValue) |
| keyToObj k v = (strOfKey k, J.showJSON v) |
| in encodeStrict $ toJSObject [ keyToObj Method mth, keyToObj Args args ] |
| |
| -- | Parse the required keys out of a call. |
| parseCall :: (J.JSON mth, J.JSON args) => String -> Result (mth, args) |
| parseCall s = do |
| arr <- fromJResult "parsing top-level JSON message" $ |
| decodeStrict s :: Result (JSObject JSValue) |
| let keyFromObj :: (J.JSON a) => MsgKeys -> Result a |
| keyFromObj = fromObj (fromJSObject arr) . strOfKey |
| (,) <$> keyFromObj Method <*> keyFromObj Args |
| |
| |
| -- | Serialize the response to String. |
| buildResponse :: Bool -- ^ Success |
| -> JSValue -- ^ The arguments |
| -> String -- ^ The serialized form |
| buildResponse success args = |
| let ja = [ (strOfKey Success, JSBool success) |
| , (strOfKey Result, args)] |
| jo = toJSObject ja |
| in encodeStrict jo |
| |
| -- | Try to decode an error from the server response. This function |
| -- will always fail, since it's called only on the error path (when |
| -- status is False). |
| decodeError :: JSValue -> ErrorResult JSValue |
| decodeError val = |
| case fromJVal val of |
| Ok e -> Bad e |
| Bad msg -> Bad $ GenericError msg |
| |
| -- | Check that luxi responses contain the required keys and that the |
| -- call was successful. |
| parseResponse :: String -> ErrorResult JSValue |
| parseResponse s = do |
| when (UTF8.replacement_char `elem` s) $ |
| failError "Failed to decode UTF-8,\ |
| \ detected replacement char after decoding" |
| oarr <- fromJResultE "Parsing LUXI response" (decodeStrict s) |
| let arr = J.fromJSObject oarr |
| status <- fromObj arr (strOfKey Success) |
| result <- fromObj arr (strOfKey Result) |
| if status |
| then return result |
| else decodeError result |
| |
| -- | Logs an outgoing message. |
| logMsg |
| :: (Show e, J.JSON e, MonadLog m) |
| => Handler i m o |
| -> i -- ^ the received request (used for logging) |
| -> GenericResult e J.JSValue -- ^ A message to be sent |
| -> m () |
| logMsg handler req (Bad err) = |
| logWarning $ "Failed to execute request " ++ hInputLogLong handler req ++ ": " |
| ++ show err |
| logMsg handler req (Ok result) = do |
| -- only log the first 2,000 chars of the result |
| logDebug $ "Result (truncated): " ++ take 2000 (J.encode result) |
| logDebug $ "Successfully handled " ++ hInputLogShort handler req |
| |
| -- | Prepares an outgoing message. |
| prepareMsg |
| :: (J.JSON e) |
| => GenericResult e J.JSValue -- ^ A message to be sent |
| -> (Bool, J.JSValue) |
| prepareMsg (Bad err) = (False, J.showJSON err) |
| prepareMsg (Ok result) = (True, result) |
| |
| |
| -- * Processing client requests |
| |
| type HandlerResult m o = m (Bool, GenericResult GanetiException o) |
| |
| data Handler i m o = Handler |
| { hParse :: J.JSValue -> J.JSValue -> Result i |
| -- ^ parses method and its arguments into the input type |
| , hInputLogShort :: i -> String |
| -- ^ short description of an input, for the INFO logging level |
| , hInputLogLong :: i -> String |
| -- ^ long description of an input, for the DEBUG logging level |
| , hExec :: i -> HandlerResult m o |
| -- ^ executes the handler on an input |
| } |
| |
| |
| handleJsonMessage |
| :: (J.JSON o, Monad m) |
| => Handler i m o -- ^ handler |
| -> i -- ^ parsed input |
| -> HandlerResult m J.JSValue |
| handleJsonMessage handler req = do |
| (close, call_result) <- hExec handler req |
| return (close, fmap J.showJSON call_result) |
| |
| -- | Takes a request as a 'String', parses it, passes it to a handler and |
| -- formats its response. |
| handleRawMessage |
| :: (J.JSON o, MonadLog m) |
| => Handler i m o -- ^ handler |
| -> String -- ^ raw unparsed input |
| -> m (Bool, String) |
| handleRawMessage handler payload = |
| case parseCall payload >>= uncurry (hParse handler) of |
| Bad err -> do |
| let errmsg = "Failed to parse request: " ++ err |
| logWarning errmsg |
| return (False, buildResponse False (J.showJSON errmsg)) |
| Ok req -> do |
| logDebug $ "Request: " ++ hInputLogLong handler req |
| (close, call_result_json) <- handleJsonMessage handler req |
| logMsg handler req call_result_json |
| let (status, response) = prepareMsg call_result_json |
| return (close, buildResponse status response) |
| |
| isRisky :: RecvResult -> Bool |
| isRisky msg = case msg of |
| RecvOk payload -> any (`isInfixOf` payload) privateParametersBlacklist |
| _ -> False |
| |
| -- | Reads a request, passes it to a handler and sends a response back to the |
| -- client. |
| handleClient |
| :: (J.JSON o, MonadBase IO m, MonadLog m) |
| => Handler i m o |
| -> Client |
| -> m Bool |
| handleClient handler client = do |
| msg <- liftBase $ recvMsgExt client |
| |
| debugMode <- liftBase isDebugMode |
| when (debugMode && isRisky msg) $ |
| logAlert "POSSIBLE LEAKING OF CONFIDENTIAL PARAMETERS. \ |
| \Daemon is running in debug mode. \ |
| \The text of the request has been logged." |
| logDebug $ "Received message (truncated): " ++ take 500 (show msg) |
| |
| case msg of |
| RecvConnClosed -> logDebug "Connection closed" >> |
| return False |
| RecvError err -> logWarning ("Error during message receiving: " ++ err) >> |
| return False |
| RecvOk payload -> do |
| (close, outMsg) <- handleRawMessage handler payload |
| liftBase $ sendMsg client outMsg |
| return close |
| |
| |
| -- | Main client loop: runs one loop of 'handleClient', and if that |
| -- doesn't report a finished (closed) connection, restarts itself. |
| clientLoop |
| :: (J.JSON o, MonadBase IO m, MonadLog m) |
| => Handler i m o |
| -> Client |
| -> m () |
| clientLoop handler client = do |
| result <- handleClient handler client |
| {- It's been observed sometimes that reading immediately after sending |
| a response leads to worse performance, as there is nothing to read and |
| the system calls are just wasted. Thus yielding before reading gives |
| other threads a chance to proceed and provides a natural pause, leading |
| to a bit more efficient communication. |
| -} |
| if result |
| then yield >> clientLoop handler client |
| else liftBase $ closeClient client |
| |
| -- | Main listener loop: accepts clients, forks an I/O thread to handle |
| -- that client. |
| listener |
| :: (J.JSON o, MonadBaseControl IO m, MonadLog m) |
| => Handler i m o |
| -> Server |
| -> m () |
| listener handler server = do |
| client <- liftBase $ acceptClient server |
| _ <- fork $ clientLoop handler client |
| return () |