Skip to main content

consortium/
gateway.rs

1//! Gateway node logic.
2//!
3//! Rust implementation of `ClusterShell.Gateway`.
4//!
5//! A gateway is a tree node that relays commands between the admin node and
6//! the subtree below it. This module contains the target (non-initiator)
7//! side of a tree channel:
8//!
9//! - [`GatewayChannel`]: high-level logic for gateways — accepts the channel
10//!   greeting, receives the topology configuration, ACKs it, and converts
11//!   processing failures into Error replies followed by channel close
12//!   (mirrors `Gateway.GatewayChannel`).
13//! - [`TreeWorkerResponder`]: builds routing notifications for the gateway
14//!   channel with the corrected `RoutingMessage` construction of upstream
15//!   commit 490323d.
16
17use crate::communication::{Channel, Message, MessageProcessingError};
18
19/// Routing event data handed to a [`TreeWorkerResponder`].
20///
21/// Mirrors the `arg` dict of upstream `TreeWorkerResponder._ev_routing`
22/// (`{"event": ..., "gateway": ..., "targets": ...}`). There is deliberately
23/// NO `srcid` field: the responder always stamps its own source worker id.
24/// Upstream commit 490323d fixed `RoutingMessage(**arg, srcid=self.srcwkr)`
25/// (keyword-after-**kwargs is a syntax error before Python 3.5) into
26/// `RoutingMessage(srcid=self.srcwkr, **arg)`; modeling the event without a
27/// srcid makes that whole bug class unrepresentable.
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct RoutingEvent {
30    /// Routing event name (eg. "connect", "reroute").
31    pub event: String,
32    /// Gateway the event relates to.
33    pub gateway: String,
34    /// Target nodes of the event.
35    pub targets: String,
36}
37
38/// Gateway-side responder for a tree worker.
39///
40/// Mirrors `Gateway.TreeWorkerResponder`: it relays routing events from the
41/// worker running on the gateway to the admin node through the gateway
42/// channel.
43#[derive(Debug)]
44pub struct TreeWorkerResponder {
45    /// Source worker id stamped on routing messages (upstream `srcwkr`).
46    srcwkr: u64,
47}
48
49impl TreeWorkerResponder {
50    /// Create a responder for the given source worker id.
51    pub fn new(srcwkr: u64) -> Self {
52        Self { srcwkr }
53    }
54
55    /// Get the source worker id.
56    pub fn srcwkr(&self) -> u64 {
57        self.srcwkr
58    }
59
60    /// Build the routing message to send on the gateway channel.
61    ///
62    /// Corrected construction (upstream 490323d): `srcid` is always the
63    /// responder's own worker id; the event only carries
64    /// event/gateway/targets.
65    pub fn routing_message(&self, arg: &RoutingEvent) -> Message {
66        Message::routing(&arg.event, &arg.gateway, &arg.targets, self.srcwkr)
67    }
68
69    /// Relay a routing event onto the gateway channel (mirrors `_ev_routing`
70    /// sending the RoutingMessage on `gwchan`).
71    pub fn ev_routing(&self, chan: &mut Channel, arg: &RoutingEvent) {
72        chan.send(&self.routing_message(arg));
73    }
74}
75
76/// Short system hostname, used as fallback gateway node name (mirrors
77/// upstream `_getshorthostname()`).
78fn short_hostname() -> String {
79    hostname::get()
80        .map(|h| h.to_string_lossy().into_owned())
81        .unwrap_or_default()
82        .split('.')
83        .next()
84        .unwrap_or_default()
85        .to_string()
86}
87
88/// High level logic for gateways (mirrors `Gateway.GatewayChannel`).
89///
90/// This is the TARGET side of a tree channel: it receives the configuration
91/// from the admin node, ACKs it, and then accepts control messages. Any
92/// processing failure — including undecodable payloads such as a gateway
93/// message stamped with an unsupported pickle protocol — is turned into an
94/// Error reply sent back to the initiator, after which the channel is
95/// closed (fatal channel error).
96#[derive(Debug)]
97pub struct GatewayChannel {
98    /// The underlying communication channel (target end: `initiator=false`).
99    pub channel: Channel,
100    /// This gateway's node name (from CFG or system hostname fallback).
101    pub nodename: Option<String>,
102    /// Decoded topology payload received in CFG.
103    pub topology: Option<Vec<u8>>,
104}
105
106impl Default for GatewayChannel {
107    fn default() -> Self {
108        Self::new()
109    }
110}
111
112impl GatewayChannel {
113    /// Create a new gateway channel ready to accept communication.
114    pub fn new() -> Self {
115        Self {
116            channel: Channel::new(false), // target side
117            nodename: None,
118            topology: None,
119        }
120    }
121
122    /// Close the gateway channel (mirrors `GatewayChannel.close`).
123    pub fn close(&mut self) {
124        self.channel.close();
125    }
126
127    /// Feed raw incoming data to the channel, mirroring the read side of
128    /// upstream `Channel.ev_read` for a target: parse errors are reported
129    /// back to the initiator as an Error message and close the channel.
130    pub fn feed(&mut self, data: &str) {
131        match self.channel.feed(data) {
132            Ok(messages) => {
133                for msg in messages {
134                    self.recv(msg);
135                }
136            }
137            Err(err) => {
138                // target side: send ErrorMessage back + close (fatal)
139                self.channel.handle_channel_error("", &err);
140            }
141        }
142    }
143
144    /// Handle one incoming message (mirrors `GatewayChannel.recv`).
145    ///
146    /// Dispatch errors are reported back to the initiator as an Error
147    /// message, then the channel is closed — including configuration
148    /// payload decode failures (90d3195's gateway side: the admin later
149    /// reports this reason as gateway stderr).
150    pub fn recv(&mut self, msg: Message) {
151        if let Err(err) = self.dispatch(&msg) {
152            self.channel.send(&Message::error(&err.to_string()));
153            self.close();
154        }
155    }
156
157    /// Message dispatch, mirroring upstream's recv() branching:
158    /// END → close; setup → recv_ctl; opened → recv_cfg; CHA → open;
159    /// anything else → "unexpected message: %s".
160    fn dispatch(&mut self, msg: &Message) -> Result<(), MessageProcessingError> {
161        if msg.ident() == "END" {
162            self.close();
163            return Ok(());
164        }
165        if self.channel.setup {
166            return self.recv_ctl(msg);
167        }
168        if self.channel.opened {
169            return self.recv_cfg(msg);
170        }
171        if msg.ident() == "CHA" {
172            self.channel.opened = true;
173            return Ok(());
174        }
175        Err(MessageProcessingError::UnexpectedMessage(msg.to_string()))
176    }
177
178    /// Receive cfg/topology configuration (mirrors `GatewayChannel.recv_cfg`).
179    ///
180    /// Only a CFG message is acceptable here. The payload is decoded (a
181    /// failure — eg. an unsupported payload protocol version — propagates
182    /// and becomes the Error reply), the channel is marked set up, and an
183    /// ACK of the configuration message id is enqueued.
184    fn recv_cfg(&mut self, msg: &Message) -> Result<(), MessageProcessingError> {
185        if msg.ident() != "CFG" {
186            return Err(MessageProcessingError::UnexpectedMessage(msg.to_string()));
187        }
188
189        // gateway node name, with system hostname fallback (upstream warns
190        // and uses _getshorthostname() when not provided)
191        let gateway = match msg {
192            Message::Configuration { gateway, .. } => gateway.clone(),
193            _ => String::new(),
194        };
195        self.nodename = Some(if gateway.is_empty() {
196            short_hostname()
197        } else {
198            gateway
199        });
200
201        // topology payload — errors (invalid payload, unsupported protocol)
202        // propagate so recv() reports them to the initiator
203        self.topology = Some(msg.data_decode()?);
204
205        self.channel.setup = true;
206        // ACK the configuration message id
207        self.channel.send(&Message::ack(msg.msgid()));
208        Ok(())
209    }
210
211    /// Receive a control message (mirrors `GatewayChannel.recv_ctl`).
212    ///
213    /// The execution side of gateway control (spawning the responder
214    /// worker) is not part of this port; payload-bearing control messages
215    /// are validated so undecodable payloads are still reported back to the
216    /// initiator like upstream.
217    fn recv_ctl(&mut self, msg: &Message) -> Result<(), MessageProcessingError> {
218        if msg.has_payload() && msg.data().is_some() {
219            // validate the payload the same way upstream's data_decode does
220            let _ = msg.data_decode()?;
221        }
222        Ok(())
223    }
224}
225
226// ============================================================================
227// Tests
228// ============================================================================
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233    use crate::communication::{base64_encode, GW_PICKLE_PROTOCOL};
234
235    // -- TreeWorkerResponder / RoutingMessage construction (490323d) --------
236
237    /// Mirror of upstream 490323d ("Gateway: fix RoutingMessage construction
238    /// error"): the responder's srcid is always stamped on the routing
239    /// message; the event data cannot override or collide with it.
240    #[test]
241    fn test_responder_routing_message_stamps_srcid() {
242        let responder = TreeWorkerResponder::new(42);
243        let arg = RoutingEvent {
244            event: "connect".into(),
245            gateway: "gw2".into(),
246            targets: "node[10-20]".into(),
247        };
248        let msg = responder.routing_message(&arg);
249        assert_eq!(msg.ident(), "RTR");
250        assert_eq!(msg.srcid(), Some(42));
251        match msg {
252            Message::Routing {
253                event,
254                gateway,
255                targets,
256                ..
257            } => {
258                assert_eq!(event, "connect");
259                assert_eq!(gateway, "gw2");
260                assert_eq!(targets, "node[10-20]");
261            }
262            other => panic!("expected Routing, got {:?}", other),
263        }
264    }
265
266    /// The routing event struct carries no srcid at all (the upstream bug
267    /// class — a duplicate keyword argument — is unrepresentable).
268    #[test]
269    fn test_routing_event_has_no_srcid_field() {
270        let arg = RoutingEvent {
271            event: "reroute".into(),
272            gateway: "gw1".into(),
273            targets: "node1".into(),
274        };
275        let responder = TreeWorkerResponder::new(7);
276        let msg = responder.routing_message(&arg);
277        // srcid comes from the responder only
278        assert_eq!(msg.srcid(), Some(7));
279    }
280
281    /// ev_routing enqueues an XML routing message on the channel.
282    #[test]
283    fn test_responder_ev_routing_sends_on_channel() {
284        let responder = TreeWorkerResponder::new(3);
285        let mut chan = Channel::new(false);
286        let arg = RoutingEvent {
287            event: "connect".into(),
288            gateway: "gw1".into(),
289            targets: "node[1-5]".into(),
290        };
291        responder.ev_routing(&mut chan, &arg);
292        let out = String::from_utf8(chan.take_outgoing().unwrap()).unwrap();
293        assert!(out.contains("type=\"RTR\""));
294        assert!(out.contains("srcid=\"3\""));
295        assert!(out.contains("event=\"connect\""));
296        assert!(out.contains("targets=\"node[1-5]\""));
297    }
298
299    // -- GatewayChannel state machine ----------------------------------------
300
301    /// Basic channel open/close (mirrors TreeGatewayTest::test_basic_noop).
302    #[test]
303    fn test_gateway_channel_open_close() {
304        let mut gw = GatewayChannel::new();
305        assert!(!gw.channel.opened);
306        assert!(!gw.channel.setup);
307
308        gw.feed("<channel version=\"1.10.1\">");
309        assert!(gw.channel.opened);
310        assert!(!gw.channel.setup);
311
312        gw.feed("</channel>");
313        assert!(gw.channel.closed);
314        assert!(!gw.channel.opened);
315    }
316
317    /// A valid CFG is decoded, setup completes, and the msgid is ACKed.
318    #[test]
319    fn test_gateway_channel_cfg_ack() {
320        let mut gw = GatewayChannel::new();
321        gw.feed("<channel version=\"1.10.1\">");
322
323        let mut cfg = Message::configuration("n1");
324        cfg.data_encode(b"topology bytes");
325        let cfg_xml = String::from_utf8(cfg.xml()).unwrap();
326        gw.feed(&cfg_xml);
327
328        assert!(gw.channel.setup);
329        assert_eq!(gw.nodename.as_deref(), Some("n1"));
330        assert_eq!(gw.topology.as_deref(), Some(b"topology bytes".as_ref()));
331
332        let out = String::from_utf8(gw.channel.take_outgoing().unwrap()).unwrap();
333        assert!(out.contains("type=\"ACK\""));
334        assert!(out.contains(&format!("ack=\"{}\"", cfg.msgid())));
335    }
336
337    /// Mirror of TreeGatewayTest::test_channel_err_pickle_proto_pl
338    /// (gateway side): a CFG whose payload uses an unknown pickle protocol
339    /// is rejected with an Error reply carrying upstream's exact reason,
340    /// then the channel closes.
341    #[test]
342    fn test_gateway_channel_err_pickle_proto_payload() {
343        let mut gw = GatewayChannel::new();
344        gw.feed("<channel version=\"1.10.1\">");
345
346        // same wire vector as upstream: base64(b'\x80\x07spam') -> protocol 7
347        let payload = base64_encode(b"\x80\x07spam");
348        gw.feed(&format!(
349            "<message msgid=\"14\" type=\"CFG\" gateway=\"n1\">{}</message>",
350            payload
351        ));
352
353        let out = String::from_utf8(gw.channel.take_outgoing().unwrap()).unwrap();
354        assert!(out.contains("type=\"ERR\""));
355        assert!(
356            out.contains(
357                "reason=\"Message CFG has an invalid payload (unsupported pickle protocol: 7)\""
358            ),
359            "unexpected reason in: {}",
360            out
361        );
362        assert!(gw.channel.closed);
363        assert!(!gw.channel.setup);
364        // protocol 7 must exceed the pinned gateway protocol
365        assert!(7 > GW_PICKLE_PROTOCOL);
366    }
367
368    /// Mirror of TreeGatewayTest::test_channel_err_no_type_msg (gateway
369    /// side): a message without a type gets "Unknown message with no type"
370    /// back and closes the channel.
371    #[test]
372    fn test_gateway_channel_err_no_type_msg() {
373        let mut gw = GatewayChannel::new();
374        gw.feed("<channel version=\"1.10.1\">");
375        gw.feed("<message msgid=\"24\"></message>");
376
377        let out = String::from_utf8(gw.channel.take_outgoing().unwrap()).unwrap();
378        assert!(out.contains("type=\"ERR\""));
379        assert!(out.contains("reason=\"Unknown message with no type\""));
380        assert!(gw.channel.closed);
381    }
382
383    /// Mirror of TreeGatewayTest::test_channel_err_empty_type_msg.
384    #[test]
385    fn test_gateway_channel_err_empty_type_msg() {
386        let mut gw = GatewayChannel::new();
387        gw.feed("<channel version=\"1.10.1\">");
388        gw.feed("<message msgid=\"24\" type=\"\"></message>");
389
390        let out = String::from_utf8(gw.channel.take_outgoing().unwrap()).unwrap();
391        assert!(out.contains("reason=\"Unknown message with no type\""));
392        assert!(gw.channel.closed);
393    }
394
395    /// Mirror of updated TreeGatewayTest::test_err_unknown_msg: the unknown
396    /// type is named in the Error reply.
397    #[test]
398    fn test_gateway_channel_err_unknown_msg() {
399        let mut gw = GatewayChannel::new();
400        gw.feed("<channel version=\"1.10.1\">");
401        gw.feed("<message msgid=\"24\" type=\"ABC\"></message>");
402
403        let out = String::from_utf8(gw.channel.take_outgoing().unwrap()).unwrap();
404        assert!(out.contains("reason=\"Unknown message type ABC\""));
405        assert!(gw.channel.closed);
406    }
407
408    /// Mirror of TreeGatewayTest::test_channel_err_unknown_tag: an invalid
409    /// starting tag is reported back (target side gets the error text).
410    #[test]
411    fn test_gateway_channel_err_unknown_tag() {
412        let mut gw = GatewayChannel::new();
413        gw.feed("<channel version=\"1.10.1\">");
414        gw.feed("<foo></foo>");
415
416        let out = String::from_utf8(gw.channel.take_outgoing().unwrap()).unwrap();
417        assert!(out.contains("type=\"ERR\""));
418        assert!(out.contains("reason=\"Invalid starting tag foo\""));
419        assert!(gw.channel.closed);
420    }
421
422    /// Mirror of TreeGatewayTest::test_err_xml_malformed-style input:
423    /// malformed XML yields "Parse error: ..." and closes the channel.
424    #[test]
425    fn test_gateway_channel_err_malformed_xml() {
426        let mut gw = GatewayChannel::new();
427        gw.feed("<channel version=\"1.10.1\">");
428        gw.feed("<message type=\"ABC\"</message>");
429
430        let out = String::from_utf8(gw.channel.take_outgoing().unwrap()).unwrap();
431        assert!(out.contains("type=\"ERR\""));
432        assert!(out.contains("reason=\"Parse error: not well-formed (invalid token)\""));
433        assert!(gw.channel.closed);
434    }
435
436    /// Mirror of TreeGatewayTest::test_channel_err_dup: a second channel
437    /// tag while opened is an "unexpected message" error.
438    #[test]
439    fn test_gateway_channel_err_dup_start() {
440        let mut gw = GatewayChannel::new();
441        gw.feed("<channel version=\"1.10.1\">");
442        assert!(gw.channel.opened);
443
444        // second channel tag: the reader emits another CHA, which dispatch
445        // routes to recv_cfg -> unexpected message
446        gw.feed("<channel version=\"1.10.1\">");
447        let out = String::from_utf8(gw.channel.take_outgoing().unwrap()).unwrap();
448        assert!(out.contains("type=\"ERR\""));
449        assert!(out.contains("unexpected message: Message CHA"));
450        assert!(gw.channel.closed);
451    }
452
453    /// A CFG with a missing payload fails decode and is reported
454    /// (mirrors TreeGatewayTest::test_channel_err_missing_pl).
455    #[test]
456    fn test_gateway_channel_err_missing_payload() {
457        let mut gw = GatewayChannel::new();
458        gw.feed("<channel version=\"1.10.1\">");
459        gw.feed("<message msgid=\"14\" type=\"CFG\" gateway=\"n1\"></message>");
460
461        let out = String::from_utf8(gw.channel.take_outgoing().unwrap()).unwrap();
462        assert!(out.contains("type=\"ERR\""));
463        assert!(out.contains("reason=\"Message CFG has an invalid payload\""));
464        assert!(gw.channel.closed);
465    }
466
467    /// Empty gateway attribute falls back to the system hostname (mirrors
468    /// upstream recv_cfg behavior).
469    #[test]
470    fn test_gateway_channel_cfg_hostname_fallback() {
471        let mut gw = GatewayChannel::new();
472        gw.feed("<channel version=\"1.10.1\">");
473        let mut cfg = Message::configuration("");
474        cfg.data_encode(b"topo");
475        gw.feed(&String::from_utf8_lossy(&cfg.xml()));
476
477        assert!(gw.channel.setup);
478        // nodename must be exactly the short-hostname fallback
479        assert_eq!(gw.nodename.as_deref(), Some(short_hostname().as_str()));
480    }
481}