]> git.ipfire.org Git - thirdparty/FORT-validator.git/commitdiff
Merge branch 'main' into fort2
authorAlberto Leiva Popper <ydahhrk@gmail.com>
Wed, 24 Sep 2025 22:43:37 +0000 (16:43 -0600)
committerAlberto Leiva Popper <ydahhrk@gmail.com>
Thu, 25 Sep 2025 16:40:08 +0000 (10:40 -0600)
105 files changed:
1  2 
configure.ac
docs/usage.md
man/fort.8
src/Makefile.am
src/algorithm.c
src/alloc.c
src/alloc.h
src/asn1/asn1c/INTEGER.c
src/asn1/asn1c/OBJECT_IDENTIFIER.c
src/asn1/asn1c/OCTET_STRING.c
src/asn1/asn1c/RsyncRequest.c
src/asn1/asn1c/RsyncRequest.h
src/asn1/asn1c/constr_SEQUENCE.c
src/asn1/asn1c/constr_SET_OF.c
src/asn1/oid.c
src/asn1/signed_data.c
src/base64.c
src/base64.h
src/cache.c
src/cache.h
src/cachetmp.c
src/common.c
src/common.h
src/config.c
src/config.h
src/config/string_array.c
src/config/time.c
src/config/uint.c
src/ext.c
src/file.c
src/file.h
src/hash.c
src/hash.h
src/http.c
src/http.h
src/init.c
src/json_handler.c
src/json_util.c
src/json_util.h
src/libcrypto_util.c
src/log.c
src/main.c
src/object/bgpsec.c
src/object/certificate.c
src/object/certificate.h
src/object/crl.c
src/object/crl.h
src/object/ghostbusters.c
src/object/ghostbusters.h
src/object/manifest.c
src/object/manifest.h
src/object/roa.c
src/object/roa.h
src/object/signed_object.c
src/object/signed_object.h
src/object/tal.c
src/object/tal.h
src/object/vcard.c
src/print_file.c
src/prometheus.c
src/resource.c
src/resource/asn.c
src/resource/asn.h
src/resource/ip4.c
src/resource/ip6.c
src/rrdp.c
src/rrdp.h
src/rsync.c
src/rtr/db/db_table.c
src/rtr/db/delta.c
src/rtr/db/vrps.c
src/rtr/db/vrps.h
src/rtr/err_pdu.c
src/rtr/pdu.h
src/rtr/pdu_sender.c
src/rtr/rtr.c
src/sig.c
src/slurm/db_slurm.c
src/slurm/db_slurm.h
src/slurm/slurm_loader.c
src/slurm/slurm_parser.c
src/stats.c
src/stream.c
src/task.c
src/task.h
src/thread_pool.c
src/thread_var.c
src/types/address.c
src/types/array.h
src/types/arraylist.h
src/types/map.c
src/types/name.c
src/types/path.c
src/types/rpp.c
src/types/rpp.h
src/types/sorted_array.c
src/types/sorted_array.h
src/types/str.c
src/types/str.h
src/types/uri.c
src/types/uri.h
src/types/uthash.h
src/validation_handler.c
src/validation_handler.h
test/mock.c

diff --cc configure.ac
Simple merge
diff --cc docs/usage.md
index 873b19847bf229517a1ba75a6f180deb65213eb6,842234720408214a8438e97e112c63dfe0e8e3a1..4e82e85362d301e31ac1bdc47dd871e3228f9ca9
@@@ -96,8 -102,11 +98,9 @@@ description: Guide to use arguments of 
        [--server.interval.retry=<unsigned integer>]
        [--server.interval.expire=<unsigned integer>]
        [--server.deltas.lifetime=<unsigned integer>]
+       [--prometheus.port=<unsigned integer>]
        [--rsync.enabled=true|false]
        [--rsync.priority=<unsigned integer>]
 -      [--rsync.retry.count=<unsigned integer>]
 -      [--rsync.retry.interval=<unsigned integer>]
        [--rsync.transfer-timeout=<unsigned integer>]
        [--http.enabled=true|false]
        [--http.priority=<unsigned integer>]
diff --cc man/fort.8
index 2bea78bb553926b2f551ffb8abb3b14eae87720f,40b5af829c93a7bf0d41018f15de44bd7a7c968c..931f516223b2bbd8c323c0471ed204f4b7ce5a12
@@@ -830,19 -864,15 +840,28 @@@ By default, the path has a NULL value
  .RE
  .P
  
+ .B \-\-http.proxy=\fIURL\fR
+ .RS 4
+ .P
+ Set a proxy to use for HTTP transfers.
+ .P
+ It can be a hostname, a dotted numerical IPv4 address or a numerical IPv6 address enclosed in brackets. The port defaults to 1080; append \fI:<port>\fR to override.
+ .RE
+ .P
 +.B \-\-rrdp.delta-threshold=\fIUNSIGNED_INTEGER\fR
 +.RS 4
 +Maximum deltas to explode per RRDP session, per iteration.
 +.P
 +(If the RRDP notification lists more than this amount of unprocessed deltas,
 +Fort will reset the session, exploding the snapshot instead.)
 +.P
 +Per draft-spaghetti-sidrops-rrdp-desynchronization's recommendation, this is
 +also the maximum number of delta hashes Fort will remember per RRDP session, to
 +detect session desynchronization.
 +.RE
 +.P
 +
  .B \-\-rsync.enabled=\fItrue\fR|\fIfalse\fR
  .RS 4
  Enables RSYNC requests.
diff --cc src/Makefile.am
index 12b13a76d2b583745c30fbf8ceb0d28504c39611,215e29ca90e0bd0cb5eda78889324e92d336284b..f1e79c40c8c829117604a67010860ee83a5d93c4
@@@ -8,17 -40,24 +8,18 @@@ fort_SOURCES += asn1/content_info.h asn
  fort_SOURCES += asn1/decode.h asn1/decode.c
  fort_SOURCES += asn1/oid.h asn1/oid.c
  fort_SOURCES += asn1/signed_data.h asn1/signed_data.c
 -
 -fort_SOURCES += types/address.h types/address.c
 -fort_SOURCES += types/bio_seq.c types/bio_seq.h
 -fort_SOURCES += types/delta.c types/delta.h
 -fort_SOURCES += types/router_key.c types/router_key.h
 -fort_SOURCES += types/serial.h types/serial.c
 -fort_SOURCES += types/uri.h types/uri.c
 -fort_SOURCES += types/vrp.c types/vrp.h
 -
 -fort_SOURCES += cache/local_cache.c cache/local_cache.h
 -
 +fort_SOURCES += base64.h base64.c
 +fort_SOURCES += cache.c cache.h
 +fort_SOURCES += cachetmp.c cachetmp.h
 +fort_SOURCES += common.c common.h
  fort_SOURCES += config/boolean.c config/boolean.h
 -fort_SOURCES += config/file_type.h config/file_type.c
+ fort_SOURCES += config/curl_offset.c config/curl_offset.h
  fort_SOURCES += config/filename_format.h config/filename_format.c
 +fort_SOURCES += config/file_type.h config/file_type.c
 +fort_SOURCES += config.h config.c
 +fort_SOURCES += config/incidences.h config/incidences.c
  fort_SOURCES += config/log_conf.h config/log_conf.c
  fort_SOURCES += config/mode.c config/mode.h
 -fort_SOURCES += config/incidences.h config/incidences.c
  fort_SOURCES += config/output_format.h config/output_format.c
  fort_SOURCES += config/str.c config/str.h
  fort_SOURCES += config/string_array.h config/string_array.c
@@@ -46,51 -87,33 +47,53 @@@ fort_SOURCES += object/roa.h object/roa
  fort_SOURCES += object/signed_object.h object/signed_object.c
  fort_SOURCES += object/tal.h object/tal.c
  fort_SOURCES += object/vcard.h object/vcard.c
 -
 +fort_SOURCES += output_printer.h output_printer.c
 +fort_SOURCES += print_file.h print_file.c
++fort_SOURCES += prometheus.h prometheus.c
 +fort_SOURCES += relax_ng.c relax_ng.h
 +fort_SOURCES += resource/asn.h resource/asn.c
 +fort_SOURCES += resource.h resource.c
  fort_SOURCES += resource/ip4.h resource/ip4.c
  fort_SOURCES += resource/ip6.h resource/ip6.c
 -fort_SOURCES += resource/asn.h resource/asn.c
 -
 -fort_SOURCES += rsync/rsync.h rsync/rsync.c
 -
 -fort_SOURCES += rtr/pdu_stream.c rtr/pdu_stream.h
 +fort_SOURCES += rrdp.h rrdp.c
 +fort_SOURCES += rsync.h rsync.c
 +fort_SOURCES += rtr/db/db_table.c rtr/db/db_table.h
 +fort_SOURCES += rtr/db/delta.c rtr/db/delta.h
 +fort_SOURCES += rtr/db/deltas_array.c rtr/db/deltas_array.h
 +fort_SOURCES += rtr/db/vrps.c rtr/db/vrps.h
  fort_SOURCES += rtr/err_pdu.c rtr/err_pdu.h
 +fort_SOURCES += rtr/pdu.c rtr/pdu.h
  fort_SOURCES += rtr/pdu_handler.c rtr/pdu_handler.h
  fort_SOURCES += rtr/pdu_sender.c rtr/pdu_sender.h
 -fort_SOURCES += rtr/pdu.c rtr/pdu.h
 +fort_SOURCES += rtr/pdu_stream.c rtr/pdu_stream.h
  fort_SOURCES += rtr/primitive_writer.c rtr/primitive_writer.h
  fort_SOURCES += rtr/rtr.c rtr/rtr.h
 -
 -fort_SOURCES += rtr/db/db_table.c rtr/db/db_table.h
 -fort_SOURCES += rtr/db/delta.c rtr/db/delta.h
 -fort_SOURCES += rtr/db/deltas_array.c rtr/db/deltas_array.h
 -fort_SOURCES += rtr/db/vrps.c rtr/db/vrps.h
 -
 +fort_SOURCES += sig.h sig.c
  fort_SOURCES += slurm/db_slurm.c slurm/db_slurm.h
  fort_SOURCES += slurm/slurm_loader.c slurm/slurm_loader.h
  fort_SOURCES += slurm/slurm_parser.c slurm/slurm_parser.h
 -
 -fort_SOURCES += thread/thread_pool.c thread/thread_pool.h
 -
 -fort_SOURCES += xml/relax_ng.c xml/relax_ng.h
++fort_SOURCES += stats.h stats.c
 +fort_SOURCES += stream.h stream.c
 +fort_SOURCES += task.h task.c
 +fort_SOURCES += thread_pool.c thread_pool.h
 +fort_SOURCES += thread_var.h thread_var.c
 +fort_SOURCES += types/address.h types/address.c
 +fort_SOURCES += types/arraylist.h
 +fort_SOURCES += types/asn.h
 +fort_SOURCES += types/bio_seq.c types/bio_seq.h
 +fort_SOURCES += types/delta.c types/delta.h
 +fort_SOURCES += types/map.h types/map.c
 +fort_SOURCES += types/name.h types/name.c
 +fort_SOURCES += types/path.h types/path.c
 +fort_SOURCES += types/router_key.c types/router_key.h
 +fort_SOURCES += types/rpp.h types/rpp.c
 +fort_SOURCES += types/serial.h types/serial.c
 +fort_SOURCES += types/sorted_array.h types/sorted_array.c
 +fort_SOURCES += types/str.h types/str.c
 +fort_SOURCES += types/uri.c types/uri.h
 +fort_SOURCES += types/uthash.h
 +fort_SOURCES += types/vrp.c types/vrp.h
 +fort_SOURCES += validation_handler.h validation_handler.c
  
  include asn1/asn1c/Makefile.include
  fort_SOURCES += $(ASN_MODULE_SRCS) $(ASN_MODULE_HDRS)
@@@ -101,11 -124,13 +104,11 @@@ fort_CFLAGS += -std=c99 -D_XOPEN_SOURCE
  fort_CFLAGS += -O2 -g $(FORT_FLAGS) ${XML2_CFLAGS}
  if BACKTRACE_ENABLED
  fort_CFLAGS += -DBACKTRACE_ENABLED
 +fort_LDFLAGS = -rdynamic
  endif
- fort_LDADD   = ${JANSSON_LIBS} ${CURL_LIBS} ${XML2_LIBS}
 -fort_LDFLAGS = $(LDFLAGS_DEBUG)
+ fort_LDADD   = ${JANSSON_LIBS} ${CURL_LIBS} ${XML2_LIBS} ${MICROHTTPD_LIBS}
  
 -# I'm tired of scrolling up, but feel free to comment this out.
 -GCC_WARNS  = -fmax-errors=1
 -
 +GCC_WARNS  = -fmax-errors=1 -fanalyzer
  GCC_WARNS += -pedantic-errors -Waddress -Walloc-zero -Walloca
  GCC_WARNS += -Wno-aggressive-loop-optimizations -Warray-bounds=2 -Wbool-compare
  GCC_WARNS += -Wbool-operation -Wno-builtin-declaration-mismatch -Wcast-align
@@@ -145,11 -180,18 +148,11 @@@ GCC_WARNS += -Wunused-value -Wunused-va
  GCC_WARNS += -Wunused-but-set-parameter -Wunused-but-set-variable
  GCC_WARNS += -Wvariadic-macros -Wvector-operation-performance -Wvla
  GCC_WARNS += -Wvolatile-register-var -Wwrite-strings
 -
 -# "Issue a warning when HSAIL cannot be emitted for the compiled function or
 -# OpenMP construct."
 -# Uh-huh.
  GCC_WARNS += -Whsa
 -
 -# I don't mind too much increasing these.
 -# Just make sure that you know what you're doing.
 +# Relatively arbitrary
  GCC_WARNS += -Wlarger-than=2048 -Walloc-size-larger-than=4096
  GCC_WARNS += -Wframe-larger-than=1024 -Wstack-usage=1024
 -
  # Can't use because of dependencies: -Waggregate-return
 -# Want to add, but needs work: -Wconversion, -Wsign-compare, -Wsign-conversion
 +# Want to add, but need work: -Wconversion, -Wsign-compare, -Wsign-conversion
  # Seem to require other compiler features: -Wchkp, -Wstack-protector,
--#     -Wstrict-aliasing
++#     -Wstrict-aliasing
diff --cc src/algorithm.c
Simple merge
diff --cc src/alloc.c
Simple merge
diff --cc src/alloc.h
Simple merge
index 7e9539750fde68bd3cc8941a432104eae9880014,843919130f64f76c3560cef7d2c828bf724b65e5..01e5202ec5f5616f4393286c1aaf4dded663d930
@@@ -7,11 -7,9 +7,9 @@@
  
  #include <assert.h>
  #include <errno.h>
 -#include <string.h>
  
- #include "alloc.h"
- #include "common.h"
- #include "asn1/asn1c/asn_codecs_prim.h"
  #include "asn1/asn1c/asn_internal.h"
++#include "common.h"
  #include "json_util.h"
  
  /*
index 53acaaeaeb27cb0986c167d4ab2404cb139b9fcc,c522f05784db896491cc21a6d9f364c28c5845e4..dd64b2009dc8ffedd2406f6c16d5dff03cb609a4
@@@ -10,8 -10,9 +10,7 @@@
  #include <openssl/asn1.h>
  #include <openssl/obj_mac.h>
  #include <openssl/objects.h>
 -#include <string.h>
  
--#include "asn1/asn1c/INTEGER.h"
  #include "asn1/asn1c/asn_internal.h"
  #include "json_util.h"
  
index 8ed51f4115ee5e7ea60f48ada4d8b4bd7ca327a1,c1efb0269a51e90f4155896cf1147ced769552fb..1abaa5ffcb2ce9ba43d3df646431b20064508826
@@@ -7,8 -7,9 +7,7 @@@
  
  #include <assert.h>
  #include <errno.h>
 -#include <string.h>
  
--#include "alloc.h"
  #include "asn1/asn1c/BIT_STRING.h"
  #include "asn1/asn1c/asn_internal.h"
  #include "asn1/asn1c/ber_decoder.h"
index d527079cca7d9f44b61c771a6a4ea046299d531d,0000000000000000000000000000000000000000..bb376ca6e8723c28975310cadfc913060341420a
mode 100644,000000..100644
--- /dev/null
@@@ -1,70 -1,0 +1,72 @@@
 +/*
 + * Generated by asn1c-0.9.29 (http://lionet.info/asn1c)
 + * From ASN.1 module "RsyncSpawnerChannel"
 + *    found in "rsync.asn1"
 + *    `asn1c -Werror -fcompound-names -fwide-types -D asn1/asn1c -no-gen-PER -no-gen-example`
 + */
 +
 +#include "asn1/asn1c/RsyncRequest.h"
 +
++#include "asn1/asn1c/constr_SEQUENCE.h"
++
 +static asn_TYPE_member_t asn_MBR_RsyncRequest_1[] = {
 +      { ATF_NOFLAGS, 0, offsetof(struct RsyncRequest, url),
 +              (ASN_TAG_CLASS_UNIVERSAL | (4 << 2)),
 +              0,
 +              &asn_DEF_OCTET_STRING,
 +              0,
 +              { 0, 0, 0 },
 +              0, 0, /* No default value */
 +              "url"
 +              },
 +      { ATF_NOFLAGS, 0, offsetof(struct RsyncRequest, path),
 +              (ASN_TAG_CLASS_UNIVERSAL | (4 << 2)),
 +              0,
 +              &asn_DEF_OCTET_STRING,
 +              0,
 +              { 0, 0, 0 },
 +              0, 0, /* No default value */
 +              "path"
 +              },
 +};
 +static const ber_tlv_tag_t asn_DEF_RsyncRequest_tags_1[] = {
 +      (ASN_TAG_CLASS_UNIVERSAL | (16 << 2))
 +};
 +static const asn_TYPE_tag2member_t asn_MAP_RsyncRequest_tag2el_1[] = {
 +    { (ASN_TAG_CLASS_UNIVERSAL | (4 << 2)), 0, 0, 1 }, /* url */
 +    { (ASN_TAG_CLASS_UNIVERSAL | (4 << 2)), 1, -1, 0 } /* path */
 +};
 +static asn_SEQUENCE_specifics_t asn_SPC_RsyncRequest_specs_1 = {
 +      sizeof(struct RsyncRequest),
 +      offsetof(struct RsyncRequest, _asn_ctx),
 +      asn_MAP_RsyncRequest_tag2el_1,
 +      2,      /* Count of tags in the map */
 +      -1,     /* First extension addition */
 +};
 +asn_TYPE_descriptor_t asn_DEF_RsyncRequest = {
 +      "RsyncRequest",
 +      "RsyncRequest",
 +      &asn_OP_SEQUENCE,
 +      asn_DEF_RsyncRequest_tags_1,
 +      sizeof(asn_DEF_RsyncRequest_tags_1)
 +              /sizeof(asn_DEF_RsyncRequest_tags_1[0]), /* 1 */
 +      asn_DEF_RsyncRequest_tags_1,    /* Same as above */
 +      sizeof(asn_DEF_RsyncRequest_tags_1)
 +              /sizeof(asn_DEF_RsyncRequest_tags_1[0]), /* 1 */
 +      { 0, 0, SEQUENCE_constraint },
 +      asn_MBR_RsyncRequest_1,
 +      2,      /* Elements count */
 +      &asn_SPC_RsyncRequest_specs_1   /* Additional specs */
 +};
 +
 +int
 +RsyncRequest_init(struct RsyncRequest *req, struct uri const *url,
 +    char const *path)
 +{
 +      memset(req, 0, sizeof(*req));
 +      if (OCTET_STRING_fromBuf(&req->url, uri_str(url), uri_len(url)) < 0)
 +              return -1;
 +      if (OCTET_STRING_fromString(&req->path, path) < 0)
 +              return -1;
 +      return 0;
 +}
index db9398dd0d566255e572674d35c9964a603a9712,0000000000000000000000000000000000000000..7fb1d080dc94cc717c1bf42ff78b62d133018e9d
mode 100644,000000..100644
--- /dev/null
@@@ -1,39 -1,0 +1,29 @@@
- #include "types/uri.h"
 +/*
 + * Generated by asn1c-0.9.29 (http://lionet.info/asn1c)
 + * From ASN.1 module "RsyncSpawnerChannel"
 + *    found in "rsync.asn1"
 + *    `asn1c -Werror -fcompound-names -fwide-types -D asn1/asn1c -no-gen-PER -no-gen-example`
 + */
 +
 +#ifndef       _RsyncRequest_H_
 +#define       _RsyncRequest_H_
 +
 +/* Including external dependencies */
- #include "asn1/asn1c/constr_SEQUENCE.h"
- #ifdef __cplusplus
- extern "C" {
- #endif
 +#include "asn1/asn1c/OCTET_STRING.h"
- #ifdef __cplusplus
- }
- #endif
++#include "types/uri.h"
 +
 +/* RsyncRequest */
 +typedef struct RsyncRequest {
 +      OCTET_STRING_t   url;
 +      OCTET_STRING_t   path;
 +      
 +      /* Context for parsing across buffer boundaries */
 +      asn_struct_ctx_t _asn_ctx;
 +} RsyncRequest_t;
 +
 +/* Implementation */
 +extern asn_TYPE_descriptor_t asn_DEF_RsyncRequest;
 +
 +int RsyncRequest_init(struct RsyncRequest *, struct uri const *, char const *);
 +
- #include "asn1/asn1c/asn_internal.h"
 +#endif        /* _RsyncRequest_H_ */
index 5702b2383616b3b258b9cc9e780e112fc5bfa169,dbf112bada71b6b93407d2a37549ef872cd0a11e..d1e7465ca47608ea7de304eec862c52bcbe3eec8
@@@ -11,7 -12,7 +11,6 @@@
  #include "asn1/asn1c/OPEN_TYPE.h"
  #include "asn1/asn1c/asn_internal.h"
  #include "asn1/asn1c/ber_decoder.h"
--#include "asn1/asn1c/constraints.h"
  #include "asn1/asn1c/der_encoder.h"
  #include "asn1/asn1c/xer_encoder.h"
  #include "json_util.h"
index d58a731aa4cdc6d6c67c68f88f53b7694db50e65,aef8f45f81bef62010f3c29c13cfce29ab7ed984..307aab22dbc2e1734391d84a236a121e1bc13f46
@@@ -11,7 -12,7 +11,6 @@@
  #include "asn1/asn1c/asn_SET_OF.h"
  #include "asn1/asn1c/asn_internal.h"
  #include "asn1/asn1c/ber_decoder.h"
--#include "asn1/asn1c/constraints.h"
  #include "asn1/asn1c/der_encoder.h"
  #include "asn1/asn1c/xer_encoder.h"
  #include "json_util.h"
diff --cc src/asn1/oid.c
Simple merge
index 960f2023ecd3ecd4db738bb26155f3ad2264d446,40f7e6f401f3a261f4a2c85a6d9a6428e1cf8e77..a8085ca560db3b2920a4a62abf0c2ab7dd2f046b
@@@ -7,11 -6,10 +6,10 @@@
  #include "asn1/asn1c/SignedDataPKCS7.h"
  #include "asn1/decode.h"
  #include "asn1/oid.h"
 -#include "crypto/hash.h"
 +#include "hash.h"
  #include "log.h"
  #include "object/certificate.h"
 -#include "object/name.h"
 +#include "object/signed_object.h"
- #include "types/name.h"
  
  static const OID oid_cta = OID_CONTENT_TYPE_ATTR;
  static const OID oid_mda = OID_MESSAGE_DIGEST_ATTR;
diff --cc src/base64.c
index 560a1a5097c1e2f35c54ba8cd7f541abf4600081,0000000000000000000000000000000000000000..2d6e17e9c5fa860886bb8c3ca516f37ea6d8ae67
mode 100644,000000..100644
--- /dev/null
@@@ -1,176 -1,0 +1,178 @@@
 +#include "base64.h"
 +
 +#include <openssl/bio.h>
 +#include <openssl/buffer.h>
 +#include <openssl/err.h>
 +#include <openssl/evp.h>
++#include <stdlib.h>
++#include <string.h>
 +
 +#include "alloc.h"
 +#include "log.h"
 +
 +/* Simple decode base64 string. Returns true on success, false on failure. */
 +bool
 +base64_decode(char *in, size_t in_len, unsigned char **out, size_t *out_len)
 +{
 +      unsigned char *result;
 +      EVP_ENCODE_CTX *ctx;
 +      int outl;
 +      int status;
 +
 +      if (in_len == 0)
 +              in_len = strlen(in);
 +
 +      /*
 +       * Will usually allocate more because of the newlines,
 +       * but I'm at peace with it.
 +       */
 +      result = pmalloc(EVP_DECODE_LENGTH(in_len));
 +
 +      ctx = EVP_ENCODE_CTX_new();
 +      if (ctx == NULL)
 +              enomem_panic();
 +
 +      EVP_DecodeInit(ctx);
 +
 +      status = EVP_DecodeUpdate(ctx, result, &outl, (unsigned char *)in, in_len);
 +      if (status == -1)
 +              goto cancel;
 +
 +      *out_len = outl;
 +
 +      status = EVP_DecodeFinal(ctx, result + outl, &outl);
 +      if (status != 1)
 +              goto cancel;
 +
 +      EVP_ENCODE_CTX_free(ctx);
 +      *out = result;
 +      *out_len += outl;
 +      return true;
 +
 +cancel:       EVP_ENCODE_CTX_free(ctx);
 +      return false;
 +}
 +
 +/*
 + * Decode a base64 encoded string (@str_encoded), the decoded value is
 + * allocated at @result with a length of @result_len.
 + *
 + * Return 0 on success, or the error code if something went wrong. Don't forget
 + * to free @result after a successful decoding.
 + */
 +bool
 +base64url_decode(char const *str_encoded, unsigned char **result,
 +    size_t *result_len)
 +{
 +      char *str_copy;
 +      size_t encoded_len;
 +      size_t pad;
 +      size_t i;
 +      bool success;
 +
 +      /*
 +       * Apparently there isn't a base64url decoder, and there isn't
 +       * much difference between base64 codification and base64url, just as
 +       * stated in RFC 4648 section 5: "This encoding is technically
 +       * identical to the previous one, except for the 62:nd and 63:rd
 +       * alphabet character, as indicated in Table 2".
 +       *
 +       * The existing base64 can be used if the 62:nd and 63:rd base64url
 +       * alphabet chars are replaced with the corresponding base64 chars, and
 +       * also if we add the optional padding that the member should have.
 +       */
 +      encoded_len = strlen(str_encoded);
 +      pad = (encoded_len % 4) > 0 ? 4 - (encoded_len % 4) : 0;
 +
 +      str_copy = pmalloc(encoded_len + pad + 1);
 +      /* Set all with pad char, then replace with the original string */
 +      memset(str_copy, '=', encoded_len + pad);
 +      memcpy(str_copy, str_encoded, encoded_len);
 +      str_copy[encoded_len + pad] = '\0';
 +
 +      for (i = 0; i < encoded_len; i++) {
 +              if (str_copy[i] == '-')
 +                      str_copy[i] = '+';
 +              else if (str_copy[i] == '_')
 +                      str_copy[i] = '/';
 +      }
 +
 +      /* Now decode as regular base64 */
 +      success = base64_decode(str_copy, encoded_len + pad, result, result_len);
 +
 +      free(str_copy);
 +      return success;
 +}
 +
 +static char *
 +to_base64url(char const *base, size_t base_len)
 +{
 +      char const *pad;
 +      char *tmp;
 +      size_t len;
 +      int i;
 +
 +      /* Remove padding, if present */
 +      len = base_len;
 +      do {
 +              pad = strchr(base, '=');
 +              if (pad == NULL)
 +                      break;
 +              len = pad - base;
 +      } while(0);
 +
 +      tmp = pmalloc(len + 1);
 +      memcpy(tmp, base, len);
 +      tmp[len] = '\0';
 +
 +      for (i = 0; i < len; i++) {
 +              if (tmp[i] == '+')
 +                      tmp[i] = '-';
 +              else if (tmp[i] == '/')
 +                      tmp[i] = '_';
 +      }
 +
 +      return tmp;
 +}
 +
 +/*
 + * Encode @in (with size @in_len) as base64url without trailing pad, and
 + * allocate at @result.
 + *
 + * TODO (SLURM, RK) From the way this function keeps being called in pairs and
 + * failing too late, it would appear the code should be caching the encoded
 + * result during construction.
 + */
 +bool
 +base64url_encode(unsigned char const *in, int in_len, char **result)
 +{
 +      BIO *b64, *mem;
 +      BUF_MEM *mem_buf;
 +
 +      ERR_clear_error();
 +
 +      mem = BIO_new(BIO_s_mem());
 +      if (mem == NULL)
 +              return false;
 +
 +      b64 = BIO_new(BIO_f_base64());
 +      if (b64 == NULL) {
 +              BIO_free(mem);
 +              return false;
 +      }
 +
 +      /*
 +       * TODO (SLURM, RK) WHY IS THERE NO ERROR HANDLING HERE
 +       * ARGGGGGGGGGGGGGGGGGGGGHHHHHHHHHHHHHHHHHHHHH
 +       */
 +      mem = BIO_push(b64, mem);
 +      BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
 +      BIO_write(b64, in, in_len);
 +      BIO_flush(b64);
 +      BIO_get_mem_ptr(mem, &mem_buf);
 +
 +      *result = to_base64url(mem_buf->data, mem_buf->length);
 +
 +      BIO_free_all(b64);
 +      return true;
 +}
diff --cc src/base64.h
index 5dbae8f875da25ddffa7c72b188e5f676a331314,d7a71d4c07ba076da9d2f175d0bbcf7b6c052d34..67c1d45974f0ed9e5425939c5fda0cd0a730a80e
@@@ -1,18 -1,13 +1,12 @@@
  #ifndef SRC_BASE64_H_
  #define SRC_BASE64_H_
  
 -#include <openssl/bio.h>
  #include <stdbool.h>
  #include <stddef.h>
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <strings.h>
- #include <sys/types.h>
- #include <unistd.h>
  
 -bool base64_decode(BIO *, unsigned char *, bool, size_t, size_t *);
 -bool base64url_decode(char const *, unsigned char **, size_t *);
 +bool base64_decode(char *, size_t, unsigned char **, size_t *);
  
 +bool base64url_decode(char const *, unsigned char **, size_t *);
  bool base64url_encode(unsigned char const *, int, char **);
  
  #endif /* SRC_BASE64_H_ */
diff --cc src/cache.c
index a22bfa0fdb327e84801c146ffea3653115d462c6,0000000000000000000000000000000000000000..3696db8c4e0fb92ec226e8033be4917d1ce6cbd7
mode 100644,000000..100644
--- /dev/null
@@@ -1,1553 -1,0 +1,1543 @@@
- #include <ftw.h>
 +#include "cache.h"
 +
 +#include <fcntl.h>
- #include <stdbool.h>
- #include <sys/queue.h>
- #include <sys/stat.h>
- #include <sys/types.h>
- #include <unistd.h>
 +#include <signal.h>
- #include "alloc.h"
 +
- #include "common.h"
 +#include "cachetmp.h"
