engine.lisp 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. (in-package :cl-user)
  2. (defpackage #:assboard.engine
  3. (:use :cl :assboard.utils)
  4. (:export #:command-error
  5. #:process-command
  6. #:format-new-posting
  7. #:format-update-posting
  8. #:format-close-posting
  9. #:format-delete-posting))
  10. (in-package #:assboard.engine)
  11. (defvar *posting-hash-salt* "[TEST]" "Key for HMAC for new post to get id-hash")
  12. (defvar *posting-hash-length* 8 "id-hash length")
  13. (defvar *command-valid-for* 600 "Seconds of validity for command timestamp")
  14. (defun sync-keys-with-trustlist ()
  15. (let ((keys (assboard.pgp:list-fingerprints))
  16. (trustlist (assboard.wot:get-trustlist)))
  17. ;; Drop no-longer-trusted keys
  18. (loop for fgp in keys
  19. unless (find fgp trustlist :test #'equal :key #'car)
  20. do (assboard.pgp:delete-key fgp))
  21. ;; Download new trusted keys
  22. (loop for (fgp . username) in trustlist
  23. unless (find fgp keys :test #'equal)
  24. do (assboard.pgp:recv-key fgp))))
  25. (define-condition command-error (error) ())
  26. (define-condition not-valid-pgp-error (command-error) ())
  27. (define-condition not-valid-command-error (command-error) ())
  28. (define-condition not-valid-signature-error (command-error) ())
  29. (define-condition not-valid-timestamp-error (command-error) ())
  30. (define-condition not-valid-hash-error (command-error) ())
  31. (defun get-post-hash (body author)
  32. "Get part of body+author HMAC to use id-hash."
  33. (let ((hmac (crypto:make-hmac
  34. (trivial-utf-8:string-to-utf-8-bytes *posting-hash-salt*)
  35. :sha512)))
  36. (crypto:update-hmac
  37. hmac
  38. (trivial-utf-8:string-to-utf-8-bytes (concatenate 'string body author)))
  39. (subseq (cl-base64:usb8-array-to-base64-string
  40. (crypto:hmac-digest hmac) :uri t)
  41. 0 *posting-hash-length*)))
  42. (defun get-author (raw)
  43. (let ((fgp (data::raw-fingerprint raw)))
  44. (or (cdr (assoc fgp
  45. assboard.wot:*trustlist-cache*
  46. :test #'equal))
  47. fgp)))
  48. (defun cmd/new-post (cmd raw)
  49. (let* ((author (get-author raw))
  50. (title-end (position #\Newline cmd))
  51. (title (subseq cmd 10 title-end))
  52. (body (subseq cmd (+ title-end 2)))
  53. (hash (get-post-hash body author))
  54. (posting
  55. (pomo:make-dao 'data::posting
  56. :author author
  57. :hash hash
  58. :title title
  59. :body body))
  60. (posting-edition
  61. (pomo:make-dao 'data::posting-edition
  62. :posting-id (data::posting-id posting)
  63. :title title
  64. :body body
  65. :raw-id (data::raw-id raw))))
  66. (values posting posting-edition)))
  67. (defun find-users-posting (hash author)
  68. (first (pomo:select-dao
  69. 'data::posting
  70. (:and
  71. (:= 'hash hash)
  72. (:= 'author author)))))
  73. (defun cmd/update-post (cmd raw)
  74. (let* ((author (get-author raw))
  75. (hash-end (position #\Colon cmd))
  76. (hash (subseq cmd 12 hash-end))
  77. (posting (find-users-posting hash author)))
  78. (unless posting
  79. (error 'not-valid-hash-error))
  80. (let* ((title-end (position #\Newline cmd))
  81. (title (subseq cmd (+ hash-end 2) title-end))
  82. (body (subseq cmd (+ title-end 2)))
  83. (posting-edition
  84. (pomo:make-dao 'data::posting-edition
  85. :posting-id (data::posting-id posting)
  86. :title title
  87. :body body
  88. :raw-id (data::raw-id raw))))
  89. (setf (data::posting-title posting) title
  90. (data::posting-body posting) body
  91. (data::posting-status posting) "open"
  92. (data::posting-updated-on posting) (local-time:now))
  93. (pomo::save-dao posting)
  94. (values posting posting-edition))))
  95. (defun cmd/close-post (cmd raw)
  96. (let* ((hash-end (position #\Newline cmd))
  97. (hash (subseq cmd 11 hash-end))
  98. (posting (find-users-posting hash (get-author raw))))
  99. (unless posting
  100. (error 'not-valid-hash-error))
  101. (setf (data::posting-status posting) "closed"
  102. (data::posting-updated-on posting) (local-time:now))
  103. (pomo::save-dao posting)
  104. (values posting nil)))
  105. (defun cmd/delete-post (cmd raw)
  106. (let* ((hash-end (position #\Newline cmd))
  107. (hash (subseq cmd 12 hash-end))
  108. (posting (find-users-posting hash (get-author raw))))
  109. (unless posting
  110. (error 'not-valid-hash-error))
  111. (setf (data::posting-status posting) "deleted"
  112. (data::posting-updated-on posting) (local-time:now))
  113. (pomo::save-dao posting)
  114. (values posting nil)))
  115. (defun format-new-posting (title body)
  116. (format nil "NEW POST: ~A~%~%~A" title body))
  117. (defun format-update-posting (post title body)
  118. (format nil "UPDATE POST ~A: ~A~%~%~A" (data::posting-hash post) title body))
  119. (defun format-close-posting (post)
  120. (format nil "CLOSE POST ~A" (data::posting-hash post)))
  121. (defun format-delete-posting (post)
  122. (format nil "DELETE POST ~A" (data::posting-hash post)))
  123. (defparameter +commands+
  124. (list (cons "NEW POST: " #'cmd/new-post)
  125. (cons "CLOSE POST " #'cmd/close-post)
  126. (cons "UPDATE POST " #'cmd/update-post)
  127. (cons "DELETE POST " #'cmd/delete-post)))
  128. (defun get-command-handler (command)
  129. (cdr (find command +commands+ :key #'car
  130. :test #'(lambda (s w) (starts-with w s)))))
  131. (defun process-command (clearsigned)
  132. ;; Remove possible CRLF
  133. (setq clearsigned (replace-all clearsigned
  134. '(#\Return #\Linefeed)
  135. (string #\Linefeed)))
  136. ;; Simple vanity check
  137. (unless (starts-with "-----BEGIN PGP SIGNED MESSAGE-----" clearsigned)
  138. (error 'not-valid-pgp-error))
  139. (handler-case
  140. (let* ((start (+ 2 (search '(#\Newline #\Newline) clearsigned)))
  141. (end (1- (search "-----BEGIN PGP SIGNATURE-----" clearsigned)))
  142. (command (subseq clearsigned start end))
  143. (handler (get-command-handler command)))
  144. ;; Command check
  145. (unless handler
  146. (error 'not-valid-command-error))
  147. ;; Signature validation
  148. (multiple-value-bind (fgp unixtime)
  149. (assboard.pgp:verify clearsigned)
  150. (unless fgp
  151. (error 'not-valid-signature-error))
  152. (let ((timestamp (local-time:unix-to-timestamp unixtime)))
  153. ;; Timestamp check
  154. (unless (< (abs (local-time:timestamp-difference
  155. (local-time:now)
  156. timestamp))
  157. *command-valid-for*)
  158. (error 'not-valid-timestamp-error))
  159. ;; All checks performed, store raw command text and process it
  160. (let ((raw (pomo:make-dao 'data::raw
  161. :clearsigned clearsigned
  162. :fingerprint fgp
  163. :timestamp timestamp)))
  164. (funcall handler command raw)))))
  165. (type-error (e)
  166. (log:error e)
  167. (error 'not-valid-command-error))))