diff --git a/lib/nntp/tiny/request.py b/lib/nntp/tiny/request.py new file mode 100644 index 0000000..55729a9 --- /dev/null +++ b/lib/nntp/tiny/request.py @@ -0,0 +1,15 @@ +import socket + + + +class NNTPRequest(): + __slots__ = 'buf', 'offset', 'sock', + + BUFFER_SIZE = 4096 + + def __init__(self, sock: socket.socket): + + self.buf: bytearray = bytearray(self.BUFFER_SIZE) + self.offset: int = 0 + self.sock: socket.socket = sock + diff --git a/lib/nntp/tiny/response.py b/lib/nntp/tiny/response.py new file mode 100644 index 0000000..5115748 --- /dev/null +++ b/lib/nntp/tiny/response.py @@ -0,0 +1,85 @@ +import enum +import socket + +from typing import Optional + +class ResponseCode(enum.Enum): + NNTP_HELP_FOLLOWS = 100 + NNTP_CAPABILITIES_FOLLOW = 101 + NNTP_DATE = 111 + NNTP_SERVICE_READY_POST_ALLOWED = 200 + NNTP_SERVICE_READY_POST_PROHIBITED = 201 + NNTP_CONNECTION_CLOSING = 205 + NNTP_GROUP_LISTING = 211 + NNTP_INFORMATION_FOLLOWS = 215 + NNTP_ARTICLE_BODY = 220 + NNTP_ARTICLE_LISTING = 221 + NNTP_BODY_LISTING = 222 + NNTP_ARTICLE_STAT_RESPONSE = 223 + NNTP_OVERVIEW_FOLLOWS = 224 + NNTP_HEADERS_FOLLOW = 225 + NNTP_ARTICLE_LISTING_ID_FOLLOWS = 230 + NNTP_GROUPS_NEW_FOLLOW = 231 + NNTP_ARTICLE_RECEIVED = 240 + NNTP_AUTH_ACCEPTED = 281 + NNTP_INQUIRY_ARTICLE = 340 + NNTP_INQUIRY_PASSPHRASE = 381 + NNTP_NEWSGROUP_NOT_FOUND = 411 + NNTP_NEWSGROUP_NOT_SELECTED = 412 + NNTP_ARTICLE_INVALID_NUMBER = 420 + NNTP_ARTICLE_NOT_FOUND_NUM = 423 + NNTP_ARTICLE_NOT_FOUND_ID = 430 + NNTP_POST_PROHIBITED = 440 + NNTP_POST_FAILED = 441 + NNTP_AUTH_FAILED = 481 + NNTP_AUTH_BAD_SEQUENCE = 482 + NNTP_COMMAND_UNKNOWN = 500 + NNTP_SYNTAX_ERROR = 501 + NNTP_COMMAND_UNAVAILABLE = 502 + NNTP_GROUPS_UNAVAILABLE = 503 + + def message(self): + return { + 100: "Help text follows", + 101: "Capabilities follow", + 200: "NNTP Service Ready, posting allowed", + 201: "NNTP Service Ready, posting prohibited", + 205: "Connection closing", + 215: "Information follows", + 224: "Overview information follows (multi-line)", + 225: "Headers follow (multi-line)", + 231: "List of new newsgroups follows", + 240: "Article received OK", + 281: "Authentication accepted", + 340: "Input article; end with .", + 381: "Enter passphrase", + 411: "No such newsgroup", + 412: "No newsgroup selected", + 420: "Current article number is invalid", + 423: "No article found by that number", + 430: "No article found by that message ID", + 440: "Posting prohibited", + 441: "Posting failed", + 481: "Authentication failed", + 482: "Authentication commands issued out of sequence", + 500: "Unknown command", + 501: "Syntax error", + 502: "Command unavailable", + 503: "No list of recommended newsgroups available" + }.get(self.value) + +class Response(): + __slots__ = 'code', 'message', 'body', + + def __init__(self, code: ResponseCode, message: Optional[str]=None, body: Optional[str]=None): + self.code = code + self.message = message or code.message() or "Unknown response" + self.body = body + + def __str__(self): + ret = "%d %s" % (self.code.value, self.message) + + if self.body: + ret += "\r\n" + self.body + + return ret diff --git a/lib/nntp/tiny/server.py b/lib/nntp/tiny/server.py new file mode 100644 index 0000000..2089ca1 --- /dev/null +++ b/lib/nntp/tiny/server.py @@ -0,0 +1,10 @@ +import enum + +class ServerCapability(enum.Flag): + NONE = 0 + AUTH = enum.auto() + POST = enum.auto() + +class Server(): + def __init_(self): + self.capabilities = NNTPServerCapability.NONE diff --git a/lib/nntp/tiny/session.py b/lib/nntp/tiny/session.py new file mode 100644 index 0000000..98be025 --- /dev/null +++ b/lib/nntp/tiny/session.py @@ -0,0 +1,73 @@ +import enum +import socket +import re + +from nntp.tiny.server import Server, ServerCapability +from nntp.tiny.response import Response + +class SessionState(enum.Flag): + NONE = 0 + AUTH_OK = enum.auto() + AUTH_POST = enum.auto() + +from nntp.tiny.buffer import LineBuffer, BufferOverflow + +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 + self.state: SessionState = SessionState.NONE + self.sock: socket.socket = sock + self.buf: LineBuffer = LineBuffer() + + 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() + + def handle(self): + line = self.readline() + + if line == '': + return + + command, args = self.RE_SPLIT.split(line.rstrip())