Developers.Code.CShakeNeighbour

From Shareaza Wiki
Jump to navigation Jump to search

CShakeNeighbour

CShakeNeighbour inherts from CNeighbour, which in turn inherits from CConnection. From CConnection, it gets a socket and methods to transfer data through it. From CNeighbour, it gets a long list of member variables that explain what the remote computer is and what it is doing. CShakeNeighbour is a long class, and contains a lot of text parsing code. It reads handshake headers from the remote computer, parses them, understands them, and sends its own.

Constructor and Destructor

The CShakeNeighbour constructor is defined like this

<source lang="c"> CShakeNeighbour::CShakeNeighbour() : CNeighbour( PROTOCOL_NULL ) </source>

Since CShakeNeighbour inherits from CNeighbour, the computer automatically runs the CNeighbour constructor before running this one. But, the CNeighbour constructor needs a protocol ID. The notation above shows how we pass it one. We're passing it PROTOCOL_NULL, which means we want a new CNeighbour object not customized for one particular protocol.

Member variables like m_bSentAddress and m_bG2Send start out FALSE. When we've sent or recieved a certain header, we'll set them to true. This lets the program see if a header has been exchanged without having to search text each time.

The destructor doesn't do anything. The member variabes that CShakeNeighbour adds on top of CNeighbour and CConnection don't need special code to be put away.

ConnectTo

This method takes an IP address and port number, and connects the socket to that remote computer. It then calls CNeighbours.Add(this) to add this newly connected CShakeNeighbour object to the list of them.

AttachTo

AttachTo takes a pointer to a CConnection object. It copies the contents of that object into this one. It then makes the handshake state nrsHandshake1, which means we're awating response headers from the remote computer.

Close

This method gets called when the socket connection is dropped. If we initiated the connection and didn't get a single byte from it, Close calls HostCache.OnFailure to report we were unable to make this IP address work.

OnConnected

CConnection::DoRun calls OnConnected when it has just opened the socket connection to the remote computer. This method greets the remote computer with these headers:

<source lang="c"> GNUTELLA CONNECT/0.6 User-Agent: Shareaza 2.1.0.97 Listen-IP: 67.176.34.172:6346 Remote-IP: 81.103.192.245 Accept: application/x-gnutella2,application/x-gnutella-packets Accept-Encoding: deflate GGEP: 0.5 Pong-Caching: 0.1 Vendor-Message: 0.1 X-Query-Routing: 0.1 X-Ultrapeer: False X-Try-Ultrapeers: 24.98.97.155:6348 2004-12-18T23:47Z \r\n </source>

Each line of text ends with the two bytes \r\n. The final line is just \r\n, and ends the whole group of headers. OnConnected then sets the node state to nrsHandshake1, which means we are now waiting for a response from the remote computer.

OnDropped

CConnection::DoRun calls OnDropped when a socket connection has been refused or lost. The method calls Close.

OnRead

OnRead is the method in the middle of how Shareaza negotiates the handshake. Here's how it works.

  1. CNeighboursBase::OnRun loops through the neighbours and calls pNeighbour->DoRun() on each.
  2. This calls CConnection::DoRun. It makes sure the socket is still open, writes data to it, and then calls OnRead.
  3. This calls CShakeNeighbour::OnRead, which calls CShakeNeighbour::ReadResponse or CConnection::ReadHeaders.
    1. ReadResponse gets called if the handshake state is nrsHandshake1.
    2. ReadHeaders gets called if the handshake state is nrsHandshake2, nrsHandshake3, or nrsRejected.

If ReadResponse or ReadHeaders change the handshake state, this method can call them more than once. If ReadResponse or ReadHeaders return FALSE, the method returns FALSE to its caller.

OnRun

CConnection::DoRun calls CShakeNeighbour::OnRun. It just makes sure the handshake hasn't been taking too long. If too much time has passed, it returns FALSE to stop talking to the remote computer.

