1 /* $OpenBSD: rrdp_notification.c,v 1.21 2024/04/12 11:50:29 job Exp $ */
2 /*
3 * Copyright (c) 2020 Nils Fisher <nils_fisher@hotmail.com>
4 * Copyright (c) 2021 Claudio Jeker <claudio@openbsd.org>
5 *
6 * Permission to use, copy, modify, and distribute this software for any
7 * purpose with or without fee is hereby granted, provided that the above
8 * copyright notice and this permission notice appear in all copies.
9 *
10 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17 */
18
19 #include <sys/stat.h>
20
21 #include <assert.h>
22 #include <err.h>
23 #include <errno.h>
24 #include <limits.h>
25 #include <fcntl.h>
26 #include <string.h>
27 #include <unistd.h>
28
29 #include <expat.h>
30 #include <openssl/sha.h>
31
32 #include "extern.h"
33 #include "rrdp.h"
34
35 enum notification_scope {
36 NOTIFICATION_SCOPE_START,
37 NOTIFICATION_SCOPE_NOTIFICATION,
38 NOTIFICATION_SCOPE_SNAPSHOT,
39 NOTIFICATION_SCOPE_NOTIFICATION_POST_SNAPSHOT,
40 NOTIFICATION_SCOPE_DELTA,
41 NOTIFICATION_SCOPE_END
42 };
43
44 struct delta_item {
45 char *uri;
46 char hash[SHA256_DIGEST_LENGTH];
47 long long serial;
48 TAILQ_ENTRY(delta_item) q;
49 };
50
51 TAILQ_HEAD(delta_q, delta_item);
52
53 struct notification_xml {
54 XML_Parser parser;
55 struct rrdp_session *repository;
56 struct rrdp_session *current;
57 const char *notifyuri;
58 char *session_id;
59 char *snapshot_uri;
60 char snapshot_hash[SHA256_DIGEST_LENGTH];
61 struct delta_q delta_q;
62 long long serial;
63 long long min_serial;
64 int version;
65 enum notification_scope scope;
66 };
67
68 static void free_delta(struct delta_item *);
69
70 static int
add_delta(struct notification_xml * nxml,const char * uri,const char hash[SHA256_DIGEST_LENGTH],long long serial)71 add_delta(struct notification_xml *nxml, const char *uri,
72 const char hash[SHA256_DIGEST_LENGTH], long long serial)
73 {
74 struct delta_item *d, *n;
75
76 if ((d = calloc(1, sizeof(struct delta_item))) == NULL)
77 err(1, "%s - calloc", __func__);
78
79 d->serial = serial;
80 d->uri = xstrdup(uri);
81 memcpy(d->hash, hash, sizeof(d->hash));
82
83 /* optimise for a sorted input */
84 n = TAILQ_LAST(&nxml->delta_q, delta_q);
85 if (n == NULL)
86 TAILQ_INSERT_HEAD(&nxml->delta_q, d, q);
87 else if (n->serial < serial)
88 TAILQ_INSERT_TAIL(&nxml->delta_q, d, q);
89 else
90 TAILQ_FOREACH(n, &nxml->delta_q, q) {
91 if (n->serial == serial) {
92 warnx("duplicate delta serial %lld ", serial);
93 free_delta(d);
94 return 0;
95 }
96 if (n->serial > serial) {
97 TAILQ_INSERT_BEFORE(n, d, q);
98 break;
99 }
100 }
101
102 return 1;
103 }
104
105 /* check that there are no holes in the list */
106 static int
check_delta(struct notification_xml * nxml)107 check_delta(struct notification_xml *nxml)
108 {
109 struct delta_item *d;
110 long long serial = 0;
111
112 TAILQ_FOREACH(d, &nxml->delta_q, q) {
113 if (serial != 0 && serial + 1 != d->serial)
114 return 0;
115 serial = d->serial;
116 }
117 return 1;
118 }
119
120 static void
free_delta(struct delta_item * d)121 free_delta(struct delta_item *d)
122 {
123 free(d->uri);
124 free(d);
125 }
126
127 /*
128 * Parse a delta serial and hash line at idx from the rrdp session state.
129 * Return the serial or 0 on error. If hash is non-NULL, it is set to the
130 * start of the hash string on success.
131 */
132 static long long
delta_parse(struct rrdp_session * s,size_t idx,char ** hash)133 delta_parse(struct rrdp_session *s, size_t idx, char **hash)
134 {
135 long long serial;
136 char *line, *ep;
137
138 if (hash != NULL)
139 *hash = NULL;
140 if (idx < 0 || idx >= sizeof(s->deltas) / sizeof(s->deltas[0]))
141 return 0;
142 if ((line = s->deltas[idx]) == NULL)
143 return 0;
144
145 errno = 0;
146 serial = strtoll(line, &ep, 10);
147 if (line[0] == '\0' || *ep != ' ')
148 return 0;
149 if (serial <= 0 || (errno == ERANGE && serial == LLONG_MAX))
150 return 0;
151
152 if (hash != NULL)
153 *hash = ep + 1;
154 return serial;
155 }
156
157 static void
start_notification_elem(struct notification_xml * nxml,const char ** attr)158 start_notification_elem(struct notification_xml *nxml, const char **attr)
159 {
160 XML_Parser p = nxml->parser;
161 int has_xmlns = 0;
162 size_t i;
163
164 if (nxml->scope != NOTIFICATION_SCOPE_START)
165 PARSE_FAIL(p,
166 "parse failed - entered notification elem unexpectedely");
167 for (i = 0; attr[i]; i += 2) {
168 const char *errstr;
169 if (strcmp("xmlns", attr[i]) == 0 &&
170 strcmp(RRDP_XMLNS, attr[i + 1]) == 0) {
171 has_xmlns = 1;
172 continue;
173 }
174 if (strcmp("session_id", attr[i]) == 0 &&
175 valid_uuid(attr[i + 1])) {
176 nxml->session_id = xstrdup(attr[i + 1]);
177 continue;
178 }
179 if (strcmp("version", attr[i]) == 0) {
180 nxml->version = strtonum(attr[i + 1],
181 1, MAX_VERSION, &errstr);
182 if (errstr == NULL)
183 continue;
184 }
185 if (strcmp("serial", attr[i]) == 0) {
186 nxml->serial = strtonum(attr[i + 1],
187 1, LLONG_MAX, &errstr);
188 if (errstr == NULL)
189 continue;
190 }
191 PARSE_FAIL(p, "parse failed - non conforming "
192 "attribute '%s' found in notification elem", attr[i]);
193 }
194 if (!(has_xmlns && nxml->version && nxml->session_id && nxml->serial))
195 PARSE_FAIL(p, "parse failed - incomplete "
196 "notification attributes");
197
198 /* Limit deltas to the ones which matter for us. */
199 if (nxml->min_serial == 0 && nxml->serial > MAX_RRDP_DELTAS)
200 nxml->min_serial = nxml->serial - MAX_RRDP_DELTAS;
201
202 nxml->scope = NOTIFICATION_SCOPE_NOTIFICATION;
203 }
204
205 static void
end_notification_elem(struct notification_xml * nxml)206 end_notification_elem(struct notification_xml *nxml)
207 {
208 XML_Parser p = nxml->parser;
209
210 if (nxml->scope != NOTIFICATION_SCOPE_NOTIFICATION_POST_SNAPSHOT)
211 PARSE_FAIL(p, "parse failed - exited notification "
212 "elem unexpectedely");
213 nxml->scope = NOTIFICATION_SCOPE_END;
214
215 if (!check_delta(nxml))
216 PARSE_FAIL(p, "parse failed - delta list has holes");
217 }
218
219 static void
start_snapshot_elem(struct notification_xml * nxml,const char ** attr)220 start_snapshot_elem(struct notification_xml *nxml, const char **attr)
221 {
222 XML_Parser p = nxml->parser;
223 int i, hasUri = 0, hasHash = 0;
224
225 if (nxml->scope != NOTIFICATION_SCOPE_NOTIFICATION)
226 PARSE_FAIL(p,
227 "parse failed - entered snapshot elem unexpectedely");
228 for (i = 0; attr[i]; i += 2) {
229 if (strcmp("uri", attr[i]) == 0 && hasUri++ == 0) {
230 if (valid_uri(attr[i + 1], strlen(attr[i + 1]),
231 HTTPS_PROTO) &&
232 valid_origin(attr[i + 1], nxml->notifyuri)) {
233 nxml->snapshot_uri = xstrdup(attr[i + 1]);
234 continue;
235 }
236 }
237 if (strcmp("hash", attr[i]) == 0 && hasHash++ == 0) {
238 if (hex_decode(attr[i + 1], nxml->snapshot_hash,
239 sizeof(nxml->snapshot_hash)) == 0)
240 continue;
241 }
242 PARSE_FAIL(p, "parse failed - non conforming "
243 "attribute '%s' found in snapshot elem", attr[i]);
244 }
245 if (hasUri != 1 || hasHash != 1)
246 PARSE_FAIL(p, "parse failed - incomplete snapshot attributes");
247
248 nxml->scope = NOTIFICATION_SCOPE_SNAPSHOT;
249 }
250
251 static void
end_snapshot_elem(struct notification_xml * nxml)252 end_snapshot_elem(struct notification_xml *nxml)
253 {
254 XML_Parser p = nxml->parser;
255
256 if (nxml->scope != NOTIFICATION_SCOPE_SNAPSHOT)
257 PARSE_FAIL(p, "parse failed - exited snapshot "
258 "elem unexpectedely");
259 nxml->scope = NOTIFICATION_SCOPE_NOTIFICATION_POST_SNAPSHOT;
260 }
261
262 static void
start_delta_elem(struct notification_xml * nxml,const char ** attr)263 start_delta_elem(struct notification_xml *nxml, const char **attr)
264 {
265 XML_Parser p = nxml->parser;
266 int i, hasUri = 0, hasHash = 0;
267 const char *delta_uri = NULL;
268 char delta_hash[SHA256_DIGEST_LENGTH];
269 long long delta_serial = 0;
270
271 if (nxml->scope != NOTIFICATION_SCOPE_NOTIFICATION_POST_SNAPSHOT)
272 PARSE_FAIL(p, "parse failed - entered delta "
273 "elem unexpectedely");
274 for (i = 0; attr[i]; i += 2) {
275 if (strcmp("uri", attr[i]) == 0 && hasUri++ == 0) {
276 if (valid_uri(attr[i + 1], strlen(attr[i + 1]),
277 HTTPS_PROTO) &&
278 valid_origin(attr[i + 1], nxml->notifyuri)) {
279 delta_uri = attr[i + 1];
280 continue;
281 }
282 }
283 if (strcmp("hash", attr[i]) == 0 && hasHash++ == 0) {
284 if (hex_decode(attr[i + 1], delta_hash,
285 sizeof(delta_hash)) == 0)
286 continue;
287 }
288 if (strcmp("serial", attr[i]) == 0 && delta_serial == 0) {
289 const char *errstr;
290
291 delta_serial = strtonum(attr[i + 1],
292 1, LLONG_MAX, &errstr);
293 if (errstr == NULL)
294 continue;
295 }
296 PARSE_FAIL(p, "parse failed - non conforming "
297 "attribute '%s' found in delta elem", attr[i]);
298 }
299 /* Only add to the list if we are relevant */
300 if (hasUri != 1 || hasHash != 1 || delta_serial == 0)
301 PARSE_FAIL(p, "parse failed - incomplete delta attributes");
302
303 /* Delta serial must be smaller or equal to the notification serial */
304 if (nxml->serial < delta_serial)
305 PARSE_FAIL(p, "parse failed - bad delta serial");
306
307 /* optimisation, add only deltas that could be interesting */
308 if (nxml->min_serial < delta_serial) {
309 if (add_delta(nxml, delta_uri, delta_hash, delta_serial) == 0)
310 PARSE_FAIL(p, "parse failed - adding delta failed");
311 }
312
313 nxml->scope = NOTIFICATION_SCOPE_DELTA;
314 }
315
316 static void
end_delta_elem(struct notification_xml * nxml)317 end_delta_elem(struct notification_xml *nxml)
318 {
319 XML_Parser p = nxml->parser;
320
321 if (nxml->scope != NOTIFICATION_SCOPE_DELTA)
322 PARSE_FAIL(p, "parse failed - exited delta elem unexpectedely");
323 nxml->scope = NOTIFICATION_SCOPE_NOTIFICATION_POST_SNAPSHOT;
324 }
325
326 static void
notification_xml_elem_start(void * data,const char * el,const char ** attr)327 notification_xml_elem_start(void *data, const char *el, const char **attr)
328 {
329 struct notification_xml *nxml = data;
330 XML_Parser p = nxml->parser;
331
332 /*
333 * Can only enter here once as we should have no ways to get back to
334 * START scope
335 */
336 if (strcmp("notification", el) == 0)
337 start_notification_elem(nxml, attr);
338 /*
339 * Will enter here multiple times, BUT never nested. will start
340 * collecting character data in that handler
341 * mem is cleared in end block, (TODO or on parse failure)
342 */
343 else if (strcmp("snapshot", el) == 0)
344 start_snapshot_elem(nxml, attr);
345 else if (strcmp("delta", el) == 0)
346 start_delta_elem(nxml, attr);
347 else
348 PARSE_FAIL(p, "parse failed - unexpected elem exit found");
349 }
350
351 static void
notification_xml_elem_end(void * data,const char * el)352 notification_xml_elem_end(void *data, const char *el)
353 {
354 struct notification_xml *nxml = data;
355 XML_Parser p = nxml->parser;
356
357 if (strcmp("notification", el) == 0)
358 end_notification_elem(nxml);
359 else if (strcmp("snapshot", el) == 0)
360 end_snapshot_elem(nxml);
361 else if (strcmp("delta", el) == 0)
362 end_delta_elem(nxml);
363 else
364 PARSE_FAIL(p, "parse failed - unexpected elem exit found");
365 }
366
367 static void
notification_doctype_handler(void * data,const char * doctypeName,const char * sysid,const char * pubid,int subset)368 notification_doctype_handler(void *data, const char *doctypeName,
369 const char *sysid, const char *pubid, int subset)
370 {
371 struct notification_xml *nxml = data;
372 XML_Parser p = nxml->parser;
373
374 PARSE_FAIL(p, "parse failed - DOCTYPE not allowed");
375 }
376
377 struct notification_xml *
new_notification_xml(XML_Parser p,struct rrdp_session * repository,struct rrdp_session * current,const char * notifyuri)378 new_notification_xml(XML_Parser p, struct rrdp_session *repository,
379 struct rrdp_session *current, const char *notifyuri)
380 {
381 struct notification_xml *nxml;
382
383 if ((nxml = calloc(1, sizeof(*nxml))) == NULL)
384 err(1, "%s", __func__);
385 TAILQ_INIT(&(nxml->delta_q));
386 nxml->parser = p;
387 nxml->repository = repository;
388 nxml->current = current;
389 nxml->notifyuri = notifyuri;
390 nxml->min_serial = delta_parse(repository, 0, NULL);
391
392 XML_SetElementHandler(nxml->parser, notification_xml_elem_start,
393 notification_xml_elem_end);
394 XML_SetUserData(nxml->parser, nxml);
395 XML_SetDoctypeDeclHandler(nxml->parser, notification_doctype_handler,
396 NULL);
397
398 return nxml;
399 }
400
401 static void
free_delta_queue(struct notification_xml * nxml)402 free_delta_queue(struct notification_xml *nxml)
403 {
404 while (!TAILQ_EMPTY(&nxml->delta_q)) {
405 struct delta_item *d = TAILQ_FIRST(&nxml->delta_q);
406 TAILQ_REMOVE(&nxml->delta_q, d, q);
407 free_delta(d);
408 }
409 }
410
411 void
free_notification_xml(struct notification_xml * nxml)412 free_notification_xml(struct notification_xml *nxml)
413 {
414 if (nxml == NULL)
415 return;
416
417 free(nxml->session_id);
418 free(nxml->snapshot_uri);
419 free_delta_queue(nxml);
420 free(nxml);
421 }
422
423 /*
424 * Collect a list of deltas to store in the repository state.
425 */
426 static void
notification_collect_deltas(struct notification_xml * nxml)427 notification_collect_deltas(struct notification_xml *nxml)
428 {
429 struct delta_item *d;
430 long long keep_serial = 0;
431 size_t cur_idx = 0, max_deltas;
432 char *hash;
433
434 max_deltas =
435 sizeof(nxml->current->deltas) / sizeof(nxml->current->deltas[0]);
436
437 if (nxml->serial > (long long)max_deltas)
438 keep_serial = nxml->serial - max_deltas + 1;
439
440 TAILQ_FOREACH(d, &nxml->delta_q, q) {
441 if (d->serial >= keep_serial) {
442 assert(cur_idx < max_deltas);
443 hash = hex_encode(d->hash, sizeof(d->hash));
444 if (asprintf(&nxml->current->deltas[cur_idx++],
445 "%lld %s", d->serial, hash) == -1)
446 err(1, NULL);
447 free(hash);
448 }
449 }
450 }
451
452 /*
453 * Validate the delta list with the information from the repository state.
454 * Remove all obsolete deltas so that the list starts with the delta with
455 * serial nxml->repository->serial + 1.
456 * Returns 1 if all deltas were valid and 0 on failure.
457 */
458 static int
notification_check_deltas(struct notification_xml * nxml)459 notification_check_deltas(struct notification_xml *nxml)
460 {
461 struct delta_item *d, *nextd;
462 char *hash, *exp_hash;
463 long long exp_serial, new_serial;
464 size_t exp_idx = 0;
465
466 exp_serial = delta_parse(nxml->repository, exp_idx++, &exp_hash);
467 new_serial = nxml->repository->serial + 1;
468
469 /* compare hash of delta against repository state info */
470 TAILQ_FOREACH_SAFE(d, &nxml->delta_q, q, nextd) {
471 while (exp_serial != 0 && exp_serial < d->serial) {
472 exp_serial = delta_parse(nxml->repository,
473 exp_idx++, &exp_hash);
474 }
475
476 if (d->serial == exp_serial) {
477 hash = hex_encode(d->hash, sizeof(d->hash));
478 if (strcmp(hash, exp_hash) != 0) {
479 warnx("%s: %s#%lld unexpected delta "
480 "mutation (expected %s, got %s)",
481 nxml->notifyuri, nxml->session_id,
482 exp_serial, hash, exp_hash);
483 free(hash);
484 return 0;
485 }
486 free(hash);
487 exp_serial = delta_parse(nxml->repository,
488 exp_idx++, &exp_hash);
489 }
490
491 /* is this delta needed? */
492 if (d->serial < new_serial) {
493 TAILQ_REMOVE(&nxml->delta_q, d, q);
494 free_delta(d);
495 }
496 }
497
498 return 1;
499 }
500
501 /*
502 * Finalize notification step, decide if a delta update is possible
503 * if either the session_id changed or the delta files fail to cover
504 * all the steps up to the new serial fall back to a snapshot.
505 * Return SNAPSHOT or DELTA for snapshot or delta processing.
506 * Return NOTIFICATION if repository is up to date.
507 */
508 enum rrdp_task
notification_done(struct notification_xml * nxml,char * last_mod)509 notification_done(struct notification_xml *nxml, char *last_mod)
510 {
511 nxml->current->last_mod = last_mod;
512 nxml->current->session_id = xstrdup(nxml->session_id);
513 notification_collect_deltas(nxml);
514
515 /* check the that the session_id was valid and still the same */
516 if (nxml->repository->session_id == NULL ||
517 strcmp(nxml->session_id, nxml->repository->session_id) != 0)
518 goto snapshot;
519
520 /* if repository serial is 0 fall back to snapshot */
521 if (nxml->repository->serial == 0)
522 goto snapshot;
523
524 /* check that all needed deltas are available and valid */
525 if (!notification_check_deltas(nxml))
526 goto snapshot;
527
528 if (nxml->repository->serial > nxml->serial)
529 warnx("%s: serial number decreased from %lld to %lld",
530 nxml->notifyuri, nxml->repository->serial, nxml->serial);
531
532 /* if our serial is equal or plus 2, the repo is up to date */
533 if (nxml->repository->serial >= nxml->serial &&
534 nxml->repository->serial - nxml->serial <= 2) {
535 nxml->current->serial = nxml->repository->serial;
536 return NOTIFICATION;
537 }
538
539 /* it makes no sense to process too many deltas */
540 if (nxml->serial - nxml->repository->serial > MAX_RRDP_DELTAS)
541 goto snapshot;
542
543 /* no deltas queued */
544 if (TAILQ_EMPTY(&nxml->delta_q))
545 goto snapshot;
546
547 /* first possible delta is no match */
548 if (nxml->repository->serial + 1 != TAILQ_FIRST(&nxml->delta_q)->serial)
549 goto snapshot;
550
551 /* update via delta possible */
552 nxml->current->serial = nxml->repository->serial;
553 nxml->repository->serial = nxml->serial;
554 return DELTA;
555
556 snapshot:
557 /* update via snapshot download */
558 free_delta_queue(nxml);
559 nxml->current->serial = nxml->serial;
560 return SNAPSHOT;
561 }
562
563 const char *
notification_get_next(struct notification_xml * nxml,char * hash,size_t hlen,enum rrdp_task task)564 notification_get_next(struct notification_xml *nxml, char *hash, size_t hlen,
565 enum rrdp_task task)
566 {
567 struct delta_item *d;
568
569 switch (task) {
570 case SNAPSHOT:
571 assert(hlen == sizeof(nxml->snapshot_hash));
572 memcpy(hash, nxml->snapshot_hash, hlen);
573 /*
574 * Ensure that the serial is correct in case a previous
575 * delta request failed.
576 */
577 nxml->current->serial = nxml->serial;
578 return nxml->snapshot_uri;
579 case DELTA:
580 /* first bump serial, then use first delta */
581 nxml->current->serial += 1;
582 d = TAILQ_FIRST(&nxml->delta_q);
583 assert(d->serial == nxml->current->serial);
584 assert(hlen == sizeof(d->hash));
585 memcpy(hash, d->hash, hlen);
586 return d->uri;
587 default:
588 errx(1, "%s: bad task", __func__);
589 }
590 }
591
592 /*
593 * Pop first element from the delta queue. Return non-0 if this was the last
594 * delta to fetch.
595 */
596 int
notification_delta_done(struct notification_xml * nxml)597 notification_delta_done(struct notification_xml *nxml)
598 {
599 struct delta_item *d;
600
601 d = TAILQ_FIRST(&nxml->delta_q);
602 assert(d->serial == nxml->current->serial);
603 TAILQ_REMOVE(&nxml->delta_q, d, q);
604 free_delta(d);
605
606 assert(!TAILQ_EMPTY(&nxml->delta_q) ||
607 nxml->serial == nxml->current->serial);
608 return TAILQ_EMPTY(&nxml->delta_q);
609 }
610
611 /* Used in regress. */
612 void
log_notification_xml(struct notification_xml * nxml)613 log_notification_xml(struct notification_xml *nxml)
614 {
615 struct delta_item *d;
616 char *hash;
617
618 logx("session_id: %s, serial: %lld", nxml->session_id, nxml->serial);
619 logx("snapshot_uri: %s", nxml->snapshot_uri);
620 hash = hex_encode(nxml->snapshot_hash, sizeof(nxml->snapshot_hash));
621 logx("snapshot hash: %s", hash);
622 free(hash);
623
624 TAILQ_FOREACH(d, &nxml->delta_q, q) {
625 logx("delta serial %lld uri: %s", d->serial, d->uri);
626 hash = hex_encode(d->hash, sizeof(d->hash));
627 logx("delta hash: %s", hash);
628 free(hash);
629 }
630 }
631