xenu_nntp/lib/nntp/tiny/session.py

331 lines
8.9 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-20 21:17:03 -05:00
2024-11-22 23:57:14 -05:00
from typing import Optional
2024-11-23 22:40:06 -05:00
from nntp.tiny.buffer import LineBuffer, BufferOverflow
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.server import Server, ServerCapability
from nntp.tiny.response import Response, ResponseCode
from nntp.tiny.newsgroup import Newsgroup
from nntp.tiny.message import Message
2024-11-20 21:17:03 -05:00
class SessionState(enum.Flag):
NONE = 0
AUTH_OK = enum.auto()
AUTH_POST = enum.auto()
2024-11-22 23:57:14 -05:00
class MessageRange():
__slots__ = 'id', 'min', 'max',
2024-11-25 00:48:56 -05:00
RE_NUM = re.compile(r'^(\d+)$')
RE_RANGE = re.compile(r'^(\d+)-(\d+)$')
RE_RANGE_LOWER = re.compile(r'^(\d+$)-$')
RE_RANGE_UPPER = re.compile(r'^-(\d+$)$')
2024-11-22 23:57:14 -05:00
def __init__(self):
self.id: int = None
self.min: int = None
self.max: int = None
def __str__(self):
if self.id is not None:
return str(self.id)
if self.min is not None and self.max is None:
return "%d-" % (self.min)
elif self.min is not None and self.max is not None:
return "%d-%d" % (self.min, self.max)
elif self.min is None and self.max is not None:
return "-%d" % (self.max)
return "?"
2024-11-23 00:20:06 -05:00
def where(self):
2024-11-22 23:57:14 -05:00
if self.id is not None:
2024-11-23 00:20:06 -05:00
return "id = %d" % (self.id)
2024-11-22 23:57:14 -05:00
if self.min is not None and self.max is None:
2024-11-23 00:20:06 -05:00
return "id >= %d" % (self.min)
2024-11-22 23:57:14 -05:00
elif self.min is not None and self.max is not None:
2024-11-23 00:20:06 -05:00
return "id >= %d and id <= %d" % (self.min, self.max)
2024-11-22 23:57:14 -05:00
elif self.min is None and self.max is not None:
2024-11-23 00:20:06 -05:00
return "id <= %d" % (self.max)
2024-11-22 23:57:14 -05:00
@staticmethod
def parse(r: str):
match = __class__.RE_NUM.match(r)
if match:
obj = __class__()
obj.id = match[1]
return obj
match = __class__.RE_RANGE.match(r)
if match:
obj = __class__()
obj.min = match[1]
obj.max = match[2]
return obj
match = __class__.RE_RANGE_LOWER.match(r)
if match:
obj = __class__()
obj.min = match[1]
return obj
match = __class__.RE_RANGE_UPPER.match(r)
if match:
obj = __class__()
obj.max = match[1]
return obj
2024-11-20 21:17:03 -05:00
class Session():
RE_SPLIT = re.compile(r'\s+')
NNTP_VERSION = 2
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',
'OVER MSGID'
]
def __init__(self, server: Server, sock: socket.socket):
self.server: Server = server
2024-11-23 00:20:06 -05:00
self.db: Database = server.db
2024-11-20 21:17:03 -05:00
self.state: SessionState = SessionState.NONE
self.sock: socket.socket = sock
self.buf: LineBuffer = LineBuffer()
2024-11-22 23:57:14 -05:00
self.newsgroup: Optional[Newsgroup] = None
2024-11-20 21:17:03 -05:00
def readline(self):
return self.buf.readline(self.sock)
def print(self, text: str, end: str="\r\n"):
return self.sock.send(bytes(text + end, 'ascii'))
def end(self):
return self.print('.')
def respond(self, code: ResponseCode, message: str=None, body=None):
response = Response(code, message, body)
return self.print(str(response))
def _cmd_capabilities(self, *args):
self.respond(ResponseCode.NNTP_CAPABILITIES_FOLLOW)
if self.state & SessionState.AUTH_POST:
self.print('POST')
if self.state & SessionState.AUTH_OK:
self.print('AUTHUSER INFO')
for item in self.NNTP_CAPABILITIES:
self.print(item)
self.end()
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(id),
min(id),
max(id)
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()
text = "%d %d %d %s" % (
row[0],
row[1],
row[2],
newsgroup.name
)
self.respond(ResponseCode.NNTP_GROUP_LISTING, text)
self.newsgroup = newsgroup
return
2024-11-23 00:20:06 -05:00
def _newsgroup_summary(self, newsgroup: Newsgroup) -> str:
sql = """
select
count(id),
min(id),
max(id)
from
newsgroup_message
where
newsgroup_id = ?
"""
cr = self.db.execute(sql, (newsgroup.id))
row = cr.fetchone()
return "%d %d %d %s" % (
row[0],
row[1],
row[2],
newsgroup.name
)
2024-11-22 23:57:14 -05:00
def _cmd_listgroup(self, *args):
newsgroup = self.newsgroup
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
id
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])
2024-11-25 00:16:15 -05:00
sql += " and " + msgrange.clause()
2024-11-22 23:57:14 -05:00
2024-11-23 00:22:24 -05:00
summary = self._newsgroup_summary(newsgroup)
cr = self.db.execute(sql, (newsgroup.id))
2024-11-22 23:57:14 -05:00
2024-11-23 00:22:24 -05:00
self.respond(ResponseCode.NNTP_GROUP_LISTING, summary)
2024-11-22 23:57:14 -05:00
for message in cr.each():
self.print(str(message.id))
return self.end()
2024-11-23 22:40:06 -05:00
def _cmd_list(self, *args):
pass
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
match = self.RE_DATE_SHORT.match(datestr)
if match:
yy, mm, dd = map(int, match[1:3])
if yy >= 70:
yyyy = 1900 + yy
else:
yyyy = 2000 + yy
match = self.RE_DATE_LONG.match(datestr)
if match:
yyyy, mm, dd = map(int, match[1:3])
if yyyy is None:
return
match = self.RE_TIME.match(timestr)
if match is None:
return
hh, mm, ss = map(int, match[1:3])
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 = """
select message_id from newsgroup_message where newsgroup_id = ? and created_on >= ?
"""
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()))
for row in cr.each():
self.print(row[0])
return self.end()
2024-11-25 00:49:53 -05:00
def _cmd_newgroups(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")
pass
COMMANDS = {
'CAPABILITIES': _cmd_capabilities,
'GROUP': _cmd_group,
'LISTGROUP': _cmd_listgroup,
'LIST': _cmd_list,
'NEWNEWS': _cmd_newnews,
'NEWSGROUPS': _cmd_newgroups,
}
2024-11-20 21:17:03 -05:00
def handle(self):
line = self.readline()
if line == '':
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)
return fn(self, *args)