Files
newznab-tmux/python/nntpproxy.py
T

247 lines
10 KiB
Python

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import json
import time
import random
import socket
import SocketServer
import socketpool
from lib.pynntp.nntp import nntp
from lib.info import bcolors
from socketpool import util
class NNTPClientConnector(socketpool.Connector, nntp.NNTPClient):
def __init__(self, host, port, backend_mod, pool=None,
username="anonymous", password="anonymous", timeout=60, use_ssl=False):
self.host = host
self.port = port
self.backend_mod = backend_mod
self._pool = pool
self._connected = False
self._life = time.time() - random.randint(0, 10)
self._start_time = time.time()
if backend_mod.Socket != socket.socket:
raise ValueError("Bad backend")
nntp.NNTPClient.__init__(self, self.host, self.port, username, password, timeout=timeout, use_ssl=use_ssl)
self.id = self.socket.getsockname()[1]
print(bcolors.PRIMARY + "New NNTP connection to %s established with id #%5d" %
(self.host, self.id) + bcolors.ENDC)
self._connected = True
self.xfeature_compress_gzip()
def __del__(self):
self.release()
def matches(self, **match_options):
target_host = match_options.get('host')
target_port = match_options.get('port')
return target_host == self.host and target_port == self.port
def is_connected(self):
if self._connected:
return socketpool.util.is_connected(self.socket)
return False
def handle_exception(self, exception):
print(bcolors.ERROR + str(exception) + bcolors.ENDC)
self.release()
self.invalidate()
def get_lifetime(self):
return self._life
def invalidate(self):
print(bcolors.PRIMARY + "Disconnecting from NNTP connection id #%5d after %d seconds." %
(self.id, (time.time() - self._start_time)) + bcolors.ENDC)
self.close()
self._connected = False
self._life = -1
def release(self):
if self._pool is not None:
if self._connected:
self._pool.release_connection(self)
else:
self._pool = None
# NNTP proxy request handler for newznab
class NNTPProxyRequestHandler(SocketServer.StreamRequestHandler):
def handle(self):
with self.server.nntp_client_pool.connection() as nntp_client:
self.wfile.write("200 localhost NNRP Service Ready.\r\n")
for line in self.rfile:
data = line.strip()
if not data.startswith("POST"):
if data.startswith("GROUP"):
print(bcolors.ALTERNATE + "%5d %s" % (nntp_client.id, data))
else:
print(bcolors.HEADER + "%5d " % nntp_client.id) + (bcolors.PRIMARY + "%s" % data)
if data.startswith("XOVER"):
try:
rng = data.split(None, 1)[1]
rng = tuple(map(int, rng.split("-")))
xover_gen = nntp_client.xover_gen(rng)
self.wfile.write("224 data follows\r\n")
for entry in xover_gen:
self.wfile.write("\t".join(entry) + "\r\n")
self.wfile.write(".\r\n")
except Exception as ex:
print(bcolors.ERROR + str(ex.message) + bcolors.ENDC)
self.wfile.write("503 internal server error\r\n")
elif data.startswith("BODY"):
msgid = data.split(None, 1)[1]
try:
body = nntp_client.body(msgid)
self.wfile.write("222 %s\r\n" % msgid)
self.wfile.write(body.replace("\r\n.", "\r\n.."))
self.wfile.write(".\r\n")
except Exception as ex:
print(bcolors.ERROR + str(ex.message) + bcolors.ENDC)
self.wfile.write("430 no such article\r\n")
elif data.startswith("GROUP"):
try:
total, first, last, group = nntp_client.group(data.split(None, 1)[1])
self.wfile.write("211 %d %d %d %s\r\n" % (total, first, last, group))
except Exception as ex:
print(bcolors.ERROR + str(ex.message) + bcolors.ENDC)
self.wfile.write("411 no such news group\r\n")
elif data.startswith("LIST OVERVIEW.FMT"):
try:
fmt = nntp_client.list_overview_fmt()
self.wfile.write("215 Order of fields in overview database.\r\n")
fmt = "\r\n".join(["%s:%s" % (f[0], "full" if f[1] else "") for f in fmt]) + "\r\n"
self.wfile.write(fmt)
self.wfile.write(".\r\n")
except Exception as ex:
print(bcolors.ERROR + str(ex.message) + bcolors.ENDC)
self.wfile.write("503 internal server error\r\n")
elif data.startswith("HEAD"):
msgid = data.split(None, 1)[1]
try:
head = nntp_client.head(msgid)
self.wfile.write("221 %s\r\n" % msgid)
head = "\r\n".join([": ".join(item) for item in head.items()]) + "\r\n\r\n"
self.wfile.write(head)
self.wfile.write(".\r\n")
except Exception as ex:
print(bcolors.ERROR + str(ex.message) + bcolors.ENDC)
self.wfile.write("430 no such article\r\n")
elif data.startswith("ARTICLE"):
msgid = data.split(None, 1)[1]
try:
article = nntp_client.article(msgid, False)
# check no of return values for compatibility with pynntp<=0.8.3
if len(article) == 2:
articleno, head, body = 0, article[0], article[1]
else:
articleno, head, body = article
self.wfile.write("220 %d %s\r\n" % (articleno, msgid))
head = "\r\n".join([": ".join(item) for item in head.items()]) + "\r\n\r\n"
self.wfile.write(head)
self.wfile.write(body.replace("\r\n.", "\r\n.."))
self.wfile.write(".\r\n")
except Exception as ex:
print(bcolors.ERROR + str(ex.message) + bcolors.ENDC)
self.wfile.write("430 no such article\r\n")
elif data == "LIST":
try:
list_gen = nntp_client.list_gen()
self.wfile.write("215 list of newsgroups follows\r\n")
for entry in list_gen:
self.wfile.write("%s %d %d %s\r\n" % entry)
self.wfile.write(".\r\n")
except Exception as ex:
print(bcolors.ERROR + str(ex.message) + bcolors.ENDC)
self.wfile.write("503 internal server error\r\n")
elif data.startswith("LIST ACTIVE") and not data.startswith("LIST ACTIVE.TIMES"):
try:
pattern = data[11:].strip() or None
active_gen = nntp_client.list_active_gen(pattern)
self.wfile.write("215 list of newsgroups follows\r\n")
for entry in active_gen:
self.wfile.write("%s %d %d %s\r\n" % entry)
self.wfile.write(".\r\n")
self.wfile.write(str(e) + "\r\n")
except Exception as ex:
print(bcolors.ERROR + str(ex.message) + bcolors.ENDC)
self.wfile.write("503 internal server error\r\n")
elif data.startswith("AUTHINFO user") or data.startswith("AUTHINFO pass"):
self.wfile.write("281 Ok\r\n")
elif data.startswith("XFEATURE"):
self.wfile.write("290 feature enabled\r\n")
elif data.startswith("QUIT"):
self.wfile.write("205 Connection closing\r\n")
break
else:
self.wfile.write("500 What?\r\n")
# NNTP proxy server for newznab
class NNTPProxyServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
allow_reuse_address = True
def __init__(self, server_address, request_handler, nntp_client_pool_obj, bind_and_activate=True):
SocketServer.TCPServer.__init__(self, server_address, request_handler, bind_and_activate=bind_and_activate)
self.nntp_client_pool = nntp_client_pool_obj
if __name__ == "__main__":
import sys
try:
if len(sys.argv) == 1:
import os
pathname = os.path.abspath(os.path.dirname(sys.argv[0]))
with open(pathname+"/lib/nntpproxy.conf", "rb") as fd:
config = json.load(fd)
else:
with open(sys.argv[1], "rb") as fd:
config = json.load(fd)
except IndexError:
sys.stderr.write("Usage: %s configfile\n" % sys.argv[0])
sys.exit(1)
except IOError as e:
sys.stderr.write("Failed to open config file (%s)\n" % e)
sys.exit(1)
except ValueError as e:
sys.stderr.write("Failed to parse config file (%s)\n" % e)
sys.exit(1)
nntp_client_pool = socketpool.ConnectionPool(
NNTPClientConnector,
retry_max=3,
retry_delay=1,
timeout=-1,
max_lifetime=30000.,
max_size=int(config["pool"]["size"]),
options=config["usenet"]
)
proxy = NNTPProxyServer((config["proxy"]["host"], config["proxy"]["port"]),
NNTPProxyRequestHandler, nntp_client_pool)
remote = (config["usenet"]["host"], config["usenet"]["port"])
print(bcolors.PRIMARY +
"NNTP proxy server started on: %s:%d, using a maximum pool size of %d." %
(config["proxy"]["host"], config["proxy"]["port"], config["pool"]["size"]))
proxy.serve_forever()