xenu_nntp/lib/nntp/tiny/session.py

236 lines
6 KiB
Python
Raw Normal View History

2024-11-20 21:17:03 -05:00
import enum
import socket
import re
2024-11-22 23:57:14 -05:00
from typing import Optional
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()
from nntp.tiny.buffer import LineBuffer, BufferOverflow
2024-11-22 23:57:14 -05:00
class MessageRange():
__slots__ = 'id', 'min', 'max',
RE_NUM = re.compile('^(\d+)$')
RE_RANGE = re.compile('^(\d+)-(\d+)$')
RE_RANGE_LOWER = re.compile('^(\d+$)-$')
RE_RANGE_UPPER = re.compile('^-(\d+$)$')
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 = [
'VERSION %d' % (self.NNTP_VERSION),
'READER',
'HDR',
'NEWNEWS',
'LIST ACTIVE NEWSGROUP OVERVIEW.FMT SUBSCRIPTIONS',
'OVER MSGID'
]
COMMANDS = {
'capabilities':
}
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-23 00:20:06 -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])
sql += " and " msgrange.clause()
2024-11-22 23:57:14 -05:00
2024-11-23 00:20:06 -05:00
text = self._newsgroup_summary(newsgroup)
cr = self.db.execute(sql, (newsgroup.id))
2024-11-22 23:57:14 -05:00
2024-11-23 00:20:06 -05:00
self.respond(ResponseCode.NNTP_GROUP_LISTING, text)
2024-11-22 23:57:14 -05:00
for message in cr.each():
self.print(str(message.id))
return self.end()
2024-11-20 21:17:03 -05:00
def handle(self):
line = self.readline()
if line == '':
return
command, args = self.RE_SPLIT.split(line.rstrip())