Skip to content

Commit 54550ef

Browse files
committed
Auto merge of #12469 - arlosi:cred-stdio, r=weihanglo
cargo-credential: reset stdin & stdout to the Console Credential providers run with stdin and stdout piped to Cargo to communicate. This makes it more difficult for providers to do anything interactive. The current workaround is for a provider to use the `cargo_credential::tty()` function when reading from the console by re-opening stdin using `/dev/tty` or `CONIN$`. This PR makes the credential provider to re-attach itself to the current console so that reading from stdin and writing to stdout "just works" when inside the `perform` method of the provider. stderr is unaffected since it's not redirected by Cargo. Only the `cargo-credential` crate is changed. No changes are needed to Cargo. cc #8933
2 parents af431e1 + 5ade1ad commit 54550ef

File tree

7 files changed

+221
-30
lines changed

7 files changed

+221
-30
lines changed

Cargo.lock

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

credential/cargo-credential-1password/src/lib.rs

+1-6
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,6 @@ impl OnePasswordKeychain {
8080
let mut cmd = Command::new("op");
8181
cmd.args(["signin", "--raw"]);
8282
cmd.stdout(Stdio::piped());
83-
cmd.stdin(cargo_credential::tty().map_err(Box::new)?);
8483
let mut child = cmd
8584
.spawn()
8685
.map_err(|e| format!("failed to spawn `op`: {}", e))?;
@@ -210,7 +209,7 @@ impl OnePasswordKeychain {
210209
Some(name) => format!("Cargo registry token for {}", name),
211210
None => "Cargo registry token".to_string(),
212211
};
213-
let mut cmd = self.make_cmd(
212+
let cmd = self.make_cmd(
214213
session,
215214
&[
216215
"item",
@@ -225,10 +224,6 @@ impl OnePasswordKeychain {
225224
CARGO_TAG,
226225
],
227226
);
228-
// For unknown reasons, `op item create` seems to not be happy if
229-
// stdin is not a tty. Otherwise it returns with a 0 exit code without
230-
// doing anything.
231-
cmd.stdin(cargo_credential::tty().map_err(Box::new)?);
232227
self.run_cmd(cmd)?;
233228
Ok(())
234229
}

credential/cargo-credential/Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ description = "A library to assist writing Cargo credential helpers."
88

99
[dependencies]
1010
anyhow.workspace = true
11+
libc.workspace = true
1112
serde = { workspace = true, features = ["derive"] }
1213
serde_json.workspace = true
1314
thiserror.workspace = true
1415
time.workspace = true
16+
windows-sys = { workspace = true, features = ["Win32_System_Console", "Win32_Foundation"] }
1517

1618
[dev-dependencies]
1719
snapbox = { workspace = true, features = ["examples"] }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//! Provider used for testing redirection of stdout.
2+
3+
use cargo_credential::{Action, Credential, CredentialResponse, Error, RegistryInfo};
4+
5+
struct MyCredential;
6+
7+
impl Credential for MyCredential {
8+
fn perform(
9+
&self,
10+
_registry: &RegistryInfo,
11+
_action: &Action,
12+
_args: &[&str],
13+
) -> Result<CredentialResponse, Error> {
14+
// Informational messages should be sent on stderr.
15+
eprintln!("message on stderr should be sent the the parent process");
16+
17+
// Reading from stdin and writing to stdout will go to the attached console (tty).
18+
println!("message from test credential provider");
19+
Err(Error::OperationNotSupported)
20+
}
21+
}
22+
23+
fn main() {
24+
cargo_credential::main(MyCredential);
25+
}

credential/cargo-credential/src/lib.rs

+11-24
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,16 @@
3838
//! ```
3939
4040
use serde::{Deserialize, Serialize};
41-
use std::{
42-
fmt::Display,
43-
fs::File,
44-
io::{self, BufRead, BufReader},
45-
};
41+
use std::{fmt::Display, io};
4642
use time::OffsetDateTime;
4743

4844
mod error;
4945
mod secret;
46+
mod stdio;
47+
5048
pub use error::Error;
5149
pub use secret::Secret;
50+
use stdio::stdin_stdout_to_console;
5251

5352
/// Message sent by the credential helper on startup
5453
#[derive(Serialize, Deserialize, Clone, Debug)]
@@ -241,32 +240,20 @@ fn doit(
241240
if request.v != PROTOCOL_VERSION_1 {
242241
return Err(format!("unsupported protocol version {}", request.v).into());
243242
}
244-
serde_json::to_writer(
245-
std::io::stdout(),
246-
&credential.perform(&request.registry, &request.action, &request.args),
247-
)?;
243+
244+
let response = stdin_stdout_to_console(|| {
245+
credential.perform(&request.registry, &request.action, &request.args)
246+
})?;
247+
248+
serde_json::to_writer(std::io::stdout(), &response)?;
248249
println!();
249250
}
250251
}
251252

252-
/// Open stdin from the tty
253-
pub fn tty() -> Result<File, io::Error> {
254-
#[cfg(unix)]
255-
const IN_DEVICE: &str = "/dev/tty";
256-
#[cfg(windows)]
257-
const IN_DEVICE: &str = "CONIN$";
258-
let stdin = std::fs::OpenOptions::new()
259-
.read(true)
260-
.write(true)
261-
.open(IN_DEVICE)?;
262-
Ok(stdin)
263-
}
264-
265253
/// Read a line of text from stdin.
266254
pub fn read_line() -> Result<String, io::Error> {
267-
let mut reader = BufReader::new(tty()?);
268255
let mut buf = String::new();
269-
reader.read_line(&mut buf)?;
256+
io::stdin().read_line(&mut buf)?;
270257
Ok(buf.trim().to_string())
271258
}
272259

+163
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
use std::{fs::File, io::Error};
2+
3+
/// Reset stdin and stdout to the attached console / tty for the duration of the closure.
4+
/// If no console is available, stdin and stdout will be redirected to null.
5+
pub fn stdin_stdout_to_console<F, T>(f: F) -> Result<T, Error>
6+
where
7+
F: FnOnce() -> T,
8+
{
9+
let open_write = |f| std::fs::OpenOptions::new().write(true).open(f);
10+
11+
let mut stdin = File::open(imp::IN_DEVICE).or_else(|_| File::open(imp::NULL_DEVICE))?;
12+
let mut stdout = open_write(imp::OUT_DEVICE).or_else(|_| open_write(imp::NULL_DEVICE))?;
13+
14+
let _stdin_guard = imp::ReplacementGuard::new(Stdio::Stdin, &mut stdin)?;
15+
let _stdout_guard = imp::ReplacementGuard::new(Stdio::Stdout, &mut stdout)?;
16+
Ok(f())
17+
}
18+
19+
enum Stdio {
20+
Stdin,
21+
Stdout,
22+
}
23+
24+
#[cfg(windows)]
25+
mod imp {
26+
use super::Stdio;
27+
use std::{fs::File, io::Error, os::windows::prelude::AsRawHandle};
28+
use windows_sys::Win32::{
29+
Foundation::{HANDLE, INVALID_HANDLE_VALUE},
30+
System::Console::{
31+
GetStdHandle, SetStdHandle, STD_HANDLE, STD_INPUT_HANDLE, STD_OUTPUT_HANDLE,
32+
},
33+
};
34+
pub const OUT_DEVICE: &str = "CONOUT$";
35+
pub const IN_DEVICE: &str = "CONIN$";
36+
pub const NULL_DEVICE: &str = "NUL";
37+
38+
/// Restores previous stdio when dropped.
39+
pub struct ReplacementGuard {
40+
std_handle: STD_HANDLE,
41+
previous: HANDLE,
42+
}
43+
44+
impl ReplacementGuard {
45+
pub(super) fn new(stdio: Stdio, replacement: &mut File) -> Result<ReplacementGuard, Error> {
46+
let std_handle = match stdio {
47+
Stdio::Stdin => STD_INPUT_HANDLE,
48+
Stdio::Stdout => STD_OUTPUT_HANDLE,
49+
};
50+
51+
let previous;
52+
unsafe {
53+
// Make a copy of the current handle
54+
previous = GetStdHandle(std_handle);
55+
if previous == INVALID_HANDLE_VALUE {
56+
return Err(std::io::Error::last_os_error());
57+
}
58+
59+
// Replace stdin with the replacement handle
60+
if SetStdHandle(std_handle, replacement.as_raw_handle() as HANDLE) == 0 {
61+
return Err(std::io::Error::last_os_error());
62+
}
63+
}
64+
65+
Ok(ReplacementGuard {
66+
previous,
67+
std_handle,
68+
})
69+
}
70+
}
71+
72+
impl Drop for ReplacementGuard {
73+
fn drop(&mut self) {
74+
unsafe {
75+
// Put previous handle back in to stdin
76+
SetStdHandle(self.std_handle, self.previous);
77+
}
78+
}
79+
}
80+
}
81+
82+
#[cfg(unix)]
83+
mod imp {
84+
use super::Stdio;
85+
use libc::{close, dup, dup2, STDIN_FILENO, STDOUT_FILENO};
86+
use std::{fs::File, io::Error, os::fd::AsRawFd};
87+
pub const IN_DEVICE: &str = "/dev/tty";
88+
pub const OUT_DEVICE: &str = "/dev/tty";
89+
pub const NULL_DEVICE: &str = "/dev/null";
90+
91+
/// Restores previous stdio when dropped.
92+
pub struct ReplacementGuard {
93+
std_fileno: i32,
94+
previous: i32,
95+
}
96+
97+
impl ReplacementGuard {
98+
pub(super) fn new(stdio: Stdio, replacement: &mut File) -> Result<ReplacementGuard, Error> {
99+
let std_fileno = match stdio {
100+
Stdio::Stdin => STDIN_FILENO,
101+
Stdio::Stdout => STDOUT_FILENO,
102+
};
103+
104+
let previous;
105+
unsafe {
106+
// Duplicate the existing stdin file to a new descriptor
107+
previous = dup(std_fileno);
108+
if previous == -1 {
109+
return Err(std::io::Error::last_os_error());
110+
}
111+
// Replace stdin with the replacement file
112+
if dup2(replacement.as_raw_fd(), std_fileno) == -1 {
113+
return Err(std::io::Error::last_os_error());
114+
}
115+
}
116+
117+
Ok(ReplacementGuard {
118+
previous,
119+
std_fileno,
120+
})
121+
}
122+
}
123+
124+
impl Drop for ReplacementGuard {
125+
fn drop(&mut self) {
126+
unsafe {
127+
// Put previous file back in to stdin
128+
dup2(self.previous, self.std_fileno);
129+
// Close the file descriptor we used as a backup
130+
close(self.previous);
131+
}
132+
}
133+
}
134+
}
135+
136+
#[cfg(test)]
137+
mod test {
138+
use std::fs::OpenOptions;
139+
use std::io::{Seek, Write};
140+
141+
use super::imp::ReplacementGuard;
142+
use super::Stdio;
143+
144+
#[test]
145+
fn stdin() {
146+
let tempdir = snapbox::path::PathFixture::mutable_temp().unwrap();
147+
let file = tempdir.path().unwrap().join("stdin");
148+
let mut file = OpenOptions::new()
149+
.read(true)
150+
.write(true)
151+
.create(true)
152+
.open(file)
153+
.unwrap();
154+
155+
writeln!(&mut file, "hello").unwrap();
156+
file.seek(std::io::SeekFrom::Start(0)).unwrap();
157+
{
158+
let _guard = ReplacementGuard::new(Stdio::Stdin, &mut file).unwrap();
159+
let line = std::io::stdin().lines().next().unwrap().unwrap();
160+
assert_eq!(line, "hello");
161+
}
162+
}
163+
}

credential/cargo-credential/tests/examples.rs

+17
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,23 @@ use std::path::Path;
22

33
use snapbox::cmd::Command;
44

5+
#[test]
6+
fn stdout_redirected() {
7+
let bin = snapbox::cmd::compile_example("stdout-redirected", []).unwrap();
8+
9+
let hello = r#"{"v":[1]}"#;
10+
let get_request = r#"{"v": 1, "registry": {"index-url":"sparse+https://test/","name":"alternative"},"kind": "get","operation": "read","args": []}"#;
11+
let err_not_supported = r#"{"Err":{"kind":"operation-not-supported"}}"#;
12+
13+
Command::new(bin)
14+
.stdin(format!("{get_request}\n"))
15+
.arg("--cargo-plugin")
16+
.assert()
17+
.stdout_eq(format!("{hello}\n{err_not_supported}\n"))
18+
.stderr_eq("message on stderr should be sent the the parent process\n")
19+
.success();
20+
}
21+
522
#[test]
623
fn file_provider() {
724
let bin = snapbox::cmd::compile_example("file-provider", []).unwrap();

0 commit comments

Comments
 (0)