diff --git a/.drone.yml b/.drone.yml index dbe1d14..244b439 100644 --- a/.drone.yml +++ b/.drone.yml @@ -15,7 +15,7 @@ pipeline: image: golang commands: - go list ./... | xargs go vet - - go list ./... | grep -v "/wire/" | grep -v "/examples/" | xargs golint -set_exit_status + - go list ./... | grep -v "/wire/" | xargs golint -set_exit_status units-tests: image: golang commands: diff --git a/.gitignore b/.gitignore index a5cc38c..ca2f96c 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ go-ricochet-coverage.out .idea .reviewboardrc /vendor/ +/testing/tor/ +/connectivity/tor/ +/tor/ diff --git a/application/examples/echobot/alicebot/alicebot.go b/application/examples/echobot/alicebot/alicebot.go new file mode 100644 index 0000000..561e5a3 --- /dev/null +++ b/application/examples/echobot/alicebot/alicebot.go @@ -0,0 +1,112 @@ +package alicebot + +import ( + "crypto/rand" + "git.openprivacy.ca/openprivacy/libricochet-go" + "git.openprivacy.ca/openprivacy/libricochet-go/channels" + "git.openprivacy.ca/openprivacy/libricochet-go/connection" + "git.openprivacy.ca/openprivacy/libricochet-go/connectivity" + "git.openprivacy.ca/openprivacy/libricochet-go/identity" + "git.openprivacy.ca/openprivacy/libricochet-go/log" + "golang.org/x/crypto/ed25519" + "os" + "time" +) + +// NewAliceBot creates a new AliceBot and establishes a connection to the given onion server. +func NewAliceBot(acn connectivity.ACN, onion string) AliceBot { + alice := new(alicebot) + alice.messages = make(map[uint32]string) + + var err error + alice.pub, alice.priv, err = ed25519.GenerateKey(rand.Reader) + if err != nil { + log.Errorf("[alice] error generating key: %v", err) + os.Exit(1) + } + + rc, err := goricochet.Open(acn, onion) + if err != nil { + log.Errorf("[alice] error connecting to echobot: %v", err) + os.Exit(1) + } + + _, err = connection.HandleOutboundConnection(rc).ProcessAuthAsV3Client(identity.InitializeV3("alice", &alice.priv, &alice.pub)) + if err != nil { + log.Errorf("[alice] failed to authenticate connection: %v", err) + os.Exit(1) + } + + alice.rc = rc + + ach := connection.AutoConnectionHandler{} + ach.Init() + + ach.RegisterChannelHandler("im.ricochet.chat", func() channels.Handler { + chat := new(channels.ChatChannel) + chat.Handler = alice + return chat + }) + + go alice.rc.Process(&ach) + + log.Infof("[alice] requesting channel...") + alice.rc.Do(func() error { + chatchannel := channels.ChatChannel{} + chatchannel.Handler = alice + + _, err := alice.rc.RequestOpenChannel("im.ricochet.chat", &chatchannel) + if err != nil { + log.Errorf("failed requestopenchannel: %v", err) + os.Exit(1) + } + return nil + }) + + return alice +} + +type alicebot struct { + messages map[uint32]string + pub ed25519.PublicKey + priv ed25519.PrivateKey + mID int + rc *connection.Connection +} + +// AliceBot is an interface for alicebot, allowing callers to send and receive messages. +type AliceBot interface { + channels.ChatChannelHandler + SendMessage(string) +} + +// SendMessage can be called to send a message to EchoBot +func (ab *alicebot) SendMessage(message string) { + // The following code opens (or creates) a new im.ricochet.chat channel to the connected service + // and sends a message. + log.Infof("[alice] sending...") + ab.rc.Do(func() error { + channel := ab.rc.Channel("im.ricochet.chat", channels.Outbound) + id, err := channels.SendMessageOnChatChannel(channel, message) + if err == nil { + ab.messages[id] = message + } + return err + }) +} + +// OpenInbound is called when EchoBot attempts to open a channel with AliceBot +func (ab *alicebot) OpenInbound() { + log.Infof("[alice] inbound connection established") +} + +// ChatMessage is called whenever AliceBot receives a message from EchoBot +func (ab *alicebot) ChatMessage(messageID uint32, when time.Time, message string) bool { + log.Infof("[alice] got message from echobot: %s", message) + return true +} + +// ChatMessageAck is called whenever AliceBot received an acknowledgement of a previously sent message. +func (ab *alicebot) ChatMessageAck(messageID uint32, accepted bool) { + log.Infof("[alice] message \"%s\" ack'd", ab.messages[messageID]) +} diff --git a/application/examples/echobot/main.go b/application/examples/echobot/main.go index f96b0d3..174e249 100644 --- a/application/examples/echobot/main.go +++ b/application/examples/echobot/main.go @@ -2,10 +2,9 @@ package main import ( "crypto/rand" - "git.openprivacy.ca/openprivacy/libricochet-go" "git.openprivacy.ca/openprivacy/libricochet-go/application" + "git.openprivacy.ca/openprivacy/libricochet-go/application/examples/echobot/alicebot" "git.openprivacy.ca/openprivacy/libricochet-go/channels" - "git.openprivacy.ca/openprivacy/libricochet-go/connection" "git.openprivacy.ca/openprivacy/libricochet-go/identity" "git.openprivacy.ca/openprivacy/libricochet-go/log" "time" @@ -15,17 +14,20 @@ import ( "os" ) +// EchoBotInstance is an Instance of the EchoBot Application. One is created for every connected peer. type EchoBotInstance struct { rai *application.Instance ra *application.RicochetApplication } +// Init establishes an EchoBotInstance func (ebi *EchoBotInstance) Init(rai *application.Instance, ra *application.RicochetApplication) { ebi.rai = rai ebi.ra = ra } -// We always want bidirectional chat channels +// OpenInbound is called when AliceBot opens a ChatChannel. In this case, because we want EchoBot to respond we +// need to open a new channel in the other direction. func (ebi *EchoBotInstance) OpenInbound() { log.Debugln("OpenInbound() ChatChannel handler called...") outboutChatChannel := ebi.rai.Connection.Channel("im.ricochet.chat", channels.Outbound) @@ -40,39 +42,39 @@ func (ebi *EchoBotInstance) OpenInbound() { } } +// ChatMessage is called whenever a connected peer sends a message to EchoBot func (ebi *EchoBotInstance) ChatMessage(messageID uint32, when time.Time, message string) bool { log.Infof("message from %v - %v", ebi.rai.RemoteHostname, message) ebi.SendChatMessage(ebi.rai, ebi.rai.RemoteHostname+" "+message) return true } +// ChatMessageAck is called whenever a connected peer acknolwedges a message that EchoBot sent. func (ebi *EchoBotInstance) ChatMessageAck(messageID uint32, accepted bool) { } +// SendChatMessage sends a chat message to the given echobot instance func (ebi *EchoBotInstance) SendChatMessage(rai *application.Instance, message string) { ebi.rai.Connection.Do(func() error { channel := ebi.rai.Connection.Channel("im.ricochet.chat", channels.Outbound) - if channel != nil { - chatchannel, ok := channel.Handler.(*channels.ChatChannel) - if ok { - chatchannel.SendMessage(message) - } - } //TODO: else? + // We are swallowing the message id and the error here, in reality you will want to handle it. + channels.SendMessageOnChatChannel(channel, message) return nil }) } +// main() encapsulates an entire ricochet ecosystem, from starting tor, to generating onion service keys to managing +// the launching of both the echobot server and the alicebot peer. +// In most systems you will only be handling one or two of these subsystems at any given time so this example might seem +// bloated, but we have tried to highlight the most interesting aspects to allow easy application to new domains. func main() { - //////////// - // SERVER // - //////////// + + // Set up Logging. log.SetLevel(log.LevelInfo) log.AddEverythingFromPattern("connectivity") - echobot := new(application.RicochetApplication) - cpubk, cprivk, err := ed25519.GenerateKey(rand.Reader) - + // Set up Tor acn, err := connectivity.StartTor(".", "") if err != nil { log.Errorf("Unable to start Tor: %v", err) @@ -80,13 +82,19 @@ func main() { } defer acn.Close() - listenService, err := acn.Listen(cprivk, application.RicochetPort) + // Set up the Echobot Server + echobot := new(application.RicochetApplication) + cpubk, cprivk, err := ed25519.GenerateKey(rand.Reader) + // Turn on the echobot onion service in Tor. + listenService, err := acn.Listen(cprivk, application.RicochetPort) if err != nil { log.Errorf("error setting up onion service: %v", err) os.Exit(1) } + // This next section looks complicated (and it is a little), but all it is doing is allowing echobot to handle + // im.ricochet.chat type channels. af := application.InstanceFactory{} af.Init() af.AddHandler("im.ricochet.chat", func(rai *application.Instance) func() channels.Handler { @@ -99,116 +107,21 @@ func main() { } }) + // Thee next few lines turn on echobot and make it available to listen to new connections. + // Note that we initialize a V3 identity for echobot. echobot.Init(acn, "echobot", identity.InitializeV3("echobot", &cprivk, &cpubk), af, new(application.AcceptAllContactManager)) log.Infof("echobot listening on %v", listenService.AddressFull()) go echobot.Run(listenService) + // Now we wait a little bit for everything to wire itself together. log.Infoln("counting to five ...") time.Sleep(time.Second * 5) - //////////// - // CLIENT // - //////////// - - //alicebot should nominally be in another package to prevent initializing it directly - alice := NewAliceBot(acn, listenService.AddressIdentity()) + // Finally, in these last few lines we setup an AliceBot who simply sends messages to echobot + alice := alicebot.NewAliceBot(acn, listenService.AddressIdentity()) alice.SendMessage("be gay") alice.SendMessage("do crime") // stick around and see what happens time.Sleep(time.Second * 30) } - -func NewAliceBot(acn connectivity.ACN, onion string) alicebot { - alice := alicebot{} - alice.messages = make(map[uint32]string) - - var err error - alice.pub, alice.priv, err = ed25519.GenerateKey(rand.Reader) - if err != nil { - log.Errorf("[alice] error generating key: %v", err) - os.Exit(1) - } - - rc, err := goricochet.Open(acn, onion) - if err != nil { - log.Errorf("[alice] error connecting to echobot: %v", err) - os.Exit(1) - } - - _, err = connection.HandleOutboundConnection(rc).ProcessAuthAsV3Client(identity.InitializeV3("alice", &alice.priv, &alice.pub)) - if err != nil { - log.Errorf("[alice] failed to authenticate connection: %v", err) - os.Exit(1) - } - - alice.rc = rc - - ach := connection.AutoConnectionHandler{} - ach.Init() - - ach.RegisterChannelHandler("im.ricochet.chat", func() channels.Handler { - chat := new(channels.ChatChannel) - chat.Handler = &alice - return chat - }) - - go alice.rc.Process(&ach) - - log.Infof("[alice] requesting channel...") - alice.rc.Do(func() error { - chatchannel := channels.ChatChannel{} - chatchannel.Handler = &alice - - _, err := alice.rc.RequestOpenChannel("im.ricochet.chat", &chatchannel) - if err != nil { - log.Errorf("failed requestopenchannel: %v", err) - os.Exit(1) - } - return nil - }) - - return alice -} - -type alicebot struct { - messages map[uint32]string - pub ed25519.PublicKey - priv ed25519.PrivateKey - mID int - rc *connection.Connection -} - -func (this *alicebot) SendMessage(message string) { - log.Infof("[alice] sending...") - this.rc.Do(func() error { - channel := this.rc.Channel("im.ricochet.chat", channels.Outbound) - if channel != nil { - peerchannel, ok := channel.Handler.(*channels.ChatChannel) - if ok { - log.Infof("[alice] sending '%s' to echobot", message) - this.messages[peerchannel.SendMessage(message)] = message - } else { - log.Errorf("couldn't cast channel:") - os.Exit(1) - } - } else { - log.Errorf("couldn't create channel") - os.Exit(1) - } - return nil - }) -} - -func (this *alicebot) OpenInbound() { - log.Infof("[alice] inbound connection established") -} - -func (this *alicebot) ChatMessage(messageID uint32, when time.Time, message string) bool { - log.Infof("[alice] got message from echobot: %s", message) - return true -} - -func (this *alicebot) ChatMessageAck(messageID uint32, accepted bool) { - log.Infof("[alice] message \"%s\" ack'd", this.messages[messageID]) -} diff --git a/application/examples/v3/main.go b/application/examples/v3/main.go deleted file mode 100644 index d9faf1e..0000000 --- a/application/examples/v3/main.go +++ /dev/null @@ -1,34 +0,0 @@ -package main - -import ( - "crypto/rand" - "encoding/base32" - "git.openprivacy.ca/openprivacy/libricochet-go/application" - "git.openprivacy.ca/openprivacy/libricochet-go/log" - "git.openprivacy.ca/openprivacy/libricochet-go/utils" - "golang.org/x/crypto/ed25519" - "strings" - - "git.openprivacy.ca/openprivacy/libricochet-go/connectivity" - "os" -) - -// An example of how to setup a v3 onion service in go -func main() { - log.SetLevel(log.LevelInfo) - tm, err := connectivity.StartTor(".", "") - if err != nil { - log.Errorf("Unable to start Tor: %v", err) - os.Exit(1) - } - defer tm.Close() - - cpubk, cprivk, _ := ed25519.GenerateKey(rand.Reader) - onion, err := tm.Listen(cprivk, application.RicochetPort) - utils.CheckError(err) - defer onion.Close() - log.Infof("Got Listener %v", onion.AddressFull()) - decodedPub, err := base32.StdEncoding.DecodeString(strings.ToUpper(onion.AddressIdentity())) - log.Infof("Decoded Public Key: %x %v", decodedPub[:32], err) - log.Infof("ed25519 Public Key: %x", cpubk) -} diff --git a/channels/chatchannel.go b/channels/chatchannel.go index b89983b..3b343d8 100644 --- a/channels/chatchannel.go +++ b/channels/chatchannel.go @@ -1,6 +1,7 @@ package channels import ( + "errors" "git.openprivacy.ca/openprivacy/libricochet-go/utils" "git.openprivacy.ca/openprivacy/libricochet-go/wire/chat" "git.openprivacy.ca/openprivacy/libricochet-go/wire/control" @@ -57,6 +58,19 @@ func (cc *ChatChannel) SendMessageWithTime(message string, when time.Time) uint3 return messageID } +// SendMessageOnChatChannel is a wrapper function which performs some necessary boilerplate +// to make sending messages easier. +func SendMessageOnChatChannel(channel *Channel, message string) (uint32, error) { + if channel != nil { + peerchannel, ok := channel.Handler.(*ChatChannel) + if ok { + return peerchannel.SendMessage(message), nil + } + return 0, errors.New("channel is not an im.ricochet.chat channel") + } + return 0, errors.New("channel pointer is nil") +} + // Acknowledge indicates that the given messageID was received, and whether // it was accepted. func (cc *ChatChannel) Acknowledge(messageID uint32, accepted bool) { diff --git a/testing/integration_test.go b/testing/integration_test.go index 2e329c1..592f8bf 100644 --- a/testing/integration_test.go +++ b/testing/integration_test.go @@ -80,13 +80,8 @@ func SendMessage(rai *application.Instance, message string) error { log.Infof("Finding Chat Channel") channel := rai.Connection.Channel("im.ricochet.chat", channels.Outbound) - if channel != nil { - log.Infof("Found Chat Channel") - chatchannel, ok := channel.Handler.(*channels.ChatChannel) - if ok { - chatchannel.SendMessage(message) - } - } else { + _, err := channels.SendMessageOnChatChannel(channel, message) + if err != nil { log.Infof("Could not find chat channel") } return nil diff --git a/testing/quality.sh b/testing/quality.sh index bc9bb13..fb20dfd 100755 --- a/testing/quality.sh +++ b/testing/quality.sh @@ -12,6 +12,4 @@ echo "" echo "Linting:" # Ignore wire packages as they are autogenerated -# Ignore examples as they are illustrative -# TODO Consider Renaming ApplicationInstance and ApplicationInstanceFactory to remove the last grep -go list ./... | grep -v "/wire/" | grep -v "/examples/" | xargs golint \ No newline at end of file +go list ./... | grep -v "/wire/" | xargs golint \ No newline at end of file