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