- #include "types/array.h"
 +#include "config.h"
 +#include "configure_ac.h"
 +#include "file.h"
 +#include "http.h"
 +#include "json_util.h"
 +#include "log.h"
 +#include "rrdp.h"
 +#include "rsync.h"
 +#include "task.h"
- #include "types/uri.h"
 +#include "types/path.h"
 +#include "types/str.h"
 +#include "types/uthash.h"
 +
 +enum node_state {
 +      /* Refresh nodes: Not downloaded yet (stale) */
 +      /* Fallback nodes: Queued for commit */
 +      DLS_OUTDATED = 0,
 +      /* Refresh nodes: Download in progress */
 +      /* Fallback nodes: N/A */
 +      DLS_ONGOING,
 +      /* Refresh nodes: Download complete */
 +      /* Fallback nodes: Committed */
 +      DLS_FRESH,
 +};
 +
 +struct node_key {
 +      /*
 +       * Hash table indexer.
 +       *
 +       * If this is an rsync node, @id is the caRepository.
 +       * If this is an HTTP node, @id is the simple URL.
 +       * If this is an RRDP refresh node, @id is the rpkiNotify.
 +       * If this is an RRDP fallback node, @id is `rpkiNotify\0caRepository`.
 +       */
 +      char *id;
 +      size_t idlen;
 +
 +      /*
 +       * If node is rsync, @http is NULL.
 +       * If node is HTTP, @http is the simple URL.
 +       * If node is RRDP, @http is the rpkiNotify.
 +       *
 +       * Points to @id; do not clean.
 +       */
 +      struct uri http;
 +      /*
 +       * If node is rsync, @rsync is the simple URL.
 +       * If node is HTTP, @rsync is NULL.
 +       * If node is RRDP, @rsync is the caRepository.
 +       *
 +       * Points to @id; do not clean.
 +       */
 +      struct uri rsync;
 +};
 +
 +/*
 + * This is a delicate structure; pay attention.
 + *
 + * During the multithreaded stage of the validation cycle, one thread will
 + * switch @state from DLS_OUTDATED to DLS_ONGOING, and become the only writer
 + * for the given node. Other threads are only allowed to lock, and with the
 + * lock, read @state (to find out they shouldn't touch anything else).
 + *
 + * The entire cache_node (except @hh) becomes (effectively) constant when the
 + * writer thread upgrades @state to DLS_FRESH.
 + *
 + * This is intended to allow the cache (ie. this module) to pass the node to the
 + * validation code (through cache_cage) without having to allocate a deep copy
 + * (@rrdp can be somewhat large), and to allow the validation code to read-only
 + * the node (except @hh) without having to hold the table mutex.
 + *
 + * C cannot entirely ensure the node remains constant after it's handed outside;
 + * this must be done through careful coding and review.
 + */
 +struct cache_node {
 +      struct node_key key;
 +      char *path;
 +
 +      enum node_state state;
 +      /* Result code of recent dl attempt (DLS_FRESH only) */
 +      validation_verdict verdict;
 +      time_t attempt_ts;      /* Refresh: Dl attempt. Fallback: Commit */
 +      time_t success_ts;      /* Refresh: Dl success. Fallback: Commit */
 +
 +      struct mft_meta mft;    /* RPP fallbacks only */
 +      struct rrdp_state *rrdp;
 +
 +      UT_hash_handle hh;      /* Hash table hook */
 +};
 +
 +typedef validation_verdict (*dl_cb)(struct cache_node *rpp);
 +
 +/*
 + * When concurrency is at play, you need @lock to access @nodes and @seq.
 + * @name, @enabled and @download stay constant through the validation.
 + *
 + * @lock also protects the nodes' @state and @hh, which have additional rules.
 + * (See cache_node.)
 + */
 +struct cache_table {
 +      char *name;
 +      bool enabled;
 +      struct cache_sequence seq;
 +      struct cache_node *nodes;       /* Hash Table */
 +      dl_cb download;
 +      pthread_mutex_t lock;
 +};
 +
 +static struct rpki_cache {
 +      /* Latest view of the remote rsync modules */
 +      /* rsync modules (repositories); indexed by plain rsync URL */
 +      struct cache_table rsync;
 +      /* Latest view of the remote HTTPS TAs */
 +      /* HTTPS files; indexed by plain HTTPS URL */
 +      struct cache_table https;
 +      /* Latest view of the remote RRDP cages */
 +      /* RRDP modules (repositories); indexed by rpkiNotify */
 +      struct cache_table rrdp;
 +
 +      /* Committed (offline fallback hard links) RPPs and TAs */
 +      /* RPPs indexed by [rpkiNotif] + caRepo; TAs indexed by plain URL. */
 +      struct cache_table fallback;
 +} cache;
 +
 +/* "Is the lockfile ours?" */
 +static volatile sig_atomic_t lockfile_owned;
 +
 +struct cache_cage {
 +      struct cache_node const *refresh;
 +      struct cache_node const *fallback;
 +      struct uri rpkiNotify;
 +      struct mft_meta *mft;           /* Fallback */
 +};
 +
 +struct cache_commit {
 +      struct uri rpkiNotify;
 +      struct uri caRepository;
 +      struct cache_mapping *files;
 +      size_t nfiles;
 +      struct mft_meta mft;            /* RPPs commits only */
 +      STAILQ_ENTRY(cache_commit) lh;
 +};
 +
 +static STAILQ_HEAD(cache_commits, cache_commit) commits = STAILQ_HEAD_INITIALIZER(commits);
 +static pthread_mutex_t commits_lock = PTHREAD_MUTEX_INITIALIZER;
 +
 +#define LOCKFILE ".lock"
 +#define METAFILE "meta.json"
 +#define TAGNAME_VERSION "fort-version"
 +
 +#ifdef UNIT_TESTING
 +static void __delete_node_cb(struct cache_node const *);
 +#endif
 +
 +static void
 +delete_node(struct cache_table *tbl, struct cache_node *node, void *arg)
 +{
 +#ifdef UNIT_TESTING
 +      __delete_node_cb(node);
 +#endif
 +
 +      if (tbl)
 +              HASH_DEL(tbl->nodes, node);
 +
 +      free(node->key.id);
 +      free(node->path);
 +      rrdp_state_free(node->rrdp);
 +      free(node);
 +}
 +
 +static void
 +foreach_node(void (*cb)(struct cache_table *, struct cache_node *, void *),
 +    void *arg)
 +{
 +      struct cache_node *node, *tmp;
 +
 +      HASH_ITER(hh, cache.rsync.nodes, node, tmp)
 +              cb(&cache.rsync, node, arg);
 +      HASH_ITER(hh, cache.https.nodes, node, tmp)
 +              cb(&cache.https, node, arg);
 +      HASH_ITER(hh, cache.rrdp.nodes, node, tmp)
 +              cb(&cache.rrdp, node, arg);
 +      HASH_ITER(hh, cache.fallback.nodes, node, tmp)
 +              cb(&cache.fallback, node, arg);
 +}
 +
 +static void
 +flush_nodes(void)
 +{
 +      foreach_node(delete_node, NULL);
 +}
 +
 +/*
 + * - Result must not be cleant.
 + * - strlen(uri_str(module)) should not be trusted.
 + */
 +static bool
 +get_rsync_module(struct uri const *url, struct uri *module)
 +{
 +      char const *str;
 +      array_index u;
 +      unsigned int slashes;
 +
 +      str = uri_str(url);
 +      slashes = 0;
 +      for (u = 0; str[u] != 0; u++)
 +              if (str[u] == '/') {
 +                      slashes++;
 +                      if (slashes == 4) {
 +                              __uri_init(module, str, u);
 +                              return true;
 +                      }
 +              }
 +
 +      if (slashes == 3 && str[u - 1] != '/') {
 +              *module = *url;
 +              return true;
 +      }
 +
 +      pr_val_err("Url '%s' does not appear to have an rsync module.", str);
 +      return false;
 +}
 +
 +char const *
 +strip_rsync_module(char const *url)
 +{
 +      array_index u;
 +      unsigned int slashes;
 +
 +      slashes = 0;
 +      for (u = 0; url[u] != 0; u++)
 +              if (url[u] == '/') {
 +                      slashes++;
 +                      if (slashes == 4)
 +                              return url + u + 1;
 +              }
 +
 +      return NULL;
 +}
 +
 +static json_t *
 +node2json(struct cache_node *node)
 +{
 +      char const *str;
 +      json_t *json;
 +
 +      json = json_obj_new();
 +      if (json == NULL)
 +              return NULL;
 +
 +      str = uri_str(&node->key.http);
 +      if (str && json_add_str(json, "http", str))
 +              goto fail;
 +      str = uri_str(&node->key.rsync);
 +      if (str && json_add_str(json, "rsync", str))
 +              goto fail;
 +      if (json_add_str(json, "path", node->path))
 +              goto fail;
 +      if (node->attempt_ts && json_add_ts(json, "attempt", node->attempt_ts))
 +              goto fail;
 +      if (node->success_ts && json_add_ts(json, "success", node->success_ts))
 +              goto fail;
 +      if (node->mft.num.size && json_add_bigint(json, "mftNum", &node->mft.num))
 +              goto fail;
 +      if (node->mft.update && json_add_ts(json, "mftUpdate", node->mft.update))
 +              goto fail;
 +      if (node->rrdp)
 +              if (json_object_add(json, "rrdp", rrdp_state2json(node->rrdp)))
 +                      goto fail;
 +
 +      return json;
 +
 +fail: json_decref(json);
 +      return NULL;
 +}
 +
 +static validation_verdict dl_rsync(struct cache_node *);
 +static validation_verdict dl_http(struct cache_node *);
 +static validation_verdict dl_rrdp(struct cache_node *);
 +
 +static void
 +init_table(struct cache_table *tbl, char *name, bool enabled, dl_cb dl)
 +{
 +      memset(tbl, 0, sizeof(*tbl));
 +      tbl->name = name;
 +      tbl->enabled = enabled;
 +      cseq_init(&tbl->seq, name, 0, false);
 +      tbl->download = dl;
 +      panic_on_fail(pthread_mutex_init(&tbl->lock, NULL),
 +          "pthread_mutex_init");
 +}
 +
 +static void
 +init_tables(void)
 +{
 +      init_table(&cache.rsync, "rsync", config_get_rsync_enabled(), dl_rsync);
 +      init_table(&cache.https, "https", config_get_http_enabled(), dl_http);
 +      init_table(&cache.rrdp, "rrdp", config_get_http_enabled(), dl_rrdp);
 +      init_table(&cache.fallback, "fallback", true, NULL);
 +}
 +
 +static int
 +reset_cache_dir(void)
 +{
 +      DIR *dir;
 +      struct dirent *file;
 +      int tmperr, abserr;
 +      unsigned int deleted;
 +
 +      pr_op_debug("Resetting cache...");
 +
 +      abserr = 0;
 +      deleted = 0;
 +
 +      dir = opendir(".");
 +      if (dir == NULL)
 +              goto end;
 +
 +      FOREACH_DIR_FILE(dir, file)
 +              if (!S_ISDOTS(file) && strcmp(file->d_name, LOCKFILE) != 0) {
 +                      tmperr = file_rm_rf(file->d_name);
 +                      if (tmperr)
 +                              abserr = tmperr;
 +                      deleted++;
 +              }
 +
 +end:  tmperr = errno;
 +      if (tmperr)
 +              abserr = tmperr;
 +      if (abserr)
 +              pr_op_warn("Cannot reset cache: %s", strerror(abserr));
 +      else
 +              pr_op_debug(deleted > 0 ? "Cache reset." : "Cache was empty.");
 +      closedir(dir);
 +      return abserr;
 +}
 +
 +static void
 +init_cachedir_tag(void)
 +{
 +      static char const *filename = "CACHEDIR.TAG";
 +      if (file_stat_errno(filename) == ENOENT)
 +              file_write_txt(filename,
 +                 "Signature: 8a477f597d28d172789f06886806bc55\n"
 +                 "# This file is a cache directory tag created by Fort.\n"
 +                 "# For information about cache directory tags, see:\n"
 +                 "#   https://bford.info/cachedir/\n");
 +}
 +
 +static int
 +lock_cache(void)
 +{
 +      int fd;
 +      int error;
 +
 +      pr_op_debug("touch " LOCKFILE);
 +
 +      /*
 +       * Suppose we get SIGTERM in the middle of this function.
 +       *
 +       * 1. open() then lockfile_owned = 1, we're interrupted between them:
 +       *    The handler doesn't delete our lock.
 +       * 2. lockfile_owned = 1 then open(), we're interrupted between them:
 +       *    The handler deletes some other instance's lock.
 +       *
 +       * 1 is better because we already couldn't guarantee the lock was
 +       * deleted on every situation. (SIGKILL)
 +       */
 +
 +      fd = open(LOCKFILE, O_CREAT | O_EXCL, 0644);
 +      if (fd < 0) {
 +              error = errno;
 +              pr_op_err("Cannot create lockfile '%s/" LOCKFILE "': %s",
 +                  config_get_local_repository(), strerror(error));
 +              return error;
 +      }
 +      close(fd);
 +
 +      lockfile_owned = 1;
 +      return 0;
 +}
 +
 +static void
 +unlock_cache(void)
 +{
 +      pr_op_debug("rm " LOCKFILE);
 +
 +      if (!lockfile_owned) {
 +              pr_op_debug("The cache wasn't locked.");
 +              return;
 +      }
 +
 +      if (unlink(LOCKFILE) < 0) {
 +              int error = errno;
 +              pr_op_err("Cannot remove lockfile: %s", strerror(error));
 +              if (error != ENOENT)
 +                      return;
 +      }
 +
 +      lockfile_owned = 0;
 +}
 +
 +/* THIS FUNCTION CAN BE CALLED FROM A SIGNAL HANDLER. */
 +void
 +cache_atexit(void)
 +{
 +      if (lockfile_owned)
 +              unlink(LOCKFILE);
 +}
 +
 +int
 +cache_setup1(void)
 +{
 +      char const *cachedir;
 +      int error;
 +
 +      cachedir = config_get_local_repository();
 +
 +      error = file_mkdir(cachedir, true);
 +      if (error)
 +              return error;
 +
 +      pr_op_debug("cd %s", cachedir);
 +      if (chdir(cachedir) < 0) {
 +              error = errno;
 +              pr_op_err("Cannot cd to %s: %s", cachedir, strerror(error));
 +              return error;
 +      }
 +
 +      return 0;
 +}
 +
 +int
 +cache_setup2(void)
 +{
 +      int error;
 +
 +      init_tables();
 +
 +      errno = 0;
 +      error = atexit(cache_atexit);
 +      if (error) {
 +              int err2 = errno;
 +              pr_op_err("Cannot register cache's exit function.");
 +              pr_op_err("Error message attempt 1: %s", strerror(error));
 +              pr_op_err("Error message attempt 2: %s", strerror(err2));
 +              return error;
 +      }
 +
 +      return 0;
 +}
 +
 +static void
 +init_rrdp_fallback_key(struct node_key *key, struct uri const *http,
 +    struct uri const *rsync)
 +{
 +      size_t hlen, rlen;
 +
 +      hlen = uri_len(http);
 +      rlen = uri_len(rsync);
 +
 +      key->idlen = hlen + rlen + 1;
 +      key->id = pmalloc(key->idlen + 1);
 +      __uri_init(&key->http, key->id, hlen);
 +      __uri_init(&key->rsync, key->id + hlen + 1, rlen);
 +
 +      memcpy(key->id, uri_str(http), hlen + 1);
 +      memcpy(key->id + hlen + 1, uri_str(rsync), rlen + 1);
 +}
 +
 +static int
 +init_node_key(struct node_key *key, struct uri const *http,
 +    struct uri const *rsync)
 +{
 +      if (http && (uri_str(http) == NULL))
 +              http = NULL;
 +      if (rsync && (uri_str(rsync) == NULL))
 +              rsync = NULL;
 +
 +      if (http != NULL && rsync != NULL) {
 +              init_rrdp_fallback_key(key, http, rsync);
 +
 +      } else if (rsync != NULL) {
 +              key->idlen = uri_len(rsync);
 +              key->id = pstrndup(uri_str(rsync), key->idlen);
 +              memset(&key->http, 0, sizeof(key->http));
 +              __uri_init(&key->rsync, key->id, key->idlen);
 +
 +      } else if (http != NULL) {
 +              key->idlen = uri_len(http);
 +              key->id = pstrndup(uri_str(http), key->idlen);
 +              __uri_init(&key->http, key->id, key->idlen);
 +              memset(&key->rsync, 0, sizeof(key->rsync));
 +
 +      } else {
 +              return false;
 +      }
 +
 +      return true;
 +}
 +
 +static struct cache_node *
 +json2node(json_t *json)
 +{
 +      struct cache_node *node;
 +      struct uri http;
 +      struct uri rsync;
 +      char const *path;
 +      json_t *rrdp;
 +      int error;
 +
 +      error = json_get_uri(json, "http", &http);
 +      if (error && (error != ENOENT)) {
 +              pr_op_debug("http: %s", strerror(error));
 +              return NULL;
 +      }
 +
 +      error = json_get_uri(json, "rsync", &rsync);
 +      if (error && (error != ENOENT)) {
 +              pr_op_debug("rsync: %s", strerror(error));
 +              uri_cleanup(&http);
 +              return NULL;
 +      }
 +
 +      node = pzalloc(sizeof(struct cache_node));
 +
 +      if (!init_node_key(&node->key, &http, &rsync)) {
 +              pr_op_debug("JSON node is missing both http and rsync tags.");
 +              uri_cleanup(&rsync);
 +              uri_cleanup(&http);
 +              goto nde;
 +      }
 +
 +      uri_cleanup(&http);
 +      uri_cleanup(&rsync);
 +
 +      error = json_get_str(json, "path", &path);
 +      if (error) {
 +              pr_op_debug("path: %s", strerror(error));
 +              goto key;
 +      }
 +      node->path = pstrdup(path);
 +
 +      error = json_get_ts(json, "attempt", &node->attempt_ts);
 +      if (error != 0 && error != ENOENT) {
 +              pr_op_debug("attempt: %s", strerror(error));
 +              goto pth;
 +      }
 +
 +      error = json_get_ts(json, "success", &node->success_ts);
 +      if (error != 0 && error != ENOENT) {
 +              pr_op_debug("success: %s", strerror(error));
 +              goto pth;
 +      }
 +
 +      error = json_get_bigint(json, "mftNum", &node->mft.num);
 +      if (error < 0) {
 +              pr_op_debug("mftNum: %s", strerror(error));
 +              goto pth;
 +      }
 +
 +      error = json_get_ts(json, "mftUpdate", &node->mft.update);
 +      if (error != 0 && error != ENOENT) {
 +              pr_op_debug("mftUpdate: %s", strerror(error));
 +              goto mft;
 +      }
 +
 +      error = json_get_object(json, "rrdp", &rrdp);
 +      if (error < 0) {
 +              pr_op_debug("rrdp: %s", strerror(error));
 +              goto mft;
 +      }
 +      if (error == 0 && rrdp_json2state(rrdp, node->path, &node->rrdp))
 +              goto mft;
 +
 +      return node;
 +
 +mft:  INTEGER_cleanup(&node->mft.num);
 +pth:  free(node->path);
 +key:  free(node->key.id);
 +nde:  free(node);
 +      return NULL;
 +}
 +
 +static int
 +check_root_metafile(void)
 +{
 +      json_error_t jerr;
 +      json_t *root;
 +      char const *file_version;
 +      int error;
 +
 +      pr_op_debug("Loading " METAFILE "...");
 +
 +      root = json_load_file(METAFILE, 0, &jerr);
 +      if (root == NULL) {
 +              if (json_error_code(&jerr) == json_error_cannot_open_file) {
 +                      pr_op_debug(METAFILE " does not exist.");
 +                      return ENOENT;
 +              } else {
 +                      pr_op_err("Json parsing failure at %s (%d:%d): %s",
 +                          METAFILE, jerr.line, jerr.column, jerr.text);
 +                      return EINVAL;
 +              }
 +      }
 +
 +      if (json_typeof(root) != JSON_OBJECT) {
 +              pr_op_err("The root tag of " METAFILE " is not an object.");
 +              goto fail;
 +      }
 +
 +      error = json_get_str(root, TAGNAME_VERSION, &file_version);
 +      if (error) {
 +              if (error > 0)
 +                      pr_op_err(METAFILE " is missing the '"
 +                          TAGNAME_VERSION "' tag.");
 +              goto fail;
 +      }
 +      if (strcmp(file_version, PACKAGE_VERSION) != 0) {
 +              pr_op_err("The cache was written by Fort %s; "
 +                  "I need to clear it.", file_version);
 +              goto fail;
 +      }
 +
 +      json_decref(root);
 +      pr_op_debug(METAFILE " loaded.");
 +      return 0;
 +
 +fail: json_decref(root);
 +      return EINVAL;
 +}
 +
 +static void
 +collect_meta(struct cache_table *tbl, struct dirent *dir)
 +{
 +      char filename[64];
 +      int wrt;
 +      json_error_t jerr;
 +      json_t *root;
 +      struct cache_node *node;
 +
 +      if (S_ISDOTS(dir))
 +              return;
 +
 +      wrt = snprintf(filename, 64, "%s/%s.json", tbl->name, dir->d_name);
 +      if (wrt >= 64)
 +              pr_crit("collect_meta: %d %s %s", wrt, tbl->name, dir->d_name);
 +
 +      pr_clutter("%s: Loading...", filename);
 +
 +      root = json_load_file(filename, 0, &jerr);
 +      if (root == NULL) {
 +              if (json_error_code(&jerr) == json_error_cannot_open_file)
 +                      pr_op_warn("%s: File does not exist.", filename);
 +              else
 +                      pr_op_warn("%s: Json parsing failure at (%d:%d): %s",
 +                          filename, jerr.line, jerr.column, jerr.text);
 +              return;
 +      }
 +
 +      if (json_typeof(root) != JSON_OBJECT) {
 +              pr_op_warn("%s: Root tag is not an object.", filename);
 +              goto end;
 +      }
 +
 +      node = json2node(root);
 +      if (node != NULL) {
 +              // XXX worry about dupes
 +              HASH_ADD_KEYPTR(hh, tbl->nodes, node->key.id, node->key.idlen,
 +                  node);
 +      }
 +
 +      pr_clutter("%s: Loaded.", filename);
 +end:  json_decref(root);
 +}
 +
 +static void
 +collect_metas(struct cache_table *tbl)
 +{
 +      DIR *dir;
 +      struct dirent *file;
 +      unsigned long id, max_id;
 +      int error;
 +
 +      dir = opendir(tbl->name);
 +      if (dir == NULL) {
 +              error = errno;
 +              if (error != ENOENT)
 +                      pr_op_warn("Cannot open %s: %s",
 +                          tbl->name, strerror(error));
 +              return;
 +      }
 +
 +      max_id = 0;
 +      FOREACH_DIR_FILE(dir, file) {
 +              if (hex2ulong(file->d_name, &id) != 0)
 +                      continue;
 +              if (id > max_id)
 +                      max_id = id;
 +              collect_meta(tbl, file);
 +      }
 +      error = errno;
 +      if (error)
 +              pr_op_warn("Could not finish traversing %s: %s",
 +                  tbl->name, strerror(error));
 +
 +      closedir(dir);
 +
 +      tbl->seq.prefix = tbl->name;
 +      tbl->seq.next_id = max_id + 1;
 +      tbl->seq.pathlen = strlen(tbl->name);
 +      tbl->seq.free_prefix = false;
 +}
 +
 +static int
 +load_index(void)
 +{
 +      int error;
 +
 +      error = check_root_metafile();
 +      if (error)
 +              return error;
 +
 +      collect_metas(&cache.rsync);
 +      collect_metas(&cache.https);
 +      collect_metas(&cache.rrdp);
 +      collect_metas(&cache.fallback);
 +      return 0;
 +}
 +
 +int
 +cache_prepare(void)
 +{
 +      int error;
 +
 +      error = lock_cache();
 +      if (error)
 +              return error;
 +
 +      if (load_index() != 0) {
 +              error = reset_cache_dir();
 +              if (error)
 +                      goto fail;
 +      }
 +
 +      error = file_mkdir("rsync", true);
 +      if (error)
 +              goto fail;
 +      error = file_mkdir("https", true);
 +      if (error)
 +              goto fail;
 +      error = file_mkdir("rrdp", true);
 +      if (error)
 +              goto fail;
 +      error = file_mkdir("fallback", true);
 +      if (error)
 +              goto fail;
 +      error = file_mkdir(CACHE_TMPDIR, true);
 +      if (error)
 +              goto fail;
 +      init_cachedir_tag();
 +
 +      return 0;
 +
 +fail: flush_nodes();
 +      unlock_cache();
 +      return error;
 +}
 +
 +static validation_verdict
 +dl_rsync(struct cache_node *module)
 +{
 +      int error;
 +      error = rsync_queue(&module->key.rsync, module->path);
 +      return error ? VV_FAIL : VV_BUSY;
 +}
 +
 +static validation_verdict
 +dl_rrdp(struct cache_node *notif)
 +{
 +      bool changed;
 +
 +      if (rrdp_update(&notif->key.http, notif->path, notif->success_ts,
 +                      &changed, &notif->rrdp))
 +              return VV_FAIL;
 +      if (changed)
 +              notif->success_ts = notif->attempt_ts;
 +      return VV_CONTINUE;
 +}
 +
 +static validation_verdict
 +dl_http(struct cache_node *file)
 +{
 +      bool changed;
 +
 +      if (http_download(&file->key.http, file->path, file->success_ts,
 +                        &changed))
 +              return VV_FAIL;
 +      if (changed)
 +              file->success_ts = file->attempt_ts;
 +      return VV_CONTINUE;
 +}
 +
 +/* Caller must lock @tbl->lock */
 +static struct cache_node *
 +find_node(struct cache_table *tbl, char const *url, size_t urlen)
 +{
 +      struct cache_node *node;
 +      HASH_FIND(hh, tbl->nodes, url, urlen, node);
 +      return node;
 +}
 +
 +static struct cache_node *
 +provide_node(struct cache_table *tbl, struct uri const *http,
 +   struct uri const *rsync)
 +{
 +      struct node_key key;
 +      struct cache_node *node;
 +
 +      if (!init_node_key(&key, http, rsync)) {
 +              pr_val_debug("Can't build node identifier: Both HTTP and rsync URLs are NULL.");
 +              return NULL;
 +      }
 +
 +      node = find_node(tbl, key.id, key.idlen);
 +      if (node) {
 +              free(key.id);
 +              return node;
 +      }
 +
 +      node = pzalloc(sizeof(struct cache_node));
 +      node->key = key;
 +      node->path = cseq_next(&tbl->seq);
 +      if (!node->path) {
 +              free(node);
 +              free(key.id);
 +              return NULL;
 +      }
 +
 +      HASH_ADD_KEYPTR(hh, tbl->nodes, node->key.id, node->key.idlen, node);
 +      return node;
 +
 +}
 +
 +static void
 +rm_metadata(struct cache_node *node)
 +{
 +      char *filename;
 +      int error;
 +
 +      filename = str_concat(node->path, ".json");
 +      pr_op_debug("rm %s", filename);
 +      if (unlink(filename) < 0) {
 +              error = errno;
 +              if (error == ENOENT)
 +                      pr_op_debug("%s already doesn't exist.", filename);
 +              else
 +                      pr_op_warn("Cannot rm %s: %s", filename, strerror(errno));
 +      }
 +
 +      free(filename);
 +}
 +
 +static void
 +write_metadata(struct cache_node *node)
 +{
 +      char *filename;
 +      json_t *json;
 +
 +      json = node2json(node);
 +      if (!json)
 +              return;
 +      filename = str_concat(node->path, ".json");
 +
 +      pr_op_debug("echo \"$json\" > %s", filename);
 +      if (json_dump_file(json, filename, JSON_INDENT(2)))
 +              pr_op_err("Unable to write %s; unknown cause.", filename);
 +
 +      free(filename);
 +      json_decref(json);
 +}
 +
 +/*
 + * @uri is either a caRepository or a rpkiNotify
 + * By contract, only sets @result on return 0.
 + * By contract, @result->state will be DLS_FRESH on return 0.
 + */
 +static validation_verdict
 +do_refresh(struct cache_table *tbl, struct uri const *uri,
 +    struct cache_node **result)
 +{
 +      struct uri module;
 +      struct cache_node *node;
 +      bool downloaded = false;
 +
 +      pr_val_debug("Trying %s (online)...", uri_str(uri));
 +
 +      if (!tbl->enabled) {
 +              pr_val_debug("Protocol disabled.");
 +              return VV_FAIL;
 +      }
 +
 +      if (tbl == &cache.rsync) {
 +              if (!get_rsync_module(uri, &module))
 +                      return VV_FAIL;
 +              mutex_lock(&tbl->lock);
 +              node = provide_node(tbl, NULL, &module);
 +      } else {
 +              mutex_lock(&tbl->lock);
 +              node = provide_node(tbl, uri, NULL);
 +      }
 +      if (!node) {
 +              mutex_unlock(&tbl->lock);
 +              return VV_FAIL;
 +      }
 +
 +      /*
 +       * Reminder: If the state is ONGOING, DO NOT read anything other than
 +       * the lock and state.
 +       */
 +
 +      switch (node->state) {
 +      case DLS_OUTDATED:
 +              node->state = DLS_ONGOING;
 +              mutex_unlock(&tbl->lock);
 +
 +              node->attempt_ts = time_fatal();
 +              rm_metadata(node);
 +              node->verdict = tbl->download(node);
 +              if (node->verdict == VV_BUSY)
 +                      goto ongoing;
 +              write_metadata(node);
 +              downloaded = true;
 +
 +              mutex_lock(&tbl->lock);
 +              node->state = DLS_FRESH;
 +              break;
 +      case DLS_ONGOING:
 +ongoing:      mutex_unlock(&tbl->lock);
 +              pr_val_debug("Refresh ongoing.");
 +              return VV_BUSY;
 +      case DLS_FRESH:
 +              break;
 +      default:
 +              pr_crit("Unknown node state: %d", node->state);
 +      }
 +
 +      mutex_unlock(&tbl->lock);
 +      /* node->state is guaranteed to be DLS_FRESH at this point. */
 +
 +      if (downloaded) /* Kickstart tasks that fell into DLS_ONGOING */
 +              task_wakeup_dormants();
 +
 +      if (node->verdict == VV_FAIL) {
 +              pr_val_debug("Refresh failed.");
 +              return VV_FAIL;
 +      }
 +
 +      pr_val_debug("Refresh succeeded.");
 +      *result = node;
 +      return VV_CONTINUE;
 +}
 +
 +static struct cache_node *
 +find_rrdp_fallback_node(struct extension_uris *uris)
 +{
 +      struct node_key key;
 +      struct cache_node *result;
 +
 +      if (!uri_str(&uris->rpkiNotify) || !uri_str(&uris->caRepository))
 +              return NULL;
 +
 +      init_rrdp_fallback_key(&key, &uris->rpkiNotify, &uris->caRepository);
 +      result = find_node(&cache.fallback, key.id, key.idlen);
 +      free(key.id);
 +
 +      return result;
 +}
 +
 +static struct cache_node *
 +get_fallback(struct extension_uris *uris)
 +{
 +      struct cache_node *rrdp;
 +      struct cache_node *rsync;
 +
 +      rrdp = find_rrdp_fallback_node(uris);
 +      rsync = find_node(&cache.fallback, uri_str(&uris->caRepository),
 +          uri_len(&uris->caRepository));
 +
 +      if (rrdp == NULL)
 +              return rsync;
 +      if (rsync == NULL)
 +              return rrdp;
 +      return (difftime(rsync->success_ts, rrdp->success_ts) > 0) ? rsync : rrdp;
 +}
 +
 +validation_verdict
 +cache_refresh_by_url(struct uri const *url, char **result)
 +{
 +      struct cache_node *node = NULL;
 +      validation_verdict vv;
 +
 +      if (uri_is_https(url)) {
 +              vv = do_refresh(&cache.https, url, &node);
 +              if (vv != VV_CONTINUE)
 +                      goto oops;
 +              *result = node ? pstrdup(node->path) : NULL;
 +              return VV_CONTINUE;
 +      }
 +
 +      if (uri_is_rsync(url)) {
 +              vv = do_refresh(&cache.rsync, url, &node);
 +              if (vv != VV_CONTINUE)
 +                      goto oops;
 +              *result = path_join(node->path, strip_rsync_module(uri_str(url)));
 +              return VV_CONTINUE;
 +      }
 +
 +      vv = VV_FAIL;
 +oops: *result = NULL;
 +      return vv;
 +}
 +
 +/* HTTPS (TAs) and rsync only; don't use this for RRDP. */
 +validation_verdict
 +cache_get_fallback(struct uri const *url, char **result)
 +{
 +      struct cache_node *node;
 +
 +      /*
 +       * The fallback table is read-only until the cleanup.
 +       * Mutex not needed here.
 +       */
 +
 +      pr_val_debug("Trying %s (offline)...", uri_str(url));
 +
 +      node = find_node(&cache.fallback, uri_str(url), uri_len(url));
 +      if (!node) {
 +              pr_val_debug("Cache data unavailable.");
 +              *result = NULL;
 +              return VV_CONTINUE;
 +      }
 +
 +      *result = pstrdup(node->path);
 +      return VV_CONTINUE;
 +}
 +
 +/*
 + * Attempts to refresh the RPP described by @sias, returns the resulting
 + * repository's mapper.
 + *
 + * XXX Fallback only if parent is fallback
 + */
 +validation_verdict
 +cache_refresh_by_uris(struct extension_uris *uris, struct cache_cage **result)
 +{
 +      struct cache_node *node;
 +      struct cache_cage *cage;
 +      struct uri rpkiNotify;
 +      validation_verdict vv;
 +
 +      // XXX Make sure somewhere validates rpkiManifest matches caRepository.
 +
 +      /* Try RRDP + optional fallback */
 +      if (uri_str(&uris->rpkiNotify) != NULL) {
 +              vv = do_refresh(&cache.rrdp, &uris->rpkiNotify, &node);
 +              if (vv == VV_CONTINUE) {
 +                      rpkiNotify = uris->rpkiNotify;
 +                      goto refresh_success;
 +              }
 +              if (vv == VV_BUSY)
 +                      return VV_BUSY;
 +      }
 +
 +      /* Try rsync + optional fallback */
 +      vv = do_refresh(&cache.rsync, &uris->caRepository, &node);
 +      if (vv == VV_CONTINUE) {
 +              memset(&rpkiNotify, 0, sizeof(rpkiNotify));
 +              goto refresh_success;
 +      }
 +      if (vv == VV_BUSY)
 +              return VV_BUSY;
 +
 +      /* Try fallback only */
 +      node = get_fallback(uris);
 +      if (!node)
 +              return VV_FAIL; /* Nothing to work with */
 +
 +      *result = cage = pzalloc(sizeof(struct cache_cage));
 +      cage->fallback = node;
 +      return VV_CONTINUE;
 +
 +refresh_success:
 +      *result = cage = pzalloc(sizeof(struct cache_cage));
 +      cage->rpkiNotify = rpkiNotify;
 +      cage->refresh = node;
 +      cage->fallback = get_fallback(uris);
 +      return VV_CONTINUE;
 +}
 +
 +/* Result needs free() */
 +static char *
 +node2file(struct cache_node const *node, struct uri const *url)
 +{
 +      if (node == NULL)
 +              return NULL;
 +      return (node->rrdp)
 +          ? /* RRDP  */ pstrdup(rrdp_file(node->rrdp, url))
 +          : /* rsync */ path_join(node->path, strip_rsync_module(uri_str(url)));
 +}
 +
 +/* Result needs free() */
 +char *
 +cage_map_file(struct cache_cage *cage, struct uri const *url)
 +{
 +      /*
 +       * Remember: In addition to honoring the consts of cache->refresh and
 +       * cache->fallback, anything these structures point to MUST NOT be
 +       * modified either.
 +       */
 +
 +      char *file;
 +
 +      file = node2file(cage->refresh, url);
 +      if (!file)
 +              file = node2file(cage->fallback, url);
 +
 +      return file;
 +}
 +
 +/*
 + * If refresh enabled,
 + *    Switches to fallback.
 + *    If fallback unavailable, disables the cage.
 + * Else, if fallback enabled, disables the cage.
 + * Else (if cage disabled) does nothing.
 + *
 + * Returns true if the next option should be attempted.
 + */
 +bool
 +cage_downgrade(struct cache_cage *cage)
 +{
 +      /*
 +       * Remember: In addition to honoring the consts of cache->refresh and
 +       * cache->fallback, anything these structures point to MUST NOT be
 +       * modified either.
 +       */
 +
 +      if (cage->refresh) {
 +              cage->refresh = NULL;
 +              return cage->fallback != NULL;
 +      }
 +      if (cage->fallback)
 +              cage->fallback = NULL;
 +      return false;
 +}
 +
 +struct mft_meta const *
 +cage_mft_fallback(struct cache_cage *cage)
 +{
 +      return cage->mft;
 +}
 +
 +/*
 + * Steals ownership of @rpp->files, @rpp->nfiles and @rpp->mft.num, but they're
 + * not going to be modified nor deleted until the cache cleanup.
 + */
 +void
 +cache_commit_rpp(struct uri const *rpkiNotify, struct uri const *caRepository,
 +    struct rpp *rpp)
 +{
 +      struct cache_commit *commit;
 +
 +      commit = pmalloc(sizeof(struct cache_commit));
 +      uri_copy(&commit->rpkiNotify, rpkiNotify);
 +      uri_copy(&commit->caRepository, caRepository);
 +      commit->files = rpp->files;
 +      commit->nfiles = rpp->nfiles;
 +      INTEGER_move(&commit->mft.num, &rpp->mft.num);
 +      commit->mft.update = rpp->mft.update;
 +
 +      mutex_lock(&commits_lock);
 +      STAILQ_INSERT_TAIL(&commits, commit, lh);
 +      mutex_unlock(&commits_lock);
 +
 +      rpp->files = NULL;
 +      rpp->nfiles = 0;
 +}
 +
 +void
 +cache_commit_file(struct cache_mapping const *map)
 +{
 +      struct cache_commit *commit;
 +
 +      commit = pzalloc(sizeof(struct cache_commit));
 +      memset(&commit->rpkiNotify, 0, sizeof(commit->rpkiNotify));
 +      memset(&commit->caRepository, 0, sizeof(commit->caRepository));
 +      commit->files = pmalloc(sizeof(*map));
 +      map_copy(commit->files, map);
 +      commit->nfiles = 1;
 +      memset(&commit->mft, 0, sizeof(commit->mft));
 +
 +      mutex_lock(&commits_lock);
 +      STAILQ_INSERT_TAIL(&commits, commit, lh);
 +      mutex_unlock(&commits_lock);
 +}
 +
 +void
 +rsync_finished(struct uri const *url, char const *path)
 +{
 +      struct cache_node *node;
 +
 +      mutex_lock(&cache.rsync.lock);
 +
 +      node = find_node(&cache.rsync, uri_str(url), uri_len(url));
 +      if (node == NULL) {
 +              mutex_unlock(&cache.rsync.lock);
 +              pr_op_err("rsync '%s -> %s' finished, but cache node does not exist.",
 +                  uri_str(url), path);
 +              return;
 +      }
 +      if (node->state != DLS_ONGOING)
 +              pr_op_warn("rsync '%s -> %s' finished, but existing node was not in ONGOING state.",
 +                  uri_str(url), path);
 +
 +      node->state = DLS_FRESH;
 +      node->verdict = VV_CONTINUE;
 +      node->success_ts = node->attempt_ts;
 +      mutex_unlock(&cache.rsync.lock);
 +
 +      task_wakeup_dormants();
 +}
 +
 +struct uri const *
 +cage_rpkiNotify(struct cache_cage *cage)
 +{
 +      return &cage->rpkiNotify;
 +}
 +
 +static void
 +cachent_print(struct cache_node *node)
 +{
 +      if (!node)
 +              return;
 +
 +      printf("\thttp:%s rsync:%s (%s): ", uri_str(&node->key.http),
 +          uri_str(&node->key.rsync), node->path);
 +      switch (node->state) {
 +      case DLS_OUTDATED:
 +              printf("stale ");
 +              break;
 +      case DLS_ONGOING:
 +              printf("downloading ");
 +              break;
 +      case DLS_FRESH:
 +              printf("fresh (%s) ", node->verdict);
 +              break;
 +      }
 +
 +      printf("attempt:%lx success:%lx ", node->attempt_ts, node->success_ts);
 +      printf("mftUpdate:%lx ", node->mft.update);
 +      rrdp_print(node->rrdp);
 +      printf("\n");
 +}
 +
 +static void
 +table_print(struct cache_table *tbl)
 +{
 +      struct cache_node *node, *tmp;
 +
 +      printf("%s enabled:%d seq:%s/%lx\n",
 +          tbl->name, tbl->enabled,
 +          tbl->seq.prefix, tbl->seq.next_id);
 +      HASH_ITER(hh, tbl->nodes, node, tmp)
 +              cachent_print(node);
 +}
 +
 +void
 +cache_print(void)
 +{
 +      table_print(&cache.rsync);
 +      table_print(&cache.https);
 +      table_print(&cache.rrdp);
 +      table_print(&cache.fallback);
 +}
 +
 +static bool
 +is_fallback(char const *path)
 +{
 +      return str_starts_with(path, "fallback/");
 +}
 +
 +/* Hard-links @rpp's approved files into the fallback directory. */
 +static void
 +commit_rpp(struct cache_commit *commit, struct cache_node *fb)
 +{
 +      struct cache_mapping *src;
 +      char const *dst;
 +      array_index i;
 +
 +      INTEGER_move(&fb->mft.num, &commit->mft.num);
 +      fb->mft.update = commit->mft.update;
 +
 +      for (i = 0; i < commit->nfiles; i++) {
 +              src = commit->files + i;
 +
 +              if (is_fallback(src->path))
 +                      continue;
 +
 +              /*
 +               * (fine)
 +               * Note, this is accidentally working perfectly for rsync too.
 +               * Might want to rename some of this.
 +               */
 +              dst = rrdp_create_fallback(fb->path, &fb->rrdp, &src->url);
 +              if (!dst)
 +                      goto skip;
 +
 +              file_ln(src->path, dst);
 +
 +skip:         free(src->path);
 +              src->path = pstrdup(dst);
 +      }
 +}
 +
 +/* Deletes abandoned (ie. no longer ref'd by manifests) fallback hard links. */
 +static void
 +discard_trash(struct cache_commit *commit, struct cache_node *fallback)
 +{
 +      DIR *dir;
 +      struct dirent *file;
 +      char *file_path;
 +      array_index i;
 +
 +      dir = opendir(fallback->path);
 +      if (dir == NULL) {
 +              pr_op_err("opendir() error: %s", strerror(errno));
 +              return;
 +      }
 +
 +      FOREACH_DIR_FILE(dir, file) {
 +              if (S_ISDOTS(file))
 +                      continue;
 +
 +              /*
 +               * TODO (fine) Bit slow; wants a hash table,
 +               * and maybe skip @file_path's reallocation.
 +               */
 +
 +              file_path = path_join(fallback->path, file->d_name);
 +
 +              for (i = 0; i < commit->nfiles; i++) {
 +                      if (commit->files[i].path == NULL)
 +                              continue;
 +                      if (strcmp(file_path, commit->files[i].path) == 0)
 +                              goto next;
 +              }
 +
 +              /*
 +               * Uh... maybe keep the file until an expiration threshold?
 +               * None of the current requirements seem to mandate it.
 +               * It sounds pretty unreasonable for a signed valid manifest to
 +               * "forget" a file, then legitimately relist it without actually
 +               * providing it.
 +               */
 +              pr_op_debug("Removing hard link: %s", file_path);
 +              if (unlink(file_path) < 0)
 +                      pr_op_warn("Could not unlink %s: %s",
 +                          file_path, strerror(errno));
 +
 +next:         free(file_path);
 +      }
 +
 +      if (errno)
 +              pr_op_err("Fallback directory traversal errored: %s",
 +                  strerror(errno));
 +      closedir(dir);
 +}
 +
 +static void
 +commit_fallbacks(time_t now)
 +{
 +      struct cache_commit *commit;
 +      struct cache_node *fb;
 +      array_index i;
 +      int error;
 +
 +      while (!STAILQ_EMPTY(&commits)) {
 +              commit = STAILQ_FIRST(&commits);
 +              STAILQ_REMOVE_HEAD(&commits, lh);
 +
 +              if (uri_str(&commit->caRepository) != NULL) {
 +                      pr_op_debug("Creating fallback for %s (%s)",
 +                          uri_str(&commit->caRepository),
 +                          uri_str(&commit->rpkiNotify));
 +
 +                      fb = provide_node(&cache.fallback,
 +                          &commit->rpkiNotify,
 +                          &commit->caRepository);
 +                      fb->attempt_ts = now;
 +                      fb->success_ts = now;
 +
 +                      pr_op_debug("mkdir -f %s", fb->path);
 +                      if (mkdir(fb->path, CACHE_FILEMODE) < 0) {
 +                              error = errno;
 +                              if (error != EEXIST) {
 +                                      pr_op_err("Cannot create '%s': %s",
 +                                          fb->path, strerror(error));
 +                                      goto skip;
 +                              }
 +
 +                              rm_metadata(fb); /* error == EEXIST */
 +                      }
 +
 +                      commit_rpp(commit, fb);
 +                      discard_trash(commit, fb);
 +
 +              } else { /* TA */
 +                      struct cache_mapping *map = &commit->files[0];
 +
 +                      pr_op_debug("Creating fallback for %s",
 +                          uri_str(&map->url));
 +
 +                      fb = provide_node(&cache.fallback, &map->url, NULL);
 +                      fb->attempt_ts = now;
 +                      fb->success_ts = now;
 +                      if (is_fallback(map->path))
 +                              goto freshen;
 +
 +                      file_ln(map->path, fb->path);
 +              }
 +
 +              write_metadata(fb);
 +
 +freshen:      fb->state = DLS_FRESH;
 +skip:         uri_cleanup(&commit->rpkiNotify);
 +              uri_cleanup(&commit->caRepository);
 +              for (i = 0; i < commit->nfiles; i++) {
 +                      uri_cleanup(&commit->files[i].url);
 +                      free(commit->files[i].path);
 +              }
 +              free(commit->files);
 +              mftm_cleanup(&commit->mft);
 +              free(commit);
 +      }
 +}
 +
 +static void
 +remove_abandoned(struct cache_table *table, struct cache_node *node, void *arg)
 +{
 +      time_t now;
 +
 +      if (node->state == DLS_FRESH)
 +              return;
 +
 +      now = *((time_t *)arg);
 +      if (difftime(node->attempt_ts + cfg_cache_threshold(), now) < 0) {
 +              rm_metadata(node);
 +              file_rm_rf(node->path);
 +              delete_node(table, node, NULL);
 +      }
 +}
 +
 +static void
 +remove_orphaned_nodes(struct cache_table *table, struct cache_node *node,
 +    void *arg)
 +{
 +      if (file_stat_errno(node->path) == ENOENT) {
 +              pr_op_debug("Missing file; deleting node: %s", node->path);
 +              delete_node(table, node, NULL);
 +      }
 +}
 +
 +static void
 +remove_orphaned_files(void)
 +{
 +      // XXX
 +}
 +
 +/* Deletes obsolete files and nodes from the cache. */
 +static void
 +cleanup_cache(void)
 +{
 +      time_t now = time_fatal();
 +
 +      /* Delete the entirety of cache/tmp/. */
 +      pr_op_debug("Cleaning up temporal files.");
 +      file_rm_rf(CACHE_TMPDIR);
 +
 +      /*
 +       * Ensure valid RPPs and TAs are linked in fallback,
 +       * by hard-linking the new files.
 +       */
 +      pr_op_debug("Committing fallbacks.");
 +      commit_fallbacks(now);
 +
 +      /*
 +       * Delete refresh nodes that haven't been downloaded in a while,
 +       * and fallback nodes that haven't been valid in a while.
 +       */
 +      pr_op_debug("Cleaning up abandoned cache files.");
 +      foreach_node(remove_abandoned, &now);
 +
 +      /* (Paranoid) Delete nodes that are no longer mapped to files. */
 +      pr_op_debug("Cleaning up orphaned nodes.");
 +      foreach_node(remove_orphaned_nodes, NULL);
 +
 +      /* (Paranoid) Delete files that are no longer mapped to nodes. */
 +      pr_op_debug("Cleaning up orphaned files.");
 +      remove_orphaned_files();
 +}
 +
 +void
 +cache_commit(void)
 +{
 +      cleanup_cache();
 +      file_write_txt(METAFILE, "{ \"fort-version\": \"" PACKAGE_VERSION "\" }");
 +      unlock_cache();
 +      flush_nodes();
 +}
 +
 +void
 +exturis_init(struct extension_uris *uris)
 +{
 +      memset(uris, 0, sizeof(*uris));
 +}
 +
 +void
 +exturis_cleanup(struct extension_uris *uris)
 +{
 +      uri_cleanup(&uris->caRepository);
 +      uri_cleanup(&uris->rpkiNotify);
 +      uri_cleanup(&uris->rpkiManifest);
 +      uri_cleanup(&uris->crldp);
 +      uri_cleanup(&uris->caIssuers);
 +      uri_cleanup(&uris->signedObject);
 +}
