Merge pull request 'master' (#1) from grunfink/snac2:master into master

Reviewed-on: https://codeberg.org/poesty/snac2/pulls/1
This commit is contained in:
poesty 2023-05-07 07:27:24 +00:00
commit e65760a349
16 changed files with 610 additions and 147 deletions

View file

@ -1,6 +1,6 @@
PREFIX=/usr/local
PREFIX_MAN=$(PREFIX)/man
CFLAGS?=-g -Wall
CFLAGS?=-g -Wall -Wextra
all: snac
@ -40,7 +40,7 @@ httpd.o: httpd.c xs.h xs_io.h xs_encdec.h xs_json.h xs_socket.h \
xs_httpd.h xs_mime.h snac.h
main.o: main.c xs.h xs_io.h xs_encdec.h xs_json.h snac.h
mastoapi.o: mastoapi.c xs.h xs_encdec.h xs_openssl.h xs_json.h xs_io.h \
xs_time.h snac.h
xs_time.h xs_glob.h snac.h
snac.o: snac.c xs.h xs_io.h xs_encdec.h xs_json.h xs_curl.h xs_openssl.h \
xs_socket.h xs_httpd.h xs_mime.h xs_regex.h xs_set.h xs_time.h xs_glob.h \
snac.h

View file

@ -1,5 +1,15 @@
# Release Notes
## 2.30
Fixed a bug that made some notifications to be missed.
New Mastodon API features: the instance public timeline is now a real one, unfavourite / unreblog is supported (somewhat). Some regression bugs regarding image posting were also fixed.
The non-standard `Ping` and `Pong` #ActivityPub activities have been implemented as proposed by @tedu@honk.tedunangst.com in the https://humungus.tedunangst.com/r/honk/v/tip/f/docs/ping.txt document (with a minor diversion: retries are managed in the same way as the rest of #snac messages).
The build process now includes the `-Wextra` flag.
## 2.29
New Mastodon API features: account search, relationships (so the Follow/Unfollow buttons now appear for each account), follow and unfollow accounts, an instance-level timeline (very kludgy), custom emojis for accounts and statuses, many bug fixes (sadly, the Mastodon official app still does not work).

View file

@ -109,7 +109,7 @@ int actor_request(snac *snac, const char *actor, xs_dict **data)
if (valid_status(status2)) {
/* renew data */
status = actor_add(snac, actor, payload);
status = actor_add(actor, payload);
if (data != NULL) {
*data = payload;
@ -437,7 +437,8 @@ void process_tags(snac *snac, const char *content, d_char **n_content, d_char **
/** messages **/
d_char *msg_base(snac *snac, char *type, char *id, char *actor, char *date, char *object)
xs_dict *msg_base(snac *snac, const char *type, const char *id,
const char *actor, const char *date, const char *object)
/* creates a base ActivityPub message */
{
xs *did = NULL;
@ -467,7 +468,7 @@ d_char *msg_base(snac *snac, char *type, char *id, char *actor, char *date, char
}
}
d_char *msg = xs_dict_new();
xs_dict *msg = xs_dict_new();
msg = xs_dict_append(msg, "@context", "https:/" "/www.w3.org/ns/activitystreams");
msg = xs_dict_append(msg, "type", type);
@ -845,6 +846,28 @@ xs_dict *msg_note(snac *snac, const xs_str *content, const xs_val *rcpts,
}
xs_dict *msg_ping(snac *user, const char *rcpt)
/* creates a Ping message (https://humungus.tedunangst.com/r/honk/v/tip/f/docs/ping.txt) */
{
xs_dict *msg = msg_base(user, "Ping", "@dummy", user->actor, NULL, NULL);
msg = xs_dict_append(msg, "to", rcpt);
return msg;
}
xs_dict *msg_pong(snac *user, const char *rcpt, const char *object)
/* creates a Pong message (https://humungus.tedunangst.com/r/honk/v/tip/f/docs/ping.txt) */
{
xs_dict *msg = msg_base(user, "Pong", "@dummy", user->actor, NULL, object);
msg = xs_dict_append(msg, "to", rcpt);
return msg;
}
void notify(snac *snac, xs_str *type, xs_str *utype, xs_str *actor, xs_dict *msg)
/* notifies the user of relevant events */
{
@ -1121,7 +1144,7 @@ int process_input_message(snac *snac, xs_dict *msg, xs_dict *req)
else
if (strcmp(type, "Update") == 0) {
if (strcmp(utype, "Person") == 0) {
actor_add(snac, actor, xs_dict_get(msg, "object"));
actor_add(actor, xs_dict_get(msg, "object"));
snac_log(snac, xs_fmt("updated actor %s", actor));
}
@ -1147,7 +1170,19 @@ int process_input_message(snac *snac, xs_dict *msg, xs_dict *req)
snac_debug(snac, 1, xs_fmt("ignored 'Delete' for unknown object %s", object));
}
else
snac_debug(snac, 1, xs_fmt("process_message type '%s' ignored", type));
if (strcmp(type, "Pong") == 0) {
snac_log(snac, xs_fmt("'Pong' received from %s", actor));
}
else
if (strcmp(type, "Ping") == 0) {
snac_log(snac, xs_fmt("'Ping' requested from %s", actor));
xs *rsp = msg_pong(snac, actor, xs_dict_get(msg, "id"));
enqueue_output_by_actor(snac, rsp, actor, 0);
}
else
snac_debug(snac, 1, xs_fmt("process_input_message type '%s' ignored", type));
if (do_notify) {
notify(snac, type, utype, actor, msg);
@ -1384,8 +1419,9 @@ void process_queue_item(xs_dict *q_item)
xs *headers = xs_dict_new();
headers = xs_dict_append(headers, "content-type", "application/json");
xs *rsp = xs_http_request("POST", url, headers,
xs *rsp = xs_http_request("POST", url, headers,
body, strlen(body), &status, NULL, NULL, 0);
rsp = xs_free(rsp);
srv_debug(0, xs_fmt("telegram post %d", status));
}
@ -1426,7 +1462,7 @@ int process_queue(void)
/** HTTP handlers */
int activitypub_get_handler(d_char *req, char *q_path,
int activitypub_get_handler(const xs_dict *req, const char *q_path,
char **body, int *b_size, char **ctype)
{
int status = 200;
@ -1519,11 +1555,13 @@ int activitypub_get_handler(d_char *req, char *q_path,
}
int activitypub_post_handler(d_char *req, char *q_path,
d_char *payload, int p_size,
int activitypub_post_handler(const xs_dict *req, const char *q_path,
char *payload, int p_size,
char **body, int *b_size, char **ctype)
/* processes an input message */
{
(void)b_size;
int status = 202; /* accepted */
char *i_ctype = xs_dict_get(req, "content-type");
snac snac;

121
data.c
View file

@ -328,6 +328,51 @@ int index_add(const char *fn, const char *id)
}
int index_del_md5(const char *fn, const char *md5)
/* deletes an md5 from an index */
{
int status = 404;
FILE *f;
pthread_mutex_lock(&data_mutex);
if ((f = fopen(fn, "r+")) != NULL) {
char line[256];
while (fgets(line, sizeof(line), f) != NULL) {
line[32] = '\0';
if (strcmp(line, md5) == 0) {
/* found! just rewind, overwrite it with garbage
and an eventual call to index_gc() will clean it
[yes: this breaks index_len()] */
fseek(f, -33, SEEK_CUR);
fwrite("-", 1, 1, f);
status = 200;
break;
}
}
fclose(f);
}
else
status = 500;
pthread_mutex_unlock(&data_mutex);
return status;
}
int index_del(const char *fn, const char *id)
/* deletes an id from an index */
{
xs *md5 = xs_md5_hex(id, strlen(id));
return index_del_md5(fn, md5);
}
int index_gc(const char *fn)
/* garbage-collects an index, deleting objects that are not here */
{
@ -772,6 +817,23 @@ int object_admire(const char *id, const char *actor, int like)
}
int object_unadmire(const char *id, const char *actor, int like)
/* actor no longer likes or announces this object */
{
int status;
xs *fn = _object_fn(id);
fn = xs_replace_i(fn, ".json", like ? "_l.idx" : "_a.idx");
status = index_del(fn, actor);
srv_debug(0,
xs_fmt("object_unadmire (%s) %s %s %d", like ? "Like" : "Announce", actor, fn, status));
return status;
}
int _object_user_cache(snac *snac, const char *id, const char *cachedir, int del)
/* adds or deletes from a user cache */
{
@ -969,8 +1031,13 @@ void timeline_update_indexes(snac *snac, const char *id)
if (valid_status(object_get(id, &msg))) {
/* if its ours and is public, also store in public */
if (is_msg_public(snac, msg))
if (is_msg_public(snac, msg)) {
object_user_cache_add(snac, id, "public");
/* also add it to the instance public timeline */
xs *ipt = xs_fmt("%s/public.idx", srv_basedir);
index_add(ipt, id);
}
}
}
}
@ -1041,7 +1108,7 @@ xs_list *timeline_top_level(snac *snac, xs_list *list)
}
d_char *timeline_simple_list(snac *snac, const char *idx_name, int skip, int show)
xs_list *timeline_simple_list(snac *snac, const char *idx_name, int skip, int show)
/* returns a timeline (with all entries) */
{
int c_max;
@ -1059,7 +1126,7 @@ d_char *timeline_simple_list(snac *snac, const char *idx_name, int skip, int sho
}
d_char *timeline_list(snac *snac, const char *idx_name, int skip, int show)
xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show)
/* returns a timeline (only top level entries) */
{
xs *list = timeline_simple_list(snac, idx_name, skip, show);
@ -1068,6 +1135,15 @@ d_char *timeline_list(snac *snac, const char *idx_name, int skip, int show)
}
xs_list *timeline_instance_list(int skip, int show)
/* returns the timeline for the full instance */
{
xs *idx = xs_fmt("%s/public.idx", srv_basedir);
return index_list_desc(idx, skip, show);
}
/** following **/
/* this needs special treatment and cannot use the object db as is,
@ -1273,30 +1349,49 @@ int is_hidden(snac *snac, const char *id)
}
int actor_add(snac *snac, const char *actor, d_char *msg)
int actor_add(const char *actor, xs_dict *msg)
/* adds an actor */
{
return object_add_ow(actor, msg);
}
int actor_get(snac *snac, const char *actor, d_char **data)
int actor_get(snac *snac1, const char *actor, xs_dict **data)
/* returns an already downloaded actor */
{
int status = 200;
d_char *d;
xs_dict *d = NULL;
if (strcmp(actor, snac->actor) == 0) {
if (strcmp(actor, snac1->actor) == 0) {
/* this actor */
if (data)
*data = msg_actor(snac);
*data = msg_actor(snac1);
return status;
}
if (xs_startswith(actor, srv_baseurl)) {
/* it's a (possible) local user */
xs *l = xs_split(actor, "/");
const char *uid = xs_list_get(l, -1);
snac user;
if (!xs_is_null(uid) && user_open(&user, uid)) {
if (data)
*data = msg_actor(&user);
user_free(&user);
return 200;
}
else
return 404;
}
/* read the object */
if (!valid_status(status = object_get(actor, &d)))
if (!valid_status(status = object_get(actor, &d))) {
d = xs_free(d);
return status;
}
if (data)
*data = d;
@ -1719,7 +1814,7 @@ static xs_dict *_new_qmsg(const char *type, const xs_val *msg, int retries)
}
void enqueue_input(snac *snac, xs_dict *msg, xs_dict *req, int retries)
void enqueue_input(snac *snac, const xs_dict *msg, const xs_dict *req, int retries)
/* enqueues an input message */
{
xs *qmsg = _new_qmsg("input", msg, retries);
@ -2020,7 +2115,11 @@ void purge_server(void)
xs *ib_dir = xs_fmt("%s/inbox", srv_basedir);
_purge_dir(ib_dir, 7);
srv_debug(1, xs_fmt("purge: global (obj: %d, idx: %d)", cnt, icnt));
/* purge the instance timeline */
xs *itl_fn = xs_fmt("%s/public.idx", srv_basedir);
int itl_gc = index_gc(itl_fn);
srv_debug(1, xs_fmt("purge: global (obj: %d, idx: %d, itl: %d)", cnt, icnt, itl_gc));
}

View file

@ -220,6 +220,36 @@ Please take note that they will show your timeline in a 'Mastodon fashion'
post display with the most active threads at the top that the web interface of
.Nm
provides.
.Ss Implementing post bots
.Nm
makes very easy to post messages in a non-interactive manner. This example
posts a string:
.Bd -literal -offset indent
uptime | snac note $SNAC_BASEDIR $SNAC_USER -
.Ed
.Pp
You can setup a line like this from a
.Xr crontab 5
or similar. Take note that you need a) command-line access to the same machine
that hosts the
.Nm
instance, and b) write permissions to the storage directories and files.
.Pp
You can also post non-interactively using the Mastodon API and a command-line
http tool like
.Xr curl 1
or similar. This has the advantage that you can do it remotely from any host,
anywhere; the only thing you need is an API Token. This is an example:
.Bd -literal -offset indent
curl -X POST https://$SNAC_HOST/api/v1/statuses \\
--header "Authorization: Bearer ${TOKEN}" -d "status=$(uptime)"
.Ed
.Pp
You can obtain an API Token by connecting to the following URL:
.Bd -literal -offset indent
https://$SNAC_HOST/oauth/x-snac-get-token
.Ed
.Pp
.Sh ENVIRONMENT
.Bl -tag -width Ds
.It Ev DEBUG

View file

@ -1,7 +1,7 @@
#! /bin/sh
if [ ! -e /data/data/server.json ]
then
echo -ne "0.0.0.0\r\n8001\r\nlocalhost\r\n\r\n" | snac init /data/data
echo -ne "0.0.0.0\r\n8001\r\nlocalhost\r\n\r\n\r\n" | snac init /data/data
snac adduser /data/data testuser
fi
SSLKEYLOGFILE=/data/key snac httpd /data/data

27
html.c
View file

@ -79,7 +79,7 @@ xs_str *actor_name(xs_dict *actor)
}
d_char *html_actor_icon(snac *snac, d_char *os, char *actor,
xs_str *html_actor_icon(xs_str *os, char *actor,
const char *date, const char *udate, const char *url, int priv)
{
xs *s = xs_str_new(NULL);
@ -168,7 +168,7 @@ d_char *html_msg_icon(snac *snac, d_char *os, char *msg)
date = xs_dict_get(msg, "published");
udate = xs_dict_get(msg, "updated");
os = html_actor_icon(snac, os, actor, date, udate, url, priv);
os = html_actor_icon(os, actor, date, udate, url, priv);
}
return os;
@ -983,7 +983,7 @@ d_char *html_entry(snac *snac, d_char *os, char *msg, int local,
}
d_char *html_user_footer(snac *snac, d_char *s)
xs_str *html_user_footer(xs_str *s)
{
xs *s1 = xs_fmt(
"<div class=\"snac-footer\">\n"
@ -1064,7 +1064,7 @@ d_char *html_timeline(snac *snac, char *list, int local, int skip, int show, int
s = xs_str_cat(s, s1);
}
s = html_user_footer(snac, s);
s = html_user_footer(s);
s = xs_str_cat(s, "</body>\n</html>\n");
@ -1088,8 +1088,7 @@ d_char *html_people_list(snac *snac, d_char *os, d_char *list, const char *heade
if (valid_status(actor_get(snac, actor_id, &actor))) {
s = xs_str_cat(s, "<div class=\"snac-post\">\n");
s = html_actor_icon(snac, s, actor, xs_dict_get(actor, "published"), NULL, NULL, 0);
s = html_actor_icon(s, actor, xs_dict_get(actor, "published"), NULL, NULL, 0);
/* content (user bio) */
char *c = xs_dict_get(actor, "summary");
@ -1182,7 +1181,7 @@ d_char *html_people(snac *snac)
s = html_people_list(snac, s, wers, L("People that follows you"), "e");
s = html_user_footer(snac, s);
s = html_user_footer(s);
s = xs_str_cat(s, "</body>\n</html>\n");
@ -1226,7 +1225,7 @@ xs_str *html_notifications(snac *snac)
const char *actor_id = xs_dict_get(noti, "actor");
xs *actor = NULL;
if (!valid_status(object_get(actor_id, &actor)))
if (!valid_status(actor_get(snac, actor_id, &actor)))
continue;
xs *a_name = actor_name(actor);
@ -1277,12 +1276,13 @@ xs_str *html_notifications(snac *snac)
s = xs_str_cat(s, s1);
}
s = html_user_footer(snac, s);
s = html_user_footer(s);
s = xs_str_cat(s, "</body>\n</html>\n");
/* set the check time to now */
xs *dummy = notify_check_time(snac, 1);
dummy = xs_free(dummy);
timeline_touch(snac);
@ -1290,7 +1290,8 @@ xs_str *html_notifications(snac *snac)
}
int html_get_handler(d_char *req, char *q_path, char **body, int *b_size, char **ctype)
int html_get_handler(const xs_dict *req, const char *q_path,
char **body, int *b_size, char **ctype)
{
char *accept = xs_dict_get(req, "accept");
int status = 404;
@ -1547,9 +1548,13 @@ int html_get_handler(d_char *req, char *q_path, char **body, int *b_size, char *
}
int html_post_handler(d_char *req, char *q_path, d_char *payload, int p_size,
int html_post_handler(const xs_dict *req, const char *q_path,
char *payload, int p_size,
char **body, int *b_size, char **ctype)
{
(void)p_size;
(void)ctype;
int status = 0;
snac snac;
char *uid, *p_path;

2
http.c
View file

@ -33,7 +33,7 @@ xs_dict *http_signed_request_raw(const char *keyid, const char *seckey,
date = xs_str_utctime(0, "%a, %d %b %Y %H:%M:%S GMT");
{
xs *s = xs_replace(url, "https:/" "/", "");
xs *s = xs_replace_n(url, "https:/" "/", "", 1);
l1 = xs_split_n(s, "/", 1);
}

View file

@ -43,12 +43,14 @@ d_char *nodeinfo_2_0(void)
}
int server_get_handler(d_char *req, char *q_path,
int server_get_handler(xs_dict *req, char *q_path,
char **body, int *b_size, char **ctype)
/* basic server services */
{
int status = 0;
(void)req;
/* is it the server root? */
if (*q_path == '\0') {
/* try to open greeting.html */
@ -285,6 +287,8 @@ static jmp_buf on_break;
void term_handler(int s)
{
(void)s;
longjmp(on_break, 1);
}
@ -401,6 +405,8 @@ static void *background_thread(void *arg)
{
time_t purge_time;
(void)arg;
/* first purge time */
purge_time = time(NULL) + 10 * 60;

22
main.c
View file

@ -30,6 +30,7 @@ int usage(void)
printf("actor {basedir} {uid} {url} Requests an actor\n");
printf("note {basedir} {uid} {'text'} Sends a note to followers\n");
printf("resetpwd {basedir} {uid} Resets the password of a user\n");
printf("ping {basedir} {uid} {actor} Pings an actor\n");
return 1;
}
@ -228,6 +229,27 @@ int main(int argc, char *argv[])
return 0;
}
if (strcmp(cmd, "ping") == 0) {
xs *actor_o = NULL;
if (valid_status(actor_request(&snac, url, &actor_o))) {
xs *msg = msg_ping(&snac, url);
enqueue_output_by_actor(&snac, msg, url, 0);
if (dbglevel) {
xs *j = xs_json_dumps_pp(msg, 4);
printf("%s\n", j);
}
}
else {
srv_log(xs_fmt("Error getting actor %s", url));
return 1;
}
return 0;
}
if (strcmp(cmd, "request") == 0) {
int status;
xs *data = NULL;

View file

@ -162,7 +162,7 @@ const char *login_page = ""
"<!DOCTYPE html>\n"
"<body><h1>%s OAuth identify</h1>\n"
"<div style=\"background-color: red; color: white\">%s</div>\n"
"<form method=\"post\" action=\"https:/" "/%s/oauth/x-snac-login\">\n"
"<form method=\"post\" action=\"https:/" "/%s/%s\">\n"
"<p>Login: <input type=\"text\" name=\"login\"></p>\n"
"<p>Password: <input type=\"password\" name=\"passwd\"></p>\n"
"<input type=\"hidden\" name=\"redir\" value=\"%s\">\n"
@ -175,6 +175,8 @@ const char *login_page = ""
int oauth_get_handler(const xs_dict *req, const char *q_path,
char **body, int *b_size, char **ctype)
{
(void)b_size;
if (!xs_startswith(q_path, "/oauth/"))
return 0;
@ -185,7 +187,7 @@ int oauth_get_handler(const xs_dict *req, const char *q_path,
int status = 404;
xs_dict *msg = xs_dict_get(req, "q_vars");
xs *cmd = xs_replace(q_path, "/oauth", "");
xs *cmd = xs_replace_n(q_path, "/oauth", "", 1);
srv_debug(1, xs_fmt("oauth_get_handler %s", q_path));
@ -206,17 +208,28 @@ int oauth_get_handler(const xs_dict *req, const char *q_path,
if (xs_is_null(state))
state = "";
*body = xs_fmt(login_page, host, "", host, ruri, cid, state, USER_AGENT);
*body = xs_fmt(login_page, host, "", host, "oauth/x-snac-login",
ruri, cid, state, USER_AGENT);
*ctype = "text/html";
status = 200;
srv_debug(0, xs_fmt("oauth authorize: generating login page"));
srv_debug(1, xs_fmt("oauth authorize: generating login page"));
}
else
srv_debug(0, xs_fmt("oauth authorize: bad client_id %s", cid));
srv_debug(1, xs_fmt("oauth authorize: bad client_id %s", cid));
}
else
srv_debug(0, xs_fmt("oauth authorize: invalid or unset arguments"));
srv_debug(1, xs_fmt("oauth authorize: invalid or unset arguments"));
}
else
if (strcmp(cmd, "/x-snac-get-token") == 0) {
const char *host = xs_dict_get(srv_config, "host");
*body = xs_fmt(login_page, host, "", host, "oauth/x-snac-get-token",
"", "", "", USER_AGENT);
*ctype = "text/html";
status = 200;
}
return status;
@ -227,6 +240,9 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
const char *payload, int p_size,
char **body, int *b_size, char **ctype)
{
(void)p_size;
(void)b_size;
if (!xs_startswith(q_path, "/oauth/"))
return 0;
@ -245,7 +261,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
else
args = xs_dup(xs_dict_get(req, "p_vars"));
xs *cmd = xs_replace(q_path, "/oauth", "");
xs *cmd = xs_replace_n(q_path, "/oauth", "", 1);
srv_debug(1, xs_fmt("oauth_post_handler %s", q_path));
@ -259,7 +275,8 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
const char *host = xs_dict_get(srv_config, "host");
/* by default, generate another login form with an error */
*body = xs_fmt(login_page, host, "LOGIN INCORRECT", host, redir, cid, state, USER_AGENT);
*body = xs_fmt(login_page, host, "LOGIN INCORRECT", host, "oauth/x-snac-login",
redir, cid, state, USER_AGENT);
*ctype = "text/html";
status = 200;
@ -268,8 +285,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
if (user_open(&snac, login)) {
/* check the login + password */
if (check_password(login, passwd,
xs_dict_get(snac.config, "passwd"))) {
if (check_password(login, passwd, xs_dict_get(snac.config, "passwd"))) {
/* success! redirect to the desired uri */
xs *code = random_str();
@ -328,7 +344,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
const char *auhdr = xs_dict_get(req, "authorization");
if (!xs_is_null(auhdr) && xs_startswith(auhdr, "Basic ")) {
xs *s1 = xs_replace(auhdr, "Basic ", "");
xs *s1 = xs_replace_n(auhdr, "Basic ", "", 1);
int size;
xs *s2 = xs_base64_dec(s1, &size);
@ -373,7 +389,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
const char *uid = xs_dict_get(app, "uid");
srv_debug(0, xs_fmt("oauth token: "
srv_debug(1, xs_fmt("oauth token: "
"successful login for %s, new token %s", uid, tokid));
xs *token = xs_dict_new();
@ -387,7 +403,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
}
}
else {
srv_debug(0, xs_fmt("oauth token: invalid or unset arguments"));
srv_debug(1, xs_fmt("oauth token: invalid or unset arguments"));
status = 400;
}
}
@ -409,7 +425,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
}
else {
token_del(tokid);
srv_debug(0, xs_fmt("oauth revoke: revoked token %s", tokid));
srv_debug(1, xs_fmt("oauth revoke: revoked token %s", tokid));
status = 200;
/* also delete the app, as it serves no purpose from now on */
@ -417,10 +433,52 @@ int oauth_post_handler(const xs_dict *req, const char *q_path,
}
}
else {
srv_debug(0, xs_fmt("oauth revoke: invalid or unset arguments"));
srv_debug(1, xs_fmt("oauth revoke: invalid or unset arguments"));
status = 403;
}
}
if (strcmp(cmd, "/x-snac-get-token") == 0) {
const char *login = xs_dict_get(args, "login");
const char *passwd = xs_dict_get(args, "passwd");
const char *host = xs_dict_get(srv_config, "host");
/* by default, generate another login form with an error */
*body = xs_fmt(login_page, host, "LOGIN INCORRECT", host, "oauth/x-snac-get-token",
"", "", "", USER_AGENT);
*ctype = "text/html";
status = 200;
if (login && passwd) {
snac user;
if (user_open(&user, login)) {
/* check the login + password */
if (check_password(login, passwd, xs_dict_get(user.config, "passwd"))) {
/* success! create a new token */
xs *tokid = random_str();
srv_debug(1, xs_fmt("x-snac-new-token: "
"successful login for %s, new token %s", login, tokid));
xs *token = xs_dict_new();
token = xs_dict_append(token, "token", tokid);
token = xs_dict_append(token, "client_id", "snac-client");
token = xs_dict_append(token, "client_secret", "");
token = xs_dict_append(token, "uid", login);
token = xs_dict_append(token, "code", "");
token_add(tokid, token);
*ctype = "text/plain";
xs_free(*body);
*body = xs_dup(tokid);
}
user_free(&user);
}
}
}
return status;
}
@ -537,7 +595,6 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg)
xs *f = xs_val_new(XSTYPE_FALSE);
xs *t = xs_val_new(XSTYPE_TRUE);
xs *n = xs_val_new(XSTYPE_NULL);
xs *el = xs_list_new();
xs *idx = NULL;
xs *ixc = NULL;
@ -787,7 +844,7 @@ int process_auth_token(snac *snac, const xs_dict *req)
/* if there is an authorization field, try to validate it */
if (!xs_is_null(v = xs_dict_get(req, "authorization")) && xs_startswith(v, "Bearer ")) {
xs *tokid = xs_replace(v, "Bearer ", "");
xs *tokid = xs_replace_n(v, "Bearer ", "", 1);
xs *token = token_get(tokid);
if (token != NULL) {
@ -815,6 +872,8 @@ int process_auth_token(snac *snac, const xs_dict *req)
int mastoapi_get_handler(const xs_dict *req, const char *q_path,
char **body, int *b_size, char **ctype)
{
(void)b_size;
if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
return 0;
@ -826,7 +885,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
int status = 404;
xs_dict *args = xs_dict_get(req, "q_vars");
xs *cmd = xs_replace(q_path, "/api", "");
xs *cmd = xs_replace_n(q_path, "/api", "", 1);
snac snac1 = {0};
int logged_in = process_auth_token(&snac1, req);
@ -1036,11 +1095,14 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
}
else
if (strcmp(cmd, "/v1/timelines/public") == 0) {
/* the public timeline (public timelines for all users) */
/* the instance public timeline (public timelines for all users) */
/* this is an ugly kludge: first users in the list get all the fame */
/* NOTE: this api call needs no authorization; but,
I need a logged-in user in mastoapi_status() for
is_msg_public() and the liked/boosted flags,
so it will silently fail for pure public access */
const char *limit_s = xs_dict_get(args, "limit");
const char *limit_s = xs_dict_get(args, "limit");
int limit = 0;
int cnt = 0;
@ -1050,44 +1112,28 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
if (limit == 0)
limit = 20;
xs *out = xs_list_new();
xs *users = user_list();
xs_list *p = users;
xs_str *uid;
xs *timeline = timeline_instance_list(0, limit);
xs *out = xs_list_new();
xs_list *p = timeline;
xs_str *md5;
while (xs_list_iter(&p, &uid) && cnt < limit) {
snac user;
while (logged_in && xs_list_iter(&p, &md5) && cnt < limit) {
xs *msg = NULL;
if (user_open(&user, uid)) {
xs *timeline = timeline_simple_list(&user, "public", 0, 4);
xs_list *p2 = timeline;
xs_str *v;
/* get the entry */
if (!valid_status(object_get_by_md5(md5, &msg)))
continue;
while (xs_list_iter(&p2, &v) && cnt < limit) {
xs *msg = NULL;
/* discard non-Notes */
if (strcmp(xs_dict_get(msg, "type"), "Note") != 0)
continue;
/* get the entry */
if (!valid_status(timeline_get_by_md5(&user, v, &msg)))
continue;
/* convert the Note into a Mastodon status */
xs *st = mastoapi_status(&snac1, msg);
/* discard non-Notes */
if (strcmp(xs_dict_get(msg, "type"), "Note") != 0)
continue;
/* discard entries not by this user */
if (!xs_startswith(xs_dict_get(msg, "id"), user.actor))
continue;
/* convert the Note into a Mastodon status */
xs *st = mastoapi_status(&user, msg);
if (st != NULL) {
out = xs_list_append(out, st);
cnt++;
}
}
user_free(&user);
if (st != NULL) {
out = xs_list_append(out, st);
cnt++;
}
}
@ -1121,7 +1167,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
xs *actor = NULL;
xs *entry = NULL;
if (!valid_status(object_get(xs_dict_get(noti, "actor"), &actor)))
if (!valid_status(actor_get(&snac1, xs_dict_get(noti, "actor"), &actor)))
continue;
if (objid != NULL && !valid_status(object_get(objid, &entry)))
@ -1287,25 +1333,6 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
cfg = xs_dict_append(cfg, "statuses", d11);
}
{
xs *d11 = xs_dict_new();
xs *mt = xs_list_new();
mt = xs_list_append(mt, "image/jpeg");
mt = xs_list_append(mt, "image/png");
mt = xs_list_append(mt, "image/gif");
d11 = xs_dict_append(d11, "supported_mime_types", mt);
d11 = xs_dict_append(d11, "image_size_limit", z);
d11 = xs_dict_append(d11, "image_matrix_limit", z);
d11 = xs_dict_append(d11, "video_size_limit", z);
d11 = xs_dict_append(d11, "video_matrix_limit", z);
d11 = xs_dict_append(d11, "video_frame_rate_limit", z);
cfg = xs_dict_append(cfg, "media_attachments", d11);
}
ins = xs_dict_append(ins, "configuration", cfg);
*body = xs_json_dumps_pp(ins, 4);
@ -1326,7 +1353,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
/* skip the 'fake' part of the id */
id = MID_TO_MD5(id);
if (valid_status(timeline_get_by_md5(&snac1, id, &msg))) {
if (valid_status(object_get_by_md5(id, &msg))) {
if (op == NULL) {
if (!is_muted(&snac1, xs_dict_get(msg, "attributedTo"))) {
/* return the status itself */
@ -1487,6 +1514,9 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
const char *payload, int p_size,
char **body, int *b_size, char **ctype)
{
(void)p_size;
(void)b_size;
if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
return 0;
@ -1513,7 +1543,7 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
printf("%s\n", j);
}*/
xs *cmd = xs_replace(q_path, "/api", "");
xs *cmd = xs_replace_n(q_path, "/api", "", 1);
snac snac = {0};
int logged_in = process_auth_token(&snac, req);
@ -1562,7 +1592,7 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
app_add(cid, app);
srv_debug(0, xs_fmt("mastoapi apps: new app %s", cid));
srv_debug(1, xs_fmt("mastoapi apps: new app %s", cid));
}
}
else
@ -1582,6 +1612,9 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
if (xs_is_null(media_ids))
media_ids = xs_dict_get(args, "media_ids[]");
if (xs_is_null(visibility))
visibility = "public";
xs *attach_list = xs_list_new();
xs *irt = NULL;
@ -1683,7 +1716,11 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
}
else
if (strcmp(op, "unfavourite") == 0) {
/* snac does not support Undo+Like */
/* partial support: as the original Like message
is not stored anywhere here, it's not possible
to send an Undo + Like; the only thing done here
is to delete the actor from the list of likes */
object_unadmire(id, snac.actor, 1);
}
else
if (strcmp(op, "reblog") == 0) {
@ -1698,7 +1735,8 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
}
else
if (strcmp(op, "unreblog") == 0) {
/* snac does not support Undo+Announce */
/* partial support: see comment in 'unfavourite' */
object_unadmire(id, snac.actor, 0);
}
else
if (strcmp(op, "bookmark") == 0) {
@ -1903,6 +1941,9 @@ int mastoapi_put_handler(const xs_dict *req, const char *q_path,
const char *payload, int p_size,
char **body, int *b_size, char **ctype)
{
(void)p_size;
(void)b_size;
if (!xs_startswith(q_path, "/api/v1/") && !xs_startswith(q_path, "/api/v2/"))
return 0;
@ -1924,7 +1965,7 @@ int mastoapi_put_handler(const xs_dict *req, const char *q_path,
if (args == NULL)
return 400;
xs *cmd = xs_replace(q_path, "/api", "");
xs *cmd = xs_replace_n(q_path, "/api", "", 1);
snac snac = {0};
int logged_in = process_auth_token(&snac, req);

31
snac.h
View file

@ -1,7 +1,7 @@
/* snac - A simple, minimalistic ActivityPub instance */
/* copyright (c) 2022 - 2023 grunfink / MIT license */
#define VERSION "2.29"
#define VERSION "2.30"
#define USER_AGENT "snac/" VERSION
@ -83,6 +83,7 @@ int object_del_if_unref(const char *id);
double object_ctime_by_md5(const char *md5);
double object_ctime(const char *id);
int object_admire(const char *id, const char *actor, int like);
int object_unadmire(const char *id, const char *actor, int like);
int object_likes_len(const char *id);
int object_announces_len(const char *id);
@ -105,14 +106,14 @@ int timeline_touch(snac *snac);
int timeline_here(snac *snac, const char *md5);
int timeline_get_by_md5(snac *snac, const char *md5, xs_dict **msg);
int timeline_del(snac *snac, char *id);
d_char *timeline_simple_list(snac *snac, const char *idx_name, int skip, int show);
d_char *timeline_list(snac *snac, const char *idx_name, int skip, int show);
xs_list *timeline_simple_list(snac *snac, const char *idx_name, int skip, int show);
xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show);
int timeline_add(snac *snac, char *id, char *o_msg);
void timeline_admire(snac *snac, char *id, char *admirer, int like);
xs_list *timeline_top_level(snac *snac, xs_list *list);
d_char *local_list(snac *snac, int max);
xs_list *local_list(snac *snac, int max);
xs_list *timeline_instance_list(int skip, int show);
int following_add(snac *snac, const char *actor, const xs_dict *msg);
int following_del(snac *snac, const char *actor);
@ -127,8 +128,8 @@ int is_muted(snac *snac, const char *actor);
void hide(snac *snac, const char *id);
int is_hidden(snac *snac, const char *id);
int actor_add(snac *snac, const char *actor, d_char *msg);
int actor_get(snac *snac, const char *actor, d_char **data);
int actor_add(const char *actor, xs_dict *msg);
int actor_get(snac *snac, const char *actor, xs_dict **data);
int static_get(snac *snac, const char *id, d_char **data, int *size);
void static_put(snac *snac, const char *id, const char *data, int size);
@ -154,7 +155,7 @@ void inbox_add(const char *inbox);
void inbox_add_by_actor(const xs_dict *actor);
xs_list *inbox_list(void);
void enqueue_input(snac *snac, xs_dict *msg, xs_dict *req, int retries);
void enqueue_input(snac *snac, const xs_dict *msg, const xs_dict *req, int retries);
void enqueue_output_raw(const char *keyid, const char *seckey,
xs_dict *msg, xs_str *inbox, int retries);
void enqueue_output(snac *snac, xs_dict *msg, xs_str *inbox, int retries);
@ -186,7 +187,7 @@ int check_signature(snac *snac, xs_dict *req, xs_str **err);
void httpd(void);
int webfinger_request(const char *qs, char **actor, char **user);
int webfinger_get_handler(d_char *req, char *q_path,
int webfinger_get_handler(xs_dict *req, char *q_path,
char **body, int *b_size, char **ctype);
const char *default_avatar_base64(void);
@ -202,6 +203,8 @@ d_char *msg_undo(snac *snac, char *object);
d_char *msg_delete(snac *snac, char *id);
d_char *msg_actor(snac *snac);
xs_dict *msg_update(snac *snac, xs_dict *object);
xs_dict *msg_ping(snac *user, const char *rcpt);
xs_dict *msg_pong(snac *user, const char *rcpt, const char *object);
int activitypub_request(snac *snac, const char *url, xs_dict **data);
int actor_request(snac *snac, const char *actor, xs_dict **data);
@ -219,17 +222,19 @@ int process_user_queue(snac *snac);
void process_queue_item(xs_dict *q_item);
int process_queue(void);
int activitypub_get_handler(d_char *req, char *q_path,
int activitypub_get_handler(const xs_dict *req, const char *q_path,
char **body, int *b_size, char **ctype);
int activitypub_post_handler(d_char *req, char *q_path,
int activitypub_post_handler(const xs_dict *req, const char *q_path,
char *payload, int p_size,
char **body, int *b_size, char **ctype);
d_char *not_really_markdown(const char *content);
d_char *sanitize(const char *str);
int html_get_handler(d_char *req, char *q_path, char **body, int *b_size, char **ctype);
int html_post_handler(d_char *req, char *q_path, d_char *payload, int p_size,
int html_get_handler(const xs_dict *req, const char *q_path,
char **body, int *b_size, char **ctype);
int html_post_handler(const xs_dict *req, const char *q_path,
char *payload, int p_size,
char **body, int *b_size, char **ctype);
int snac_init(const char *_basedir);

View file

@ -21,7 +21,7 @@ int webfinger_request(const char *qs, char **actor, char **user)
if (xs_startswith(qs, "https:/" "/")) {
/* actor query: pick the host */
xs *s = xs_replace(qs, "https:/" "/", "");
xs *s = xs_replace_n(qs, "https:/" "/", "", 1);
l = xs_split_n(s, "/", 1);
@ -74,7 +74,7 @@ int webfinger_request(const char *qs, char **actor, char **user)
char *subject = xs_dict_get(obj, "subject");
if (subject)
*user = xs_replace(subject, "acct:", "");
*user = xs_replace_n(subject, "acct:", "", 1);
}
if (actor != NULL) {
@ -105,6 +105,8 @@ int webfinger_get_handler(d_char *req, char *q_path,
{
int status;
(void)b_size;
if (strcmp(q_path, "/.well-known/webfinger") != 0)
return 0;
@ -137,7 +139,7 @@ int webfinger_get_handler(d_char *req, char *q_path,
else
if (xs_startswith(resource, "acct:")) {
/* it's an account name */
xs *an = xs_replace(resource, "acct:", "");
xs *an = xs_replace_n(resource, "acct:", "", 1);
xs *l = NULL;
/* strip a possible leading @ */

12
xs.h
View file

@ -65,8 +65,10 @@ xs_str *xs_str_new(const char *str);
xs_str *xs_str_wrap_i(const char *prefix, xs_str *str, const char *suffix);
#define xs_str_prepend_i(str, prefix) xs_str_wrap_i(prefix, str, NULL)
#define xs_str_cat(str, suffix) xs_str_wrap_i(NULL, str, suffix)
xs_str *xs_replace_i(xs_str *str, const char *sfrom, const char *sto);
#define xs_replace(str, sfrom, sto) xs_replace_i(xs_dup(str), sfrom, sto)
xs_str *xs_replace_in(xs_str *str, const char *sfrom, const char *sto, int times);
#define xs_replace_i(str, sfrom, sto) xs_replace_in(str, sfrom, sto, XS_ALL)
#define xs_replace(str, sfrom, sto) xs_replace_in(xs_dup(str), sfrom, sto, XS_ALL)
#define xs_replace_n(str, sfrom, sto, times) xs_replace_in(xs_dup(str), sfrom, sto, times)
xs_str *xs_fmt(const char *fmt, ...);
int xs_str_in(const char *haystack, const char *needle);
int _xs_startsorends(const char *str, const char *xfix, int ends);
@ -416,7 +418,7 @@ xs_str *xs_str_wrap_i(const char *prefix, xs_str *str, const char *suffix)
}
xs_str *xs_replace_i(xs_str *str, const char *sfrom, const char *sto)
xs_str *xs_replace_in(xs_str *str, const char *sfrom, const char *sto, int times)
/* replaces inline all sfrom with sto */
{
XS_ASSERT_TYPE(str, XSTYPE_STRING);
@ -426,7 +428,7 @@ xs_str *xs_replace_i(xs_str *str, const char *sfrom, const char *sto)
char *ss;
int offset = 0;
while ((ss = strstr(str + offset, sfrom)) != NULL) {
while (times > 0 && (ss = strstr(str + offset, sfrom)) != NULL) {
int n_offset = ss - str;
str = xs_collapse(str, n_offset, sfsz);
@ -434,6 +436,8 @@ xs_str *xs_replace_i(xs_str *str, const char *sfrom, const char *sto)
memcpy(str + n_offset, sto, stsz);
offset = n_offset + stsz;
times--;
}
return str;

View file

@ -7,13 +7,20 @@
xs_str *xs_hex_enc(const xs_val *data, int size);
xs_val *xs_hex_dec(const xs_str *hex, int *size);
int xs_is_hex(const char *str);
xs_str *xs_base32_enc(const xs_val *data, int sz);
xs_str *xs_base32hex_enc(const xs_val *data, int sz);
xs_val *xs_base32_dec(const xs_str *data, int *size);
xs_val *xs_base32hex_dec(const xs_str *data, int *size);
xs_str *xs_base64_enc(const xs_val *data, int sz);
xs_val *xs_base64_dec(const xs_str *data, int *size);
int xs_is_base64(const char *str);
xs_str *xs_utf8_enc(xs_str *str, unsigned int cpoint);
#ifdef XS_IMPLEMENTATION
/** hex **/
xs_str *xs_hex_enc(const xs_val *data, int size)
/* returns an hexdump of data */
{
@ -78,16 +85,178 @@ int xs_is_hex(const char *str)
}
xs_str *xs_base64_enc(const xs_val *data, int sz)
/* encodes data to base64 */
/** base32 */
static char *xs_b32_tbl = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"234567=";
static char *xs_b32hex_tbl = "0123456789"
"ABCDEFGHIJKLMNOPQRSTUV=";
/*
00000|00011|11111|12222|22223|33333|33444|44444
*/
xs_str *xs_base32_enc_tbl(const xs_val *data, int sz, const char *b32_tbl)
/* encodes data to base32 using a table */
{
xs_str *s = xs_str_new(NULL);
unsigned char *p;
int n;
p = (unsigned char *)data;
for (n = 0; n < sz; n += 5) {
int l = sz - n;
char enc[9] = "========";
enc[0] = b32_tbl[(p[n] >> 3) & 0x1f];
if (l > 1) {
enc[1] = b32_tbl[(p[n] << 2 | p[n + 1] >> 6) & 0x1f];
enc[2] = b32_tbl[(p[n + 1] >> 1) & 0x1f];
if (l > 2) {
enc[3] = b32_tbl[(p[n + 1] << 4 | p[n + 2] >> 4) & 0x1f];
if (l > 3) {
enc[4] = b32_tbl[(p[n + 2] << 1 | p[n + 3] >> 7) & 0x1f];
enc[5] = b32_tbl[(p[n + 3] >> 2) & 0x1f];
if (l > 4) {
enc[6] = b32_tbl[(p[n + 3] << 3 | p[n + 4] >> 5) & 0x1f];
enc[7] = b32_tbl[(p[n + 4]) & 0x1f];
}
else
enc[6] = b32_tbl[(p[n + 3] << 3) & 0x1f];
}
else
enc[4] = b32_tbl[(p[n + 2] << 1) & 0x1f];
}
else
enc[3] = b32_tbl[(p[n + 1] << 4) & 0x1f];
}
else
enc[1] = b32_tbl[(p[n] << 2) & 0x1f];
s = xs_str_cat(s, enc);
}
return s;
}
xs_str *xs_base32_enc(const xs_val *data, int sz)
/* encodes data to base32 */
{
return xs_base32_enc_tbl(data, sz, xs_b32_tbl);
}
xs_str *xs_base32hex_enc(const xs_val *data, int sz)
/* encodes data to base32 with HEX alphabet (RFC4648) */
{
return xs_base32_enc_tbl(data, sz, xs_b32hex_tbl);
}
xs_val *xs_base32_dec_tbl(const xs_str *data, int *size, const char *b32_tbl)
/* decodes data from base32 using a table */
{
xs_val *s = NULL;
int sz = 0;
char *p;
p = (char *)data;
/* size of data must be a multiple of 8 */
if (strlen(p) % 8)
return NULL;
for (p = (char *)data; *p; p += 8) {
int cs[8];
int n;
unsigned char tmp[5];
for (n = 0; n < 8; n++) {
char *ss = strchr(b32_tbl, p[n]);
if (ss == NULL) {
/* not a base32 char */
return xs_free(s);
}
cs[n] = ss - b32_tbl;
}
n = 0;
/* #0 byte */
tmp[n++] = cs[0] << 3 | cs[1] >> 2;
if (cs[2] != 32) {
/* #1 byte */
tmp[n++] = (cs[1] & 0x3) << 6 | cs[2] << 1 | (cs[3] & 0x10) >> 4;
if (cs[4] != 32) {
/* #2 byte */
tmp[n++] = (cs[3] & 0xf) << 4 | cs[4] >> 1;
if (cs[5] != 32) {
/* #3 byte */
tmp[n++] = (cs[4] & 0x1) << 7 | cs[5] << 2 | cs[6] >> 3;
if (cs[7] != 32) {
/* #4 byte */
tmp[n++] = (cs[6] & 0x7) << 5 | cs[7];
}
}
}
}
/* must be done manually because data can be pure binary */
s = xs_realloc(s, _xs_blk_size(sz + n));
memcpy(s + sz, tmp, n);
sz += n;
}
/* asciiz it to use it as a string */
s = xs_realloc(s, _xs_blk_size(sz + 1));
s[sz] = '\0';
*size = sz;
return s;
}
xs_val *xs_base32_dec(const xs_str *data, int *size)
/* decodes data from base32 */
{
return xs_base32_dec_tbl(data, size, xs_b32_tbl);
}
xs_val *xs_base32hex_dec(const xs_str *data, int *size)
/* decodes data from base32 with HEX alphabet (RFC4648) */
{
return xs_base32_dec_tbl(data, size, xs_b32hex_tbl);
}
/** base64 */
static char *xs_b64_tbl = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789+/=";
xs_str *xs_base64_enc_tbl(const xs_val *data, int sz, const char *b64_tbl)
/* encodes data to base64 using a table */
{
xs_str *s;
unsigned char *p;
char *i;
int bsz, n;
static char *b64_tbl = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789+/";
bsz = ((sz + 3 - 1) / 3) * 4;
i = s = xs_realloc(NULL, _xs_blk_size(bsz + 1));
@ -123,15 +292,19 @@ xs_str *xs_base64_enc(const xs_val *data, int sz)
}
xs_val *xs_base64_dec(const xs_str *data, int *size)
/* decodes data from base64 */
xs_str *xs_base64_enc(const xs_val *data, int sz)
/* encodes data to base64 */
{
return xs_base64_enc_tbl(data, sz, xs_b64_tbl);
}
xs_val *xs_base64_dec_tbl(const xs_str *data, int *size, const char *b64_tbl)
/* decodes data from base64 using a table */
{
xs_val *s = NULL;
int sz = 0;
char *p;
static char *b64_tbl = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789+/=";
p = (char *)data;
@ -184,6 +357,34 @@ xs_val *xs_base64_dec(const xs_str *data, int *size)
}
xs_val *xs_base64_dec(const xs_str *data, int *size)
/* decodes data from base64 */
{
return xs_base64_dec_tbl(data, size, xs_b64_tbl);
}
int xs_is_base64_tbl(const char *str, const char *b64_tbl)
/* returns 1 if str is a base64 string, with table */
{
while (*str) {
if (strchr(b64_tbl, *str++) == NULL)
return 0;
}
return 1;
}
int xs_is_base64(const char *str)
/* returns 1 if str is a base64 string */
{
return xs_is_base64_tbl(str, xs_b64_tbl);
}
/** utf-8 **/
xs_str *xs_utf8_enc(xs_str *str, unsigned int cpoint)
/* encodes an Unicode codepoint to utf8 */
{

View file

@ -1 +1 @@
/* a885c7cc4c8e6384ae23125ed05f434471ccc6fb */
/* dfdd729248d7169b80cb6a7462fe6c0ba6efeb16 */