shithub: rc-nntpd

Download patch

ref: 53c3169386da4cffee65e3dfe94723c299e98d05
author: Linux User <l@malkuth.lan>
date: Sun Jul 16 12:54:41 EDT 2023

first commit

--- /dev/null
+++ b/LICENSE
@@ -1,0 +1,7 @@
+Copyright 2023 kitzman@disroot.org
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
--- /dev/null
+++ b/README
@@ -1,0 +1,27 @@
+rc-nntpd is a NNTP server inspired by rc-httpd.
+
+The scripts expect to be installed under /rc/bin/rc-nntpd.
+
+The rc_nntpd_dir setting in the script defaults to the installation
+directory.
+
+You can adjust the NNTP storage path in select-handler:11.
+
+For persisting connection states, the /tmp directory is used. One
+can use a dedicated ramfs, or any kind of directory which is
+not subject to snapshotting. The variable is in select-handler:14.
+
+The service can be started with listen(8). To have the port listening
+on the server, you can can use the tcp119 file:
+
+#!/bin/rc
+net=$3
+exec /rc/bin/rc-nntpd/rc-nntpd >>[2]/sys/log/nntp
+
+For a TLS-encrypted connection, one can use tlssrv(8). A certificate
+must be created, and the correct listen(8) file added.
+
+TODO:
+	- XOVER
+	- posting
+	- authentication
--- /dev/null
+++ b/handlers/get-article-by-uuid
@@ -1,0 +1,26 @@
+#!/bin/rc
+
+. log.rc
+
+group_name=$1
+article_uuid=$2
+
+if (~ $group_name '' || ~ $article_uuid '')
+	exit 500
+
+group_path=`{echo -n $group_name | sed 's/\./\//'}
+group_path=$news/$group_path
+
+for (article_path in `{walk -f $group_path}) {
+	article_found=`{cat $article_path | \
+					gunzip | \
+					grep -i 'message-id: ' | \
+					grep $article_uuid }
+	if (! ~ $article_found '') {
+		article_id=`{basename $article_path}
+		echo $article_id
+		exit
+	}
+}
+
+exit 430
--- /dev/null
+++ b/handlers/handle-nntp
@@ -1,0 +1,507 @@
+#!/bin/rc
+
+. log.rc
+
+switch($cmd(1)) {
+case 'CAPABILITIES'
+	# Capabilities command
+	response 101 'Capability list follows (multi-line)'
+	multiline <<!
+VERSION 2
+READER
+LIST ACTIVE NEWSGROUPS
+IMPLEMENTATION RC-NNTPD 0.1 2023-07-14
+!
+	exit 101
+
+case 'DATE'
+	# Returns the current date of the server
+	current_date=`{date -f 'YYYYMMDDhhmmss'}
+	if (! ~ $status '') {
+		log 'unable to get date'
+		response 500 'Internal server error'
+		exit 500
+	}
+	response 111 $current_date
+	exit 111
+
+case 'LIST'
+	# List newsgroups
+	candidates=`{walk -d $news | sort}
+	newsdirs=()
+	newsgroups=()
+
+	for (newsdir in $candidates) {
+		first_file=`{ls $newsdir | sed 1q}
+		if (~ $first_file '' || test -f $first_file)
+			newsdirs=($newsdirs $newsdir)
+	}
+	for (newsdir in $newsdirs) {
+		newsgroup=`{echo $newsdir | \
+					sed 's/\/storage\///g' | \
+					sed 's/\//./g'}
+		newsgroups=($newsgroups $newsgroup)
+	}
+
+	list_keyword=$cmd(2)
+	if (! ~ $list_keyword '' && \
+		! ~ $list_keyword 'ACTIVE' && \
+		! ~ $list_keyword 'NEWSGROUPS') {
+		response 501 'Invalid LIST keyword argument'
+		exit 501
+	}
+
+	response 215 'list of newsgroups follows'
+	{ for (newsgroup in $newsgroups) {
+		if (~ $list_keyword '' || ~ $list_keyword 'ACTIVE') {
+			group_path=`{echo -n $newsgroup | sed 's/\./\//'}
+			group_path=$news/$group_path
+
+			group_low=`{ls $group_path | sort -n | \
+						sed 1q | xargs basename}
+			group_high=`{ls $group_path | sort -n | \
+						tail -n 1 | xargs basename}
+
+			if (~ $group_ac '0') {
+				group_low=0
+				group_high=0
+			}
+			echo $newsgroup^' '^$group_low^' '^$group_high^' n'
+		}
+		if (~ $list_keyword 'NEWSGROUPS')
+			echo $newsgroup $newsgroup
+	} } | multiline
+
+	exit 215
+
+case 'GROUP'
+	# Sets the current group
+	if (~ $cmd(2) '') {
+		response 501 'No group supplied'
+		exit 501
+	}
+
+	group=$cmd(2)
+	group_path=`{echo -n $group | sed 's/\./\//'}
+	group_path=$news/$group_path
+	if (! test -d $group_path) {
+		response 411 'No such newsgroup'
+		exit 411
+	}
+
+	group_ac=`{ls $group_path | wc -l | sed 's/[ ]*//'}
+
+	group_low=`{ls $group_path | sort -n | sed 1q | xargs basename}
+	group_high=`{ls $group_path | sort -n | tail -n 1 | xargs basename}
+
+	if (~ $group_ac '0') {
+		group_low=0
+		group_high=0
+	}
+
+	echo $group >$current_group
+	echo $group_ac >$current_group_ac
+	echo $group_low >$current_group_low
+	echo $group_high >$current_group_high
+	echo -n '' >$current_article
+
+	response 211 $group_ac^' '^$group_low^' '^$group_high^' '^$group
+	exit 211
+
+case 'LISTGROUP'
+	# List group contents
+	group=$cmd(2)
+	range=$cmd(3)
+
+	if (~ $group '') {
+		group=`{cat $current_group >[2]/dev/null}
+	}
+	if (~ $group '') {
+		response 412 'No newsgroup selected'
+		exit 412
+	}
+
+	group_path=`{echo -n $group | sed 's/\./\//'}
+	group_path=$news/$group_path
+	if (! test -d $group_path) {
+		response 411 'No such newsgroup'
+		exit 411
+	}
+
+	group_ac=`{ls $group_path | wc -l | sed 's/[ ]*//'}
+
+	group_low=`{ls $group_path | sort -n | sed 1q | xargs basename}
+	group_high=`{ls $group_path | sort -n | tail -n 1 | xargs basename}
+
+	if (~ $group_ac '0') {
+		group_low=0
+		group_high=0
+	}
+
+	low=`{echo $range | sed 's/([0-9]*)\-.*/\1/g'}
+	high=`{echo $range | sed 's/.*\-([0-9]*)/\1/g'}
+
+	if (~ $low '') {
+		low=$group_low
+	}
+
+	if (~ $high '') {
+		high=$group_high
+	}
+
+	response 211 $group_ac^' '^$group_low^' '^$group_high^' '^$group
+
+	ls $group_path | \
+		syscall -o read 0 buf 512 >[2]/dev/null | \
+		xargs -n 1 basename | \
+		awk '{ if ($1 <= '^$high^' && $1 >= '^$low^') print $1 }' | \
+		multiline
+
+case 'ARTICLE'
+	# Retrieve article
+	group=`{cat $current_group}
+	if (~ $group '') {
+		response 412 'No newsgroup selected'
+		exit 412
+	}
+
+	if (! ~ $cmd(2) '') {
+		article=$cmd(2)
+	}
+	if not {
+		if (! ~ `{cat $current_article >[2]/dev/null} '') {
+			article=`{cat $current_article}
+		}
+		if not {
+			response 501 'No article number supplied'
+			exit 501
+		}
+	}
+
+	group_path=`{echo -n $group | sed 's/\./\//'}
+	group_path=$news/$group_path
+
+	empty=`{echo $article | tr -d '[0-9]'}
+	is_article_id=0
+
+	if (~ $empty '') {
+		is_article_id=1
+	}
+
+	if (~ $is_article_id 0) {
+		article=`{get-article-by-uuid $group $article}
+		status=`{echo $status | sed 's/.* ([0-9]+)$/\1/'}
+		switch ($status) {
+		case ''
+		case 430
+			response 430 'No article with that message-id'
+			exit 430
+		case *
+			response 500 'Internal server error'
+			exit 500
+		}
+	}
+
+	if (! test -f $group_path/$article) {
+		response 420 'Current article number is invalid'
+		exit 420
+	}
+
+	echo $article >$current_article
+	article_uuid=`{cat $group_path/$article | \
+		gunzip | \
+		grep -i 'message-id: ' | \
+		sed 's/[Mm]essage-[Ii][Dd]:[ ]?(.*)$/\1/'}
+
+	response 223 $article^' '^$article_uuid
+	cat $group_path/$article | gunzip | multiline
+	exit 223
+
+case 'LAST'
+	# Retrieve previous article
+	if (~ `{cat $current_group >[2]/dev/null} '') {
+		response 412 'No newsgroup selected'
+		exit 412
+	}
+
+	if (~ `{cat $current_article >[2]/dev/null} '') {
+		response 420 'Current article number is invalid'
+		exit 420
+	}
+
+	group=`{cat $current_group}
+	group_ac=`{cat $current_group_ac}
+	group_low=`{cat $current_group_low}
+	group_high=`{cat $current_group_high}
+
+	article=`{cat $current_article}
+
+	group_path=`{echo -n $group | sed 's/\./\//'}
+	group_path=$news/$group_path
+
+	last_article=`{awk 'BEGIN {
+	for (i = '^$article^' - 1; i >= '^$group_low^'; i--) {
+		if (system(sprintf("test -f '^$group_path^'/%d", i)) == 0) {
+			print i
+			exit
+		}
+	}}'}
+
+	if (~ $last_article '') {
+		response 422 'No previous article in this group'
+		exit 422
+	}
+
+	echo $last_article >$current_article
+	article=$last_article
+	article_uuid=`{cat $group_path/$article | \
+		gunzip | \
+		grep -i 'message-id: ' | \
+		sed 's/[Mm]essage-[Ii][Dd]:[ ]?(.*)$/\1/'}
+
+	response 223 $article^' '^$article_uuid
+	exit 223
+
+case 'NEXT'
+	# Retrieve next article
+	if (~ `{cat $current_group >[2]/dev/null} '') {
+		response 412 'No newsgroup selected'
+		exit 412
+	}
+
+	if (~ `{cat $current_article >[2]/dev/null} '') {
+		response 420 'Current article number is invalid'
+		exit 420
+	}
+
+	group=`{cat $current_group}
+	group_ac=`{cat $current_group_ac}
+	group_low=`{cat $current_group_low}
+	group_high=`{cat $current_group_high}
+
+	article=`{cat $current_article}
+
+	group_path=`{echo -n $group | sed 's/\./\//'}
+	group_path=$news/$group_path
+
+	next_article=`{awk 'BEGIN {
+	for (i = '^$article^' + 1; i <= '^$group_high^'; i++) {
+		if (system(sprintf("test -f '^$group_path^'/%d", i)) == 0) {
+			print i
+			exit
+		}
+	}}'}
+
+	if (~ $next_article '') {
+		response 422 'No next article in this group'
+		exit 422
+	}
+
+	echo $next_article >$current_article
+	article=$next_article
+	article_uuid=`{cat $group_path/$article | \
+		gunzip | \
+		grep -i 'message-id: ' | \
+		sed 's/[Mm]essage-[Ii][Dd]:[ ]?(.*)$/\1/'}
+
+	response 223 $article^' '^$article_uuid
+	exit 223
+
+case 'HEAD'
+	# Retrieve the article headers
+	group=`{cat $current_group >[2]/dev/null}
+	if (~ $group '') {
+		response 412 'No newsgroup selected'
+		exit 412
+	}
+
+	if (! ~ $cmd(2) '') {
+		article=$cmd(2)
+	}
+	if not {
+		if (! ~ `{cat $current_article >[2]/dev/null} '') {
+			article=`{cat $current_article}
+		}
+		if not {
+			response 501 'No article number supplied'
+			exit 501
+		}
+	}
+
+	group_path=`{echo -n $group | sed 's/\./\//'}
+	group_path=$news/$group_path
+
+	empty=`{echo $article | tr -d '[0-9]'}
+	is_article_id=0
+
+	if (~ $empty '') {
+		is_article_id=1
+	}
+
+	if (~ $is_article_id 0) {
+		article=`{get-article-by-uuid $group $article}
+		status=`{echo $status | sed 's/.* ([0-9]+)$/\1/'}
+		switch ($status) {
+		case ''
+		case 430
+			response 430 'No article with that message-id'
+			exit 430
+		case *
+			response 500 'Internal server error'
+			exit 500
+		}
+	}
+
+	if (! test -f $group_path/$article) {
+		response 420 'Current article number is invalid'
+		exit 420
+	}
+
+	article_uuid=`{cat $group_path/$article | \
+		gunzip | \
+		grep -i 'message-id: ' | \
+		sed 's/[Mm]essage-[Ii][Dd]:[ ]?(.*)$/\1/'}
+
+	response 221 $article^' '^$article_uuid
+
+	cat $group_path/$article | \
+		gunzip | \
+		awk 'BEGIN { s=0 }
+{ if (length($0) == 0) s=1
+  if (s == 0) print $0
+}' | multiline
+	exit 221
+
+case 'BODY'
+	# Retrieve the article body
+	group=`{cat $current_group >[2]/dev/null}
+	if (~ $group '') {
+		response 412 'No newsgroup selected'
+		exit 412
+	}
+
+	if (! ~ $cmd(2) '') {
+		article=$cmd(2)
+	}
+	if not {
+		if (! ~ `{cat $current_article >[2]/dev/null} '') {
+			article=`{cat $current_article}
+		}
+		if not {
+			response 501 'No article number supplied'
+			exit 501
+		}
+	}
+
+	group_path=`{echo -n $group | sed 's/\./\//'}
+	group_path=$news/$group_path
+
+	empty=`{echo $article | tr -d '[0-9]'}
+	is_article_id=0
+
+	if (~ $empty '') {
+		is_article_id=1
+	}
+
+	if (~ $is_article_id 0) {
+		article=`{get-article-by-uuid $group $article}
+		status=`{echo $status | sed 's/.* ([0-9]+)$/\1/'}
+		switch ($status) {
+		case ''
+		case 430
+			response 430 'No article with that message-id'
+			exit 430
+		case *
+			response 500 'Internal server error'
+			exit 500
+		}
+	}
+
+	if (! test -f $group_path/$article) {
+		response 420 'Current article number is invalid'
+		exit 420
+	}
+
+	article_uuid=`{cat $group_path/$article | \
+		gunzip | \
+		grep -i 'message-id: ' | \
+		sed 's/[Mm]essage-[Ii][Dd]:[ ]?(.*)$/\1/'}
+
+	response 221 $article^' '^$article_uuid
+
+	cat $group_path/$article | \
+		gunzip | \
+		awk 'BEGIN { s=0 }
+{ if (s == 1) print $0
+  if (length($0) == 0) s=1
+}' | multiline
+	exit 221
+
+case 'STAT'
+	# Test if the article by id and message-id exists
+	group=`{cat $current_group >[2]/dev/null}
+	if (~ $group '') {
+		response 412 'No newsgroup selected'
+		exit 412
+	}
+
+	if (! ~ $cmd(2) '') {
+		article=$cmd(2)
+	}
+	if not {
+		if (! ~ `{cat $current_article >[2]/dev/null} '') {
+			article=`{cat $current_article}
+		}
+		if not {
+			response 501 'No article number supplied'
+			exit 501
+		}
+	}
+
+	group_path=`{echo -n $group | sed 's/\./\//'}
+	group_path=$news/$group_path
+
+	empty=`{echo $article | tr -d '[0-9]'}
+	is_article_id=0
+
+	if (~ $empty '') {
+		is_article_id=1
+	}
+
+	if (~ $is_article_id 0) {
+		article=`{get-article-by-uuid $group $article}
+		status=`{echo $status | sed 's/.* ([0-9]+)$/\1/'}
+		switch ($status) {
+		case ''
+		case 430
+			response 430 'No article with that message-id'
+			exit 430
+		case *
+			response 500 'Internal server error'
+			exit 500
+		}
+	}
+
+	if (! test -f $group_path/$article) {
+		response 420 'Article number is invalid'
+		exit 420
+	}
+
+	article_uuid=`{cat $group_path/$article | \
+		gunzip | \
+		grep -i 'message-id: ' | \
+		sed 's/[Mm]essage-[Ii][Dd]:[ ]?(.*)$/\1/'}
+
+	response 223 $article^' '^$article_uuid
+	exit 223
+
+case 'QUIT'
+	# Quit command
+	rm $lockfile
+
+case *
+	# Default handler
+	log 'client sent command' $cmd 'which is not recognized'
+	response 500 'Unknown command'
+	exit 500
+}
--- /dev/null
+++ b/handlers/log.rc
@@ -1,0 +1,4 @@
+fn log {
+	datestr=`{date -f 'YYYY-MM-DD hh:mm:ss'}
+	echo '['^$"datestr^']' $* >[1=2]
+}
--- /dev/null
+++ b/handlers/multiline
@@ -1,0 +1,4 @@
+#!/bin/rc
+cr=
+sed 's/$/'^$cr^'/g'
+echo '.'^$cr
--- /dev/null
+++ b/handlers/response
@@ -1,0 +1,2 @@
+#!/bin/rc
+echo $1^' '^$2^$cr
--- /dev/null
+++ b/mkfile
@@ -1,0 +1,6 @@
+install:V:
+	mkdir -p /rc/bin/rc-nntpd/handlers
+	cp rc-nntpd /rc/bin/rc-nntpd/
+	cp select-handler /rc/bin/rc-nntpd/
+	dircp handlers /rc/bin/rc-nntpd/handlers
+	mkdir -p /rc/bin/rc-nntpd/skel/^(bin rc dev storage env tmp)
--- /dev/null
+++ b/rc-nntpd
@@ -1,0 +1,7 @@
+#!/bin/rc
+rfork nE
+rc_nntpd_dir=/rc/bin/rc-nntpd
+bind -b $rc_nntpd_dir/handlers /bin
+cr=
+
+. $rc_nntpd_dir/select-handler
--- /dev/null
+++ b/select-handler
@@ -1,0 +1,80 @@
+#!/bin/rc
+
+rfork
+
+skel=$rc_nntpd_dir^/skel
+
+bind /bin $skel^/bin
+bind /rc $skel^/rc
+bind -c '#e' $skel^/env
+bind -c '#c' $skel^/dev
+bind -c /nntp/storage $skel^/storage
+
+# Connection state directory
+rc_nntpd_statedir=/nntp/tmp
+
+# ramfs -m $rc_nntp_statedir
+# if (! test -r '#s'/nntp.ramfs)
+# 	ramfs -s nntp.ramfs -m $rc_nntpd_statedir
+# if not
+# 	mount '#s'/nntp.ramfs $rc_nntp_statedir
+
+bind -c $rc_nntpd_statedir $skel^/tmp
+
+cat /adm/timezone/local >$skel^/env/timezone
+
+bind $skel /
+news=/storage
+
+# bootstrapping
+cr=
+. log.rc
+
+connid=`{cat /dev/random | tr -dc '0-9a-zA-Z' | read -c8}
+
+cmd=()
+lockfile=/tmp/lock
+
+current_group=/tmp/$connid/group
+current_article=/tmp/$connid/article
+current_group_ac=/tmp/$connid/group_ac
+current_group_low=/tmp/$connid/group_low
+current_group_high=/tmp/$connid/group_high
+
+touch $current_group
+touch $current_article
+touch $current_group_ac
+touch $current_group_low
+touch $current_group_high
+
+# exit handler
+fn sigexit {
+	rm -rf /tmp/$connid
+}
+
+# server entrypoint
+
+if (! test -r $news && ! ~ `{ns | grep storage} '') {
+	log 'no working storage found; quitting'
+	exit
+}
+
+log 'client connection established, id' $connid
+response 200 'NNTP Service Ready, posting prohibited'
+
+touch $lockfile
+while(test -f $lockfile) {
+	cmdline=`{read >[2]/dev/null}
+	if (! ~ $status '') {
+		log 'client connection hungup, bye?'
+		exit
+	}
+
+	cmd=`{echo $cmdline | sed 's/'^$cr^'//g'}
+	handle-nntp
+	sleep 1
+}
+
+log 'client connection' $connid 'closing, bye'
+response 205 'NNTP Service exits normally'
+exit