kaptchapelo.lisp 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  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 Affero General Public License
  5. ;;; as 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 Affero General Public License for more details.
  12. ;;;
  13. ;;; You should have received a copy of the GNU Affero General Public License
  14. ;;; along with this program. If not, see <https://www.gnu.org/licenses/>.
  15. (defpackage #:kaptchapelo
  16. (:use #:cl)
  17. (:export :start-server))
  18. (in-package #:kaptchapelo)
  19. (defun random-string ()
  20. "Create a random string consisting of at least 19 characters: Random numbers."
  21. (apply #'str:concat
  22. (mapcar (lambda (a) (format nil "~A" a))
  23. (loop for i from 0 upto 19 collect (random 16)))))
  24. (defun byte-array-to-hex-string (simple-array)
  25. "Given an array of bytes (integers), return an equivalent string in hex."
  26. (string-downcase
  27. (reduce #'str:concat
  28. (loop for byte across simple-array
  29. collect (format nil "~2,'0X" byte)))))
  30. (defun random-file (directory &key (file-ext ""))
  31. "Select a random file from the given DIRECTORY of a specific FILE-EXTension."
  32. (alexandria:random-elt
  33. (directory (str:concat (format nil "~A" directory)
  34. "/*"
  35. (if (not (str:emptyp file-ext))
  36. (str:concat "." file-ext)
  37. "")))))
  38. (defun new-captcha-json (captcha-image-uri captcha-text-file)
  39. "Create a Kocaptcha-compatibile captcha challenge in JSON-format."
  40. (yason:with-output-to-string* ()
  41. (yason:encode-plist
  42. (list "md5" (byte-array-to-hex-string captcha-text-file)
  43. "url" captcha-image-uri
  44. ;; I don’t know what Kocaptcha’s token does! :P
  45. "token" (random-string)))))
  46. (defun new-captcha-response (captcha-dir)
  47. "Create a Clack HTTP response with a new captcha."
  48. (let* ((captcha-txt-file (random-file captcha-dir :file-ext "txt"))
  49. (captcha-img-file (str:concat (pathname-name captcha-txt-file) ".png"))
  50. (captcha-md5-str (byte-array-to-hex-string
  51. (md5:md5sum-file captcha-txt-file))))
  52. (list 201 '(:content-type "application/json")
  53. (list (new-captcha-json (str:concat "/captcha/" captcha-img-file)
  54. captcha-md5-str)))))
  55. (defun image-response (request-uri captcha-dir)
  56. "Given a /captcha/… REQUEST-URI and the CAPTCHA-DIR where images can be found,
  57. make a Clack HTTP response that serves the appropriate image."
  58. (let ((image-path (str:replace-first "/captcha/" (format nil "~A" captcha-dir) request-uri)))
  59. (list 201 '(:content-type "image/png")
  60. (pathname image-path))))
  61. (defun index-response ()
  62. "Return a friendly “salutations” Clack-response for those visting the root-page."
  63. '(201 (:content-type "text/plain")
  64. ("You’ve installed Kaptĉapelo; good work! If you’d like a captcha challenge, visit /new !")))
  65. (defun 404-response ()
  66. "Create a 404-comlpaining HTTP Clack-response."
  67. '(404 (:content-type "text/plain") ("No such page.")))
  68. (defun server (env captcha-dir)
  69. "The heart of the server; returns requests for Clack."
  70. (let* ((uri (quri:uri (getf env :request-uri)))
  71. (uri-path (quri:uri-path uri)))
  72. (format *error-output* "~A" uri-path)
  73. (cond
  74. ;; Create a new captcha at /new
  75. ((string= uri-path "/new")
  76. (new-captcha-response captcha-dir))
  77. ;; For lost souls visiting /[index.html], say “hi.”
  78. ((or (string= uri-path "/")
  79. (string= uri-path "/index.html"))
  80. (index-response))
  81. ;; At /captcha/*.png, server the given image.
  82. ((and (str:starts-with? "/captcha/" uri-path)
  83. (str:ends-with? ".png" uri-path))
  84. (image-response uri-path captcha-dir))
  85. ;; Otherwise… IDK, 404! ¯\_(ツ)_/¯
  86. ('t
  87. (404-response)))))
  88. (defun start-server (&key (address "0.0.0.0") (port 5001) (background 't)
  89. (captcha-directory #p"captcha/"))
  90. "Start the Kaptchapelo server, which takes captcha challenges from the given
  91. CAPTCHA-DIRECTORY. Challenges are made up of two files:
  92. * A challenge PNG file (ex. bird.png)
  93. * An answer TXT file (ex. bird.txt)
  94. Note that the The answer text-file should not contain a trailing newline."
  95. (clack:clackup
  96. (lambda (env)
  97. (funcall #'server env captcha-directory))
  98. :address address
  99. :port port
  100. :use-thread background))