Last active
April 14, 2020 16:48
-
-
Save florianjs/47b40e8f9d9fd7e1dac183afe8e2f0a6 to your computer and use it in GitHub Desktop.
A simple P2P chat with Noise
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"bufio" | |
"context" | |
"errors" | |
"fmt" | |
"io" | |
"os" | |
"os/signal" | |
"strings" | |
"time" | |
"github.com/gookit/color" | |
"github.com/perlin-network/noise" | |
"github.com/perlin-network/noise/kademlia" | |
"github.com/spf13/pflag" | |
) | |
var ( | |
hostFlag = pflag.IPP("host", "h", nil, "binding host") | |
portFlag = pflag.Uint16P("port", "p", 0, "binding port") | |
addressFlag = pflag.StringP("address", "a", "", "publicly reachable network address") | |
) | |
type chatMessage struct { | |
contents string | |
} | |
func (m chatMessage) Marshal() []byte { | |
return []byte(m.contents) | |
} | |
func unmarshalChatMessage(buf []byte) (chatMessage, error) { | |
return chatMessage{contents: strings.ToValidUTF8(string(buf), "")}, nil | |
} | |
// check panics if err is not nil. | |
func check(err error) { | |
if err != nil { | |
panic(err) | |
} | |
} | |
// printedLength is the total prefix length of a public key associated to a chat users ID. | |
const printedLength = 8 | |
// An example chat application on Noise. | |
func main() { | |
// Parse flags/options. | |
pflag.Parse() | |
// Create a new configured node. | |
node, err := noise.NewNode( | |
noise.WithNodeBindHost(*hostFlag), | |
noise.WithNodeBindPort(*portFlag), | |
noise.WithNodeAddress(*addressFlag), | |
) | |
check(err) | |
// Release resources associated to node at the end of the program. | |
defer node.Close() | |
// Register the chatMessage Go type to the node with an associated unmarshal function. | |
node.RegisterMessage(chatMessage{}, unmarshalChatMessage) | |
// Register a message handler to the node. | |
node.Handle(handle) | |
// Instantiate Kademlia. | |
events := kademlia.Events{ | |
OnPeerAdmitted: func(id noise.ID) { | |
fmt.Printf("Learned about a new peer %s(%s).\n", id.Address, id.ID.String()[:printedLength]) | |
}, | |
OnPeerEvicted: func(id noise.ID) { | |
fmt.Printf("Forgotten a peer %s(%s).\n", id.Address, id.ID.String()[:printedLength]) | |
}, | |
} | |
overlay := kademlia.New(kademlia.WithProtocolEvents(events)) | |
// Bind Kademlia to the node. | |
node.Bind(overlay.Protocol()) | |
// Have the node start listening for new peers. | |
check(node.Listen()) | |
// Print out the nodes ID and a help message comprised of commands. | |
help(node) | |
// Ping nodes to initially bootstrap and discover peers from. | |
bootstrap(node, pflag.Args()...) | |
// Attempt to discover peers if we are bootstrapped to any nodes. | |
discover(overlay) | |
// Accept chat message inputs and handle chat commands in a separate goroutine. | |
go input(func(line string) { | |
chat(node, overlay, line) | |
}) | |
// Wait until Ctrl+C or a termination call is done. | |
c := make(chan os.Signal, 1) | |
signal.Notify(c, os.Interrupt) | |
<-c | |
// Close stdin to kill the input goroutine. | |
check(os.Stdin.Close()) | |
// Empty println. | |
println() | |
} | |
// input handles inputs from stdin. | |
func input(callback func(string)) { | |
r := bufio.NewReader(os.Stdin) | |
for { | |
buf, _, err := r.ReadLine() | |
if err != nil { | |
if errors.Is(err, io.EOF) { | |
return | |
} | |
check(err) | |
} | |
line := string(buf) | |
if len(line) == 0 { | |
continue | |
} | |
callback(line) | |
} | |
} | |
func digits(num uint16) uint16 { | |
return num % 100 | |
} | |
// ANCHOR | |
// handle handles and prints out valid chat messages from peers. | |
func handle(ctx noise.HandlerContext) error { | |
customColor := uint8(digits(ctx.ID().Port)) | |
fromColor := color.S256(15, customColor) | |
adminColor := color.S256(15, 9) | |
if ctx.IsRequest() { | |
return nil | |
} | |
obj, err := ctx.DecodeMessage() | |
if err != nil { | |
return nil | |
} | |
msg, ok := obj.(chatMessage) | |
if !ok { | |
return nil | |
} | |
if len(msg.contents) == 0 { | |
return nil | |
} | |
if ctx.ID().Port == 9000 { | |
adminColor.Printf("(ADMIN)") | |
fmt.Println(" > " + msg.contents) | |
} else { | |
fromColor.Printf("(%s)", ctx.ID().ID.String()[:printedLength]) | |
fmt.Println(" > " + msg.contents) | |
} | |
if msg.contents == "Founded!" { | |
fmt.Println("NONCE FOUNDED") | |
} | |
return nil | |
} | |
// ANCHOR | |
// help prints out the users ID and commands available. | |
func help(node *noise.Node) { | |
color := color.New(color.FgBlue, color.OpBold) | |
color.Printf("Your ID is %s(%s). Type '/discover' to attempt to discover new "+ | |
"peers, or '/peers' to list out all peers you are connected to.\n", | |
node.ID().Address, | |
node.ID().ID.String()[:printedLength], | |
) | |
} | |
// bootstrap pings and dials an array of network addresses which we may interact with and discover peers from. | |
func bootstrap(node *noise.Node, addresses ...string) { | |
for _, addr := range addresses { | |
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) | |
_, err := node.Ping(ctx, addr) | |
cancel() | |
if err != nil { | |
fmt.Printf("Failed to ping bootstrap node (%s). Skipping... [error: %s]\n", addr, err) | |
continue | |
} | |
} | |
} | |
// discover uses Kademlia to discover new peers from nodes we already are aware of. | |
func discover(overlay *kademlia.Protocol) { | |
ids := overlay.Discover() | |
var str []string | |
for _, id := range ids { | |
str = append(str, fmt.Sprintf("%s(%s)", id.Address, id.ID.String()[:printedLength])) | |
} | |
if len(ids) > 0 { | |
fmt.Printf("Discovered %d peer(s): [%v]\n", len(ids), strings.Join(str, ", ")) | |
} else { | |
fmt.Printf("Did not discover any peers.\n") | |
} | |
} | |
// peers prints out all peers we are already aware of. | |
func peers(overlay *kademlia.Protocol) { | |
ids := overlay.Table().Peers() | |
var str []string | |
for _, id := range ids { | |
str = append(str, fmt.Sprintf("%s(%s)", id.Address, id.ID.String()[:printedLength])) | |
} | |
fmt.Printf("You know %d peer(s): [%v]\n", len(ids), strings.Join(str, ", ")) | |
} | |
// chat handles sending chat messages and handling chat commands. | |
func chat(node *noise.Node, overlay *kademlia.Protocol, line string) { | |
switch line { | |
case "/discover": | |
discover(overlay) | |
return | |
case "/peers": | |
peers(overlay) | |
return | |
default: | |
} | |
if strings.HasPrefix(line, "/") { | |
help(node) | |
return | |
} | |
for _, id := range overlay.Table().Peers() { | |
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) | |
err := node.SendMessage(ctx, id.Address, chatMessage{contents: line}) | |
cancel() | |
if err != nil { | |
fmt.Printf("Failed to send message to %s(%s). Skipping... [error: %s]\n", | |
id.Address, | |
id.ID.String()[:printedLength], | |
err, | |
) | |
continue | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment