snac2

Fork of https://codeberg.org/grunfink/snac2
git clone https://git.inz.fi/snac2
Log | Files | Refs | README | LICENSE

http.c (8344B)


      1 /* snac - A simple, minimalistic ActivityPub instance */
      2 /* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
      3 
      4 #include "xs.h"
      5 #include "xs_io.h"
      6 #include "xs_openssl.h"
      7 #include "xs_curl.h"
      8 #include "xs_time.h"
      9 #include "xs_json.h"
     10 
     11 #include "snac.h"
     12 
     13 xs_dict *http_signed_request_raw(const char *keyid, const char *seckey,
     14                             const char *method, const char *url,
     15                             const xs_dict *headers,
     16                             const char *body, int b_size,
     17                             int *status, xs_str **payload, int *p_size,
     18                             int timeout)
     19 /* does a signed HTTP request */
     20 {
     21     xs *l1 = NULL;
     22     xs *date = NULL;
     23     xs *digest = NULL;
     24     xs *s64 = NULL;
     25     xs *signature = NULL;
     26     xs *hdrs = NULL;
     27     const char *host;
     28     const char *target;
     29     const char *k, *v;
     30     xs_dict *response;
     31 
     32     date = xs_str_utctime(0, "%a, %d %b %Y %H:%M:%S GMT");
     33 
     34     {
     35         xs *s1 = xs_replace_n(url, "http:/" "/", "", 1);
     36         xs *s = xs_replace_n(s1, "https:/" "/", "", 1);
     37         l1 = xs_split_n(s, "/", 1);
     38     }
     39 
     40     /* strip the url to get host and target */
     41     host = xs_list_get(l1, 0);
     42 
     43     if (xs_list_len(l1) == 2)
     44         target = xs_list_get(l1, 1);
     45     else
     46         target = "";
     47 
     48     /* digest */
     49     {
     50         xs *s;
     51 
     52         if (body != NULL)
     53             s = xs_sha256_base64(body, b_size);
     54         else
     55             s = xs_sha256_base64("", 0);
     56 
     57         digest = xs_fmt("SHA-256=%s", s);
     58     }
     59 
     60     {
     61         /* build the string to be signed */
     62         xs *s = xs_fmt("(request-target): %s /%s\n"
     63                        "host: %s\n"
     64                        "digest: %s\n"
     65                        "date: %s",
     66                     strcmp(method, "POST") == 0 ? "post" : "get",
     67                     target, host, digest, date);
     68 
     69         s64 = xs_evp_sign(seckey, s, strlen(s));
     70     }
     71 
     72     /* build now the signature header */
     73     signature = xs_fmt("keyId=\"%s#main-key\","
     74                        "algorithm=\"rsa-sha256\","
     75                        "headers=\"(request-target) host digest date\","
     76                        "signature=\"%s\"",
     77                         keyid, s64);
     78 
     79     /* transfer the original headers */
     80     hdrs = xs_dict_new();
     81     int c = 0;
     82     while (xs_dict_next(headers, &k, &v, &c))
     83         hdrs = xs_dict_append(hdrs, k, v);
     84 
     85     /* add the new headers */
     86     if (strcmp(method, "POST") == 0)
     87         hdrs = xs_dict_append(hdrs, "content-type", "application/activity+json");
     88     else
     89         hdrs = xs_dict_append(hdrs, "accept",       "application/activity+json");
     90 
     91     xs *user_agent = xs_fmt("%s; +%s/", USER_AGENT, srv_baseurl);
     92 
     93     hdrs = xs_dict_append(hdrs, "date",         date);
     94     hdrs = xs_dict_append(hdrs, "signature",    signature);
     95     hdrs = xs_dict_append(hdrs, "digest",       digest);
     96     hdrs = xs_dict_append(hdrs, "host",         host);
     97     hdrs = xs_dict_append(hdrs, "user-agent",   user_agent);
     98 
     99     response = xs_http_request(method, url, hdrs,
    100                            body, b_size, status, payload, p_size, timeout);
    101 
    102     srv_archive("SEND", url, hdrs, body, b_size, *status, response, *payload, *p_size);
    103 
    104     return response;
    105 }
    106 
    107 
    108 xs_dict *http_signed_request(snac *snac, const char *method, const char *url,
    109                             const xs_dict *headers,
    110                             const char *body, int b_size,
    111                             int *status, xs_str **payload, int *p_size,
    112                             int timeout)
    113 /* does a signed HTTP request */
    114 {
    115     const char *seckey = xs_dict_get(snac->key, "secret");
    116     xs_dict *response;
    117 
    118     response = http_signed_request_raw(snac->actor, seckey, method, url,
    119                 headers, body, b_size, status, payload, p_size, timeout);
    120 
    121     return response;
    122 }
    123 
    124 
    125 int check_signature(const xs_dict *req, xs_str **err)
    126 /* check the signature */
    127 {
    128     const char *sig_hdr = xs_dict_get(req, "signature");
    129     xs *keyId = NULL;
    130     xs *headers = NULL;
    131     xs *signature = NULL;
    132     xs *created = NULL;
    133     xs *expires = NULL;
    134     char *p;
    135     const char *pubkey;
    136     const char *k;
    137 
    138     if (xs_is_null(sig_hdr)) {
    139         *err = xs_fmt("missing 'signature' header");
    140         return 0;
    141     }
    142 
    143     {
    144         /* extract the values */
    145         xs *l = xs_split(sig_hdr, ",");
    146         int c = 0;
    147         const xs_val *v;
    148 
    149         while (xs_list_next(l, &v, &c)) {
    150             xs *kv = xs_split_n(v, "=", 1);
    151 
    152             if (xs_list_len(kv) != 2)
    153                 continue;
    154 
    155             xs *k1 = xs_strip_i(xs_dup(xs_list_get(kv, 0)));
    156             xs *v1 = xs_strip_chars_i(xs_dup(xs_list_get(kv, 1)), " \"");
    157 
    158             if (!strcmp(k1, "keyId"))
    159                 keyId = xs_dup(v1);
    160             else
    161             if (!strcmp(k1, "headers"))
    162                 headers = xs_dup(v1);
    163             else
    164             if (!strcmp(k1, "signature"))
    165                 signature = xs_dup(v1);
    166             else
    167             if (!strcmp(k1, "created"))
    168                 created = xs_dup(v1);
    169             else
    170             if (!strcmp(k1, "expires"))
    171                 expires = xs_dup(v1);
    172         }
    173     }
    174 
    175     if (keyId == NULL || headers == NULL || signature == NULL) {
    176         *err = xs_fmt("bad signature header");
    177         return 0;
    178     }
    179 
    180     /* strip the # from the keyId */
    181     if ((p = strchr(keyId, '#')) != NULL)
    182         *p = '\0';
    183 
    184     /* also strip cgi variables */
    185     if ((p = strchr(keyId, '?')) != NULL)
    186         *p = '\0';
    187 
    188     xs *actor = NULL;
    189     int status;
    190 
    191     if (!valid_status((status = actor_request(NULL, keyId, &actor)))) {
    192         *err = xs_fmt("actor request error %s %d", keyId, status);
    193         return 0;
    194     }
    195 
    196     if ((k = xs_dict_get(actor, "publicKey")) == NULL ||
    197         ((pubkey = xs_dict_get(k, "publicKeyPem")) == NULL)) {
    198         *err = xs_fmt("cannot get pubkey from %s", keyId);
    199         return 0;
    200     }
    201 
    202     /* now build the string to be signed */
    203     xs *sig_str = xs_str_new(NULL);
    204 
    205     {
    206         xs *l = xs_split(headers, " ");
    207         xs_list *p;
    208         const xs_val *v;
    209 
    210         p = l;
    211         while (xs_list_iter(&p, &v)) {
    212             const char *hc;
    213             xs *ss = NULL;
    214 
    215             if (*sig_str != '\0')
    216                 sig_str = xs_str_cat(sig_str, "\n");
    217 
    218             if (strcmp(v, "(request-target)") == 0) {
    219                 ss = xs_fmt("%s: post %s", v, xs_dict_get(req, "path"));
    220             }
    221             else
    222             if (strcmp(v, "(created)") == 0) {
    223                 ss = xs_fmt("%s: %s", v, created);
    224             }
    225             else
    226             if (strcmp(v, "(expires)") == 0) {
    227                 ss = xs_fmt("%s: %s", v, expires);
    228             }
    229             else
    230             if (strcmp(v, "host") == 0) {
    231                 hc = xs_dict_get(req, "host");
    232 
    233                 /* if there is no host header or some garbage like
    234                    address:host has arrived here due to misconfiguration,
    235                    signature verify will totally fail, so let's Leroy Jenkins
    236                    with the global server hostname instead */
    237                 if (hc == NULL || xs_str_in(hc, ":") != -1)
    238                     hc = xs_dict_get(srv_config, "host");
    239 
    240                 ss = xs_fmt("host: %s", hc);
    241             }
    242             else {
    243                 /* add the header */
    244                 if ((hc = xs_dict_get(req, v)) == NULL) {
    245                     *err = xs_fmt("cannot find header '%s'", v);
    246                     return 0;
    247                 }
    248 
    249                 ss = xs_fmt("%s: %s", v, hc);
    250             }
    251 
    252             sig_str = xs_str_cat(sig_str, ss);
    253         }
    254     }
    255 
    256     if (xs_evp_verify(pubkey, sig_str, strlen(sig_str), signature) != 1) {
    257         *err = xs_fmt("RSA verify error %s", keyId);
    258         return 0;
    259     }
    260 
    261     return 1;
    262 }
    263 
    264 int parse_range(const xs_dict *req, int *start, int *end)
    265 {
    266     const char *rng = xs_dict_get(req, "range");
    267     if (rng && xs_str_in(rng, ",") == -1 && xs_startswith(rng, "bytes=")) {
    268         xs *l = xs_split_n(rng + strlen("bytes="), "-", 2);
    269         if (xs_list_len(l) == 2) {
    270             const char *ss = xs_list_get(l, 0);
    271             const char *es = xs_list_get(l, 1);
    272 
    273             if (ss[strspn(ss, "0123456789")] == '\0' &&
    274                     es[strspn(es, "0123456789")] == '\0') {
    275                 *start = atoi(ss);
    276                 *end = es[0] == '\0' ? XS_ALL : atoi(es);
    277 
    278                 return 1;
    279             }
    280         }
    281     }
    282 
    283     return 0;
    284 }