{-# LANGUAGE TupleSections #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE OverloadedStrings #-} {-| This module implements the XML-RPC based login protocol provided by Linden Labs' Second Life system. Since Linden Labs' login requires SSL which is not widely implemented in Haskell, this requires an stunnel proxy listening on port 8001 of localhost. -} module Network.Metaverse.Login ( MVToken(..), loginXmlRpc ) where import Data.Char import Data.Digest.MD5 import Data.Maybe import Data.UUID import Data.Word import System.Info.MAC import Network.HTTP.Enumerator import Text.XML.Expat.Tree import Text.XML.Expat.Format import qualified Data.ByteString as B import qualified Data.ByteString.Lazy as L import Data.ByteString.Char8 () -- for string instance data MVToken = MVToken { tokenAgentID :: UUID, tokenSimIP :: String, tokenSimPort :: Word16, tokenSessionID :: UUID, tokenCircuitCode :: Word32 } deriving Show {- Convert a list of bytes to a hexadecimal string; needed for building the login packet -} toHexString :: [Word8] -> String toHexString = concatMap toHex where toHex n = [ hexDigit (n `div` 16), hexDigit (n `mod` 16) ] hexDigit d | d < 10 = chr (ord '0' + fromIntegral d) | otherwise = chr (ord 'a' + fromIntegral d - 10) {- Login implementation. This implements the pure XML-RPC login sequence for Second Life (but not the avatar presence piece, which is left until later.) -} hashPassword :: String -> String hashPassword = ("$1$" ++) . toHexString . hash . map (fromIntegral . ord) loginRequest :: String -> String -> String -> String -> L.ByteString loginRequest first last passwd ethmac = format $ Element ("methodCall" :: String) [] [ Element "methodName" [] [ Text "login_to_simulator" ], Element "params" [] [ Element "param" [] [ Element "value" [] [ Element "struct" [] [ Element "member" [] [ Element "name" [] [ Text "first" ], Element "value" [] [ Element "string" [] [ Text first ]] ], Element "member" [] [ Element "name" [] [ Text "last" ], Element "value" [] [ Element "string" [] [ Text last ]] ], Element "member" [] [ Element "name" [] [ Text "passwd" ], Element "value" [] [ Element "string" [] [ Text (hashPassword passwd) ]] ], Element "member" [] [ Element "name" [] [ Text "start" ], Element "value" [] [ Element "string" [] [ Text "home" ]] ], Element "member" [] [ Element "name" [] [ Text "channel" ], Element "value" [] [ Element "string" [] [ Text "mvclient" ]] ], Element "member" [] [ Element "name" [] [ Text "version" ], Element "value" [] [ Element "string" [] [ Text "0.3" ]] ], Element "member" [] [ Element "name" [] [ Text "platform" ], Element "value" [] [ Element "string" [] [ Text "Lin" ]] ], Element "member" [] [ Element "name" [] [ Text "mac" ], Element "value" [] [ Element "string" [] [ Text ethmac ]] ], Element "member" [] [ Element "name" [] [ Text "agree_to_tos" ], Element "value" [] [ Element "boolean" [] [ Text "1" ]] ], Element "member" [] [ Element "name" [] [ Text "read_critical" ], Element "value" [] [ Element "boolean" [] [ Text "1" ]] ], Element "member" [] [ Element "name" [] [ Text "id0" ], Element "value" [] [ Element "string" [] [ Text ethmac ]] ], Element "member" [] [ Element "name" [] [ Text "last_exec_event" ], Element "value" [] [ Element "int" [] [ Text "0" ]] ], Element "member" [] [ Element "name" [] [ Text "options" ], Element "value" [] [ Element "array" [] [Element "data" [] [ Element "value" [] [ Element "string" [] [ Text "inventory-root" ]], Element "value" [] [ Element "string" [] [ Text "adult_compliant" ]] ]]] ] ]]]] ] stripWhitespace :: Node String String -> Maybe (Node String String) stripWhitespace (Element n a c) = Just $ Element n a (mapMaybe stripWhitespace c) stripWhitespace (Text t) | all isSpace t = Nothing | otherwise = Just $ Text t tokenFromTree :: Node String String -> MVToken tokenFromTree (Element "methodResponse" [] [ Element "params" [] [ Element "param" [] [ Element "value" [] [ Element "struct" [] members ]]]]) = case lookupMember "login" members of "true" -> MVToken { tokenAgentID = read (lookupMember "agent_id" members), tokenSimIP = lookupMember "sim_ip" members, tokenSimPort = read (lookupMember "sim_port" members), tokenSessionID = read (lookupMember "session_id" members), tokenCircuitCode = read (lookupMember "circuit_code" members) } _ -> error $ "Login failed: " ++ lookupMember "reason" members where lookupMember name members = head (catMaybes (map (field name) members)) field name (Element "member" [] [ Element "name" [] [ Text n ], Element "value" [] [ Element _ [] [ Text v ]]]) | name == n = Just v | otherwise = Nothing field name _ = Nothing tokenFromTree _ = error "Bad XML format in response" loginXmlRpc :: String -> String -> String -> IO MVToken loginXmlRpc first last passwd = mac >>= maybe (error "Couldn't get MAC address") doLogin where doLogin ethmac = do let msg = loginRequest first last passwd (show ethmac) urlReq <- parseUrl loginURL let req = urlReq { method = "POST", requestBody = RequestBodyLBS msg, requestHeaders = ("Content-type", "text/xml") : requestHeaders urlReq } resp <- withManager $ httpLbsRedirect req let presult = parse' defaultParseOptions (B.concat $ L.toChunks $ responseBody resp) case presult of Left err -> error $ "Bad response: " ++ show err Right tree -> return (tokenFromTree (fromJust $ stripWhitespace tree)) loginURL = "https://login.agni.lindenlab.com/cgi-bin/login.cgi"