Compare commits

..

2 commits

3 changed files with 75 additions and 137 deletions

64
README
View file

@ -65,17 +65,47 @@ 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 $ ./loop --help
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 runs. Whenever you run loop, make sure you're in its home It's time to create an account for you. Whenever you run loop, make
directory because it will look for the file accounts.lisp always sure you're in its home directory because it will look for the file
relatively to the current working directory of the process. The same accounts.lisp always relatively to the current working directory of
applies if you set up a cron job later on---make sure the job, too, the process. The same applies if you set up a cron job later
sets LOOP's home directory as its current working directory. on---make sure the job, too, sets LOOP's home directory as its current
working directory.
(*) Create your account (*) Create your account
@ -121,28 +151,6 @@ 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,7 +12,6 @@
(: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)
@ -191,27 +190,13 @@
(in-package :loop) (in-package :loop)
(loop (loop
(print/finish "LOOP> ") (print/finish "LOOP> ")
(handler-case
(let ((expr (read))) (let ((expr (read)))
(if (eq 'quit expr) (if (eq 'quit expr)
(progn
;; At this point, there's a newline still in the stdin due
;; to read having returned an expression, so we must wipe
;; that out.
(clear-input)
(return (return
(make-response (make-response
:code 200 :request r :code 200 :request r
:data "Okay, no more REPL hacking."))) :data "Okay, no more REPL hacking."))
(println "~a" (eval expr)))) (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
@ -1293,16 +1278,6 @@
(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")
@ -1321,26 +1296,25 @@
(when pa (when pa
(cli/change-passwd pa args)) (cli/change-passwd pa args))
(when repl (when repl
(repl (make-request :verb "repl" :args '(command-line)))) (repl))
(when (and (not la) (not ca) (not pa) (not repl)) (when (and (not la) (not ca) (not pa))
(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" :version "0.1" ;; :authors '("Circling Skies <loop@antartida.xyz>")
:license "GPL v3" :license "GPL v3"
:options (cli/options) :options (cli/options)
:handler #'cli/main-with-handlers)) :handler #'cli/main))
(defun main-loop () (defun main-loop ()
(loop
(let* ((bs (nntp-read-line)) (let* ((bs (nntp-read-line))
(ln (bytes->string (ucs-2->ascii bs)))) (ln (bytes->string (ucs-2->ascii bs))))
(let ((r (send-response! (dispatch-line ln)))) (let ((r (send-response! (dispatch-line ln))))
(when (response-quit? r) (when (not (response-quit? r))
(return)))))) (main-loop)))))
(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))))

74
loop.nw
View file

@ -377,54 +377,21 @@ LOOP> (list-groups)
(local.control.news local.test) (local.control.news local.test)
\end{verbatim} \end{verbatim}
The REPL can also be use in the middle of an NNTP conversation, which We implement it by creating a [[repl]] procedure in \lp's package with
is useful for debugging. All authenticated users can use it, making an option handled by [[cli/command]].
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> ")
(handler-case
(let ((expr (read))) (let ((expr (read)))
(if (eq 'quit expr) (if (eq 'quit expr)
(progn
;; At this point, there's a newline still in the stdin due
;; to read having returned an expression, so we must wipe
;; that out.
(clear-input)
(return (return
(make-response (make-response
:code 200 :request r :code 200 :request r
:data "Okay, no more REPL hacking."))) :data "Okay, no more REPL hacking."))
(println "~a" (eval expr)))) (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}
@ -631,12 +598,11 @@ itself---so we can cascade actions based on a user's request.
<<Main loop>>= <<Main loop>>=
(defun main-loop () (defun main-loop ()
(loop
(let* ((bs (nntp-read-line)) (let* ((bs (nntp-read-line))
(ln (bytes->string (ucs-2->ascii bs)))) (ln (bytes->string (ucs-2->ascii bs))))
(let ((r (send-response! (dispatch-line ln)))) (let ((r (send-response! (dispatch-line ln))))
(when (response-quit? r) (when (not (response-quit? r))
(return)))))) (main-loop)))))
(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))))
@ -736,16 +702,6 @@ 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")
@ -764,18 +720,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 (make-request :verb "repl" :args '(command-line)))) (repl))
(when (and (not la) (not ca) (not pa) (not repl)) (when (and (not la) (not ca) (not pa))
(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>>" :version "<<Version>>" ;; :authors '("Circling Skies <loop@antartida.xyz>")
:license "GPL v3" :license "GPL v3"
:options (cli/options) :options (cli/options)
:handler #'cli/main-with-handlers)) :handler #'cli/main))
@ %def cli/options cli/command @ %def cli/options cli/command
\section{Request dispatching mechanism} \section{Request dispatching mechanism}
@ -2026,10 +1982,11 @@ all or it has been discussed with the community beforehand.
\subsection{{\tt REPL}} \subsection{{\tt REPL}}
\lp\ is totally {\em hackable}. Users can say {\tt repl} to have Opens a REPL for the pleasure of hacking \lp. The only thing we
complete control over their \lxxp\ process. XXX: we should implement require is that the user be authenticated. This means that any user
an option [[--disable-repl]] so that REPL hacking is turned off. has total control over \lp. XXX: implement an option
(This would mean your users are not true hackers.) [[--disable-repl]] so that REPL hacking is turned off. (This would
mean your users are not true hackers.)
<<Command repl>>= <<Command repl>>=
(defun cmd-repl (r) (defun cmd-repl (r)
@ -2730,7 +2687,6 @@ 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)