SendMinimalHeaders

Sends headers like this:

<source lang="c"> User-Agent: Shareaza 1.2.3.4 Accept: application/x-gnutella2,application/x-gnutella-packets </source>

This header tells the remote computer we are Shareaza:

User-Agent: Shareaza 1.2.3.4

Then, if we are connecting to Gnutella2 today and we initiated the connection, we tell the remote computer we accept Gnutella2 packets:

Accept: application/x-gnutella2,application/x-gnutella-packets

If they connected to us with a header like this one, we respond to it with these two:

Accept: application/x-gnutella2 Content-Type: application/x-gnutella2

Which mean that we accept Gnutella2 packets also, and will be sending them.

SendPublicHeaders

Sends headers like:

<source lang="c"> User-Agent: Shareaza 2.1.0.97 Listen-IP: 67.176.34.172:6346 Remote-IP: 81.103.192.245 Accept: application/x-gnutella2,application/x-gnutella-packets Accept-Encoding: deflate GGEP: 0.5 Pong-Caching: 0.1 Vendor-Message: 0.1 X-Query-Routing: 0.1 X-Ultrapeer: False </source>

Just like SendMinimalHeaders, this method starts out telling the remote computer we are Shareaza:

User-Agent: Shareaza 1.2.3.4

Then, it tells the remote computer the Internet IP address and port number we have a socket listening on:

Listen-IP: 1.2.3.4:5

If this header is sent, m_bSentAddress gets set to true. But, we try to send it here regardless of whether m_bSentAddress is already true. Next, we tell the remote computer what IP address it seems to have from here:

Remote-IP: 1.2.3.4

We get the IP address from m_pHost.sin_addr. After that, we setup Gnutella2 communications. If we initiated the connection to the remote computer, we tell it we accept Gnutella2 packets:

Accept: application/x-gnutella2,application/x-gnutella-packets

If they connected to us, m_bInitiated is FALSE, and if they connected to us with a header like that one above, m_bG2Accept is TRUE. We reply that we also accept Gnutella2 packets, and will be sending them.

Accept: application/x-gnutella2 Content-Type: application/x-gnutella2

In Shareaza Settings, there are checkboxes where the user can allow Shareaza to exchange compressed information with remote computers. If these checkboxes are checked, m_bCanDeflate is TRUE, and we tell the remote computer:

Accept-Encoding: deflate

This means we can decompress the compressed data the remote computer sends us. In addition to that, if the remote computer connected to us and sent a header like that, m_bDeflateAccept is TRUE, and we reply with:

Content-Encoding: deflate

This means we will be sending compressed data. If we are connecting to Gnutella today, we tell the remote computer the advanced Gnutella features that we support:

GGEP: 0.5 Pong-Caching: 0.1 Vendor-Message: 0.1 X-Query-Routing: 0.1

The final portion of SendPublicHeaders deals with Gnutella ultrapeers and Gnutella2 hubs. The member variable m_bUltraPeerSet is our record of whether the remote computer is ultra or not. The value is TS_UNKNOWN when it hasn't told us yet, and then gets set to TS_TRUE or TS_FALSE. We tell the remote computer if we are ultra or not with one of these headers:

X-Ultrapeer: True or X-Ultrapeer: False

SendPrivateHeaders

This method sends headers in response to a bunch of headers from the remote computer. In this sense, they are private.

<source lang="c"> Listen-IP: 1.2.3.4:5 Accept: application/x-gnutella2 Content-Type: application/x-gnutella2 Accept-Encoding: deflate Content-Encoding: deflate </source>

The headers confirm Gnutella2 packet exchange and data compression. First, if we haven't told the remote computer the IP address we are listening on yet, a call to SendMyAddress does it. If we initiated the connection, and the remote computer responded with these headers:

Content-Type: application/x-gnutella2 or Content-Type: application/x-shareaza make m_bG2Send true

Accept: application/x-gnutella2 or Accept: application/x-shareaza make m_bG2Accept true

