XML-RPC in Rust and Python
- 3 minutes read - 510 wordsA lazy Sunday afternoon and my interest was piqued by XML-RPC
Client
A very basic XML-RPC client wrapped in a Cloud Functions function:
main.py
:
import functions_framework
import os
import xmlrpc.client
endpoint = os.get_env("ENDPOINT")
proxy = xmlrpc.client.ServerProxy(endpoint)
@functions_framework.http
def add(request):
print(request)
rqst = request.get_json(silent=True)
resp = proxy.add(
{"x":{
"real":rqst["x"]["real"],
"imag":rqst["x"]["imag"]
},
"y":{
"real":rqst["y"]["real"],
"imag":rqst["y"]["imag"]
}
})
return resp
requirements.txt
:
functions-framework==3.*
Run it:
python3 -m venv venv
source venv/bin/activate
python3 -m pip install --requirement requirements.txt
export ENDPOINT="..."
python3 main.py
Server
Forcing myself to go Rust first and there’s an (old) xml-rpc crate.
main.rs
:
use serde::{Deserialize, Serialize};
use std::net::{IpAddr,Ipv4Addr,SocketAddr};
use xml_rpc::Server;
#[derive(Debug, Deserialize, Serialize)]
struct Complex {
real: f64,
imag: f64,
}
#[derive(Debug, Deserialize, Serialize)]
struct Add {
x: Complex,
y: Complex,
}
#[derive(Debug, Deserialize, Serialize)]
struct Result {
result: Complex,
}
fn main() {
let mut server = Server::new();
// Add to complex numbers
server.register_simple("add", |rqst: Add| {
Ok(Result {
result: Complex {
real: rqst.x.real + rqst.y.real,
imag: rqst.x.imag + rqst.y.imag,
},
})
});
// Defaults to 8080
let address = SocketAddr::new(
IpAddr::V4(
Ipv4Addr::new(0,0,0,0)
), 8080);
let bound_server = server
.bind(&address)
.expect("success");
bound_server.run();
}
Cargo.toml
:
[dependencies]
rouille = "3.6.2"
serde = {version = "1.0.206", features = ["derive"]}
serde_xml = "0.9.1"
xml-rpc = "0.0.12"
cargo run
Test
- Use
curl
to invoke the Cloud Functions function (locally|remote). - The Cloud Function function includes the XML-RPC client
- The XML-RPC client invokes the
add
RPC against the XML-RPC server - The XML-RPC server performs the addition and returns the result
- The Cloud Function function returns the response to `curl.
Local
RPC_PORT="8080"
FFP_PORT="8888"
# Used by the Function to access the XML-RPC server
export ENDPOINT="http://localhost:${RPC_PORT}"
# Run Functions Framework on port 8888 to avoid Rust server
functions-framework-python \
--port=8888 \
--target=add
Then, from another shell, invoke curl
:
curl \
--request POST \
--header "Content-Type: application/json" \
-d '{"x":{"real":5.0,"imag":5.0},"y":{"real":5.0,"imag":5.0}}' \
http://localhost:${FFP_PORT}
Should yield:
{"result":{"imag":10.0,"real":10.0}}
Remote
In order to deploy the Cloud Functions function we need to be able to access the XML-RPC server remotely.
Tailscale is perfect (!) for this:
RPC_PORT="8080"
tailscale funnel --https 443 ${RPC_PORT}
Will publish the XML-RPC Rust server’s port (RPC_PORT
) to the Internet accessible via your Tailscale machine’s fully-qualified DNS on port 443.
Something of the form: {machine}.{tailnet}.ts.net:443
This becomes the replacement value for the environment variable ENTRYPOINT
(see below).
And we need to deploy the Cloud Function:
PROJECT="..."
REGION="..."
NAME="add"
# Used by the Function to access the XML-RPC Server
ENTRYPOINT="https://{machine}.{tailnet}.ts.net:443"
gcloud functions deploy ${NAME} \
--no-gen2 \
--runtime="python312" \
--source=${PWD} \
--entrypoint="add" \
--set-env-vars=ENTRYPOINT=${ENDPOINT} \
--trigger-http \
--no-allow-unauthenticated \
--region=${REGION} \
--project=${PROJECT}
Once that suceeeds, from another shell invoke curl
:
PROJECT="..."
REGION="..."
NAME="..."
# Used by curl to invoke Cloud Functions function
ENDPOINT=$(\
gcloud functions describe ${NAME} \
--region=${REGION} \
--project=${PROJECT} \
--format="value(httpsTrigger.url)")
TOKEN=$(gcloud auth print-identity-token)
curl \
--request POST \
--header "Content-Type: application/json" \
--header "Authorization: Bearer ${TOKEN}" \
-d '{"x":{"real":5.0,"imag":5.0},"y":{"real":5.0,"imag":5.0}}' \
${ENDPOINT}
Should yield:
{"result":{"imag":10.0,"real":10.0}}
Tidy
Delete the Cloud Functions function when it’s no longer needed:
gcloud functions delete ${NAME} \
--region=${REGION} \
--project=${PROJECT} \
--quiet
Don’t forget to kill the Tailscale Funnel to terminate public access (CTRL-C) to the XML-RPC server.