2.94
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 */