SickGear/lib/adba/aniDBlink.py
2023-02-09 13:41:15 +00:00

238 lines
7.9 KiB
Python

#!/usr/bin/env python
# coding=utf-8
#
# This file is part of aDBa.
#
# aDBa is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# aDBa is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with aDBa. If not, see <http://www.gnu.org/licenses/>.
import logging
import socket
import sys
import threading
import zlib
from time import time, sleep
from .aniDBerrors import *
from .aniDBresponses import ResponseResolver
logger = logging.getLogger('adba')
logger.addHandler(logging.NullHandler())
class AniDBLink(threading.Thread):
def __init__(self, server, port, myport, delay=2, timeout=20):
super(AniDBLink, self).__init__()
self.server = server
self.port = port
self.target = (server, port)
self.timeout = timeout
self.myport = 0
self.bound = self.connectSocket(myport, self.timeout)
self.cmd_queue = {None: None}
self.resp_tagged_queue = {}
self.resp_untagged_queue = []
self.tags = []
self.lastpacket = time()
self.delay = delay
self.session = None
self.banned = False
self.crypt = None
self._stop = threading.Event()
self._quiting = False
self.QuitProcessed = False
self.daemon = True
self.start()
def connectSocket(self, myport, timeout):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.sock.settimeout(timeout)
portlist = [myport] + [7654]
for port in portlist:
try:
self.sock.bind(('', port))
except:
continue
else:
self.myport = port
return True
else:
return False
def disconnectSocket(self):
self.sock.shutdown(socket.SHUT_RD)
# close is not called as the garbage collection from python will handle this for us. Calling close can also cause issues with the threaded code.
# self.sock.close()
def stop(self):
logger.info("Releasing socket and stopping link thread")
self._quiting = True
self.disconnectSocket()
self._stop.set()
def stopped(self):
return self._stop.isSet()
def print_log_dummy(self, data):
pass
def run(self):
while not self._quiting:
try:
data = self.sock.recv(8192)
except socket.timeout:
self._handle_timeouts()
continue
except OSError as error:
logger.exception('Exception: %s' % error)
break
logger.debug("NetIO < %r" % data)
try:
for i in range(2):
try:
tmp = data
resp = None
if tmp[:2] == b'\x00\x00':
tmp = zlib.decompressobj().decompress(tmp[2:])
logger.debug("UnZip | %r" % tmp)
resp = ResponseResolver(tmp)
except Exception as e:
logger.exception('Exception: %s' % e)
sys.excepthook(*sys.exc_info())
self.crypt = None
self.session = None
else:
break
if not resp:
raise AniDBPacketCorruptedError("Either decrypting, decompressing or parsing the packet failed")
cmd = self._cmd_dequeue(resp)
resp = resp.resolve(cmd)
resp.parse()
if resp.rescode in ('200', '201'):
self.session = resp.attrs['sesskey']
if resp.rescode in ('209',):
logger.error("sorry encryption is not supported")
raise AniDBError()
# self.crypt=aes(md5(resp.req.apipassword+resp.attrs['salt']).digest())
if resp.rescode in ('203', '403', '500', '501', '503', '506'):
self.session = None
self.crypt = None
if resp.rescode in ('504', '555'):
self.banned = True
logger.critical(("AniDB API informs that user or client is banned:", resp.resstr))
resp.handle()
if not cmd or not cmd.mode:
self._resp_queue(resp)
else:
self.tags.remove(resp.restag)
except:
sys.excepthook(*sys.exc_info())
logger.error("Avoiding flood by paranoidly panicing: Aborting link thread, killing connection, releasing waiters and quiting")
self.sock.close()
try:
cmd.waiter.release()
except:
pass
for tag, cmd in self.cmd_queue.items():
try:
cmd.waiter.release()
except:
pass
sys.exit()
if self._quiting:
self.QuitProcessed = True
def _handle_timeouts(self):
willpop = []
for tag, cmd in self.cmd_queue.items():
if not tag:
continue
if time() - cmd.started > self.timeout:
self.tags.remove(cmd.tag)
willpop.append(cmd.tag)
cmd.waiter.release()
for tag in willpop:
self.cmd_queue.pop(tag)
def _resp_queue(self, response):
if response.restag:
self.resp_tagged_queue[response.restag] = response
else:
self.resp_untagged_queue.append(response)
def getresponse(self, command):
if command:
resp = self.resp_tagged_queue.pop(command.tag)
else:
resp = self.resp_untagged_queue.pop()
self.tags.remove(resp.restag)
return resp
def _cmd_queue(self, command):
self.cmd_queue[command.tag] = command
self.tags.append(command.tag)
def _cmd_dequeue(self, resp):
if not resp.restag:
return None
else:
return self.cmd_queue.pop(resp.restag)
def _delay(self):
return self.delay < 2.1 and 2.1 or self.delay
def _do_delay(self):
age = time() - self.lastpacket
delay = self._delay()
if age <= delay:
sleep(delay - age)
def _send(self, command):
if self.banned:
logger.debug("NetIO | BANNED")
raise AniDBBannedError("Not sending, banned")
self._do_delay()
self.lastpacket = time()
command.started = time()
data = command.raw_data()
# Encode data to bytes if needed
try:
data = data.encode("ASCII")
except AttributeError: # On Python 3: 'bytes' object has no attribute 'encode'
pass
self.sock.sendto(data, self.target)
if command.command == 'AUTH':
logger.debug("NetIO > sensitive data is not logged!")
def new_tag(self):
if not len(self.tags):
maxtag = "T000"
else:
maxtag = max(self.tags)
newtag = "T%03d" % (int(maxtag[1:]) + 1)
return newtag
def request(self, command):
if not (self.session and command.session) and command.command not in ('AUTH', 'PING', 'ENCRYPT'):
raise AniDBMustAuthError("You must be authed to execute commands besides AUTH and PING")
command.started = time()
self._cmd_queue(command)
self._send(command)