1use 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 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 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;