liminal_cli/network/
live.rs1use std::future::Future;
5use std::pin::pin;
6use std::sync::atomic::{AtomicU64, Ordering};
7use std::task::{Context, Poll, Waker};
8
9use liminal_sdk::{
10 ConnectionPoolConfig, ConversationHandle, RemoteConfig, RemoteConversationHandle,
11 SubscriptionStream, lys_pass_credential,
12};
13use serde_json::Value;
14
15use super::{Counts, Rail, RailError};
16use crate::config::{Config, Credential, Limits};
17use crate::lys::LysAgent;
18
19pub const TOPOLOGY_SUBJECT: &str = "console.topology";
21
22pub struct Live {
24 server: String,
25 channel: String,
26 source: Source,
27 limits: Limits,
28 counts: Counts,
29 conversations: AtomicU64,
30}
31
32impl std::fmt::Debug for Live {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 f.debug_struct("Live")
35 .field("server", &self.server)
36 .field("channel", &self.channel)
37 .field("source", &self.source)
38 .field("limits", &self.limits)
39 .finish_non_exhaustive()
40 }
41}
42
43impl Live {
44 pub fn new(config: &Config) -> Result<Self, &'static str> {
49 let source = match &config.credential {
50 Credential::Token(token) => Source::Fixed(lys_pass_credential(token.as_str())),
51 Credential::Lys(table) => {
52 Source::Lys(LysAgent::new(table, config.limits.reply_timeout())?)
53 }
54 };
55 Ok(Self {
56 server: config.server.clone(),
57 channel: config.channel.clone(),
58 source,
59 limits: config.limits,
60 counts: Counts::default(),
61 conversations: AtomicU64::new(0),
62 })
63 }
64
65 fn credential(&self) -> Result<Vec<u8>, RailError> {
68 match &self.source {
69 Source::Fixed(bytes) => Ok(bytes.clone()),
70 Source::Lys(agent) => agent
71 .pass()
72 .map(|pass| lys_pass_credential(&pass))
73 .map_err(|error| RailError::Refused(error.0)),
74 }
75 }
76}
77
78#[derive(Debug)]
80enum Source {
81 Fixed(Vec<u8>),
83 Lys(LysAgent),
85}
86
87impl Rail for Live {
88 fn topology(&self) -> Result<Value, RailError> {
89 let stream = self.admit(|credential| {
90 Counts::add(&self.counts.connections_opened);
91 SubscriptionStream::open_with_auth(
92 &self.server,
93 TOPOLOGY_SUBJECT,
94 Vec::new(),
95 credential,
96 )
97 .map_err(|error| {
98 Counts::add(&self.counts.connections_closed);
99 RailError::Refused(error.to_string())
100 })
101 })?;
102 Counts::add(&self.counts.subscribes);
103 let record = stream.recv_timeout(self.limits.reply_timeout());
104 drop(stream);
105 Counts::add(&self.counts.connections_closed);
106 let record = record.map_err(|_| RailError::NoRecord {
107 millis: self.limits.reply_timeout_ms,
108 })?;
109 Counts::add(&self.counts.records);
110 if record.payload().len() > self.limits.record_bytes {
111 return Err(RailError::RecordTooLarge {
112 limit: self.limits.record_bytes,
113 });
114 }
115 serde_json::from_slice(record.payload()).map_err(|error| RailError::Unreadable {
116 message: error.to_string(),
117 })
118 }
119
120 fn request(
121 &self,
122 subject: &str,
123 input: &Value,
124 reply: bool,
125 ) -> Result<Option<Value>, RailError> {
126 self.conversations.fetch_add(1, Ordering::SeqCst);
127 Counts::add(&self.counts.connections_opened);
128 let outcome = self.request_once(subject, input, reply);
129 Counts::add(&self.counts.connections_closed);
130 outcome
131 }
132
133 fn pass_for(&self, audience: &str) -> Result<String, RailError> {
134 match &self.source {
135 Source::Fixed(_) => Err(RailError::Refused(
136 "pass_for needs a [lys] table: a ticket on a fixed token cannot ask Lys for a pass"
137 .to_owned(),
138 )),
139 Source::Lys(agent) => agent
140 .pass_for(audience)
141 .map_err(|error| RailError::Refused(error.0)),
142 }
143 }
144
145 fn tap(&self, channel: &str) -> Result<super::TapSubscription, RailError> {
146 self.admit(|credential| {
147 super::live_tap::open(&self.server, channel, credential, self.limits.record_bytes)
148 })
149 }
150
151 fn server_address(&self) -> &str {
152 &self.server
153 }
154
155 fn counts(&self) -> &Counts {
156 &self.counts
157 }
158
159 fn request_bytes(&self) -> usize {
160 self.limits.request_bytes
161 }
162
163 fn flow_steps(&self) -> usize {
164 self.limits.flow_steps
165 }
166}
167
168impl Live {
169 fn request_once(
170 &self,
171 subject: &str,
172 input: &Value,
173 reply: bool,
174 ) -> Result<Option<Value>, RailError> {
175 let mut attempted = false;
176 let config = self.admit(|credential| {
177 if attempted {
178 Counts::add(&self.counts.connections_closed);
179 Counts::add(&self.counts.connections_opened);
180 }
181 attempted = true;
182 RemoteConfig::new(
183 self.server.clone(),
184 self.channel.clone(),
185 subject.to_owned(),
186 ConnectionPoolConfig::new(1, 10, 16),
187 )
188 .and_then(|config| config.connect_tcp_with_auth(credential))
189 .map_err(|error| RailError::Refused(error.to_string()))
190 })?;
191 let conversation = RemoteConversationHandle::new(&config);
192 Counts::add(&self.counts.requests);
193 if !reply {
194 conversation
195 .send(input)
196 .map_err(|error| RailError::Refused(error.to_string()))?;
197 return Ok(None);
198 }
199 conversation
200 .request(input)
201 .map_err(|error| RailError::Refused(error.to_string()))?;
202 let answer: Value = block_on(conversation.receive())
203 .and_then(|received| received.map_err(|error| error.to_string()))
204 .map_err(RailError::Refused)?;
205 Counts::add(&self.counts.replies);
206 Ok(Some(answer))
207 }
208}
209
210fn block_on<F: Future>(future: F) -> Result<F::Output, String> {
213 let mut future = pin!(future);
214 let waker = Waker::noop();
215 let mut context = Context::from_waker(waker);
216 match future.as_mut().poll(&mut context) {
217 Poll::Ready(value) => Ok(value),
218 Poll::Pending => Err("the synchronous transport parked unexpectedly".to_owned()),
219 }
220}
221
222#[path = "pass_renewal.rs"]
223mod pass_renewal;