Skip to main content

wechat_protocol/
lan_login.rs

1//! Temporary, attempt-scoped LAN QR-login challenge. No account authorization
2//! is granted by the local connection; normal polling/manual authentication wins.
3use crate::{protobuf as p, wire, DeviceProfile};
4use anyhow::{ensure, Result};
5use std::{
6    fmt,
7    io::{self, Write},
8    net::{Ipv4Addr, TcpListener, TcpStream},
9    sync::{
10        atomic::{AtomicBool, Ordering},
11        mpsc, Arc, Mutex,
12    },
13    thread::{self, JoinHandle},
14    time::{Duration, Instant},
15};
16use zeroize::Zeroizing;
17
18pub(crate) const CGI: u16 = 22312;
19pub(crate) const PATH: &str = "/cgi-bin/micromsg-bin/extdeviceuploadlaninfo";
20const MAX_TOKEN: usize = 4096;
21const TICK: Duration = Duration::from_millis(20);
22
23#[derive(Debug)]
24struct LanError(&'static str);
25impl fmt::Display for LanError {
26    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27        write!(f, "LAN login challenge failed ({})", self.0)
28    }
29}
30impl std::error::Error for LanError {}
31pub(crate) fn failure(reason: &'static str) -> anyhow::Error {
32    LanError(reason).into()
33}
34pub fn login_lan_error_reason(error: &anyhow::Error) -> Option<&'static str> {
35    error.downcast_ref::<LanError>().map(|e| e.0)
36}
37
38#[derive(Clone)]
39pub(crate) struct Scope {
40    deadline: Instant,
41    cancelled: Arc<dyn Fn() -> bool + Send + Sync>,
42}
43impl Scope {
44    pub(crate) fn new(
45        deadline: Instant,
46        cancelled: impl Fn() -> bool + Send + Sync + 'static,
47    ) -> Self {
48        Self {
49            deadline,
50            cancelled: Arc::new(cancelled),
51        }
52    }
53    fn active(&self) -> bool {
54        Instant::now() < self.deadline && !(self.cancelled)()
55    }
56}
57struct State {
58    stop: AtomicBool,
59    sent: AtomicBool,
60    done: AtomicBool,
61    scope: Scope,
62}
63impl State {
64    fn active(&self) -> bool {
65        !self.stop.load(Ordering::Acquire) && self.scope.active()
66    }
67}
68
69pub(crate) struct Challenge {
70    addresses: Vec<Ipv4Addr>,
71    port: u16,
72    state: Arc<State>,
73    token: Mutex<Option<mpsc::SyncSender<Zeroizing<Vec<u8>>>>>,
74    worker: Option<JoinHandle<()>>,
75}
76impl Challenge {
77    pub(crate) fn bind(scope: Scope) -> Result<Self> {
78        if !scope.active() {
79            return Err(failure("cancelled_or_expired"));
80        }
81        let addresses = wechat_platform::local_ipv4_addresses()
82            .map_err(|_| failure("interface_enumeration_failed"))?;
83        if addresses.is_empty() {
84            return Err(failure("no_ipv4_address"));
85        }
86        Self::from_listener(bind_ports(8050..=8100)?, addresses, scope)
87    }
88    pub(crate) fn from_listener(
89        listener: TcpListener,
90        addresses: Vec<Ipv4Addr>,
91        scope: Scope,
92    ) -> Result<Self> {
93        listener.set_nonblocking(true)?;
94        let port = listener.local_addr()?.port();
95        let state = Arc::new(State {
96            stop: AtomicBool::new(false),
97            sent: AtomicBool::new(false),
98            done: AtomicBool::new(false),
99            scope,
100        });
101        let (token, incoming) = mpsc::sync_channel::<Zeroizing<Vec<u8>>>(1);
102        let shared = state.clone();
103        let worker = thread::Builder::new()
104            .name("wechat-lan-login".into())
105            .spawn(move || {
106                // Keep early peer connections queued until the authenticated upload
107                // supplies a token. Never reply with an empty/uninitialized token.
108                let frame = loop {
109                    if !shared.active() {
110                        shared.done.store(true, Ordering::Release);
111                        return;
112                    }
113                    match incoming.recv_timeout(TICK) {
114                        Ok(frame) => break frame,
115                        Err(mpsc::RecvTimeoutError::Timeout) => continue,
116                        Err(_) => {
117                            shared.done.store(true, Ordering::Release);
118                            return;
119                        }
120                    }
121                };
122                while shared.active() {
123                    match listener.accept() {
124                        Ok((mut stream, _peer)) => {
125                            if write_frame(&mut stream, &frame, &shared).is_ok() {
126                                shared.sent.store(true, Ordering::Release);
127                            }
128                        }
129                        Err(error) if error.kind() == io::ErrorKind::WouldBlock => {}
130                        Err(error) if error.kind() == io::ErrorKind::Interrupted => continue,
131                        Err(_) => break,
132                    }
133                    thread::sleep(TICK);
134                }
135                shared.done.store(true, Ordering::Release);
136            })?;
137        Ok(Self {
138            addresses,
139            port,
140            state,
141            token: Mutex::new(Some(token)),
142            worker: Some(worker),
143        })
144    }
145    pub(crate) fn request(
146        &self,
147        device: &DeviceProfile,
148        identifier: &[u8],
149        random: &[u8; 16],
150    ) -> Result<Vec<u8>> {
151        request(device, identifier, &self.addresses, self.port, random)
152    }
153    pub(crate) fn arm(&self, token: &[u8]) -> Result<()> {
154        if !self.active() {
155            return Err(failure("cancelled_or_expired"));
156        }
157        let frame = reply_frame(token).map_err(|_| failure("invalid_token"))?;
158        self.token
159            .lock()
160            .map_err(|_| failure("listener_unavailable"))?
161            .take()
162            .ok_or_else(|| failure("already_armed"))?
163            .send(frame)
164            .map_err(|_| failure("listener_unavailable"))
165    }
166    pub(crate) fn active(&self) -> bool {
167        self.state.active() && !self.state.done.load(Ordering::Acquire)
168    }
169    pub(crate) fn status(&self) -> &'static str {
170        if !self.active() {
171            "stopped"
172        } else if self.state.sent.load(Ordering::Acquire) {
173            "token_sent"
174        } else {
175            "waiting_for_phone"
176        }
177    }
178    pub(crate) fn stop(&self) {
179        self.state.stop.store(true, Ordering::Release);
180    }
181}
182impl Drop for Challenge {
183    fn drop(&mut self) {
184        self.stop();
185        if let Some(worker) = self.worker.take() {
186            let _ = worker.join();
187        }
188    }
189}
190fn bind_ports(ports: impl IntoIterator<Item = u16>) -> Result<TcpListener> {
191    for port in ports {
192        match TcpListener::bind((Ipv4Addr::UNSPECIFIED, port)) {
193            Ok(listener) => return Ok(listener),
194            Err(error) if error.kind() == io::ErrorKind::AddrInUse => continue,
195            Err(_) => return Err(failure("listener_unavailable")),
196        }
197    }
198    Err(failure("ports_unavailable"))
199}
200fn write_frame(stream: &mut TcpStream, frame: &[u8], state: &State) -> io::Result<()> {
201    stream.set_nonblocking(true)?;
202    let deadline = (Instant::now() + Duration::from_secs(1)).min(state.scope.deadline);
203    let mut offset = 0;
204    while offset < frame.len() {
205        if !state.active() || Instant::now() >= deadline {
206            return Err(io::Error::new(io::ErrorKind::TimedOut, "LAN reply ended"));
207        }
208        match stream.write(&frame[offset..]) {
209            Ok(0) => return Err(io::ErrorKind::WriteZero.into()),
210            Ok(n) => offset += n,
211            Err(error) if error.kind() == io::ErrorKind::Interrupted => continue,
212            Err(error) if error.kind() == io::ErrorKind::WouldBlock => thread::sleep(TICK),
213            Err(error) => return Err(error),
214        }
215    }
216    Ok(())
217}
218
219pub(crate) fn request(
220    device: &DeviceProfile,
221    identifier: &[u8],
222    addresses: &[Ipv4Addr],
223    port: u16,
224    random: &[u8; 16],
225) -> Result<Vec<u8>> {
226    ensure!(
227        !identifier.is_empty() && identifier.len() <= 4096,
228        "Invalid login identifier"
229    );
230    ensure!(
231        !addresses.is_empty() && addresses.len() <= 64 && port != 0,
232        "Invalid LAN address list"
233    );
234    let mut entries = Vec::new();
235    for ip in addresses {
236        let entry = p::message(&[
237            p::bytes(1, ip.to_string().as_bytes()),
238            p::uint(2, port as u64),
239        ])?;
240        entries.push(p::bytes(1, &entry));
241    }
242    p::message(&[
243        p::bytes(1, &wire::base_request(device, 0, 0)?),
244        p::bytes(2, &p::message(&entries)?),
245        p::bytes(3, identifier),
246        p::bytes(4, &p::buffer(random)),
247    ])
248}
249pub(crate) fn response(input: &[u8]) -> Result<Zeroizing<Vec<u8>>> {
250    ensure!(input.len() <= 16384, "LAN upload response too large");
251    let fields = p::decode(input)?;
252    ensure!(p::base_code(&fields)? == 0, "LAN upload rejected");
253    let token = p::data(&fields, 2)?;
254    ensure!(
255        !token.is_empty() && token.len() <= MAX_TOKEN,
256        "Invalid LAN token length"
257    );
258    std::str::from_utf8(token)?;
259    Ok(Zeroizing::new(token.to_vec()))
260}
261fn reply_frame(token: &[u8]) -> Result<Zeroizing<Vec<u8>>> {
262    ensure!(
263        !token.is_empty() && token.len() <= MAX_TOKEN,
264        "Invalid LAN token length"
265    );
266    let token = std::str::from_utf8(token)?;
267    #[derive(serde::Serialize)]
268    struct Reply<'a> {
269        scene: &'static str,
270        token: &'a str,
271    }
272    let json = Zeroizing::new(serde_json::to_vec(&Reply {
273        scene: "xwechat",
274        token,
275    })?);
276    // mars::lan_cs default packer: big-endian u32 total size, including itself.
277    let mut frame = Zeroizing::new(Vec::with_capacity(json.len() + 4));
278    frame.extend_from_slice(&u32::try_from(json.len() + 4)?.to_be_bytes());
279    frame.extend_from_slice(&json);
280    Ok(frame)
281}
282
283#[cfg(test)]
284#[path = "lan_login_tests.rs"]
285mod tests;