Compare commits

..

4 commits

3 changed files with 137 additions and 75 deletions

64
README
View file

@ -65,47 +65,17 @@ at /usr/lib/x86_64-linux-gnu.
First, try it out. First, try it out.
--8<-------------------------------------------------------->8---
$ cd /path/to/loop/home $ cd /path/to/loop/home
$ ./loop --help $ ./loop
NAME:
loop - An NNTP server for a circle of friends.
USAGE:
loop [options] [arguments ...]
OPTIONS:
--change-passwd <VALUE> <username> <new-password> changes password
--create-account <VALUE> <username> <invited-by> creates a new account
--help display usage information and exit
--logging turn on debug logging on stderr
--version display version and exit
-l, --list-accounts lists accounts
-r, --repl run a REPL on port 4006
-s, --server runs NNTP server reading from stdout
AUTHORS:
Circling Skies <loop@antartida.xyz>
LICENSE:
GPL v3
--8<-------------------------------------------------------->8---
You can talk to the NNTP server with -s:
--8<-------------------------------------------------------->8---
$ ./loop -s
200 Welcome! Say ``help'' for a menu. 200 Welcome! Say ``help'' for a menu.
quit quit
205 Good-bye. 205 Good-bye.
--8<-------------------------------------------------------->8---
It's time to create an account for you. Whenever you run loop, make It runs. Whenever you run loop, make sure you're in its home
sure you're in its home directory because it will look for the file directory because it will look for the file accounts.lisp always
accounts.lisp always relatively to the current working directory of relatively to the current working directory of the process. The same
the process. The same applies if you set up a cron job later applies if you set up a cron job later on---make sure the job, too,
on---make sure the job, too, sets LOOP's home directory as its current sets LOOP's home directory as its current working directory.
working directory.
(*) Create your account (*) Create your account
@ -151,6 +121,28 @@ never done this, it will be better education if you learn to use
daemontools and ucspi-tcp before going live with a LOOP community. daemontools and ucspi-tcp before going live with a LOOP community.
It's easy and fun. It's easy and fun.
(*) LOOP's REPL
LOOP is totally hackable. Even if you're remotely connected, you can
have total control over the loop process with the repl command:
$ telnet example.com 119
Trying example.com...
Connected to example.com.
Escape character is '^]'.
200 Welcome! Say ``help'' for a menu.
login you <secret>
200 Welcome, YOU.
repl
LOOP> *client*
#S(CLIENT :GROUP NIL :ARTICLE 1 :USERNAME YOU :AUTH? YES)
LOOP> (list-groups)
(comp.editors.emacs comp.lisp comp.programming comp.unix
humanities.poetry local.control.news local.havoc local.system.reports
local.test local.users.evalu math.calculus math.havoc)
LOOP> quit
200 Okay, no more REPL hacking.
(*) Cron jobs (*) Cron jobs
If you'd like to remove inactive accounts, we wrote If you'd like to remove inactive accounts, we wrote

View file

