2.80 xs_webmention.h
  1/* copyright (c) 2025 grunfink et al. / MIT license */
  2
  3#ifndef _XS_WEBMENTION_H
  4
  5#define _XS_WEBMENTION_H
  6
  7int xs_webmention_send(const char *source, const char *target, const char *user_agent);
  8int xs_webmention_hook(const char *source, const char *target, const char *user_agent);
  9
 10
 11#ifdef XS_IMPLEMENTATION
 12
 13int xs_webmention_send(const char *source, const char *target, const char *user_agent)
 14/* sends a Webmention to target.
 15   Returns: < 0, error; 0, no Webmention endpoint; > 0, Webmention sent */
 16{
 17    int status = 0;
 18    xs *endpoint = NULL;
 19
 20    xs *ua = xs_fmt("%s (Webmention)", user_agent ? user_agent : "xs_webmention");
 21    xs *headers = xs_dict_new();
 22    headers = xs_dict_set(headers, "accept", "text/html");
 23    headers = xs_dict_set(headers, "user-agent", ua);
 24
 25    xs *h_req = NULL;
 26    int p_size = 0;
 27
 28    /* try first a HEAD, to see if there is a Webmention Link header */
 29    h_req = xs_http_request("HEAD", target, headers, NULL, 0, &status, NULL, &p_size, 0);
 30
 31    /* return immediate failures */
 32    if (status < 200 || status > 299)
 33        return -1;
 34
 35    const char *link = xs_dict_get(h_req, "link");
 36
 37    if (xs_is_string(link) && xs_regex_match(link, "rel *= *(\"|')?webmention")) {
 38        /* endpoint is between < and > */
 39        xs *r = xs_regex_select_n(link, "<[^>]+>", 1);
 40
 41        if (xs_list_len(r) == 1) {
 42            endpoint = xs_dup(xs_list_get(r, 0));
 43            endpoint = xs_strip_chars_i(endpoint, "<>");
 44        }
 45    }
 46
 47    if (endpoint == NULL) {
 48        /* no Link header; get the content */
 49        xs *g_req = NULL;
 50        xs *payload = NULL;
 51
 52        g_req = xs_http_request("GET", target, headers, NULL, 0, &status, &payload, &p_size, 0);
 53
 54        if (status < 200 || status > 299)
 55            return -1;
 56
 57        const char *ctype = xs_dict_get(g_req, "content-type");
 58
 59        /* not HTML? no point in looking inside */
 60        if (!xs_is_string(ctype) || xs_str_in(ctype, "text/html") == -1)
 61            return -2;
 62
 63        if (!xs_is_string(payload))
 64            return -3;
 65
 66        xs *links = xs_regex_select(payload, "<(a +|link +)[^>]+>");
 67        const char *link;
 68
 69        xs_list_foreach(links, link) {
 70            if (xs_regex_match(link, "rel *= *(\"|')?webmention")) {
 71                /* found; extract the href */
 72                xs *r = xs_regex_select_n(link, "href *= *(\"|')?[^\"]+(\"|')", 1);
 73
 74                if (xs_list_len(r) == 1) {
 75                    xs *l = xs_split_n(xs_list_get(r, 0), "=", 1);
 76
 77                    if (xs_list_len(l) == 2) {
 78                        endpoint = xs_dup(xs_list_get(l, 1));
 79                        endpoint = xs_strip_chars_i(endpoint, " \"'");
 80
 81                        break;
 82                    }
 83                }
 84            }
 85        }
 86    }
 87
 88    /* is it a relative endpoint? */
 89    if (xs_is_string(endpoint)) {
 90        if (!xs_startswith(endpoint, "https://") && !xs_startswith(endpoint, "http://")) {
 91            xs *l = xs_split(target, "/");
 92
 93            if (xs_list_len(l) < 3)
 94                endpoint = xs_free(endpoint);
 95            else {
 96                xs *s = xs_fmt("%s/" "/%s", xs_list_get(l, 0), xs_list_get(l, 2));
 97                endpoint = xs_str_wrap_i(s, endpoint, NULL);
 98            }
 99        }
100    }
101
102    if (xs_is_string(endpoint)) {
103        /* got it! */
104        headers = xs_dict_set(headers, "content-type", "application/x-www-form-urlencoded");
105
106        xs *body = xs_fmt("source=%s&target=%s", source, target);
107
108        xs *rsp = xs_http_request("POST", endpoint, headers, body, strlen(body), &status, NULL, 0, 0);
109
110        if (status < 200 || status > 299)
111            status = -4;
112        else
113            status = 1;
114    }
115    else
116        status = 0;
117
118    return status;
119}
120
121
122int xs_webmention_hook(const char *source, const char *target, const char *user_agent)
123/* a Webmention has been received for a target that is ours; check if the source
124   really contains a link to our target */
125{
126    int status = 0;
127
128    xs *ua = xs_fmt("%s (Webmention)", user_agent ? user_agent : "xs_webmention");
129    xs *headers = xs_dict_new();
130    headers = xs_dict_set(headers, "accept", "text/html");
131    headers = xs_dict_set(headers, "user-agent", ua);
132
133    xs *g_req = NULL;
134    xs *payload = NULL;
135    int p_size = 0;
136
137    g_req = xs_http_request("GET", source, headers, NULL, 0, &status, &payload, &p_size, 0);
138
139    if (status < 200 || status > 299)
140        return -1;
141
142    if (!xs_is_string(payload))
143        return -2;
144
145    /* note: a "rogue" webmention can include a link to our target in commented-out HTML code */
146
147    xs *links = xs_regex_select(payload, "<(a +|link +)[^>]+>");
148    const char *link;
149
150    status = 0;
151    xs_list_foreach(links, link) {
152        /* if the link contains our target, it's valid */
153        if (xs_str_in(link, target) != -1) {
154            status = 1;
155            break;
156        }
157    }
158
159    return status;
160}
161
162
163#endif /* XS_IMPLEMENTATION */
164
165#endif /* _XS_WEBMENTION_H */