Skip to content
Snippets Groups Projects
Commit 3ddb17b2 authored by Jake Taylor's avatar Jake Taylor :lips:
Browse files

Merge branch 'jonah/e2e' into 'master'

Add example E2e client

See merge request !4
parents cddcced3 a66425cb
No related branches found
No related tags found
1 merge request!4Add example E2e client
# xxdk E2E client example
This mini-repository contains example logic for running an e2e client.
This is provided by the xx network team as a springboard to help consumers
better understand our API and how it may be used.
`main.go` contains the crux of the logic. We avoid complicating our example by
avoiding the usage of CLI flags for basic variables you may change in the code.
This file initiates an xxdk E2E client, using the authentication callbacks in
`auth.go`. With that established, it registers a generic message listener
and establishes authentication with a partner. Finally, it sends a test message
and listens for incoming messages until stopped by the user.
`utils.go` contains utility functions for running the program. In this case,
we provide a tool initializing a log and one which writes a contact to a file.
`listener.go` contains logic for handling the reception of a message via the
e2e client. In this example, it is very basic. We invite consumers
to use this as a basis to implement more complex message listeners.
## Build Instructions
In these instructions we will go over building a connection client using our
example. In order to build a client which successfully sends a message through
the connection, we must first go over how to build and run a connection server.
### Building a Client
package main
import (
jww "github.com/spf13/jwalterweatherman"
"gitlab.com/elixxir/client/cmix/identity/receptionID"
"gitlab.com/elixxir/client/cmix/rounds"
"gitlab.com/elixxir/client/xxdk"
"gitlab.com/elixxir/crypto/contact"
)
// auth implements the xxdk.AuthCallbacks interface
type auth struct {
confirmChan chan contact.Contact
}
// Request is called when requests are received
// Currently confirms all incoming auth requests
func (a *auth) Request(partner contact.Contact, receptionID receptionID.EphemeralIdentity,
round rounds.Round, e2e *xxdk.E2e) {
_, err := e2e.GetAuth().Confirm(partner)
if err != nil {
jww.ERROR.Printf("Failed to confirm auth for %s: %+v", partner.ID.String(), err)
}
}
// Confirm is called when an e2e request is confirmed.
// Currently passes the confirmed contact over a channel to notify the main thread of the confirmation
func (a *auth) Confirm(partner contact.Contact, receptionID receptionID.EphemeralIdentity,
round rounds.Round, e2e *xxdk.E2e) {
a.confirmChan <- partner
}
func (a *auth) Reset(partner contact.Contact, receptionID receptionID.EphemeralIdentity,
round rounds.Round, e2e *xxdk.E2e) {
}
module gitlab.com/elixxir/xxdk-examples/e2eServer
go 1.17
require (
github.com/spf13/jwalterweatherman v1.1.0
gitlab.com/elixxir/client v1.5.1-0.20220706193049-a0b718049663
gitlab.com/elixxir/crypto v0.0.7-0.20220606201132-c370d5039cea
gitlab.com/elixxir/primitives v0.0.3-0.20220606195757-40f7a589347f
gitlab.com/xx_network/primitives v0.0.4-0.20220630163313-7890038258c6
)
require (
github.com/badoux/checkmail v1.2.1 // indirect
github.com/cloudflare/circl v1.1.0 // indirect
github.com/elliotchance/orderedmap v1.4.0 // indirect
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 // indirect
github.com/ttacon/libphonenumber v1.2.1 // indirect
github.com/tyler-smith/go-bip39 v1.1.0 // indirect
gitlab.com/elixxir/bloomfilter v0.0.0-20211222005329-7d931ceead6f // indirect
gitlab.com/elixxir/comms v0.0.4-0.20220603231314-e47e4af13326 // indirect
gitlab.com/elixxir/ekv v0.1.7 // indirect
gitlab.com/xx_network/comms v0.0.4-0.20220630163702-f3d372ef6acd // indirect
gitlab.com/xx_network/crypto v0.0.5-0.20220606200528-3f886fe49e81 // indirect
gitlab.com/xx_network/ring v0.0.3-0.20220222211904-da613960ad93 // indirect
golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed // indirect
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac // indirect
golang.org/x/text v0.3.6 // indirect
google.golang.org/genproto v0.0.0-20210105202744-fe13368bc0e1 // indirect
google.golang.org/grpc v1.42.0 // indirect
google.golang.org/protobuf v1.27.1 // indirect
)
This diff is collapsed.
package main
import (
jww "github.com/spf13/jwalterweatherman"
"gitlab.com/elixxir/client/e2e/receive"
)
// listener implements the receive.Listener interface
type listener struct {
name string
}
// Hear will be called whenever a message matching
// the RegisterListener call is received
// User-defined message handling logic goes here
func (l listener) Hear(item receive.Message) {
jww.INFO.Printf("Message received: %v", item)
}
// Name is used for debugging purposes
func (l listener) Name() string {
return l.name
}
// Sending Normal messages (Getting Started guide)
package main
import (
"errors"
jww "github.com/spf13/jwalterweatherman"
"gitlab.com/elixxir/client/catalog"
"gitlab.com/elixxir/client/xxdk"
"gitlab.com/elixxir/primitives/fact"
"gitlab.com/xx_network/primitives/id"
"io/fs"
"io/ioutil"
"os"
"os/signal"
"syscall"
"time"
"gitlab.com/elixxir/crypto/contact"
)
func main() {
// Logging
initLog(1, "client2.log")
// Create a new client object-------------------------------------------------------
// Path to the recipient contact file
recipientContactPath := "contact.xxc"
myContactPath := "recipient.xxc"
// You would ideally use a configuration tool to acquire these parameters
statePath := "statePathRecipient"
statePass := "password"
// The following connects to mainnet. For historical reasons it is called a json file
// but it is actually a marshalled file with a cryptographic signature attached.
// This may change in the future.
ndfURL := "https://elixxir-bins.s3.us-west-1.amazonaws.com/ndf/mainnet.json"
certificatePath := "../mainnet.crt"
ndfPath := "ndf.json"
// Check if state exists
if _, err := os.Stat(statePath); errors.Is(err, fs.ErrNotExist) {
// Attempt to read the NDF
var ndfJSON []byte
ndfJSON, err = ioutil.ReadFile(ndfPath)
if err != nil {
jww.INFO.Printf("NDF does not exist: %+v", err)
}
// If NDF can't be read, retrieve it remotely
if ndfJSON == nil {
cert, err := ioutil.ReadFile(certificatePath)
if err != nil {
jww.FATAL.Panicf("Failed to read certificate: %v", err)
}
ndfJSON, err = xxdk.DownloadAndVerifySignedNdfWithUrl(ndfURL, string(cert))
if err != nil {
jww.FATAL.Panicf("Failed to download NDF: %+v", err)
}
}
// Initialize the state
err = xxdk.NewCmix(string(ndfJSON), statePath, []byte(statePass), "")
if err != nil {
jww.FATAL.Panicf("Failed to initialize state: %+v", err)
}
}
// Login to your client session-----------------------------------------------------
// Login with the same sessionPath and sessionPass used to call NewClient()
baseClient, err := xxdk.LoadCmix(statePath, []byte(statePass), xxdk.GetDefaultCMixParams())
if err != nil {
jww.FATAL.Panicf("Failed to load state: %+v", err)
}
// Get reception identity (automatically created if one does not exist)
identityStorageKey := "identityStorageKey"
identity, err := xxdk.LoadReceptionIdentity(identityStorageKey, baseClient)
if err != nil {
// If no extant xxdk.ReceptionIdentity, generate and store a new one
identity, err = xxdk.MakeReceptionIdentity(baseClient)
if err != nil {
jww.FATAL.Panicf("Failed to generate reception identity: %+v", err)
}
err = xxdk.StoreReceptionIdentity(identityStorageKey, identity, baseClient)
if err != nil {
jww.FATAL.Panicf("Failed to store new reception identity: %+v", err)
}
}
writeContact(myContactPath, identity.GetContact())
// Create an E2E client
// Pass in auth object which controls auth callbacks for this client
params := xxdk.GetDefaultE2EParams()
jww.INFO.Printf("Using E2E parameters: %+v", params)
confirmChan := make(chan contact.Contact, 5)
xxdkClient, err := xxdk.Login(baseClient, &auth{confirmChan: confirmChan}, identity, params)
if err != nil {
jww.FATAL.Panicf("Unable to Login: %+v", err)
}
e2eClient := xxdkClient.GetE2E()
// Start network threads------------------------------------------------------------
// Set networkFollowerTimeout to a value of your choice (seconds)
networkFollowerTimeout := 5 * time.Second
err = xxdkClient.StartNetworkFollower(networkFollowerTimeout)
if err != nil {
jww.FATAL.Panicf("Failed to start network follower: %+v", err)
}
// Set up a wait for the network to be connected
waitUntilConnected := func(connected chan bool) {
waitTimeout := 30 * time.Second
timeoutTimer := time.NewTimer(waitTimeout)
isConnected := false
// Wait until we connect or panic if we cannot before the timeout
for !isConnected {
select {
case isConnected = <-connected:
jww.INFO.Printf("Network Status: %v", isConnected)
break
case <-timeoutTimer.C:
jww.FATAL.Panicf("Timeout on starting network follower")
}
}
}
// Create a tracker channel to be notified of network changes
connected := make(chan bool, 10)
// Provide a callback that will be signalled when network health status changes
xxdkClient.GetCmix().AddHealthCallback(
func(isConnected bool) {
connected <- isConnected
})
// Wait until connected or crash on timeout
waitUntilConnected(connected)
// Register a listener for messages--------------------------------------------------
// Listen for all types of messages using catalog.NoType
// Listen for messages from all users using id.ZeroUser
// User-defined behavior for message reception goes in the listener
_ = e2eClient.RegisterListener(&id.ZeroUser, catalog.NoType, listener{name: "e2e Message Listener"})
// Connect with the recipient--------------------------------------------------
if recipientContactPath != "" {
// Wait for 30 seconds to ensure network connectivity
time.Sleep(30 * time.Second)
// Recipient's contact (read from a Client CLI-generated contact file)
contactData, err := ioutil.ReadFile(recipientContactPath)
if err != nil {
jww.FATAL.Panicf("Failed to read recipient contact file: %+v", err)
}
// Imported "gitlab.com/elixxir/crypto/contact"
// which provides an `Unmarshal` function to convert the byte slice ([]byte) output
// of `ioutil.ReadFile()` to the `Contact` type expected by `RequestAuthenticatedChannel()`
recipientContact, err := contact.Unmarshal(contactData)
if err != nil {
jww.FATAL.Panicf("Failed to get contact data: %+v", err)
}
jww.INFO.Printf("Recipient contact: %+v", recipientContact)
// Check that the partner exists, if not send a request
_, err = e2eClient.GetPartner(recipientContact.ID)
if err != nil {
_, err = xxdkClient.GetAuth().Request(recipientContact, fact.FactList{})
if err != nil {
jww.FATAL.Panicf("Failed to send contact request to %s: %+v", recipientContact.ID.String(), err)
}
timeout := time.NewTimer(30 * time.Second)
select {
case pc := <-confirmChan:
if !pc.ID.Cmp(recipientContact.ID) {
jww.FATAL.Panicf("Did not receive confirmation for the requested contact")
}
break
case <-timeout.C:
jww.FATAL.Panicf("Timed out waiting to receive confirmation of e2e relationship with partner")
}
}
// Send a message to the recipient----------------------------------------------------
// Test message
msgBody := "If this message is sent successfully, we'll have established contact with the recipient."
roundIDs, messageID, timeSent, err := e2eClient.SendE2E(catalog.XxMessage, recipientContact.ID, []byte(msgBody), params.Base)
if err != nil {
jww.FATAL.Panicf("Failed to send message: %+v", err)
}
jww.INFO.Printf("Message %v sent in RoundIDs: %+v at %v", messageID, roundIDs, timeSent)
}
// Keep app running to receive messages-----------------------------------------------
// Wait until the user terminates the program
c := make(chan os.Signal)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
<-c
err = xxdkClient.StopNetworkFollower()
if err != nil {
jww.ERROR.Printf("Failed to stop network follower: %+v", err)
} else {
jww.INFO.Printf("Stopped network follower.")
}
os.Exit(0)
}
package main
import (
jww "github.com/spf13/jwalterweatherman"
"gitlab.com/elixxir/crypto/contact"
"gitlab.com/xx_network/primitives/utils"
"io/ioutil"
"log"
"os"
)
func initLog(threshold uint, logPath string) {
if logPath != "-" && logPath != "" {
// Disable stdout output
jww.SetStdoutOutput(ioutil.Discard)
// Use log file
logOutput, err := os.OpenFile(logPath,
os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
panic(err.Error())
}
jww.SetLogOutput(logOutput)
}
if threshold > 1 {
jww.INFO.Printf("log level set to: TRACE")
jww.SetStdoutThreshold(jww.LevelTrace)
jww.SetLogThreshold(jww.LevelTrace)
jww.SetFlags(log.LstdFlags | log.Lmicroseconds)
} else if threshold == 1 {
jww.INFO.Printf("log level set to: DEBUG")
jww.SetStdoutThreshold(jww.LevelDebug)
jww.SetLogThreshold(jww.LevelDebug)
jww.SetFlags(log.LstdFlags | log.Lmicroseconds)
} else {
jww.INFO.Printf("log level set to: INFO")
jww.SetStdoutThreshold(jww.LevelInfo)
jww.SetLogThreshold(jww.LevelInfo)
}
}
func writeContact(outfilePath string, c contact.Contact) {
err := utils.WriteFileDef(outfilePath, c.Marshal())
if err != nil {
jww.ERROR.Printf("could not write contact file: %+v", err)
} else {
jww.INFO.Printf("contact written to %s successfully", outfilePath)
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment