diff options
| -rw-r--r-- | Makefile | 2 | ||||
| -rw-r--r-- | html.c | 9 | ||||
| -rw-r--r-- | httpd.c | 14 | ||||
| -rw-r--r-- | mastoapi.c | 814 | ||||
| -rw-r--r-- | snac.h | 11 | ||||
| -rw-r--r-- | xs_encdec.h | 13 | ||||
| -rw-r--r-- | xs_version.h | 2 |
7 files changed, 860 insertions, 5 deletions
| @@ -5,7 +5,7 @@ CFLAGS?=-g -Wall | |||
| 5 | all: snac | 5 | all: snac |
| 6 | 6 | ||
| 7 | snac: snac.o main.o data.o http.o httpd.o webfinger.o \ | 7 | snac: snac.o main.o data.o http.o httpd.o webfinger.o \ |
| 8 | activitypub.o html.o utils.o format.o upgrade.o | 8 | activitypub.o html.o utils.o format.o upgrade.o mastoapi.o |
| 9 | $(CC) $(CFLAGS) -L/usr/local/lib *.o -lcurl -lcrypto -pthread $(LDFLAGS) -o $@ | 9 | $(CC) $(CFLAGS) -L/usr/local/lib *.o -lcurl -lcrypto -pthread $(LDFLAGS) -o $@ |
| 10 | 10 | ||
| 11 | .c.o: | 11 | .c.o: |
| @@ -332,7 +332,7 @@ d_char *html_top_controls(snac *snac, d_char *s) | |||
| 332 | "<input type=\"hidden\" name=\"in_reply_to\" value=\"\">\n" | 332 | "<input type=\"hidden\" name=\"in_reply_to\" value=\"\">\n" |
| 333 | "<p>%s: <input type=\"checkbox\" name=\"sensitive\">\n" | 333 | "<p>%s: <input type=\"checkbox\" name=\"sensitive\">\n" |
| 334 | "<p>%s: <input type=\"checkbox\" name=\"mentioned_only\">\n" | 334 | "<p>%s: <input type=\"checkbox\" name=\"mentioned_only\">\n" |
| 335 | "<p><input type=\"file\" name=\"attach\">\n" | 335 | "<p>%s: <input type=\"file\" name=\"attach\">\n" |
| 336 | "<p>%s: <input type=\"text\" name=\"alt_text\">\n" | 336 | "<p>%s: <input type=\"text\" name=\"alt_text\">\n" |
| 337 | "<p><input type=\"submit\" class=\"button\" value=\"%s\">\n" | 337 | "<p><input type=\"submit\" class=\"button\" value=\"%s\">\n" |
| 338 | "</form><p>\n" | 338 | "</form><p>\n" |
| @@ -425,6 +425,7 @@ d_char *html_top_controls(snac *snac, d_char *s) | |||
| 425 | snac->actor, | 425 | snac->actor, |
| 426 | L("Sensitive content"), | 426 | L("Sensitive content"), |
| 427 | L("Only for mentioned people"), | 427 | L("Only for mentioned people"), |
| 428 | L("Image"), | ||
| 428 | L("Image description"), | 429 | L("Image description"), |
| 429 | L("Post"), | 430 | L("Post"), |
| 430 | 431 | ||
| @@ -593,7 +594,7 @@ d_char *html_entry_controls(snac *snac, d_char *os, char *msg, const char *md5) | |||
| 593 | 594 | ||
| 594 | "<p>%s: <input type=\"checkbox\" name=\"sensitive\">\n" | 595 | "<p>%s: <input type=\"checkbox\" name=\"sensitive\">\n" |
| 595 | "<p>%s: <input type=\"checkbox\" name=\"mentioned_only\">\n" | 596 | "<p>%s: <input type=\"checkbox\" name=\"mentioned_only\">\n" |
| 596 | "<p><input type=\"file\" name=\"attach\">\n" | 597 | "<p>%s: <input type=\"file\" name=\"attach\">\n" |
| 597 | "<p>%s: <input type=\"text\" name=\"alt_text\">\n" | 598 | "<p>%s: <input type=\"text\" name=\"alt_text\">\n" |
| 598 | 599 | ||
| 599 | "<input type=\"hidden\" name=\"redir\" value=\"%s_entry\">\n" | 600 | "<input type=\"hidden\" name=\"redir\" value=\"%s_entry\">\n" |
| @@ -609,6 +610,7 @@ d_char *html_entry_controls(snac *snac, d_char *os, char *msg, const char *md5) | |||
| 609 | id, | 610 | id, |
| 610 | L("Sensitive content"), | 611 | L("Sensitive content"), |
| 611 | L("Only for mentioned people"), | 612 | L("Only for mentioned people"), |
| 613 | L("Image"), | ||
| 612 | L("Image description"), | 614 | L("Image description"), |
| 613 | md5, | 615 | md5, |
| 614 | L("Post") | 616 | L("Post") |
| @@ -632,7 +634,7 @@ d_char *html_entry_controls(snac *snac, d_char *os, char *msg, const char *md5) | |||
| 632 | 634 | ||
| 633 | "<p>%s: <input type=\"checkbox\" name=\"sensitive\">\n" | 635 | "<p>%s: <input type=\"checkbox\" name=\"sensitive\">\n" |
| 634 | "<p>%s: <input type=\"checkbox\" name=\"mentioned_only\">\n" | 636 | "<p>%s: <input type=\"checkbox\" name=\"mentioned_only\">\n" |
| 635 | "<p><input type=\"file\" name=\"attach\">\n" | 637 | "<p>%s: <input type=\"file\" name=\"attach\">\n" |
| 636 | "<p>%s: <input type=\"text\" name=\"alt_text\">\n" | 638 | "<p>%s: <input type=\"text\" name=\"alt_text\">\n" |
| 637 | 639 | ||
| 638 | "<input type=\"hidden\" name=\"redir\" value=\"%s_entry\">\n" | 640 | "<input type=\"hidden\" name=\"redir\" value=\"%s_entry\">\n" |
| @@ -648,6 +650,7 @@ d_char *html_entry_controls(snac *snac, d_char *os, char *msg, const char *md5) | |||
| 648 | id, | 650 | id, |
| 649 | L("Sensitive content"), | 651 | L("Sensitive content"), |
| 650 | L("Only for mentioned people"), | 652 | L("Only for mentioned people"), |
| 653 | L("Image"), | ||
| 651 | L("Image description"), | 654 | L("Image description"), |
| 652 | md5, | 655 | md5, |
| 653 | L("Post") | 656 | L("Post") |
| @@ -177,6 +177,12 @@ void httpd_connection(FILE *f) | |||
| 177 | status = activitypub_get_handler(req, q_path, &body, &b_size, &ctype); | 177 | status = activitypub_get_handler(req, q_path, &body, &b_size, &ctype); |
| 178 | 178 | ||
| 179 | if (status == 0) | 179 | if (status == 0) |
| 180 | status = oauth_get_handler(req, q_path, &body, &b_size, &ctype); | ||
| 181 | |||
| 182 | if (status == 0) | ||
| 183 | status = mastoapi_get_handler(req, q_path, &body, &b_size, &ctype); | ||
| 184 | |||
| 185 | if (status == 0) | ||
| 180 | status = html_get_handler(req, q_path, &body, &b_size, &ctype); | 186 | status = html_get_handler(req, q_path, &body, &b_size, &ctype); |
| 181 | } | 187 | } |
| 182 | else | 188 | else |
| @@ -186,6 +192,14 @@ void httpd_connection(FILE *f) | |||
| 186 | payload, p_size, &body, &b_size, &ctype); | 192 | payload, p_size, &body, &b_size, &ctype); |
| 187 | 193 | ||
| 188 | if (status == 0) | 194 | if (status == 0) |
| 195 | status = oauth_post_handler(req, q_path, | ||
| 196 | payload, p_size, &body, &b_size, &ctype); | ||
| 197 | |||
| 198 | if (status == 0) | ||
| 199 | status = mastoapi_post_handler(req, q_path, | ||
| 200 | payload, p_size, &body, &b_size, &ctype); | ||
| 201 | |||
| 202 | if (status == 0) | ||
| 189 | status = html_post_handler(req, q_path, | 203 | status = html_post_handler(req, q_path, |
| 190 | payload, p_size, &body, &b_size, &ctype); | 204 | payload, p_size, &body, &b_size, &ctype); |
| 191 | } | 205 | } |
diff --git a/mastoapi.c b/mastoapi.c new file mode 100644 index 0000000..b223ce4 --- /dev/null +++ b/mastoapi.c | |||
| @@ -0,0 +1,814 @@ | |||
| 1 | /* snac - A simple, minimalistic ActivityPub instance */ | ||
| 2 | /* copyright (c) 2022 - 2023 grunfink / MIT license */ | ||
| 3 | |||
| 4 | #include "xs.h" | ||
| 5 | #include "xs_encdec.h" | ||
| 6 | #include "xs_openssl.h" | ||
| 7 | #include "xs_json.h" | ||
| 8 | #include "xs_io.h" | ||
| 9 | #include "xs_time.h" | ||
| 10 | |||
| 11 | #include "snac.h" | ||
| 12 | |||
| 13 | static xs_str *random_str(void) | ||
| 14 | /* just what is says in the tin */ | ||
| 15 | { | ||
| 16 | unsigned int data[4] = {0}; | ||
| 17 | FILE *f; | ||
| 18 | |||
| 19 | if ((f = fopen("/dev/random", "r")) != NULL) { | ||
| 20 | fread(data, sizeof(data), 1, f); | ||
| 21 | fclose(f); | ||
| 22 | } | ||
| 23 | else { | ||
| 24 | data[0] = random() % 0xffffffff; | ||
| 25 | data[1] = random() % 0xffffffff; | ||
| 26 | data[2] = random() % 0xffffffff; | ||
| 27 | data[3] = random() % 0xffffffff; | ||
| 28 | } | ||
| 29 | |||
| 30 | return xs_hex_enc((char *)data, sizeof(data)); | ||
| 31 | } | ||
| 32 | |||
| 33 | |||
| 34 | int app_add(const char *id, const xs_dict *app) | ||
| 35 | /* stores an app */ | ||
| 36 | { | ||
| 37 | if (!xs_is_hex(id)) | ||
| 38 | return 500; | ||
| 39 | |||
| 40 | int status = 201; | ||
| 41 | xs *fn = xs_fmt("%s/app/", srv_basedir); | ||
| 42 | FILE *f; | ||
| 43 | |||
| 44 | mkdirx(fn); | ||
| 45 | fn = xs_str_cat(fn, id); | ||
| 46 | fn = xs_str_cat(fn, ".json"); | ||
| 47 | |||
| 48 | if ((f = fopen(fn, "w")) != NULL) { | ||
| 49 | xs *j = xs_json_dumps_pp(app, 4); | ||
| 50 | fwrite(j, strlen(j), 1, f); | ||
| 51 | fclose(f); | ||
| 52 | } | ||
| 53 | else | ||
| 54 | status = 500; | ||
| 55 | |||
| 56 | return status; | ||
| 57 | } | ||
| 58 | |||
| 59 | |||
| 60 | xs_dict *app_get(const char *id) | ||
| 61 | /* gets an app */ | ||
| 62 | { | ||
| 63 | if (!xs_is_hex(id)) | ||
| 64 | return NULL; | ||
| 65 | |||
| 66 | xs *fn = xs_fmt("%s/app/%s.json", srv_basedir, id); | ||
| 67 | xs_dict *app = NULL; | ||
| 68 | FILE *f; | ||
| 69 | |||
| 70 | if ((f = fopen(fn, "r")) != NULL) { | ||
| 71 | xs *j = xs_readall(f); | ||
| 72 | fclose(f); | ||
| 73 | |||
| 74 | app = xs_json_loads(j); | ||
| 75 | } | ||
| 76 | |||
| 77 | return app; | ||
| 78 | } | ||
| 79 | |||
| 80 | |||
| 81 | int app_del(const char *id) | ||
| 82 | /* deletes an app */ | ||
| 83 | { | ||
| 84 | if (!xs_is_hex(id)) | ||
| 85 | return -1; | ||
| 86 | |||
| 87 | xs *fn = xs_fmt("%s/app/%s.json", srv_basedir, id); | ||
| 88 | |||
| 89 | return unlink(fn); | ||
| 90 | } | ||
| 91 | |||
| 92 | |||
| 93 | int token_add(const char *id, const xs_dict *token) | ||
| 94 | /* stores a token */ | ||
| 95 | { | ||
| 96 | if (!xs_is_hex(id)) | ||
| 97 | return 500; | ||
| 98 | |||
| 99 | int status = 201; | ||
| 100 | xs *fn = xs_fmt("%s/token/", srv_basedir); | ||
| 101 | FILE *f; | ||
| 102 | |||
| 103 | mkdirx(fn); | ||
| 104 | fn = xs_str_cat(fn, id); | ||
| 105 | fn = xs_str_cat(fn, ".json"); | ||
| 106 | |||
| 107 | if ((f = fopen(fn, "w")) != NULL) { | ||
| 108 | xs *j = xs_json_dumps_pp(token, 4); | ||
| 109 | fwrite(j, strlen(j), 1, f); | ||
| 110 | fclose(f); | ||
| 111 | } | ||
| 112 | else | ||
| 113 | status = 500; | ||
| 114 | |||
| 115 | return status; | ||
| 116 | } | ||
| 117 | |||
| 118 | |||
| 119 | xs_dict *token_get(const char *id) | ||
| 120 | /* gets a token */ | ||
| 121 | { | ||
| 122 | if (!xs_is_hex(id)) | ||
| 123 | return NULL; | ||
| 124 | |||
| 125 | xs *fn = xs_fmt("%s/token/%s.json", srv_basedir, id); | ||
| 126 | xs_dict *token = NULL; | ||
| 127 | FILE *f; | ||
| 128 | |||
| 129 | if ((f = fopen(fn, "r")) != NULL) { | ||
| 130 | xs *j = xs_readall(f); | ||
| 131 | fclose(f); | ||
| 132 | |||
| 133 | token = xs_json_loads(j); | ||
| 134 | } | ||
| 135 | |||
| 136 | return token; | ||
| 137 | } | ||
| 138 | |||
| 139 | |||
| 140 | int token_del(const char *id) | ||
| 141 | /* deletes a token */ | ||
| 142 | { | ||
| 143 | if (!xs_is_hex(id)) | ||
| 144 | return -1; | ||
| 145 | |||
| 146 | xs *fn = xs_fmt("%s/token/%s.json", srv_basedir, id); | ||
| 147 | |||
| 148 | return unlink(fn); | ||
| 149 | } | ||
| 150 | |||
| 151 | |||
| 152 | const char *login_page = "" | ||
| 153 | "<!DOCTYPE html>\n" | ||
| 154 | "<body><h1>%s OAuth identify</h1>\n" | ||
| 155 | "<div style=\"background-color: red; color: white\">%s</div>\n" | ||
| 156 | "<form method=\"post\" action=\"https:/" "/%s/oauth/x-snac-login\">\n" | ||
| 157 | "<p>Login: <input type=\"text\" name=\"login\"></p>\n" | ||
| 158 | "<p>Password: <input type=\"password\" name=\"passwd\"></p>\n" | ||
| 159 | "<input type=\"hidden\" name=\"redir\" value=\"%s\">\n" | ||
| 160 | "<input type=\"hidden\" name=\"cid\" value=\"%s\">\n" | ||
| 161 | "<input type=\"hidden\" name=\"state\" value=\"%s\">\n" | ||
| 162 | "<input type=\"submit\" value=\"OK\">\n" | ||
| 163 | "</form><p>%s</p></body>\n" | ||
| 164 | ""; | ||
| 165 | |||
| 166 | int oauth_get_handler(const xs_dict *req, const char *q_path, | ||
| 167 | char **body, int *b_size, char **ctype) | ||
| 168 | { | ||
| 169 | if (!xs_startswith(q_path, "/oauth/")) | ||
| 170 | return 0; | ||
| 171 | |||
| 172 | { | ||
| 173 | xs *j = xs_json_dumps_pp(req, 4); | ||
| 174 | printf("oauth get:\n%s\n", j); | ||
| 175 | } | ||
| 176 | |||
| 177 | int status = 404; | ||
| 178 | xs_dict *msg = xs_dict_get(req, "q_vars"); | ||
| 179 | xs *cmd = xs_replace(q_path, "/oauth", ""); | ||
| 180 | |||
| 181 | srv_debug(0, xs_fmt("oauth_get_handler %s", q_path)); | ||
| 182 | |||
| 183 | if (strcmp(cmd, "/authorize") == 0) { | ||
| 184 | const char *cid = xs_dict_get(msg, "client_id"); | ||
| 185 | const char *ruri = xs_dict_get(msg, "redirect_uri"); | ||
| 186 | const char *rtype = xs_dict_get(msg, "response_type"); | ||
| 187 | const char *state = xs_dict_get(msg, "state"); | ||
| 188 | |||
| 189 | status = 400; | ||
| 190 | |||
| 191 | if (cid && ruri && rtype && strcmp(rtype, "code") == 0) { | ||
| 192 | xs *app = app_get(cid); | ||
| 193 | |||
| 194 | if (app != NULL) { | ||
| 195 | const char *host = xs_dict_get(srv_config, "host"); | ||
| 196 | |||
| 197 | if (xs_is_null(state)) | ||
| 198 | state = ""; | ||
| 199 | |||
| 200 | *body = xs_fmt(login_page, host, "", host, ruri, cid, state, USER_AGENT); | ||
| 201 | *ctype = "text/html"; | ||
| 202 | status = 200; | ||
| 203 | |||
| 204 | srv_debug(0, xs_fmt("oauth authorize: generating login page")); | ||
| 205 | } | ||
| 206 | else | ||
| 207 | srv_debug(0, xs_fmt("oauth authorize: bad client_id %s", cid)); | ||
| 208 | } | ||
| 209 | else | ||
| 210 | srv_debug(0, xs_fmt("oauth authorize: invalid or unset arguments")); | ||
| 211 | } | ||
| 212 | |||
| 213 | return status; | ||
| 214 | } | ||
| 215 | |||
| 216 | |||
| 217 | int oauth_post_handler(const xs_dict *req, const char *q_path, | ||
| 218 | const char *payload, int p_size, | ||
| 219 | char **body, int *b_size, char **ctype) | ||
| 220 | { | ||
| 221 | if (!xs_startswith(q_path, "/oauth/")) | ||
| 222 | return 0; | ||
| 223 | |||
| 224 | { | ||
| 225 | xs *j = xs_json_dumps_pp(req, 4); | ||
| 226 | printf("oauth post:\n%s\n", j); | ||
| 227 | } | ||
| 228 | |||
| 229 | int status = 404; | ||
| 230 | xs_dict *msg = xs_dict_get(req, "p_vars"); | ||
| 231 | xs *cmd = xs_replace(q_path, "/oauth", ""); | ||
| 232 | |||
| 233 | srv_debug(0, xs_fmt("oauth_post_handler %s", q_path)); | ||
| 234 | |||
| 235 | if (strcmp(cmd, "/x-snac-login") == 0) { | ||
| 236 | const char *login = xs_dict_get(msg, "login"); | ||
| 237 | const char *passwd = xs_dict_get(msg, "passwd"); | ||
| 238 | const char *redir = xs_dict_get(msg, "redir"); | ||
| 239 | const char *cid = xs_dict_get(msg, "cid"); | ||
| 240 | const char *state = xs_dict_get(msg, "state"); | ||
| 241 | |||
| 242 | const char *host = xs_dict_get(srv_config, "host"); | ||
| 243 | |||
| 244 | /* by default, generate another login form with an error */ | ||
| 245 | *body = xs_fmt(login_page, host, "LOGIN INCORRECT", host, redir, cid, state, USER_AGENT); | ||
| 246 | *ctype = "text/html"; | ||
| 247 | status = 200; | ||
| 248 | |||
| 249 | if (login && passwd && redir && cid) { | ||
| 250 | snac snac; | ||
| 251 | |||
| 252 | if (user_open(&snac, login)) { | ||
| 253 | /* check the login + password */ | ||
| 254 | if (check_password(login, passwd, | ||
| 255 | xs_dict_get(snac.config, "passwd"))) { | ||
| 256 | /* success! redirect to the desired uri */ | ||
| 257 | xs *code = random_str(); | ||
| 258 | |||
| 259 | xs_free(*body); | ||
| 260 | *body = xs_fmt("%s?code=%s", redir, code); | ||
| 261 | status = 303; | ||
| 262 | |||
| 263 | /* if there is a state, add it */ | ||
| 264 | if (!xs_is_null(state) && *state) { | ||
| 265 | *body = xs_str_cat(*body, "&state="); | ||
| 266 | *body = xs_str_cat(*body, state); | ||
| 267 | } | ||
| 268 | |||
| 269 | srv_debug(0, xs_fmt("oauth x-snac-login: success, redirect to %s", *body)); | ||
| 270 | |||
| 271 | /* assign the login to the app */ | ||
| 272 | xs *app = app_get(cid); | ||
| 273 | |||
| 274 | if (app != NULL) { | ||
| 275 | app = xs_dict_set(app, "uid", login); | ||
| 276 | app = xs_dict_set(app, "code", code); | ||
| 277 | app_add(cid, app); | ||
| 278 | } | ||
| 279 | else | ||
| 280 | srv_log(xs_fmt("oauth x-snac-login: error getting app %s", cid)); | ||
| 281 | } | ||
| 282 | else | ||
| 283 | srv_debug(0, xs_fmt("oauth x-snac-login: login '%s' incorrect", login)); | ||
| 284 | |||
| 285 | user_free(&snac); | ||
| 286 | } | ||
| 287 | else | ||
| 288 | srv_debug(0, xs_fmt("oauth x-snac-login: bad user '%s'", login)); | ||
| 289 | } | ||
| 290 | else | ||
| 291 | srv_debug(0, xs_fmt("oauth x-snac-login: invalid or unset arguments")); | ||
| 292 | } | ||
| 293 | else | ||
| 294 | if (strcmp(cmd, "/token") == 0) { | ||
| 295 | const char *gtype = xs_dict_get(msg, "grant_type"); | ||
| 296 | const char *code = xs_dict_get(msg, "code"); | ||
| 297 | const char *cid = xs_dict_get(msg, "client_id"); | ||
| 298 | const char *csec = xs_dict_get(msg, "client_secret"); | ||
| 299 | const char *ruri = xs_dict_get(msg, "redirect_uri"); | ||
| 300 | xs *wrk = NULL; | ||
| 301 | |||
| 302 | /* no client_secret? check if it's inside an authorization header | ||
| 303 | (AndStatus does it this way) */ | ||
| 304 | if (xs_is_null(csec)) { | ||
| 305 | const char *auhdr = xs_dict_get(req, "authorization"); | ||
| 306 | |||
| 307 | if (!xs_is_null(auhdr) && xs_startswith(auhdr, "Basic ")) { | ||
| 308 | xs *s1 = xs_replace(auhdr, "Basic ", ""); | ||
| 309 | int size; | ||
| 310 | xs *s2 = xs_base64_dec(s1, &size); | ||
| 311 | |||
| 312 | if (!xs_is_null(s2)) { | ||
| 313 | xs *l1 = xs_split(s2, ":"); | ||
| 314 | |||
| 315 | if (xs_list_len(l1) == 2) { | ||
| 316 | wrk = xs_dup(xs_list_get(l1, 1)); | ||
| 317 | csec = wrk; | ||
| 318 | } | ||
| 319 | } | ||
| 320 | } | ||
| 321 | } | ||
| 322 | |||
| 323 | if (gtype && code && cid && csec && ruri) { | ||
| 324 | xs *app = app_get(cid); | ||
| 325 | |||
| 326 | if (app == NULL) { | ||
| 327 | status = 401; | ||
| 328 | srv_log(xs_fmt("oauth token: invalid app %s", cid)); | ||
| 329 | } | ||
| 330 | else | ||
| 331 | if (strcmp(csec, xs_dict_get(app, "client_secret")) != 0) { | ||
| 332 | status = 401; | ||
| 333 | srv_log(xs_fmt("oauth token: invalid client_secret for app %s", cid)); | ||
| 334 | } | ||
| 335 | else { | ||
| 336 | xs *rsp = xs_dict_new(); | ||
| 337 | xs *cat = xs_number_new(time(NULL)); | ||
| 338 | xs *tokid = random_str(); | ||
| 339 | |||
| 340 | rsp = xs_dict_append(rsp, "access_token", tokid); | ||
| 341 | rsp = xs_dict_append(rsp, "token_type", "Bearer"); | ||
| 342 | rsp = xs_dict_append(rsp, "created_at", cat); | ||
| 343 | |||
| 344 | *body = xs_json_dumps_pp(rsp, 4); | ||
| 345 | *ctype = "application/json"; | ||
| 346 | status = 200; | ||
| 347 | |||
| 348 | const char *uid = xs_dict_get(app, "uid"); | ||
| 349 | |||
| 350 | srv_debug(0, xs_fmt("oauth token: " | ||
| 351 | "successful login for %s, new token %s", uid, tokid)); | ||
| 352 | |||
| 353 | xs *token = xs_dict_new(); | ||
| 354 | token = xs_dict_append(token, "token", tokid); | ||
| 355 | token = xs_dict_append(token, "client_id", cid); | ||
| 356 | token = xs_dict_append(token, "client_secret", csec); | ||
| 357 | token = xs_dict_append(token, "uid", uid); | ||
| 358 | token = xs_dict_append(token, "code", code); | ||
| 359 | |||
| 360 | token_add(tokid, token); | ||
| 361 | } | ||
| 362 | } | ||
| 363 | else { | ||
| 364 | srv_debug(0, xs_fmt("oauth token: invalid or unset arguments")); | ||
| 365 | status = 400; | ||
| 366 | } | ||
| 367 | } | ||
| 368 | else | ||
| 369 | if (strcmp(cmd, "/revoke") == 0) { | ||
| 370 | const char *cid = xs_dict_get(msg, "client_id"); | ||
| 371 | const char *csec = xs_dict_get(msg, "client_secret"); | ||
| 372 | const char *tokid = xs_dict_get(msg, "token"); | ||
| 373 | |||
| 374 | if (cid && csec && tokid) { | ||
| 375 | xs *token = token_get(tokid); | ||
| 376 | |||
| 377 | *body = xs_str_new("{}"); | ||
| 378 | *ctype = "application/json"; | ||
| 379 | |||
| 380 | if (token == NULL || strcmp(csec, xs_dict_get(token, "client_secret")) != 0) { | ||
| 381 | srv_debug(0, xs_fmt("oauth revoke: bad secret for token %s", tokid)); | ||
| 382 | status = 403; | ||
| 383 | } | ||
| 384 | else { | ||
| 385 | token_del(tokid); | ||
| 386 | srv_debug(0, xs_fmt("oauth revoke: revoked token %s", tokid)); | ||
| 387 | status = 200; | ||
| 388 | |||
| 389 | /* also delete the app, as it serves no purpose from now on */ | ||
| 390 | app_del(cid); | ||
| 391 | } | ||
| 392 | } | ||
| 393 | else { | ||
| 394 | srv_debug(0, xs_fmt("oauth revoke: invalid or unset arguments")); | ||
| 395 | status = 403; | ||
| 396 | } | ||
| 397 | } | ||
| 398 | |||
| 399 | return status; | ||
| 400 | } | ||
| 401 | |||
| 402 | |||
| 403 | xs_str *mastoapi_id(const xs_dict *msg) | ||
| 404 | /* returns a somewhat Mastodon-compatible status id */ | ||
| 405 | { | ||
| 406 | char tmp[256] = ""; | ||
| 407 | int n = 0; | ||
| 408 | const char *id = xs_dict_get(msg, "id"); | ||
| 409 | const char *published = xs_dict_get(msg, "published"); | ||
| 410 | |||
| 411 | if (!xs_is_null(published)) { | ||
| 412 | /* transfer all numbers from the published date */ | ||
| 413 | while (*published && n < sizeof(tmp) - 1) { | ||
| 414 | if (*published >= '0' && *published <= '9') | ||
| 415 | tmp[n++] = *published; | ||
| 416 | published++; | ||
| 417 | } | ||
| 418 | tmp[n] = '\0'; | ||
| 419 | } | ||
| 420 | |||
| 421 | xs *md5 = xs_md5_hex(id, strlen(id)); | ||
| 422 | |||
| 423 | return xs_str_cat(xs_str_new(tmp), md5); | ||
| 424 | } | ||
| 425 | |||
| 426 | |||
| 427 | int mastoapi_get_handler(const xs_dict *req, const char *q_path, | ||
| 428 | char **body, int *b_size, char **ctype) | ||
| 429 | { | ||
| 430 | if (!xs_startswith(q_path, "/api/v1/")) | ||
| 431 | return 0; | ||
| 432 | |||
| 433 | srv_debug(0, xs_fmt("mastoapi_get_handler %s", q_path)); | ||
| 434 | { | ||
| 435 | xs *j = xs_json_dumps_pp(req, 4); | ||
| 436 | printf("mastoapi get:\n%s\n", j); | ||
| 437 | } | ||
| 438 | |||
| 439 | int status = 404; | ||
| 440 | xs_dict *args = xs_dict_get(req, "q_vars"); | ||
| 441 | xs *cmd = xs_replace(q_path, "/api/v1", ""); | ||
| 442 | char *v; | ||
| 443 | |||
| 444 | snac snac = {0}; | ||
| 445 | int logged_in = 0; | ||
| 446 | |||
| 447 | /* if there is an authorization field, try to validate it */ | ||
| 448 | if (!xs_is_null(v = xs_dict_get(req, "authorization")) && xs_startswith(v, "Bearer ")) { | ||
| 449 | xs *tokid = xs_replace(v, "Bearer ", ""); | ||
| 450 | xs *token = token_get(tokid); | ||
| 451 | |||
| 452 | if (token != NULL) { | ||
| 453 | const char *uid = xs_dict_get(token, "uid"); | ||
| 454 | |||
| 455 | if (!xs_is_null(uid) && user_open(&snac, uid)) { | ||
| 456 | logged_in = 1; | ||
| 457 | srv_debug(0, xs_fmt("mastoapi auth: valid token for user %s", uid)); | ||
| 458 | } | ||
| 459 | else | ||
| 460 | srv_log(xs_fmt("mastoapi auth: corrupted token %s", tokid)); | ||
| 461 | } | ||
| 462 | else | ||
| 463 | srv_log(xs_fmt("mastoapi auth: invalid token %s", tokid)); | ||
| 464 | } | ||
| 465 | |||
| 466 | if (strcmp(cmd, "/accounts/verify_credentials") == 0) { | ||
| 467 | if (logged_in) { | ||
| 468 | xs *acct = xs_dict_new(); | ||
| 469 | |||
| 470 | acct = xs_dict_append(acct, "id", xs_dict_get(snac.config, "uid")); | ||
| 471 | acct = xs_dict_append(acct, "username", xs_dict_get(snac.config, "uid")); | ||
| 472 | acct = xs_dict_append(acct, "acct", xs_dict_get(snac.config, "uid")); | ||
| 473 | acct = xs_dict_append(acct, "display_name", xs_dict_get(snac.config, "name")); | ||
| 474 | acct = xs_dict_append(acct, "created_at", xs_dict_get(snac.config, "published")); | ||
| 475 | acct = xs_dict_append(acct, "note", xs_dict_get(snac.config, "bio")); | ||
| 476 | acct = xs_dict_append(acct, "url", snac.actor); | ||
| 477 | |||
| 478 | xs *avatar = NULL; | ||
| 479 | char *av = xs_dict_get(snac.config, "avatar"); | ||
| 480 | |||
| 481 | if (xs_is_null(av) || *av == '\0') | ||
| 482 | avatar = xs_fmt("%s/susie.png", srv_baseurl); | ||
| 483 | else | ||
| 484 | avatar = xs_dup(av); | ||
| 485 | |||
| 486 | acct = xs_dict_append(acct, "avatar", avatar); | ||
| 487 | |||
| 488 | *body = xs_json_dumps_pp(acct, 4); | ||
| 489 | *ctype = "application/json"; | ||
| 490 | status = 200; | ||
| 491 | } | ||
| 492 | else { | ||
| 493 | status = 422; // "Unprocessable entity" (no login) | ||
| 494 | } | ||
| 495 | } | ||
| 496 | else | ||
| 497 | if (strcmp(cmd, "/timelines/home") == 0) { | ||
| 498 | /* the private timeline */ | ||
| 499 | if (logged_in) { | ||
| 500 | const char *max_id = xs_dict_get(args, "max_id"); | ||
| 501 | const char *since_id = xs_dict_get(args, "since_id"); | ||
| 502 | const char *min_id = xs_dict_get(args, "min_id"); | ||
| 503 | const char *limit_s = xs_dict_get(args, "limit"); | ||
| 504 | int limit = 0; | ||
| 505 | int cnt = 0; | ||
| 506 | |||
| 507 | if (!xs_is_null(limit_s)) | ||
| 508 | limit = atoi(limit_s); | ||
| 509 | |||
| 510 | if (limit == 0) | ||
| 511 | limit = 20; | ||
| 512 | |||
| 513 | xs *timeline = timeline_simple_list(&snac, "private", 0, XS_ALL); | ||
| 514 | |||
| 515 | xs *out = xs_list_new(); | ||
| 516 | xs_list *p = timeline; | ||
| 517 | xs_str *v; | ||
| 518 | |||
| 519 | while (xs_list_iter(&p, &v) && cnt < limit) { | ||
| 520 | xs *msg = NULL; | ||
| 521 | |||
| 522 | /* only return entries older that max_id */ | ||
| 523 | if (max_id) { | ||
| 524 | if (strcmp(v, max_id) == 0) | ||
| 525 | max_id = NULL; | ||
| 526 | |||
| 527 | continue; | ||
| 528 | } | ||
| 529 | |||
| 530 | /* only returns entries newer than since_id */ | ||
| 531 | if (since_id) { | ||
| 532 | if (strcmp(v, since_id) == 0) | ||
| 533 | break; | ||
| 534 | } | ||
| 535 | |||
| 536 | /* only returns entries newer than min_id */ | ||
| 537 | /* what does really "Return results immediately newer than ID" mean? */ | ||
| 538 | if (min_id) { | ||
| 539 | if (strcmp(v, min_id) == 0) | ||
| 540 | break; | ||
| 541 | } | ||
| 542 | |||
| 543 | /* get the entry */ | ||
| 544 | if (!valid_status(timeline_get_by_md5(&snac, v, &msg))) | ||
| 545 | continue; | ||
| 546 | |||
| 547 | /* discard non-Notes */ | ||
| 548 | if (strcmp(xs_dict_get(msg, "type"), "Note") != 0) | ||
| 549 | continue; | ||
| 550 | |||
| 551 | xs *actor = NULL; | ||
| 552 | actor_get(&snac, xs_dict_get(msg, "attributedTo"), &actor); | ||
| 553 | |||
| 554 | /* if the author is not here, discard */ | ||
| 555 | if (actor == NULL) | ||
| 556 | continue; | ||
| 557 | |||
| 558 | /** shave the yak converting an ActivityPub Note to a Mastodon status **/ | ||
| 559 | |||
| 560 | xs *acct = xs_dict_new(); | ||
| 561 | |||
| 562 | const char *display_name = xs_dict_get(actor, "name"); | ||
| 563 | if (xs_is_null(display_name) || *display_name == '\0') | ||
| 564 | display_name = xs_dict_get(actor, "preferredUsername"); | ||
| 565 | |||
| 566 | const char *id = xs_dict_get(actor, "id"); | ||
| 567 | const char *pub = xs_dict_get(actor, "published"); | ||
| 568 | xs *acct_md5 = xs_md5_hex(id, strlen(id)); | ||
| 569 | acct = xs_dict_append(acct, "id", acct_md5); | ||
| 570 | acct = xs_dict_append(acct, "username", xs_dict_get(actor, "preferredUsername")); | ||
| 571 | acct = xs_dict_append(acct, "acct", xs_dict_get(actor, "preferredUsername")); | ||
| 572 | acct = xs_dict_append(acct, "display_name", display_name); | ||
| 573 | |||
| 574 | if (pub) | ||
| 575 | acct = xs_dict_append(acct, "created_at", pub); | ||
| 576 | |||
| 577 | acct = xs_dict_append(acct, "note", xs_dict_get(actor, "summary")); | ||
| 578 | acct = xs_dict_append(acct, "url", id); | ||
| 579 | |||
| 580 | xs *avatar = NULL; | ||
| 581 | xs_dict *av = xs_dict_get(actor, "icon"); | ||
| 582 | |||
| 583 | if (xs_type(av) == XSTYPE_DICT) | ||
| 584 | avatar = xs_dup(xs_dict_get(av, "url")); | ||
| 585 | else | ||
| 586 | avatar = xs_fmt("%s/susie.png", srv_baseurl); | ||
| 587 | |||
| 588 | acct = xs_dict_append(acct, "avatar", avatar); | ||
| 589 | |||
| 590 | xs *f = xs_val_new(XSTYPE_FALSE); | ||
| 591 | xs *t = xs_val_new(XSTYPE_TRUE); | ||
| 592 | xs *n = xs_val_new(XSTYPE_NULL); | ||
| 593 | xs *el = xs_list_new(); | ||
| 594 | xs *idx = NULL; | ||
| 595 | xs *ixc = NULL; | ||
| 596 | |||
| 597 | char *tmp; | ||
| 598 | id = xs_dict_get(msg, "id"); | ||
| 599 | xs *mid = mastoapi_id(msg); | ||
| 600 | |||
| 601 | xs *st = xs_dict_new(); | ||
| 602 | |||
| 603 | st = xs_dict_append(st, "id", mid); | ||
| 604 | st = xs_dict_append(st, "uri", id); | ||
| 605 | st = xs_dict_append(st, "url", id); | ||
| 606 | st = xs_dict_append(st, "created_at", xs_dict_get(msg, "published")); | ||
| 607 | st = xs_dict_append(st, "account", acct); | ||
| 608 | st = xs_dict_append(st, "content", xs_dict_get(msg, "content")); | ||
| 609 | |||
| 610 | st = xs_dict_append(st, "visibility", | ||
| 611 | is_msg_public(&snac, msg) ? "public" : "private"); | ||
| 612 | |||
| 613 | tmp = xs_dict_get(msg, "sensitive"); | ||
| 614 | if (xs_is_null(tmp)) | ||
| 615 | tmp = f; | ||
| 616 | |||
| 617 | st = xs_dict_append(st, "sensitive", tmp); | ||
| 618 | |||
| 619 | tmp = xs_dict_get(msg, "summary"); | ||
| 620 | if (xs_is_null(tmp)) | ||
| 621 | tmp = ""; | ||
| 622 | |||
| 623 | st = xs_dict_append(st, "spoiler_text", tmp); | ||
| 624 | |||
| 625 | /* create the list of attachments */ | ||
| 626 | xs *matt = xs_list_new(); | ||
| 627 | xs_list *att = xs_dict_get(msg, "attachment"); | ||
| 628 | xs_str *aobj; | ||
| 629 | |||
| 630 | while (xs_list_iter(&att, &aobj)) { | ||
| 631 | const char *mtype = xs_dict_get(aobj, "mediaType"); | ||
| 632 | |||
| 633 | if (!xs_is_null(mtype) && xs_startswith(mtype, "image/")) { | ||
| 634 | xs *matteid = xs_fmt("%s_%d", id, xs_list_len(matt)); | ||
| 635 | xs *matte = xs_dict_new(); | ||
| 636 | |||
| 637 | matte = xs_dict_append(matte, "id", matteid); | ||
| 638 | matte = xs_dict_append(matte, "type", "image"); | ||
| 639 | matte = xs_dict_append(matte, "url", xs_dict_get(aobj, "url")); | ||
| 640 | matte = xs_dict_append(matte, "preview_url", xs_dict_get(aobj, "url")); | ||
| 641 | matte = xs_dict_append(matte, "remote_url", xs_dict_get(aobj, "url")); | ||
| 642 | matte = xs_dict_append(matte, "description", xs_dict_get(aobj, "name")); | ||
| 643 | |||
| 644 | matt = xs_list_append(matt, matte); | ||
| 645 | } | ||
| 646 | } | ||
| 647 | |||
| 648 | st = xs_dict_append(st, "media_attachments", matt); | ||
| 649 | |||
| 650 | st = xs_dict_append(st, "mentions", el); | ||
| 651 | st = xs_dict_append(st, "tags", el); | ||
| 652 | st = xs_dict_append(st, "emojis", el); | ||
| 653 | |||
| 654 | xs_free(idx); | ||
| 655 | xs_free(ixc); | ||
| 656 | idx = object_likes(id); | ||
| 657 | ixc = xs_number_new(xs_list_len(idx)); | ||
| 658 | |||
| 659 | st = xs_dict_append(st, "favourites_count", ixc); | ||
| 660 | st = xs_dict_append(st, "favourited", | ||
| 661 | xs_list_in(idx, snac.md5) != -1 ? t : f); | ||
| 662 | |||
| 663 | xs_free(idx); | ||
| 664 | xs_free(ixc); | ||
| 665 | idx = object_announces(id); | ||
| 666 | ixc = xs_number_new(xs_list_len(idx)); | ||
| 667 | |||
| 668 | st = xs_dict_append(st, "reblogs_count", ixc); | ||
| 669 | st = xs_dict_append(st, "reblogged", | ||
| 670 | xs_list_in(idx, snac.md5) != -1 ? t : f); | ||
| 671 | |||
| 672 | xs_free(idx); | ||
| 673 | xs_free(ixc); | ||
| 674 | idx = object_children(id); | ||
| 675 | ixc = xs_number_new(xs_list_len(idx)); | ||
| 676 | |||
| 677 | st = xs_dict_append(st, "replies_count", ixc); | ||
| 678 | |||
| 679 | /* default in_reply_to values */ | ||
| 680 | st = xs_dict_append(st, "in_reply_to_id", n); | ||
| 681 | st = xs_dict_append(st, "in_reply_to_account_id", n); | ||
| 682 | |||
| 683 | tmp = xs_dict_get(msg, "inReplyTo"); | ||
| 684 | if (!xs_is_null(tmp)) { | ||
| 685 | xs *irto = NULL; | ||
| 686 | |||
| 687 | if (valid_status(object_get(tmp, &irto))) { | ||
| 688 | xs *irt_mid = mastoapi_id(irto); | ||
| 689 | st = xs_dict_set(st, "in_reply_to_id", irt_mid); | ||
| 690 | |||
| 691 | char *at = NULL; | ||
| 692 | if (!xs_is_null(at = xs_dict_get(irto, "attributedTo"))) { | ||
| 693 | xs *at_md5 = xs_md5_hex(at, strlen(at)); | ||
| 694 | st = xs_dict_set(st, "in_reply_to_account_id", at_md5); | ||
| 695 | } | ||
| 696 | } | ||
| 697 | } | ||
| 698 | |||
| 699 | st = xs_dict_append(st, "reblog", n); | ||
| 700 | st = xs_dict_append(st, "poll", n); | ||
| 701 | st = xs_dict_append(st, "card", n); | ||
| 702 | st = xs_dict_append(st, "language", n); | ||
| 703 | |||
| 704 | tmp = xs_dict_get(msg, "sourceContent"); | ||
| 705 | if (xs_is_null(tmp)) | ||
| 706 | tmp = ""; | ||
| 707 | |||
| 708 | st = xs_dict_append(st, "text", tmp); | ||
| 709 | |||
| 710 | tmp = xs_dict_get(msg, "updated"); | ||
| 711 | if (xs_is_null(tmp)) | ||
| 712 | tmp = n; | ||
| 713 | |||
| 714 | st = xs_dict_append(st, "edited_at", tmp); | ||
| 715 | |||
| 716 | out = xs_list_append(out, st); | ||
| 717 | |||
| 718 | cnt++; | ||
| 719 | } | ||
| 720 | |||
| 721 | *body = xs_json_dumps_pp(out, 4); | ||
| 722 | *ctype = "application/json"; | ||
| 723 | status = 200; | ||
| 724 | |||
| 725 | srv_debug(0, xs_fmt("mastoapi timeline: returned %d entries", xs_list_len(out))); | ||
| 726 | } | ||
| 727 | else { | ||
| 728 | status = 401; // unauthorized | ||
| 729 | } | ||
| 730 | } | ||
| 731 | else | ||
| 732 | if (strcmp(cmd, "/timelines/public") == 0) { | ||
| 733 | /* the public timeline */ | ||
| 734 | } | ||
| 735 | |||
| 736 | /* user cleanup */ | ||
| 737 | if (logged_in) | ||
| 738 | user_free(&snac); | ||
| 739 | |||
| 740 | return status; | ||
| 741 | } | ||
| 742 | |||
| 743 | |||
| 744 | int mastoapi_post_handler(const xs_dict *req, const char *q_path, | ||
| 745 | const char *payload, int p_size, | ||
| 746 | char **body, int *b_size, char **ctype) | ||
| 747 | { | ||
| 748 | if (!xs_startswith(q_path, "/api/v1/")) | ||
| 749 | return 0; | ||
| 750 | |||
| 751 | srv_debug(0, xs_fmt("mastoapi_post_handler %s", q_path)); | ||
| 752 | { | ||
| 753 | xs *j = xs_json_dumps_pp(req, 4); | ||
| 754 | printf("mastoapi post:\n%s\n", j); | ||
| 755 | } | ||
| 756 | |||
| 757 | int status = 404; | ||
| 758 | xs *msg = NULL; | ||
| 759 | char *i_ctype = xs_dict_get(req, "content-type"); | ||
| 760 | |||
| 761 | if (xs_startswith(i_ctype, "application/json")) | ||
| 762 | msg = xs_json_loads(payload); | ||
| 763 | else | ||
| 764 | msg = xs_dup(xs_dict_get(req, "p_vars")); | ||
| 765 | |||
| 766 | if (msg == NULL) | ||
| 767 | return 400; | ||
| 768 | |||
| 769 | { | ||
| 770 | xs *j = xs_json_dumps_pp(msg, 4); | ||
| 771 | printf("%s\n", j); | ||
| 772 | } | ||
| 773 | |||
| 774 | xs *cmd = xs_replace(q_path, "/api/v1", ""); | ||
| 775 | |||
| 776 | if (strcmp(cmd, "/apps") == 0) { | ||
| 777 | const char *name = xs_dict_get(msg, "client_name"); | ||
| 778 | const char *ruri = xs_dict_get(msg, "redirect_uris"); | ||
| 779 | const char *scope = xs_dict_get(msg, "scope"); | ||
| 780 | |||
| 781 | if (xs_type(ruri) == XSTYPE_LIST) | ||
| 782 | ruri = xs_dict_get(ruri, 0); | ||
| 783 | |||
| 784 | if (name && ruri) { | ||
| 785 | xs *app = xs_dict_new(); | ||
| 786 | xs *id = xs_replace_i(tid(0), ".", ""); | ||
| 787 | xs *cid = random_str(); | ||
| 788 | xs *csec = random_str(); | ||
| 789 | xs *vkey = random_str(); | ||
| 790 | |||
| 791 | app = xs_dict_append(app, "name", name); | ||
| 792 | app = xs_dict_append(app, "redirect_uri", ruri); | ||
| 793 | app = xs_dict_append(app, "client_id", cid); | ||
| 794 | app = xs_dict_append(app, "client_secret", csec); | ||
| 795 | app = xs_dict_append(app, "vapid_key", vkey); | ||
| 796 | app = xs_dict_append(app, "id", id); | ||
| 797 | |||
| 798 | *body = xs_json_dumps_pp(app, 4); | ||
| 799 | *ctype = "application/json"; | ||
| 800 | status = 200; | ||
| 801 | |||
| 802 | app = xs_dict_append(app, "code", ""); | ||
| 803 | |||
| 804 | if (scope) | ||
| 805 | app = xs_dict_append(app, "scope", scope); | ||
| 806 | |||
| 807 | app_add(cid, app); | ||
| 808 | |||
| 809 | srv_debug(0, xs_fmt("mastoapi apps: new app %s", cid)); | ||
| 810 | } | ||
| 811 | } | ||
| 812 | |||
| 813 | return status; | ||
| 814 | } | ||
| @@ -223,3 +223,14 @@ int resetpwd(snac *snac); | |||
| 223 | int job_fifo_ready(void); | 223 | int job_fifo_ready(void); |
| 224 | void job_post(const xs_val *job, int urgent); | 224 | void job_post(const xs_val *job, int urgent); |
| 225 | void job_wait(xs_val **job); | 225 | void job_wait(xs_val **job); |
| 226 | |||
| 227 | int oauth_get_handler(const xs_dict *req, const char *q_path, | ||
| 228 | char **body, int *b_size, char **ctype); | ||
| 229 | int oauth_post_handler(const xs_dict *req, const char *q_path, | ||
| 230 | const char *payload, int p_size, | ||
| 231 | char **body, int *b_size, char **ctype); | ||
| 232 | int mastoapi_get_handler(const xs_dict *req, const char *q_path, | ||
| 233 | char **body, int *b_size, char **ctype); | ||
| 234 | int mastoapi_post_handler(const xs_dict *req, const char *q_path, | ||
| 235 | const char *payload, int p_size, | ||
| 236 | char **body, int *b_size, char **ctype); | ||
diff --git a/xs_encdec.h b/xs_encdec.h index 5966583..b88736e 100644 --- a/xs_encdec.h +++ b/xs_encdec.h | |||
| @@ -6,6 +6,7 @@ | |||
| 6 | 6 | ||
| 7 | xs_str *xs_hex_enc(const xs_val *data, int size); | 7 | xs_str *xs_hex_enc(const xs_val *data, int size); |
| 8 | xs_val *xs_hex_dec(const xs_str *hex, int *size); | 8 | xs_val *xs_hex_dec(const xs_str *hex, int *size); |
| 9 | int xs_is_hex(const char *str); | ||
| 9 | xs_str *xs_base64_enc(const xs_val *data, int sz); | 10 | xs_str *xs_base64_enc(const xs_val *data, int sz); |
| 10 | xs_val *xs_base64_dec(const xs_str *data, int *size); | 11 | xs_val *xs_base64_dec(const xs_str *data, int *size); |
| 11 | xs_str *xs_utf8_enc(xs_str *str, unsigned int cpoint); | 12 | xs_str *xs_utf8_enc(xs_str *str, unsigned int cpoint); |
| @@ -65,6 +66,18 @@ xs_val *xs_hex_dec(const xs_str *hex, int *size) | |||
| 65 | } | 66 | } |
| 66 | 67 | ||
| 67 | 68 | ||
| 69 | int xs_is_hex(const char *str) | ||
| 70 | /* returns 1 if str is an hex string */ | ||
| 71 | { | ||
| 72 | while (*str) { | ||
| 73 | if (strchr("0123456789abcdefABCDEF", *str++) == NULL) | ||
| 74 | return 0; | ||
| 75 | } | ||
| 76 | |||
| 77 | return 1; | ||
| 78 | } | ||
| 79 | |||
| 80 | |||
| 68 | xs_str *xs_base64_enc(const xs_val *data, int sz) | 81 | xs_str *xs_base64_enc(const xs_val *data, int sz) |
| 69 | /* encodes data to base64 */ | 82 | /* encodes data to base64 */ |
| 70 | { | 83 | { |
diff --git a/xs_version.h b/xs_version.h index 559fab6..eff4ddf 100644 --- a/xs_version.h +++ b/xs_version.h | |||
| @@ -1 +1 @@ | |||
| /* b4afa5f823a998a263159ebfe9be67b81a8cc774 */ | /* 69d6e64d31491688ba4411e71c55e6c25482b17e */ | ||