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 }