dbus-core-0.9: src/connections.anansi
:# Copyright (C) 2009-2011 John Millikin <jmillikin@gmail.com>
:#
:# This program is free software: you can redistribute it and/or modify
:# it under the terms of the GNU General Public License as published by
:# the Free Software Foundation, either version 3 of the License, or
:# any later version.
:#
:# This program is distributed in the hope that it will be useful,
:# but WITHOUT ANY WARRANTY; without even the implied warranty of
:# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
:# GNU General Public License for more details.
:#
:# You should have received a copy of the GNU General Public License
:# along with this program. If not, see <http://www.gnu.org/licenses/>.
\section{Connections}
\begin{multicols}{2}
A {\tt Connection} is an opaque handle to an open \dbus{} socket, with
an internal state for maintaining the current message serial and enforcing
thread-safe access to the socket. Every connection has an \emph{address},
which is where the remote server is listening, and a \emph{transport}, which
manages actually sending bytes between the two applications.
\vfill
\columnbreak
:d DBus.Connection
|apidoc DBus.Connection.Connection|
data Connection = Connection
{ connectionAddress :: Address
, connectionSocket :: Socket
, connectionSerial :: IORef Serial
, connectionReadLock :: MVar ()
, connectionWriteLock :: MVar ()
}
:
\end{multicols}
\begin{multicols}{2}
TODO
\vfill
\columnbreak
:d DBus.Connection.Error
newtype ConnectionError = ConnectionError String
deriving (Show, Eq, Typeable)
instance Exception ConnectionError
connectionError :: String -> IO a
connectionError = throwIO . ConnectionError
:
\end{multicols}
\begin{multicols}{2}
TODO
\vfill
\columnbreak
:d DBus.Connection
|apidoc DBus.Connection.connect|
connect :: [Transport] -> [Mechanism] -> Address -> IO Connection
connect transports mechanisms addr = do
msock <- connectTransport transports addr
sock <- case msock of
Just s -> return s
Nothing -> connectionError (concat
[ "Unknown address method: "
, show (addressMethod addr)
])
authed <- authenticate sock mechanisms
when (not authed)
(connectionError "Authentication failed")
serial <- newIORef (Serial 1)
readLock <- newMVar ()
writeLock <- newMVar ()
return (Connection addr sock serial
readLock writeLock)
|apidoc DBus.Connection.disconnect|
disconnect :: Connection -> IO ()
disconnect = socketClose . connectionSocket
:
\end{multicols}
\clearpage
\subsection{Addresses}
\begin{multicols}{2}
An address has two components, the \emph{method} and the \emph{parameters}.
The method (such as {\tt "unix"} or {\tt "tcp"}) describes how the socket
should be opened. The parameters include additional information, such as
hostnames, required by the method.
\vfill
\columnbreak
:d DBus.Address
data Address = Address Text (Map Text Text)
deriving (Eq)
addressMethod :: Address -> Text
addressMethod (Address x _ ) = x
addressParameters :: Address -> Map Text Text
addressParameters (Address _ x) = x
:
\end{multicols}
\begin{multicols}{2}
A bus address is in the format {\tt $method$:$key$=$value$,...}
where the method may be empty and parameters are optional. An address's
parameter list, if present, may end with a comma. Multiple parameters may
have the same key; in this case, only the first parameter for each key will
be stored.
In parameter values, any byte may be encoded by prepending the \% character
to its value in hexadecimal. \% is not allowed to appear unless it is
followed by two hexadecimal digits. Every other allowed byte is termed
an ``optionally encoded'' byte, and may appear unescaped in parameter
values.
\vfill
\columnbreak
:d DBus.Address
address :: Text -> Maybe Address
address = runParser $ do
addr <- parseAddress
eof
return addr
parseAddress :: Parser Address
parseAddress = parser where
parser = do
method <- many (noneOf ":;")
void (char ':')
params <- sepEndBy param (char ',')
return (Address
(Data.Text.pack method)
(Data.Map.fromList params))
param = do
key <- many1 (noneOf "=;,")
void (char '=')
value <- many1 valueChar
let pack = Data.Text.pack
return (pack key, pack value)
valueChar = encoded <|> unencoded
encoded = do
void (char '%')
hex <- count 2 hexDigit
return (chr (hexToInt hex))
unencoded = oneOf optionallyEncoded
optionallyEncoded :: [Char]
optionallyEncoded = concat
[ ['0'..'9']
, ['a'..'z']
, ['A'..'Z']
, "-_/\\*."
]
:
\end{multicols}
\begin{multicols}{2}
Addresses in environment variables are separated by semicolons, and the full
address list may end in a semicolon.
\vfill
\columnbreak
:d DBus.Address
addresses :: Text -> Maybe [Address]
addresses = runParser $ do
xs <- sepEndBy1 parseAddress (char ';')
eof
return xs
:
\end{multicols}
\clearpage
\subsubsection{Printing addresses}
\begin{multicols}{2}
Users might want to print addresses and open connections when working in a
{\sc repl}, so I'll define some simple {\tt Show} instances. {\tt Connection}
is given a special format to mark it as {\sc io}-ish, while {\tt address} is
formatted as if it's a string wrapper.
\vfill
\columnbreak
:d DBus.Address
instance Show Address where
showsPrec d x = showParen (d > 10) $
showString "Address " .
shows (addressText x)
:
:d DBus.Connection
instance Show Connection where
showsPrec _ x =
showString "<Connection " .
shows (connectionAddress x) .
showString ">"
:
\end{multicols}
\begin{multicols}{2}
Formatting addresses is just the reverse of parsing them.
\vfill
\columnbreak
:d DBus.Address
addressText :: Address -> Text
addressText addr = Data.Text.concat chunks where
chunks = [ addressMethod addr, ":"
, paramsText]
params = addressParameters addr
paramsText = Data.Text.intercalate "," $ do
(k, v) <- Data.Map.toList params
let k' = Data.Text.unpack k
let v' = Data.Text.unpack v
let encoded = concatMap encode v'
let str = concat [k', "=", encoded]
return (Data.Text.pack str)
encode c = if elem c optionallyEncoded
then [c]
else printf "%%%02X" (ord c)
:
\end{multicols}
\subsubsection{Environmental addresses}
:d DBus.Address
getenv :: String -> IO (Maybe Text)
getenv name = Control.Exception.catch
(fmap (Just . Data.Text.pack) (System.Environment.getEnv name))
(\(Control.Exception.SomeException _) -> return Nothing)
getSystem :: IO (Maybe [Address])
getSystem = do
let system = "unix:path=/var/run/dbus/system_bus_socket"
env <- getenv "DBUS_SYSTEM_BUS_ADDRESS"
return (addresses (maybe system id env))
getSession :: IO (Maybe [Address])
getSession = do
env <- getenv "DBUS_SESSION_BUS_ADDRESS"
return (env >>= addresses)
getStarter :: IO (Maybe [Address])
getStarter = do
env <- getenv "DBUS_STARTER_BUS_ADDRESS"
return (env >>= addresses)
:
\clearpage
\subsection{Transports}
\begin{multicols}{2}
The transport abstracts how the operating system connects to the server
process; typically, this is based on {\sc unix} or {\sc tcp} sockets, but
users might use alternative transports for special use cases. This library
only requires the ability to read from, write to, and close an open socket,
so those methods are bundled into {\tt Transport}.
\vfill
\columnbreak
:d DBus.Connection.Transport
|apidoc DBus.Connection.Transport|
data Transport = Transport Text (Address -> IO Socket)
data Socket = Socket
(ByteString -> IO ())
(Word32 -> IO ByteString)
(IO ())
transport :: Text -> (Address -> IO Socket) -> Transport
transport = Transport
socket :: (ByteString -> IO ()) -> (Word32 -> IO ByteString) -> IO () -> Socket
socket = Socket
socketPut :: Socket -> ByteString -> IO ()
socketPut (Socket x _ _) = x
socketGet :: Socket -> Word32 -> IO ByteString
socketGet (Socket _ x _) = x
socketClose :: Socket -> IO ()
socketClose (Socket _ _ x) = x
connectTransport :: [Transport] -> Address -> IO (Maybe Socket)
connectTransport transports addr = loop transports where
m = addressMethod addr
loop [] = return Nothing
loop ((Transport n io):ts) = if n == m
then fmap Just (io addr)
else loop ts
:
\end{multicols}
\subsubsection{Generic handle-based transport}
\begin{multicols}{2}
Both UNIX and TCP are backed by standard handles, and can therefore use
a shared handle-based transport backend. This sets reasonable defaults
(no buffering, no newline mangling) for the handle parameters.
\vfill
\columnbreak
:d DBus.Connection.Transport
connectHandle :: System.IO.Handle -> IO Socket
connectHandle h = do
System.IO.hSetBuffering h System.IO.NoBuffering
System.IO.hSetBinaryMode h True
return (Socket
(Data.ByteString.hPut h)
(Data.ByteString.hGet h . fromIntegral)
(System.IO.hClose h))
:
\end{multicols}
\clearpage
\subsubsection{UNIX Transport}
The {\sc unix} transport accepts two parameters: {\tt path}, which is a
simple filesystem path, and {\tt abstract}, which is a path in the
Linux-specific abstract domain. One, and only one, of these parameters must
be specified.
:d DBus.Connection.Transport
unix :: Transport
unix = transport "unix" connectUNIX
connectUNIX :: Address -> IO Socket
connectUNIX a = getHandle >>= connectHandle where
params = addressParameters a
param key = Data.Map.lookup key params
tooMany = "Only one of `path' or `abstract' may be specified for the\
\ `unix' transport."
tooFew = "One of `path' or `abstract' must be specified for the\
\ `unix' transport."
path = case (param "path", param "abstract") of
(Just _, Just _) -> connectionError tooMany
(Nothing, Nothing) -> connectionError tooFew
(Just x, Nothing) -> return (Data.Text.unpack x)
(Nothing, Just x) -> return ('\x00' : Data.Text.unpack x)
getHandle = do
port <- fmap Network.UnixSocket path
Network.connectTo "localhost" port
:
\clearpage
\subsubsection{TCP}
The {\sc tcp} transport has three parameters:
\begin{itemize}
\item {\tt host} -- optional, defaults to {\tt "localhost"}
\item {\tt port} -- unsigned 16-bit integer
\item {\tt family} -- optional, defaults to {\sc unspec}, choices are
{\tt "ipv4"} or {\tt "ipv6"}
\end{itemize}
:d DBus.Connection.Transport
tcp :: Transport
tcp = Transport "tcp" connectTCP
connectTCP :: Address -> IO Socket
connectTCP a = getHandle >>= connectHandle where
params = addressParameters a
param key = Data.Map.lookup key params
getHandle = do
port <- getPort
family <- getFamily
addrs <- getAddresses family
sock<- openSocket port addrs
Network.Socket.socketToHandle sock System.IO.ReadWriteMode
hostname = maybe "localhost" Data.Text.unpack (param "host")
unknownFamily x = concat ["Unknown socket family for TCP transport: ", show x]
getFamily = case param "family" of
Just "ipv4" -> return Network.Socket.AF_INET
Just "ipv6" -> return Network.Socket.AF_INET6
Nothing -> return Network.Socket.AF_UNSPEC
Just x -> connectionError (unknownFamily x)
missingPort = "TCP transport requires the `port' parameter."
badPort x = concat ["Invalid socket port for TCP transport: ", show x]
getPort = case param "port" of
Nothing -> connectionError missingPort
Just x -> case parse parseWord16 "" (Data.Text.unpack x) of
Right x' -> return (Network.Socket.PortNum x')
Left _ -> connectionError (badPort x)
:
Parsing the port is a bit complicated; assuming every character is an ASCII
digit, the port is converted to an {\tt Integer} and confirmed valid.
{\tt PortNumber} is expected to be in big-endian byte order, so the parsed
value must be converted from host order using {\tt Data.Binary}.
:d DBus.Connection.Transport
parseWord16 = do
chars <- many1 digit
eof
let value = read chars :: Integer
unless (value > 0 && value <= 65535) $
-- Calling 'fail' is acceptable here, because Parsec 2
-- offers no other error reporting mechanism, and
-- implements 'fail'.
fail "bad port"
let word = fromIntegral value
return (Data.Binary.Get.runGet Data.Binary.Get.getWord16host (Data.Binary.Put.runPut (Data.Binary.Put.putWord16be word)))
:
:d DBus.Connection.Transport
getAddresses family = do
let hints = Network.Socket.defaultHints
{ Network.Socket.addrFlags = [Network.Socket.AI_ADDRCONFIG]
, Network.Socket.addrFamily = family
, Network.Socket.addrSocketType = Network.Socket.Stream
}
Network.Socket.getAddrInfo (Just hints) (Just hostname) Nothing
:
The {\tt SockAddr} values returned from {\tt getAddrInfo} don't have any
port set, so it must be manually changed to whatever was in the {\tt port}
option.
:d DBus.Connection.Transport
setPort port (Network.Socket.SockAddrInet _ x) = Network.Socket.SockAddrInet port x
setPort port (Network.Socket.SockAddrInet6 _ x y z) = Network.Socket.SockAddrInet6 port x y z
setPort _ addr = addr
:
{\tt getAddrInfo} returns multiple addresses; each one is tried in turn,
until a valid address is found. If none are found, or are usable, an
exception will be thrown.
:d DBus.Connection.Transport
openSocket _ [] = connectionError ("Failed to open socket to address " ++ show a)
openSocket port (addr:addrs) = Control.Exception.catch (openSocket' port addr) $
\(Control.Exception.SomeException _) -> openSocket port addrs
openSocket' port addr = do
sock <- Network.Socket.socket (Network.Socket.addrFamily addr)
(Network.Socket.addrSocketType addr)
(Network.Socket.addrProtocol addr)
Network.Socket.connect sock . setPort port . Network.Socket.addrAddress $ addr
return sock
:
\clearpage
\subsection{Authentication}
\begin{multicols}{2}
TODO
\vfill
\columnbreak
:d DBus.Connection.Authentication
authenticate :: Socket
-> [Mechanism]
-> IO Bool
authenticate s mechanisms = do
socketPut s (Data.ByteString.pack [0])
let loop [] = return False
loop ((Mechanism m):next) = do
success <- m s
if success
then return True
else loop next
loop mechanisms
:
:d DBus.Connection.Authentication
newtype Mechanism = Mechanism (Socket -> IO Bool)
mechanism :: (Socket -> IO Bool) -> Mechanism
mechanism = Mechanism
data Auth a = Auth
{ unAuth :: Socket -> IO a
}
instance Monad Auth where
return a = Auth (\_ -> return a)
m >>= k = Auth $ \s -> do
x <- unAuth m s
unAuth (k x) s
liftIO :: IO a -> Auth a
liftIO io = Auth (\_ -> io)
:
\end{multicols}
\begin{multicols}{2}
The authentication protocol is based on {\sc ascii} text.
\vfill
\columnbreak
:d DBus.Connection.Authentication
putLine :: String -> Auth ()
putLine line = Auth $ \s -> do
let pack = Data.ByteString.Char8.pack
socketPut s (pack (line ++ "\r\n"))
getLine :: Auth String
getLine = Auth $ \s -> do
let head = Data.ByteString.Char8.head
let getchr = liftM head (socketGet s 1)
raw <- readUntil "\r\n" getchr
return (dropEnd 2 raw)
:
\end{multicols}
\clearpage
\subsubsection{EXTERNAL}
Although the official spec describes only the {\tt DBUS\_COOKIE\_SHA1}
mechanism, I have never seen it used in real life. Everything actually uses
{\tt EXTERNAL}, so that's what this library implements.
:d DBus.Connection.Authentication
external :: Mechanism
external = Mechanism $ unAuth $ do
uid <- liftIO System.Posix.User.getRealUserID
let token = concatMap (printf "%02X" . ord) (show uid)
putLine ("AUTH EXTERNAL " ++ token)
resp <- getLine
case takeWhile (/= ' ') resp of
"OK" -> do
putLine "BEGIN"
return True
_ -> return False
:
\clearpage
\subsection{Sending and receiving messages}
Sending a message will increment the connection's internal serial state.
The second parameter is present to allow registration of a callback before
the message has actually been sent, which avoids race conditions in
multi-threaded clients.
:d DBus.Connection
|apidoc DBus.Connection.send|
send :: Message msg => Connection -> msg -> (Serial -> IO a) -> IO (Either MarshalError a)
send connection msg io = do
serial <- nextSerial connection
case marshalMessage LittleEndian serial msg of
Right bytes -> do
let sock = connectionSocket connection
let lock = connectionWriteLock connection
result <- io serial
withMVar lock (\_ -> socketPut sock bytes)
return (Right result)
Left err -> return (Left err)
nextSerial :: Connection -> IO Serial
nextSerial connection = atomicModifyIORef
(connectionSerial connection)
(\serial@(Serial x) -> (Serial (x + 1), serial))
:
Messages are received wrapped in a {\tt ReceivedMessage} value. If an
error is encountered while unmarshaling, an exception will be thrown.
:d DBus.Connection
|apidoc DBus.Connection.receive|
receive :: Connection -> IO (Either UnmarshalError ReceivedMessage)
receive connection = do
let sock = connectionSocket connection
let lock = connectionReadLock connection
withMVar lock (\_ -> unmarshalMessageM (socketGet sock))
: