mirror of
https://github.com/0xrsydn/idx-cli.git
synced 2026-11-07 09:45:36 +00:00
fix(stockbit): keep a working session through a failed login check
- login restored the previous session on any check error, even after the check had rotated the pasted session: a timeout then left no working session. It now keeps a rotated session unless Stockbit refused it. - The browser sign-in marked a cookie refused on any error, so a 429 or a timeout held a signed-in user until the wait ran out. Passing errors are retried after a pause; only a refusal is final. - The local page checked concurrent pastes on their own threads, so a later one could overwrite the session the first had just rotated. Pastes are now checked one at a time, and none after one is accepted. Claude-Session: https://claude.ai/code/session_01RqPPVeVDrnLoP7NUZmLRxY
This commit is contained in:
parent
81a3fe7266
commit
e7f1fa2c90
3 changed files with 139 additions and 6 deletions
|
|
@ -72,13 +72,24 @@ impl LoginPage {
|
|||
///
|
||||
/// Each connection gets its own thread: browsers open idle preconnects,
|
||||
/// and answering them one at a time used to hold the page back for the
|
||||
/// read timeout on every one.
|
||||
/// read timeout on every one. Pastes are checked one at a time, and none
|
||||
/// after one is accepted: two at once (a double click, the bookmarklet
|
||||
/// and the clipboard button) would each store a session, and the later
|
||||
/// could overwrite the one Stockbit had just rotated.
|
||||
pub fn wait<F>(&self, timeout: Duration, verify: F) -> Result<(), IdxError>
|
||||
where
|
||||
F: Fn(&str) -> Result<(), IdxError> + Send + Sync + 'static,
|
||||
{
|
||||
let (done_tx, done_rx) = std::sync::mpsc::channel::<()>();
|
||||
let verify = std::sync::Arc::new(verify);
|
||||
let accepted = std::sync::Mutex::new(false);
|
||||
let verify = std::sync::Arc::new(move |paste: &str| {
|
||||
let mut accepted = accepted.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if !*accepted {
|
||||
verify(paste)?;
|
||||
*accepted = true;
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
let page = std::sync::Arc::new(self.page());
|
||||
let path = std::sync::Arc::new(format!("/login/{}", self.secret));
|
||||
let busy = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
||||
|
|
@ -575,6 +586,37 @@ mod tests {
|
|||
(status, body)
|
||||
}
|
||||
|
||||
/// Two pastes at once are checked one at a time, and the second is not
|
||||
/// checked at all once the first is accepted.
|
||||
#[test]
|
||||
fn concurrent_pastes_are_checked_once() {
|
||||
let page = LoginPage::bind(0).unwrap();
|
||||
let url = page.url();
|
||||
let checks = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
||||
let counted = checks.clone();
|
||||
let handle = std::thread::spawn(move || {
|
||||
page.wait(Duration::from_secs(10), move |_| {
|
||||
counted.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
||||
std::thread::sleep(Duration::from_millis(300));
|
||||
Ok(())
|
||||
})
|
||||
});
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
let clients: Vec<_> = (0..2)
|
||||
.map(|_| {
|
||||
let url = url.clone();
|
||||
std::thread::spawn(move || request(&url, "POST", "good"))
|
||||
})
|
||||
.collect();
|
||||
for client in clients {
|
||||
let (status, body) = client.join().unwrap();
|
||||
assert_eq!(status, 200);
|
||||
assert!(body.contains("\"ok\":true"), "{body}");
|
||||
}
|
||||
handle.join().unwrap().unwrap();
|
||||
assert_eq!(checks.load(std::sync::atomic::Ordering::SeqCst), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serves_the_page_only_with_the_secret_and_accepts_a_good_paste() {
|
||||
let page = LoginPage::bind(0).unwrap();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue