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