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 }