@ -12,6 +12,7 @@
(:import-from :lisp-unit define-test) (:import-from :lisp-unit define-test)
(:import-from :org.shirakumo.filesystem-utils (:import-from :org.shirakumo.filesystem-utils
directory-p list-directories list-files) directory-p list-directories list-files)
(:import-from :sb-sys interactive-interrupt)
(:export :main)) (:export :main))
(in-package #:loop) (in-package #:loop)
@ -190,13 +191,27 @@
(in-package :loop) (in-package :loop)
(loop (loop
(print/finish "LOOP> ") (print/finish "LOOP> ")
(let ((expr (read))) (handler-case
(if (eq 'quit expr) (let ((expr (read)))
(return (if (eq 'quit expr)
(make-response (progn
:code 200 :request r ;; At this point, there's a newline still in the stdin due
:data "Okay, no more REPL hacking.")) ;; to read having returned an expression, so we must wipe
(println "~a" (eval expr)))))) ;; that out.
(clear-input)
(return
(make-response
:code 200 :request r
:data "Okay, no more REPL hacking.")))
(println "~a" (eval expr))))
(end-of-file ()
(print/finish "^D~%")
(uiop:quit 0))
(interactive-interrupt ()
(print/finish "^C~%")
(uiop:quit 0))
(t (c)
(print/finish "Oops: ~a~%" c)))))
(defun empty-response () (make-response :code 400 :data "I beg your pardon?")) (defun empty-response () (make-response :code 400 :data "I beg your pardon?"))
(defun prepend-response-with (message r) (defun prepend-response-with (message r)
(make-response (make-response
@ -1278,6 +1293,16 @@
(if okay? (if okay?
(println "Okay, account ~a now has password ``~a''." username new-passwd) (println "Okay, account ~a now has password ``~a''." username new-passwd)
(println "Sorry, could not change password: ~a." problem)))))) (println "Sorry, could not change password: ~a." problem))))))
(defun cli/main-with-handlers (cmd)
(handler-case
(cli/main cmd)
(end-of-file ()
(print/finish "^D~%")
(uiop:quit 0))
(interactive-interrupt ()
(print/finish "^C~%")
(uiop:quit 0))))
(defun cli/main (cmd) (defun cli/main (cmd)
(read-accounts!) (read-accounts!)
(connect-index! "message-id.db") (connect-index! "message-id.db")
@ -1296,25 +1321,26 @@
(when pa (when pa
(cli/change-passwd pa args)) (cli/change-passwd pa args))
(when repl (when repl
(repl)) (repl (make-request :verb "repl" :args '(command-line))))
(when (and (not la) (not ca) (not pa)) (when (and (not la) (not ca) (not pa) (not repl))
(server-start)))) (server-start))))
(defun cli/command () (defun cli/command ()
(clingon:make-command (clingon:make-command
:name "loop" :name "loop"
:description "An NNTP server for a circle of friends." :description "An NNTP server for a circle of friends."
:version "0.1" ;; :authors '("Circling Skies <loop@antartida.xyz>") :version "0.1"
:license "GPL v3" :license "GPL v3"
:options (cli/options) :options (cli/options)
:handler #'cli/main)) :handler #'cli/main-with-handlers))
(defun main-loop () (defun main-loop ()
(let* ((bs (nntp-read-line)) (loop
(ln (bytes->string (ucs-2->ascii bs)))) (let* ((bs (nntp-read-line))
(let ((r (send-response! (dispatch-line ln)))) (ln (bytes->string (ucs-2->ascii bs))))
(when (not (response-quit? r)) (let ((r (send-response! (dispatch-line ln))))
(main-loop))))) (when (response-quit? r)
(return))))))
(defun request-quit? (r) (and r (string= 'quit (request-verb r)))) (defun request-quit? (r) (and r (string= 'quit (request-verb r))))
(defun response-quit? (r) (and r (request-quit? (response-request r)))) (defun response-quit? (r) (and r (request-quit? (response-request r))))

90
loop.nw
View file

@ -377,21 +377,54 @@ LOOP> (list-groups)
(local.control.news local.test) (local.control.news local.test)
\end{verbatim} \end{verbatim}
We implement it by creating a [[repl]] procedure in \lp's package with The REPL can also be use in the middle of an NNTP conversation, which
an option handled by [[cli/command]]. is useful for debugging. All authenticated users can use it, making
passwords only a barrier to spammers, not to your circle of friends,
since everyone can find a way to log in with anyone's account, change
their passwords or just do whatever they want to \lxxp\ process or
databases.
As the comment below explains, we need to [[clear-output]] in the
[[*standard-input*]] to avoid the following problem:
%
\begin{verbatim}
repl
LOOP> quit
200 Okay, no more REPL hacking.
400 I beg your pardon?
\end{verbatim}
%
What happens is that [[read]] consumes just the symbol \verb|quit|, so
there remains a \verb|\n| or \verb|\r\n| in the [[*standard-input*]],
which we need to wipe out, or [[nntp-read-line]] will consume an empty
line, which is what causes that 400 response.
<<Loop's REPL>>= <<Loop's REPL>>=
(defun repl (r) (defun repl (r)
(in-package :loop) (in-package :loop)
(loop (loop
(print/finish "LOOP> ") (print/finish "LOOP> ")
(let ((expr (read))) (handler-case
(if (eq 'quit expr) (let ((expr (read)))
(return (if (eq 'quit expr)
(make-response (progn
:code 200 :request r ;; At this point, there's a newline still in the stdin due
:data "Okay, no more REPL hacking.")) ;; to read having returned an expression, so we must wipe
(println "~a" (eval expr)))))) ;; that out.
(clear-input)
(return
(make-response
:code 200 :request r
:data "Okay, no more REPL hacking.")))
(println "~a" (eval expr))))
(end-of-file ()
(print/finish "^D~%")
(uiop:quit 0))
(interactive-interrupt ()
(print/finish "^C~%")
(uiop:quit 0))
(t (c)
(print/finish "Oops: ~a~%" c)))))
@ %def repl @ %def repl
\section{Description of the package} \section{Description of the package}
@ -598,11 +631,12 @@ itself---so we can cascade actions based on a user's request.
<<Main loop>>= <<Main loop>>=
(defun main-loop () (defun main-loop ()
(let* ((bs (nntp-read-line)) (loop
(ln (bytes->string (ucs-2->ascii bs)))) (let* ((bs (nntp-read-line))
(let ((r (send-response! (dispatch-line ln)))) (ln (bytes->string (ucs-2->ascii bs))))
(when (not (response-quit? r)) (let ((r (send-response! (dispatch-line ln))))
(main-loop))))) (when (response-quit? r)
(return))))))
(defun request-quit? (r) (and r (string= 'quit (request-verb r)))) (defun request-quit? (r) (and r (string= 'quit (request-verb r))))
(defun response-quit? (r) (and r (request-quit? (response-request r)))) (defun response-quit? (r) (and r (request-quit? (response-request r))))
@ -702,6 +736,16 @@ to do that yet.
Now let's write the main procedure in command-line parsing. Now let's write the main procedure in command-line parsing.
<<Command-line parsing>>= <<Command-line parsing>>=
(defun cli/main-with-handlers (cmd)
(handler-case
(cli/main cmd)
(end-of-file ()
(print/finish "^D~%")
(uiop:quit 0))
(interactive-interrupt ()
(print/finish "^C~%")
(uiop:quit 0))))
(defun cli/main (cmd) (defun cli/main (cmd)
(read-accounts!) (read-accounts!)
(connect-index! "message-id.db") (connect-index! "message-id.db")
@ -720,18 +764,18 @@ Now let's write the main procedure in command-line parsing.
(when pa (when pa
(cli/change-passwd pa args)) (cli/change-passwd pa args))
(when repl (when repl
(repl)) (repl (make-request :verb "repl" :args '(command-line))))
(when (and (not la) (not ca) (not pa)) (when (and (not la) (not ca) (not pa) (not repl))
(server-start)))) (server-start))))
(defun cli/command () (defun cli/command ()
(clingon:make-command (clingon:make-command
:name "loop" :name "loop"
:description "<<Description>>" :description "<<Description>>"
:version "<<Version>>" ;; :authors '("Circling Skies <loop@antartida.xyz>") :version "<<Version>>"
:license "GPL v3" :license "GPL v3"
:options (cli/options) :options (cli/options)
:handler #'cli/main)) :handler #'cli/main-with-handlers))
@ %def cli/options cli/command @ %def cli/options cli/command
\section{Request dispatching mechanism} \section{Request dispatching mechanism}
@ -1982,11 +2026,10 @@ all or it has been discussed with the community beforehand.
\subsection{{\tt REPL}} \subsection{{\tt REPL}}
Opens a REPL for the pleasure of hacking \lp. The only thing we \lp\ is totally {\em hackable}. Users can say {\tt repl} to have
require is that the user be authenticated. This means that any user complete control over their \lxxp\ process. XXX: we should implement
has total control over \lp. XXX: implement an option an option [[--disable-repl]] so that REPL hacking is turned off.
[[--disable-repl]] so that REPL hacking is turned off. (This would (This would mean your users are not true hackers.)
mean your users are not true hackers.)
<<Command repl>>= <<Command repl>>=
(defun cmd-repl (r) (defun cmd-repl (r)
@ -2687,6 +2730,7 @@ something to think about.
(:import-from :lisp-unit define-test) (:import-from :lisp-unit define-test)
(:import-from :org.shirakumo.filesystem-utils (:import-from :org.shirakumo.filesystem-utils
directory-p list-directories list-files) directory-p list-directories list-files)
(:import-from :sb-sys interactive-interrupt)
(:export :main)) (:export :main))
(in-package #:loop) (in-package #:loop)