Then m_bG2Send and m_bG2Accept will be TRUE. We reply with these two headers:

Accept: application/x-gnutella2 Content-Type: application/x-gnutella2

Confirming that we accept Gnutella2 packets, and will be sending them. These headers confirm data compression:

Accept-Encoding: deflate Content-Encoding: deflate

We only need to send them here if we initiated the connection to the remote computer.

SendHostHeaders

This method sends headers like this:

<source lang="c"> GNUTELLA/0.6 503 Need an Ultrapeer X-Try-Ultrapeers: 12.23.137.27:6346 2005-01-27T18:42Z,221.229.146.89:5555 20... </source>

The first line is passed to the method as pszMessage. If pszMessage is NULL, the method just sends the X--Try-Ultrapeers header. This header lists some IP addresses the remote computer can try to contact. Information about each computer is separated by a comma. A single record looks like this:

12.23.137.27:6346 2005-01-27T18:42Z

The text starts with the IP address, then a colon, and the port number. After a space, there is a date and time. This tells how long the computer has been online.

ReadResponse

When OnRead is called and the node state is nrsHandshake1, ReadResponse gets called. The handshake 1 state means that we've finished sending a group of headers to the remote computer, and now the remote computer will respond with its own block to us. This method reads the first line from the remote computer, and decides what to do. The first line could be any of the following:

GNUTELLA OK GNUTELLA/0.6 200 OK GNUTELLA CONNECT/0.4

The method reads the first line, and changes the node handshake state based on the following chart:

<source lang="c"> We initiated the connection The remote computer connected to us

Us Them Us Them

GNUTELLA CONNECT/0.6 GNUTELLA CONNECT/0.6 More-Headers (2) \r\n More-Headers (1) \r\n

                       GNUTELLA/0.6 200 OK    GNUTELLA/0.6 200 OK
                       (2)                    More-Headers
                       More-Headers           \r\n
                       \r\n                   (1)

GNUTELLA/0.6 200 OK GNUTELLA/0.6 200 OK More-Headers (3) \r\n More-Headers (1) \r\n </source>

If we've just finished sending a group of headers, we will have set the node state to 1. ReadResponse only gets called when the handshake state is 1. If the remote computer initiated the connection to us, and now it's saying GNUTELLA/0.6 200 OK, we change the state to nrsHandshake3. If we initiated the connection and this is the remote computer's reply, we change the state to nrsHandshake2.

Changing the handshake state controls what OnRead calls next. If it's 1, this method gets called. If it's 2 or 3, CConnection::ReadHeaders gets called instead.

OnHeaderLine

OnHeaderLine takes a handshake header and value parsed from a line sent by the remote computer. The method reads it and sets a member variable to reflect the remote computer's capabilities. The method returns TRUE to keep going, or FALSE to indicate that the handshake is over or we should stop trying to read it.

The value of the User-Agent header gets set in the member variable m_sUserAgent. If the value contains the text Shareaza, m_bShareaza is set to TRUE. The method now calls IsAgentBlocked to determine if we should talk to the remote computer.

The remote computer tells us our IP address with the Remote-IP header. The method passes the value to Network.AcquireLocalAddress.

The remote computer tells us its IP address with the X-My-Address, Listen-IP, or Node header. We don't need to know the IP address it thinks it has, because our record of the IP address we connected to is better. But, the port number in this header is useful. The line m_pHost.sin_port = htons( nPort ) saves it in the SOCKADDR_IN structure in this CShakeNeighbour object.

The |= operator is used a lot in lines that look like this:

<source lang="c"> b |= result; </source>

This is equivalent to:

<source lang="c"> b = b || result; </source>

If b is already true, this line doesn't change anything. If b is false and result is true, b becomes true. This line is good for answering the question: "Have I done this already?"

OnHeadersComplete, OnHeadersCompleteG1, and OnHeadersCompleteG2

