xenu_nntp/lib/nntp/tiny/session.py

868 lines
24 KiB
Python
Raw Normal View History

2024-11-23 22:40:06 -05:00
import re
2024-11-20 21:17:03 -05:00
import enum
import socket
2024-11-23 22:40:06 -05:00
import datetime
import fnmatch
2024-11-25 20:27:14 -05:00
import traceback
2024-11-27 12:36:02 -05:00
import email.header
import email.utils
2024-11-20 21:17:03 -05:00
2024-11-22 23:57:14 -05:00
from typing import Optional
from nntp.tiny.socket import Connection
2024-11-23 00:20:06 -05:00
from nntp.tiny.db import Database
2024-11-22 23:57:14 -05:00
from nntp.tiny.response import Response, ResponseCode
from nntp.tiny.newsgroup import Newsgroup
2024-11-29 23:45:14 -05:00
from nntp.tiny.user import User, UserPermission
from nntp.tiny.message import (Message, MessageRange, MessagePart,
each_line)
2024-11-20 21:17:03 -05:00
2024-11-26 10:44:21 -05:00
class SessionMode(enum.Enum):
READER = 1
class Session(Connection):
2024-11-25 17:17:08 -05:00
NNTP_VERSION = 2
2024-11-20 21:17:03 -05:00
NNTP_CAPABILITIES = [
2024-11-25 00:16:15 -05:00
'VERSION %d' % (NNTP_VERSION),
2024-11-20 21:17:03 -05:00
'READER',
'HDR',
'NEWNEWS',
'LIST ACTIVE NEWSGROUP OVERVIEW.FMT SUBSCRIPTIONS',
2024-11-29 23:45:14 -05:00
'OVER MSGID',
'AUTHINFO USER',
2024-11-20 21:17:03 -05:00
]
2024-11-25 17:17:08 -05:00
RE_SPLIT = re.compile(r'\s+')
2024-11-26 16:55:44 -05:00
def __init__(self, server, sock: socket.socket):
2024-11-26 18:50:47 -05:00
self.server = server
self.db: Database = server.connect_to_db()
self.mode: SessionMode = SessionMode.READER
2024-11-29 23:23:08 -05:00
self.active: bool = True
2024-11-20 21:17:03 -05:00
2024-11-29 23:45:14 -05:00
self.newsgroup: Optional[Newsgroup] = None
self.user: Optional[User] = None
self.perms: Optional[UserPermission] = None
self.article_id: Optional[int] = None
2024-11-22 23:57:14 -05:00
super().__init__(sock)
2024-11-20 21:17:03 -05:00
def respond(self, code: ResponseCode, message: str=None, body=None):
response = Response(code, message, body)
return self.print(str(response))
2024-11-29 23:45:14 -05:00
def _cmd_authinfo_user(self, username):
if self.user is not None:
return self.respond(ResponseCode.NNTP_AUTH_BAD_SEQUENCE)
self.user = self.db.get(User, {'username': username})
return self.respond(ResponseCode.NNTP_INQUIRY_PASSPHRASE)
def _cmd_authinfo_pass(self, password):
if self.user is None or not self.user.auth(password):
return self.respond(ResponseCode.NNTP_AUTH_FAILED)
self.perms = self.user.permissions(self.db)
return self.respond(ResponseCode.NNTP_AUTH_ACCEPTED)
AUTHINFO_SUBCOMMANDS = {
'USER': _cmd_authinfo_user,
'PASS': _cmd_authinfo_pass
}
def _cmd_authinfo(self, *args):
if len(args) == 0:
return self.respond(ResponseCode.NNTP_SYNTAX_ERROR, "No subcommand provided")
subcmd, *subargs = args
fn = self.AUTHINFO_SUBCOMMANDS.get(subcmd.upper())
if fn is None:
return self.respond(ResponseCode.NNTP_COMMAND_UNKNOWN)
return fn(self, *subargs)
2024-11-20 21:17:03 -05:00
def _cmd_capabilities(self, *args):
self.respond(ResponseCode.NNTP_CAPABILITIES_FOLLOW)
2024-11-29 23:45:14 -05:00
if self.perms and self.perms & UserPermission.POST:
self.print('POST')
2024-11-20 21:17:03 -05:00
for item in self.NNTP_CAPABILITIES:
self.print(item)
self.end()
2024-11-26 10:44:21 -05:00
def _cmd_mode(self, *args):
if len(args) != 1 or args[0] != 'READER':
return self.respond(ResponseCode.NNTP_SYNTAX_ERROR)
self.mode = SessionMode.READER
return self.respond(ResponseCode.NNTP_POST_PROHIBITED)
2024-11-22 23:57:14 -05:00
def _cmd_group(self, name: str):
if name not in self.server.newsgroups:
return self.respond(ResponseCode.NNTP_NEWSGROUP_NOT_FOUND)
newsgroup = self.server.newsgroups[name]
sql = """
select
count(message_id),
min(message_id),
max(message_id)
2024-11-22 23:57:14 -05:00
from
newsgroup_message
where
newsgroup_id = ?
"""
2024-11-25 00:49:24 -05:00
cr = self.db.execute(sql, (newsgroup.id,))
2024-11-22 23:57:14 -05:00
row = cr.fetchone()
if row is None:
text = "%d %d %d %s" % (
0, 0, 0, newsgroup.name
)
self.article_id = None
else:
text = "%d %d %d %s" % (
row[0],
row[1],
row[2],
newsgroup.name
)
2024-11-22 23:57:14 -05:00
self.article_id = row[1]
2024-11-22 23:57:14 -05:00
self.newsgroup = newsgroup
2024-11-22 23:57:14 -05:00
return self.respond(ResponseCode.NNTP_GROUP_LISTING, text)
2024-11-22 23:57:14 -05:00
2024-11-26 12:08:56 -05:00
def _cmd_last(self):
if self.newsgroup is None:
return self.respond(ResponseCode.NNTP_NEWSGROUP_NOT_SELECTED)
if self.article_id is None:
return self.respond(ResponseCode.NNTP_ARTICLE_INVALID_NUMBER)
sql = """
select
max(message_id)
2024-11-26 12:08:56 -05:00
from
newsgroup_message
where
newsgroup_id = ?
and message_id < ?
2024-11-26 12:08:56 -05:00
"""
cr = self.db.execute(sql, (self.newsgroup.id, self.article_id))
row = cr.fetchone()
if row is None or row[0] is None:
return self.respond(ResponseCode.NNTP_ARTICLE_NO_PREVIOUS)
self.article_id = row[0]
return self.respond(ResponseCode.NNTP_ARTICLE_STAT_RESPONSE)
def _cmd_next(self):
if self.newsgroup is None:
return self.respond(ResponseCode.NNTP_NEWSGROUP_NOT_SELECTED)
if self.article_id is None:
return self.respond(ResponseCode.NNTP_ARTICLE_INVALID_NUMBER)
sql = """
select
min(message_id)
2024-11-26 12:08:56 -05:00
from
newsgroup_message
where
message_id = ?
2024-11-26 12:08:56 -05:00
and id > ?
"""
cr = self.db.execute(sql, (self.newsgroup.id, self.article_id))
row = cr.fetchone()
if row is None or row[0] is None:
return self.respond(ResponseCode.NNTP_ARTICLE_NO_NEXT)
self.article_id = row[0]
return self.respond(ResponseCode.NNTP_ARTICLE_STAT_RESPONSE)
2024-12-04 23:52:29 -05:00
def _newsgroup_summary(self, newsgroup: Newsgroup, since: Optional[datetime.datetime]=None) -> str:
2024-11-23 00:20:06 -05:00
sql = """
select
2024-12-04 23:52:29 -05:00
count(newsgroup_message.message_id),
min(newsgroup_message.message_id),
max(newsgroup_message.message_id)
2024-11-23 00:20:06 -05:00
from
2024-12-04 23:52:29 -05:00
newsgroup_message,
message
2024-11-23 00:20:06 -05:00
where
2024-12-04 23:52:29 -05:00
message.id = newsgroup_message.message_id
and newsgroup_message.newsgroup_id = ?
2024-11-23 00:20:06 -05:00
"""
2024-12-04 23:52:29 -05:00
values = [newsgroup.id]
if since is not None:
sql += " and message.created_on >= ?"
values.append(since.isoformat())
cr = self.db.execute(sql, values)
2024-11-23 00:20:06 -05:00
row = cr.fetchone()
return {
'count': row[0],
'min': row[1],
'max': row[2],
'name': newsgroup.name,
2024-12-02 17:22:56 -05:00
'perms': 'y' if newsgroup.writable else 'n'
}
2024-11-22 23:57:14 -05:00
def _cmd_listgroup(self, *args):
newsgroup = self.newsgroup
2024-11-25 17:18:22 -05:00
2024-11-22 23:57:14 -05:00
if len(args) == 0 and newsgroup is None:
return self.respond(ResponseCode.NNTP_NEWSGROUP_NOT_SELECTED)
elif len(args) > 0:
newsgroup = self.server.newsgroups.get(args[0])
if newsgroup is None:
return self.respond(ResponseCode.NNTP_NEWSGROUP_NOT_FOUND)
2024-11-23 00:20:06 -05:00
sql = """
select
message_id
2024-11-23 00:20:06 -05:00
from
newsgroup_message
where
newsgroup_id = ?
"""
2024-11-22 23:57:14 -05:00
if len(args) > 1:
2024-11-23 00:20:06 -05:00
msgrange = MessageRange.parse(args[1])
sql += " and " + msgrange.where('message_id')
2024-11-22 23:57:14 -05:00
2024-11-23 00:22:24 -05:00
summary = self._newsgroup_summary(newsgroup)
2024-12-02 14:29:27 -05:00
cr = self.db.execute(sql, (newsgroup.id,))
2024-11-22 23:57:14 -05:00
text = "%d %d %d %s" % (
summary['count'],
summary['min'],
summary['max'],
summary['name']
)
self.respond(ResponseCode.NNTP_GROUP_LISTING, text)
2024-11-22 23:57:14 -05:00
2024-12-02 14:29:27 -05:00
while True:
row = cr.fetchone()
if row is None:
break
self.print(str(row[0]))
2024-11-22 23:57:14 -05:00
return self.end()
def print_newsgroup_summary(self, newsgroup: Newsgroup, since: Optional[datetime.datetime]=None):
2024-12-04 23:52:29 -05:00
summary = self._newsgroup_summary(newsgroup, since)
2024-11-25 15:53:32 -05:00
return self.print("%s %d %d %s" % (
summary['name'],
summary['min'],
summary['max'],
2024-11-25 15:53:32 -05:00
summary['perms']
))
def _cmd_list_newsgroups(self):
self.respond(ResponseCode.NNTP_INFORMATION_FOLLOWS)
for name in self.server.newsgroups:
newsgroup = self.server.newsgroups[name]
self.print("%s %s" % (
newsgroup.name,
newsgroup.description
))
2024-11-25 15:53:32 -05:00
return self.end()
2024-11-25 17:18:38 -05:00
def _newsgroup_last_active(self, newsgroup: Newsgroup):
sql = """
select
max(message.created_on)
2024-11-25 17:18:38 -05:00
from
newsgroup_message,
message
2024-11-25 17:18:38 -05:00
where
2024-11-30 19:09:25 -05:00
message.id = newsgroup_message.message_id
and newsgroup_message.newsgroup_id = ?
2024-11-25 17:18:38 -05:00
"""
cr = self.db.execute(sql, (newsgroup.id,))
row = cr.fetchone()
if row is None:
return
return datetime.datetime.fromisoformat(row[0])
def _cmd_list_active(self):
now = datetime.datetime.now(datetime.UTC)
self.respond(ResponseCode.NNTP_INFORMATION_FOLLOWS)
for name in self.server.newsgroups:
newsgroup = self.server.newsgroups[name]
last_active = self._newsgroup_last_active(newsgroup)
if now - last_active < datetime.timedelta(days=1):
self.print_newsgroup_summary(newsgroup)
2024-11-25 17:18:38 -05:00
return self.end()
2024-11-26 14:05:34 -05:00
def _cmd_list_active_times(self):
self.respond(ResponseCode.NNTP_INFORMATION_FOLLOWS)
for name in self.server.newsgroups:
newsgroup = self.server.newsgroups[name]
self.print("%s %d %s" % (
name,
newsgroup.created_on.timestamp(),
newsgroup.created_by
))
return self.end()
2024-11-26 13:28:33 -05:00
OVERVIEW_FMT_HEADERS = [
'Subject',
'From',
'Date',
'Message-ID',
'References',
'Bytes',
'Lines',
]
def _cmd_list_overview_fmt(self):
self.respond(ResponseCode.NNTP_INFORMATION_FOLLOWS, "Order of fields in overview database")
for header in self.OVERVIEW_FMT_HEADERS:
self.print("%s:" % (header,))
return self.end()
2024-11-26 16:02:54 -05:00
SUPPORTED_HEADERS = [
':',
':lines',
':bytes',
]
def _cmd_list_headers(self):
self.respond(ResponseCode.NNTP_INFORMATION_FOLLOWS, "metadata items supported")
for name in self.SUPPORTED_HEADERS:
self.print(name)
self.end()
2024-11-25 15:53:32 -05:00
LIST_SUBCOMMANDS = {
2024-11-26 13:28:33 -05:00
'NEWSGROUPS': _cmd_list_newsgroups,
'ACTIVE': _cmd_list_active,
2024-11-26 14:05:34 -05:00
'ACTIVE.TIMES': _cmd_list_active_times,
2024-11-26 13:28:33 -05:00
'OVERVIEW.FMT': _cmd_list_overview_fmt,
2024-11-26 16:02:54 -05:00
'HEADERS': _cmd_list_headers,
2024-11-25 15:53:32 -05:00
}
2024-11-23 22:40:06 -05:00
def _cmd_list(self, *args):
2024-11-25 15:53:32 -05:00
if len(args) == 0:
return self.respond(ResponseCode.NNTP_SYNTAX_ERROR, "No subcommand provided")
subcmd, *subargs = args
fn = self.LIST_SUBCOMMANDS.get(subcmd.upper())
if fn is None:
return self.respond(ResponseCode.NNTP_COMMAND_UNKNOWN)
return fn(self, *subargs)
2024-11-23 22:40:06 -05:00
RE_DATE_SHORT = re.compile(r'^(\d{2})(\d{2})(\d{2})$')
RE_DATE_LONG = re.compile(r'^(\d{4})(\d{2})(\d{2})$')
RE_TIME = re.compile(r'^(\d{2})(\d{2})(\d{2})$')
def _parse_date_time(self, datestr: str, timestr: str):
yyyy, mm, dd = None, None, None,
hh, MM, ss = None, None, None
2024-11-25 17:18:22 -05:00
2024-11-23 22:40:06 -05:00
match = self.RE_DATE_SHORT.match(datestr)
if match:
2024-12-04 22:25:48 -05:00
yy = int(match[1])
mm = int(match[2])
dd = int(match[3])
2024-11-23 22:40:06 -05:00
if yy >= 70:
yyyy = 1900 + yy
else:
yyyy = 2000 + yy
match = self.RE_DATE_LONG.match(datestr)
if match:
2024-12-04 22:25:48 -05:00
yyyy = int(match[1])
mm = int(match[2])
dd = int(match[3])
2024-11-23 22:40:06 -05:00
if yyyy is None:
return
match = self.RE_TIME.match(timestr)
if match is None:
return
2024-12-04 22:25:48 -05:00
hh = int(match[1])
MM = int(match[2])
ss = int(match[3])
2024-11-23 22:40:06 -05:00
return datetime.datetime(yyyy, mm, dd, hh, MM, ss)
def _cmd_newnews(self, wildmat, datestr, timestr, *args):
gmt = False
if len(args) == 1:
if args[0] == "GMT":
gmt = True
else:
return self.send_response(ResponseCode.NNTP_SYNTAX_ERROR, "Only optional 'GMT' allowed")
elif len(args) > 1:
return self.send_response(ResponseCode.NNTP_SYNTAX_ERROR, "Too many arguments")
timestamp = self._parse_date_time(datestr, timestr)
if timestamp is None:
return self.send_response(ResponseCode.NNTP_SYNTAX_ERROR, "Invalid date or time")
self.respond(ResponseCode.NNTP_ARTICLE_LISTING_ID_FOLLOWS)
sql = """
2024-11-26 11:25:40 -05:00
select
message.id
2024-11-26 11:25:40 -05:00
from
newsgroup_message,
message
2024-11-26 11:25:40 -05:00
where
message.id = newsgroup_message.message_id
and newsgroup_message.newsgroup_id = ?
and message.created_on >= ?
2024-11-23 22:40:06 -05:00
"""
for name in self.server.newsgroups:
if fnmatch.fnmatch(name, wildmat):
newsgroup = self.server.newsgroups[name]
cr = self.db.execute(sql, (newsgroup.id, timestamp.isoformat()))
2024-12-04 22:25:48 -05:00
while True:
row = cr.fetchone()
if row is None:
break
self.print(str(row[0]))
2024-11-23 22:40:06 -05:00
return self.end()
def _cmd_newgroups(self, datestr, timestr, *args):
2024-11-25 00:49:53 -05:00
gmt = False
if len(args) == 1:
if args[0] == "GMT":
gmt = True
else:
2024-11-25 14:25:55 -05:00
return self.respond(ResponseCode.NNTP_SYNTAX_ERROR, "Only optional 'GMT' allowed")
2024-11-25 00:49:53 -05:00
elif len(args) > 1:
2024-11-25 14:25:55 -05:00
return self.respond(ResponseCode.NNTP_SYNTAX_ERROR, "Too many arguments")
2024-11-25 00:49:53 -05:00
2024-12-04 23:52:29 -05:00
timestamp = self._parse_date_time(datestr, timestr)
2024-11-25 14:25:55 -05:00
self.respond(ResponseCode.NNTP_GROUPS_NEW_FOLLOW)
for name in self.server.newsgroups:
newsgroup = self.server.newsgroups[name]
2024-11-25 15:53:32 -05:00
self.print_newsgroup_summary(newsgroup, timestamp)
2024-11-25 14:25:55 -05:00
return self.end()
2024-11-25 00:49:53 -05:00
2024-11-27 00:22:12 -05:00
def _each_message_by_id(self, identifier: str, success: ResponseCode=ResponseCode.NNTP_INFORMATION_FOLLOWS):
if identifier is None:
if self.newsgroup is None:
self.respond(ResponseCode.NNTP_NEWSGROUP_NOT_SELECTED)
return
if self.article_id is None:
self.respond(ResponseCode.NNTP_ARTICLE_INVALID_NUMBER)
return
message = self.db.get(Message, {'id': str(self.article_id)})
if message is None:
self.respond(ResponseCode.NNTP_ARTICLE_INVALID_NUMBER)
return
2024-11-27 00:22:12 -05:00
self.respond(success)
yield message
elif identifier[0] == '<':
message = self.db.query(Message, {
'message_id': identifier
}).fetchone()
if message is None:
self.respond(ResponseCode.NNTP_ARTICLE_NOT_FOUND_ID)
return
2024-11-27 00:22:12 -05:00
self.respond(success)
yield message
2024-11-25 20:27:55 -05:00
else:
if self.newsgroup is None:
self.respond(ResponseCode.NNTP_NEWSGROUP_NOT_SELECTED)
return
msgrange = MessageRange.parse(identifier)
sql = """
select
message.*
from
newsgroup_message,
message
where
2024-11-30 19:09:25 -05:00
message.id = newsgroup_message.message_id
and newsgroup_message.newsgroup_id = ?
"""
sql += " and " + msgrange.where('newsgroup_message.message_id')
cr = self.db.query_sql(Message, sql, (self.newsgroup.id,))
first = True
for message in cr.each():
if first:
first = False
2024-11-27 00:22:12 -05:00
self.respond(success)
yield message
if first:
2024-11-30 20:30:58 -05:00
self.respond(ResponseCode.NNTP_ARTICLE_NOT_FOUND_NUM)
self.end()
2024-11-25 20:27:55 -05:00
def _message_by_id(self, identifier: Optional[str]=None):
if identifier is None:
if self.newsgroup is None:
self.respond(ResponseCode.NNTP_NEWSGROUP_NOT_SELECTED)
return
2024-11-25 20:27:55 -05:00
if self.article_id is None:
self.respond(ResponseCode.NNTP_ARTICLE_INVALID_NUMBER)
return
2024-11-25 20:27:55 -05:00
message = self.db.get(Message, {'id': self.article_id})
if message is None:
self.respond(ResponseCode.NNTP_ARTICLE_NOT_FOUND_NUM)
return
return message
elif identifier[0] == '<':
message = self.db.query(Message, {
'message_id': identifier
}).fetchone()
if message is None:
self.respond(ResponseCode.NNTP_ARTICLE_NOT_FOUND_ID)
return
return message
2024-11-25 20:27:55 -05:00
else:
message = self.db.get(Message, {'id': int(identifier)})
if message is None:
self.respond(ResponseCode.NNTP_ARTICLE_NOT_FOUND_NUM)
return
return message
def message_respond(self, part: MessagePart, identifier: Optional[str]=None):
message = self._message_by_id(identifier)
if message is None:
return
2024-11-25 20:27:55 -05:00
text = "%d %s" % (
message.id,
message.message_id
)
self.respond(ResponseCode.NNTP_ARTICLE_LISTING, text)
self.message_send(message, part)
self.end()
2024-11-25 20:27:55 -05:00
2024-11-25 20:34:26 -05:00
def _cmd_head(self, identifier: Optional[str]=None):
self.message_respond(MessagePart.HEAD, identifier)
2024-11-25 20:27:55 -05:00
2024-11-25 20:34:26 -05:00
def _cmd_body(self, identifier: Optional[str]=None):
self.message_respond(MessagePart.BODY, identifier)
2024-11-25 20:27:55 -05:00
2024-11-25 20:34:26 -05:00
def _cmd_article(self, identifier: Optional[str]=None):
self.message_respond(MessagePart.WHOLE, identifier)
2024-11-25 21:53:08 -05:00
def _cmd_hdr(self, name: str, identifier: Optional[str]=None):
for message in self._each_message_by_id(identifier):
return self.print("%d %s" % (
message.id, message.headers.get(name, '')
))
2024-11-25 21:53:08 -05:00
2024-11-26 13:12:42 -05:00
def _message_overview(self, message: Message) -> dict:
2024-11-27 15:27:35 -05:00
def f(s: str):
return s.replace('\t', ' ').replace('\r', '').replace('\n', ' ').replace('\0', '')
parts = [
2024-11-26 13:12:42 -05:00
str(message.id),
2024-11-27 12:36:02 -05:00
email.header.Header(message.subject).encode(),
email.header.Header(message.sender).encode(),
email.utils.format_datetime(message.created_on),
2024-11-26 13:12:42 -05:00
message.message_id,
2024-11-28 07:59:06 -05:00
message.reference_ids or '',
2024-11-27 00:06:12 -05:00
str(len(message.content)),
str(message.content.count('\n') + 1),
2024-11-27 15:27:35 -05:00
]
2024-12-04 23:23:54 -05:00
HEADERS_SKIP = {
'subject': True,
'from': True,
'date': True,
'message-id': True,
'references': True
}
2024-11-27 15:27:35 -05:00
for k in message.headers:
2024-12-04 23:23:54 -05:00
if k.casefold() in HEADERS_SKIP:
continue
2024-11-27 15:27:35 -05:00
parts.append("%s: %s" % (
k, message.headers[k]
))
return map(f, parts)
2024-11-26 13:12:42 -05:00
def _cmd_over(self, identifier: Optional[str]=None):
2024-11-27 00:22:12 -05:00
for message in self._each_message_by_id(identifier, ResponseCode.NNTP_OVERVIEW_FOLLOWS):
overview = self._message_overview(message)
2024-11-26 13:12:42 -05:00
2024-11-26 17:20:13 -05:00
self.print('\t'.join(overview))
2024-11-26 13:12:42 -05:00
2024-11-25 22:01:49 -05:00
def _cmd_stat(self, identifier: Optional[str]=None):
message = self._message_by_id(identifier)
2024-11-25 22:01:49 -05:00
if message is None:
return
2024-11-25 22:01:49 -05:00
text = "%d %s" % (message.id, message.message_id)
2024-11-25 22:01:49 -05:00
self.article_id = message.id
2024-11-25 22:01:49 -05:00
return self.respond(ResponseCode.NNTP_ARTICLE_STAT_RESPONSE, text)
2024-11-30 17:11:27 -05:00
RE_NEWSGROUPS_SPLIT = re.compile(r'\s*,\s*')
def _save_message(self, message: Message, success: ResponseCode):
2024-11-30 17:11:27 -05:00
value = message.header('Newsgroups')
if value is None or value == '':
return ResponseCode.NNTP_POST_FAILED
2024-11-30 17:11:27 -05:00
names = map(lambda s: s.lower(), self.RE_NEWSGROUPS_SPLIT.split(value))
2024-11-30 17:11:27 -05:00
newsgroups = list()
for name in names:
newsgroup = self.server.newsgroups.get(name)
2024-12-02 17:22:56 -05:00
if newsgroup is None or not newsgroup.writable:
return ResponseCode.NNTP_POST_PROHIBITED
2024-11-30 17:11:27 -05:00
newsgroups.append(self.server.newsgroups[name])
if len(newsgroups) == 0:
return ResponseCode.NNTP_POST_FAILED
2024-11-30 17:11:27 -05:00
message.message_id_assign()
if not message.validate():
return ResponseCode.NNTP_POST_FAILED
self.db.add(message)
2024-11-30 17:11:27 -05:00
for newsgroup in newsgroups:
sql = """
insert into newsgroup_message (
newsgroup_id, message_id
) values (?, ?)
"""
cr = self.db.execute(sql, (newsgroup.id, message.id))
2024-11-30 20:31:13 -05:00
self.db.commit()
return success
2024-11-30 17:11:27 -05:00
def _post_impl(self, message_id=None):
2024-11-30 19:35:36 -05:00
if self.perms is None or not self.perms & UserPermission.POST:
2024-11-30 19:37:14 -05:00
return self.respond(ResponseCode.NNTP_POST_PROHIBITED)
if message_id:
sql = """
select
count(message_id)
from
message
where
message_id = ?
"""
cr = self.db.execute(sql, (message_id,))
row = cr.fetchone()
2024-11-30 18:24:47 -05:00
if row is not None and row[0] > 0:
return self.respond(ResponseCode.NNTP_ARTICLE_NOT_WANTED_ID)
2024-11-30 18:24:47 -05:00
code_inquiry = ResponseCode.NNTP_INQUIRY_ARTICLE_ID
code_received = ResponseCode.NNTP_ARTICLE_RECEIVED_ID
else:
code_inquiry = ResponseCode.NNTP_INQUIRY_ARTICLE
code_received = ResponseCode.NNTP_ARTICLE_RECEIVED
2024-11-30 18:24:47 -05:00
self.respond(code_inquiry)
2024-11-30 18:24:47 -05:00
message = Message()
while True:
line = self.readline()
if line == '':
self.active = False
break
stripped = line.rstrip()
if stripped == '.':
message.finish()
code = self._save_message(message, code_received)
return self.respond(code)
elif stripped == '..':
line = line[1:]
2024-11-30 18:24:47 -05:00
try:
message.readline(line)
except:
return self.respond(ResponseCode.NNTP_POST_FAILED)
def _cmd_post(self):
return self._post_impl()
def _cmd_ihave(self, message_id):
return self._post_impl(message_id)
2024-11-26 10:19:02 -05:00
def _cmd_date(self):
timestamp = datetime.datetime.now(datetime.UTC)
return self.respond(ResponseCode.NNTP_DATE,
timestamp.strftime("%Y%m%d%H%M%S"))
2024-11-26 10:27:03 -05:00
def _cmd_quit(self):
2024-11-29 23:23:08 -05:00
self.active = False
2024-11-26 10:27:03 -05:00
return self.respond(ResponseCode.NNTP_CONNECTION_CLOSING)
2024-11-25 00:49:53 -05:00
COMMANDS = {
2024-11-29 23:45:14 -05:00
'AUTHINFO': _cmd_authinfo,
2024-11-25 00:49:53 -05:00
'CAPABILITIES': _cmd_capabilities,
2024-11-26 10:44:21 -05:00
'MODE': _cmd_mode,
2024-11-25 00:49:53 -05:00
'GROUP': _cmd_group,
2024-11-26 12:08:56 -05:00
'LAST': _cmd_last,
'NEXT': _cmd_next,
2024-11-25 00:49:53 -05:00
'LISTGROUP': _cmd_listgroup,
'LIST': _cmd_list,
'NEWNEWS': _cmd_newnews,
2024-11-25 14:25:55 -05:00
'NEWGROUPS': _cmd_newgroups,
2024-11-25 20:27:55 -05:00
'HEAD': _cmd_head,
'BODY': _cmd_body,
2024-11-25 21:53:08 -05:00
'ARTICLE': _cmd_article,
'HDR': _cmd_hdr,
2024-11-26 12:15:56 -05:00
'XHDR': _cmd_hdr,
2024-11-26 13:12:42 -05:00
'OVER': _cmd_over,
2024-11-26 17:20:22 -05:00
'XOVER': _cmd_over,
2024-11-25 22:01:49 -05:00
'STAT': _cmd_stat,
2024-11-30 17:11:27 -05:00
'POST': _cmd_post,
2024-11-30 18:24:47 -05:00
'IHAVE': _cmd_ihave,
2024-11-26 10:19:02 -05:00
'DATE': _cmd_date,
2024-11-26 10:27:03 -05:00
'QUIT': _cmd_quit,
2024-11-25 00:49:53 -05:00
}
def greet(self):
2024-11-26 22:21:20 -05:00
self.respond(ResponseCode.NNTP_SERVICE_READY_POST_PROHIBITED)
2024-11-26 16:55:44 -05:00
def handle_command(self):
2024-11-20 21:17:03 -05:00
line = self.readline()
if line == '':
2024-11-29 23:23:08 -05:00
self.active = False
2024-11-20 21:17:03 -05:00
return
2024-11-25 00:49:53 -05:00
tokens = self.RE_SPLIT.split(line.rstrip())
command, *args = tokens
fn = self.COMMANDS.get(command.upper())
if fn is None:
return self.respond(ResponseCode.NNTP_COMMAND_UNKNOWN)
2024-11-25 14:27:58 -05:00
try:
return fn(self, *args)
except TypeError as e:
2024-11-25 20:27:14 -05:00
traceback.print_exception(e)
2024-11-25 14:27:58 -05:00
return self.respond(ResponseCode.NNTP_SYNTAX_ERROR)
except Exception as e:
2024-11-25 20:27:14 -05:00
traceback.print_exception(e)
2024-11-25 14:27:58 -05:00
return self.respond(ResponseCode.NNTP_COMMAND_UNAVAILABLE)
2024-11-26 16:55:44 -05:00
def handle(self):
self.greet()
2024-11-28 07:36:41 -05:00
try:
2024-11-29 23:23:08 -05:00
while self.active:
2024-11-28 07:36:41 -05:00
self.handle_command()
2024-11-26 16:55:44 -05:00
2024-11-28 07:36:41 -05:00
self.sock.close()
2024-12-04 23:28:25 -05:00
except (BrokenPipeError, ConnectionResetError):
2024-11-28 09:09:00 -05:00
pass