Updated for XXXs down to unfinished reference code

-- Azaroth
This commit is contained in:
Rob Sanderson 2004-07-27 22:38:37 +00:00
parent 32ba3b10b1
commit afbeb4d582

View File

@ -1,8 +1,13 @@
#!/usr/bin/env python #!/usr/bin/env python
# You need a copyright notice here, azaroth.
# You might do well to put links to the documentation you're basing this on # Copyright (c) Robert Sanderson All rights reserved.
# here. Then other people can help maintain it more easily. # See LICENCE file in this distribution for details.
# Documentation on DCC specification
# http://www.mirc.co.uk/help/dccresum.txt
# www.irchelp.org/irchelp/rfc/dccspec.html
# Also several other lesser known/implemented/used commands are floating around
# but not implemented in major clients.
import os import os
import time import time
@ -18,47 +23,48 @@ import ircutils
import threading import threading
def isDCC(msg): def isDCC(msg):
# This needs to be more specific, it would catch too many messages return msg.command == 'PRIVMSG' and msg.args[1][:5] == '\x01DCC '
# discussing DCC stuff. I added a test for this.
return msg.command == 'PRIVMSG' and msg.args[1][1:4] == 'DCC'
conf.registerGlobalValue(conf.supybot.protocols.dcc, 'timeout',
registry.Integer(120, "Timeout on DCC sockets"))
# ---- Out Handlers ---- # ---- Out Handlers ----
class DCCHandler: class DCCHandler:
# You should reconsider whether all these arguments are necessary in the
# constructor. Long argument lists generally mean bad factoring.
def __init__(self, irc, nick, hostmask, logger=None): def __init__(self, irc, nick, hostmask, logger=None):
self.irc = irc self.irc = irc
self.nick = nick self.nick = nick
self.hostmask = hostmask self.hostmask = hostmask
self.sock = None self.sock = None
# XXX () in if/while. if logger:
if (logger):
# XXX Log something specific
self.log = logger self.log = logger
else: else:
self.log = log self.log = log
# XXX Why 120? Shouldn't that be configurable?
self.timeout = 120 self.timeout = conf.supybot.protocols.dcc.timeout()
def start(self): def start(self):
# What's this self.open? I don't see an open method.
t = threading.Thread(target=self.open) t = threading.Thread(target=self.open)
world.threadsSpawned += 1 world.threadsSpawned += 1
t.setDaemon(True) t.setDaemon(True)
t.start() t.start()
def open(self):
# Override in subclasses to do something
pass
def clientConnected(self): def clientConnected(self):
# Override in subclasses to do something when a client connects
pass pass
def clientClosed(self): def clientClosed(self):
# Override to do something when a client closes connection
pass pass
class SendHandler(DCCHandler): class SendHandler(DCCHandler):
""" Handle sending a file """ """ Handle sending a file """
# What I said about long argument lists in DCCHandler applies doubly here.
def __init__(self, irc, nick, hostmask, filename, logger=None, start=0): def __init__(self, irc, nick, hostmask, filename, logger=None, start=0):
DCCHandler.__init__(self, irc, nick, hostmask, logger) DCCHandler.__init__(self, irc, nick, hostmask, logger)
self.filename = filename self.filename = filename
@ -76,167 +82,122 @@ class SendHandler(DCCHandler):
def open(self): def open(self):
ip = conf.supybot.externalIP() ip = conf.supybot.externalIP()
host = ircutils.hostFromHostmask(self.irc.prefix)
if not ip:
try:
ip = socket.gethostbyname(host)
except: # XXX Don't use blank excepts.
# XXX Be sure you mention who you are and why you couldn't do
# what you should.
self.log.warning('Could not determine IP address')
return
try: try:
self.filesize = os.path.getsize(self.filename) self.filesize = os.path.getsize(self.filename)
except OSError, e: except OSError, e:
# XXX: There should be a log.warning or log.error here. self.log.warning('Requested file does not exist: %r',
# Doesn't exist self.filename)
# ^^^^^^^^^^^^^ What doesn't exist?
return return
sock = utils.getSocket(ip) sock = utils.getSocket(ip)
try: try:
sock.bind((ip, 0)) sock.bind((ip, 0))
except socket.error, e: except socket.error, e:
# XXX: There should be a log.warning or log.error here. self.log.warning('Could not bind a socket to send.')
return return
port = sock.getsockname()[1] port = sock.getsockname()[1]
self.port = port self.port = port
self.registerPort() self.registerPort()
i = ircutils.dccIP(ip) i = ircutils.dccIP(ip)
sock.listen(1) sock.listen(1)
## self.irc.queueMsg(ircmsgs.privmsg(self.nick,
## '\x01DCC SEND %s %s %d %d\x01' % (self.filename, i, port,
## self.filesize)))
## That formatting is bad. Try this instead:
## msg = ircmsgs.privmsg(self.nick,
## '\x01DCC SEND %s %s %s %s\x01' % \
## (self.filename, i, port, self.filesize))
## self.irc.queueMsg(msg)
## Even better, I'll define a function ircmsgs.py.
msg = ircmsgs.dcc(self.nick, 'SEND', self.filename, i, port, self.filesize) msg = ircmsgs.dcc(self.nick, 'SEND', self.filename, i, port, self.filesize)
self.irc.queueMsg(msg) self.irc.queueMsg(msg)
# Wait for possible RESUME
# That comment isn't enough, why are we really sleeping? Why only one # Wait for possible RESUME request to be handled which may change
# second? A link to some documentation might help. # self.startPosition
# See Resume doc (URL in header)
time.sleep(1) time.sleep(1)
try: try:
(realSock, addr) = sock.accept() (realSock, addr) = sock.accept()
except socket.error, e: except socket.error, e:
sock.close() sock.close()
# Remember, we dont' use % in log strings -- we just put the
# arguments there after the string. utils.exnToString(e) gives a
# prettier string form than just e; the latter doesn't show what the
# class of the exception is.
# XXX % in log.
self.log.info('%r: Send init errored with %s', self.log.info('%r: Send init errored with %s',
self.filename, utils.exnToString(e)) self.filename, utils.exnToString(e))
return return
# XXX % in log. self.log.info('%r: Sending to %s', self.filename, self.nick)
self.log.info('%r: Sending to %s' % (self.filename, self.nick))
# There shouldn't be blank space like this in a single function. But
# then again, a single function shouldn't be this long. Perhaps you
# can break this into multiple functions?
self.sock = realSock self.sock = realSock
fh = file(self.filename) fh = file(self.filename)
# Why no arguments to this clientConnected method?
self.clientConnected()
self.startTime = time.time()
try: try:
slow = 0 self.clientConnected()
fh.seek(self.startPosition) self.startTime = time.time()
self.currentPosition = fh.tell() packetSize = conf.supybot.protocols.dcc.packetSize()
# Again, please no parentheses around the conditions of while loops try:
# and if statements. slow = 0
# XXX () in while/if fh.seek(self.startPosition)
while (self.currentPosition < self.filesize):
data = fh.read(min(1024, self.filesize-self.currentPosition))
self.sock.send(data)
self.currentPosition = fh.tell() self.currentPosition = fh.tell()
self.bytesSent += 1024 while self.currentPosition < self.filesize:
self.packetSent() data = fh.read(min(packetSize, self.filesize - \
self.currentPosition))
self.sock.send(data)
self.currentPosition = fh.tell()
self.bytesSent += 1024
self.packetSent()
except socket.error, e:
exn = utils.exnToString(e)
self.log.info('%r: Send errored with %s', self.filename, exn)
except SendException, e:
exn = utils.exnToString(e)
self.log.info('%r: Send aborted with %s', self.filename, exn)
except socket.error, e: self.endTime = time.time()
# Aborted/timedout chat duration = self.endTime - self.startTime
# XXX % in log. self.log.info('%r: Sent %s/%s', self.filename, fh.tell(),
self.log.info('%r: Send errored with %s' % self.filesize)
(self.filename, e)) # Sleep to allow client to finish reading data.
except SendException, e: # This is needed as we'll be here immediately after the final
# XXX % in log. # packet
self.log.info('%r: Send aborted with %s' % time.sleep(1.0)
(self.filename, e)) self.clientClosed()
# Wait for send to complete finally:
self.endTime = time.time() # Ensure that we're not leaking handles
duration = self.endTime - self.startTime fh.close()
# XXX. % in log. Also use %r here instead of %s. self.sock.close()
self.log.info('\'%s\': Sent %s/%s' %
(self.filename, fh.tell(), self.filesize))
# Why sleep here?
time.sleep(1.0)
# XXX These closes should go in a finally: block.
fh.close()
self.sock.close()
self.clientClosed()
class ChatHandler(DCCHandler): class ChatHandler(DCCHandler):
""" Handle a DCC chat (initiate) """ """ Handle a DCC chat (initiate) """
def handleLine(self, line): def lineReceived(self, line):
# What's this for? Documentation! # Override in subclasses to process a line of text
pass pass
def open(self): def open(self):
ip = conf.supybot.externalIP() ip = conf.supybot.externalIP()
# XXX () in while/if
if (not ip):
# Try and find it with utils
# No, I'll fix externalIP to return something sane for you.
pass
sock = utils.getSocket(ip) sock = utils.getSocket(ip)
lineLength = conf.supybot.protocols.dcc.chatLineLength()
try: try:
sock.bind((ip, 0)) sock.bind((ip, 0))
except socket.error, e: except socket.error, e:
# XXX Who are you, and why can't you? self.log.error('Could not bind chat socket.')
self.irc.error('Unable to initiate DCC CHAT.')
return return
port = sock.getsockname()[1] port = sock.getsockname()[1]
i = ircutils.dccIP(ip) i = ircutils.dccIP(ip)
sock.listen(1) sock.listen(1)
# XXX Use ircmsgs.dcc msg = ircmsgs.dcc(self.nick, 'CHAT', 'chat', i, port)
self.irc.queueMsg(ircmsgs.privmsg(self.nick, self.irc.queueMsg(msg)
'\x01DCC CHAT chat %s %s\x01' % (i, port)))
try: try:
(realSock, addr) = sock.accept() (realSock, addr) = sock.accept()
except socket.timeout: except socket.timeout:
# XXX % in log sock.close()
self.log.info('CHAT to %s timed out' % self.nick) self.log.info('CHAT to %s timed out', self.nick)
return return
# XXX % in log. self.log.info('CHAT accepted from %s', self.nick)
self.log.info('CHAT accepted from %s' % self.nick)
realSock.settimeout(self.timeout) realSock.settimeout(self.timeout)
self.startTime = time.time() self.startTime = time.time()
self.sock = realSock self.sock = realSock
try: try:
self.clientConnected() self.clientConnected()
# XXX () in while/if while 1:
while (1): line = realSock.recv(lineLength)
# XXX Why 66000? if line != "\n":
line = realSock.recv(66000) self.lineReceived(line)
# XXX Don't use <>; use !=.
if (line <> "\n"):
self.handleLine(line)
except socket.error, e: except socket.error, e:
# Aborted/timedout chat. Only way to end. self.log.info('CHAT ended with %s', self.nick)
# Are you sure you don't still need to close the socket here? finally:
# XXX % in log. Also, don't use parens around a single value. self.sock.close()
self.log.info('CHAT ended with %s' % (self.nick)) self.endTime = time.time()
self.endTime = time.time() self.clientClosed()
self.clientClosed()
# ---- In Handlers ---- # ---- In Handlers ----
@ -247,8 +208,7 @@ class DCCReqHandler:
self.irc = irc self.irc = irc
self.msg = msg self.msg = msg
self.args = args self.args = args
# XXX: () in while/if if logger:
if (logger):
self.log = logger self.log = logger
else: else:
self.log = log self.log = log
@ -259,112 +219,89 @@ class DCCReqHandler:
t.setDaemon(True) t.setDaemon(True)
t.start() t.start()
# Always put a blank line between methods, even if they just pass.
def clientConnected(self): def clientConnected(self):
pass pass
# XXX No blank line between methods.
def clientClosed(self): def clientClosed(self):
pass pass
class SendReqHandler(DCCReqHandler): class SendReqHandler(DCCReqHandler):
""" We're being sent a file """ """ We're being sent a file """
# These should be setup in __init__ unless they have a real reason for
# being class variables.
ip = ""
filename = ""
port = 0
filesize = 0
def __init__(self, *args, **kw): def __init__(self, *args, **kw):
DCCReqHandler.__init__(self, *args, **kw) DCCReqHandler.__init__(self, *args, **kw)
# This should be added to by subclasses # This should be added to by subclasses
self.incomingDir = conf.supybot.directories.data() self.incomingDir = conf.supybot.directories.data()
def receivedPacket(self):
# What's this method for? Document.
pass
def open(self):
# XXX: Should this be factored into a separate function?
self.filename = self.args[0] self.filename = self.args[0]
self.ip = ircutils.unDccIP(int(self.args[1])) self.ip = ircutils.unDccIP(int(self.args[1]))
self.port = int(self.args[2]) self.port = int(self.args[2])
self.filesize = int(self.args[3]) self.filesize = int(self.args[3])
# XXX () in while/if
def receivedPacket(self):
# Override in subclass to do something with each packet received
pass
def open(self):
if (os.path.exists(self.filename)): if (os.path.exists(self.filename)):
currsize = os.path.getsize(self.filename) currsize = os.path.getsize(self.filename)
if (self.filesize > currsize): if (self.filesize > currsize):
# Send RESUME DCC message and wait for ACCEPT # Send RESUME DCC message and wait for ACCEPT
# XXX Line too long. msg = ircutils.dcc(self.nick, 'RESUME', self.filename,
self.irc.queueMsg(ircmsgs.privmsg(self.msg.nick, self.port, currsize)
'\x01DCC RESUME %s %d %d\x01' % (self.filename, self.port, currsize))) self.irc.queueMsg(msg)
# URHG?
# What does "URGH?" mean? Details!
return return
sock = utils.getSocket(self.ip) sock = utils.getSocket(self.ip)
try: try:
sock.connect((self.ip, self.port)) sock.connect((self.ip, self.port))
except: except socket.error, e:
# XXX blank except, no log. self.log.info('File receive could not connect')
return return
self.clientConnected() self.clientConnected()
rootedName = os.path.abspath(os.path.join(self.incomingDir,
# XXX long line; also, what does "rootedName" really mean? self.filename))
rootedName = os.path.abspath(os.path.join(self.incomingDir, self.filename)) if not rootedName.startswith(self.incomingDir):
# XXX Use the startswith() method on strings. self.log.warning('%s tried to send relative file', self.msg.nick)
if (rootedName[:len(self.incomingDir)] != self.incomingDir):
# XXX % in log.
self.log.warning('%s tried to send relative file' % self.msg.nick)
return return
# XXX f is generally used for functions; use fh, or fd, or a name fh = file(rootedName, 'w')
# representative what file is being used.
f = file(rootedName, 'w')
self.bytesReceived = 0 self.bytesReceived = 0
self.startTime = time.time() self.startTime = time.time()
# XXX This is a src/ plugin. It shouldn't depend on any plugin. You pktSize = conf.supybot.protocols.dcc.packetSize()
# definitely can't use any plugin's registry variables. This is the self.log.info('%r: Send starting from %s', self.filename,
# number 1 biggest problem in this file. self.msg.nick))
pktSize = conf.supybot.plugins.FServe.packetSize()
# XXX % in log, use %r
self.log.info('\'%s\': Send starting from %s' %
(self.filename, self.msg.nick))
try: try:
# XXX () in while/if while self.bytesReceived < self.filesize:
while (self.bytesReceived < self.filesize):
amnt = min(self.filesize - self.bytesReceived, pktSize) amnt = min(self.filesize - self.bytesReceived, pktSize)
d = sock.recv(amnt) d = sock.recv(amnt)
self.bytesReceived += len(d) self.bytesReceived += len(d)
# XXX What's this do? Document please :) # Required to send back packed integer to acknowledge receive
sock.send(struct.pack("!I", self.bytesReceived)) sock.send(struct.pack("!I", self.bytesReceived))
f.write(d) f.write(d)
self.receivedPacket() self.receivedPacket()
except socket.error, e: except socket.error, e:
# % in log, use %r. exn = utils.exnToString(e)
self.log.info('\'%s\': Send died with %s' % (filename, e)) self.log.info('%r: Send died with %s', filename, exn)
# XXX If you intend to fall through, i.e., not to return here, you finally:
# should have a comment to that effect. self.endTime = time.time()
self.endTime = time.time() sock.close()
# XXX Perhaps these closes should go in a finally: block? f.close()
sock.close() self.log.info('%r: Received %s/%s in %d seconds',
f.close() self.filename, self.bytesReceived, self.filesize,
# XXX % in log, use %r. self.endTime - self.startTime)
self.log.info('\'%s\': Received %s/%s in %d seconds' % self.clientClosed()
(self.filename, self.bytesReceived, self.filesize,
self.endTime - self.startTime))
self.clientClosed()
class ResumeReqHandler(DCCReqHandler): class ResumeReqHandler(DCCReqHandler):
def _getSendHandler(self): def _getSendHandler(self):
# XXX Explain this comment more. # This will work in theory, BUT note well, if you instantiate
# This is bad. It will just start a new send. # and do not override this to return the REAL SendHandler
# It needs to look up original # the client may still get the original startPosition of 0
# See RESUME documentation URL in header
hostmask = self.irc.state.nickToHostmask(self.msg.nick) hostmask = self.irc.state.nickToHostmask(self.msg.nick)
h = SendHandler(self.irc, self.msg.nick, hostmask, self.filename, h = SendHandler(self.irc, self.msg.nick, hostmask, self.filename,
start=self.startPosition) start=self.startPosition)
@ -372,22 +309,20 @@ class ResumeReqHandler(DCCReqHandler):
def open(self): def open(self):
# filename is (apparently) ignored by mIRC # filename is (apparently) ignored by mIRC
# so don't depend on it.
self.filename = self.args[0] self.filename = self.args[0]
self.port = int(self.args[1]) self.port = int(self.args[1])
self.startPosition = int(self.args[2]) self.startPosition = int(self.args[2])
# XXX Use ircmsgs.dcc.
self.irc.queueMsg(ircmsgs.privmsg(self.msg.nick,
'\x01DCC ACCEPT %s %s %s\x01' % (self.filename, self.port,
self.startPosition)))
msg = ircutils.dcc(self.msg.nick, "ACCEPT", self.filename, self.port,
self.startPosition)
self.irc.queueMsg(msg)
cxn = self._getSendHandler() cxn = self._getSendHandler()
cxn.startPosition = self.startPosition cxn.startPosition = self.startPosition
# XXX % in log. self.log.info('%r: RESUME received for %s', self.filename,
self.log.info('%r: RESUME received for %s' % self.startPosition)
(self.filename, self.startPosition))
# --- IGNORE FROM HERE DOWN ---
def handleACCEPT(self): def handleACCEPT(self):
port = int(self.args[1]) port = int(self.args[1])