Customizing Party Communication#
In the previous chapter, we have seen how to define and test protocol interactions with Fandango. In this chapter, we describe additional ways to control communication behavior.
Added in version 1.1: These features are available in Fandango 1.1 and later.
Party Classes#
All the communication between Fandango and its parties takes place via dedicated classes.
In fact, prefixes like Client and Server in protocol testing or In and Out when checking outputs all stand for predefined classes that implement a set of methods to handle the communication:
ClientandServerareNetworkPartyclasses, detailed in theFandangoPartyreference.InandOutareFandangoPartyclasses, detailed in theFandangoPartyreference.
The fact that these are classes allows you to implement your own communication classes and define code that should be executed when starting, stopping, or communicating with a party. This is typically done by subclassing one of the existing classes.
Extending Party Functionality#
Starting a Party#
The NetworkParty class has a start() method that is invoked whenever the respective party is started.
You can extend this method to execute additional code, for instance to create configuration files.
To this end, simply subclass the respective party class and override its start() method:
def create_configuration_file() -> None:
... # Your code goes here
class MyClient(Client):
def start(self) -> None:
create_configuration_file()
super().start() # Invoke the start() method of the superclass
This definition goes right into the .fan file.
Important
In the grammar, be sure to use your own class name - in this case, MyClient. Only then will your definition be used.
<start> ::= <MyClient:send> <Server:respond>
<send> ::= ...
Tip
You can also override existing class names. With this hack, you do not have to change the grammar:
class Client(Client): # extends the original `Client` class
def start(self) -> None:
create_configuration_file()
super().start() # Invoke the start() method of the superclass
See the FandangoParty reference for details on start().
Stopping a Party#
In the same way as extending starting a party, one can also hook into stopping a party – for cleanup actions, for instance.
The stop() method is useful for this:
def cleanup_server() -> None:
... # Your code goes here
class Server(Server):
def stop(self):
cleanup_server()
super().stop() # Invoke the stop() method of the superclass
Again, this definition goes right into the .fan file.
See the FandangoParty reference for details on stop().
Sending Messages#
Whenever Fandango wants to send messages to a party, it invokes its send() message.
As with start() and stop(), above, you can extend the send() method for your own needs.
This includes
altering messages (e.g. encrypt or compress them)
logging information
reacting to specific messages
Here is an example in which we compress the DerivationTree passed via a compress() function.
def compress(msg: bytes) -> bytes:
compressed_message = ... # Your code goes here
return compressed_message
class Server(Server):
def send(self, message: DerivationTree | bytes | str, recipient: Optional[str]):
compressed_message = compress(message.to_bytes())
super().send(compressed_message, recipient)
Important
When called from Fandango, message always is of type DerivationTree;
this allows you to access its constituents (as in to_bytes(), above).
However, the FandangoParty classes all support sending DerivationTree, str, and bytes types, so you do not have to re-create a DerivationTree object when calling send().
Note
For encodings, compression, and other alterations, you can also use a data converter instead.
See the FandangoParty reference for details on send().
Receiving Messages#
Whenever Fandango receives messages from a party, it invokes its receive() message.
Again, you can extend this for your own needs.
Here is an example in which we decompress the message received via a decompress() function.
def decompress(msg: bytes) -> bytes:
decompressed_message = ... # Your code goes here
return decompressed_message
class Server(Server):
def receive(self, message: str | bytes, sender: Optional[str]):
decompressed_message = decompress(message)
super().receive(decompressed_message, sender)
Again, this definition goes right into the .fan file.
See the FandangoParty reference for details on receive().
Own Network Parties#
If your setting uses more than just client or server parties, or if the existing --client and --server options to Fandango are not sufficient, you can define your own network parties by subclassing one of the NetworkParty classes.
As an example, consider a protocol where two clients Client_1 and Client_2 connect to the servers Server_1 and Server_2, respectively.
Using the NetworkParty constructor, the respective .fan file would contain these definitions:
class Client_1(NetworkParty):
def __init__(self):
super().__init__("localhost:8000",
connection_mode=ConnectionMode.CONNECT)
class Server_1(NetworkParty):
def __init__(self):
super().__init__("localhost:8000",
connection_mode=ConnectionMode.EXTERNAL)
class Client_2(NetworkParty):
def __init__(self):
super().__init__("localhost:8000",
connection_mode=ConnectionMode.CONNECT)
class Server_2(NetworkParty):
def __init__(self):
super().__init__("localhost:8001",
connection_mode=ConnectionMode.EXTERNAL)
The grammar can then use Client_1, Client_2, Server_1, and Server_2 as prefixes, and specify a protocol that involves all four parties.
As specified, Fandango would operate as client (1 and 2) and fuzz the two servers.
Tip
Use one .fan file to specify the protocol, and a second one (which includes the first) to specify details on where and how the clients and servers are located.
This allows alternate settings to also make use (= include) the protocol spec - say, a setting in which Fandango acts as server(s) to fuzz the clients.
Accessing Network Parties at Runtime#
If the party configuration changes at runtime (see the FTP case study for an example), you may need to access the individual party objects.
Use PARTY.instance() for this, where PARTY is the class of the respective party.
For instance, to stop the Server object, use
... Server.instance().stop()
If you have given your party an individual name, pass its name as a string argument.
In this case, the class is ignored; FandangoParty will do fine. The code
... FandangoParty.instance("Server").stop()
is equivalent to the above.
In the next chapters, we look at how FTP and DNS protocol specs make use of these features.