git.lucas.co / cce-browser
web browser (Servo)
git clone https://git.lucas.co/cce-browser.git

examples/wpe_autofill.rs (9.2K)

  1 //! Proves the page half of account autocomplete across a frame boundary.
  2 //!
  3 //! Serves a page on `127.0.0.1` with a sign-in form in an iframe from
  4 //! `localhost` — two origins, the shape of iCloud's sign-in — and checks,
  5 //! against the real engine:
  6 //!
  7 //! * a focused field in the frame is reported with the **frame's** origin,
  8 //!   and the frame's offset in the page arrives as a `Frame` event;
  9 //! * a fill answered to that frame's token lands in the frame's fields,
 10 //!   quotes and all;
 11 //! * pressing the sign-in button reports the credential for saving;
 12 //! * the relay's `postMessage` traffic never reaches the page's own script;
 13 //! * a document without focus (a background tab, a window the person left)
 14 //!   reports nothing.
 15 //!
 16 //! No keyring is involved: this is the engine side only.
 17 //!
 18 //! `cargo run --release -p cce-browser --example wpe_autofill`
 19 
 20 #[cfg(not(feature = "wpe"))]
 21 fn main() {
 22     eprintln!("build with --features wpe");
 23 }
 24 
 25 #[cfg(feature = "wpe")]
 26 #[derive(Debug, Clone, Copy)]
 27 pub enum EditingCommand { Copy, Cut, Paste }
 28 
 29 #[cfg(feature = "wpe")]
 30 #[path = "../src/pages.rs"]
 31 mod pages;
 32 #[cfg(feature = "wpe")]
 33 #[path = "../src/downloads.rs"]
 34 mod downloads;
 35 #[cfg(feature = "wpe")]
 36 // The host's vi channel and scripts.
 37 #[path = "../src/vi.rs"]
 38 #[allow(dead_code)]
 39 mod vi;
 40 
 41 #[path = "../src/wpe/mod.rs"]
 42 mod wpe;
 43 
 44 /// Quotes, a backslash and a closing script tag: everything that would end a
 45 /// string spliced into script source. (No newline — a password field strips
 46 /// line breaks, so one could never round-trip.)
 47 const PASSWORD: &str = "s3\"cr\\et</script>";
 48 
 49 /// The embedding page: the frame sits at a known place, behind a border and
 50 /// padding, so the reported offset can be checked to the pixel.
 51 const TOP: &str = r#"<!doctype html><html><body style="margin:0">
 52 <form onsubmit="event.preventDefault()">
 53   <input id="tu" type="email" style="position:absolute; left:10px; top:10px; width:200px; height:24px">
 54   <input id="tp" type="password" style="position:absolute; left:10px; top:50px; width:200px; height:24px">
 55 </form>
 56 <iframe id="f" src="http://localhost:PORT/frame"
 57   style="position:absolute; left:100px; top:150px; width:400px; height:200px;
 58          border:5px solid #888; padding:7px"></iframe>
 59 <script>
 60   // The page's own listener: it must see the frame's probes, never the
 61   // watcher's relay.
 62   const tu = document.getElementById('tu'), tp = document.getElementById('tp');
 63   tp.addEventListener('input', () => { document.title = 'top:' + tu.value + '|' + tp.value; });
 64   window.addEventListener('message', (e) => {
 65     if (e.data && e.data.cceAccountsFrame) { document.title += ' LEAK'; return; }
 66     if (e.data && e.data.probe !== undefined) document.title = 'frame:' + e.data.probe;
 67   });
 68 </script></body></html>"#;
 69 
 70 /// The sign-in frame. Its own script reports what its fields hold, which is
 71 /// how the test reads a fill back out of a cross-origin frame.
 72 const FRAME: &str = r#"<!doctype html><html><body style="margin:0">
 73 <form onsubmit="event.preventDefault()">
 74   <input id="u" name="username" style="position:absolute; left:10px; top:10px; width:200px; height:24px">
 75   <input id="p" type="password" style="position:absolute; left:10px; top:50px; width:200px; height:24px">
 76   <button id="go" type="submit" style="position:absolute; left:10px; top:90px; width:80px; height:24px">Sign in</button>
 77 </form>
 78 <script>
 79   const u = document.getElementById('u'), p = document.getElementById('p');
 80   const tell = () => parent.postMessage({ probe: u.value + '|' + p.value }, '*');
 81   u.addEventListener('input', tell); p.addEventListener('input', tell);
 82 </script></body></html>"#;
 83 
 84 #[cfg(feature = "wpe")]
 85 fn serve() -> u16 {
 86     use std::io::{Read, Write};
 87     let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
 88     let port = listener.local_addr().unwrap().port();
 89     std::thread::spawn(move || {
 90         for stream in listener.incoming() {
 91             let Ok(mut s) = stream else { continue };
 92             let mut buf = [0u8; 4096];
 93             let n = s.read(&mut buf).unwrap_or(0);
 94             let req = String::from_utf8_lossy(&buf[..n]);
 95             let body = if req.starts_with("GET /frame") { FRAME.to_string() } else {
 96                 TOP.replace("PORT", &port.to_string())
 97             };
 98             let _ = write!(
 99                 s,
100                 "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
101                 body.len(),
102                 body
103             );
104         }
105     });
106     port
107 }
108 
109 #[cfg(feature = "wpe")]
110 fn main() {
111     use cce_ui::widget::MouseButton;
112     use wpe::FormEvent;
113 
114     let port = serve();
115     let mut host = wpe::WebKitHost::new(url::Url::parse("about:blank").unwrap(), (800, 600));
116     host.set_accounts_enabled(true);
117     host.focus(true);
118     let settle = |h: &mut wpe::WebKitHost, n: u32| {
119         for _ in 0..n {
120             h.pump(); h.frame_drawn();
121             std::thread::sleep(std::time::Duration::from_millis(50));
122         }
123     };
124     let drain = |h: &wpe::WebKitHost| {
125         let mut out = Vec::new();
126         while let Some(e) = h.take_form_event() {
127             out.push(e);
128         }
129         out
130     };
131     let click = |h: &mut wpe::WebKitHost, x: f32, y: f32| {
132         h.mouse_move(x, y);
133         h.mouse_button_ui(MouseButton::Left, true, x, y);
134         h.mouse_button_ui(MouseButton::Left, false, x, y);
135     };
136 
137     settle(&mut host, 10);
138     host.load(url::Url::parse(&format!("http://127.0.0.1:{port}/top")).unwrap());
139     settle(&mut host, 40);
140     drain(&host);
141 
142     let mut ok = true;
143     let mut check = |what: &str, pass: bool, detail: String| {
144         ok &= pass;
145         println!("{what:<44} {} {detail}", if pass { "OK   " } else { "WRONG" });
146     };
147 
148     // The frame's content box starts at 100+5+7, 150+5+7; its username
149     // field at 10,10 inside that.
150     click(&mut host, 112.0 + 50.0, 162.0 + 20.0);
151     settle(&mut host, 10);
152     let events = drain(&host);
153     let field = events.iter().find_map(|e| match e {
154         FormEvent::Field { origin, frame, top, password, rect, moved: false, .. } => {
155             Some((origin.clone(), frame.clone(), *top, *password, *rect))
156         }
157         _ => None,
158     });
159     let token = field.as_ref().map(|f| f.1.clone()).unwrap_or_default();
160     check(
161         "frame field reported with the frame's origin",
162         field.as_ref().is_some_and(|f| {
163             f.0 == format!("http://localhost:{port}") && !f.2 && !f.3 && f.4.0 == 10.0 && f.4.1 == 10.0
164         }),
165         format!("{field:?}"),
166     );
167     let offset = events.iter().find_map(|e| match e {
168         FormEvent::Frame { frame, offset } if *frame == token => Some(*offset),
169         _ => None,
170     });
171     check("frame offset relayed to the top", offset == Some((112.0, 162.0)), format!("{offset:?}"));
172 
173     let filled = host.fill_credentials(&token, "alice", PASSWORD);
174     settle(&mut host, 10);
175     let title = host.title().unwrap_or_default();
176     check(
177         "fill answered to the frame's token",
178         filled && title == format!("frame:alice|{PASSWORD}"),
179         format!("{title:?}"),
180     );
181     check("a token nobody asked with fills nothing", !host.fill_credentials("00", "x", "y"), String::new());
182 
183     click(&mut host, 112.0 + 40.0, 162.0 + 100.0);
184     settle(&mut host, 10);
185     let submit = drain(&host).into_iter().find_map(|e| match e {
186         FormEvent::Submit { origin, username, password, top, .. } => {
187             Some((origin, username, password.expose().to_string(), top))
188         }
189         _ => None,
190     });
191     check(
192         "sign-in button reports the credential",
193         submit.as_ref().is_some_and(|s| {
194             s.0 == format!("http://localhost:{port}") && s.1 == "alice" && s.2 == PASSWORD && !s.3
195         }),
196         format!("{:?}", submit.as_ref().map(|s| (&s.0, &s.1, s.3))),
197     );
198 
199     check("the page never saw the relay", !host.title().unwrap_or_default().contains("LEAK"), String::new());
200 
201     // The top frame takes the same path, with its own token.
202     click(&mut host, 60.0, 20.0);
203     settle(&mut host, 10);
204     let top = drain(&host).into_iter().find_map(|e| match e {
205         FormEvent::Field { frame, top: true, moved: false, origin, .. } => Some((frame, origin)),
206         _ => None,
207     });
208     check(
209         "top-frame field reported as the top",
210         top.as_ref().is_some_and(|t| t.1 == format!("http://127.0.0.1:{port}") && t.0.len() == 24),
211         format!("{top:?}"),
212     );
213     let top_token = top.map(|t| t.0).unwrap_or_default();
214     check("an answered ask cannot be answered again", !host.fill_credentials(&token, "x", "y"), String::new());
215     let filled = host.fill_credentials(&top_token, "bob@example.com", "pw");
216     settle(&mut host, 10);
217     let title = host.title().unwrap_or_default();
218     check("fill answered to the top's token", filled && title == "top:bob@example.com|pw", format!("{title:?}"));
219 
220     // Without focus the document is not live: nothing is reported.
221     host.focus(false);
222     settle(&mut host, 4);
223     drain(&host);
224     click(&mut host, 112.0 + 50.0, 162.0 + 60.0);
225     settle(&mut host, 10);
226     let quiet = drain(&host);
227     check(
228         "an unfocused document reports no fields",
229         !quiet.iter().any(|e| matches!(e, FormEvent::Field { .. })),
230         format!("{} events", quiet.len()),
231     );
232 
233     println!("\nautofill: {}", if ok { "OK" } else { "BROKEN" });
234     std::process::exit(if ok { 0 } else { 1 });
235 }