webtentacle.lisp 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. ;;; Copyright 2023, Jaidyn Ann <jadedctrl@posteo.at>
  2. ;;;
  3. ;;; This program is free software: you can redistribute it and/or
  4. ;;; modify it under the terms of the GNU General Public License as
  5. ;;; published by the Free Software Foundation, either version 3 of
  6. ;;; the License, or (at your option) any later version.
  7. ;;;
  8. ;;; This program is distributed in the hope that it will be useful,
  9. ;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. ;;; GNU General Public License for more details.
  12. ;;;
  13. ;;; You should have received a copy of the GNU General Public License
  14. ;;; along with this program. If not, see <https://www.gnu.org/licenses/>.
  15. (defpackage #:webtentacle
  16. (:use #:cl)
  17. (:export :server :start-server :clack-response))
  18. (in-package #:webtentacle)
  19. (defun resource-json (&key subject aliases properties links)
  20. "Given the RESOURCE’s information, return the applicable Webfinger JSON.
  21. Details of the values of RESOURCE, ALIASES, PROPERTIES, and LINKS can be found
  22. in the docstring of SERVER."
  23. (let ((yason:*symbol-key-encoder* #'yason:encode-symbol-as-lowercase)
  24. (yason:*symbol-encoder* #'yason:encode-symbol-as-lowercase))
  25. (yason:with-output-to-string* ()
  26. (yason:with-object ()
  27. (when subject
  28. (yason:encode-object-element "subject" subject))
  29. (when (and aliases (listp aliases))
  30. (yason:encode-object-element "aliases" aliases))
  31. (when (and properties (listp properties))
  32. (yason:encode-object-element
  33. "properties"
  34. (alexandria:plist-hash-table properties)))
  35. (when (and links (listp links))
  36. (yason:encode-object-element
  37. "links"
  38. ;; Each link needs to be a hash-table (so it's encoded as a JSON object.
  39. (mapcar
  40. (lambda (link)
  41. ;; Each link’s properties/titles need to be hash-tables, likewise.
  42. (let ((properties (getf link 'properties))
  43. (titles (getf link 'titles)))
  44. (when (and properties (not (hash-table-p properties)))
  45. (setf (getf link 'properties) (alexandria:plist-hash-table properties)))
  46. (when (and titles (not (hash-table-p titles)))
  47. (setf (getf link 'titles) (alexandria:plist-hash-table titles))))
  48. (alexandria:plist-hash-table link))
  49. links)))))))
  50. (defun fake-info-func (resource)
  51. "A testing function. This is a RESOURCE-INFO-FUNC function that outputs garbage."
  52. (let ((profile (str:concat "https://example.example/users/" resource)))
  53. (list
  54. :subject resource
  55. :aliases (list profile "https://example.example/users/old-user")
  56. :links
  57. `((href ,profile
  58. rel "http://webfinger.net/rel/profile-page"
  59. type "text/html"
  60. properties (:apple 3 :bear 4))
  61. (href ,profile
  62. rel "self"
  63. type "application/activity+json")))))
  64. (defun filter-link-rels (rels link-plists)
  65. "Given a list of link property-lists, filter out links whose rel properties
  66. aren’t a member of the RELS list.
  67. If RELS is nil, nothing is filtered out.
  68. If RELS is a list of strings, only links with rel properties matching a member
  69. in RELS will remain."
  70. (if rels
  71. (remove-if-not
  72. (lambda (plist)
  73. (member (getf plist 'rel) rels :test #'equal))
  74. link-plists)
  75. link-plists))
  76. (defun filter-resource-info-rels (rels resource-info)
  77. "Filter the :LINKS property-list’s properties from a RESOURCE-INFO property-list,
  78. by their relations.
  79. If RELS is nil, nothing is filtered out.
  80. If RELS is a list of strings, only links with rel properties matching a member
  81. in RELS will remain."
  82. (setf (getf resource-info :links) (filter-link-rels rels (getf resource-info :links)))
  83. resource-info)
  84. (defun clack-response (resource-info-func resource &rest rels)
  85. "Given a RESOURCE-INFO-FUNC (as per the specification of SERVER’s docstring), and
  86. the RESOURCE and RELS parameters from a Webfinger HTTP request, return the
  87. response JSON in Clack’s format.
  88. This can be used if you don’t want to wrap your server with SERVER, and would
  89. rather handle the Webfinger path yourself."
  90. (list
  91. 200
  92. '(:content-type "text/plain")
  93. (list
  94. (format
  95. nil "~A"
  96. (handler-case
  97. (cond ((or (not resource) (str:emptyp resource))
  98. "\"No resource specified\"")
  99. ;; If not a URI (or even an acct URI without the “acct:”)
  100. ((and (not (str:containsp ":" resource))
  101. (not (str:containsp "@" resource)))
  102. "\"Resource not a URI\"")
  103. ('t
  104. (or (apply #'resource-json
  105. (filter-resource-info-rels
  106. rels
  107. (funcall resource-info-func resource)))
  108. "\"Couldn't find resource\"")))
  109. (error (any-error)
  110. (format nil "\"Server error: ~A\"" any-error)))))))
  111. (defun server (env resource-info-func &optional (clack-app nil))
  112. "Start handling Webfinger requests, wrapping around the given CLACK-APP body
  113. function.
  114. RESOURCE-INFO-FUNC should be a function that will return resource information to
  115. be served by Webfinger.
  116. RESOURCE-INFO-FUNC should take one parameter, the resource string.
  117. It should return a property-list with some of the following properties:
  118. * :SUBJECT
  119. * :ALIASES
  120. * :PROPERTIES
  121. * :LINKS
  122. You need at minimum :SUBJECT, all else is optional.
  123. :ALIASES is a simple list of URLs.
  124. :PROPERTIES is a simple property-list of whatever you want.
  125. :LINKS is a list of property-lists, each with some of (or all) of the keys:
  126. * rel
  127. * types
  128. * href
  129. * titles
  130. * properties
  131. … all of which are strings, except for the plists “titles” & “properties.”
  132. “properties” should be a property-list containing whatever you want.
  133. “titles” should contain a property for each language-code, with its
  134. value being the corresponding title; for example,
  135. '(en “Birds & Planes”
  136. eo “Birdoj k Aviadiloj”
  137. es “No habla español :-(”)
  138. "
  139. (let* ((uri (quri:uri (getf env :request-uri)))
  140. (params (quri:uri-query-params uri)))
  141. (if (string= (quri:uri-path uri) "/.well-known/webfinger")
  142. ;; We only want to handle the *exact* webfinger path
  143. (apply #'clack-response
  144. (append (list resource-info-func
  145. (cdr (assoc "resource" params :test #'string=)))
  146. ;; We want all “rel” parameters, not just the first one
  147. (mapcar
  148. #'cdr
  149. (remove-if-not
  150. (lambda (pair)
  151. (string= (car pair) "rel"))
  152. params))))
  153. ;; At any other path, give control back over to the user’s server
  154. (or (and clack-app (funcall clack-app env))
  155. '(512 (:content-type "text/plain") ("HECK"))))))
  156. (defun start-server (resource-info-func)
  157. "Run a Webfinger HTTP server, given a RESOURCE-INFO-FUNC (see SERVER’s docstring).
  158. This is useful if you want to delegate Webfinger-handling to this library with a
  159. reverse-proxy.
  160. It is also useful for debugging this library."
  161. (clack:clackup
  162. (lambda (env)
  163. (funcall #'server env resource-info-func))))