Skip to content

Commit 5ca019c

Browse files
committed
Add client support for sending custom global requests
1 parent d3ae702 commit 5ca019c

5 files changed

Lines changed: 157 additions & 5 deletions

File tree

russh/src/client/encrypted.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ use crate::keys::key::parse_public_key;
3030
use crate::parsing::{ChannelOpenConfirmation, ChannelType, OpenChannelMessage, ensure_end};
3131
use crate::session::{Encrypted, EncryptedState, GlobalRequestResponse};
3232
use crate::{
33-
Channel, ChannelId, ChannelMsg, ChannelOpenFailure, ChannelParams, Error, MethodSet, Sig, auth,
34-
map_err, msg,
33+
Channel, ChannelId, ChannelMsg, ChannelOpenFailure, ChannelParams, CryptoVec, Error, MethodSet,
34+
Sig, auth, map_err, msg,
3535
};
3636

3737
impl Session {
@@ -962,6 +962,9 @@ impl Session {
962962
map_err!(ensure_end(&r))?;
963963
let _ = return_channel.send(true);
964964
}
965+
Some(GlobalRequestResponse::Other(return_channel)) => {
966+
let _ = return_channel.send(Some(CryptoVec::from_slice(r)));
967+
}
965968
None => {
966969
error!("Received global request failure for unknown request!")
967970
}
@@ -993,6 +996,9 @@ impl Session {
993996
Some(GlobalRequestResponse::CancelStreamLocalForward(return_channel)) => {
994997
let _ = return_channel.send(false);
995998
}
999+
Some(GlobalRequestResponse::Other(return_channel)) => {
1000+
let _ = return_channel.send(None);
1001+
}
9961002
None => {
9971003
error!("Received global request failure for unknown request!")
9981004
}

russh/src/client/mod.rs

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ use crate::session::{CommonSession, EncryptedState, GlobalRequestResponse, NewKe
7070
use crate::ssh_read::SshRead;
7171
use crate::sshbuffer::{IncomingSshPacket, PacketWriter, SSHBuffer, SshId};
7272
use crate::{
73-
ChannelId, ChannelOpenFailure, Disconnect, Error, Limits, MethodSet, Sig, auth, map_err, msg,
74-
negotiation,
73+
ChannelId, ChannelOpenFailure, CryptoVec, Disconnect, Error, Limits, MethodSet, Sig, auth,
74+
map_err, msg, negotiation,
7575
};
7676

7777
mod encrypted;
@@ -241,6 +241,13 @@ pub enum Msg {
241241
NoMoreSessions {
242242
want_reply: bool,
243243
},
244+
/// Send a global request with a custom name to the remote
245+
SendGlobalRequest {
246+
name: String,
247+
data: CryptoVec,
248+
/// Provide a channel for the reply result to request a reply from the server
249+
reply_channel: Option<oneshot::Sender<Option<CryptoVec>>>,
250+
},
244251
}
245252

246253
impl From<(ChannelId, ChannelMsg)> for Msg {
@@ -1056,6 +1063,47 @@ impl<H: Handler> Handle<H> {
10561063
.await
10571064
.map_err(|_| Error::SendError)
10581065
}
1066+
1067+
/// Send a global request with a custom, non-standard name to the remote peer.
1068+
///
1069+
/// `data` is the request-specific payload and is appended verbatim after the
1070+
/// request name and the want-reply flag (see RFC 4254 section 4). When
1071+
/// `want_reply` is true this waits for the peer's reply and returns its
1072+
/// response-specific data, which may be empty. When it is false it returns
1073+
/// `Ok(None)` without waiting for a reply.
1074+
pub async fn send_global_request<A: Into<String>>(
1075+
&self,
1076+
name: A,
1077+
data: &[u8],
1078+
want_reply: bool,
1079+
) -> Result<Option<CryptoVec>, Error> {
1080+
let (reply_channel, reply_recv) = if want_reply {
1081+
let (send, recv) = oneshot::channel();
1082+
(Some(send), Some(recv))
1083+
} else {
1084+
(None, None)
1085+
};
1086+
self.sender
1087+
.send(Msg::SendGlobalRequest {
1088+
name: name.into(),
1089+
data: CryptoVec::from_slice(data),
1090+
reply_channel,
1091+
})
1092+
.await
1093+
.map_err(|_| Error::SendError)?;
1094+
1095+
match reply_recv {
1096+
Some(reply_recv) => match reply_recv.await {
1097+
Ok(Some(data)) => Ok(Some(data)),
1098+
Ok(None) => Err(Error::RequestDenied),
1099+
Err(e) => {
1100+
error!("Unable to receive send_global_request result: {e:?}");
1101+
Err(Error::Disconnect)
1102+
}
1103+
},
1104+
None => Ok(None),
1105+
}
1106+
}
10591107
}
10601108

10611109
impl<H: Handler> Future for Handle<H> {
@@ -1668,6 +1716,13 @@ impl Session {
16681716
Msg::NoMoreSessions { want_reply } => {
16691717
let _ = self.no_more_sessions(want_reply);
16701718
}
1719+
Msg::SendGlobalRequest {
1720+
name,
1721+
data,
1722+
reply_channel,
1723+
} => {
1724+
let _ = self.send_global_request(reply_channel, &name, &data);
1725+
}
16711726
Msg::ServerChannelOpenReply { pending, result } => {
16721727
self.finalize_server_channel_open_reply(pending, result)?;
16731728
}

russh/src/client/session.rs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use tokio::sync::oneshot;
44

55
use crate::client::Session;
66
use crate::session::EncryptedState;
7-
use crate::{map_err, msg, ChannelId, Disconnect, Pty, Sig};
7+
use crate::{map_err, msg, ChannelId, CryptoVec, Disconnect, Pty, Sig};
88

99
impl Session {
1010
fn channel_open_generic<F>(
@@ -403,6 +403,34 @@ impl Session {
403403
Ok(())
404404
}
405405

406+
/// Sends a global request with a custom name to the server.
407+
///
408+
/// `data` is appended verbatim after the request name and want-reply flag.
409+
/// If `reply_channel` is not None, sets want_reply and returns the server's
410+
/// response-specific data via the channel, [`Some`] on success or [`None`]
411+
/// on failure.
412+
pub fn send_global_request(
413+
&mut self,
414+
reply_channel: Option<oneshot::Sender<Option<CryptoVec>>>,
415+
name: &str,
416+
data: &[u8],
417+
) -> Result<(), crate::Error> {
418+
if let Some(ref mut enc) = self.common.encrypted {
419+
let want_reply = reply_channel.is_some();
420+
if let Some(reply_channel) = reply_channel {
421+
self.open_global_requests
422+
.push_back(crate::session::GlobalRequestResponse::Other(reply_channel));
423+
}
424+
push_packet!(enc.write, {
425+
msg::GLOBAL_REQUEST.encode(&mut enc.write)?;
426+
name.encode(&mut enc.write)?;
427+
(want_reply as u8).encode(&mut enc.write)?;
428+
enc.write.extend_from_slice(data);
429+
});
430+
}
431+
Ok(())
432+
}
433+
406434
pub fn send_keepalive(&mut self, want_reply: bool) -> Result<(), crate::Error> {
407435
self.open_global_requests
408436
.push_back(crate::session::GlobalRequestResponse::Keepalive);

russh/src/client/test.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,4 +164,64 @@ mod tests {
164164
msg => panic!("Unexpected message {msg:?}"),
165165
}
166166
}
167+
168+
#[tokio::test]
169+
async fn test_send_custom_global_request() {
170+
let _ = env_logger::try_init();
171+
172+
let client_key = PrivateKey::random(&mut rng(), ssh_key::Algorithm::Ed25519).unwrap();
173+
174+
let mut config = server::Config::default();
175+
config.auth_rejection_time = std::time::Duration::from_secs(1);
176+
config.inactivity_timeout = None;
177+
config
178+
.keys
179+
.push(PrivateKey::random(&mut rng(), ssh_key::Algorithm::Ed25519).unwrap());
180+
let config = Arc::new(config);
181+
182+
let mut server = TestServer {
183+
clients: Arc::new(Mutex::new(HashMap::new())),
184+
id: 0,
185+
};
186+
187+
let socket = TcpListener::bind("127.0.0.1:0").await.unwrap();
188+
let addr = socket.local_addr().unwrap();
189+
190+
tokio::spawn(async move {
191+
let (socket, _) = socket.accept().await.unwrap();
192+
let server_handler = server.new_client(None);
193+
server::run_stream(config, socket, server_handler)
194+
.await
195+
.unwrap();
196+
});
197+
198+
let client_config = Arc::new(Config::default());
199+
let mut session = connect(client_config, addr, Client {}).await.unwrap();
200+
201+
let auth_result = session
202+
.authenticate_publickey(
203+
std::env::var("USER").unwrap_or("user".to_string()),
204+
PrivateKeyWithHashAlg::new(
205+
Arc::new(client_key),
206+
session.best_supported_rsa_hash().await.unwrap().flatten(),
207+
),
208+
)
209+
.await
210+
.unwrap();
211+
assert!(auth_result.success());
212+
213+
// The default server has no handler for this request name, so it replies
214+
// with a failure, which surfaces as RequestDenied.
215+
let denied = session
216+
.send_global_request("custom-request@example.com", b"payload", true)
217+
.await;
218+
assert!(matches!(denied, Err(Error::RequestDenied)));
219+
220+
// Without a reply requested the call returns immediately.
221+
let no_reply = session
222+
.send_global_request("custom-request@example.com", b"payload", false)
223+
.await
224+
.unwrap();
225+
assert!(no_reply.is_none());
226+
}
167227
}

russh/src/session.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -811,6 +811,9 @@ pub(crate) enum GlobalRequestResponse {
811811
/// request was for StreamLocalForward, sends true for success or false for failure
812812
StreamLocalForward(oneshot::Sender<bool>),
813813
CancelStreamLocalForward(oneshot::Sender<bool>),
814+
/// request had a custom name; sends `Some` with the response-specific
815+
/// payload on success or `None` on failure
816+
Other(oneshot::Sender<Option<CryptoVec>>),
814817
}
815818

816819
#[cfg(test)]

0 commit comments

Comments
 (0)