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