OnRead calls CShakeNeighbour::ReadResponse for the first line of a group of headers from the remote computer, and CConnection::ReadHeaders for the lines after that. If ReadHeaders reads a blank line, it calls this method, OnHeadersComplete. Control is passed to OnHeadersCompleteG1 or OnHeadersCompleteG2, depending on whether the remote computer is running Gnutella or Gnutella2.

Computers running Gnutella can act in one of two roles on the network. Ultrapeers are computers with a fast Internet connection and lots of memory. They are in charge of serving leaves. Gnutella2 also has two network roles, but they are titled hubs and leaves. The bulk of these methods figure out if we can connect to the remote computer based on our two network roles. For instance, two leaves should never connect. A hub should connect to a few other hubs, and then a lot of leaves. All this is made more complicated by the fact that the rules for Gnutella and Gnutella2 are not the same. Also, we can be a hub to some computers and a leaf to others, change our state between the two, and assure a computer we just told we were a hub that we are actually just a leaf.

CShakeNeighbour inherits a member variable called m_nNodeType from CNeighbour. This variable is an enum type defined in Neighbour.h:

<source lang="c"> typedef enum NeighbourNodeEnum {

   // The remote computer can be a leaf, or an ultrapeer or hub, and so can we
   ntNode, // We are both Gnutella ultrapeers or Gnutella2 hubs
   ntHub,  // We are a leaf, and this connection is to a Gnutella ultrapeer or Gnutella2 hub above us
   ntLeaf  // We are a Gnutella ultrapeer or Gnutella2 hub, and this connection is to a leaf below us

} NrsNode; </source>

This variable doesn't describe what role the remote computer is in. Rather, it describes the relationship between the remote computer and our roles. If we are both Gnutella ultrapeers or Gnutella2 hubs, then the value is ntNode. If the connection is down from us to a leaf, it's ntLeaf. If we're a leaf and they're the ultrapeer or hub above us, then the value is ntHub.

If the remote computer survives this method, it calls OnHandshakeComplete.

OnHandshakeComplete

At this point, it's helpful to look at the CConnection inheritance tree again:

File:Neighbour.gif

CConnection defines a bunch of member variables, including the all-important socket. All of the other classes inherit from CConnection. This means they pick up all of the methods and member variables defined by CConnection, while adding lots more of their own.

CShakeNeighbour uses the socket from CConnection to exchange handshake headers. When the handshake is over, it knows if it should be a CG1Neighbour or CG2Neighbour object. It needs to become one of these objects, as they have additional member variables and methods that are specific to these networks and will be really useful for communicating on them.

To accomplish this, OnHandshakeComplete runs this code:

<source lang="c"> // If the remote computer can send and understand Gnutella2 packets if ( m_bG2Send && m_bG2Accept ) { // Record that the remote computer supports query routing m_bQueryRouting = TRUE;

// Make a new Gnutella2 neighbour object by copying values from this ShakeNeighbour one pNeighbour = new CG2Neighbour( this );

} // The remote computer is just Gnutella, not Gnutella2 else { // Record that the remote computer is not running Shareaza m_bShareaza = FALSE;

// Make a new Gnutella neighbour object by copying values from this ShakeNeighbour one pNeighbour = new CG1Neighbour( this ); } </source>

The code new CG2Neighbour( this ) creates a new CG2Neighbour object, passing it a pointer to this CShakeNeighbour one. The CG2Neighbour constructor uses the pointer to copy all of the values from this object into the new one. OnHandshakeComplete ends with delete this. Once the method has made a new more specialized object based on this one, it doesn't need this one any more.

Mike has expressed that this may not be the best design for this part of Shareaza. Inheritance is a powerful feature of object-oriented programming, but so is encapsulation. Instead of inheriting from CConnection, the more advanced classes could point to a CConnection object. Then, when CShakeNeighbour needs to become CG2Neighbour, it would just pass the pointer to the CConnection object it's been using.