1
0

xs_webmention.h 4.9 KB

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