1 /*
2  * Copyright (c) 2020 Nils Fisher <nils_fisher@hotmail.com>
3  * Copyright (c) 2021 Claudio Jeker <claudio@openbsd.org>
4  *
5  * Permission to use, copy, modify, and distribute this software for any
6  * purpose with or without fee is hereby granted, provided that the above
7  * copyright notice and this permission notice appear in all copies.
8  *
9  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16  */
17 
18 #include <sys/stat.h>
19 
20 #include <assert.h>
21 #include <err.h>
22 #include <errno.h>
23 #include <limits.h>
24 #include <fcntl.h>
25 #include <string.h>
26 #include <unistd.h>
27 
28 #include <expat.h>
29 #include <openssl/sha.h>
30 
31 #include "extern.h"
32 #include "rrdp.h"
33 
34 enum notification_scope {
35 	NOTIFICATION_SCOPE_START,
36 	NOTIFICATION_SCOPE_NOTIFICATION,
37 	NOTIFICATION_SCOPE_SNAPSHOT,
38 	NOTIFICATION_SCOPE_NOTIFICATION_POST_SNAPSHOT,
39 	NOTIFICATION_SCOPE_DELTA,
40 	NOTIFICATION_SCOPE_END
41 };
42 
43 struct delta_item {
44 	char			*uri;
45 	char			 hash[SHA256_DIGEST_LENGTH];
46 	long long		 serial;
47 	TAILQ_ENTRY(delta_item)	 q;
48 };
49 
50 TAILQ_HEAD(delta_q, delta_item);
51 
52 struct notification_xml {
53 	XML_Parser		 parser;
54 	struct rrdp_session	*repository;
55 	struct rrdp_session	*current;
56 	char			*session_id;
57 	char			*snapshot_uri;
58 	char			 snapshot_hash[SHA256_DIGEST_LENGTH];
59 	struct delta_q		 delta_q;
60 	long long		 serial;
61 	int			 version;
62 	enum notification_scope	 scope;
63 };
64 
65 static void	free_delta(struct delta_item *);
66 
67 static int
68 add_delta(struct notification_xml *nxml, const char *uri,
69     const char hash[SHA256_DIGEST_LENGTH], long long serial)
70 {
71 	struct delta_item *d, *n;
72 
73 	if ((d = calloc(1, sizeof(struct delta_item))) == NULL)
74 		err(1, "%s - calloc", __func__);
75 
76 	d->serial = serial;
77 	d->uri = xstrdup(uri);
78 	memcpy(d->hash, hash, sizeof(d->hash));
79 
80 	/* optimise for a sorted input */
81 	n = TAILQ_LAST(&nxml->delta_q, delta_q);
82 	if (n == NULL)
83 		TAILQ_INSERT_HEAD(&nxml->delta_q, d, q);
84 	else if (n->serial < serial)
85 		TAILQ_INSERT_TAIL(&nxml->delta_q, d, q);
86 	else
87 		TAILQ_FOREACH(n, &nxml->delta_q, q) {
88 			if (n->serial == serial) {
89 				warnx("duplicate delta serial %lld ", serial);
90 				free_delta(d);
91 				return 0;
92 			}
93 			if (n->serial > serial) {
94 				TAILQ_INSERT_BEFORE(n, d, q);
95 				break;
96 			}
97 		}
98 
99 	return 1;
100 }
101 
102 static void
103 free_delta(struct delta_item *d)
104 {
105 	free(d->uri);
106 	free(d);
107 }
108 
109 static void
110 start_notification_elem(struct notification_xml *nxml, const char **attr)
111 {
112 	XML_Parser p = nxml->parser;
113 	int has_xmlns = 0;
114 	size_t i;
115 
116 	if (nxml->scope != NOTIFICATION_SCOPE_START)
117 		PARSE_FAIL(p,
118 		    "parse failed - entered notification elem unexpectedely");
119 	for (i = 0; attr[i]; i += 2) {
120 		const char *errstr;
121 		if (strcmp("xmlns", attr[i]) == 0) {
122 			has_xmlns = 1;
123 			continue;
124 		}
125 		if (strcmp("session_id", attr[i]) == 0) {
126 			nxml->session_id = xstrdup(attr[i + 1]);
127 			continue;
128 		}
129 		if (strcmp("version", attr[i]) == 0) {
130 			nxml->version = strtonum(attr[i + 1],
131 			    1, MAX_VERSION, &errstr);
132 			if (errstr == NULL)
133 				continue;
134 		}
135 		if (strcmp("serial", attr[i]) == 0) {
136 			nxml->serial = strtonum(attr[i + 1],
137 			    1, LLONG_MAX, &errstr);
138 			if (errstr == NULL)
139 				continue;
140 		}
141 		PARSE_FAIL(p, "parse failed - non conforming "
142 		    "attribute found in notification elem");
143 	}
144 	if (!(has_xmlns && nxml->version && nxml->session_id && nxml->serial))
145 		PARSE_FAIL(p, "parse failed - incomplete "
146 		    "notification attributes");
147 
148 	nxml->scope = NOTIFICATION_SCOPE_NOTIFICATION;
149 }
150 
151 static void
152 end_notification_elem(struct notification_xml *nxml)
153 {
154 	XML_Parser p = nxml->parser;
155 
156 	if (nxml->scope != NOTIFICATION_SCOPE_NOTIFICATION_POST_SNAPSHOT)
157 		PARSE_FAIL(p, "parse failed - exited notification "
158 		    "elem unexpectedely");
159 	nxml->scope = NOTIFICATION_SCOPE_END;
160 }
161 
162 static void
163 start_snapshot_elem(struct notification_xml *nxml, const char **attr)
164 {
165 	XML_Parser p = nxml->parser;
166 	int i, hasUri = 0, hasHash = 0;
167 
168 	if (nxml->scope != NOTIFICATION_SCOPE_NOTIFICATION)
169 		PARSE_FAIL(p,
170 		    "parse failed - entered snapshot elem unexpectedely");
171 	for (i = 0; attr[i]; i += 2) {
172 		if (strcmp("uri", attr[i]) == 0 && hasUri++ == 0) {
173 			if (valid_uri(attr[i + 1], strlen(attr[i + 1]),
174 			    "https://")) {
175 				nxml->snapshot_uri = xstrdup(attr[i + 1]);
176 				continue;
177 			}
178 		}
179 		if (strcmp("hash", attr[i]) == 0 && hasHash++ == 0) {
180 			if (hex_decode(attr[i + 1], nxml->snapshot_hash,
181 			    sizeof(nxml->snapshot_hash)) == 0)
182 				continue;
183 		}
184 		PARSE_FAIL(p, "parse failed - non conforming "
185 		    "attribute found in snapshot elem");
186 	}
187 	if (hasUri != 1 || hasHash != 1)
188 		PARSE_FAIL(p, "parse failed - incomplete snapshot attributes");
189 
190 	nxml->scope = NOTIFICATION_SCOPE_SNAPSHOT;
191 }
192 
193 static void
194 end_snapshot_elem(struct notification_xml *nxml)
195 {
196 	XML_Parser p = nxml->parser;
197 
198 	if (nxml->scope != NOTIFICATION_SCOPE_SNAPSHOT)
199 		PARSE_FAIL(p, "parse failed - exited snapshot "
200 		    "elem unexpectedely");
201 	nxml->scope = NOTIFICATION_SCOPE_NOTIFICATION_POST_SNAPSHOT;
202 }
203 
204 static void
205 start_delta_elem(struct notification_xml *nxml, const char **attr)
206 {
207 	XML_Parser p = nxml->parser;
208 	int i, hasUri = 0, hasHash = 0;
209 	const char *delta_uri = NULL;
210 	char delta_hash[SHA256_DIGEST_LENGTH];
211 	long long delta_serial = 0;
212 
213 	if (nxml->scope != NOTIFICATION_SCOPE_NOTIFICATION_POST_SNAPSHOT)
214 		PARSE_FAIL(p, "parse failed - entered delta "
215 		    "elem unexpectedely");
216 	for (i = 0; attr[i]; i += 2) {
217 		if (strcmp("uri", attr[i]) == 0 && hasUri++ == 0) {
218 			if (valid_uri(attr[i + 1], strlen(attr[i + 1]),
219 			    "https://")) {
220 				delta_uri = attr[i + 1];
221 				continue;
222 			}
223 		}
224 		if (strcmp("hash", attr[i]) == 0 && hasHash++ == 0) {
225 			if (hex_decode(attr[i + 1], delta_hash,
226 			    sizeof(delta_hash)) == 0)
227 				continue;
228 		}
229 		if (strcmp("serial", attr[i]) == 0 && delta_serial == 0) {
230 			const char *errstr;
231 
232 			delta_serial = strtonum(attr[i + 1],
233 			    1, LLONG_MAX, &errstr);
234 			if (errstr == NULL)
235 				continue;
236 		}
237 		PARSE_FAIL(p, "parse failed - non conforming "
238 		    "attribute found in snapshot elem");
239 	}
240 	/* Only add to the list if we are relevant */
241 	if (hasUri != 1 || hasHash != 1 || delta_serial == 0)
242 		PARSE_FAIL(p, "parse failed - incomplete delta attributes");
243 
244 	/* optimisation, add only deltas that could be interesting */
245 	if (nxml->repository->serial != 0 &&
246 	    nxml->repository->serial < delta_serial &&
247 	    nxml->repository->session_id != NULL &&
248 	    strcmp(nxml->session_id, nxml->repository->session_id) == 0) {
249 		if (add_delta(nxml, delta_uri, delta_hash, delta_serial) == 0)
250 			PARSE_FAIL(p, "parse failed - adding delta failed");
251 	}
252 
253 	nxml->scope = NOTIFICATION_SCOPE_DELTA;
254 }
255 
256 static void
257 end_delta_elem(struct notification_xml *nxml)
258 {
259 	XML_Parser p = nxml->parser;
260 
261 	if (nxml->scope != NOTIFICATION_SCOPE_DELTA)
262 		PARSE_FAIL(p, "parse failed - exited delta elem unexpectedely");
263 	nxml->scope = NOTIFICATION_SCOPE_NOTIFICATION_POST_SNAPSHOT;
264 }
265 
266 static void
267 notification_xml_elem_start(void *data, const char *el, const char **attr)
268 {
269 	struct notification_xml *nxml = data;
270 	XML_Parser p = nxml->parser;
271 
272 	/*
273 	 * Can only enter here once as we should have no ways to get back to
274 	 * START scope
275 	 */
276 	if (strcmp("notification", el) == 0)
277 		start_notification_elem(nxml, attr);
278 	/*
279 	 * Will enter here multiple times, BUT never nested. will start
280 	 * collecting character data in that handler
281 	 * mem is cleared in end block, (TODO or on parse failure)
282 	 */
283 	else if (strcmp("snapshot", el) == 0)
284 		start_snapshot_elem(nxml, attr);
285 	else if (strcmp("delta", el) == 0)
286 		start_delta_elem(nxml, attr);
287 	else
288 		PARSE_FAIL(p, "parse failed - unexpected elem exit found");
289 }
290 
291 static void
292 notification_xml_elem_end(void *data, const char *el)
293 {
294 	struct notification_xml *nxml = data;
295 	XML_Parser p = nxml->parser;
296 
297 	if (strcmp("notification", el) == 0)
298 		end_notification_elem(nxml);
299 	else if (strcmp("snapshot", el) == 0)
300 		end_snapshot_elem(nxml);
301 	else if (strcmp("delta", el) == 0)
302 		end_delta_elem(nxml);
303 	else
304 		PARSE_FAIL(p, "parse failed - unexpected elem exit found");
305 }
306 
307 struct notification_xml *
308 new_notification_xml(XML_Parser p, struct rrdp_session *repository,
309     struct rrdp_session *current)
310 {
311 	struct notification_xml *nxml;
312 
313 	if ((nxml = calloc(1, sizeof(*nxml))) == NULL)
314 		err(1, "%s", __func__);
315 	TAILQ_INIT(&(nxml->delta_q));
316 	nxml->parser = p;
317 	nxml->repository = repository;
318 	nxml->current = current;
319 
320 	XML_SetElementHandler(nxml->parser, notification_xml_elem_start,
321 	    notification_xml_elem_end);
322 	XML_SetUserData(nxml->parser, nxml);
323 
324 	return nxml;
325 }
326 
327 void
328 free_notification_xml(struct notification_xml *nxml)
329 {
330 	if (nxml == NULL)
331 		return;
332 
333 	free(nxml->session_id);
334 	free(nxml->snapshot_uri);
335 	while (!TAILQ_EMPTY(&nxml->delta_q)) {
336 		struct delta_item *d = TAILQ_FIRST(&nxml->delta_q);
337 		TAILQ_REMOVE(&nxml->delta_q, d, q);
338 		free_delta(d);
339 	}
340 	free(nxml);
341 }
342 
343 /*
344  * Finalize notification step, decide if a delta update is possible
345  * if either the session_id changed or the delta files fail to cover
346  * all the steps up to the new serial fall back to a snapshot.
347  * Return SNAPSHOT or DELTA for snapshot or delta processing.
348  * Return NOTIFICATION if repository is up to date.
349  */
350 enum rrdp_task
351 notification_done(struct notification_xml *nxml, char *last_mod)
352 {
353 	struct delta_item *d;
354 	long long s, last_s = 0;
355 
356 	nxml->current->last_mod = last_mod;
357 	nxml->current->session_id = xstrdup(nxml->session_id);
358 
359 	/* check the that the session_id was valid and still the same */
360 	if (nxml->repository->session_id == NULL ||
361 	    strcmp(nxml->session_id, nxml->repository->session_id) != 0)
362 		goto snapshot;
363 
364 	/* if repository serial is 0 fall back to snapshot */
365 	if (nxml->repository->serial == 0)
366 		goto snapshot;
367 
368 	if (nxml->repository->serial == nxml->serial) {
369 		nxml->current->serial = nxml->serial;
370 		return NOTIFICATION;
371 	}
372 
373 	/* check that all needed deltas are available */
374 	s = nxml->repository->serial + 1;
375 	TAILQ_FOREACH(d, &nxml->delta_q, q) {
376 		if (d->serial != s++)
377 			goto snapshot;
378 		last_s = d->serial;
379 	}
380 	if (last_s != nxml->serial)
381 		goto snapshot;
382 
383 	/* update via delta possible */
384 	nxml->current->serial = nxml->repository->serial;
385 	nxml->repository->serial = nxml->serial;
386 	return DELTA;
387 
388 snapshot:
389 	/* update via snapshot download */
390 	nxml->current->serial = nxml->serial;
391 	return SNAPSHOT;
392 }
393 
394 const char *
395 notification_get_next(struct notification_xml *nxml, char *hash, size_t hlen,
396     enum rrdp_task task)
397 {
398 	struct delta_item *d;
399 
400 	switch (task) {
401 	case SNAPSHOT:
402 		assert(hlen == sizeof(nxml->snapshot_hash));
403 		memcpy(hash, nxml->snapshot_hash, hlen);
404 		/*
405 		 * Ensure that the serial is correct in case a previous
406 		 * delta request failed.
407 		 */
408 		nxml->current->serial = nxml->serial;
409 		return nxml->snapshot_uri;
410 	case DELTA:
411 		/* first bump serial, then use first delta  */
412 		nxml->current->serial += 1;
413 		d = TAILQ_FIRST(&nxml->delta_q);
414 		assert(d->serial == nxml->current->serial);
415 		assert(hlen == sizeof(d->hash));
416 		memcpy(hash, d->hash, hlen);
417 		return d->uri;
418 	default:
419 		errx(1, "%s: bad task", __func__);
420 	}
421 }
422 
423 /*
424  * Pop first element from the delta queue. Return non-0 if this was the last
425  * delta to fetch.
426  */
427 int
428 notification_delta_done(struct notification_xml *nxml)
429 {
430 	struct delta_item *d;
431 
432 	d = TAILQ_FIRST(&nxml->delta_q);
433 	assert(d->serial == nxml->current->serial);
434 	TAILQ_REMOVE(&nxml->delta_q, d, q);
435 	free_delta(d);
436 
437 	assert(!TAILQ_EMPTY(&nxml->delta_q) ||
438 	    nxml->serial == nxml->current->serial);
439 	return TAILQ_EMPTY(&nxml->delta_q);
440 }
441 
442 void
443 log_notification_xml(struct notification_xml *nxml)
444 {
445 	logx("session_id: %s, serial: %lld", nxml->session_id, nxml->serial);
446 	logx("snapshot_uri: %s", nxml->snapshot_uri);
447 }
448