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

Merge branch 'RestSingleUseClient' into 'master'

Rest single use client

See merge request !3
parents 3f5c8341 1ecb4b78
No related branches found
No related tags found
1 merge request!3Rest single use client
......@@ -25,3 +25,5 @@ session*/
*.xxchan
*.pem
*.log
statePath/
*.xxc
\ No newline at end of file
# xxdk Restlike Single Use Client Example
This mini-repository contains the example logic for running a basic REST-like
single use 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. With that client established, a
REST-like client is built on top. Using a precanned contact object created
in `restSingleUseServer` this REST-like client contacts the server with a simple
request.
`utils.go` contains utility functions for running the program. In this case,
we provide a tool initializing a log.
## Build Instructions
In these instructions we will go over building a REST-like client using our
example. In order to build a client which successfully sends a request and
receives a response, we must first go over how to build and run a REST-like
single use server.
### Building a Server
In order to run a server, the following commands may be run:
```bash
cd restSingleUseServer/
go build -o server .
./server
```
This will initialize the server. You may verify its functionality by checking
the `server.log` file. It is a long-running process which may be
stopped by a user inputted kill signal. This will create a file
`restSingleUseServer.xxc`, which is the contact file for the server.
A REST-like client may parse this file in order to send a request to this
server.
### Building a Client
Please follow the steps above before continuing to these instructions.
In order to run the client, you must first move the aforementioned
`restSingleUseServer.xxc` file to the path where you will run the client.
```bash
cd restSingleUseServer/
cp restSingleUseServer.xxc /path/to/restSingleUseClient
```
Once the contact object is local to the client, you may build and run
the client:
```bash
cd restSingleUseClient/
go build -o client .
./client
```
This is a long-running process which may be stopped by a user inputted kill
signal. We recommend allow the process to run for a long enough time to complete
its requests to the server and receive the server's responses. We go into detail
on what this entails below.
Once the REST-like client has set up and sent its request, you can verify
by checking the server's log for this string `Request received:`
```bash
grep "Request received" restSingleUseServer/server.log
INFO 2022/07/07 10:55:57.623516 Request received: headers:{headers:"This is a header"} method:1 uri:"handleClient"
INFO 2022/07/07 10:56:21.181945 Request received: headers:{headers:"This is a header"} method:1 uri:"handleClient"
```
By default, the client sends two requests, synchronous and asynchronous. Both
requests should be received by the server in order to accomplish a successful
client-server request.
In order to verify the response, look at the client log for the string
`Response: `:
```bash
grep "Response: " restSingleUseClient/client.log
INFO 2022/07/07 11:43:42.923030 Response: content:"This is content" headers:{headers:"this is a response"}
INFO 2022/07/07 11:43:50.376968 Response: content:"This is content" headers:{headers:"this is a response"}
```
As by default, there are two requests received by the server, the client will
receive two responses.
module restlikeClientExample
go 1.18
require (
github.com/spf13/jwalterweatherman v1.1.0
gitlab.com/elixxir/client v1.5.1-0.20220706193049-a0b718049663
)
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/crypto v0.0.7-0.20220606201132-c370d5039cea // indirect
gitlab.com/elixxir/ekv v0.1.7 // indirect
gitlab.com/elixxir/primitives v0.0.3-0.20220606195757-40f7a589347f // 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/primitives v0.0.4-0.20220630163313-7890038258c6 // 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 (
"github.com/pkg/errors"
jww "github.com/spf13/jwalterweatherman"
"gitlab.com/elixxir/client/restlike"
restSingle "gitlab.com/elixxir/client/restlike/single"
"gitlab.com/elixxir/client/single"
"gitlab.com/elixxir/client/xxdk"
"gitlab.com/elixxir/crypto/contact"
"io/fs"
"io/ioutil"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
// Logging
initLog(1, "client.log")
// Create a new client object----------------------------------------------
// NOTE: For some (or all) of these parameters, you may want to use a
// configuration tool of some kind
// Path to the server contact file
serverContactPath := "restSingleUseServer.xxc"
// Set state file parameters
statePath := "statePath"
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"
// Set the restlike parameters
exampleURI := restlike.URI("handleClient")
exampleMethod := restlike.Get
exampleContentBytes := []byte("this is some content")
exampleContent := restlike.Data{}
copy(exampleContent[:], exampleContentBytes)
exampleHeaders := &restlike.Headers{
Headers: []byte("This is a header"),
}
singleParams := single.GetDefaultRequestParams()
// 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 using the state file
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)
}
}
// Create an E2E client
// The 'restlike' package handles AuthCallbacks,
// xxdk.DefaultAuthCallbacks is fine here
params := xxdk.GetDefaultE2EParams()
jww.INFO.Printf("Using E2E parameters: %+v", params)
e2eClient, err := xxdk.Login(baseClient, xxdk.DefaultAuthCallbacks{},
identity, params)
if err != nil {
jww.FATAL.Panicf("Unable to Login: %+v", err)
}
// Start network threads---------------------------------------------------
// Set networkFollowerTimeout to a value of your choice (seconds)
networkFollowerTimeout := 5 * time.Second
err = e2eClient.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
e2eClient.GetCmix().AddHealthCallback(
func(isConnected bool) {
connected <- isConnected
})
// Wait until connected or crash on timeout
waitUntilConnected(connected)
// Build contact object----------------------------------------------------
// Recipient's contact (read from a Client CLI-generated contact file)
contactData, err := ioutil.ReadFile(serverContactPath)
if err != nil {
jww.FATAL.Panicf("Failed to read server 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()`
serverContact, err := contact.Unmarshal(contactData)
if err != nil {
jww.FATAL.Panicf("Failed to get contact data: %+v", err)
}
jww.INFO.Printf("Recipient contact: %+v", serverContact)
// Construct request-------------------------------------------------------
stream := e2eClient.GetRng().GetStream()
defer stream.Close()
grp, err := identity.GetGroup()
if err != nil {
jww.FATAL.Panicf("Failed to get group from identity: %+v", err)
}
request := restSingle.Request{
Net: e2eClient.GetCmix(),
Rng: stream,
E2eGrp: grp,
}
// Send request to the server synchronously--------------------------------
// This is a synchronous request, meaning it will block until
// a response is received
response, err := request.Request(serverContact, exampleMethod, exampleURI,
exampleContent, exampleHeaders, singleParams)
if err != nil {
jww.FATAL.Panicf("Failed to call synchronous request "+
"with server: %+v", err)
}
jww.INFO.Printf("Response: %+v", response)
// Send request to the server asynchronously--------------------------------
// In order to asynchronously request, a callback is used to handle
// when the response is received. More complex response handling may be
// implemented within the `restlike.RequestCallback`.
responseChan := make(chan *restlike.Message, 1)
cb := restlike.RequestCallback(func(message *restlike.Message) {
responseChan <- message
})
// Make request
err = request.AsyncRequest(serverContact, exampleMethod, exampleURI,
exampleContent, exampleHeaders, cb, singleParams)
if err != nil {
jww.FATAL.Panicf("Failed to call asynchronous request with server: %+v",
err)
}
response = <-responseChan
jww.INFO.Printf("Response: %+v", response)
// 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)
jww.DEBUG.Printf("Waiting for SIGTERM signal to close process")
<-c
err = e2eClient.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"
"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)
}
}
# xxdk Connection Server Example
# xxdk Restlike Single Use Server Example
This mini-respository contains the example logic for running a basic REST-like
single use server. 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. With that client established, a
REST-like server is build on top. This program creates contact file
`restSingleUseServer` which may be used by a client to contact the
server.
`utils.go` contains utility functions for running the program. In this case,
we provide a tool initializing a log. It also contains a utility to write the
contact file to disk.
`endpoints.go` contains a simple example of request handling by the server.
This prints out the request and builds a response to return the to requester.
This endpoint may be modified for more complex request handling, or more
endpoints with various request handling may be put here.
## Build Instructions
In these instructions we will go over building a REST-like server using our
example. This will not include the scope of running a client receive request.
That documentation may be found in the `README.md` for `restSingleUseClient`.
In order to run a server, the following commands may be run:
```bash
cd restSingleUseServer/
go build -o server .
./server
```
This will initialize the server. You may verify its functionality by checking
the `server.log` file. It is a long-running process which may be
stopped by a user inputted kill signal. This will create a file
`restSingleUseServer.xxc`, which is the contact file for the server.
A REST-like client may parse this file in order to send a request to this
server.
......@@ -12,7 +12,10 @@ import (
// the lower level of the restlike package returns an error
// to the requester.
// User-defined message handling logic goes here.
func Callback(request *restlike.Message) (response *restlike.Message) {
func Callback(request *restlike.Message) *restlike.Message {
jww.INFO.Printf("Request received: %v", request)
return
response := &restlike.Message{}
response.Headers = &restlike.Headers{Headers: []byte("this is a response")}
response.Content = []byte("This is content")
return response
}
......@@ -16,14 +16,14 @@ import (
func main() {
// Logging
initLog(1, "client.log")
initLog(1, "server.log")
// Create a new client object----------------------------------------------
// NOTE: For some (or all) of these parameters, you may want to use a
// configuration tool of some kind
// Set the output contact file path
contactFilePath := "restlikeServer.xxc"
contactFilePath := "restSingleUseServer.xxc"
// Set state file parameters
statePath := "statePath"
......@@ -127,6 +127,7 @@ func main() {
// Initialize the server
restlikeServer := single.NewServer(identity.ID, dhKeyPrivateKey,
grp, e2eClient.GetCmix())
jww.INFO.Printf("Initialized restlike single use server")
// Implement restlike endpoint---------------------------------------------
......@@ -135,6 +136,7 @@ func main() {
if err != nil {
jww.FATAL.Panicf("Failed to add endpoint to server: %v", err)
}
jww.DEBUG.Printf("Added endpoint for restlike single use server")
// Start network threads---------------------------------------------------
......@@ -178,6 +180,7 @@ func main() {
// Wait until the user terminates the program
c := make(chan os.Signal)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
jww.DEBUG.Printf("Waiting for SIGTERM signal to close process")
<-c
err = e2eClient.StopNetworkFollower()
......@@ -189,6 +192,7 @@ func main() {
// Close server on function exit
restlikeServer.Close()
jww.INFO.Printf("Closed restlike server")
os.Exit(0)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment