RPC Server #103
5 changed files with 239 additions and 38 deletions
|
@ -11,23 +11,26 @@
|
||||||
|
|
||||||
module Zenith.RPC where
|
module Zenith.RPC where
|
||||||
|
|
||||||
|
import Control.Exception (try)
|
||||||
|
import Control.Monad.IO.Class (liftIO)
|
||||||
import Data.Aeson
|
import Data.Aeson
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import Data.Typeable
|
|
||||||
import GHC.Generics (Generic)
|
|
||||||
import Servant
|
import Servant
|
||||||
import ZcashHaskell.Types (RpcError(..), RpcResponse(..))
|
import ZcashHaskell.Types (ZebraGetBlockChainInfo(..), ZebraGetInfo(..))
|
||||||
|
import Zenith.Core (checkBlockChain, checkZebra)
|
||||||
import Zenith.Types
|
import Zenith.Types
|
||||||
( Config(..)
|
( Config(..)
|
||||||
, RpcCall(..)
|
, RpcCall(..)
|
||||||
|
, ZenithInfo(..)
|
||||||
, ZenithMethod(..)
|
, ZenithMethod(..)
|
||||||
, ZenithParams(..)
|
, ZenithParams(..)
|
||||||
|
, ZenithResponse(..)
|
||||||
)
|
)
|
||||||
|
|
||||||
type ZenithRPC
|
type ZenithRPC
|
||||||
= "status" :> Get '[ JSON] Value :<|> BasicAuth "zenith-realm" Bool :> ReqBody
|
= "status" :> Get '[ JSON] Value :<|> BasicAuth "zenith-realm" Bool :> ReqBody
|
||||||
'[ JSON]
|
'[ JSON]
|
||||||
RpcCall :> Post '[ JSON] (RpcResponse Value)
|
RpcCall :> Post '[ JSON] ZenithResponse
|
||||||
|
|
||||||
zenithServer :: Config -> Server ZenithRPC
|
zenithServer :: Config -> Server ZenithRPC
|
||||||
zenithServer config = getinfo :<|> handleRPC
|
zenithServer config = getinfo :<|> handleRPC
|
||||||
|
@ -39,40 +42,38 @@ zenithServer config = getinfo :<|> handleRPC
|
||||||
[ "version" .= ("0.7.0.0-beta" :: String)
|
[ "version" .= ("0.7.0.0-beta" :: String)
|
||||||
, "network" .= ("testnet" :: String)
|
, "network" .= ("testnet" :: String)
|
||||||
]
|
]
|
||||||
handleRPC :: Bool -> RpcCall -> Handler (RpcResponse Value)
|
handleRPC :: Bool -> RpcCall -> Handler ZenithResponse
|
||||||
handleRPC isAuth req =
|
handleRPC isAuth req =
|
||||||
case method req of
|
case method req of
|
||||||
UnknownMethod ->
|
UnknownMethod ->
|
||||||
return $
|
return $ ErrorResponse (callId req) (-32601) "Method not found"
|
||||||
MakeRpcResponse
|
|
||||||
(Just $ RpcError (-32601) "Method not found")
|
|
||||||
(callId req)
|
|
||||||
Nothing
|
|
||||||
GetInfo ->
|
GetInfo ->
|
||||||
case parameters req of
|
case parameters req of
|
||||||
BlankParams ->
|
BlankParams -> do
|
||||||
|
let host = c_zebraHost config
|
||||||
|
let port = c_zebraPort config
|
||||||
|
zInfo <-
|
||||||
|
liftIO $ try $ checkZebra host port :: Handler
|
||||||
|
(Either IOError ZebraGetInfo)
|
||||||
|
case zInfo of
|
||||||
|
Left e ->
|
||||||
return $
|
return $
|
||||||
MakeRpcResponse
|
ErrorResponse (callId req) (-32000) "Zebra not available"
|
||||||
Nothing
|
Right zI -> do
|
||||||
|
bInfo <-
|
||||||
|
liftIO $ try $ checkBlockChain host port :: Handler
|
||||||
|
(Either IOError ZebraGetBlockChainInfo)
|
||||||
|
case bInfo of
|
||||||
|
Left e1 ->
|
||||||
|
return $
|
||||||
|
ErrorResponse (callId req) (-32000) "Zebra not available"
|
||||||
|
Right bI ->
|
||||||
|
return $
|
||||||
|
InfoResponse
|
||||||
(callId req)
|
(callId req)
|
||||||
(Just $ object ["data" .= ("Here's your info" :: String)])
|
(ZenithInfo "0.7.0.0-beta" (zgb_net bI) (zgi_build zI))
|
||||||
_anyOtherParams ->
|
_anyOtherParams ->
|
||||||
return $
|
return $ ErrorResponse (callId req) (-32602) "Invalid params"
|
||||||
MakeRpcResponse
|
|
||||||
(Just $ RpcError (-32602) "Invalid params")
|
|
||||||
(callId req)
|
|
||||||
Nothing
|
|
||||||
Test ->
|
|
||||||
case parameters req of
|
|
||||||
TestParams x ->
|
|
||||||
return $
|
|
||||||
MakeRpcResponse Nothing (callId req) (Just $ object ["data" .= x])
|
|
||||||
_anyOtherParams ->
|
|
||||||
return $
|
|
||||||
MakeRpcResponse
|
|
||||||
(Just $ RpcError (-32602) "Invalid params")
|
|
||||||
(callId req)
|
|
||||||
Nothing
|
|
||||||
|
|
||||||
authenticate :: Config -> BasicAuthCheck Bool
|
authenticate :: Config -> BasicAuthCheck Bool
|
||||||
authenticate config = BasicAuthCheck check
|
authenticate config = BasicAuthCheck check
|
||||||
|
|
|
@ -18,11 +18,13 @@ import Data.Maybe (fromMaybe)
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import qualified Data.Text.Encoding as E
|
import qualified Data.Text.Encoding as E
|
||||||
import Data.Text.Encoding.Error (lenientDecode)
|
import Data.Text.Encoding.Error (lenientDecode)
|
||||||
|
import qualified Data.Vector as V
|
||||||
import Database.Persist.TH
|
import Database.Persist.TH
|
||||||
import GHC.Generics
|
import GHC.Generics
|
||||||
import ZcashHaskell.Types
|
import ZcashHaskell.Types
|
||||||
( OrchardSpendingKey(..)
|
( OrchardSpendingKey(..)
|
||||||
, Phrase(..)
|
, Phrase(..)
|
||||||
|
, RpcError(..)
|
||||||
, Rseed(..)
|
, Rseed(..)
|
||||||
, SaplingSpendingKey(..)
|
, SaplingSpendingKey(..)
|
||||||
, Scope(..)
|
, Scope(..)
|
||||||
|
@ -100,15 +102,17 @@ data Config = Config
|
||||||
-- ** Zenith methods
|
-- ** Zenith methods
|
||||||
data ZenithMethod
|
data ZenithMethod
|
||||||
= GetInfo
|
= GetInfo
|
||||||
| Test
|
|
||||||
| UnknownMethod
|
| UnknownMethod
|
||||||
deriving (Eq, Prelude.Show)
|
deriving (Eq, Prelude.Show)
|
||||||
|
|
||||||
|
instance ToJSON ZenithMethod where
|
||||||
|
toJSON GetInfo = Data.Aeson.String "getinfo"
|
||||||
|
toJSON UnknownMethod = Data.Aeson.Null
|
||||||
|
|
||||||
instance FromJSON ZenithMethod where
|
instance FromJSON ZenithMethod where
|
||||||
parseJSON =
|
parseJSON =
|
||||||
withText "ZenithMethod" $ \case
|
withText "ZenithMethod" $ \case
|
||||||
"getinfo" -> pure GetInfo
|
"getinfo" -> pure GetInfo
|
||||||
"test" -> pure Test
|
|
||||||
_ -> pure UnknownMethod
|
_ -> pure UnknownMethod
|
||||||
|
|
||||||
data ZenithParams
|
data ZenithParams
|
||||||
|
@ -117,6 +121,71 @@ data ZenithParams
|
||||||
| TestParams !T.Text
|
| TestParams !T.Text
|
||||||
deriving (Eq, Prelude.Show)
|
deriving (Eq, Prelude.Show)
|
||||||
|
|
||||||
|
instance ToJSON ZenithParams where
|
||||||
|
toJSON BlankParams = Data.Aeson.Array V.empty
|
||||||
|
toJSON BadParams = Data.Aeson.Null
|
||||||
|
toJSON (TestParams t) = Data.Aeson.Array $ V.fromList [Data.Aeson.String t]
|
||||||
|
|
||||||
|
data ZenithResponse
|
||||||
|
= InfoResponse !T.Text !ZenithInfo
|
||||||
|
| ErrorResponse !T.Text !Double !T.Text
|
||||||
|
deriving (Eq, Prelude.Show)
|
||||||
|
|
||||||
|
instance ToJSON ZenithResponse where
|
||||||
|
toJSON (InfoResponse t i) =
|
||||||
|
object ["jsonrpc" .= ("2.0" :: String), "id" .= t, "result" .= i]
|
||||||
|
toJSON (ErrorResponse i c m) =
|
||||||
|
object
|
||||||
|
[ "jsonrpc" .= ("2.0" :: String)
|
||||||
|
, "id" .= i
|
||||||
|
, "error" .= object ["code" .= c, "message" .= m]
|
||||||
|
]
|
||||||
|
|
||||||
|
instance FromJSON ZenithResponse where
|
||||||
|
parseJSON =
|
||||||
|
withObject "ZenithParams" $ \obj -> do
|
||||||
|
jr <- obj .: "jsonrpc"
|
||||||
|
i <- obj .: "id"
|
||||||
|
e <- obj .:? "error"
|
||||||
|
r <- obj .:? "result"
|
||||||
|
if jr /= ("2.0" :: String)
|
||||||
|
then fail "Malformed JSON"
|
||||||
|
else do
|
||||||
|
case e of
|
||||||
|
Nothing -> do
|
||||||
|
case r of
|
||||||
|
Nothing -> fail "Malformed JSON"
|
||||||
|
Just r1 ->
|
||||||
|
case r1 of
|
||||||
|
Object k -> do
|
||||||
|
v <- k .:? "version"
|
||||||
|
case (v :: Maybe String) of
|
||||||
|
Nothing -> fail "Unknown result"
|
||||||
|
Just v' -> do
|
||||||
|
k1 <- parseJSON r1
|
||||||
|
pure $ InfoResponse i k1
|
||||||
|
Array n -> undefined
|
||||||
|
_anyOther -> fail "Malformed JSON"
|
||||||
|
Just e1 -> pure $ ErrorResponse i (ecode e1) (emessage e1)
|
||||||
|
|
||||||
|
data ZenithInfo = ZenithInfo
|
||||||
|
{ zi_version :: !T.Text
|
||||||
|
, zi_network :: !ZcashNet
|
||||||
|
, zi_zebra :: !T.Text
|
||||||
|
} deriving (Eq, Prelude.Show)
|
||||||
|
|
||||||
|
instance ToJSON ZenithInfo where
|
||||||
|
toJSON (ZenithInfo v n z) =
|
||||||
|
object ["version" .= v, "network" .= n, "zebraVersion" .= z]
|
||||||
|
|
||||||
|
instance FromJSON ZenithInfo where
|
||||||
|
parseJSON =
|
||||||
|
withObject "ZenithInfo" $ \obj -> do
|
||||||
|
v <- obj .: "version"
|
||||||
|
n <- obj .: "network"
|
||||||
|
z <- obj .: "zebraVersion"
|
||||||
|
pure $ ZenithInfo v n z
|
||||||
|
|
||||||
-- | A type to model Zenith RPC calls
|
-- | A type to model Zenith RPC calls
|
||||||
data RpcCall = RpcCall
|
data RpcCall = RpcCall
|
||||||
{ jsonrpc :: !T.Text
|
{ jsonrpc :: !T.Text
|
||||||
|
@ -125,6 +194,10 @@ data RpcCall = RpcCall
|
||||||
, parameters :: !ZenithParams
|
, parameters :: !ZenithParams
|
||||||
} deriving (Eq, Prelude.Show)
|
} deriving (Eq, Prelude.Show)
|
||||||
|
|
||||||
|
instance ToJSON RpcCall where
|
||||||
|
toJSON (RpcCall jr i m p) =
|
||||||
|
object ["jsonrpc" .= jr, "id" .= i, "method" .= m, "params" .= p]
|
||||||
|
|
||||||
instance FromJSON RpcCall where
|
instance FromJSON RpcCall where
|
||||||
parseJSON =
|
parseJSON =
|
||||||
withObject "RpcCall" $ \obj -> do
|
withObject "RpcCall" $ \obj -> do
|
||||||
|
@ -138,9 +211,6 @@ instance FromJSON RpcCall where
|
||||||
if null (p :: [Value])
|
if null (p :: [Value])
|
||||||
then pure $ RpcCall v i GetInfo BlankParams
|
then pure $ RpcCall v i GetInfo BlankParams
|
||||||
else pure $ RpcCall v i GetInfo BadParams
|
else pure $ RpcCall v i GetInfo BadParams
|
||||||
Test -> do
|
|
||||||
p <- obj .: "params"
|
|
||||||
pure $ RpcCall v i Test (TestParams $ head p)
|
|
||||||
|
|
||||||
-- ** `zebrad`
|
-- ** `zebrad`
|
||||||
-- | Type for modeling the tree state response
|
-- | Type for modeling the tree state response
|
||||||
|
|
100
test/ServerSpec.hs
Normal file
100
test/ServerSpec.hs
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
|
||||||
|
import Control.Concurrent (forkIO, threadDelay)
|
||||||
|
import Control.Exception (SomeException, try)
|
||||||
|
import Data.Aeson
|
||||||
|
import qualified Data.ByteString as BS
|
||||||
|
import Data.Configurator
|
||||||
|
import qualified Data.Text as T
|
||||||
|
import qualified Data.Text.Encoding as E
|
||||||
|
import Network.HTTP.Simple
|
||||||
|
import Network.Wai.Handler.Warp (run)
|
||||||
|
import Servant
|
||||||
|
import Test.HUnit
|
||||||
|
import Test.Hspec
|
||||||
|
import ZcashHaskell.Types (ZcashNet(..))
|
||||||
|
import Zenith.RPC (ZenithRPC(..), authenticate, zenithServer)
|
||||||
|
import Zenith.Types
|
||||||
|
( Config(..)
|
||||||
|
, RpcCall(..)
|
||||||
|
, ZenithInfo(..)
|
||||||
|
, ZenithMethod(..)
|
||||||
|
, ZenithParams(..)
|
||||||
|
, ZenithResponse(..)
|
||||||
|
)
|
||||||
|
|
||||||
|
main :: IO ()
|
||||||
|
main = do
|
||||||
|
config <- load ["$(HOME)/Zenith/zenith.cfg"]
|
||||||
|
dbFilePath <- require config "dbFilePath"
|
||||||
|
nodeUser <- require config "nodeUser"
|
||||||
|
nodePwd <- require config "nodePwd"
|
||||||
|
zebraPort <- require config "zebraPort"
|
||||||
|
zebraHost <- require config "zebraHost"
|
||||||
|
nodePort <- require config "nodePort"
|
||||||
|
let myConfig = Config dbFilePath zebraHost zebraPort nodeUser nodePwd nodePort
|
||||||
|
hspec $ do
|
||||||
|
describe "RPC methods" $ do
|
||||||
|
beforeAll_ (startAPI myConfig) $ do
|
||||||
|
describe "getinfo" $ do
|
||||||
|
it "bad credentials" $ do
|
||||||
|
res <-
|
||||||
|
makeZenithCall
|
||||||
|
"127.0.0.1"
|
||||||
|
nodePort
|
||||||
|
"baduser"
|
||||||
|
"idontknow"
|
||||||
|
GetInfo
|
||||||
|
BlankParams
|
||||||
|
res `shouldBe` Left "Invalid credentials"
|
||||||
|
it "correct credentials" $ do
|
||||||
|
res <-
|
||||||
|
makeZenithCall
|
||||||
|
"127.0.0.1"
|
||||||
|
nodePort
|
||||||
|
nodeUser
|
||||||
|
nodePwd
|
||||||
|
GetInfo
|
||||||
|
BlankParams
|
||||||
|
case res of
|
||||||
|
Left e -> assertFailure e
|
||||||
|
Right r ->
|
||||||
|
r `shouldBe`
|
||||||
|
InfoResponse "zh" (ZenithInfo "0.7.0.0-beta" TestNet "v1.8.0")
|
||||||
|
|
||||||
|
startAPI :: Config -> IO ()
|
||||||
|
startAPI config = do
|
||||||
|
putStrLn "Starting test RPC server"
|
||||||
|
let ctx = authenticate config :. EmptyContext
|
||||||
|
forkIO $
|
||||||
|
run (c_zenithPort config) $
|
||||||
|
serveWithContext
|
||||||
|
(Servant.Proxy :: Servant.Proxy ZenithRPC)
|
||||||
|
ctx
|
||||||
|
(zenithServer config)
|
||||||
|
threadDelay 1000000
|
||||||
|
putStrLn "Test server is up!"
|
||||||
|
|
||||||
|
-- | Make a Zebra RPC call
|
||||||
|
makeZenithCall ::
|
||||||
|
T.Text -- ^ Hostname for `zebrad`
|
||||||
|
-> Int -- ^ Port for `zebrad`
|
||||||
|
-> BS.ByteString
|
||||||
|
-> BS.ByteString
|
||||||
|
-> ZenithMethod -- ^ RPC method to call
|
||||||
|
-> ZenithParams -- ^ List of parameters
|
||||||
|
-> IO (Either String ZenithResponse)
|
||||||
|
makeZenithCall host port usr pwd m params = do
|
||||||
|
let payload = RpcCall "2.0" "zh" m params
|
||||||
|
let myRequest =
|
||||||
|
setRequestBodyJSON payload $
|
||||||
|
setRequestPort port $
|
||||||
|
setRequestHost (E.encodeUtf8 host) $
|
||||||
|
setRequestBasicAuth usr pwd $ setRequestMethod "POST" defaultRequest
|
||||||
|
r <- httpJSONEither myRequest
|
||||||
|
case getResponseStatusCode r of
|
||||||
|
403 -> return $ Left "Invalid credentials"
|
||||||
|
200 ->
|
||||||
|
case getResponseBody r of
|
||||||
|
Left e -> return $ Left $ show e
|
||||||
|
Right r' -> return $ Right r'
|
|
@ -1 +1 @@
|
||||||
Subproject commit e8074419cfb54559a4c09731ad2448d5930869a2
|
Subproject commit cc72fadef36ee8ac235dfd9b8bea4de4ce3122bf
|
30
zenith.cabal
30
zenith.cabal
|
@ -166,3 +166,33 @@ test-suite zenith-tests
|
||||||
, zenith
|
, zenith
|
||||||
pkgconfig-depends: rustzcash_wrapper
|
pkgconfig-depends: rustzcash_wrapper
|
||||||
default-language: Haskell2010
|
default-language: Haskell2010
|
||||||
|
|
||||||
|
test-suite zenithserver-tests
|
||||||
|
type: exitcode-stdio-1.0
|
||||||
|
ghc-options: -threaded -rtsopts -with-rtsopts=-N
|
||||||
|
main-is: ServerSpec.hs
|
||||||
|
hs-source-dirs:
|
||||||
|
test
|
||||||
|
build-depends:
|
||||||
|
base >=4.12 && <5
|
||||||
|
, bytestring
|
||||||
|
, aeson
|
||||||
|
, configurator
|
||||||
|
, monad-logger
|
||||||
|
, data-default
|
||||||
|
, sort
|
||||||
|
, text
|
||||||
|
, time
|
||||||
|
, http-conduit
|
||||||
|
, persistent
|
||||||
|
, persistent-sqlite
|
||||||
|
, hspec
|
||||||
|
, hexstring
|
||||||
|
, warp
|
||||||
|
, servant-server
|
||||||
|
, HUnit
|
||||||
|
, directory
|
||||||
|
, zcash-haskell
|
||||||
|
, zenith
|
||||||
|
pkgconfig-depends: rustzcash_wrapper
|
||||||
|
default-language: Haskell2010
|
||||||
|
|
Loading…
Reference in a new issue