DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
smb_client.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file smb_client.cpp
6 * @brief SMB2 client dialogue engine (see smb_client.h). Drives the wire codecs through the real
7 * NEGOTIATE / NTLMv2 SESSION_SETUP / TREE_CONNECT / CREATE exchange over a send/recv seam.
8 */
9
10#include "smb_client.h"
11
12#if DETWS_ENABLE_SMB
13
14#include "ntlm.h"
15#include "ntlmssp.h"
16#include "smb2.h"
17#include "spnego.h"
18#include <Arduino.h> // esp_fill_random() (real on device, mocked on native)
19#include <string.h>
20
21// ASCII/Latin-1 -> UTF-16LE (SMB paths are ASCII); returns byte length (2 * chars), 0 on null/overflow.
22static size_t utf16le(const char *s, uint8_t *out, size_t cap)
23{
24 if (!s)
25 return 0;
26 size_t n = 0;
27 for (; s[n]; n++)
28 {
29 if ((n * 2 + 2) > cap)
30 return 0;
31 out[n * 2] = (uint8_t)s[n];
32 out[n * 2 + 1] = 0;
33 }
34 return n * 2;
35}
36
37// Find the MsvAvTimestamp (AvId 7) FILETIME in a CHALLENGE target-info blob; copy 8 bytes, else 0-fill.
38static void find_av_timestamp(const uint8_t *ti, size_t ti_len, uint8_t out[8])
39{
40 memset(out, 0, 8);
41 size_t p = 0;
42 while (p + 4 <= ti_len)
43 {
44 uint16_t id = (uint16_t)(ti[p] | (ti[p + 1] << 8));
45 uint16_t len = (uint16_t)(ti[p + 2] | (ti[p + 3] << 8));
46 p += 4;
47 if (id == 0) // MsvAvEOL
48 break;
49 if (id == 7 && len == 8 && p + 8 <= ti_len)
50 {
51 memcpy(out, ti + p, 8);
52 return;
53 }
54 p += len;
55 }
56}
57
58static bool read_exact(SmbRecvFn recv, void *ctx, uint8_t *buf, size_t n)
59{
60 size_t got = 0;
61 while (got < n)
62 {
63 int r = recv(ctx, buf + got, n - got);
64 if (r <= 0)
65 return false;
66 got += (size_t)r;
67 }
68 return true;
69}
70
71// Frame the SMB2 message that sits at frame+4 (msg_len bytes) with the Direct-TCP prefix and send it.
72static bool send_msg(SmbSendFn send, void *ctx, uint8_t *frame, size_t msg_len)
73{
74 frame[0] = 0x00;
75 frame[1] = (uint8_t)(msg_len >> 16);
76 frame[2] = (uint8_t)(msg_len >> 8);
77 frame[3] = (uint8_t)msg_len;
78 size_t total = 4 + msg_len;
79 return send(ctx, frame, total) == (int)total;
80}
81
82// Receive one Direct-TCP-framed SMB2 message into rx; return its length, -2 on overflow, -1 on IO.
83static int recv_msg(SmbRecvFn recv, void *ctx, uint8_t *rx, size_t cap)
84{
85 uint8_t pre[4];
86 if (!read_exact(recv, ctx, pre, 4) || pre[0] != 0x00)
87 return -1;
88 size_t len = ((size_t)pre[1] << 16) | ((size_t)pre[2] << 8) | pre[3];
89 if (len == 0 || len > cap)
90 return len ? -2 : -1;
91 if (!read_exact(recv, ctx, rx, len))
92 return -1;
93 return (int)len;
94}
95
96// SMB dialogue working buffers, kept off the caller's stack: smb_open alone needs ~4 KB, which
97// overflows the default 8 KB Arduino loopTask (seen on HW as "Stack canary watchpoint triggered").
98// The client drives one sequential dialogue at a time (open -> read/write -> close), so a single
99// owned working set is correct; it is not reentrant across two concurrent SMB connections.
100struct SmbClientCtx
101{
102 uint8_t tx[DETWS_SMB_BUF];
103 uint8_t rx[DETWS_SMB_BUF];
104 uint8_t nt_resp[DETWS_SMB_BUF / 2];
105 uint8_t ntauth[DETWS_SMB_BUF / 2];
106 uint8_t sp2[DETWS_SMB_BUF / 2];
107 uint8_t utf16[DETWS_SMB_BUF / 2];
108};
109static SmbClientCtx s_smb;
110
111// Send the framed message currently in s_smb.tx (mlen bytes at tx+4) and receive the reply into s_smb.rx.
112// Returns the reply length (>=0), or -1 with *res set to the mapped IO / overflow error.
113static int smb_round_trip(SmbSendFn send, SmbRecvFn recv, void *ctx, size_t mlen, SmbResult *res)
114{
115 if (!send_msg(send, ctx, s_smb.tx, mlen))
116 {
117 *res = SmbResult::SMB_ERR_IO;
118 return -1;
119 }
120 int rl = recv_msg(recv, ctx, s_smb.rx, sizeof(s_smb.rx));
121 if (rl < 0)
122 {
123 *res = (rl == -2) ? SmbResult::SMB_ERR_OVERFLOW : SmbResult::SMB_ERR_IO;
124 return -1;
125 }
126 return rl;
127}
128
129// Step 1 - NEGOTIATE: advertise SMB2 and confirm the server's negotiate response parses.
130static SmbResult smb_negotiate(SmbSendFn send, SmbRecvFn recv, void *ctx)
131{
132 uint8_t guid[16];
133 esp_fill_random(guid, 16);
134 size_t mlen = smb2_build_negotiate(s_smb.tx + 4, sizeof(s_smb.tx) - 4, guid,
135 Smb2SecurityMode::SMB2_NEGOTIATE_SIGNING_ENABLED);
136 if (!mlen)
137 return SmbResult::SMB_ERR_OVERFLOW;
138 SmbResult rt = SmbResult::SMB_ERR_IO;
139 int rl = smb_round_trip(send, recv, ctx, mlen, &rt);
140 if (rl < 0)
141 return rt;
142 Smb2NegotiateResp neg;
143 if (!smb2_parse_negotiate_response(s_smb.rx, (size_t)rl, &neg))
144 return SmbResult::SMB_ERR_PROTOCOL;
145 return SmbResult::SMB_OK;
146}
147
148// Steps 2-4 - NTLMv2 SESSION_SETUP: SPNEGO/NTLMSSP negotiate, compute the NTLMv2 response to the server
149// challenge, then authenticate. Fills *session_id from the server's SessionId.
150static SmbResult smb_session_setup(const SmbConfig *cfg, const char *domain, SmbSendFn send, SmbRecvFn recv, void *ctx,
151 uint64_t *session_id)
152{
153 // 2. SESSION_SETUP round 1: NTLMSSP NEGOTIATE wrapped in SPNEGO
154 uint8_t ntneg[64];
155 uint8_t sp1[128];
156 size_t ntneg_n = ntlmssp_build_negotiate(ntneg, sizeof(ntneg), NtlmsspFlags::NTLMSSP_CLIENT_DEFAULT_FLAGS);
157 size_t sp1_n = spnego_wrap_negotiate(ntneg, ntneg_n, sp1, sizeof(sp1));
158 size_t mlen = smb2_build_session_setup(s_smb.tx + 4, sizeof(s_smb.tx) - 4, 1, 0,
159 Smb2SecurityMode::SMB2_NEGOTIATE_SIGNING_ENABLED, sp1, sp1_n);
160 if (!mlen)
161 return SmbResult::SMB_ERR_OVERFLOW;
162 SmbResult rt = SmbResult::SMB_ERR_IO;
163 int rl = smb_round_trip(send, recv, ctx, mlen, &rt);
164 if (rl < 0)
165 return rt;
166 Smb2Header h1;
167 if (!smb2_parse_header(s_smb.rx, (size_t)rl, &h1) || h1.status != Smb2Status::SMB2_STATUS_MORE_PROCESSING_REQUIRED)
168 return SmbResult::SMB_ERR_AUTH;
169 *session_id = h1.session_id;
170 Smb2SessionSetupResp ss1;
171 if (!smb2_parse_session_setup_response(s_smb.rx, (size_t)rl, &ss1) || !ss1.sec_buf)
172 return SmbResult::SMB_ERR_PROTOCOL;
173 const uint8_t *chal_tok = nullptr;
174 size_t chal_len = 0;
175 if (!spnego_parse_response(ss1.sec_buf, ss1.sec_buf_len, &chal_tok, &chal_len))
176 return SmbResult::SMB_ERR_PROTOCOL;
177 NtlmChallenge ch;
178 if (!ntlmssp_parse_challenge(chal_tok, chal_len, &ch))
179 return SmbResult::SMB_ERR_PROTOCOL;
180
181 // 3. Compute the NTLMv2 response, wrap the AUTHENTICATE in SPNEGO
182 uint8_t nt_hash[16];
183 uint8_t owf[16];
184 ntlm_nt_hash(cfg->pass, nt_hash);
185 if (!ntlm_ntowfv2(nt_hash, cfg->user, domain, owf))
186 return SmbResult::SMB_ERR_OVERFLOW;
187 uint8_t cli_chal[8];
188 uint8_t ts[8];
189 uint8_t skey[16];
190 esp_fill_random(cli_chal, 8);
191 find_av_timestamp(ch.target_info, ch.target_info_len, ts);
192 size_t nt_len = ntlm_v2_response(owf, ch.server_challenge, cli_chal, ts, ch.target_info, ch.target_info_len,
193 s_smb.nt_resp, sizeof(s_smb.nt_resp), skey);
194 if (!nt_len)
195 return SmbResult::SMB_ERR_OVERFLOW;
196 size_t ntauth_n = ntlmssp_build_authenticate(s_smb.ntauth, sizeof(s_smb.ntauth), nullptr, 0, s_smb.nt_resp, nt_len,
197 domain, cfg->user, cfg->workstation, ch.flags);
198 if (!ntauth_n)
199 return SmbResult::SMB_ERR_OVERFLOW;
200 size_t sp2_n = spnego_wrap_authenticate(s_smb.ntauth, ntauth_n, s_smb.sp2, sizeof(s_smb.sp2));
201 if (!sp2_n)
202 return SmbResult::SMB_ERR_OVERFLOW;
203
204 // 4. SESSION_SETUP round 2 (echo the server SessionId)
205 mlen = smb2_build_session_setup(s_smb.tx + 4, sizeof(s_smb.tx) - 4, 2, *session_id,
206 Smb2SecurityMode::SMB2_NEGOTIATE_SIGNING_ENABLED, s_smb.sp2, sp2_n);
207 if (!mlen)
208 return SmbResult::SMB_ERR_OVERFLOW;
209 rl = smb_round_trip(send, recv, ctx, mlen, &rt);
210 if (rl < 0)
211 return rt;
212 Smb2Header h2;
213 if (!smb2_parse_header(s_smb.rx, (size_t)rl, &h2))
214 return SmbResult::SMB_ERR_PROTOCOL;
215 if (h2.status != Smb2Status::SMB2_STATUS_SUCCESS)
216 return SmbResult::SMB_ERR_AUTH;
217 return SmbResult::SMB_OK;
218}
219
220// Step 5 - TREE_CONNECT to \\server\share. Fills *tree_id.
221static SmbResult smb_tree_connect(const SmbConfig *cfg, uint64_t session_id, SmbSendFn send, SmbRecvFn recv, void *ctx,
222 uint32_t *tree_id)
223{
224 size_t utf16_n = utf16le(cfg->share, s_smb.utf16, sizeof(s_smb.utf16));
225 if (!utf16_n)
226 return SmbResult::SMB_ERR_OVERFLOW;
227 size_t mlen = smb2_build_tree_connect(s_smb.tx + 4, sizeof(s_smb.tx) - 4, 3, session_id, s_smb.utf16, utf16_n);
228 if (!mlen)
229 return SmbResult::SMB_ERR_OVERFLOW;
230 SmbResult rt = SmbResult::SMB_ERR_IO;
231 int rl = smb_round_trip(send, recv, ctx, mlen, &rt);
232 if (rl < 0)
233 return rt;
234 Smb2Header h3;
235 Smb2TreeConnectResp tc;
236 if (!smb2_parse_header(s_smb.rx, (size_t)rl, &h3) || h3.status != Smb2Status::SMB2_STATUS_SUCCESS)
237 return SmbResult::SMB_ERR_PROTOCOL;
238 if (!smb2_parse_tree_connect_response(s_smb.rx, (size_t)rl, &tc))
239 return SmbResult::SMB_ERR_PROTOCOL;
240 *tree_id = h3.tree_id;
241 return SmbResult::SMB_OK;
242}
243
244// Step 6 - CREATE (open) the file; fills the handle h on success.
245static SmbResult smb_create(const SmbConfig *cfg, SmbHandle *h, uint64_t session_id, uint32_t tree_id, SmbSendFn send,
246 SmbRecvFn recv, void *ctx)
247{
248 size_t utf16_n = utf16le(cfg->path, s_smb.utf16, sizeof(s_smb.utf16));
249 if (!utf16_n)
250 return SmbResult::SMB_ERR_OVERFLOW;
251 size_t mlen =
252 smb2_build_create(s_smb.tx + 4, sizeof(s_smb.tx) - 4, 4, session_id, tree_id, cfg->desired_access,
253 Smb2ShareAccess::SMB2_FILE_SHARE_READ | Smb2ShareAccess::SMB2_FILE_SHARE_WRITE,
254 cfg->disposition, Smb2CreateOptions::SMB2_FILE_NON_DIRECTORY_FILE, s_smb.utf16, utf16_n);
255 if (!mlen)
256 return SmbResult::SMB_ERR_OVERFLOW;
257 SmbResult rt = SmbResult::SMB_ERR_IO;
258 int rl = smb_round_trip(send, recv, ctx, mlen, &rt);
259 if (rl < 0)
260 return rt;
261 Smb2Header h4;
262 Smb2CreateResp cr;
263 if (!smb2_parse_header(s_smb.rx, (size_t)rl, &h4) || h4.status != Smb2Status::SMB2_STATUS_SUCCESS)
264 return SmbResult::SMB_ERR_PROTOCOL;
265 if (!smb2_parse_create_response(s_smb.rx, (size_t)rl, &cr))
266 return SmbResult::SMB_ERR_PROTOCOL;
267 h->session_id = session_id;
268 h->tree_id = tree_id;
269 memcpy(h->file_id, cr.file_id, 16);
270 h->file_size = cr.end_of_file;
271 h->next_message_id = 5;
272 return SmbResult::SMB_OK;
273}
274
275SmbResult smb_open(const SmbConfig *cfg, SmbHandle *h, SmbSendFn send, SmbRecvFn recv, void *ctx)
276{
277 if (!cfg || !h || !send || !recv || !cfg->user || !cfg->pass || !cfg->share || !cfg->path)
278 return SmbResult::SMB_ERR_ARG;
279
280 const char *domain = cfg->domain ? cfg->domain : "";
281
282 SmbResult r = smb_negotiate(send, recv, ctx);
283 if (r != SmbResult::SMB_OK)
284 return r;
285
286 uint64_t session_id = 0;
287 r = smb_session_setup(cfg, domain, send, recv, ctx, &session_id);
288 if (r != SmbResult::SMB_OK)
289 return r;
290
291 uint32_t tree_id = 0;
292 r = smb_tree_connect(cfg, session_id, send, recv, ctx, &tree_id);
293 if (r != SmbResult::SMB_OK)
294 return r;
295
296 return smb_create(cfg, h, session_id, tree_id, send, recv, ctx);
297}
298
299SmbResult smb_close(SmbHandle *h, SmbSendFn send, SmbRecvFn recv, void *ctx)
300{
301 if (!h || !send || !recv)
302 return SmbResult::SMB_ERR_ARG;
303 uint8_t tx[128];
304 uint8_t rx[128];
305 size_t mlen = smb2_build_close(tx + 4, sizeof(tx) - 4, h->next_message_id, h->session_id, h->tree_id, h->file_id);
306 if (!mlen)
307 return SmbResult::SMB_ERR_OVERFLOW;
308 if (!send_msg(send, ctx, tx, mlen))
309 return SmbResult::SMB_ERR_IO;
310 int rl = recv_msg(recv, ctx, rx, sizeof(rx));
311 if (rl < 0)
312 return rl == -2 ? SmbResult::SMB_ERR_OVERFLOW : SmbResult::SMB_ERR_IO;
313 Smb2Header hd;
314 Smb2CloseResp cl;
315 if (!smb2_parse_header(rx, (size_t)rl, &hd) || hd.status != Smb2Status::SMB2_STATUS_SUCCESS)
316 return SmbResult::SMB_ERR_PROTOCOL;
317 if (!smb2_parse_close_response(rx, (size_t)rl, &cl))
318 return SmbResult::SMB_ERR_PROTOCOL;
319 h->next_message_id++;
320 return SmbResult::SMB_OK;
321}
322
323SmbResult smb_read(SmbHandle *h, uint64_t offset, uint8_t *out, size_t cap, size_t *out_len, SmbSendFn send,
324 SmbRecvFn recv, void *ctx)
325{
326 if (!h || !out || !out_len || !send || !recv)
327 return SmbResult::SMB_ERR_ARG;
328 *out_len = 0;
329 const size_t chunk_max = DETWS_SMB_BUF - 96; // room for the header + READ response body
330 size_t total = 0;
331 while (total < cap)
332 {
333 size_t want = cap - total;
334 if (want > chunk_max)
335 want = chunk_max;
336 size_t mlen = smb2_build_read(s_smb.tx + 4, sizeof(s_smb.tx) - 4, h->next_message_id, h->session_id, h->tree_id,
337 h->file_id, (uint32_t)want, offset + total);
338 if (!mlen)
339 return SmbResult::SMB_ERR_OVERFLOW;
340 SmbResult rt = SmbResult::SMB_ERR_IO;
341 int rl = smb_round_trip(send, recv, ctx, mlen, &rt);
342 if (rl < 0)
343 return rt;
344 Smb2Header hd;
345 if (!smb2_parse_header(s_smb.rx, (size_t)rl, &hd))
346 return SmbResult::SMB_ERR_PROTOCOL;
347 h->next_message_id++;
348 if (hd.status == Smb2Status::SMB2_STATUS_END_OF_FILE)
349 break;
350 if (hd.status != Smb2Status::SMB2_STATUS_SUCCESS)
351 return SmbResult::SMB_ERR_PROTOCOL;
352 Smb2ReadResp r;
353 if (!smb2_parse_read_response(s_smb.rx, (size_t)rl, &r) || r.data_len > want)
354 return SmbResult::SMB_ERR_PROTOCOL;
355 if (r.data_len == 0)
356 break;
357 memcpy(out + total, r.data, r.data_len);
358 total += r.data_len;
359 if (r.data_len < want)
360 break; // a short read means we reached the end of the file
361 }
362 *out_len = total;
363 return SmbResult::SMB_OK;
364}
365
366SmbResult smb_write(SmbHandle *h, uint64_t offset, const uint8_t *data, size_t len, size_t *written, SmbSendFn send,
367 SmbRecvFn recv, void *ctx)
368{
369 if (!h || !data || !written || !send || !recv)
370 return SmbResult::SMB_ERR_ARG;
371 *written = 0;
372 const size_t chunk_max = DETWS_SMB_BUF - 128; // room for the header + WRITE request body
373 size_t total = 0;
374 while (total < len)
375 {
376 size_t want = len - total;
377 if (want > chunk_max)
378 want = chunk_max;
379 size_t mlen = smb2_build_write(s_smb.tx + 4, sizeof(s_smb.tx) - 4, h->next_message_id, h->session_id,
380 h->tree_id, h->file_id, data + total, want, offset + total);
381 if (!mlen)
382 return SmbResult::SMB_ERR_OVERFLOW;
383 if (!send_msg(send, ctx, s_smb.tx, mlen))
384 return SmbResult::SMB_ERR_IO;
385 int rl = recv_msg(recv, ctx, s_smb.rx, sizeof(s_smb.rx));
386 if (rl < 0)
387 return rl == -2 ? SmbResult::SMB_ERR_OVERFLOW : SmbResult::SMB_ERR_IO;
388 Smb2Header hd;
389 if (!smb2_parse_header(s_smb.rx, (size_t)rl, &hd))
390 return SmbResult::SMB_ERR_PROTOCOL;
391 h->next_message_id++;
392 if (hd.status != Smb2Status::SMB2_STATUS_SUCCESS)
393 return SmbResult::SMB_ERR_PROTOCOL;
394 Smb2WriteResp w;
395 if (!smb2_parse_write_response(s_smb.rx, (size_t)rl, &w) || w.count == 0 || w.count > want)
396 return SmbResult::SMB_ERR_PROTOCOL; // no progress or a bogus count
397 total += w.count;
398 }
399 if (offset + total > h->file_size)
400 h->file_size = offset + total;
401 *written = total;
402 return SmbResult::SMB_OK;
403}
404
405#endif // DETWS_ENABLE_SMB
#define DETWS_SMB_BUF
SMB2 client work-buffer size (bytes) for smb_client's request/response framing.
NTLMv2 response computation (MS-NLMP §3.3.2) for the SMB2 client (DETWS_ENABLE_SMB).
NTLMSSP message codec (MS-NLMP §2.2.1) for the SMB2 client (DETWS_ENABLE_SMB).
SMB2 client wire codec (MS-SMB2), DETWS_ENABLE_SMB - increment 1: the transport frame,...
SMB2 client dialogue engine (DETWS_ENABLE_SMB) - drives the smb2 / ntlm / spnego wire codecs through ...
SPNEGO (RFC 4178) GSS-API wrapping of the NTLMSSP tokens for the SMB2 client (DETWS_ENABLE_SMB).