Skip to content

Instantly share code, notes, and snippets.

@florianjs
Last active April 14, 2020 16:48
Show Gist options
  • Save florianjs/47b40e8f9d9fd7e1dac183afe8e2f0a6 to your computer and use it in GitHub Desktop.
Save florianjs/47b40e8f9d9fd7e1dac183afe8e2f0a6 to your computer and use it in GitHub Desktop.
A simple P2P chat with Noise
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