# Twisted, the Framework of Your Internet # Copyright (C) 2002 Bryce "Zooko" O'Whielacronx # # This library is free software; you can redistribute it and/or # modify it under the terms of version 2.1 of the GNU Lesser General Public # License as published by the Free Software Foundation. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ Gnutella, v0.4 http://www9.limewire.com/developer/gnutella_protocol_0.4.pdf This module is incomplete. The "GnutellaTalker" class is complete. The "GnutellaPinger" and "GnutellaPonger" are complete and able to chat with one another. The "GnutellaRouter" and "GnutellaServent" classes are yet to be written. """ # system imports import random, re, string, struct, types # twisted import from twisted.internet import reactor from twisted.protocols.basic import LineReceiver from twisted.python import log true = 1 false = 0 DESCRIPTORLENGTH=16 CONNSTRINGRE=re.compile("^GNUTELLA CONNECT/([^\r\n]*)") CONNSTRING="GNUTELLA CONNECT/0.4" ACKSTRING="GNUTELLA OK" HEADERLENGTH=23 HEADERENCODING="<%dsBBBI" % DESCRIPTORLENGTH # descriptorId, payloadDescriptor, ttl, hops, payloadLength OURMAXPAYLOADLENGTH=640 * 2**10 PAYLOADLENGTHLENGTH=4 PAYLOADLENGTHOFFSET=HEADERLENGTH-PAYLOADLENGTHLENGTH PAYLOADENCODING="= 0) and (x < 256), os)) != 4: return false if string.join(map(str, os), '.') != s: return false return true PINGPD=0x00 PONGPD=0x01 PUSHPD=0x40 QUERYPD=0x80 QUERYHITPD=0x81 payloadDescriptor2Name = { 0x00: "Ping", 0x01: "Pong", 0x40: "Push", 0x80: "Query", 0x81: "QueryHit", } def popTrailingNulls(s): while s[-1] == '\x00': s = s[:-1] return s class GnutellaTalker(LineReceiver): """ This just speaks the Gnutella protocol and translates it into Python methods for higher-level services to program with. You probably want a higher-level class like GnutellaRouter or GnutellaServent. If you really want to use this class itself, then the way to use it is to subclass it and override the methods named {ping,pong,push,query,queryHit}Received(). One constraint that it imposes which is not specified in the Gnutella 0.4 spec is that payload lengths must be less than or equal to 640 KB. If the payload length is greater than that, GnutellaTalker closes the connection. """ # METHODS OF INTEREST TO CLIENTS (including subclasses) def __init__(self): self.initiator = false # True iff this instance initiated an outgoing TCP connection rather than being constructed to handle an incoming TCP connection. self.handshake = "start" # state transitions: "start" -> "initiatorsaidhello", "initiatorsaidhello" -> "completed" self.gotver = None self.prng = None # HashExpander("MYSECRETSEED") self.buf = '' def setInitiator(self): assert self.handshake == "start" assert self.initiator == false self.initiator = true def sendPing(self, ttl): """ Precondition: ttl must be > 0 and <= MAXUINT8.: (ttl > 0) and (ttl <= MAXUINT8): "ttl: %s" % str(ttl) """ assert (ttl > 0) and (ttl <= MAXUINT8), "ttl must be > 0 and <= MAXUINT8." + " -- " + "ttl: %s" % str(ttl) log.msg("%s.sendPing(%s)" % (str(self), str(ttl),)) self.sendDescriptor(self._nextDescriptorId(), PINGPD, ttl, "") def sendPong(self, ttl, descriptorId, host, port, numberOfFilesShared, kbShared): """ Precondition: ttl must be > 0 and <= MAXUINT8.: (ttl > 0) and (ttl <= MAXUINT8): "ttl: %s" % str(ttl) Precondition: descriptorId must be a string of length DESCRIPTORLENGTH.: (type(descriptorId) is types.StringType) and (len(descriptorId) == DESCRIPTORLENGTH): "descriptorId: %s :: %s" % (repr(descriptorId), str(type(descriptorId)),) Precondition: host must be a well-formed IPv4 address.: is_ipv4(host): "host: %s" % str(host) Precondition: port must be > 0 and <= MAXUINT16.: (port > 0) and (port <= MAXUINT16): "port: %s" % str(port) Precondition: numberOfFilesShared must be >= 0 and <= MAXUINT32.: (numberOfFilesShared >= 0) and (numberOfFilesShared <= MAXUINT32): "numberOfFilesShared: %s" % str(numberOfFilesShared) Precondition: kbShared must be >- 0 and <= MAXUINT32: (kbShared >= 0) and (kbShared <= MAXUINT32): "kbShared: %s" % str(kbShared) """ assert (ttl > 0) and (ttl <= MAXUINT8), "precondition failure: " + "ttl must be > 0 and <= MAXUINT8." + " -- " + "ttl: %s" % str(ttl) assert (type(descriptorId) is types.StringType) and (len(descriptorId) == DESCRIPTORLENGTH), "precondition failure: " + "descriptorId must be a string of length DESCRIPTORLENGTH." + " -- " + "descriptorId: %s :: %s" % (repr(descriptorId), str(type(descriptorId)),) assert is_ipv4(host), "precondition failure: " + "host must be a well-formed IPv4 address" + " -- " + "host: %s" % str(host) assert (port > 0) and (port <= MAXUINT16), "precondition failure: " + "port must be > 0 and <= MAXUINT16" + " -- " + "port: %s" % str(port) assert (numberOfFilesShared >= 0) and (numberOfFilesShared <= MAXUINT32), "precondition failure: " + "numberOfFilesShared must be >= 0 and <= MAXUINT32." + " -- " + "numberOfFilesShared: %s" % str(numberOfFilesShared) assert (kbShared >= 0) and (kbShared <= MAXUINT32), "precondition failure: " + "kbShared must be >- 0 and <= MAXUINT32" + " -- " + "kbShared: %s" % str(kbShared) log.msg("%s.sendPong(%s, %s, %s, %s, %s, %s)" % (str(self,), str(ttl), repr(descriptorId), str(host), str(port), str(numberOfFilesShared), str(kbShared),)) (ipA0, ipA1, ipA2, ipA3,) = map(int, string.split(host, '.')) self.sendDescriptor(descriptorId, PONGPD, ttl, struct.pack(PONGPAYLOADENCODING, port, ipA0, ipA1, ipA2, ipA3, numberOfFilesShared, kbShared)) # METHODS OF INTEREST TO SUBCLASSES def pingReceived(self, descriptorId, ttl, hops): """ Override this to handle ping messages. Precondition: descriptorId must be a string of length DESCRIPTORLENGTH.: (type(descriptorId) is types.StringType) and (len(descriptorId) == DESCRIPTORLENGTH): "descriptorId: %s :: %s" % (repr(descriptorId), str(type(descriptorId)),) """ assert (type(descriptorId) is types.StringType) and (len(descriptorId) == DESCRIPTORLENGTH), "precondition failure: " + "descriptorId must be a string of length DESCRIPTORLENGTH." + " -- " + "descriptorId: %s :: %s" % (repr(descriptorId), str(type(descriptorId)),) log.msg("%s.pingReceived(%s, %s, %s)" % (str(self), repr(descriptorId), str(ttl), str(hops),)) def pongReceived(self, descriptorId, ttl, hops, ipAddress, port, numberOfFilesShared, kbShared): """ Override this to handle pong messages. Precondition: descriptorId must be a string of length DESCRIPTORLENGTH.: (type(descriptorId) is types.StringType) and (len(descriptorId) == DESCRIPTORLENGTH): "descriptorId: %s :: %s" % (repr(descriptorId), str(type(descriptorId)),) @param ipAddress: a string representing an IPv4 address like this "140.184.83.37"; This is the representation that the Python Standard Library's socket.connect() expects. @param port: an integer port number @param numberOfFilesShared: a long @param kbShared: a long """ assert (type(descriptorId) is types.StringType) and (len(descriptorId) == DESCRIPTORLENGTH), "precondition failure: " + "descriptorId must be a string of length DESCRIPTORLENGTH." + " -- " + "descriptorId: %s :: %s" % (repr(descriptorId), str(type(descriptorId)),) log.msg("%s.pongReceived(%s, %s, %s, ipAddress=%s, port=%s, numberOfFilesShared=%s, kbShared=%s)" % (str(self), repr(descriptorId), str(ttl), str(hops), str(ipAddress), str(port), str(numberOfFilesShared), str(kbShared), )) def queryReceived(self, descriptorId, ttl, hops, searchCriteria, minimumSpeed): """ Override this to handle query messages. Precondition: descriptorId must be a string of length DESCRIPTORLENGTH.: (type(descriptorId) is types.StringType) and (len(descriptorId) == DESCRIPTORLENGTH): "descriptorId: %s :: %s" % (repr(descriptorId), str(type(descriptorId)),) @param searchCriteria: a string @param minimumSpeed: integer KB/s -- you are not supposed to respond to this query if you can't serve at least this fast """ assert (type(descriptorId) is types.StringType) and (len(descriptorId) == DESCRIPTORLENGTH), "precondition failure: " + "descriptorId must be a string of length DESCRIPTORLENGTH." + " -- " + "descriptorId: %s :: %s" % (repr(descriptorId), str(type(descriptorId)),) log.msg("%s.queryReceived(%s, %s, %s, searchCriteria=%s, minimumSpeed=%s" % (str(self), repr(descriptorId), str(ttl), str(hops), str(searchCriteria), str(minimumSpeed),)) def queryHitReceived(self, descriptorId, ttl, hops, ipAddress, port, resultSet, serventIdentifier, speed): """ Override this to handle query hit messages. Precondition: descriptorId must be a string of length DESCRIPTORLENGTH.: (type(descriptorId) is types.StringType) and (len(descriptorId) == DESCRIPTORLENGTH): "descriptorId: %s :: %s" % (repr(descriptorId), str(type(descriptorId)),) @param ipAddress: a string representing an IPv4 address like this "140.184.83.37"; This is the representation that the Python Standard Library's socket.connect() expects. @param port: an integer port number @param resultSet: a list of tuples of (fileIndex, fileSize, fileName,) where fileIndex is a long, fileSize (in bytes) is a long, and fileName is a string @param serventIdentifier: string of length 16 @param speed: integer KB/s claimed by the responding host """ assert (type(descriptorId) is types.StringType) and (len(descriptorId) == DESCRIPTORLENGTH), "precondition failure: " + "descriptorId must be a string of length DESCRIPTORLENGTH." + " -- " + "descriptorId: %s :: %s" % (repr(descriptorId), str(type(descriptorId)),) log.msg("%s.queryHitReceived(%s, %s, %s, ipAddress=%s, port=%s, resultSet=%s, serventIdentifier=%s, speed=%s" % (str(self), repr(descriptorId), str(ttl), str(hops), str(ipAddress), str(port), str(resultSet), str(serventIdentifier), str(speed),)) def pushReceived(descriptorId, ttl, hops, ipAddress, port, serventIdentifier, fileIndex): """ Override this to handle push messages. Precondition: descriptorId must be a string of length DESCRIPTORLENGTH.: (type(descriptorId) is types.StringType) and (len(descriptorId) == DESCRIPTORLENGTH): "descriptorId: %s :: %s" % (repr(descriptorId), str(type(descriptorId)),) @param ipAddress: a string representing an IPv4 address like this "140.184.83.37"; This is the representation that the Python Standard Library's socket.connect() expects. @param port: an integer port number @param serventIdentifier: string of length 16 @param fileIndex: a long """ assert (type(descriptorId) is types.StringType) and (len(descriptorId) == DESCRIPTORLENGTH), "precondition failure: " + "descriptorId must be a string of length DESCRIPTORLENGTH." + " -- " + "descriptorId: %s :: %s" % (repr(descriptorId), str(type(descriptorId)),) log.msg("%s.pushReceived(%s, %s, %s, ipAddress=%s, port=%s, serventIdentifier=%s, fileIndex=%s" % (str(self), repr(descriptorId), str(ttl), str(hops), str(ipAddress), str(port), str(serventIdentifier), str(fileIndex),)) # METHODS OF INTEREST TO THIS CLASS ONLY def _nextDescriptorId(self): return string.join(map(chr, map(random.randrange, [0]*DESCRIPTORLENGTH, [256]*DESCRIPTORLENGTH)), '') def sendDescriptor(self, descriptorId, payloadDescriptor, ttl, payload): """ Precondition: descriptorId must be a string of length DESCRIPTORLENGTH.: (type(descriptorId) is types.StringType) and (len(descriptorId) == DESCRIPTORLENGTH): "descriptorId: %s :: %s" % (repr(descriptorId), str(type(descriptorId)),) Precondition: payload must not be larger than MAXUINT32 bytes.: len(payload) <= MAXUINT32: "len(payload): %s" """ assert (type(descriptorId) is types.StringType) and (len(descriptorId) == DESCRIPTORLENGTH), "precondition failure: " + "descriptorId must be a string of length DESCRIPTORLENGTH." + " -- " + "descriptorId: %s :: %s" % (repr(descriptorId), str(type(descriptorId)),) assert len(payload) <= MAXUINT32, "precondition failure: " + "payload must not be larger than MAXUINT32 bytes." + " -- " + "len(payload): %s" self.transport.write(struct.pack(HEADERENCODING, descriptorId, payloadDescriptor, ttl, 0, len(payload))) self.transport.write(payload) def connectionMade(self): log.msg("%s.connectionMade(); host: %s, peer: %s" % (str(self), str(self.transport.getHost()), str(self.transport.getPeer()),)) self.userapp.setHost(self.transport.getHost()) if self.initiator: log.msg("sending %s" % CONNSTRING) self.sendLine(CONNSTRING) self.handshake = "initiatorsaidhello" def _abortConnection(self, logmsg): log.msg(logmsg + ", self: %s" % str(self)) self.transport.loseConnection() return def handlePing(self, descriptorId, ttl, hops, payload): """ A ping message has arrived. """ if payload != '': self._abortConnection("Received non-empty Ping payload. Closing connection. payload: %s" % str(payload)) return self.pingReceived(descriptorId, ttl, hops) def handlePong(self, descriptorId, ttl, hops, payload): try: (port, ipA0, ipA1, ipA2, ipA3, numberOfFilesShared, kbShared,) = struct.unpack(PONGPAYLOADENCODING, payload) except struct.error, le: self._abortConnection("Received ill-formatted Pong payload. Closing connection. payload: %s, le: %s" % (str(payload), str(le),)) return ipAddress = string.join(map(str, (ipA0, ipA1, ipA2, ipA3,)), '.') self.pongReceived(descriptorId, ttl, hops, ipAddress, port, numberOfFilesShared, kbShared) def handleQuery(self, descriptorId, ttl, hops, payload): try: (minimumSpeed,) = struct.unpack("= HEADERLENGTH: try: (payloadLength,) = struct.unpack(PAYLOADENCODING, self.buf[PAYLOADLENGTHOFFSET:HEADERLENGTH]) except struct.error, le: self._abortConnection("Received ill-formatted raw data. Closing connection. self.buf: %s" % str(self.buf)) return if (payloadLength > OURMAXPAYLOADLENGTH) or (payloadLength < 0): # 640 KB ought to be enough for anybody... self._abortConnection("Received payload > %d KB or < than 0 in size. Closing connection. payloadLength: %s" % ((OURMAXPAYLOADLENGTH / 2**10), str(payloadLength),)) return descriptorlength = HEADERLENGTH + payloadLength if len(self.buf) >= descriptorlength: descriptor, self.buf = self.buf[:descriptorlength], self.buf[descriptorlength:] self.descriptorReceived(descriptor) class GnutellaPinger(GnutellaTalker): """ Just for testing. It does nothing but send PINGs. """ def __init__(self): GnutellaTalker.__init__(self) self.initiator = true def connectionMade(self): GnutellaTalker.connectionMade(self) self.loopAndSendPing() def loopAndSendPing(self): GnutellaTalker.sendPing(self, ttl=4) reactor.callLater(4, self.loopAndSendPing) class GnutellaPonger(GnutellaTalker): """ Just for testing. It does nothing but PONG your PINGs. """ def __init__(self): GnutellaTalker.__init__(self) def pingReceived(self, descriptorId, ttl, hops): GnutellaTalker.pingReceived(self, descriptorId, ttl, hops) self.sendPong(ttl=hops+1, descriptorId=descriptorId, host=self.userapp.getHost()[1], port=self.userapp.getHost()[2], numberOfFilesShared=0, kbShared=0) class GnutellaRouter(GnutellaTalker): """ This is a well-behaved Gnutella servent that routes messages as it should. It does not, however, serve any actual files. If you want to run a Gnutella servent that serves files, try the GnutellaServent class. If you want to use GnutellaRouter for something, subclass it and override the methods named {ping,pong,push,query,queryHit}Received(). But please remember that you have to call GnutellaRouter's `pingReceived()' from your overridden `pingReceived()' if you want it to route the ping! """ def __init__(self): GnutellaTalker.__init__(self) ### XXXX incomplete. Zooko stopped here to go to bed after the first night of hacking this file. --Zooko 2002-07-15