diff --cc src/cache.h
index d78b125f230bd69adf93b979c00658f81265bdbc,0000000000000000000000000000000000000000..bebd072acff745308d6adcea4e6ba2a94d98e509
mode 100644,000000..100644
--- /dev/null
@@@ -1,58 -1,0 +1,55 @@@
- #include <stdbool.h>
 +#ifndef SRC_CACHE_LOCAL_CACHE_H_
 +#define SRC_CACHE_LOCAL_CACHE_H_
 +
- #include "types/map.h"
 +#include "common.h"
- #include "types/uri.h"
 +#include "types/rpp.h"
 +
 +int cache_setup1(void);
 +int cache_setup2(void);
 +void cache_atexit(void);
 +
 +int cache_prepare(void);      /* Prepare cache for new validation cycle */
 +void cache_commit(void);      /* Finish validation cycle */
 +
 +struct extension_uris {
 +      struct uri caRepository;        /* RPP cage */
 +      struct uri rpkiNotify;          /* RRDP Notification */
 +      struct uri rpkiManifest;
 +
 +      /**
 +       * CRL Distribution Points's fullName. Non-TA certificates only.
 +       * RFC 6487, section 4.8.6.
 +       */
 +      struct uri crldp;
 +      /**
 +       * AIA's caIssuers. Non-TA certificates only.
 +       * RFC 6487, section 4.8.7.
 +       */
 +      struct uri caIssuers;
 +      /**
 +       * SIA's signedObject. EE certificates only.
 +       * RFC 6487, section 4.8.8.2.
 +       */
 +      struct uri signedObject;
 +};
 +
 +void exturis_init(struct extension_uris *);
 +void exturis_cleanup(struct extension_uris *);
 +
 +validation_verdict cache_refresh_by_url(struct uri const *, char **);
 +validation_verdict cache_get_fallback(struct uri const *, char **);
 +
 +struct cache_cage;
 +validation_verdict cache_refresh_by_uris(struct extension_uris *,
 +    struct cache_cage **);
 +char *cage_map_file(struct cache_cage *, struct uri const *);
 +bool cage_downgrade(struct cache_cage *);
 +struct mft_meta const *cage_mft_fallback(struct cache_cage *);
 +void cache_commit_rpp(struct uri const *, struct uri const *, struct rpp *);
 +void cache_commit_file(struct cache_mapping const *);
 +
 +struct uri const *cage_rpkiNotify(struct cache_cage *);
 +
 +void cache_print(void);               /* Dump cache in stdout */
 +
 +#endif /* SRC_CACHE_LOCAL_CACHE_H_ */
diff --cc src/cachetmp.c
index b9cccc8d007bacf1b95e5a653e62a578be2552ac,0000000000000000000000000000000000000000..42ffa0c01baba83a487664f4d3fc25e15bd965ce
mode 100644,000000..100644
--- /dev/null
@@@ -1,31 -1,0 +1,29 @@@
- #include <stdio.h>
 +#include "cachetmp.h"
 +
 +#include <stdatomic.h>
- #include "types/path.h"
 +
 +#include "log.h"
 +
 +static atomic_uint file_counter;
 +
 +/*
 + * Returns (in @buf, which needs to length CACHE_TMPFILE_BUFLEN) a unique
 + * temporary file name in the local cache.
 + * Note, it's a name, and it's pretty much reserved. The file itself will not be
 + * created.
 + *
 + * The file will not be automatically deleted when it is closed or the program
 + * terminates.
 + */
 +void
 +cache_tmpfile(char *buf)
 +{
 +      unsigned int next;
 +      int written;
 +
 +      next = atomic_fetch_add(&file_counter, 1u);
 +
 +      written = snprintf(buf, CACHE_TMPFILE_BUFLEN, CACHE_TMPDIR "/%X", next);
 +      if (written >= CACHE_TMPFILE_BUFLEN)
 +              pr_crit("I ran out of temporal directories: %u", next);
 +}
diff --cc src/common.c
index 12383c7a078020cfaf3995e16a64650118d6b5f0,46deb10ab6d710badfab909b0d933dbd2a6b3aa9..0fbef3b1249310c0fb816d12373302d96b9ca8ba
@@@ -6,16 -3,14 +6,18 @@@
  #include <dirent.h>
  #include <errno.h>
  #include <limits.h>
++#include <stdlib.h>
+ #include <string.h>
  #include <time.h>
+ #include <unistd.h>
  
  #include "alloc.h"
