Line data Source code
1 : #include "test_helpers.h"
2 : #include "imap_client.h"
3 : #include <stdlib.h>
4 : #include <string.h>
5 : #include <unistd.h>
6 : #include <sys/socket.h>
7 : #include <sys/wait.h>
8 : #include <netinet/in.h>
9 : #include <arpa/inet.h>
10 : #include <openssl/ssl.h>
11 : #include <openssl/err.h>
12 :
13 : /* ── Helpers ─────────────────────────────────────────────────────────────── */
14 :
15 : /*
16 : * Create a listening TCP socket bound to a random loopback port.
17 : * Returns the fd and fills *port_out with the actual port number.
18 : * Returns -1 on error.
19 : */
20 3 : static int make_listener(int *port_out) {
21 3 : int fd = socket(AF_INET, SOCK_STREAM, 0);
22 3 : if (fd < 0) return -1;
23 3 : int one = 1;
24 3 : setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
25 3 : struct sockaddr_in addr = {0};
26 3 : addr.sin_family = AF_INET;
27 3 : addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
28 3 : addr.sin_port = 0; /* OS picks a free port */
29 6 : if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0 ||
30 3 : listen(fd, 1) < 0) {
31 0 : close(fd);
32 0 : return -1;
33 : }
34 3 : socklen_t len = sizeof(addr);
35 3 : getsockname(fd, (struct sockaddr *)&addr, &len);
36 3 : *port_out = ntohs(addr.sin_port);
37 3 : return fd;
38 : }
39 :
40 : /*
41 : * Create a server-side SSL_CTX loaded with the test self-signed certificate.
42 : * Returns NULL on failure.
43 : */
44 3 : static SSL_CTX *create_server_ctx(void) {
45 3 : SSL_CTX *ctx = SSL_CTX_new(TLS_server_method());
46 3 : if (!ctx) {
47 0 : ERR_print_errors_fp(stderr);
48 0 : return NULL;
49 : }
50 3 : if (SSL_CTX_use_certificate_file(ctx, "tests/certs/test.crt", SSL_FILETYPE_PEM) <= 0) {
51 0 : fprintf(stderr, "Failed to load test cert: tests/certs/test.crt\n");
52 0 : ERR_print_errors_fp(stderr);
53 0 : SSL_CTX_free(ctx);
54 0 : return NULL;
55 : }
56 3 : if (SSL_CTX_use_PrivateKey_file(ctx, "tests/certs/test.key", SSL_FILETYPE_PEM) <= 0) {
57 0 : fprintf(stderr, "Failed to load test key: tests/certs/test.key\n");
58 0 : ERR_print_errors_fp(stderr);
59 0 : SSL_CTX_free(ctx);
60 0 : return NULL;
61 : }
62 3 : return ctx;
63 : }
64 :
65 : /*
66 : * Minimal IMAP server child process (TLS).
67 : *
68 : * Accepts ONE connection over TLS, sends `greeting`, then loops reading lines
69 : * and replies according to the command:
70 : * LOGIN → sends login_reply
71 : * LOGOUT → sends BYE + OK, exits cleanly
72 : * other → sends "TAG BAD Unknown"
73 : *
74 : * After the first LOGOUT (or client disconnect) the child exits.
75 : */
76 0 : static void run_mock_server(int listen_fd,
77 : const char *greeting,
78 : const char *login_reply,
79 : SSL_CTX *ctx) {
80 0 : int cfd = accept(listen_fd, NULL, NULL);
81 0 : close(listen_fd);
82 0 : if (cfd < 0) {
83 0 : SSL_CTX_free(ctx);
84 0 : _exit(1);
85 : }
86 :
87 : /* timeout so the child never hangs in CI */
88 0 : struct timeval tv = {.tv_sec = 3, .tv_usec = 0};
89 0 : setsockopt(cfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
90 :
91 0 : SSL *ssl = SSL_new(ctx);
92 0 : SSL_CTX_free(ctx); /* child no longer needs ctx after SSL_new */
93 0 : SSL_set_fd(ssl, cfd);
94 0 : if (SSL_accept(ssl) <= 0) {
95 0 : ERR_print_errors_fp(stderr);
96 0 : SSL_free(ssl);
97 0 : close(cfd);
98 0 : _exit(1);
99 : }
100 :
101 0 : SSL_write(ssl, greeting, (int)strlen(greeting));
102 :
103 0 : char buf[1024];
104 0 : while (1) {
105 0 : int n = SSL_read(ssl, buf, (int)(sizeof(buf) - 1));
106 0 : if (n <= 0) break;
107 0 : buf[n] = '\0';
108 :
109 : /* Extract tag (first token) */
110 0 : char tag[32] = "*";
111 0 : sscanf(buf, "%31s", tag);
112 :
113 0 : if (strstr(buf, "LOGIN")) {
114 0 : SSL_write(ssl, login_reply, (int)strlen(login_reply));
115 0 : } else if (strstr(buf, "APPEND")) {
116 : /* Handle LITERAL+ {N+} and synchronising {N} literals */
117 0 : char *lbrace = strrchr(buf, '{');
118 0 : long lsize = 0;
119 0 : int sync = 1;
120 0 : if (lbrace) {
121 0 : char *end = NULL;
122 0 : lsize = strtol(lbrace + 1, &end, 10);
123 0 : if (end && *end == '+') sync = 0;
124 : }
125 0 : if (lsize <= 0) {
126 0 : char bad2[64];
127 0 : snprintf(bad2, sizeof(bad2), "%s BAD Missing size\r\n", tag);
128 0 : SSL_write(ssl, bad2, (int)strlen(bad2));
129 : } else {
130 0 : if (sync) SSL_write(ssl, "+ OK\r\n", 6);
131 : /* Drain the literal bytes already in buf + remaining reads */
132 0 : char *ptr = lbrace ? strchr(lbrace, '}') : NULL;
133 0 : long already = 0;
134 0 : if (ptr) {
135 : /* skip past '}' and optional '+' and '\r\n' */
136 0 : ptr++;
137 0 : if (*ptr == '+') ptr++;
138 0 : if (*ptr == '\r') ptr++;
139 0 : if (*ptr == '\n') ptr++;
140 0 : already = (long)(buf + n - ptr);
141 0 : if (already > lsize) already = lsize;
142 : }
143 0 : long remaining = lsize - already;
144 0 : char tmp[512];
145 0 : while (remaining > 0) {
146 0 : int r2 = SSL_read(ssl, tmp,
147 : remaining > (long)sizeof(tmp) ?
148 : (int)sizeof(tmp) : (int)remaining);
149 0 : if (r2 <= 0) break;
150 0 : remaining -= r2;
151 : }
152 0 : char ok2[80];
153 0 : snprintf(ok2, sizeof(ok2),
154 : "%s OK [APPENDUID 1 99] APPEND completed\r\n", tag);
155 0 : SSL_write(ssl, ok2, (int)strlen(ok2));
156 : }
157 0 : } else if (strstr(buf, "LOGOUT")) {
158 0 : const char *bye = "* BYE Logging out\r\n";
159 0 : SSL_write(ssl, bye, (int)strlen(bye));
160 0 : char ok[64];
161 0 : snprintf(ok, sizeof(ok), "%s OK LOGOUT completed\r\n", tag);
162 0 : SSL_write(ssl, ok, (int)strlen(ok));
163 0 : break;
164 : } else {
165 0 : char bad[64];
166 0 : snprintf(bad, sizeof(bad), "%s BAD Unknown\r\n", tag);
167 0 : SSL_write(ssl, bad, (int)strlen(bad));
168 : }
169 : }
170 0 : SSL_shutdown(ssl);
171 0 : SSL_free(ssl);
172 0 : close(cfd);
173 0 : _exit(0);
174 : }
175 :
176 : /* ── Test: imap_connect / read_response ─────────────────────────────────── */
177 :
178 : /*
179 : * Regression test for the use-after-free bug in read_response().
180 : *
181 : * Previously, linebuf_free() was called before strncasecmp(status, "OK"),
182 : * so if the allocator reused the memory the check would fail, returning -1
183 : * even though the server sent "TAG OK Logged in". This caused spurious
184 : * "LOGIN failed" errors in production.
185 : *
186 : * The test verifies that imap_connect() returns non-NULL (login accepted)
187 : * when the server genuinely responds with OK — including the long
188 : * [CAPABILITY ...] inline text that Dovecot sends, which increases the
189 : * chance of the allocator reusing freed memory.
190 : */
191 1 : void test_imap_connect_login_ok(void) {
192 1 : int port = 0;
193 1 : int lfd = make_listener(&port);
194 1 : ASSERT(lfd >= 0, "make_listener: could not bind");
195 :
196 1 : SSL_CTX *ctx = create_server_ctx();
197 1 : ASSERT(ctx != NULL, "create_server_ctx failed");
198 :
199 : /* Simulate a real Dovecot-style OK with a long CAPABILITY string so the
200 : * allocator is more likely to reuse memory if a bug exists. */
201 1 : const char *greeting =
202 : "* OK [CAPABILITY IMAP4rev1 SASL-IR LOGIN-REFERRALS ID ENABLE IDLE"
203 : " LITERAL+ AUTH=PLAIN AUTH=LOGIN] Dovecot ready.\r\n";
204 1 : char login_reply[512];
205 1 : snprintf(login_reply, sizeof(login_reply),
206 : "A0001 OK [CAPABILITY IMAP4rev1 SASL-IR LOGIN-REFERRALS ID ENABLE"
207 : " IDLE SORT SORT=DISPLAY THREAD=REFERENCES THREAD=REFS"
208 : " THREAD=ORDEREDSUBJECT MULTIAPPEND URL-PARTIAL CATENATE UNSELECT"
209 : " CHILDREN NAMESPACE UIDPLUS LIST-EXTENDED I18NLEVEL=1 CONDSTORE"
210 : " QRESYNC ESEARCH ESORT SEARCHRES WITHIN CONTEXT=SEARCH"
211 : " LIST-STATUS BINARY MOVE SNIPPET=FUZZY PREVIEW=FUZZY PREVIEW"
212 : " STATUS=SIZE SAVEDATE LITERAL+ NOTIFY SPECIAL-USE QUOTA]"
213 : " Logged in\r\n");
214 :
215 1 : pid_t pid = fork();
216 1 : ASSERT(pid >= 0, "fork failed");
217 :
218 1 : if (pid == 0) {
219 0 : run_mock_server(lfd, greeting, login_reply, ctx);
220 : /* _exit is called inside run_mock_server */
221 : }
222 1 : close(lfd);
223 1 : SSL_CTX_free(ctx);
224 :
225 1 : char url[64];
226 1 : snprintf(url, sizeof(url), "imaps://127.0.0.1:%d", port);
227 1 : ImapClient *c = imap_connect(url, "user", "pass", 0);
228 :
229 1 : ASSERT(c != NULL,
230 : "imap_connect must return non-NULL when server responds OK "
231 : "(regression: use-after-free in read_response caused OK check to fail)");
232 :
233 1 : imap_disconnect(c);
234 :
235 1 : int status = 0;
236 1 : waitpid(pid, &status, 0);
237 : }
238 :
239 : /*
240 : * Verify that imap_connect() returns NULL when the server explicitly
241 : * rejects the login (NO / BAD response).
242 : */
243 1 : void test_imap_connect_login_rejected(void) {
244 1 : int port = 0;
245 1 : int lfd = make_listener(&port);
246 1 : ASSERT(lfd >= 0, "make_listener: could not bind");
247 :
248 1 : SSL_CTX *ctx = create_server_ctx();
249 1 : ASSERT(ctx != NULL, "create_server_ctx failed");
250 :
251 1 : const char *greeting = "* OK Mock ready\r\n";
252 1 : const char *login_reply = "A0001 NO [AUTHENTICATIONFAILED] Invalid credentials\r\n";
253 :
254 1 : pid_t pid = fork();
255 1 : ASSERT(pid >= 0, "fork failed");
256 :
257 1 : if (pid == 0) {
258 0 : run_mock_server(lfd, greeting, login_reply, ctx);
259 : }
260 1 : close(lfd);
261 1 : SSL_CTX_free(ctx);
262 :
263 1 : char url[64];
264 1 : snprintf(url, sizeof(url), "imaps://127.0.0.1:%d", port);
265 1 : ImapClient *c = imap_connect(url, "bad_user", "bad_pass", 0);
266 :
267 1 : ASSERT(c == NULL, "imap_connect must return NULL when server says NO");
268 :
269 1 : int status = 0;
270 1 : waitpid(pid, &status, 0);
271 : }
272 :
273 : /*
274 : * Verify that imap_append() correctly uses LITERAL+ (non-synchronising
275 : * literal, "{N+}") and the server returns OK after receiving the message.
276 : */
277 1 : void test_imap_append_literal_plus(void) {
278 1 : int port = 0;
279 1 : int lfd = make_listener(&port);
280 1 : ASSERT(lfd >= 0, "make_listener: could not bind");
281 :
282 1 : SSL_CTX *ctx = create_server_ctx();
283 1 : ASSERT(ctx != NULL, "create_server_ctx failed");
284 :
285 1 : const char *greeting = "* OK Mock ready\r\n";
286 1 : const char *login_reply =
287 : "A0001 OK [CAPABILITY IMAP4rev1 LITERAL+] Logged in\r\n";
288 :
289 1 : pid_t pid = fork();
290 1 : ASSERT(pid >= 0, "fork failed");
291 :
292 1 : if (pid == 0) {
293 0 : run_mock_server(lfd, greeting, login_reply, ctx);
294 : }
295 1 : close(lfd);
296 1 : SSL_CTX_free(ctx);
297 :
298 1 : char url[64];
299 1 : snprintf(url, sizeof(url), "imaps://127.0.0.1:%d", port);
300 1 : ImapClient *c = imap_connect(url, "user", "pass", 0);
301 1 : ASSERT(c != NULL, "imap_append test: imap_connect must succeed");
302 :
303 1 : const char *msg = "From: a@b.com\r\nSubject: Test\r\n\r\nHello.\r\n";
304 1 : int rc = imap_append(c, "Sent", msg, strlen(msg));
305 1 : ASSERT(rc == 0, "imap_append must return 0 (OK) with LITERAL+");
306 :
307 1 : imap_disconnect(c);
308 :
309 1 : int status = 0;
310 1 : waitpid(pid, &status, 0);
311 : }
312 :
313 : /* ── Test suite entry point ──────────────────────────────────────────────── */
314 :
315 1 : void test_imap_client(void) {
316 : /* Verify that a NULL pointer is handled gracefully */
317 1 : imap_disconnect(NULL);
318 :
319 : /* imap_connect with a bad host must return NULL, not crash */
320 1 : ImapClient *c = imap_connect("imaps://invalid.host.example.invalid",
321 : "user", "pass", 1);
322 1 : ASSERT(c == NULL, "imap_connect to invalid host should return NULL");
323 :
324 1 : test_imap_connect_login_ok();
325 1 : test_imap_connect_login_rejected();
326 1 : test_imap_append_literal_plus();
327 : }
|