--#include "config.h"
  #include "log.h"
  
 +validation_verdict const VV_CONTINUE = "Continue";
 +validation_verdict const VV_FAIL = "Failure";
 +validation_verdict const VV_BUSY = "Busy";
 +
  bool
  str_starts_with(char const *str, char const *prefix)
  {
diff --cc src/common.h
index 09eb8e43b8078190f40235a9c1b49db043a126fb,01c3a788731faff2b2622c92e591630c7b2ca30f..0d4d2bec449fc1df8454b4d384e0e8bcccb34a44
@@@ -1,9 -1,8 +1,11 @@@
  #ifndef SRC_RTR_COMMON_H_
  #define SRC_RTR_COMMON_H_
  
++#include <netinet/in.h>
  #include <pthread.h>
  #include <stdbool.h>
++#include <stddef.h>
 +#include <stdint.h>
  #include <sys/stat.h>
  #include <sys/types.h>
  
diff --cc src/config.c
index bfbf04883ccaa5a4404ea254518d9aa5f31780db,25a5c7676a517e9e3aee0a6d87f2f793313a4991..6bc99a6b923451f8db50c72c43a1e17c24ebc0de
@@@ -1,19 -1,17 +1,19 @@@
  #include "config.h"
  
- #include <curl/curl.h>
--#include <errno.h>
  #include <getopt.h>
  #include <libxml/xmlreader.h>
 +#include <limits.h>
+ #include <microhttpd.h>
  #include <openssl/opensslv.h>
- #include <stdlib.h>
++#include <sys/socket.h>
  #include <syslog.h>
  
 -#include "common.h"
 +#include "alloc.h"
  #include "config/boolean.h"
+ #include "config/curl_offset.h"
  #include "config/incidences.h"
  #include "config/str.h"
 +#include "config/time.h"
  #include "config/uint.h"
  #include "config/work_offline.h"
  #include "configure_ac.h"
  #include "init.h"
  #include "json_handler.h"
  #include "log.h"
 -#include "state.h"
 -#include "thread/thread_pool.h"
++#include "object/tal.h"
 +#include "thread_pool.h"
 +#include "types/array.h"
 +#include "types/path.h"
  
  /**
   * To add a member to this structure,
@@@ -119,31 -126,14 +124,30 @@@ struct rpki_config 
                unsigned int low_speed_limit;
                /* CURLOPT_LOW_SPEED_TIME for our HTTP transfers. */
                unsigned int low_speed_time;
-               /*
-                * CURLOPT_MAXFILESIZE, except it also works for unknown size
-                * files. (Though this is reactive, not preventive.)
-                */
-               unsigned int max_file_size;
+               /* CURLOPT_MAXFILESIZE_LARGE for our HTTP transfers. */
+               curl_off_t max_file_size;
                /* Directory where CA certs to verify peers are found */
                char *ca_path;
+               /* See CURLOPT_PROXY */
+               char *proxy;
        } http;
  
 +      struct {
 +              /*
 +               * Maximum deltas to explode per RRDP session, per iteration.
 +               *
 +               * (If the RRDP notification lists more than this amount of
 +               * unprocessed deltas, Fort will reset the session, exploding
 +               * the snapshot instead.)
 +               *
 +               * Per draft-spaghetti-sidrops-rrdp-desynchronization's
 +               * recommendation, this is also the maximum number of delta
 +               * hashes Fort will remember per RRDP session, to detect session
 +               * desynchronization.
 +               */
 +              unsigned int delta_threshold;
 +      } rrdp;
 +
        struct {
                /** Enables operation logs **/
                bool enabled;
@@@ -600,21 -610,17 +617,29 @@@ static const struct option_field option
                .offset = offsetof(struct rpki_config, http.ca_path),
                .doc = "Directory where CA certificates are found, used to verify the peer",
                .arg_doc = "<directory>",
-               .json_null_allowed = false,
+               .json_null_allowed = true,
+       }, {
+               .id = 9013,
+               .name = "http.proxy",
+               .type = &gt_string,
+               .offset = offsetof(struct rpki_config, http.proxy),
+               .doc = "Name of proxy to use",
+               .arg_doc = "<URI>",
+               .json_null_allowed = true,
        },
  
 +      /* RRDP */
 +      {
 +              .id = 10000,
 +              .name = "rrdp.delta-threshold",
 +              .type = &gt_uint,
 +              .offset = offsetof(struct rpki_config, rrdp.delta_threshold),
 +              .doc = "Maximum deltas to explode per RRDP session, per iteration. "
 +                     "(Fall back to snapshot if threshold exceeded.)",
 +              .min = 1,
 +              .max = 128,
 +      },
 +
        /* Logging fields */
        {
                .id = 4000,
@@@ -986,28 -986,35 +1014,31 @@@ set_default_values(void
        rpki_config.server.interval.expire = 7200;
        rpki_config.server.deltas_lifetime = 2;
  
+       rpki_config.prometheus.port = 0;
        rpki_config.rsync.enabled = true;
        rpki_config.rsync.priority = 50;
 +      rpki_config.rsync.max = 1;
        rpki_config.rsync.strategy = pstrdup("<deprecated>");
 -      rpki_config.rsync.retry.count = 1;
 -      rpki_config.rsync.retry.interval = 4;
        rpki_config.rsync.transfer_timeout = 900;
        rpki_config.rsync.program = pstrdup("rsync");
 -      string_array_init(&rpki_config.rsync.args.flat,
 -          flat_rsync_args, ARRAY_LEN(flat_rsync_args));
 -      string_array_init(&rpki_config.rsync.args.recursive,
 -          recursive_rsync_args, ARRAY_LEN(recursive_rsync_args));
 +      string_array_init(&rpki_config.rsync.args, trash, ARRAY_LEN(trash));
  
        rpki_config.http.enabled = true;
 -      /* Higher priority than rsync by default */
        rpki_config.http.priority = 60;
 -      rpki_config.http.retry.count = 1;
 -      rpki_config.http.retry.interval = 4;
        rpki_config.http.user_agent = pstrdup(PACKAGE_NAME "/" PACKAGE_VERSION);
        rpki_config.http.max_redirs = 10;
        rpki_config.http.connect_timeout = 30;
        rpki_config.http.transfer_timeout = 900;
        rpki_config.http.low_speed_limit = 100000;
        rpki_config.http.low_speed_time = 10;
-       rpki_config.http.max_file_size = 1000000000;
+       rpki_config.http.max_file_size = 2000000000;
        rpki_config.http.ca_path = NULL; /* Use system default */
+       rpki_config.http.proxy = NULL;
  
 +      /* TODO (fine) 64 may be too much; optimize it. */
 +      rpki_config.rrdp.delta_threshold = 64;
 +
        rpki_config.log.enabled = true;
        rpki_config.log.tag = NULL;
        rpki_config.log.color = false;
@@@ -1064,9 -1073,17 +1097,17 @@@ validate_config(void
            rpki_config.server.interval.retry)
                return pr_op_err("Expire interval must be greater than refresh and retry intervals");
  
 -      if (rpki_config.slurm != NULL && !valid_file_or_dir(rpki_config.slurm, true))
 +      if (rpki_config.slurm != NULL && !file_is_valid(rpki_config.slurm, true))
                return pr_op_err("Invalid slurm location.");
  
+       if (rpki_config.http.proxy == NULL) {
+               proxy = curl_getenv("https_proxy");
+               if (proxy == NULL)
+                       proxy = curl_getenv("HTTPS_PROXY");
+               if (proxy != NULL && proxy[0] != '\0')
+                       rpki_config.http.proxy = pstrdup(proxy);
+       }
        return 0;
  }
  
@@@ -1448,6 -1434,30 +1495,12 @@@ config_get_http_enabled(void
        return !rpki_config.work_offline && rpki_config.http.enabled;
  }
  
 -unsigned int
 -config_get_http_priority(void)
 -{
 -      return rpki_config.http.priority;
 -}
 -
 -unsigned int
 -config_get_http_retry_count(void)
 -{
 -      return rpki_config.http.retry.count;
 -}
 -
 -unsigned int
 -config_get_http_retry_interval(void)
 -{
 -      return rpki_config.http.retry.interval;
 -}
 -
+ char const *
+ config_get_http_proxy(void)
+ {
+       return rpki_config.http.proxy;
+ }
  char const *
  config_get_http_user_agent(void)
  {
diff --cc src/config.h
index f64bd78fbd17cbb7ba569f166e0bfef609839fc7,ee21f0d51d9fd90c278df62f858d04830b087850..68aaaf2c0b3a744b97d725e0924f573946e18f50
@@@ -5,7 -6,6 +6,8 @@@
  #include <netdb.h>
  #include <netinet/in.h>
  #include <stdint.h>
 +#include <sys/stat.h>
++#include <sys/types.h>
  
  #include "config/file_type.h"
  #include "config/filename_format.h"
@@@ -40,14 -40,20 +43,15 @@@ long config_get_http_connect_timeout(vo
  long config_get_http_transfer_timeout(void);
  long config_get_http_low_speed_limit(void);
  long config_get_http_low_speed_time(void);
long config_get_http_max_file_size(void);
curl_off_t config_get_http_max_file_size(void);
  char const *config_get_http_ca_path(void);
 +unsigned int config_get_rrdp_delta_threshold(void);
  bool config_get_rsync_enabled(void);
 -unsigned int config_get_rsync_priority(void);
 -unsigned int config_get_rsync_retry_count(void);
 -unsigned int config_get_rsync_retry_interval(void);
 -long config_get_rsync_transfer_timeout(void);
 -char *config_get_rsync_program(void);
 -struct string_array const *config_get_rsync_args(void);
 +unsigned int config_rsync_max(void);
 +long config_rsync_timeout(void);
 +char const *config_get_rsync_program(void);
  bool config_get_http_enabled(void);
 -unsigned int config_get_http_priority(void);
 -unsigned int config_get_http_retry_count(void);
 -unsigned int config_get_http_retry_interval(void);
+ char const *config_get_http_proxy(void);
  char const *config_get_output_roa(void);
  char const *config_get_output_bgpsec(void);
  enum output_format config_get_output_format(void);
index b05e62f5606966d8ebd3d14742f17f8e2fdb00cb,a8645e2dc3471e99e8bdc933b45c5be3453214e2..323df95ff48e05fdc58ae36733a5395f75372713
@@@ -5,8 -6,7 +6,7 @@@
  #include "alloc.h"
  #include "config/str.h"
  #include "log.h"
- #include "types/path.h"
 -#include "str_token.h"
 +#include "types/str.h"
  
  void
  string_array_init(struct string_array *array, char const *const *values,
index fc1deee8c4fd9e803df6e96799f1b29838b87bb6,0000000000000000000000000000000000000000..fcac29c67f18b74c74e0195133bbcaf6e78178a8
mode 100644,000000..100644
--- /dev/null
@@@ -1,55 -1,0 +1,54 @@@
- #include <errno.h>
 +#include "config/time.h"
 +
- #include <time.h>
 +#include <getopt.h>
++#include <string.h>
 +
 +#include "common.h"
 +#include "log.h"
 +
 +static void
 +print_time(struct option_field const *field, void *value)
 +{
 +      time_t tt;
 +      char str[FORT_TS_LEN];
 +      int error;
 +
 +      tt = *((time_t *)value);
 +      if (tt == 0)
 +              return;
 +
 +      error = time2str(tt, str);
 +      if (error)
 +              pr_crit("time2str: %s", strerror(error));
 +
 +      pr_op_info("%s: %s", field->name, str);
 +}
 +
 +static int
 +parse_argv_time(struct option_field const *field, char const *str,
 +    void *result)
 +{
 +      if (str == NULL || strlen(str) == 0)
 +              return pr_op_err("--%s needs an argument.", field->name);
 +
 +      return str2time(str, result);
 +}
 +
 +static int
 +parse_json_time(struct option_field const *opt, json_t *json, void *result)
 +{
 +      if (!json_is_string(json))
 +              return pr_op_err("The '%s' element is not a JSON string.",
 +                  opt->name);
 +
 +      return str2time(json_string_value(json), result);
 +}
 +
 +const struct global_type gt_time = {
 +      .has_arg = required_argument,
 +      .size = sizeof(time_t),
 +      .print = print_time,
 +      .parse.argv = parse_argv_time,
 +      .parse.json = parse_json_time,
 +      .arg_doc = FORT_TS_FORMAT,
 +};
Simple merge
diff --cc src/ext.c
index d4dab681208bd8d3479183910c541cf7f03e20f1,72531a36404cf613d299614f7d769f9ed20fea7b..39221be353ce418319048441a85dbe3903494d32
+++ b/src/ext.c
@@@ -3,8 -3,8 +3,11 @@@
  #include <openssl/asn1t.h>
  #include <openssl/obj_mac.h>
  #include <openssl/objects.h>
++#include <sys/socket.h>
  
 -#include "crypto/hash.h"
++#include "asn1/asn1c/ASIdentifiers.h"
++#include "asn1/asn1c/IPAddressFamily.h"
 +#include "hash.h"
  #include "json_util.h"
  #include "libcrypto_util.h"
  #include "log.h"
diff --cc src/file.c
index a46a9bd1768599396449c27e65f632793a6e5230,29b1dce9168dfcea1994f0cff9bcd98f14112bcf..a2a349f1564f41ab1c3cbb9232c7b8bd92f9775f
@@@ -3,11 -3,8 +3,9 @@@
  #include <ftw.h>
  
  #include "alloc.h"
 +#include "common.h"
  #include "config/mode.h"
  #include "log.h"
- #include "types/path.h"
  
  int
  file_open(char const *file_name, FILE **result, struct stat *stat)
diff --cc src/file.h
index 8041c6958828b27718510b8eed6c7c59071b95de,f334dd222dc4b87dca0554dbacdfdf3577bf8f80..f8058a06689ebf19485b8854a8cc80c344d81e6c
@@@ -3,12 -3,9 +3,10 @@@
  
  #include <dirent.h>
  #include <errno.h>
 +#include <stdbool.h>
  #include <stddef.h>
  #include <stdio.h>
- #include <stdlib.h>
  #include <string.h>
- #include <strings.h>
  #include <sys/stat.h>
  #include <sys/types.h>
  #include <unistd.h>
diff --cc src/hash.c
index cb7a7a4259314cc68f99dbeccd3ad1e828d66af4,0000000000000000000000000000000000000000..d27f6b64e0a162f7acbc4a50f19e9b073c89ea1c
mode 100644,000000..100644
--- /dev/null
@@@ -1,251 -1,0 +1,252 @@@
 +#include "hash.h"
 +
 +#include <openssl/evp.h>
++#include <stdlib.h>
 +
 +#include "alloc.h"
 +#include "file.h"
 +#include "log.h"
 +
 +/*
 + * TODO (fine) Delete this structure (use md directly) once OpenSSL < 3 support
 + * is dropped.
 + */
 +struct hash_algorithm {
 +#if OPENSSL_VERSION_NUMBER >= 0x30000000L
 +      EVP_MD *md;
 +#else
 +      EVP_MD const *md;
 +#endif
 +      size_t size;
 +      char const *name;
 +};
 +
 +/*
 + * EVP_sha256() and EVP_sha1() are now mildly deprecated ("present for
 + * compatibility with OpenSSL before version 3.0").
 + *
 + * This is because they want to encourage explicit fetching, but also because
 + * they want us to stop hardcoding the algorithms in the code.
 + *
 + * But we're RFC-bound to use these algorithms, so we only want the explicit
 + * fetching part. (Which is done during hash_setup().)
 + */
 +static struct hash_algorithm sha1;
 +static struct hash_algorithm sha256;
 +
 +int
 +hash_setup(void)
 +{
 +#if OPENSSL_VERSION_NUMBER >= 0x30000000L
 +      sha1.md = EVP_MD_fetch(NULL, "SHA1", NULL);
 +      if (sha1.md == NULL)
 +              return pr_op_err("This version of libcrypto does not seem to support SHA1.");
 +      sha1.size = EVP_MD_get_size(sha1.md);
 +      sha1.name = EVP_MD_get0_name(sha1.md);
 +
 +      sha256.md = EVP_MD_fetch(NULL, "SHA256", NULL);
 +      if (sha256.md == NULL) {
 +              EVP_MD_free(sha1.md);
 +              return pr_op_err("This version of libcrypto does not seem to support SHA256.");
 +      }
 +      sha256.size = EVP_MD_get_size(sha256.md);
 +      sha256.name = EVP_MD_get0_name(sha256.md);
 +
 +#else
 +      sha1.md = EVP_get_digestbyname("sha1");
 +      if (sha1.md == NULL)
 +              return pr_op_err("This version of libcrypto does not seem to support SHA1.");
 +      sha1.size = EVP_MD_size(sha1.md);
 +      sha1.name = EVP_MD_name(sha1.md);
 +
 +      sha256.md = EVP_get_digestbyname("sha256");
 +      if (sha256.md == NULL)
 +              return pr_op_err("This version of libcrypto does not seem to support SHA256.");
 +      sha256.size = EVP_MD_size(sha256.md);
 +      sha256.name = EVP_MD_name(sha256.md);
 +
 +#endif
 +
 +      return 0;
 +}
 +
 +void
 +hash_teardown(void)
 +{
 +#if OPENSSL_VERSION_NUMBER >= 0x30000000L
 +      EVP_MD_free(sha256.md);
 +      EVP_MD_free(sha1.md);
 +#endif
 +}
 +
 +struct hash_algorithm const *
 +hash_get_sha1(void)
 +{
 +      return &sha1;
 +}
 +
 +struct hash_algorithm const *
 +hash_get_sha256(void)
 +{
 +      return &sha256;
 +}
 +
 +int
 +hash_file(struct hash_algorithm const *algorithm, char const *filename,
 +    unsigned char *result, size_t *result_size)
 +{
 +      FILE *file;
 +      struct stat stat;
 +      unsigned char *buffer;
 +      size_t consumed;
 +      EVP_MD_CTX *ctx;
 +      unsigned int hash_size;
 +      int error;
 +
 +      error = file_open(filename, &file, &stat);
 +      if (error)
 +              return error;
 +
 +      buffer = pmalloc(stat.st_blksize);
 +
 +      ctx = EVP_MD_CTX_new();
 +      if (ctx == NULL)
 +              enomem_panic();
 +
 +      if (!EVP_DigestInit_ex(ctx, algorithm->md, NULL)) {
 +              error = val_crypto_err("EVP_DigestInit_ex() failed");
 +              goto end;
 +      }
 +
 +      do {
 +              consumed = fread(buffer, 1, stat.st_blksize, file);
 +              error = ferror(file);
 +              if (error) {
 +                      pr_val_err("File reading error. Error message (apparently): %s",
 +                         strerror(error));
 +                      goto end;
 +              }
 +
 +              if (!EVP_DigestUpdate(ctx, buffer, consumed)) {
 +                      error = val_crypto_err("EVP_DigestUpdate() failed");
 +                      goto end;
 +              }
 +
 +      } while (!feof(file));
 +
 +      if (!EVP_DigestFinal_ex(ctx, result, &hash_size)) {
 +              error = val_crypto_err("EVP_DigestFinal_ex() failed");
 +              goto end;
 +      }
 +      if (hash_size != algorithm->size) {
 +              error = pr_op_err("libcrypto returned a %s hash sized %u bytes.",
 +                  algorithm->name, hash_size);
 +      }
 +
 +      if (result_size)
 +              *result_size = hash_size;
 +
 +end:
 +      EVP_MD_CTX_free(ctx);
 +      free(buffer);
 +      file_close(file);
 +      return error;
 +}
 +
 +int
 +hash_validate_file(struct hash_algorithm const *algorithm, char const *path,
 +    unsigned char const *expected, size_t expected_len)
 +{
 +      unsigned char actual[EVP_MAX_MD_SIZE];
 +      size_t actual_len;
 +      int error;
 +
 +      pr_clutter("Validating file hash: %s", path);
 +
 +      if (expected_len != hash_get_size(algorithm))
 +              return pr_val_err("%s string has bogus size: %zu",
 +                  hash_get_name(algorithm), expected_len);
 +
 +      error = hash_file(algorithm, path, actual, &actual_len);
 +      if (error)
 +              return error;
 +
 +      if (expected_len != actual_len)
 +              goto fail;
 +      if (memcmp(expected, actual, expected_len) != 0)
 +              goto fail;
 +
 +      return 0;
 +
 +fail:
 +      error = pr_val_err("File '%s' does not match its expected hash.", path);
 +#ifdef UNIT_TESTING
 +      size_t i;
 +      printf("Expected: ");
 +      for (i = 0; i < expected_len; i++)
 +              printf("%02x", expected[i]);
 +      printf("\nActual:   ");
 +      for (i = 0; i < actual_len; i++)
 +              printf("%02x", actual[i]);
 +      printf("\n");
 +#endif
 +      return error;
 +}
 +
 +static int
 +hash_buffer(struct hash_algorithm const *algorithm,
 +    unsigned char const *content, size_t content_len, unsigned char *hash)
 +{
 +      EVP_MD_CTX *ctx;
 +      unsigned int actual_len;
 +
 +      ctx = EVP_MD_CTX_new();
 +      if (ctx == NULL)
 +              enomem_panic();
 +
 +      if (!EVP_DigestInit_ex(ctx, algorithm->md, NULL) ||
 +          !EVP_DigestUpdate(ctx, content, content_len) ||
 +          !EVP_DigestFinal_ex(ctx, hash, &actual_len)) {
 +              EVP_MD_CTX_free(ctx);
 +              return val_crypto_err("Buffer hashing failed");
 +      }
 +
 +      EVP_MD_CTX_free(ctx);
 +
 +      if (actual_len != algorithm->size)
 +              pr_crit("libcrypto returned a %s hash sized %u bytes.",
 +                  algorithm->name, actual_len);
 +
 +      return 0;
 +}
 +
 +int
 +hash_validate(struct hash_algorithm const *algorithm, unsigned char const *data,
 +    size_t data_len, unsigned char const *expected, size_t expected_len)
 +{
 +      unsigned char actual[EVP_MAX_MD_SIZE];
 +      int error;
 +
 +      error = hash_buffer(algorithm, data, data_len, actual);
 +      if (error)
 +              return -error;
 +
 +      if (expected_len != algorithm->size)
 +              return EINVAL;
 +      if (memcmp(expected, actual, expected_len) != 0)
 +              return EINVAL;
 +
 +      return 0;
 +}
 +
 +char const *
 +hash_get_name(struct hash_algorithm const *algorithm)
 +{
 +      return algorithm->name;
 +}
 +
 +size_t
 +hash_get_size(struct hash_algorithm const *algorithm)
 +{
 +      return algorithm->size;
 +}
diff --cc src/hash.h
index 386ecb413bc8cfa04a2855103f873255ce20172d,0000000000000000000000000000000000000000..1269b3b1c721bfca423afc01840dbedab948e3f7
mode 100644,000000..100644
--- /dev/null
@@@ -1,31 -1,0 +1,25 @@@
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <strings.h>
- #include <sys/types.h>
- #include <unistd.h>
 +#ifndef SRC_HASH_H_
 +#define SRC_HASH_H_
 +
 +#include <stddef.h>
 +
 +struct hash_algorithm;
 +
 +int hash_setup(void);
 +void hash_teardown(void);
 +
 +struct hash_algorithm const *hash_get_sha1(void);
 +struct hash_algorithm const *hash_get_sha256(void);
 +
 +int hash_file(struct hash_algorithm const *, char const *, unsigned char *,
 +    size_t *);
 +
 +int hash_validate_file(struct hash_algorithm const *, char const *,
 +    unsigned char const *, size_t);
 +int hash_validate(struct hash_algorithm const *, unsigned char const *, size_t,
 +    unsigned char const *, size_t);
 +
 +char const *hash_get_name(struct hash_algorithm const *);
 +size_t hash_get_size(struct hash_algorithm const *);
 +
 +#endif /* SRC_HASH_H_ */
diff --cc src/http.c
index 17e5d70fef5df10e0275b74c3964a62d1d7d2aa0,2bd31519ea133045a8aab123e21c64317b4f402b..cf4c98e0bb53dfedf3a76e463e0e6472ffd9f8ee
@@@ -1,7 -1,7 +1,5 @@@
 -#include "http/http.h"
 +#include "http.h"
  
- #include "alloc.h"
 -#include "cache/local_cache.h"
--#include "common.h"
  #include "config.h"
  #include "file.h"
  #include "log.h"
diff --cc src/http.h
index 8f352d74cbcc0dba03a71efca5444152349bd197,0000000000000000000000000000000000000000..7d18cfb103b5258f4b8a763a2844243d7598bf64
mode 100644,000000..100644
--- /dev/null
@@@ -1,14 -1,0 +1,13 @@@
- #include <stdbool.h>
 +#ifndef SRC_HTTP_H_
 +#define SRC_HTTP_H_
 +
 +#include <curl/curl.h>
 +
 +#include "types/uri.h"
 +
 +int http_init(void);
 +void http_cleanup(void);
 +
 +int http_download(struct uri const *, char const *, curl_off_t, bool *);
 +
 +#endif /* SRC_HTTP_H_ */
diff --cc src/init.c
index b2bafbc7f26b54c5c3402cd40f89f9e80357bf49,a4d7e7e67481b84c7312675fc3bc689e49950edc..924315e2eb3e873b37f6de4193b6492f1179e5e4
@@@ -1,9 -1,8 +1,11 @@@
  #include "init.h"
  
 +#include <errno.h>
++#include <stdio.h>
++
  #include "config.h"
 -#include "data_structure/path_builder.h"
 -#include "http/http.h"
 +#include "http.h"
 +#include "types/path.h"
  
  static int
  fetch_url(char const *url, char const *filename)
Simple merge
diff --cc src/json_util.c
index af9e0ce050502486cfc0e3c94d1f353f4723a256,58d9f2a11774bea5b56d83e373ed130b4672bea0..aa557e8bb20483c9a245c9f658406326bfb0f807
@@@ -1,13 -1,19 +1,10 @@@
  #include "json_util.h"
  
  #include <errno.h>
--#include <limits.h>
 -#include <string.h>
--#include <time.h>
  
- #include "alloc.h"
 +#include "common.h"
  #include "log.h"
  
 -/*
 - * Careful with this; several of the conversion specification characters
 - * documented in the Linux man page are not actually portable.
 - */
 -#define JSON_TS_FORMAT "%Y-%m-%dT%H:%M:%SZ"
 -#define JSON_TS_LEN 21 /* strlen("YYYY-mm-ddTHH:MM:SSZ") + 1 */
 -
  int
  json_get_str(json_t *parent, char const *name, char const **result)
  {
diff --cc src/json_util.h
index 3e08b75bd1d4163d305d2771ecbf404ec781d235,dcd22dbf9d421732900fec76a2cc9ea8ec54cf1b..08d820784263e57479ecaed923b89d44c90e4b0f
@@@ -1,24 -1,15 +1,11 @@@
  #ifndef SRC_JSON_UTIL_H_
  #define SRC_JSON_UTIL_H_
  
--#include <arpa/inet.h>
--#include <jansson.h>
  #include <netdb.h>
--#include <netinet/in.h>
--#include <stdbool.h>
--#include <stddef.h>
--#include <stdint.h>
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <strings.h>
  #include <sys/stat.h>
--#include <sys/types.h>
- #include <unistd.h>
 +
 +#include "asn1/asn1c/INTEGER.h"
- #include "file.h"
 +#include "types/uri.h"
  
  /*
   * Contract of get functions:
index f4e60d2845faa2340823d0778202c006cc924366,74437c55711aecefbbd8971c349c497507d068da..bc2ada0c6fb8b2b5568801173d9475711328d912
@@@ -7,9 -7,9 +7,8 @@@
  #include <openssl/pem.h>
  #include <time.h>
  
--#include "alloc.h"
  #include "asn1/asn1c/OBJECT_IDENTIFIER.h"
 -#include "extension.h"
 +#include "ext.h"
  #include "json_util.h"
  #include "log.h"
  
diff --cc src/log.c
index f63ef23f8ba1b92d6fafb6d0224048e70b93e5c5,ecff284eb3a192ac63711c4f48b5c5e789bf3168..24314041b2f6a38ac65bd6002abbbb348cce5cb0
+++ b/src/log.c
@@@ -6,9 -6,11 +6,10 @@@
  #endif
  #include <openssl/err.h>
  #include <pthread.h>
 -#include <signal.h>
  #include <stdarg.h>
 -#include <sys/stat.h>
  #include <syslog.h>
  #include <time.h>
++#include <unistd.h>
  
  #include "config.h"
  #include "thread_var.h"
diff --cc src/main.c
index 5a3eaaebeff4937eb6e43d3403f0e0317348866f,84cc2d58e3e2b0c39125ff49854f966b3970b26e..cb0152624dca8324207469f861e290a1f8ae8799
@@@ -1,21 -1,16 +1,23 @@@
  #include <errno.h>
  
 +#include "cache.h"
  #include "config.h"
 -#include "extension.h"
 -#include "http/http.h"
 +#include "ext.h"
 +#include "hash.h"
 +#include "http.h"
  #include "log.h"
  #include "nid.h"
 +#include "output_printer.h"
  #include "print_file.h"
+ #include "prometheus.h"
 +#include "relax_ng.h"
++#include "rsync.h"
 +#include "rtr/db/vrps.h"
  #include "rtr/rtr.h"
- #include "rsync.h"
 +#include "sig.h"
+ #include "stats.h"
 +#include "task.h"
  #include "thread_var.h"
 -#include "xml/relax_ng.h"
  
  static int
  fort_standalone(void)
@@@ -66,10 -59,11 +66,10 @@@ fort_server(void
                pr_op_info("Main loop: Time to work!");
  
                error = vrps_update(&changed);
 -              if (error == -EINTR)
 +              if (error == EINTR)
                        break;
                if (error) {
-                       pr_op_debug("Main loop: %s", strerror(error));
 -                      pr_op_debug("Main loop: Error %d (%s)", error,
 -                          strerror(abs(error)));
++                      pr_op_debug("Main loop: %s", strerror(abs(error)));
                        continue;
                }
                if (changed)
@@@ -119,28 -113,29 +119,34 @@@ main(int argc, char **argv
        int error;
  
        /* Initializations */
 +      /* (Do not start any threads until after rsync_setup() has forked.) */
  
 -      error = log_setup(false);
 +      error = log_setup();
        if (error)
                goto just_quit;
 -
 -      error = thvar_init();
 +      error = handle_flags_config(argc, argv);
        if (error)
                goto revert_log;
 -      error = incidence_init();
 +
 +      error = cache_setup1();
        if (error)
 -              goto revert_log;
 -      error = handle_flags_config(argc, argv);
 +              goto revert_config;
 +
 +      rsync_setup(NULL, NULL); /* Fork rsync spawner ASAP */
 +      register_signal_handlers();
 +
 +      error = thvar_init();
        if (error)
 -              goto revert_log;
 +              goto revert_rsync;
-       error = nid_init();
+       error = stats_setup();
        if (error)
 -              goto revert_config;
 +              goto revert_rsync;
+       error = prometheus_setup();
+       if (error)
+               goto revert_stats;
+       error = nid_init();
+       if (error)
+               goto revert_prometheus;
        error = extension_init();
        if (error)
                goto revert_nid;
@@@ -186,12 -171,12 +192,16 @@@ revert_relax_ng
        relax_ng_cleanup();
  revert_http:
        http_cleanup();
 +revert_hash:
 +      hash_teardown();
  revert_nid:
        nid_destroy();
+ revert_prometheus:
+       prometheus_teardown();
+ revert_stats:
+       stats_teardown();
 +revert_rsync:
 +      rsync_teardown();
  revert_config:
        free_rpki_config();
  revert_log:
index 0eba44bb03da6119ad270b3d8412af0f1ffce469,8f5a45d6cf21bcbe249175e01e01a4cc56aa600e..f81a019161cb06142add9be51f16a66e938304b2
@@@ -2,7 -2,7 +2,7 @@@
  
  #include "log.h"
  #include "object/certificate.h"
--#include "validation_handler.h"
++#include "types/router_key.h"
  
  struct resource_params {
        unsigned char const *ski;
index 870796599b88802855784c939b61dab34a7f3198,731c9b82c03fbaaa2b857700601fb95a60033c5e..7e4b416a48a29c428594ac606a7ce9235efe6ad3
@@@ -8,30 -8,21 +8,23 @@@
  #include <openssl/obj_mac.h>
  #include <openssl/objects.h>
  #include <openssl/rsa.h>
--#include <syslog.h>
--#include <time.h>
  
  #include "algorithm.h"
  #include "asn1/asn1c/IPAddrBlocks.h"
  #include "asn1/decode.h"
- #include "cache.h"
--#include "common.h"
  #include "config.h"
 -#include "extension.h"
 +#include "ext.h"
  #include "libcrypto_util.h"
  #include "log.h"
  #include "nid.h"
 +#include "object/ghostbusters.h"
  #include "object/manifest.h"
 -#include "str_token.h"
 +#include "object/roa.h"
 +#include "object/signed_object.h"
- #include "object/tal.h"
 +#include "task.h"
  #include "thread_var.h"
- #include "types/path.h"
 +#include "types/name.h"
- #include "types/uri.h"
 +#include "types/str.h"
  
  /*
   * The X509V3_EXT_METHOD that references NID_sinfo_access uses the AIA item.
@@@ -108,29 -154,70 +101,63 @@@ validate_signature_algorithm(X509 *cert
        return validate_certificate_signature_algorithm(nid, "Certificate");
  }
  
+ static bool
+ is_valid_printable_string_char(char c)
+ {
+       if ('A' <= c && c <= 'Z')
+               return true;
+       if ('a' <= c && c <= 'z')
+               return true;
+       if ('0' <= c && c <= '9')
+               return true;
+       if (c == ' ')
+               return true;
+       if ('\'' <= c && c <= ')')
+               return true;
+       if ('+' <= c && c <= '/')
+               return true;
+       if (c == ':' || c == '=' || c == '?')
+               return true;
+       return false;
+ }
+ static int
+ validate_printable_string(char const *str, char const *what)
+ {
+       for (; *str != '\0'; str++)
+               if (!is_valid_printable_string_char(*str))
+                       return pr_val_err("Invalid character in '%s' PrintableString: 0x%X",
+                           what, *str);
+       return 0;
+ }
  static int
 -validate_issuer(X509 *cert, bool is_ta)
 +validate_issuer(struct rpki_certificate *cert)
  {
        X509_NAME *issuer;
        struct rfc5280_name *name;
+       char const *commonName;
        int error;
  
 -      issuer = X509_get_issuer_name(cert);
 -
 -      if (!is_ta)
 -              return validate_issuer_name("Certificate", issuer);
 +      issuer = X509_get_issuer_name(cert->x509);
  
 -      /* TODO wait. Shouldn't we check subject == issuer? */
 +      if (cert->type != CERTYPE_TA)
 +              return validate_issuer_name(issuer, cert->parent->x509);
  
        error = x509_name_decode(issuer, "issuer", &name);
        if (error)
                return error;
-       pr_clutter("Issuer: %s", x509_name_commonName(name));
-       x509_name_put(name);
  
-       return 0;
+       commonName = x509_name_commonName(name);
 -      pr_val_debug("Issuer: %s", commonName);
++      pr_clutter("Issuer: %s", commonName);
+       error = validate_printable_string(commonName, "Issuer");
+       x509_name_put(name);
+       return error;
  }
  
 -/*
 - * Compare public keys, call @diff_alg_cb when the algorithm is different, call
 - * @diff_pk_cb when the public key is different; return 0 if both are equal.
 - */
  static int
 -spki_cmp(X509_PUBKEY *tal_spki, X509_PUBKEY *cert_spki,
 -    int (*diff_alg_cb)(void), int (*diff_pk_cb)(void))
 +spki_cmp(X509_PUBKEY *tal_spki, X509_PUBKEY *cert_spki)
  {
        ASN1_OBJECT *tal_alg;
        ASN1_OBJECT *cert_alg;
@@@ -178,7 -261,10 +206,10 @@@ validate_subject(X509 *cert
        error = x509_name_decode(X509_get_subject_name(cert), "subject", &name);
        if (error)
                return error;
-       pr_clutter("Subject: %s", x509_name_commonName(name));
+       commonName = x509_name_commonName(name);
 -      pr_val_debug("Subject: %s", commonName);
++      pr_clutter("Subject: %s", commonName);
+       error = validate_printable_string(commonName, "Subject");
  
        x509_name_put(name);
        return error;
index db3c704ba05039fa7cfcc23240d9dff0156bbd45,37413f5244cb04ae3007dd822e9e9e05da8808e8..7a3e2e35dadd76d7b9a305e88609328306838f6c
@@@ -1,15 -1,10 +1,13 @@@
  #ifndef SRC_OBJECT_CERTIFICATE_H_
  #define SRC_OBJECT_CERTIFICATE_H_
  
- #include <openssl/x509.h>
 -#include "asn1/asn1c/ANY.h"
 -#include "asn1/asn1c/SignatureValue.h"
 -#include "certificate_refs.h"
 +#include <stdatomic.h>
 +#include <sys/queue.h>
 +
- #include "asn1/asn1c/ANY.h"
- #include "asn1/asn1c/SignatureValue.h"
++#include "asn1/signed_data.h"
 +#include "cache.h"
++#include "object/tal.h"
  #include "resource.h"
- #include "types/rpp.h"
  
  /* Certificate types in the RPKI */
  enum cert_type {
index 98e49f3bf72929456b42b50e4f0b434d1a2d1e80,62c5efdd1faef3d9d2971f4aee6f8617e303fe74..f5e5ae0b5522f1332049b31ccfacf94e91ca4772
@@@ -2,16 -2,15 +2,15 @@@
  
  #include <openssl/bio.h>
  #include <openssl/bn.h>
--#include <syslog.h>
  
  #include "algorithm.h"
 -#include "extension.h"
 +#include "ext.h"
  #include "log.h"
 -#include "object/name.h"
 +#include "thread_var.h"
 +#include "types/name.h"
  
  static int
 -__crl_load(struct rpki_uri *uri, X509_CRL **result)
 +__crl_load(char const *path, X509_CRL **result)
  {
        X509_CRL *crl;
        BIO *bio;
index 04240ff3d01d83f5dbe648b94454d214d6031e45,7aacffdf4b362418c093a08fd5b763b131fc5719..a47f098dd64619dcad73ce1781c39c7aa1e3f259
@@@ -2,8 -2,9 +2,9 @@@
  #define SRC_OBJECT_CRL_H_
  
  #include <openssl/x509.h>
 -#include "types/uri.h"
 +#include "types/map.h"
  
 -int crl_load(struct rpki_uri *uri, X509_CRL **);
 +int crl_load(struct cache_mapping *, X509 *, X509_CRL **);
  
  #endif /* SRC_OBJECT_CRL_H_ */
index 900e66b750c07024def45ffe4d961f8b735e7705,2aae99123fc5a391307594943aa60796aca508df..bd49617f2fae90a94409c8aa4c9e9f2cebb2ff9b
@@@ -1,8 -1,6 +1,6 @@@
  #include "object/ghostbusters.h"
  
- #include <errno.h>
--#include "log.h"
++#include "object/certificate.h"
  #include "object/signed_object.h"
  #include "object/vcard.h"
  #include "thread_var.h"
index 39350737e479881a5615a4a63a77882faadb024b,84087b75ebdb4aa838d21ec0c89f9bf6f2cd6dc4..86430da537a915356fc7e2eacc1c53cc9490b52f
@@@ -1,8 -1,8 +1,9 @@@
  #ifndef SRC_OBJECT_GHOSTBUSTERS_H_
  #define SRC_OBJECT_GHOSTBUSTERS_H_
  
- #include "object/certificate.h"
 -#include "rpp.h"
++#include "asn1/signed_data.h"
++#include "types/map.h"
  
 -int ghostbusters_traverse(struct rpki_uri *, struct rpp *);
 +int ghostbusters_traverse(struct cache_mapping *, struct rpki_certificate *);
  
  #endif /* SRC_OBJECT_GHOSTBUSTERS_H_ */
index 514db9bcaa8445deb1d7927c2144d630022f85bb,8e404aa78f38cd48fd98dbf6057b7af021cc6e98..1a132306593dddd07a49757ad2d2056825132f34
@@@ -4,24 -1,32 +4,21 @@@
  #include "object/manifest.h"
  
  #include "algorithm.h"
- #include "alloc.h"
  #include "asn1/asn1c/Manifest.h"
  #include "asn1/decode.h"
- #include "common.h"
 -#include "crypto/hash.h"
 +#include "config.h"
 +#include "hash.h"
  #include "log.h"
++#include "object/certificate.h"
 +#include "object/crl.h"
  #include "object/signed_object.h"
  #include "thread_var.h"
- #include "types/path.h"
- #include "types/uri.h"
  
  static int
 -cage(struct rpki_uri **uri, struct rpki_uri *notif)
 -{
 -      if (notif == NULL) {
 -              /* No need to cage */
 -              uri_refget(*uri);
 -              return 0;
 -      }
 -
 -      return uri_create_caged(uri,
 -          tal_get_file_name(validation_tal(state_retrieve())), notif,
 -          uri_get_global(*uri), uri_get_global_len(*uri));
 -}
 -
 -static int
 -decode_manifest(struct signed_object *sobj, struct Manifest **result)
 +decode_manifest(struct signed_object *so, struct Manifest **result)
  {
        return asn1_decode_octet_string(
 -              sobj->sdata->encapContentInfo.eContent,
 +              so->sdata->encapContentInfo.eContent,
                &asn_DEF_Manifest,
                (void **) result,
                true
index 58be295df07634b01a7b6756f3fb799232298a8e,fa55a6f32bd6c2f80c6ac2197f50ed10db290681..2bd224f9360437bb6f40093a4d2c0721744fcd39
@@@ -1,13 -1,8 +1,10 @@@
  #ifndef SRC_OBJECT_MANIFEST_H_
  #define SRC_OBJECT_MANIFEST_H_
  
- #include <openssl/sha.h>
- #include <openssl/x509.h>
 -#include "rpp.h"
++#include "asn1/signed_data.h"
 +#include "cache.h"
- #include "object/certificate.h"
  
 -int handle_manifest(struct rpki_uri *, struct rpki_uri *, struct rpp **);
 +int manifest_traverse(struct cache_mapping const *, struct cache_cage *,
 +    struct rpki_certificate *);
  
  #endif /* SRC_OBJECT_MANIFEST_H_ */
index 1fcfe0f10a40c6df510c89ab67cfe5910c6487be,0b4067abe3c000a010063435b6bd1e33a323f4a2..32a4693790fb44220ea53abf9536437b9d29d512
@@@ -3,15 -3,14 +3,16 @@@
  #include "asn1/asn1c/RouteOriginAttestation.h"
  #include "asn1/decode.h"
  #include "log.h"
++#include "object/certificate.h"
  #include "object/signed_object.h"
  #include "thread_var.h"
 +#include "validation_handler.h"
  
  static int
 -decode_roa(struct signed_object *sobj, struct RouteOriginAttestation **result)
 +decode_roa(struct signed_object *so, struct RouteOriginAttestation **result)
  {
        return asn1_decode_octet_string(
 -              sobj->sdata->encapContentInfo.eContent,
 +              so->sdata->encapContentInfo.eContent,
                &asn_DEF_RouteOriginAttestation,
                (void **) result,
                true
index 9642f3cc935b8bfd518c2b7ae41078c6129129e3,3efb97a35f13e11f194317324568fcdc96216f61..125684aff09491fb300c8ff3afc2484d6a0cd8b9
@@@ -1,8 -1,8 +1,9 @@@
  #ifndef SRC_OBJECT_ROA_H_
  #define SRC_OBJECT_ROA_H_
  
- #include "object/certificate.h"
 -#include "rpp.h"
++#include "asn1/signed_data.h"
++#include "types/map.h"
  
 -int roa_traverse(struct rpki_uri *, struct rpp *);
 +int roa_traverse(struct cache_mapping *, struct rpki_certificate *);
  
  #endif /* SRC_OBJECT_ROA_H_ */
Simple merge
index 6d8657cedb09a79de735d6c0d5c3759a4571ddcc,89155dd5e1bbeef1453fa577289f8448d69442ae..98fea480fe321a97a5c55854c10c2b5c871ce8c2
@@@ -2,24 -2,17 +2,24 @@@
  #define SRC_OBJECT_SIGNED_OBJECT_H_
  
  #include "asn1/asn1c/ContentInfo.h"
- #include "asn1/asn1c/SignedData.h"
  #include "asn1/oid.h"
+ #include "asn1/signed_data.h"
 +#include "types/map.h"
  
  struct signed_object {
 +      struct cache_mapping const *map;
        struct ContentInfo *cinfo;
        struct SignedData *sdata;
 +      OCTET_STRING_t const *sid;
 +      SignatureValue_t const *signature;
  };
  
 -int signed_object_decode(struct signed_object *, struct rpki_uri *);
 -int signed_object_validate(struct signed_object *, struct oid_arcs const *,
 -    struct ee_cert *);
 +int signed_object_decode(struct signed_object *, struct cache_mapping const *);
 +
 +struct rpki_certificate;
 +int signed_object_validate(struct signed_object *, struct rpki_certificate *,
 +    struct oid_arcs const *);
 +
  void signed_object_cleanup(struct signed_object *);
  
  #endif /* SRC_OBJECT_SIGNED_OBJECT_H_ */
index 5e6f0bcc911efbd28ad759883d82e3f31263ab3b,335157480165974ff50bfa15c6b97abced0241bf..819e2db26bf8e5525f50ad59a1116888755531eb
@@@ -1,21 -1,19 +1,15 @@@
  #include "object/tal.h"
  
 -#include <errno.h>
 -#include <openssl/evp.h>
 -#include <sys/queue.h>
 -#include <time.h>
 +#include <ctype.h>
- #include <sys/queue.h>
- #include <time.h>
  
 -#include "common.h"
 +#include "base64.h"
- #include "cache.h"
- #include "common.h"
  #include "config.h"
 -#include "crypto/base64.h"
 -#include "line_file.h"
 +#include "file.h"
  #include "log.h"
 -#include "stats.h"
 +#include "object/certificate.h"
 +#include "task.h"
  #include "thread_var.h"
 -
 -typedef int (*foreach_uri_cb)(struct tal *, struct rpki_uri *, void *);
 +#include "types/path.h"
- #include "types/str.h"
- #include "types/uri.h"
  
  struct tal {
        char const *file_name;
@@@ -144,185 -327,231 +138,191 @@@ tal_get_spki(struct tal *tal, unsigned 
        *len = tal->spki_len;
  }
  
 -struct rpki_cache *
 -tal_get_cache(struct tal *tal)
 -{
 -      return tal->cache;
 -}
 -
 -/**
 - * Performs the whole validation walkthrough on uri @uri, which is assumed to
 - * have been extracted from TAL @tal.
 - */
  static int
 -handle_tal_uri(struct tal *tal, struct rpki_uri *uri, struct db_table *db)
 +queue_tal(char const *tal_path, void *arg)
  {
 -      struct validation_handler validation_handler;
 -      struct validation *state;
 -      struct cert_stack *certstack;
 -      struct deferred_cert deferred;
 -      int error;
 -
 -      pr_val_debug("TAL URI '%s' {", uri_val_get_printable(uri));
 -
 -      validation_handler.handle_roa_v4 = handle_roa_v4;
 -      validation_handler.handle_roa_v6 = handle_roa_v6;
 -      validation_handler.handle_router_key = handle_router_key;
 -      validation_handler.arg = db;
 -
 -      error = validation_prepare(&state, tal, &validation_handler);
 -      if (error)
 -              return ENSURE_NEGATIVE(error);
 -
 -      if (!uri_is_certificate(uri)) {
 -              pr_op_err("TAL URI does not point to a certificate. (Expected .cer, got '%s')",
 -                  uri_op_get_printable(uri));
 -              error = EINVAL;
 -              goto end;
 +      if (task_enqueue_tal(tal_path) < 1) {
 +              pr_op_err("Could not enqueue task '%s'; abandoning validation.",
 +                  tal_path);
 +              return EINVAL;
        }
  
 -      /* Handle root certificate. */
 -      error = certificate_traverse(NULL, uri);
 -      if (error) {
 -              switch (validation_pubkey_state(state)) {
 -              case PKS_INVALID:
 -                      error = EINVAL;
 -                      goto end;
 -              case PKS_VALID:
 -              case PKS_UNTESTED:
 -                      error = ENSURE_NEGATIVE(error);
 -                      goto end;
 -              }
 -              pr_crit("Unknown public key state: %u",
 -                  validation_pubkey_state(state));
 -      }
 +      return 0;
 +}
  
 -      /*
 -       * From now on, the tree should be considered valid, even if subsequent
 -       * certificates fail.
 -       * (the root validated successfully; subtrees are isolated problems.)
 -       */
 +static validation_verdict
 +validate_ta(struct tal *tal, struct cache_mapping const *ta_map)
 +{
 +      struct rpki_certificate *ta;
 +      validation_verdict vv;
  
 -      /* Handle every other certificate. */
 -      certstack = validation_certstack(state);
 -      if (certstack == NULL)
 -              pr_crit("Validation state has no certificate stack");
 +      ta = pzalloc(sizeof(struct rpki_certificate));
 +      map_copy(&ta->map, ta_map);
 +      ta->tal = tal;
 +      atomic_init(&ta->refcount, 1);
  
 -      do {
 -              error = deferstack_pop(certstack, &deferred);
 -              if (error == -ENOENT) {
 -                      error = 0; /* No more certificates left; we're done */
 -                      goto end;
 -              } else if (error) /* All other errors are critical, currently */
 -                      pr_crit("deferstack_pop() returned illegal %d.", error);
 -
 -              /*
 -               * Ignore result code; remaining certificates are unrelated,
 -               * so they should not be affected.
 -               */
 -              certificate_traverse(deferred.pp, deferred.uri);
 -
 -              uri_refput(deferred.uri);
 -              rpp_refput(deferred.pp);
 -      } while (true);
 +      vv = cer_traverse(ta);
  
 -end:  validation_destroy(state);
 -      pr_val_debug("}");
 -      return error;
 +      cer_free(ta);
 +      return vv;
  }
  
 -static int
 -__handle_tal_uri(struct rpki_uri *uri, void *arg)
 +static validation_verdict
 +try_urls(struct tal *tal, bool (*url_is_protocol)(struct uri const *),
 +    validation_verdict (*get_path)(struct uri const *, char **))
  {
 -      struct handle_tal_args *args = arg;
 -      return handle_tal_uri(&args->tal, uri, args->db);
 -}
 +      struct uri *url;
 +      char *path;
 +      struct cache_mapping map;
 +      validation_verdict vv;
  
 -static void *
 -do_file_validation(void *arg)
 -{
 -      struct validation_thread *thread = arg;
 -      struct handle_tal_args args;
 -      time_t start, finish;
 +      ARRAYLIST_FOREACH(&tal->urls, url) {
 +              if (!url_is_protocol(url))
 +                      continue;
  
 -      start = time(NULL);
 +              vv = get_path(url, &path);
 +              if (vv == VV_BUSY)
 +                      return VV_BUSY;
 +              if (vv == VV_FAIL || !path)
 +                      continue;
  
 -      fnstack_init();
 -      fnstack_push(thread->tal_file);
 +              map.url = *url;
 +              map.path = path;
  
 -      thread->error = tal_init(&args.tal, thread->tal_file);
 -      if (thread->error)
 -              goto end;
 +              vv = validate_ta(tal, &map);
 +              if (vv == VV_BUSY) {
 +                      free(path);
 +                      return VV_BUSY;
 +              }
 +              if (vv == VV_FAIL) {
 +                      free(path);
 +                      continue;
 +              }
  
 -      args.db = db_table_create();
 -      thread->error = cache_download_alt(args.tal.cache, &args.tal.uris,
 -          false, __handle_tal_uri, &args);
 -      if (thread->error) {
 -              pr_op_err("None of the URIs of the TAL '%s' yielded a successful traversal.",
 -                  thread->tal_file);
 -              db_table_destroy(args.db);
 -      } else {
 -              thread->db = args.db;
 +              cache_commit_file(&map);
 +              free(path);
 +              return VV_CONTINUE;
        }
  
 -      tal_cleanup(&args.tal);
 -end:  fnstack_cleanup();
 -
 -      finish = time(NULL);
 -      if (start != ((time_t) -1) && finish != ((time_t) -1))
 -              pr_op_debug("The %s tree took %.0lf seconds.",
 -                  args.tal.file_name, difftime(finish, start));
 -      return NULL;
 +      return VV_FAIL;
  }
  
 -static void
 -thread_destroy(struct validation_thread *thread)
 +static validation_verdict
 +traverse_tal(char const *tal_path)
  {
 -      free(thread->tal_file);
 -      db_table_destroy(thread->db);
 -      free(thread);
 -}
 +      struct tal tal;
 +      validation_verdict vv;
  
 -/* Creates a thread for the @tal_file TAL */
 -static int
 -spawn_tal_thread(char const *tal_file, void *arg)
 -{
 -      struct threads_list *threads = arg;
 -      struct validation_thread *thread;
 -      int error;
 +      fnstack_push(tal_path);
  
 -      thread = pmalloc(sizeof(struct validation_thread));
 +      if (tal_init(&tal, tal_path) != 0) {
 +              vv = VV_FAIL;
 +              goto end1;
 +      }
  
 -      thread->tal_file = pstrdup(tal_file);
 -      thread->db = NULL;
 -      thread->error = -EINTR;
 -      SLIST_INSERT_HEAD(threads, thread, next);
 +      /* Online attempts */
 +      vv = try_urls(&tal, uri_is_https, cache_refresh_by_url);
 +      if (vv != VV_FAIL)
 +              goto end2;
 +      vv = try_urls(&tal, uri_is_rsync, cache_refresh_by_url);
 +      if (vv != VV_FAIL)
 +              goto end2;
 +      /* Offline fallback attempts */
 +      vv = try_urls(&tal, uri_is_https, cache_get_fallback);
 +      if (vv != VV_FAIL)
 +              goto end2;
 +      vv = try_urls(&tal, uri_is_rsync, cache_get_fallback);
 +      if (vv != VV_FAIL)
 +              goto end2;
 +
 +      pr_op_err("None of the TAL URIs yielded a successful traversal.");
 +      vv = VV_FAIL;
 +
 +end2: tal_cleanup(&tal);
 +end1: fnstack_pop();
 +      return vv;
 +}
 +
 +static void *
 +pick_up_work(void *arg)
 +{
 +      struct validation_task *task = NULL;
 +      validation_verdict vv;
 +
 +      while ((task = task_dequeue(task)) != NULL) {
 +              switch (task->type) {
 +              case VTT_RPP:
 +                      if (cer_traverse(task->u.ca) == VV_BUSY) {
 +                              task_requeue_dormant(task);
 +                              task = NULL;
 +                      }
 +                      break;
 +              case VTT_TAL:
 +                      vv = traverse_tal(task->u.tal);
 +                      if (vv == VV_BUSY) {
 +                              task_requeue_dormant(task);
 +                              task = NULL;
 +                      } else if (vv == VV_FAIL) {
 +                              task_stop();
 +                      }
 +                      break;
 +              }
  
 -      error = pthread_create(&thread->pid, NULL, do_file_validation, thread);
 -      if (error) {
 -              pr_op_err("Could not spawn validation thread for %s: %s",
 -                  tal_file, strerror(error));
 -              free(thread->tal_file);
 -              free(thread);
        }
  
 -      return error;
 +      return NULL;
  }
  
 -struct db_table *
 +int
  perform_standalone_validation(void)
  {
 -      struct threads_list threads = SLIST_HEAD_INITIALIZER(threads);
 -      struct validation_thread *thread;
 -      struct db_table *db = NULL;
 -      int error = 0;
 -      int tmperr;
 -
 -      cache_setup();
 -
 -      /* TODO (fine) Maybe don't spawn threads if there's only one TAL */
 -      if (foreach_file(config_get_tal(), ".tal", true, spawn_tal_thread,
 -                       &threads) != 0) {
 -              while (!SLIST_EMPTY(&threads)) {
 -                      thread = SLIST_FIRST(&threads);
 -                      SLIST_REMOVE_HEAD(&threads, next);
 -                      thread_destroy(thread);
 -              }
 -              return NULL;
 -      }
 +      pthread_t threads[5]; // XXX variabilize
 +      array_index t;
 +      int error;
  
 -      /* Wait for all */
 -      while (!SLIST_EMPTY(&threads)) {
 -              thread = SLIST_FIRST(&threads);
 -              tmperr = pthread_join(thread->pid, NULL);
 -              if (tmperr)
 -                      pr_crit("pthread_join() threw %d (%s) on the '%s' thread.",
 -                          tmperr, strerror(tmperr), thread->tal_file);
 -              SLIST_REMOVE_HEAD(&threads, next);
 -              if (thread->error) {
 -                      error = thread->error;
 -                      pr_op_warn("Validation from TAL '%s' yielded error %d (%s); discarding all validation results.",
 -                          thread->tal_file, error, strerror(abs(error)));
 -              }
 +      error = cache_prepare();
 +      if (error)
 +              return error;
 +      fnstack_init();
 +      task_start();
  
 -              if (!error) {
 -                      stats_set_tal_vrps(thread->tal_file, "ipv4",
 -                          db_table_roa_count_v4(thread->db));
 -                      stats_set_tal_vrps(thread->tal_file, "ipv6",
 -                          db_table_roa_count_v6(thread->db));
 -
 -                      if (db == NULL) {
 -                              db = thread->db;
 -                              thread->db = NULL;
 -                      } else {
 -                              error = db_table_join(db, thread->db);
 -                      }
 -              }
 +      error = foreach_file(config_get_tal(), ".tal", true, queue_tal, NULL);
 +      if (error)
 +              goto end;
  
 -              thread_destroy(thread);
 -      }
 +      /*
 +       * From now on, the trees should be considered valid, even if subsequent
 +       * certificates fail.
 +       * (The roots validated successfully; subtrees are isolated problems.)
 +       */
  
 -      cache_teardown();
 +      for (t = 0; t < 5; t++) {
 +              error = pthread_create(&threads[t], NULL, pick_up_work, NULL);
 +              if (error)
 +                      pr_crit("pthread_create(%zu) failed: %s",
 +                          t, strerror(error));
 +      }
  
 -      /* If one thread has errors, we can't keep the resulting table. */
 -      if (error) {
 -              db_table_destroy(db);
 -              db = NULL;
 +      for (t = 0; t < 5; t++) {
 +              error = pthread_join(threads[t], NULL);
 +              if (error)
 +                      pr_crit("pthread_join(%zu) failed: %s",
 +                          t, strerror(error));
        }
  
 -      return db;
++//    // FIXME
++//    stats_set_tal_vrps(thread->tal_file, "ipv4",
++//        db_table_roa_count_v4(thread->db));
++//    stats_set_tal_vrps(thread->tal_file, "ipv6",
++//        db_table_roa_count_v6(thread->db));
++
 +end:  if (task_stop())
 +              error = EINVAL; /* pick_up_work(), VTT_TAL */
 +      fnstack_cleanup();
 +      /*
 +       * Commit even on failure, as there's no reason to throw away something
 +       * we might have recently downloaded if it managed to be marked valid.
 +       */
 +      cache_commit();
 +      return error;
  }
index 70f73303e9f527d8aad6569fa778954eb6bb3a96,2fa1ef8f392fac66854416e008e890647f154f0c..e436b8ddad0904b9506e8062ba72e0f639bed2a0
@@@ -1,9 -1,10 +1,9 @@@
 -#ifndef TAL_OBJECT_H_
 -#define TAL_OBJECT_H_
 +#ifndef SRC_OBJECT_TAL_H_
 +#define SRC_OBJECT_TAL_H_
  
- #include "stddef.h"
 -/* This is RFC 8630. */
++#include <stddef.h>
  
 -#include "cache/local_cache.h"
 -#include "rtr/db/db_table.h"
 +/* This is RFC 8630. */
  
  struct tal;
  
Simple merge
index 146743ebfd2cfbcdf29b2a6fee5fabad2f922663,f677818a416a4445bb65e29927370410fb51184e..0b8fecc17ee41b7185165e0c7882745fc2fd444b
  #include "asn1/asn1c/json_encoder.h"
  #include "common.h"
  #include "config.h"
 -#include "data_structure/path_builder.h"
  #include "log.h"
 -#include "object/tal.h"
 -#include "rsync/rsync.h"
 +#include "rsync.h"
  #include "types/bio_seq.h"
- #include "types/uri.h"
 +#include "types/path.h"
  
  #define HDRSIZE 32
  
index 0000000000000000000000000000000000000000,e3f6052f714a22594a030300ba28df634135db31..ed64589843e5da53e925466c850c9167a8cafb03
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,165 +1,164 @@@
 -#include <sys/types.h>
+ #include "prometheus.h"
+ #include <microhttpd.h>
+ #include <string.h>
+ #include <sys/socket.h>
+ #include "config.h"
+ #include "log.h"
+ #include "stats.h"
+ #if MHD_VERSION > 0x00097000
+ #define MHD_RESULT enum MHD_Result
+ #else
+ #define MHD_RESULT int
+ #endif
+ #define OPENMETRICS_CT \
+       "application/openmetrics-text; version=1.0.0; charset=utf-8"
+ #define PLAINTEXT_CT \
+       "text/plain; version=0.0.4; charset=utf-8"
+ static struct MHD_Daemon *prometheus_daemon;
+ static MHD_RESULT
+ respond(struct MHD_Connection *conn, char *msg, unsigned int status)
+ {
+       struct MHD_Response *response;
+       MHD_RESULT result;
+       response = MHD_create_response_from_buffer(strlen(msg), msg,
+           MHD_RESPMEM_PERSISTENT);
+       result = MHD_queue_response(conn, status, response);
+       MHD_destroy_response(response);
+       return result;
+ }
+ static float
+ find_q(char const *accept, char const *ct)
+ {
+       char const *value;
+       char const *limit;
+       char const *qstr;
+       float q;
+       value = strstr(accept, ct);
+       if (!value)
+               return 0;
+       limit = strchr(value, ',');
+       if (!limit)
+               limit = value + strlen(value);
+       qstr = strstr(value, ";q=");
+       if (!qstr || qstr > limit)
+               return 1;
+       return (sscanf(qstr, ";q=%f", &q) == EOF) ? 0.5 /* Shrug */ : q;
+ }
+ static void
+ set_content_type(struct MHD_Connection *conn, struct MHD_Response *res)
+ {
+       char const *accept;
+       float om_q, txt_q;
+       char const *ct;
+       MHD_RESULT ret;
+       accept = MHD_lookup_connection_value(conn, MHD_HEADER_KIND,
+           MHD_HTTP_HEADER_ACCEPT);
+       if (accept != NULL) {
+               om_q = find_q(accept, "application/openmetrics-text");
+               txt_q = find_q(accept, "text/plain");
+               if (om_q < 0.001f && txt_q < 0.001f)
+                       /* Likely a browser; these tend to prefer plaintext. */
+                       ct = PLAINTEXT_CT;
+               else
+                       ct = (om_q >= txt_q) ? OPENMETRICS_CT : PLAINTEXT_CT;
+       } else {
+               ct = OPENMETRICS_CT;
+       }
+       ret = MHD_add_response_header(res, "Content-Type", ct);
+       if (ret != MHD_YES) {
+               pr_op_debug("Could not set Content-Type HTTP header.");
+               /* Keep going; maybe the client won't care. */
+       }
+ }
+ static MHD_RESULT
+ send_metrics(struct MHD_Connection *conn)
+ {
+       char *stats;
+       struct MHD_Response *res;
+       MHD_RESULT ret;
+       pr_op_debug("Handling Prometheus request...");
+       stats = stats_export();
+ #if MHD_VERSION > 0x00096000
+       res = MHD_create_response_from_buffer_with_free_callback(strlen(stats),
+           stats, free);
+ #else
+       res = MHD_create_response_from_buffer(strlen(stats), stats,
+           MHD_RESPMEM_MUST_FREE);
+ #endif
+       set_content_type(conn, res);
+       ret = MHD_queue_response(conn, MHD_HTTP_OK, res);
+       MHD_destroy_response(res);
+       pr_op_debug("Prometheus request handled.");
+       return ret;
+ }
+ static MHD_RESULT
+ handle_prometheus_req(void *cls, struct MHD_Connection *conn,
+               const char *url, const char *method, const char *version,
+               const char *upload, size_t *uplen, void **state)
+ {
+       if (strcmp(method, "GET") != 0)
+               return respond(conn, "Invalid HTTP Method\n", MHD_HTTP_BAD_REQUEST);
+       if (strcmp(url, "/") == 0)
+               return respond(conn, "OK\n", MHD_HTTP_OK);
+       if (strcmp(url, "/metrics") == 0)
+               return send_metrics(conn);
+       return respond(conn, "Bad Request\n", MHD_HTTP_BAD_REQUEST);
+ }
+ int
+ prometheus_setup(void)
+ {
+       unsigned int port;
+       port = config_get_prometheus_port();
+       if (config_get_mode() != SERVER || port == 0)
+               return 0;
+       pr_op_debug("Starting Prometheus server...");
+       prometheus_daemon = MHD_start_daemon(
+           MHD_USE_THREAD_PER_CONNECTION,      /* flags */
+           port,                               /* port */
+           NULL, NULL,                         /* accept policy */
+           &handle_prometheus_req, NULL,       /* handler */
+           MHD_OPTION_END                      /* options */
+       );
+       if (prometheus_daemon == NULL)
+               return pr_op_err("Could not start Prometheus server; Unknown error");
+       pr_op_debug("Prometheus server started.");
+       return 0;
+ }
+ void
+ prometheus_teardown(void)
+ {
+       MHD_stop_daemon(prometheus_daemon);
+ }
diff --cc src/resource.c
index e7607f639834c268a0f81127e36328df342c65b3,793acfc771d367608a0e0e17a1b49aa4b2c1f20e..3cf5ca4c07aa3e6b6053a3a8a9eb89ce5f8301cf
@@@ -1,13 -1,12 +1,12 @@@
  #include "resource.h"
  
  #include <errno.h>
++#include <string.h>
  
 +#include "alloc.h"
  #include "log.h"
  #include "resource/ip4.h"
  #include "resource/ip6.h"
 -#include "sorted_array.h"
--#include "thread_var.h"
- #include "types/sorted_array.h"
  
  /* The resources we extracted from one certificate. */
  struct resources {
index 4a70512e99de3fcf7b290b66dc8b7afa6dba9aa4,d9bebdc097ab3d9cfb86ec2effff4c506d2976ae..0eb5517eab65008d30c6e9b655992e73105a777a
@@@ -1,7 -1,7 +1,5 @@@
  #include "resource/asn.h"
  
- #include "types/sorted_array.h"
 -#include "sorted_array.h"
--
  struct asn_cb {
        foreach_asn_cb cb;
        void *arg;
index 325816f2776715f1eb919aee95feb47837ad046d,2d16c28e074008e6d3ae34c0de48e06b4437a833..093c8feeff6f98cf34fc119df1427163468fe238
@@@ -1,10 -1,9 +1,8 @@@
  #ifndef SRC_RESOURCE_ASN_H_
  #define SRC_RESOURCE_ASN_H_
  
--#include <stdbool.h>
--
 -#include "as_number.h"
 +#include "types/asn.h"
 +#include "types/sorted_array.h"
  
  /*
   * Implementation note: This is just a casted struct sorted_array.
index 502cb31f4b153c17de2d351316d319b2efb42de7,4bab3fa3c5107057ffea1287d4164b241d25d657..e29f4c8de57f3ef5fe129b526e5250e0d36c4d0d
@@@ -1,7 -1,7 +1,5 @@@
  #include "resource/ip4.h"
  
- #include "types/sorted_array.h"
 -#include "sorted_array.h"
--
  struct r4_node {
        uint32_t min; /* This is an IPv4 address in host byte order */
        uint32_t max; /* This is an IPv4 address in host byte order */
index 28478bb933eeb7025a5955652f4f0f2cc3c891d8,c2d8fc8ccc5ba8634c316f07ca7d0c2fd91d8dc5..9536ed7f97da2620bb1a260959b96c7cba561ba8
@@@ -1,7 -1,9 +1,7 @@@
  #include "resource/ip6.h"
  
- #include "types/sorted_array.h"
+ #include <string.h>
  
 -#include "sorted_array.h"
 -
  static int
  addr_cmp(struct in6_addr const *a, struct in6_addr const *b)
  {
diff --cc src/rrdp.c
index 8795364f09c4900757310d3920531677efa78244,4d5355b6d9cb986217a306f21e78e58d530e6a51..faec03198b8411b1baa039b94e776170139c8605
@@@ -1,26 -1,17 +1,23 @@@
  #include "rrdp.h"
  
- #include <openssl/bn.h>
 -#include <ctype.h>
+ #include <libxml/globals.h>
 -#include <openssl/evp.h>
  #include <openssl/sha.h>
 +#include <sys/queue.h>
  
- #include "cache.h"
 +#include "base64.h"
 +#include "cachetmp.h"
  #include "common.h"
 -#include "crypto/base64.h"
 -#include "crypto/hash.h"
 +#include "config.h"
  #include "file.h"
 +#include "hash.h"
 +#include "http.h"
 +#include "json_util.h"
  #include "log.h"
 +#include "relax_ng.h"
++#include "rtr/db/delta.h"
  #include "thread_var.h"
- #include "types/arraylist.h"
- #include "types/path.h"
 -#include "xml/relax_ng.h"
 +#include "types/str.h"
- #include "types/uri.h"
 +#include "types/uthash.h"
  
  /* RRDP's XML namespace */
  #define RRDP_NAMESPACE                "http://www.ripe.net/rpki/rrdp"
diff --cc src/rrdp.h
index dad75f3ca1621f3f244a1fa62bfefbba705114cf,f3de0a366eead6bace92541acba1358d08d45b2b..0b96817b64ceef95d24dcf2d4b9f414cfed034ee
@@@ -1,27 -1,8 +1,26 @@@
  #ifndef SRC_RRDP_H_
  #define SRC_RRDP_H_
  
- #include <stdbool.h>
- #include <time.h>
 +#include <jansson.h>
- #include "file.h"
++#include <sys/stat.h>
++#include <sys/types.h>
 +
  #include "types/uri.h"
  
 -int rrdp_update(struct rpki_uri *);
 +struct rrdp_state;
 +
 +int rrdp_update(struct uri const *, char const *, time_t, bool *,
 +    struct rrdp_state **);
 +char const *rrdp_file(struct rrdp_state const *, struct uri const *);
 +
 +char const *rrdp_create_fallback(char *, struct rrdp_state **,
 +    struct uri const *);
 +
 +json_t *rrdp_state2json(struct rrdp_state *);
 +int rrdp_json2state(json_t *, char *, struct rrdp_state **);
 +
 +void rrdp_state_free(struct rrdp_state *);
 +
 +void rrdp_print(struct rrdp_state *);
  
  #endif /* SRC_RRDP_H_ */
diff --cc src/rsync.c
index 859e4e572a271e5b0a0ff2022c488baf8549e5ef,0000000000000000000000000000000000000000..ca32ee7875e95fee6d5fded861acc02ed1d990dd
mode 100644,000000..100644
--- /dev/null
@@@ -1,871 -1,0 +1,868 @@@
- #include <stream.h>
 +#include "rsync.h"
 +
 +#include <errno.h>
 +#include <fcntl.h>
 +#include <poll.h>
 +#include <signal.h>
- #include <syslog.h>
++#include <stdarg.h>
 +#include <sys/queue.h>
 +#include <sys/wait.h>
- #include "alloc.h"
 +
- #include "types/array.h"
++#include "asn1/asn1c/RsyncRequest.h"
++#include "asn1/asn1c/ber_decoder.h"
++#include "asn1/asn1c/der_encoder.h"
 +#include "common.h"
 +#include "config.h"
 +#include "log.h"
- #include "asn1/asn1c/ber_decoder.h"
- #include "asn1/asn1c/der_encoder.h"
- #include "asn1/asn1c/RsyncRequest.h"
++#include "stream.h"
 +#include "types/map.h"
 +
 +#define RSP /* rsync spawner prefix */ "[rsync spawner] "
 +#define SRTP "[spawner response thread] "
 +
 +static char const *rsync_args[20]; /* Last must be NULL */
 +
 +static const int RDFD = 0;
 +static const int WRFD = 1;
 +
 +#define STDERR_WRITE(fds) fds[0][1]
 +#define STDOUT_WRITE(fds) fds[1][1]
 +#define STDERR_READ(fds)  fds[0][0]
 +#define STDOUT_READ(fds)  fds[1][0]
 +
 +static pid_t spawner; /* The subprocess that spawns rsync runs */
 +
 +/*
 + * "Parent-spawner socket."
 + * Used by both parent and spawner to speak with each other.
 + */
 +struct {
 +      struct read_stream rd;          /* Read fd and extra tools */
 +      int wr;                         /* Write fd */
 +
 +      struct RsyncRequest *rr;        /* Scratchpad buffer for stream read */
 +      pthread_mutex_t wrlock;         /* To sync writes */
 +} pssk;
 +
 +struct rsync_task {
 +      int pid;
 +      struct uri url;
 +      char *path;
 +      int stdoutfd;   /* Child rsync's standard output */
 +      int stderrfd;   /* Child rsync's standard error */
 +      struct timespec expiration;
 +
 +      LIST_ENTRY(rsync_task) lh;
 +};
 +
 +LIST_HEAD(rsync_task_list, rsync_task);
 +
 +struct rsync_tasks {
 +      struct rsync_task_list active;
 +      int a; /* total active */
 +
 +      struct rsync_task_list queued;
 +};
 +
 +#ifndef LIST_FOREACH_SAFE
 +#define LIST_FOREACH_SAFE(var, ls, lh, tmp)                           \
 +    for (                                                             \
 +        var = LIST_FIRST(ls), tmp = (var ? LIST_NEXT(var, lh) : NULL);        \
 +        var != NULL;                                                  \
 +        var = tmp, tmp = (var ? LIST_NEXT(var, lh) : NULL)            \
 +    )
 +#endif
 +
 +/* Spawner response thread; The thread that listens to spawner responses. */
 +static pthread_t srt;
 +
 +static void
 +__spsk_init(int rdfd, int wrfd)
 +{
 +      rstream_init(&pssk.rd, rdfd, 512);
 +      pssk.wr = wrfd;
 +      pssk.rr = NULL;
 +      pthread_mutex_init(&pssk.wrlock, NULL);
 +}
 +
 +static void
 +spsk_init(int rdpipes[2], int wrpipes[2])
 +{
 +      close(wrpipes[RDFD]);
 +      close(rdpipes[WRFD]);
 +      __spsk_init(rdpipes[RDFD], wrpipes[WRFD]);
 +}
 +
 +static void
 +spsk_cleanup(void)
 +{
 +      rstream_close(&pssk.rd, true);
 +      if (pssk.wr != -1) {
 +              close(pssk.wr);
 +              pssk.wr = -1;
 +      }
 +      ASN_STRUCT_FREE(asn_DEF_RsyncRequest, pssk.rr);
 +      pthread_mutex_destroy(&pssk.wrlock);
 +}
 +
 +static int
 +write_cb(const void *buffer, size_t size, void *arg)
 +{
 +      return stream_full_write(pssk.wr, buffer, size);
 +}
 +
 +static void
 +notify_parent(struct rsync_task *task)
 +{
 +      struct RsyncRequest req;
 +      asn_enc_rval_t result;
 +
 +      if (pssk.wr == -1) {
 +              pr_op_err(RSP "Cannot message parent process: "
 +                  "The socket is closed.");
 +              return;
 +      }
 +
 +      /*
 +       * TODO (asn1) these error messages are too generic.
 +       * The asn1 code needs better error reporting.
 +       */
 +
 +      if (RsyncRequest_init(&req, &task->url, task->path) < 0) {
 +              pr_op_err(RSP "Cannot message parent process: "
 +                  "The request object cannot be created");
 +              return;
 +      }
 +
 +      result = der_encode(&asn_DEF_RsyncRequest, &req, write_cb, NULL);
 +      if (result.encoded == -1) {
 +              pr_op_err(RSP "Cannot message parent process: Unknown error");
 +              /* TODO (asn1) Do this if the error was I/O:
 +               * close(spsk.wr);
 +               * spsk.wr = -1;
 +               * // Can't signal finished rsyncs anymore; reject future ones.
 +               * rstream_close(&spsk.rd, true);
 +               */
 +      }
 +
 +      pr_op_debug(RSP "Parent notified; sent %zd bytes.", result.encoded);
 +      ASN_STRUCT_FREE_CONTENTS_ONLY(asn_DEF_RsyncRequest, &req);
 +}
 +
 +static void
 +void_task(struct rsync_task *task)
 +{
 +      notify_parent(task);
 +
 +      uri_cleanup(&task->url);
 +      free(task->path);
 +      free(task);
 +}
 +
 +static void
 +finish_task(struct rsync_tasks *tasks, struct rsync_task *task)
 +{
 +      LIST_REMOVE(task, lh);
 +      tasks->a--;
 +      void_task(task);
 +}
 +
 +static void
 +init_pfd(struct pollfd *pfd, int fd)
 +{
 +      pfd->fd = fd;
 +      pfd->events = POLLIN;
 +      pfd->revents = 0;
 +}
 +
 +static struct pollfd *
 +create_pfds(int request_fd, struct rsync_task_list *tasks, size_t tn)
 +{
 +      struct pollfd *pfds;
 +      struct rsync_task *task;
 +      size_t p;
 +
 +      pfds = pmalloc((2 * tn + 1) * sizeof(struct pollfd));
 +      p = 0;
 +
 +      init_pfd(&pfds[p++], request_fd);
 +      LIST_FOREACH(task, tasks, lh) {
 +              init_pfd(&pfds[p++], task->stdoutfd);
 +              init_pfd(&pfds[p++], task->stderrfd);
 +      }
 +
 +      return pfds;
 +}
 +
 +static int
 +create_pipes(int fds[2][2])
 +{
 +      int error;
 +
 +      if (pipe(fds[0]) < 0) {
 +              error = errno;
 +              pr_op_err_st(RSP "Piping rsync stderr: %s", strerror(error));
 +              return error;
 +      }
 +
 +      if (pipe(fds[1]) < 0) {
 +              error = errno;
 +              pr_op_err_st(RSP "Piping rsync stdout: %s", strerror(error));
 +              close(fds[0][0]);
 +              close(fds[0][1]);
 +              return error;
 +      }
 +
 +      return 0;
 +}
 +
 +static void
 +prepare_rsync_args(char **args, struct uri const *url, char const *path)
 +{
 +      size_t i;
 +
 +      /*
 +       * execvp() is not going to tweak these strings;
 +       * stop angsting over the const-to-raw conversion.
 +       */
 +
 +      for (i = 0; rsync_args[i] != NULL; i++)
 +              args[i] = (char *)rsync_args[i];
 +      args[i++] = (char *)uri_str(url);
 +      args[i++] = (char *)path;
 +      args[i++] = NULL;
 +}
 +
 +/*
 + * Duplicate parent FDs, to pipe rsync output:
 + * - fds[0] = stderr
 + * - fds[1] = stdout
 + */
 +static void
 +duplicate_fds(int fds[2][2])
 +{
 +      /* Use the loop to catch interruptions */
 +      while ((dup2(STDERR_WRITE(fds), STDERR_FILENO) == -1)
 +              && (errno == EINTR)) {}
 +      close(STDERR_WRITE(fds));
 +      close(STDERR_READ(fds));
 +
 +      while ((dup2(STDOUT_WRITE(fds), STDOUT_FILENO) == -1)
 +          && (errno == EINTR)) {}
 +      close(STDOUT_WRITE(fds));
 +      close(STDOUT_READ(fds));
 +}
 +
 +static int
 +execvp_rsync(struct uri const *url, char const *path, int fds[2][2])
 +{
 +      char *args[20];
 +
 +      prepare_rsync_args(args, url, path);
 +      duplicate_fds(fds);
 +
 +      if (execvp(args[0], args) < 0)
 +              return errno;
 +
 +      return EINVAL; /* Unreachable, but whatever */
 +}
 +
 +static int
 +fork_rsync(struct rsync_task *task)
 +{
 +      int fork_fds[2][2];
 +      int error;
 +
 +      error = create_pipes(fork_fds);
 +      if (error)
 +              return error;
 +
 +      fflush(stdout);
 +      fflush(stderr);
 +
 +      task->pid = fork();
 +      if (task->pid < 0) {
 +              error = errno;
 +              pr_op_err_st(RSP "Couldn't spawn the rsync process: %s",
 +                  strerror(error));
 +              close(STDERR_READ(fork_fds));
 +              close(STDOUT_READ(fork_fds));
 +              close(STDERR_WRITE(fork_fds));
 +              close(STDOUT_WRITE(fork_fds));
 +              return error;
 +      }
 +
 +      if (task->pid == 0) /* Child code */
 +              exit(execvp_rsync(&task->url, task->path, fork_fds));
 +
 +      /* Parent code */
 +
 +      close(STDERR_WRITE(fork_fds));
 +      close(STDOUT_WRITE(fork_fds));
 +      task->stderrfd = STDERR_READ(fork_fds);
 +      task->stdoutfd = STDOUT_READ(fork_fds);
 +      return 0;
 +}
 +
 +static void
 +activate_task(struct rsync_tasks *tasks, struct rsync_task *task,
 +    struct timespec *now)
 +{
 +      ts_add(&task->expiration, now, 1000 * config_rsync_timeout());
 +
 +      if (fork_rsync(task) != 0) {
 +              void_task(task);
 +              return;
 +      }
 +
 +      LIST_INSERT_HEAD(&tasks->active, task, lh);
 +      tasks->a++;
 +}
 +
 +/* Steals ownership of @map. */
 +static void
 +post_task(struct cache_mapping *map, struct rsync_tasks *tasks,
 +    struct timespec *now)
 +{
 +      struct rsync_task *task;
 +
 +      task = pzalloc(sizeof(struct rsync_task));
 +      task->url = map->url;
 +      task->path = map->path;
 +
 +      if (tasks->a >= config_rsync_max()) {
 +              LIST_INSERT_HEAD(&tasks->queued, task, lh);
 +              pr_op_debug(RSP "Queued task %d: %s -> %s",
 +                  task->pid, uri_str(&task->url), task->path);
 +      } else {
 +              activate_task(tasks, task, now);
 +              pr_op_debug(RSP "Got new task %d: %s -> %s",
 +                  task->pid, uri_str(&task->url), task->path);
 +      }
 +}
 +
 +static int
 +next_task(struct cache_mapping *result)
 +{
 +      asn_dec_rval_t decres;
 +      ssize_t consumed;
 +      int error;
 +
 +again:        if (pssk.rd.len > 0) {
 +              decres = ber_decode(&asn_DEF_RsyncRequest, (void **)&pssk.rr,
 +                  pssk.rd.buffer, pssk.rd.len);
 +
 +              memmove(pssk.rd.buffer, pssk.rd.buffer + decres.consumed,
 +                  pssk.rd.len - decres.consumed);
 +              pssk.rd.len -= decres.consumed;
 +
 +              switch (decres.code) {
 +              case RC_OK:
 +                      __uri_init(&result->url,
 +                          OCTET_STRING_toString(&pssk.rr->url),
 +                          pssk.rr->url.size);
 +                      result->path = OCTET_STRING_toString(&pssk.rr->path);
 +                      ASN_STRUCT_RESET(asn_DEF_RsyncRequest, pssk.rr);
 +                      return 0;
 +              case RC_WMORE:
 +                      break;
 +              case RC_FAIL:
 +                      rstream_close(&pssk.rd, true);
 +                      return EINVAL;
 +              }
 +      }
 +
 +      if (pssk.rd.fd == -1)
 +              return EBADF;
 +      consumed = read(pssk.rd.fd, pssk.rd.buffer + pssk.rd.len,
 +          pssk.rd.capacity - pssk.rd.len);
 +      if (consumed < 0) {
 +              error = errno;
 +              if (error != EAGAIN && error != EWOULDBLOCK)
 +                      rstream_close(&pssk.rd, true);
 +              return error;
 +      }
 +      if (consumed == 0) { /* EOS */
 +              rstream_close(&pssk.rd, true);
 +              return ENOENT;
 +      }
 +
 +      pssk.rd.len += consumed;
 +      goto again;
 +}
 +
 +static void
 +handle_parent_fd(struct pollfd *pfd, struct rsync_tasks *tasks,
 +    struct timespec *now)
 +{
 +      struct cache_mapping map;
 +
 +      if (pssk.rd.fd == -1)
 +              return;
 +
 +      if (pfd->revents & POLLNVAL) {
 +              pr_op_err(RSP "bad parent fd: %i", pfd->fd);
 +              rstream_close(&pssk.rd, false);
 +
 +      } else if (pfd->revents & POLLERR) {
 +              pr_op_err(RSP "Generic error during parent fd poll.");
 +              rstream_close(&pssk.rd, true);
 +
 +      } else if (pfd->revents & (POLLIN | POLLHUP)) {
 +              while (next_task(&map) == 0)
 +                      post_task(&map, tasks, now);
 +      }
 +}
 +
 +static void
 +log_buffer(char const *buffer, ssize_t read, bool is_error)
 +{
 +      char *cpy, *cur, *tmp;
 +
 +      cpy = pmalloc(read + 1);
 +
 +      strncpy(cpy, buffer, read);
 +      cpy[read] = '\0';
 +
 +      /* Break lines to one line at log */
 +      cur = cpy;
 +      while ((tmp = strchr(cur, '\n')) != NULL) {
 +              *tmp = '\0';
 +              if (strlen(cur) == 0) {
 +                      cur = tmp + 1;
 +                      continue;
 +              }
 +              if (is_error)
 +                      pr_op_err("[RSYNC exec] %s", cur);
 +              else
 +                      pr_op_debug("[RSYNC exec] %s", cur);
 +              cur = tmp + 1;
 +      }
 +      free(cpy);
 +}
 +
 +/* 0 = still more to read; 1 = stream down */
 +static int
 +log_rsync_output(struct pollfd *pfd, size_t p)
 +{
 +      char buffer[1024];
 +      ssize_t count;
 +      int error;
 +
 +      count = read(pfd->fd, buffer, sizeof(buffer));
 +      if (count == 0)
 +              goto down; /* EOF */
 +      if (count == -1) {
 +              error = errno;
 +              if (error == EINTR)
 +                      return 0; /* Dunno; retry */
 +              pr_op_err(RSP "rsync buffer read error: %s", strerror(error));
 +              goto down; /* Error */
 +      }
 +
 +      log_buffer(buffer, count, (p & 1) == 0);
 +      return 0; /* Keep going */
 +
 +down: close(pfd->fd);
 +      pfd->fd = -1;
 +      return 1;
 +}
 +
 +/* Returns 1 if the stream ended */
 +static int
 +handle_rsync_fd(struct pollfd *pfd, size_t p)
 +{
 +      if (pfd->fd == -1) {
 +              pr_op_debug(RSP "File descriptor already closed.");
 +              return 1;
 +      }
 +
 +      if (pfd->revents & POLLNVAL) {
 +              pr_op_err(RSP "rsync bad fd: %i", pfd->fd);
 +              return 1;
 +      }
 +
 +      if (pfd->revents & POLLERR) {
 +              pr_op_err(RSP "Generic error during rsync poll.");
 +              close(pfd->fd);
 +              return 1;
 +      }
 +
 +      if (pfd->revents & (POLLIN | POLLHUP))
 +              return log_rsync_output(pfd, p);
 +
 +      return 0;
 +}
 +
 +static int
 +wait_subprocess(char const *name, pid_t pid)
 +{
 +      int status;
 +      int error;
 +
 +again:        status = 0;
 +      if (waitpid(pid, &status, 0) < 0) {
 +              error = errno;
 +              pr_op_err("Could not wait for %s: %s", name, strerror(error));
 +              return error;
 +      }
 +
 +      if (WIFEXITED(status)) {
 +              /* Happy path (but also sad path sometimes) */
 +              error = WEXITSTATUS(status);
 +              pr_op_debug("%s ended. Result: %d", name, error);
 +              return error ? EIO : 0;
 +      }
 +
 +      if (WIFSIGNALED(status)) {
 +              pr_op_warn("%s interrupted by signal %d (%s).",
 +                  name, WTERMSIG(status), strsignal(WTERMSIG(status)));
 +              return EINTR;
 +      }
 +
 +      if (WIFCONTINUED(status)) {
 +              /*
 +               * Testing warning:
 +               * I can't trigger this branch. It always exits or signals;
 +               * SIGSTOP then SIGCONT doesn't seem to wake up waitpid().
 +               * It's concerning because every sample code I've found assumes
 +               * waitpid() returning always means the subprocess ended, so
 +               * they never retry. But that contradicts all documentation,
 +               * yet seems to be accurate to reality.
 +               */
 +              pr_op_debug("%s has resumed.", name);
 +              goto again;
 +      }
 +
 +      /* Dead code */
 +      return pr_op_err("Unknown waitpid() status; giving up %s.", name);
 +}
 +
 +static void
 +kill_subprocess(struct rsync_task *task)
 +{
 +      if (task->stdoutfd != -1)
 +              close(task->stdoutfd);
 +      if (task->stderrfd != -1)
 +              close(task->stderrfd);
 +      kill(task->pid, SIGTERM);
 +}
 +
 +static void
 +activate_queued(struct rsync_tasks *tasks, struct timespec *now)
 +{
 +      struct rsync_task *task;
 +
 +      task = LIST_FIRST(&tasks->queued);
 +      if (task == NULL)
 +              return;
 +
 +      pr_op_debug(RSP "Activating queued task %s -> %s.",
 +          uri_str(&task->url), task->path);
 +      LIST_REMOVE(task, lh);
 +      activate_task(tasks, task, now);
 +}
 +
 +/* Returns true if the task died. */
 +static bool
 +maybe_expire(struct rsync_tasks *tasks, struct rsync_task *task,
 +    struct timespec *now)
 +{
 +      struct timespec epoch;
 +
 +      ts_add(&epoch, now, 100);
 +      if (ts_cmp(&epoch, &task->expiration) < 0)
 +              return false;
 +
 +      pr_op_debug(RSP "Task %d ran out of time.", task->pid);
 +      kill_subprocess(task);
 +      wait_subprocess("rsync", task->pid);
 +      finish_task(tasks, task);
 +      activate_queued(tasks, now);
 +
 +      return true;
 +}
 +
 +static int
 +spawner_run(void)
 +{
 +      struct pollfd *pfds;    /* Channels to children */
 +      size_t p, pfds_count;
 +      struct timespec now, expiration;
 +      int timeout;
 +
 +      int events;
 +
 +      struct rsync_tasks tasks;
 +      struct rsync_task *task, *tmp;
 +
 +      int error;
 +
 +      LIST_INIT(&tasks.active);
 +      LIST_INIT(&tasks.queued);
 +      tasks.a = 0;
 +      error = 0;
 +
 +      ts_now(&now);
 +
 +      do {
 +              /*
 +               * 0: request pipe
 +               * odd: stdouts
 +               * even > 0: stderrs
 +               */
 +              pfds = create_pfds(pssk.rd.fd, &tasks.active, tasks.a);
 +              pfds_count = 2 * tasks.a + 1;
 +              expiration.tv_sec = now.tv_sec + 10;
 +              expiration.tv_nsec = now.tv_nsec;
 +              LIST_FOREACH(task, &tasks.active, lh)
 +                      if ((ts_cmp(&now, &task->expiration) < 0) &&
 +                          (ts_cmp(&task->expiration, &expiration) < 0))
 +                              expiration = task->expiration;
 +
 +              timeout = ts_delta(&now, &expiration);
 +              pr_op_debug(RSP "Timeout decided: %dms", timeout);
 +              events = poll(pfds, pfds_count, timeout);
 +              if (events < 0) {
 +                      error = errno;
 +                      free(pfds);
 +                      break;
 +              }
 +
 +              ts_now(&now);
 +
 +              if (events == 0) { /* Timeout */
 +                      pr_op_debug(RSP "Woke up because of timeout.");
 +                      LIST_FOREACH_SAFE(task, &tasks.active, lh, tmp)
 +                              maybe_expire(&tasks, task, &now);
 +                      goto cont;
 +              }
 +
 +              pr_op_debug(RSP "Woke up because of input.");
 +              p = 1;
 +              LIST_FOREACH_SAFE(task, &tasks.active, lh, tmp) {
 +                      if (maybe_expire(&tasks, task, &now))
 +                              continue;
 +
 +                      if (handle_rsync_fd(&pfds[p], p)) {
 +                              pr_op_debug(RSP "Task %d: Stdout closed.",
 +                                  task->pid);
 +                              task->stdoutfd = -1;
 +                      }
 +                      p++;
 +                      if (handle_rsync_fd(&pfds[p], p)) {
 +                              pr_op_debug(RSP "Task %d: Stderr closed.",
 +                                  task->pid);
 +                              task->stderrfd = -1;
 +                      }
 +                      p++;
 +                      if (task->stdoutfd == -1 && task->stderrfd == -1) {
 +                              pr_op_debug(RSP "Both stdout & stderr are closed; ending task %d.",
 +                                  task->pid);
 +                              wait_subprocess("rsync", task->pid);
 +                              finish_task(&tasks, task);
 +                              activate_queued(&tasks, &now);
 +                      }
 +              }
 +              handle_parent_fd(&pfds[0], &tasks, &now);
 +
 +cont:         free(pfds);
 +      } while ((pssk.rd.fd != -1 || tasks.a > 0));
 +      pr_op_debug(RSP "The parent stream is closed and there are no rsync tasks running. Cleaning up...");
 +
 +      LIST_FOREACH_SAFE(task, &tasks.active, lh, tmp) {
 +              kill_subprocess(task);
 +              wait_subprocess("rsync", task->pid);
 +              finish_task(&tasks, task);
 +      }
 +      LIST_FOREACH_SAFE(task, &tasks.queued, lh, tmp) {
 +              LIST_REMOVE(task, lh);
 +              void_task(task);
 +      }
 +
 +      spsk_cleanup();
 +
 +      free_rpki_config();
 +      log_teardown();
 +      return error;
 +}
 +
 +static int
 +nonblock_pipe(int *fds)
 +{
 +      int error;
 +      int flags;
 +
 +      if (pipe(fds) < 0) {
 +              error = errno;
 +              pr_op_warn("Cannot create pipe: %s", strerror(error));
 +              return error;
 +      }
 +
 +      flags = fcntl(fds[RDFD], F_GETFL);
 +      if (flags < 0) {
 +              error = errno;
 +              pr_op_warn("Cannot retrieve pipe flags: %s", strerror(error));
 +              goto cancel;
 +      }
 +      if (fcntl(fds[RDFD], F_SETFL, flags | O_NONBLOCK) < 0) {
 +              error = errno;
 +              pr_op_warn("Cannot enable O_NONBLOCK: %s", strerror(error));
 +              goto cancel;
 +      }
 +
 +      return 0;
 +
 +cancel:       close(fds[RDFD]);
 +      close(fds[WRFD]);
 +      return error;
 +}
 +
 +static void *
 +rcv_spawner_responses(void *arg)
 +{
 +      struct cache_mapping map = { 0 };
 +
 +      while (next_task(&map) == 0) {
 +              rsync_finished(&map.url, map.path);
 +              map_cleanup(&map);
 +      }
 +
 +      return NULL;
 +}
 +
 +void
 +rsync_setup(char const *program, ...)
 +{
 +      int parent2spawner[2];  /* Pipe: Parent writes, spawner reads */
 +      int spawner2parent[2];  /* Pipe: Spawner writes, parent reads */
 +
 +      va_list args;
 +      array_index i;
 +      char const *arg;
 +      int error;
 +
 +      if (!config_get_rsync_enabled())
 +              return;
 +
 +      if (program != NULL) {
 +              rsync_args[0] = arg = program;
 +              va_start(args, program);
 +              for (i = 1; arg != NULL; i++) {
 +                      arg = va_arg(args, char const *);
 +                      rsync_args[i] = arg;
 +              }
 +              va_end(args);
 +      } else {
 +              /* XXX review */
 +              /* XXX Where is --delete? */
 +              i = 0;
 +              rsync_args[i++] = config_get_rsync_program();
 +              rsync_args[i++] = "-rtz";
 +              rsync_args[i++] = "--omit-dir-times";
 +              rsync_args[i++] = "--contimeout";
 +              rsync_args[i++] = "20";
 +              rsync_args[i++] = "--max-size";
 +              rsync_args[i++] = "20MB";
 +              rsync_args[i++] = "--timeout";
 +              rsync_args[i++] = "15";
 +              rsync_args[i++] = "--include=*/";
 +              rsync_args[i++] = "--include=*.cer";
 +              rsync_args[i++] = "--include=*.crl";
 +              rsync_args[i++] = "--include=*.gbr";
 +              rsync_args[i++] = "--include=*.mft";
 +              rsync_args[i++] = "--include=*.roa";
 +              rsync_args[i++] = "--exclude=*";
 +              rsync_args[i++] = NULL;
 +      }
 +
 +      if (nonblock_pipe(parent2spawner) != 0)
 +              goto fail1;
 +      if (pipe(spawner2parent) < 0) {
 +              pr_op_warn("Cannot create pipe: %s", strerror(errno));
 +              goto fail2;
 +      }
 +
 +      fflush(stdout);
 +      fflush(stderr);
 +
 +      spawner = fork();
 +      if (spawner < 0) {
 +              pr_op_warn("Cannot fork rsync spawner: %s", strerror(errno));
 +              goto fail3;
 +      }
 +
 +      if (spawner == 0) { /* Client code */
 +              spsk_init(parent2spawner, spawner2parent);
 +              exit(spawner_run());
 +      }
 +
 +      /* Parent code */
 +      /* (Threads can now be spawned.) */
 +
 +      spsk_init(spawner2parent, parent2spawner);
 +
 +      error = pthread_create(&srt, NULL, rcv_spawner_responses, NULL);
 +      if (error) {
 +              pr_op_warn("Cannot start rsync spawner listener thread: %s",
 +                  strerror(error));
 +              spsk_cleanup();
 +              goto fail1;
 +      }
 +
 +      return;
 +
 +fail3:        close(spawner2parent[RDFD]);
 +      close(spawner2parent[WRFD]);
 +fail2:        close(parent2spawner[RDFD]);
 +      close(parent2spawner[WRFD]);
 +fail1:        pr_op_warn("rsync will not be available.");
 +      pssk.rd.fd = pssk.wr = -1;
 +}
 +
 +/*
 + * Queues rsync; doesn't wait.
 + *
 + * Whenever at least one rsync is finished, the function rsync_finished()
 + * will be automatically called.
 + */
 +int
 +rsync_queue(struct uri const *url, char const *path)
 +{
 +      struct RsyncRequest req;
 +      asn_enc_rval_t result;
 +      int error;
 +
 +      if (RsyncRequest_init(&req, url, path) < 0)
 +              return EINVAL;
 +
 +      mutex_lock(&pssk.wrlock);
 +
 +      if (pssk.wr == -1) {
 +              error = EIO;
 +              goto end;
 +      }
 +
 +      result = der_encode(&asn_DEF_RsyncRequest, &req, write_cb, NULL);
 +      if (result.encoded == -1) {
 +              close(pssk.wr);
 +              pssk.wr = -1;
 +              error = EIO;
 +              goto end;
 +      }
 +
 +      error = 0;
 +end:  mutex_unlock(&pssk.wrlock);
 +      ASN_STRUCT_FREE_CONTENTS_ONLY(asn_DEF_RsyncRequest, &req);
 +      return error;
 +}
 +
 +void
 +rsync_teardown(void)
 +{
 +      if (!config_get_rsync_enabled())
 +              return;
 +
 +      spsk_cleanup();
 +      wait_subprocess("rsync spawner", spawner);
 +}
index 2fab2dd2209bfe473e6b852f14f876183f33cb57,2038381d503726a24ffa5129ad343ce773f412ec..004ec2928ce17a6339d49ba585c3a873f9accaed
@@@ -1,12 -1,10 +1,11 @@@
  #include "rtr/db/db_table.h"
  
  #include <errno.h>
- #include <pthread.h>
  
  #include "alloc.h"
 -#include "data_structure/uthash.h"
 +#include "common.h"
  #include "log.h"
 +#include "types/uthash.h"
  
  struct hashable_roa {
        struct vrp data;
@@@ -21,21 -19,15 +20,23 @@@ struct hashable_key 
  struct db_table {
        struct hashable_roa *roas;
        struct hashable_key *router_keys;
+       unsigned int total_roas_v4;
+       unsigned int total_roas_v6;
++
 +      pthread_mutex_t lock;
  };
  
  struct db_table *
  db_table_create(void)
  {
 -      return pzalloc(sizeof(struct db_table));
 +      struct db_table *table;
 +
-       table = pmalloc(sizeof(struct db_table));
-       table->roas = NULL;
-       table->router_keys = NULL;
++      table = pzalloc(sizeof(struct db_table));
 +      panic_on_fail(pthread_mutex_init(&table->lock, NULL),
 +          "pthread_mutex_init");
 +
 +      return table;
  }
  
  void
@@@ -79,10 -65,17 +80,17 @@@ add_roa(struct db_table *table, struct 
        if (error) {
                pr_val_err("ROA couldn't be added to hash table: %s",
                    strerror(error));
 -              return -error;
 +              return error;
        }
-       if (old != NULL)
+       if (old == NULL) {
+               switch (new->data.addr_fam) {
+               case AF_INET:   table->total_roas_v4++; break;
+               case AF_INET6:  table->total_roas_v6++; break;
+               }
+       } else {
                free(old);
+       }
  
        return 0;
  }
index 09eb72d7e76c6b239856b991dd9b8bfaf9e4f371,bd625748148f1a15898e958f8f34aaea26f21dc3..4dd4cd01d10a33362890574ead7bc6cf3d6e0984
@@@ -1,10 -1,11 +1,11 @@@
  #include "rtr/db/delta.h"
  
  #include <stdatomic.h>
+ #include <string.h>
  
 -#include "data_structure/array_list.h"
  #include "log.h"
  #include "types/address.h"
 +#include "types/arraylist.h"
  
  struct delta_v4 {
        uint32_t as;
index 29997b01b437a988cf0fef3bffd161b73c6851a7,f23107d6b3f6cece57f3c53a7a81406d8d3b54b8..8679bbc28ae961f890c5b70d365b73871b063c1a
@@@ -1,9 -1,8 +1,9 @@@
  #include "rtr/db/vrps.h"
  
--#include <errno.h>
++#include <string.h>
  #include <time.h>
  
 +#include "alloc.h"
  #include "common.h"
  #include "config.h"
  #include "log.h"
index a31861e2438d5b2971be59809d22f180ca710941,f3a6521150cdda69451652f8edffdca53bb42363..f6569b0fca90786955fdacab67eeed8ed644ae2c
@@@ -7,8 -7,8 +7,6 @@@
   * This module stores VRPs and their serials.
   */
  
 -#include "as_number.h"
--#include "types/address.h"
- #include "types/asn.h"
  #include "types/delta.h"
  #include "types/serial.h"
  
Simple merge
diff --cc src/rtr/pdu.h
Simple merge
index bdad6106414ac19178004b380401374891a3d9af,2e50c23d98e21b62f76486caab24ceb0d6df6456..a3d218d24ce1a667fffa2d1b2362bfc7f196fba3
@@@ -2,6 -2,7 +2,9 @@@
  
  #include <errno.h>
  #include <poll.h>
+ #include <string.h>
++#include <sys/socket.h>
++#include <unistd.h>
  
  #include "alloc.h"
  #include "config.h"
diff --cc src/rtr/rtr.c
index 7b32c2c95137204bd121fc3f59a36a5198ca3bc0,9757b9cb8c267c17d9ece4c1bfa0a5bbff161da5..b0b02db5f2599c23e18f05d496674021e8ea1c0e
  #include "rtr/db/vrps.h"
  #include "rtr/pdu_handler.h"
  #include "rtr/pdu_sender.h"
 -#include "thread/thread_pool.h"
+ #include "stats.h"
 +#include "thread_pool.h"
++#include "types/address.h"
 +#include "types/arraylist.h"
  
  struct rtr_server {
        int fd;
diff --cc src/sig.c
index cd59b028e6410588f32448eb2aed0a5ebf41e4a5,0000000000000000000000000000000000000000..f592268414e8d8fc66d2465be41fc74feac054e4
mode 100644,000000..100644
--- /dev/null
+++ b/src/sig.c
@@@ -1,115 -1,0 +1,111 @@@
- #include <stddef.h>
- #include <string.h>
- #include <sys/types.h>
- #include <unistd.h>
 +#include "sig.h"
 +
 +#include <errno.h>
 +#ifdef BACKTRACE_ENABLED
 +#include <execinfo.h>
 +#endif
 +#include <signal.h>
 +
 +#include "cache.h"
 +#include "log.h"
 +#include "output_printer.h"
 +
 +/*
 + * Ensures libgcc is loaded; otherwise backtrace() might allocate
 + * during a signal handler (which is illegal).
 + */
 +static void
 +setup_backtrace(void)
 +{
 +#ifdef BACKTRACE_ENABLED
 +      void *dummy;
 +      dummy = NULL;
 +      backtrace(&dummy, 1);
 +#endif
 +}
 +
 +void
 +print_stack_trace(void)
 +{
 +#ifdef BACKTRACE_ENABLED
 +      /*
 +       * See https://stackoverflow.com/questions/29982643
 +       * I went with rationalcoder's answer, because I think not printing
 +       * stack traces on segfaults is a nice way of ending up committing
 +       * suicide.
 +       */
 +      void *array[64];
 +      size_t size;
 +      size = backtrace(array, 64);
 +      backtrace_symbols_fd(array, size, STDERR_FILENO);
 +#endif
 +}
 +
 +/*
 + * THIS IS A SIGNAL HANDLER.
 + * Legal functions: https://pubs.opengroup.org/onlinepubs/9799919799/
 + */
 +static void
 +do_cleanup(int signum)
 +{
 +      if (signum == SIGSEGV || signum == SIGBUS)
 +              print_stack_trace();
 +
 +      cache_atexit();
 +      output_atexit();
 +
 +      /* Trigger default handler */
 +      /* XXX unsafe on multithreaded */
 +      signal(signum, SIG_DFL);
 +      kill(getpid(), signum);
 +}
 +
 +/* Remember to enable -rdynamic (See print_stack_trace()). */
 +void
 +register_signal_handlers(void)
 +{
 +      /* Important: All of these need to terminate by default */
 +      int const cleanups[] = {
 +          SIGFPE, SIGSEGV, SIGBUS, SIGABRT, SIGSYS,   /* 24.2.1 */
 +          SIGTERM, SIGINT, SIGQUIT, SIGHUP,           /* 24.2.2 */
 +          SIGUSR1, SIGUSR2,                           /* 24.2.7 */
 +          0
 +      };
 +      struct sigaction action;
 +      unsigned int i;
 +
 +      setup_backtrace();
 +
 +      memset(&action, 0, sizeof(action));
 +      action.sa_handler = do_cleanup;
 +      sigfillset(&action.sa_mask);
 +      action.sa_flags = 0;
 +
 +      for (i = 0; cleanups[i]; i++)
 +              if (sigaction(cleanups[i], &action, NULL) < 0)
 +                      pr_op_err("'%s' signal action registration failure: %s",
 +                          strsignal(cleanups[i]), strerror(errno));
 +
 +      /*
 +       * SIGPIPE can be triggered by any I/O function. libcurl is particularly
 +       * tricky:
 +       *
 +       * > libcurl makes an effort to never cause such SIGPIPEs to trigger,
 +       * > but some operating systems have no way to avoid them and even on
 +       * > those that have there are some corner cases when they may still
 +       * > happen
 +       * (Documentation of CURLOPT_NOSIGNAL)
 +       *
 +       * All SIGPIPE means is "the peer closed the connection for some
 +       * reason."
 +       * Which is a normal I/O error, and should be handled by the normal
 +       * error propagation logic, not by a signal handler.
 +       * So, ignore SIGPIPE.
 +       *
 +       * https://github.com/NICMx/FORT-validator/issues/49
 +       */
 +      action.sa_handler = SIG_IGN;
 +      if (sigaction(SIGPIPE, &action, NULL) < 0)
 +              pr_op_err("SIGPIPE action registration failure: %s",
 +                  strerror(errno));
 +}
index 8cb3af10654e0e759165b67dead5b54fc2b795a1,910fedd6d68eb5b7c313c5533f724401ea7ab871..824060237d5b99a9804bc31c44517a3fddfff4e8
@@@ -1,12 -1,13 +1,13 @@@
  #include "slurm/db_slurm.h"
  
  #include <errno.h>
+ #include <string.h>
  #include <time.h>
  
 +#include "base64.h"
  #include "common.h"
 -#include "crypto/base64.h"
 -#include "data_structure/array_list.h"
  #include "log.h"
 +#include "types/arraylist.h"
  
  struct slurm_prefix_wrap {
        struct slurm_prefix element;
index 7c18aecedfeee9f3971a798cb80ddc42d0f4e870,2eefde3b37095b1dffb566be0f31703334e1cf9d..426740264465d4de103b5e2765b051eea58efd96
@@@ -1,15 -1,8 +1,9 @@@
 -#ifndef SRC_SLURM_db_slurm_H_
 -#define SRC_SLURM_db_slurm_H_
 +#ifndef SRC_SLURM_DB_SLURM_H_
 +#define SRC_SLURM_DB_SLURM_H_
  
  #include <openssl/evp.h>
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <strings.h>
 +#include <stddef.h>
  #include <sys/queue.h>
- #include <sys/types.h>
- #include <unistd.h>
  
  #include "types/router_key.h"
  #include "types/vrp.h"
index d93e7619cc9794e9fda995ad8263d57b3ebafff5,883a57dd764ea78057462effcda305fdf2c2c87a..6062762d4b135850c8c6267927d6d7c4f39f147f
@@@ -1,12 -1,10 +1,13 @@@
  #include "slurm/slurm_loader.h"
  
  #include <errno.h>
 +#include <openssl/sha.h>
++#include <string.h>
  
 +#include "alloc.h"
  #include "common.h"
  #include "config.h"
 -#include "crypto/hash.h"
 +#include "hash.h"
  #include "log.h"
  #include "slurm/slurm_parser.h"
  
index afc54d42e5fb5b2cf45034ca2faf58b267d85d05,44fcd38e1eaeb76974e9f9ba37c16f6295f635ce..71decdfa52039a5abe117b93cbbe1ad6f581167e
@@@ -2,10 -2,13 +2,9 @@@
  
  #include <errno.h>
  #include <openssl/asn1.h>
 -#include <stdlib.h>
 -#include <string.h>
 -#include <strings.h>
  
  #include "algorithm.h"
--#include "alloc.h"
 -#include "crypto/base64.h"
 +#include "base64.h"
  #include "json_util.h"
  #include "log.h"
  #include "slurm/db_slurm.h"
diff --cc src/stats.c
index 0000000000000000000000000000000000000000,96dca04e8f1b54faeaf5ffa17b8504481a31665c..5816ff036649846ef32b04a3badfddba5a352fc7
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,187 +1,187 @@@
 -#include "data_structure/uthash.h"
+ #include "stats.h"
+ #include <stdarg.h>
+ #include <time.h>
+ #include "alloc.h"
+ #include "common.h"
+ #include "log.h"
++#include "types/uthash.h"
+ struct stats_gauge {
+       char *name;
+       unsigned int value;
+       time_t timestamp;
+       UT_hash_handle hh;
+ };
+ static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
+ static struct stats_gauge *gauges;
+ struct stats_gauge *stat_rtr_ready;
+ struct stats_gauge *stat_rtr_connections;
+ /* Steals ownership of @name */
+ static struct stats_gauge *
+ add_gauge(char *name, size_t namelen, unsigned int value)
+ {
+       struct stats_gauge *old;
+       struct stats_gauge *new;
+       struct stats_gauge *delete;
+       struct stats_gauge *result;
+       new = pzalloc(sizeof(struct stats_gauge));
+       new->name = name;
+       new->value = value;
+       new->timestamp = time(NULL);
+       if (namelen == 0)
+               namelen = strlen(name);
+       mutex_lock(&lock);
+       HASH_FIND(hh, gauges, name, namelen, old);
+       if (old != NULL) {
+               old->value = value;
+               old->timestamp = new->timestamp;
+               delete = new;
+               result = old;
+       } else {
+               HASH_ADD_KEYPTR(hh, gauges, name, namelen, new);
+               delete = NULL;
+               result = new;
+       }
+       mutex_unlock(&lock);
+       if (delete) {
+               free(delete->name);
+               free(delete);
+       }
+       return result;
+ }
+ #define ADD_GAUGE(name) add_gauge(pstrdup(name), 0, 0)
+ int
+ stats_setup(void)
+ {
+       stat_rtr_ready = ADD_GAUGE("fort_rtr_ready");
+       stat_rtr_connections = ADD_GAUGE("fort_rtr_current_connections");
+       return 0;
+ }
+ void
+ stats_teardown(void)
+ {
+       struct stats_gauge *gauge, *tmp;
+       HASH_ITER(hh, gauges, gauge, tmp) {
+               HASH_DEL(gauges, gauge);
+               free(gauge->name);
+               free(gauge);
+       }
+ }
+ void
+ stats_gauge_set(struct stats_gauge *gauge, unsigned int value)
+ {
+       time_t now = time(NULL);
+       mutex_lock(&lock);
+       gauge->value = value;
+       gauge->timestamp = now;
+       mutex_unlock(&lock);
+ }
+ void
+ stats_set_tal_vrps(char const *tal_path, char const *proto, unsigned int value)
+ {
+       char const *ta, *dot;
+       size_t talen;
+       size_t baselen;
+       size_t keylen;
+       char *key;
+       int chars;
+       ta = strrchr(tal_path, '/');
+       ta = (ta == NULL) ? tal_path : (ta + 1);
+       dot = strrchr(ta, '.');
+       talen = dot ? (dot - ta) : strlen(ta);
+       baselen = strlen("fort_valid_vrps_total{ta=\"\",proto=\"\"}");
+       keylen = baselen + talen + strlen(proto) + 1;
+       key = pmalloc(keylen);
+       chars = snprintf(key, keylen,
+           "fort_valid_vrps_total{ta=\"%.*s\",proto=\"%s\"}",
+           (int)talen, ta, proto);
+       if (chars < 0 || keylen <= chars) {
+               free(key);
+               pr_op_warn("Cannot create valid_vrps_total stat: %d", chars);
+               return;
+       }
+       add_gauge(key, keylen - 1, value);
+ }
+ struct stats_buffer {
+       char *str;
+       char *cursor;
+       size_t capacity;
+ };
+ static bool
+ printf_buf(struct stats_buffer *buf, char const *fmt, ...)
+ {
+       size_t available;
+       int written;
+       va_list ap;
+       available = buf->capacity - (buf->cursor - buf->str);
+       va_start(ap, fmt);
+       written = vsnprintf(buf->cursor, available, fmt, ap);
+       va_end(ap);
+       if (written < 0 || available <= written)
+               return false;
+       buf->cursor += written;
+       return true;
+ }
+ char *
+ stats_export(void)
+ {
+       struct stats_buffer buf;
+       struct stats_gauge *gauge, *tmp;
+       buf.capacity = 1024;
+       buf.str = buf.cursor = pmalloc(buf.capacity);
+       HASH_ITER(hh, gauges, gauge, tmp) {
+               if (!printf_buf(&buf, "%s %u", gauge->name, gauge->value))
+                       goto cancel;
+               if (gauge->timestamp != ((time_t)-1))
+                       if (!printf_buf(&buf, " %jd", (intmax_t)gauge->timestamp))
+                               goto cancel;
+               if (!printf_buf(&buf, "\n"))
+                       goto cancel;
+       }
+       if (!printf_buf(&buf, "# EOF\n"))
+               goto cancel;
+       if (buf.cursor >= buf.str + buf.capacity)
+               goto cancel;
+       *buf.cursor = '\0';
+       return buf.str;
+ cancel:
+       free(buf.str);
+       pr_op_err("Cannot create Prometheus response: Too many stats");
+       return NULL;
+ }
diff --cc src/stream.c
index 6e0ba4317fe2bf91a7868b9a178db0c1cf02b5ed,0000000000000000000000000000000000000000..a7d5d07569448e16f9fcda5a229ad36e8ad10240
mode 100644,000000..100644
--- /dev/null
@@@ -1,78 -1,0 +1,79 @@@
- #include <stdbool.h>
- #include <stddef.h>
 +#include "stream.h"
 +
 +#include <errno.h>
++#include <stdlib.h>
++#include <sys/types.h>
++#include <unistd.h>
 +
 +#include "alloc.h"
 +#include "log.h"
 +
 +void
 +rstream_init(struct read_stream *stream, int fd, size_t initial_capacity)
 +{
 +      stream->fd = fd;
 +      stream->buffer = pmalloc(initial_capacity);
 +      stream->len = 0;
 +      stream->capacity = initial_capacity;
 +}
 +
 +void
 +rstream_close(struct read_stream *stream, bool do_close)
 +{
 +      if (stream->fd == -1)
 +              return;
 +
 +      if (do_close)
 +              close(stream->fd);
 +      free(stream->buffer);
 +      stream->fd = -1;
 +}
 +
 +/*
 + * Reads until exactly @len bytes (sleeping if necessary), EOS or error.
 + * @stream's capacity must be >= len.
 + *
 + * Returns:
 + * - >= 0: Number of bytes read (< @len only if EOS reached).
 + * - < 0: error; remove sign for proper code.
 + */
 +int
 +rstream_full_read(struct read_stream *stream, size_t len)
 +{
 +      size_t offset;
 +      ssize_t rd;
 +
 +      if (stream->buffer == NULL || stream->capacity < len)
 +              return ENOSPC;
 +
 +      for (offset = 0; offset < len; offset += rd) {
 +              rd = read(stream->fd, stream->buffer + offset, len - offset);
 +              if (rd < 0)
 +                      return errno;
 +              if (rd == 0)
 +                      break;
 +      }
 +
 +      return offset;
 +}
 +
 +/* Full write (sleeps if necessary) or error. */
 +int
 +stream_full_write(int fd, unsigned char const *buf, size_t len)
 +{
 +      ssize_t wr;
 +      size_t offset;
 +
 +      offset = 0;
 +      do {
 +              wr = write(fd, buf + offset, len);
 +              if (wr < 0)
 +                      return errno;
 +              if (wr > len)
 +                      pr_crit("wr > len: %zd > %zu", wr, len);
 +              len -= wr;
 +              offset += wr;
 +      } while (len > 0);
 +
 +      return 0;
 +}
diff --cc src/task.c
index 1410a4513288a3cfd0291f88b835d317b0f34a7f,0000000000000000000000000000000000000000..ad0fb8f3a75245b5c0195df25be4052e46bf2fd3
mode 100644,000000..100644
--- /dev/null
@@@ -1,275 -1,0 +1,274 @@@
- #include "alloc.h"
- #include "common.h"
 +#include "task.h"
 +
 +#include <errno.h>
 +
 +#include "log.h"
++#include "object/certificate.h"
 +
 +STAILQ_HEAD(validation_tasks, validation_task);
 +
 +/* Queued, not yet claimed tasks */
 +static struct validation_tasks waiting;
 +/* Queued, but not yet available for claiming */
 +static struct validation_tasks dormant;
 +/*
 + * Total currently existing tasks
 + * (length(@waiting) + length(@dormant) + total active tasks)
 + */
 +static int ntasks;
 +
 +static bool enabled = true;
 +
 +static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
 +static pthread_cond_t awakener = PTHREAD_COND_INITIALIZER;
 +
 +static char const *
 +task_name(struct validation_task *task)
 +{
 +      switch (task->type) {
 +      case VTT_RPP:
 +              return uri_str(&task->u.ca->map.url);
 +      case VTT_TAL:
 +              return task->u.tal;
 +      }
 +
 +      pr_crit("Unknown task type: %u", task->type);
 +      return NULL;
 +}
 +
 +static void
 +task_free(struct validation_task *task)
 +{
 +      switch (task->type) {
 +      case VTT_RPP:
 +              cer_free(task->u.ca);
 +              break;
 +      case VTT_TAL:
 +              free(task->u.tal);
 +              break;
 +      }
 +
 +      free(task);
 +}
 +
 +void
 +task_setup(void)
 +{
 +      STAILQ_INIT(&waiting);
 +      STAILQ_INIT(&dormant);
 +      ntasks = 0;
 +      enabled = true;
 +      panic_on_fail(pthread_mutex_init(&lock, NULL), "pthread_mutex_init");
 +      panic_on_fail(pthread_cond_init(&awakener, NULL), "pthread_cond_init");
 +}
 +
 +static void
 +cleanup_tasks(struct validation_tasks *tasks)
 +{
 +      struct validation_task *task;
 +
 +      while (!STAILQ_EMPTY(tasks)) {
 +              task = STAILQ_FIRST(tasks);
 +              STAILQ_REMOVE_HEAD(tasks, lh);
 +              task_free(task);
 +      }
 +}
 +
 +static void
 +cleanup(void)
 +{
 +      enabled = false;
 +      ntasks = 0;
 +      cleanup_tasks(&waiting);
 +      cleanup_tasks(&dormant);
 +}
 +
 +void
 +task_start(void)
 +{
 +      cleanup();
 +      enabled = true;
 +}
 +
 +/* Returns true if the module had already been stopped. */
 +bool
 +task_stop(void)
 +{
 +      bool result;
 +
 +      mutex_lock(&lock);
 +      result = !enabled;
 +      cleanup();
 +      mutex_unlock(&lock);
 +
 +      return result;
 +}
 +
 +void
 +task_teardown(void)
 +{
 +      pthread_mutex_destroy(&lock);
 +      pthread_cond_destroy(&awakener);
 +}
 +
 +static int
 +enqueue_task(struct validation_task *task)
 +{
 +      mutex_lock(&lock);
 +      if (enabled) {
 +              STAILQ_INSERT_TAIL(&waiting, task, lh);
 +              task = NULL;
 +              ntasks++;
 +      }
 +      mutex_unlock(&lock);
 +
 +      if (task) {
 +              task_free(task); /* Couldn't queue */
 +              return 0;
 +      }
 +
 +      return 1;
 +}
 +
 +unsigned int
 +task_enqueue_tal(char const *tal_path)
 +{
 +      struct validation_task *task;
 +
 +      task = pmalloc(sizeof(struct validation_task));
 +      task->type = VTT_TAL;
 +      task->u.tal = pstrdup(tal_path);
 +
 +      return enqueue_task(task);
 +}
 +
 +/*
 + * Defers a task for later.
 + * Call task_wakeup() once you've queued all your tasks.
 + * Returns number of deferred tasks.
 + */
 +unsigned int
 +task_enqueue_rpp(struct cache_mapping *map, struct rpki_certificate *parent)
 +{
 +      struct validation_task *task;
 +      struct rpki_certificate *ca;
 +
 +      atomic_fetch_add(&parent->refcount, 1);
 +
 +      ca = pzalloc(sizeof(struct rpki_certificate));
 +      uri_copy(&ca->map.url, &map->url);
 +      ca->map.path = pstrdup(map->path);
 +      ca->parent = parent;
 +      atomic_init(&ca->refcount, 1);
 +
 +      task = pmalloc(sizeof(struct validation_task));
 +      task->type = VTT_RPP;
 +      task->u.ca = ca;
 +
 +      return enqueue_task(task);
 +}
 +
 +/* Steals ownership of @task. */
 +void
 +task_requeue_dormant(struct validation_task *task)
 +{
 +      mutex_lock(&lock);
 +      if (enabled) {
 +              STAILQ_INSERT_TAIL(&dormant, task, lh);
 +              task = NULL;
 +      }
 +      mutex_unlock(&lock);
 +
 +      if (task)
 +              task_free(task); /* Couldn't queue */
 +}
 +
 +/* Wakes up all sleeping task threads. */
 +void
 +task_wakeup(void)
 +{
 +      mutex_lock(&lock);
 +      panic_on_fail(pthread_cond_broadcast(&awakener),
 +          "pthread_cond_broadcast");
 +      mutex_unlock(&lock);
 +}
 +
 +/* Upgrades all dormant tasks, and wakes up all sleeping task threads. */
 +void
 +task_wakeup_dormants(void)
 +{
 +      mutex_lock(&lock);
 +      STAILQ_CONCAT(&waiting, &dormant);
 +      panic_on_fail(pthread_cond_broadcast(&awakener),
 +          "pthread_cond_broadcast");
 +      mutex_unlock(&lock);
 +}
 +
 +/*
 + * Frees the @prev previous task, and returns the next one.
 + *
 + * If no task is available yet, will sleep until someone calls task_wakeup() or
 + * task_wakeup_dormants().
 + * If all the tasks are done, returns NULL.
 + *
 + * Steals ownership of @prev.
 + * Assumes at least one task has been queued before the first dequeue.
 + */
 +struct validation_task *
 +task_dequeue(struct validation_task *prev)
 +{
 +      struct validation_task *task;
 +      struct timespec timeout;
 +      int error;
 +
 +      if (prev)
 +              task_free(prev);
 +      timeout.tv_nsec = 0;
 +
 +      mutex_lock(&lock);
 +
 +      if (!enabled)
 +              goto end;
 +
 +      if (prev) {
 +              ntasks--;
 +              if (ntasks < 0)
 +                      pr_crit("active < 0: %d", ntasks);
 +      }
 +
 +      while (ntasks > 0) {
 +              pr_op_debug("task_dequeue(): %u existing tasks.", ntasks);
 +
 +              task = STAILQ_FIRST(&waiting);
 +              if (task != NULL) {
 +                      STAILQ_REMOVE_HEAD(&waiting, lh);
 +                      mutex_unlock(&lock);
 +                      pr_op_debug("task_dequeue(): Claimed task '%s'.",
 +                          task_name(task));
 +                      return task;
 +              }
 +
 +              pr_op_debug("task_dequeue(): Sleeping...");
 +              timeout.tv_sec = time_fatal() + 10;
 +              error = pthread_cond_timedwait(&awakener, &lock, &timeout);
 +              switch (error) {
 +              case 0:
 +                      pr_op_debug("task_dequeue(): Woke up by cond.");
 +                      break;
 +              case ETIMEDOUT:
 +                      pr_op_debug("task_dequeue(): Woke up by timeout.");
 +                      break;
 +              case EINTR:
 +                      pr_op_debug("task_dequeue(): Interrupted by signal.");
 +                      goto end;
 +              default:
 +                      panic_on_fail(error, "pthread_cond_wait");
 +              }
 +      }
 +
 +      pr_op_debug("task_dequeue(): No more tasks; done.");
 +      panic_on_fail(pthread_cond_broadcast(&awakener),
 +          "pthread_cond_broadcast");
 +end:  mutex_unlock(&lock);
 +      return NULL;
 +}
diff --cc src/task.h
index f47d14986c508c708abac49aca43a96df46bfc4e,0000000000000000000000000000000000000000..0d2ce245f6a012faf0a8fc350bb51c4675ead087
mode 100644,000000..100644
--- /dev/null
@@@ -1,35 -1,0 +1,36 @@@
- #include "object/certificate.h"
 +#ifndef SRC_TASK_H_
 +#define SRC_TASK_H_
 +
 +#include <sys/queue.h>
 +
++#include "asn1/signed_data.h"
++#include "object/tal.h"
 +#include "types/map.h"
 +
 +enum validation_task_type {
 +      VTT_RPP,
 +      VTT_TAL,
 +};
 +
 +struct validation_task {
 +      enum validation_task_type type;
 +      union {
 +              char *tal;
 +              struct rpki_certificate *ca;
 +      } u;
 +      STAILQ_ENTRY(validation_task) lh;
 +};
 +
 +void task_setup(void);
 +void task_start(void);
 +bool task_stop(void);
 +void task_teardown(void);
 +
 +unsigned int task_enqueue_tal(char const *);
 +unsigned int task_enqueue_rpp(struct cache_mapping *, struct rpki_certificate *);
 +void task_requeue_dormant(struct validation_task *);
 +void task_wakeup(void);
 +void task_wakeup_dormants(void);
 +struct validation_task *task_dequeue(struct validation_task *);
 +
 +#endif /* SRC_TASK_H_ */
index a5e38bb652a25ddc9ddfa33d268ae63f5894bcd5,e84a53e7cabfaa1dcb37b0b590f91559126c0393..17eff19511ab2fc30ec93a5e407e2ffaa8316203
@@@ -1,5 -1,7 +1,7 @@@
 -#include "thread/thread_pool.h"
 +#include "thread_pool.h"
  
+ #include <stdlib.h>
+ #include <string.h>
  #include <sys/queue.h>
  
  #include "alloc.h"
index 8524d7ae4e015e6a3f2c4cb71f10f200c6d1d179,1d891011dc2cc2ed6f87357d1baac8e890f27fb6..ed0b7a9bfe23efcdaba983dc90500235b3fd90cb
@@@ -2,9 -2,9 +2,8 @@@
  
  #include <pthread.h>
  
- #include "alloc.h"
  #include "log.h"
  
 -static pthread_key_t state_key;
  static pthread_key_t filenames_key;
  
  struct filename_stack {
index dc5ffc988f223822a5bf809aeb809f357acfdde4,911015fb48b0cd45e42fb38ab57eb3e35ba18000..b07e0a567066e0884cb5944539f2de9a7c5bc75a
@@@ -1,9 -1,9 +1,10 @@@
  #include "types/address.h"
  
  #include <errno.h>
++#include <stdlib.h>
++#include <string.h>
  
  #include "log.h"
--#include "thread_var.h"
  
  static void
  init_quadrant(struct in6_addr *addr, unsigned int slot, uint32_t value)
index 2e8f68b72c5b3fba9dfae7bedb982c5dc2fe48d0,0000000000000000000000000000000000000000..29af9b1da15edf6df0ac9e419b651b22ead28433
mode 100644,000000..100644
--- /dev/null
@@@ -1,15 -1,0 +1,9 @@@
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <strings.h>
- #include <sys/types.h>
- #include <unistd.h>
 +#ifndef SRC_TYPES_ARRAY_H_
 +#define SRC_TYPES_ARRAY_H_
 +
 +#include <stddef.h>
 +
 +typedef size_t array_index;
 +#define ARRAY_LEN(array) (sizeof(array) / sizeof((array)[0]))
 +
 +#endif /* SRC_TYPES_ARRAY_H_ */
index 686f58ef2093c1ce8733397ae25fdef95442958d,1bec87695a7c730b1cf0d053910ebfa09292513b..4610346695df2b296c419f4fa4a70756195e978d
@@@ -1,8 -1,11 +1,11 @@@
 -#ifndef SRC_DATA_STRUCTURE_ARRAY_LIST_H_
 -#define SRC_DATA_STRUCTURE_ARRAY_LIST_H_
 +#ifndef SRC_TYPES_ARRAYLIST_H_
 +#define SRC_TYPES_ARRAYLIST_H_
  
+ #include <stdlib.h>
+ #include <strings.h>
  #include "alloc.h"
 -#include "data_structure/common.h"
 +#include "types/array.h"
  
  #define DEFINE_ARRAY_LIST_STRUCT(name, elem_type)                     \
        struct name {                                                   \
                size_t capacity;                                        \
        }
  
--#define DECLARE_ARRAY_LIST_FUNCTIONS(name, elem_type)                 \
--      void name##_init(struct name *);                                \
--      void name##_cleanup(struct name *, void (*cb)(elem_type *));    \
--      void name##_add(struct name *list, elem_type *elem);
--
  #define DEFINE_ARRAY_LIST_FUNCTIONS(name, elem_type, modifiers)               \
        modifiers void                                                  \
        name##_init(struct name *list)                                  \
diff --cc src/types/map.c
index 1d3b642a77b48c35b0ff266ef934fd847ae4574a,0000000000000000000000000000000000000000..4ad35992ffe66321513856a88712a8108051f142
mode 100644,000000..100644
--- /dev/null
@@@ -1,48 -1,0 +1,47 @@@
- #include "alloc.h"
 +#include "types/map.h"
 +
 +#include "config.h"
 +#include "log.h"
 +#include "types/path.h"
 +
 +static char const *
 +map_get_printable(struct cache_mapping const *map, enum filename_format format)
 +{
 +      switch (format) {
 +      case FNF_GLOBAL:
 +              return uri_str(&map->url);
 +      case FNF_LOCAL:
 +              return map->path;
 +      case FNF_NAME:
 +              return path_filename(uri_str(&map->url));
 +      }
 +
 +      pr_crit("Unknown file name format: %u", format);
 +      return NULL;
 +}
 +
 +char const *
 +map_val_get_printable(struct cache_mapping const *map)
 +{
 +      return map_get_printable(map, config_get_val_log_file_format());
 +}
 +
 +char const *
 +map_op_get_printable(struct cache_mapping const *map)
 +{
 +      return map_get_printable(map, config_get_op_log_file_format());
 +}
 +
 +void
 +map_copy(struct cache_mapping *dst, struct cache_mapping const *src)
 +{
 +      uri_copy(&dst->url, &src->url);
 +      dst->path = pstrdup(src->path);
 +}
 +
 +void
 +map_cleanup(struct cache_mapping *map)
 +{
 +      uri_cleanup(&map->url);
 +      free(map->path);
 +}
index 76214d990e9f512acec6048d9fdf5c7cd7e10337,e8c3d728e9c605b7dc5b3163ad12a652495f3ec1..11419440b05443ad563506b327ed3fffa1909099
@@@ -3,10 -3,10 +3,11 @@@
  #include <openssl/asn1.h>
  #include <openssl/obj_mac.h>
  #include <openssl/objects.h>
--#include <syslog.h>
++#include <stdlib.h>
++#include <string.h>
  
 +#include "alloc.h"
  #include "log.h"
 -#include "thread_var.h"
  
  /**
   * It's an RFC5280 name, but from RFC 6487's perspective.
index 8456b52d7a75c0d7e59e2df0b508f455e2df2999,0000000000000000000000000000000000000000..bbbdd45f6b9a330c73a27a0b042e39a2582806ea
mode 100644,000000..100644
--- /dev/null
@@@ -1,71 -1,0 +1,72 @@@
- #include "log.h"
 +#include "types/path.h"
 +
++#include <string.h>
++
 +#include "alloc.h"
 +#include "types/str.h"
 +
 +char const *
 +path_filename(char const *path)
 +{
 +      char *slash = strrchr(path, '/');
 +      return slash ? (slash + 1) : path;
 +}
 +
 +static void
 +trim_leading_slashes(struct sized_string *str)
 +{
 +      while (str->str[0] == '/') {
 +              str->str++;
 +              str->len--;
 +      }
 +}
 +
 +static void
 +trim_trailing_slashes(struct sized_string *str)
 +{
 +      while (str->len > 1 && str->str[str->len - 1] == '/')
 +              str->len--;
 +}
 +
 +/* Result needs cleanup, cannot return NULL. */
 +char *
 +path_join(char const *path1, char const *path2)
 +{
 +      struct sized_string p1;
 +      struct sized_string p2;
 +      size_t n;
 +      char *result;
 +
 +      if (path1) {
 +              p1.str = path1;
 +              p1.len = strlen(path1);
 +              trim_trailing_slashes(&p1);
 +      } else {
 +              memset(&p1, 0, sizeof(p1));
 +      }
 +
 +      if (path2) {
 +              p2.str = path2;
 +              p2.len = strlen(path2);
 +              trim_leading_slashes(&p2);
 +      } else {
 +              memset(&p2, 0, sizeof(p2));
 +      }
 +
 +      if (p1.len == 0 && p2.len == 0)
 +              return pstrdup("");
 +      if (p1.len == 0 || p1.str[0] == '\0')
 +              return pstrndup(p2.str, p2.len);
 +      if (p2.len == 0 || p2.str[0] == '\0')
 +              return pstrndup(p1.str, p1.len);
 +
 +      n = p1.len + p2.len + 2;
 +      result = pmalloc(n);
 +
 +      memcpy(result, p1.str, p1.len);
 +      result[p1.len] = '/';
 +      memcpy(result + p1.len + 1, p2.str, p2.len);
 +      result[n - 1] = '\0';
 +
 +      return result;
 +}
diff --cc src/types/rpp.c
index 8f01b378239fcc3ae56993351fea09c3d2eb83fe,0000000000000000000000000000000000000000..b1a6fba52ba0897ea53611819e833faf1df29418
mode 100644,000000..100644
--- /dev/null
@@@ -1,23 -1,0 +1,21 @@@
- #include "types/array.h"
 +#include "types/rpp.h"
 +
 +void
 +rpp_cleanup(struct rpp *rpp)
 +{
 +      array_index i;
 +
 +      for (i = 0; i < rpp->nfiles; i++)
 +              map_cleanup(&rpp->files[i]);
 +      free(rpp->files);
 +      rpp->files = NULL;
 +      rpp->nfiles = 0;
 +
 +      rpp->crl.map = NULL;
 +      if (rpp->crl.obj != NULL) {
 +              X509_CRL_free(rpp->crl.obj);
 +              rpp->crl.obj = NULL;
 +      }
 +
 +      mftm_cleanup(&rpp->mft);
 +}
diff --cc src/types/rpp.h
index 1f40e5b3775aa0d8494d6be17d80d01770f76c65,0000000000000000000000000000000000000000..ea8d16ba493e9fa8e75e342bbeae538ed8c1fdbe
mode 100644,000000..100644
--- /dev/null
@@@ -1,31 -1,0 +1,31 @@@
- #include <time.h>
 +#ifndef SRC_RPP_H_
 +#define SRC_RPP_H_
 +
 +#include <openssl/x509.h>
++#include <sys/stat.h>
 +
 +#include "asn1/asn1c/INTEGER.h"
 +#include "types/map.h"
 +
 +struct mft_meta {
 +      INTEGER_t num;                          /* Manifest's manifestNumber */
 +      time_t update;                          /* Manifest's thisUpdate */
 +};
 +
 +/* Repository Publication Point */
 +struct rpp {
 +      struct cache_mapping *files;
 +      size_t nfiles;                          /* Number of maps in @files */
 +
 +      struct {
 +              struct cache_mapping *map;      /* Points to @files entry */
 +              X509_CRL *obj;
 +      } crl;
 +
 +      struct mft_meta mft;
 +};
 +
 +#define mftm_cleanup(m) INTEGER_cleanup(&(m)->num);
 +void rpp_cleanup(struct rpp *);
 +
 +#endif /* SRC_RPP_H_ */
index c63ecebb24fa1f1c345b2fe7d612cd4a6b47fbf7,e6d10bf108344d19f54eac871683e417a8c9f6d3..2871558b4cc6c612ea1c0d52af6888d8c88ed57c
@@@ -1,5 -1,9 +1,9 @@@
 -#include "sorted_array.h"
 +#include "types/sorted_array.h"
  
+ #include <stdlib.h>
+ #include <string.h>
+ #include <strings.h>
  #include "alloc.h"
  #include "log.h"
  
Simple merge
diff --cc src/types/str.c
index 84b83bb02fd18e43bad022f4cad95695395b98e7,05e4fcb4675648274abc05f142309a31fb4fda7a..91f68116dc9e7a627712dbaeac9c104f1a024250
@@@ -1,42 -1,11 +1,43 @@@
 -#include "str_token.h"
 +#include "types/str.h"
  
- #include <string.h>
 +#include <errno.h>
  #include <openssl/bio.h>
+ #include <stdint.h>
++#include <stdlib.h>
+ #include <string.h>
  
  #include "alloc.h"
- #include "array.h"
  #include "log.h"
- #include "types/path.h"
++#include "types/array.h"
 +
 +/* Allocates the result; will need free(). Never returns NULL. */
 +char *
 +str_concat(char const *s1, char const *s2)
 +{
 +      size_t n;
 +      char *result;
 +      int written;
 +
 +      n = strlen(s1) + strlen(s2) + 1;
 +      result = pmalloc(n);
 +
 +      written = snprintf(result, n, "%s%s", s1, s2);
 +      if (written != n - 1)
 +              pr_crit("str_concat: %zu %d %s %s", n, written, s1, s2);
 +
 +      return result;
 +}
 +
 +int
 +hex2ulong(char const *hex, unsigned long *ulong)
 +{
 +      char *endptr;
 +
 +      errno = 0;
 +      *ulong = strtoul(hex, &endptr, 16);
 +
 +      return (errno || endptr[0] != 0) ? -1 : 0;
 +}
  
  /**
   * Does not assume that @string is NULL-terminated.
diff --cc src/types/str.h
index 33a9a9e91f077958b4e2a765aede49f4fba45fe2,016f6e7947eec51442ff8a6bba904ff31fdc87bf..23eb35c0f522a922a6a749ab544e53e5bffb8cb5
@@@ -4,16 -4,8 +4,17 @@@
  #include <openssl/asn1.h>
  #include <openssl/bn.h>
  #include <stdbool.h>
+ #include <stddef.h>
  
 +struct sized_string {
 +      char const *str;
 +      size_t len;
 +};
 +
 +char *str_concat(char const *, char const *);
 +
 +int hex2ulong(char const *, unsigned long *);
 +
  int ia5s2string(ASN1_IA5STRING *, char **);
  int BN2string(BIGNUM *, char **);
  
diff --cc src/types/uri.c
index 7dfe0c1cc3e431a68104d696c767d86624737d6a,c6e2039d25de926f49b4e142dee971ed150b922f..50d02bdb38f126d1b3cb2cebd751ad962d891e58
  #include "types/uri.h"
  
- #include "alloc.h"
 +#include <arpa/inet.h>
 +#include <errno.h>
++#include <sys/socket.h>
 +
  #include "common.h"
 -#include "config.h"
 -#include "data_structure/path_builder.h"
  #include "log.h"
- #include "types/path.h"
 -#include "object/tal.h"
  
 -/**
 - * Design notes:
 - *
 - * Because we need to generate @local from @global, @global's allowed character
 - * set must be a subset of @local. Because this is Unix, @local must never
 - * contain NULL (except as a terminating character). Therefore, even though IA5
 - * allows NULL, @global won't.
 - *
 - * Because we will simply embed @global (minus "rsync://") into @local, @local's
 - * encoding must be IA5-compatible. In other words, UTF-16 and UTF-32 are out of
 - * the question.
 - *
 - * Aside from the reference counter, instances are meant to be immutable.
 - */
 -struct rpki_uri {
 -      /**
 -       * "Global URI".
 -       * The one that always starts with "rsync://" or "https://".
 -       *
 -       * These things are IA5-encoded, which means you're not bound to get
 -       * non-ASCII characters.
 -       */
 -      char *global;
 -      /** Length of @global. */
 -      size_t global_len;
 -
 -      /**
 -       * "Local URI".
 -       * The file pointed by @global, but cached in the local filesystem.
 -       *
 -       * I can't find a standard that defines this, but lots of complaints on
 -       * the Internet imply that Unix file paths are specifically meant to be
 -       * C strings.
 -       *
 -       * So just to clarify: This is a string that permits all characters,
 -       * printable or otherwise, except \0. (Because that's the terminating
 -       * character.)
 -       *
 -       * Even though it might contain characters that are non-printable
 -       * according to ASCII, we assume that we can just dump it into the
 -       * output without trouble, because the input should have the same
 -       * encoding as the output.
 -       *
 -       * Technically, "global" URI "https://a.b.c/d/..///./d" is not the same
 -       * identifier as "https://a.b.c/d", but since we're supposed to download
 -       * to a filesystem where "https/a.b.c/d" is the same file as
 -       * "https/a.b.c/d/..///./d", @local will always be normalized.
 -       */
 -      char *local;
 -      /* "local_len" is never needed right now. */
 -
 -      enum uri_type type;
 -      bool is_notif; /* Does it point to an RRDP Notification? */
 -
 -      unsigned int references;
 +#define URI_ALLOW_UNKNOWN_SCHEME (1 << 0)
 +
 +static error_msg EM_SCHEME_EMPTY = "Scheme seems empty";
 +static error_msg EM_SCHEME_1ST = "First scheme character is not a letter";
 +static error_msg EM_SCHEME_NTH = "Scheme character is not letter, digit, plus, period or hyphen";
 +static error_msg EM_SCHEME_NOCOLON = "Scheme not terminated";
 +static error_msg EM_SCHEME_UNKNOWN = "Unknown scheme";
 +static error_msg EM_SCHEME_NOTREMOTE = "Missing \"://\"";
 +static error_msg EM_PCT_NOTHEX = "Invalid hexadecimal digit in percent encoding";
 +static error_msg EM_PCT_NOT3 = "Unterminated percent-encoding";
 +static error_msg EM_UTF8 = "Invalid UTF-8";
 +static error_msg EM_USERINFO_BADCHR = "Illegal character in userinfo component";
 +static error_msg EM_USERINFO_DISALLOWED = "Protocol disallows userinfo";
 +static error_msg EM_HOST_BADCHR = "Illegal character in host component";
 +static error_msg EM_HOST_EMPTY = "Protocol disallows empty host";
 +static error_msg EM_HOST_LITERAL = "Unparseable IP literal in the host";
 +static error_msg EM_PORT_BADCHR = "Illegal non-digit character in port component";
 +static error_msg EM_PORT_RANGE = "Port value is out of range";
 +static error_msg EM_PATH_BADCHR = "Illegal character in path component";
 +static error_msg EM_QUERY_DISALLOWED = "Protocol disallows query";
 +static error_msg EM_QF_BADCHR = "Illegal character in query or fragment";
 +static error_msg EM_FRAGMENT_DISALLOWED = "Protocol disallows fragment";
 +
 +struct sized_ustring {
 +      unsigned char const *str;
 +      size_t len;
  };
  
 -/*
 - * @character is an integer because we sometimes receive signed chars, and other
 - * times we get unsigned chars.
 - * Casting a negative char into a unsigned char is undefined behavior.
 - */
 -static int
 -validate_url_character(int character)
 -{
 -      /*
 -       * RFCs 1738 and 3986 define a very specific range of allowed
 -       * characters, but I don't think we're that concerned about URL
 -       * correctness. Validating the URL properly is more involved than simply
 -       * checking legal characters, anyway.
 -       *
 -       * What I really need this validation for is ensure that we won't get
 -       * any trouble later, when we attempt to convert the global URI to a
 -       * local file.
 -       *
 -       * Sample trouble: Getting UTF-8 characters. Why are they trouble?
 -       * Because we don't have any guarantees that the system's file name
 -       * encoding is UTF-8. URIs are not supposed to contain UTF-8 in the
 -       * first place, so we have no reason to deal with encoding conversion.
 -       *
 -       * To be perfectly fair, we have no guarantees that the system's file
 -       * name encoding is ASCII-compatible either, but I need to hang onto
 -       * SOMETHING.
 -       *
 -       * (Asking users to use UTF-8 is fine, but asking users to use something
 -       * ASCII-compatible is a little better.)
 -       *
 -       * So just make sure that the character is printable ASCII.
 -       *
 -       * TODO (next iteration) Consider exhaustive URL validation.
 -       */
 -      return (0x20 <= character && character <= 0x7E)
 -          ? 0
 -          : pr_val_err("URL has non-printable character code '%d'.", character);
 -}
 -
 -/**
 - * Initializes @uri->global* by cloning @str.
 - * This function does not assume that @str is null-terminated.
 - */
 -static int
 -str2global(char const *str, size_t str_len, struct rpki_uri *uri)
 -{
 -      int error;
 -      size_t i;
 +struct uri_buffer {
 +      char *dst;
 +      array_index d;
 +      size_t capacity;
 +};
  
 -      for (i = 0; i < str_len; i++) {
 -              error = validate_url_character(str[i]);
 -              if (error)
 -                      return error;
 -      }
 +struct schema_metadata {
 +      unsigned int default_port;
 +      bool allow_userinfo;
 +      bool allow_empty_host;
 +      bool allow_query;
 +      bool allow_fragment;
 +};
  
 -      uri->global = pmalloc(str_len + 1);
 -      strncpy(uri->global, str, str_len);
 -      uri->global[str_len] = '\0';
 -      uri->global_len = str_len;
 +struct schema_metadata const HTTPS = {
 +      .default_port = 443,
 +      .allow_userinfo = false,
 +      .allow_empty_host = false,
 +      .allow_query = true,
 +      .allow_fragment = true,
 +};
  
 -      return 0;
 -}
 +struct schema_metadata const RSYNC = {
 +      .default_port = 873,
 +      .allow_userinfo = true,
 +      /* Seems actually allowed, but RPKI doesn't like it. */
 +      .allow_empty_host = false,
 +      .allow_query = false,
 +      .allow_fragment = false,
 +};
  
  static bool
 -is_valid_mft_file_chara(uint8_t chara)
 +is_proto(struct sized_ustring *scheme, char const *proto)
  {
 -      return ('a' <= chara && chara <= 'z')
 -          || ('A' <= chara && chara <= 'Z')
 -          || ('0' <= chara && chara <= '9')
 -          || (chara == '-')
 -          || (chara == '_');
 -}
 -
 -/* RFC 9286, section 4.2.2 */
 -static int
 -validate_mft_file(IA5String_t *ia5)
 -{
 -      size_t dot;
 -      size_t i;
 -
 -      if (ia5->size < 5)
 -              return pr_val_err("File name is too short (%zu < 5).", ia5->size);
 -      dot = ia5->size - 4;
 -      if (ia5->buf[dot] != '.')
 -              return pr_val_err("File name seems to lack a three-letter extension.");
 -
 -      for (i = 0; i < ia5->size; i++) {
 -              if (i != dot && !is_valid_mft_file_chara(ia5->buf[i])) {
 -                      return pr_val_err("File name contains illegal character #%u",
 -                          ia5->buf[i]);
 -              }
 -      }
 -
 -      /*
 -       * Actual extension doesn't matter; if there's no handler,
 -       * we'll naturally ignore the file.
 -       */
 -      return 0;
 +      return strncasecmp((char const *)scheme->str, proto, scheme->len) == 0;
  }
  
 -/**
 - * Initializes @uri->global given manifest path @mft and its referenced file
 - * @ia5.
 - *
 - * ie. if @mft is "rsync://a/b/c.mft" and @ia5 is "d.cer", @uri->global will
 - * be "rsync://a/b/d.cer".
 - *
 - * Assumes that @mft is a "global" URL. (ie. extracted from rpki_uri.global.)
 - */
 -static int
 -ia5str2global(struct rpki_uri *uri, char const *mft, IA5String_t *ia5)
 +static struct schema_metadata const *
 +get_metadata(struct sized_ustring *scheme)
  {
 -      char *joined;
 -      char const *slash_pos;
 -      int dir_len;
 -      int error;
 -
 -      /*
 -       * IA5String is a subset of ASCII. However, IA5String_t doesn't seem to
 -       * be guaranteed to be NULL-terminated.
 -       * `(char *) ia5->buf` is fair, but `strlen(ia5->buf)` is not.
 -       */
 -
 -      error = validate_mft_file(ia5);
 -      if (error)
 -              return error;
 -
 -      slash_pos = strrchr(mft, '/');
 -      if (slash_pos == NULL)
 -              return pr_val_err("Manifest URL '%s' contains no slashes.", mft);
 +      if (scheme->len != 5)
 +              return NULL;
  
 -      dir_len = (slash_pos + 1) - mft;
 -      joined = pmalloc(dir_len + ia5->size + 1);
 +      if (is_proto(scheme, "https"))
 +              return &HTTPS;
 +      if (is_proto(scheme, "rsync"))
 +              return &RSYNC;
  
 -      strncpy(joined, mft, dir_len);
 -      strncpy(joined + dir_len, (char *) ia5->buf, ia5->size);
 -      joined[dir_len + ia5->size] = '\0';
 +      return NULL;
 +}
  
 -      uri->global = joined;
 -      uri->global_len = dir_len + ia5->size;
 -      return 0;
 +static bool
 +is_lowercase(unsigned char chr)
 +{
 +      return 'a' <= chr && chr <= 'z';
  }
  
 -struct path_parser {
 -      char const *token;
 -      char const *slash;
 -      size_t len;
 -};
 +static bool
 +is_uppercase(unsigned char chr)
 +{
 +      return 'A' <= chr && chr <= 'Z';
 +}
  
 -/* Return true if there's a new token, false if we're done. */
++/* RFC 9286, section 4.2.2 */
  static bool
 -path_next(struct path_parser *parser)
 +is_lowercase_hex(unsigned char chr)
  {
 -      if (parser->slash == NULL)
 -              return false;
 +      return 'a' <= chr && chr <= 'f';
 +}
  
 -      parser->token = parser->slash + 1;
 -      parser->slash = strchr(parser->token, '/');
 -      parser->len = (parser->slash != NULL)
 -          ? (parser->slash - parser->token)
 -          : strlen(parser->token);
 +static bool
 +is_uppercase_hex(unsigned char chr)
 +{
 +      return 'A' <= chr && chr <= 'F';
 +}
  
 -      return parser->token[0] != 0;
 +static bool
 +is_digit(unsigned char chr)
 +{
 +      return '0' <= chr && chr <= '9';
  }
  
  static bool
diff --cc src/types/uri.h
index 2fa555bf3cc05a1612d39ef2b00508d907eee416,c5886ec82e8b7d3949d6dba937c1e98700454da2..ba877dd457db7aed8e9f50ec53f833a76670ed63
@@@ -2,42 -2,70 +2,44 @@@
  #define SRC_TYPES_URI_H_
  
  #include <stdbool.h>
- #include <stddef.h>
+ #include <string.h>
  
 -#include "asn1/asn1c/IA5String.h"
 -#include "data_structure/array_list.h"
 +#include "types/arraylist.h"
  
 -enum uri_type {
 -      /* rsync URL */
 -      UT_RSYNC,
 -      /* HTTPS URL */
 -      UT_HTTPS,
 -      /*
 -       * URI (not URL).
 -       * In practice it's always rsync, but it doesn't matter.
 -       */
 -      UT_CAGED,
 -};
 -
 -struct rpki_uri;
 -
 -int __uri_create(struct rpki_uri **, char const *, enum uri_type,
 -    bool, struct rpki_uri *, void const *, size_t);
 -int uri_create_mft(struct rpki_uri **, char const *, struct rpki_uri *,
 -    struct rpki_uri *, IA5String_t *);
 -struct rpki_uri *uri_create_cache(char const *);
 +#define RPKI_SCHEMA_LEN 8 /* strlen("rsync://"), strlen("https://") */
  
 -#define uri_create(uri, tal, type, is_notif, notif, guri) \
 -      __uri_create(uri, tal, type, is_notif, notif, guri, strlen(guri))
 -#define uri_create_caged(uri, tal, notif, guri, guri_len) \
 -      __uri_create(uri, tal, UT_CAGED, false, notif, guri, guri_len)
 -#define uri_create_cage(uri, tal, notif) \
 -      uri_create_caged(uri, tal, notif, "", 0)
 +struct uri {
 +      char *_str;
 +      size_t _len;
 +};
  
 -struct rpki_uri *uri_refget(struct rpki_uri *);
 -void uri_refput(struct rpki_uri *);
 +typedef char const *error_msg;
  
 -/*
 - * Note that, if you intend to print some URI, you're likely supposed to use
 - * uri_get_printable() instead.
 - */
 -char const *uri_get_global(struct rpki_uri *);
 -char const *uri_get_local(struct rpki_uri *);
 -size_t uri_get_global_len(struct rpki_uri *);
 +error_msg uri_init(struct uri *, char const *);
 +void __uri_init(struct uri *, char const *, size_t);
 +#define __URI_INIT(uri, str) __uri_init(uri, str, strlen(str))
 +void uri_copy(struct uri *, struct uri const *);
 +void uri_cleanup(struct uri *);
  
 -bool uri_equals(struct rpki_uri *, struct rpki_uri *);
 -bool uri_has_extension(struct rpki_uri *, char const *);
 -bool uri_is_certificate(struct rpki_uri *);
 -bool uri_is_notif(struct rpki_uri *);
 +#define uri_str(u) ((char const *)((u)->_str))
 +#define uri_len(u) ((size_t const)((u)->_len))
  
 -enum uri_type uri_get_type(struct rpki_uri *);
 -bool uri_is_rsync(struct rpki_uri *);
 -bool uri_is_https(struct rpki_uri *);
 +bool uri_is_rsync(struct uri const *);
 +bool uri_is_https(struct uri const *);
  
 -char const *uri_val_get_printable(struct rpki_uri *);
 -char const *uri_op_get_printable(struct rpki_uri *);
 +bool uri_equals(struct uri const *, struct uri const *);
 +bool uri_has_extension(struct uri const *, char const *);
 +bool uri_same_origin(struct uri const *, struct uri const *);
  
 -char *uri_get_rrdp_workspace(char const *, struct rpki_uri *);
 +int uri_parent(struct uri const *, struct uri *);
 +void uri_child(struct uri const *, char const *, size_t, struct uri *);
 +#define URI_CHILD(uri, name, child) uri_child(uri, name, strlen(name), child)
  
  /* Plural */
  
 -DEFINE_ARRAY_LIST_STRUCT(uri_list, struct rpki_uri *);
 -
 -void uris_init(struct uri_list *);
 -void uris_cleanup(struct uri_list *);
 -
 -void uris_add(struct uri_list *, struct rpki_uri *);
 +DEFINE_ARRAY_LIST_STRUCT(uris, struct uri);
- DECLARE_ARRAY_LIST_FUNCTIONS(uris, struct uri)
++void uris_init(struct uris *);
++void uris_cleanup(struct uris *, void (*cb)(struct uri *));
++void uris_add(struct uris *list, struct uri *elem);
  
  #endif /* SRC_TYPES_URI_H_ */
Simple merge
index 34298ebbca7f143323f9527cbff02fe0c03a7b02,9b02ac1e0a86c1f22cfafd773805a0b3496877f0..6817c5e014045f0a99f348581ee99228e7797231
@@@ -1,21 -1,21 +1,19 @@@
  #include "validation_handler.h"
  
- #include "rtr/db/db_table.h"
 -#include "log.h"
 -#include "thread_var.h"
--
 -/*
 - * Never returns NULL by contract.
 - */
 -static struct validation_handler const *
 -get_current_threads_handler(void)
 -{
 -      struct validation_handler const *handler;
 +static struct db_table *table;
  
 -      handler = validation_get_validation_handler(state_retrieve());
 -      if (handler == NULL)
 -              pr_crit("This thread lacks a validation handler.");
 +void
 +vhandle_init(void)
 +{
 +      table = db_table_create();
 +}
  
 -      return handler;
 +struct db_table *
 +vhandle_claim(void)
 +{
 +      struct db_table *result = table;
 +      table = NULL;
 +      return result;
  }
  
  int
index d06ae1478ea464b9199c2c538b1cfc512db0ad6c,2e284c1f923297e00185646fc1b0ed953d6fc87e..02365dc8a603c6476209f843b40d6738f44cf2b6
@@@ -1,17 -1,42 +1,18 @@@
  #ifndef SRC_VALIDATION_HANDLER_H_
  #define SRC_VALIDATION_HANDLER_H_
  
--#include "rtr/db/vrps.h"
++#include "rtr/db/db_table.h"
++#include "types/asn.h"
  
 -/**
 - * Functions that handle validation results.
 - *
 - * At some point, I believe we will end up separating the validator code into a
 - * library, so it can be used by other applications aside from Fort's RTR
 - * server.
 - *
 - * This structure is designed with that in mind; it's the callback collection
 - * that the library's user application will fill up, so it can do whatever it
 - * wants with the validated ROAs.
 - *
 - * Because it's intended to be used by arbitrary applications, it needs to be
 - * generic. Please refrain from adding callbacks that are specifically meant for
 - * a particular use case.
 - *
 - * All of these functions can be NULL.
 - */
 -struct validation_handler {
 -      /** Called every time Fort has successfully validated an IPv4 ROA. */
 -      int (*handle_roa_v4)(uint32_t, struct ipv4_prefix const *, uint8_t,
 -          void *);
 -      /** Called every time Fort has successfully validated an IPv6 ROA. */
 -      int (*handle_roa_v6)(uint32_t, struct ipv6_prefix const *, uint8_t,
 -          void *);
 -      /** Called every time Fort has successfully validated a BGPsec cert */
 -      int (*handle_router_key)(unsigned char const *,
 -          struct asn_range const *, unsigned char const *, void *);
 -      /** Generic user-defined argument for the functions above. */
 -      void *arg;
 -};
 +void vhandle_init(void);
 +struct db_table *vhandle_claim(void);
  
 -int vhandler_handle_roa_v4(uint32_t, struct ipv4_prefix const *, uint8_t);
 -int vhandler_handle_roa_v6(uint32_t, struct ipv6_prefix const *, uint8_t);
 -int vhandler_handle_router_key(unsigned char const *, struct asn_range const *,
 +/* Called every time Fort has successfully validated an IPv4 ROA. */
 +int vhandle_roa_v4(uint32_t, struct ipv4_prefix const *, uint8_t);
 +/* Called every time Fort has successfully validated an IPv6 ROA. */
 +int vhandle_roa_v6(uint32_t, struct ipv6_prefix const *, uint8_t);
 +/* Called every time Fort has successfully validated a BGPsec cert. */
 +int handle_router_key(unsigned char const *, struct asn_range const *,
      unsigned char const *);
  
  #endif /* SRC_VALIDATION_HANDLER_H_ */
diff --cc test/mock.c
index ff7763875338297db9b3de3933c7686acabcfb40,c5bfff431629d1a36e50878ac362d78ed536ca6e..b7bdc41cbe99ec978728eaed9dd7ee8023ceca0e
@@@ -2,35 -2,26 +2,36 @@@
  
  #include <errno.h>
  #include <arpa/inet.h>
 -#include "config.h"
 +#include <fcntl.h>
 +#include <openssl/err.h>
 +#include <stdarg.h>
 +#include <sys/types.h>
 +#include <sys/stat.h>
 +#include <time.h>
++#include <unistd.h>
 +
 +#include "common.h"
  #include "log.h"
 -#include "state.h"
 -#include "thread_var.h"
 -#include "config/filename_format.h"
 -#include "config/mode.h"
 -#include "incidence/incidence.h"
 -
 -/**
 - * Some core functions, as linked from unit tests.
 - */
 -
 -MOCK_TRUE(log_val_enabled, unsigned int l)
 -MOCK_TRUE(log_op_enabled, unsigned int l)
 -
 -/* CFLAGS=-DPRINT_PRS make check */
 -#ifdef PRINT_PRS
 -#define MOCK_PRINT                                                    \
 +#include "types/map.h"
 +
 +/* Some core functions, as linked from unit tests. */
 +
 +#if 0
 +
 +static void
 +print_monotime(void)
 +{
 +      struct timespec now;
 +      if (clock_gettime(CLOCK_MONOTONIC, &now) < 0)
 +              pr_crit("clock_gettime() returned '%s'", strerror(errno));
 +      printf("%ld.%.3ld ", now.tv_sec, now.tv_nsec / 1000000);
 +}
 +
 +#define MOCK_PRINT(color)                                             \
        do {                                                            \
                va_list args;                                           \
 +              printf(color);                                          \
 +              print_monotime();                                       \
                va_start(args, format);                                 \
                vfprintf(stdout, format, args);                         \
                va_end(args);                                           \