diff --git a/bindings/fileTransfer.go b/bindings/fileTransfer.go new file mode 100644 index 0000000000000000000000000000000000000000..cce492165115b0981cec066a8033ad2016a06abc --- /dev/null +++ b/bindings/fileTransfer.go @@ -0,0 +1,326 @@ +package bindings + +import ( + "encoding/json" + "gitlab.com/elixxir/client/e2e" + "gitlab.com/elixxir/client/e2e/rekey" + "gitlab.com/elixxir/client/fileTransfer" + ftCrypto "gitlab.com/elixxir/crypto/fileTransfer" + "gitlab.com/xx_network/primitives/id" + "time" +) + +/* File Transfer Structs and Interfaces */ + +// FileTransfer object is a bindings-layer struct which wraps a fileTransfer.FileTransfer interface +type FileTransfer struct { + ft fileTransfer.FileTransfer +} + +// ReceivedFile is a public struct which represents the contents of an incoming file +// Example JSON: +// { +// "TransferID":"B4Z9cwU18beRoGbk5xBjbcd5Ryi9ZUFA2UBvi8FOHWo=", // ID of the incoming transfer for receiving +// "SenderID":"emV6aW1hAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD", // ID of sender of incoming file +// "Preview":"aXQncyBtZSBhIHByZXZpZXc=", // Preview of the incoming file +// "Name":"testfile.txt", // Name of incoming file +// "Type":"text file", // Incoming file type +// "Size":2048 // Incoming file size +// } +type ReceivedFile struct { + TransferID []byte + SenderID []byte + Preview []byte + Name string + Type string + Size uint32 +} + +// FileSend is a public struct which represents a file to be transferred +// { +// "Name":"testfile.txt", // File name +// "Type":"text file", // File type +// "Preview":"aXQncyBtZSBhIHByZXZpZXc=", // Preview of contents +// "Contents":"VGhpcyBpcyB0aGUgZnVsbCBjb250ZW50cyBvZiB0aGUgZmlsZSBpbiBieXRlcw==" // Full contents of the file +// } +type FileSend struct { + Name string + Type string + Preview []byte + Contents []byte +} + +// Progress is a public struct which represents the progress of an in-progress file transfer +// Example JSON: +// {"Completed":false, // Status of transfer (true if done) +// "Transmitted":128, // Bytes transferred so far +// "Total":2048, // Total size of file +// "Err":null // Error status (if any) +// } +type Progress struct { + Completed bool + Transmitted uint16 + Total uint16 + Err error +} + +// ReceiveFileCallback is a bindings-layer interface which is called when a file is received +// Accepts the result of calling json.Marshal on a ReceivedFile struct +type ReceiveFileCallback interface { + Callback(payload []byte, err error) +} + +// FileTransferSentProgressCallback is a bindings-layer interface which is called with the progress of a sending file +// Accepts the result of calling json.Marshal on a Progress struct & a FilePartTracker interface +type FileTransferSentProgressCallback interface { + Callback(payload []byte, t *FilePartTracker, err error) +} + +// FileTransferReceiveProgressCallback is a bindings-layer interface which is called with the progress of a received file +// Accepts the result of calling json.Marshal on a Progress struct & a FilePartTracker interface +type FileTransferReceiveProgressCallback interface { + Callback(payload []byte, t *FilePartTracker, err error) +} + +/* Main functions */ + +// InitFileTransfer creates a bindings-level File Transfer manager +// Accepts client ID, ReceiveFileCallback and a ReporterFunc +func InitFileTransfer(clientID int, cb ReceiveFileCallback, r ReporterFunc) (*FileTransfer, error) { + // Get bindings client from singleton + c, err := clientTrackerSingleton.get(clientID) + if err != nil { + return nil, err + } + + // Create the callback to wrap the ReceiveFileCallback + rcb := func(tid *ftCrypto.TransferID, fileName, fileType string, + sender *id.ID, size uint32, preview []byte) { + f := &ReceivedFile{ + TransferID: tid.Bytes(), + SenderID: sender.Marshal(), + Preview: preview, + Name: fileName, + Type: fileType, + Size: size, + } + cb.Callback(json.Marshal(f)) + } + + // Client info + myID := c.api.GetUser().TransmissionID + kv := c.api.GetStorage().GetKV() + e2eGrp := c.api.GetStorage().GetE2EGroup() + rng := c.api.GetRng() + + // Create e2e manager (should this have a singleton?) + err = e2e.Init(kv, myID, c.api.GetUser().E2eDhPrivateKey, e2eGrp, rekey.GetDefaultParams()) + e2eManager, err := e2e.Load(kv, c.api.GetCmix(), myID, e2eGrp, rng, &reporter{ + r: r, + }) + + // Create file transfer manager + m, err := fileTransfer.NewManager(rcb, fileTransfer.DefaultParams(), myID, + c.api.GetCmix(), e2eManager, kv, rng) + + // Add file transfer processes to client services tracking + err = c.api.AddService(m.StartProcesses) + if err != nil { + return nil, err + } + + // Return wrapped manager + return &FileTransfer{ft: m}, nil +} + +// Send is the bindings-level function for sending a File +func (f *FileTransfer) Send(payload, recipientID []byte, retry float32, + period string, callback FileTransferSentProgressCallback) ([]byte, error) { + // Unmarshal recipient ID + recipient, err := id.Unmarshal(recipientID) + if err != nil { + return nil, err + } + + // Parse duration to time.Duration + p, err := time.ParseDuration(period) + + // Wrap transfer progress callback to be passed to fileTransfer layer + cb := func(completed bool, arrived, total uint16, + t fileTransfer.FilePartTracker, err error) { + prog := &Progress{ + Completed: completed, + Transmitted: arrived, + Total: total, + Err: err, + } + pm, err := json.Marshal(prog) + callback.Callback(pm, &FilePartTracker{t}, err) + } + + // Unmarshal payload + fs := &FileSend{} + err = json.Unmarshal(payload, fs) + if err != nil { + return nil, err + } + + // Send file + ftID, err := f.ft.Send(fs.Name, fs.Type, fs.Contents, recipient, retry, fs.Preview, cb, p) + if err != nil { + return nil, err + } + + // Return Transfer ID + return ftID.Bytes(), nil +} + +// Receive returns the full file on the completion of the transfer. +// It deletes internal references to the data and unregisters any attached +// progress callback. Returns an error if the transfer is not complete, the +// full file cannot be verified, or if the transfer cannot be found. +// +// Receive can only be called once the progress callback returns that the +// file transfer is complete. +func (f *FileTransfer) Receive(tidBytes []byte) ([]byte, error) { + tid := ftCrypto.UnmarshalTransferID(tidBytes) + return f.ft.Receive(&tid) +} + +// CloseSend deletes a file from the internal storage once a transfer has +// completed or reached the retry limit. Returns an error if the transfer +// has not run out of retries. +// +// This function should be called once a transfer completes or errors out +// (as reported by the progress callback). +func (f *FileTransfer) CloseSend(tidBytes []byte) error { + tid := ftCrypto.UnmarshalTransferID(tidBytes) + return f.ft.CloseSend(&tid) +} + +/* Callback registration functions */ + +func (f *FileTransfer) RegisterSentProgressCallback(tidBytes []byte, + callback FileTransferSentProgressCallback, period string) error { + cb := func(completed bool, arrived, total uint16, + t fileTransfer.FilePartTracker, err error) { + prog := &Progress{ + Completed: completed, + Transmitted: arrived, + Total: total, + Err: err, + } + pm, err := json.Marshal(prog) + callback.Callback(pm, &FilePartTracker{t}, err) + } + p, err := time.ParseDuration(period) + if err != nil { + return err + } + tid := ftCrypto.UnmarshalTransferID(tidBytes) + + return f.ft.RegisterSentProgressCallback(&tid, cb, p) +} + +func (f *FileTransfer) RegisterReceivedProgressCallback(tidBytes []byte, callback FileTransferReceiveProgressCallback, period string) error { + cb := func(completed bool, received, total uint16, + t fileTransfer.FilePartTracker, err error) { + prog := &Progress{ + Completed: completed, + Transmitted: received, + Total: total, + Err: err, + } + pm, err := json.Marshal(prog) + callback.Callback(pm, &FilePartTracker{t}, err) + } + p, err := time.ParseDuration(period) + if err != nil { + return err + } + tid := ftCrypto.UnmarshalTransferID(tidBytes) + return f.ft.RegisterReceivedProgressCallback(&tid, cb, p) +} + +/* Utility Functions */ + +func (f *FileTransfer) MaxFileNameLen() int { + return f.ft.MaxFileNameLen() +} + +func (f *FileTransfer) MaxFileTypeLen() int { + return f.ft.MaxFileTypeLen() +} + +func (f *FileTransfer) MaxFileSize() int { + return f.ft.MaxFileSize() +} + +func (f *FileTransfer) MaxPreviewSize() int { + return f.ft.MaxPreviewSize() +} + +//////////////////////////////////////////////////////////////////////////////// +// File Part Tracker // +//////////////////////////////////////////////////////////////////////////////// + +// FilePartTracker contains the interfaces.FilePartTracker. +type FilePartTracker struct { + m fileTransfer.FilePartTracker +} + +// GetPartStatus returns the status of the file part with the given part number. +// The possible values for the status are: +// 0 = unsent +// 1 = sent (sender has sent a part, but it has not arrived) +// 2 = arrived (sender has sent a part, and it has arrived) +// 3 = received (receiver has received a part) +func (fpt *FilePartTracker) GetPartStatus(partNum int) int { + return int(fpt.m.GetPartStatus(uint16(partNum))) +} + +// GetNumParts returns the total number of file parts in the transfer. +func (fpt *FilePartTracker) GetNumParts() int { + return int(fpt.m.GetNumParts()) +} + +//////////////////////////////////////////////////////////////////////////////// +// Event Reporter // +//////////////////////////////////////////////////////////////////////////////// + +// EventReport is a public struct which represents the contents of an event report +// Example JSON: +// {"Priority":1, +// "Category":"Test Events", +// "EventType":"Ping", +// "Details":"This is an example of an event report" +// } +type EventReport struct { + Priority int + Category string + EventType string + Details string +} + +// ReporterFunc is a bindings-layer interface which receives info from the Event Manager +// Accepts result of json.Marshal on an EventReport object +type ReporterFunc interface { + Report(payload []byte, err error) +} + +// reporter is the internal struct to match the event.Reporter interface +type reporter struct { + r ReporterFunc +} + +// Report matches the event.Reporter interface, wraps the info in an EventReport struct +// and passes the marshalled struct to the internal callback +func (r *reporter) Report(priority int, category, evtType, details string) { + rep := &EventReport{ + Priority: priority, + Category: category, + EventType: evtType, + Details: details, + } + r.r.Report(json.Marshal(rep)) +} diff --git a/bindings/fileTransfer_test.go b/bindings/fileTransfer_test.go new file mode 100644 index 0000000000000000000000000000000000000000..a89967891505ea911384361924a746d9094260ff --- /dev/null +++ b/bindings/fileTransfer_test.go @@ -0,0 +1,59 @@ +package bindings + +import ( + "encoding/json" + "gitlab.com/elixxir/crypto/fileTransfer" + "gitlab.com/xx_network/crypto/csprng" + "gitlab.com/xx_network/primitives/id" + "testing" +) + +func TestFileTransfer_inputs(t *testing.T) { + fs := &FileSend{ + Name: "testfile.txt", + Type: "text file", + Preview: []byte("it's me a preview"), + Contents: []byte("This is the full contents of the file in bytes"), + } + fsm, _ := json.Marshal(fs) + t.Log("FileSend example json:") + t.Log(string(fsm)) + t.Log("\n") + + tid, _ := fileTransfer.NewTransferID(csprng.NewSystemRNG()) + sid := id.NewIdFromString("zezima", id.User, t) + rf := &ReceivedFile{ + TransferID: tid.Bytes(), + SenderID: sid.Marshal(), + Preview: []byte("it's me a preview"), + Name: "testfile.txt", + Type: "text file", + Size: 2048, + } + rfm, _ := json.Marshal(rf) + t.Log("ReceivedFile example json:") + t.Log(string(rfm)) + t.Log("\n") + + p := &Progress{ + Completed: false, + Transmitted: 128, + Total: 2048, + Err: nil, + } + pm, _ := json.Marshal(p) + t.Log("Progress example json:") + t.Log(string(pm)) + t.Log("\n") + + er := &EventReport{ + Priority: 1, + Category: "Test Events", + EventType: "Ping", + Details: "This is an example of an event report", + } + erm, _ := json.Marshal(er) + t.Log("EventReport example json:") + t.Log(string(erm)) + t.Log("\n") +} diff --git a/bindings/restlike.go b/bindings/restlike.go index cc45b2e6113578c8cf290cdebee235eddd1f8dbd..98b9c4b450b2b6dc84a8c7c3c054a2daee0e8332 100644 --- a/bindings/restlike.go +++ b/bindings/restlike.go @@ -33,7 +33,7 @@ type RestlikeMessage struct { // RestlikeRequest performs a normal restlike request // request - marshalled RestlikeMessage // Returns marshalled result RestlikeMessage -func RestlikeRequest(clientID int, connectionID int, request []byte) ([]byte, error) { +func RestlikeRequest(clientID, connectionID int, request []byte) ([]byte, error) { cl, err := clientTrackerSingleton.get(clientID) if err != nil { return nil, err diff --git a/bindings/restlikeSingle.go b/bindings/restlikeSingle.go new file mode 100644 index 0000000000000000000000000000000000000000..b39576ce06820435fc9a0d97e28ddfa48e607578 --- /dev/null +++ b/bindings/restlikeSingle.go @@ -0,0 +1,81 @@ +package bindings + +import ( + "encoding/json" + "gitlab.com/elixxir/client/restlike" + "gitlab.com/elixxir/client/restlike/single" + singleuse "gitlab.com/elixxir/client/single" + "gitlab.com/elixxir/crypto/contact" +) + +// RestlikeCallback is the public function type bindings can use to make an asynchronous restlike request +// It accepts a json marshalled restlike.Message and an error (the results of calling json.Marshal on the message) +type RestlikeCallback interface { + Callback([]byte, error) +} + +// RequestRestLike sends a restlike request to a given contact +// Accepts marshalled contact object as recipient, byte slice payload & headers, method enum and a URI +// Returns json marshalled restlike.Message & error +func RequestRestLike(clientID int, recipient, request []byte) ([]byte, error) { + c, err := clientTrackerSingleton.get(clientID) + if err != nil { + return nil, err + } + req := single.Request{ + Net: c.api.GetCmix(), + Rng: c.api.GetRng().GetStream(), + E2eGrp: c.api.GetStorage().GetE2EGroup(), + } + + message := &RestlikeMessage{} + err = json.Unmarshal(request, message) + + recipientContact, err := contact.Unmarshal(recipient) + if err != nil { + return nil, err + } + + resp, err := req.Request(recipientContact, restlike.Method(message.Method), restlike.URI(message.URI), + message.Content, &restlike.Headers{ + Headers: message.Headers, + Version: 0, + }, singleuse.GetDefaultRequestParams()) + if err != nil { + return nil, err + } + return json.Marshal(resp) +} + +// AsyncRequestRestLike sends an asynchronous restlike request to a given contact +// Accepts marshalled contact object as recipient, byte slice payload & headers, method enum, URI, and a RestlikeCallback +// Returns an error, and the RestlikeCallback will be called with the results of json marshalling the response when received +func AsyncRequestRestLike(clientID int, recipient, request []byte, cb RestlikeCallback) error { + c, err := clientTrackerSingleton.get(clientID) + if err != nil { + return err + } + req := single.Request{ + Net: c.api.GetCmix(), + Rng: c.api.GetRng().GetStream(), + E2eGrp: c.api.GetStorage().GetE2EGroup(), + } + + message := &RestlikeMessage{} + err = json.Unmarshal(request, message) + + recipientContact, err := contact.Unmarshal(recipient) + if err != nil { + return err + } + + rlcb := func(message *restlike.Message) { + cb.Callback(json.Marshal(message)) + } + + return req.AsyncRequest(recipientContact, restlike.Method(message.Method), restlike.URI(message.URI), + message.Content, &restlike.Headers{ + Headers: message.Headers, + Version: 0, + }, rlcb, singleuse.GetDefaultRequestParams()) +} diff --git a/bindings/single.go b/bindings/single.go index 23ae6b6ddd32967acf0551cda0e87d38b3b8a6d6..64e79e29a6d4e3c0aac8df3310717b2519740e7b 100644 --- a/bindings/single.go +++ b/bindings/single.go @@ -9,6 +9,47 @@ import ( "gitlab.com/xx_network/primitives/id" ) +/* PUBLIC WRAPPER METHODS */ + +// TransmitSingleUse accepts a marshalled recipient contact object, tag, payload, SingleUseResponse callback func & a +// Client. Transmits payload to recipient via single use +func TransmitSingleUse(clientID int, recipient []byte, tag string, payload []byte, responseCB SingleUseResponse) ([]byte, error) { + cl, err := clientTrackerSingleton.get(clientID) + if err != nil { + return nil, err + } + + recipientContact, err := contact.Unmarshal(recipient) + if err != nil { + return nil, err + } + + rcb := &singleUseResponse{response: responseCB} + + rids, eid, err := single.TransmitRequest(recipientContact, tag, payload, rcb, single.GetDefaultRequestParams(), cl.api.GetCmix(), cl.api.GetRng().GetStream(), cl.api.GetStorage().GetE2EGroup()) + + if err != nil { + return nil, err + } + sr := SingleUseSendReport{ + EphID: eid, + RoundsList: makeRoundsList(rids), + } + return json.Marshal(sr) +} + +// Listen starts a single use listener on a given tag using the passed in client and SingleUseCallback func +func Listen(clientID int, tag string, cb SingleUseCallback) (StopFunc, error) { + cl, err := clientTrackerSingleton.get(clientID) + if err != nil { + return nil, err + } + + listener := singleUseListener{scb: cb} + l := single.Listen(tag, cl.api.GetUser().ReceptionID, cl.api.GetUser().E2eDhPrivateKey, cl.api.GetCmix(), cl.api.GetStorage().GetE2EGroup(), listener) + return l.Stop, nil +} + // JSON Types // SingleUseSendReport is the bindings struct used to represent information returned by single.TransmitRequest @@ -61,11 +102,15 @@ type StopFunc func() // SingleUseCallback func is passed into Listen and called when messages are received // Accepts a SingleUseCallbackReport marshalled to json -type SingleUseCallback func(callbackReport []byte, err error) +type SingleUseCallback interface { + Callback(callbackReport []byte, err error) +} // SingleUseResponse is the public facing callback func passed by bindings clients into TransmitSingleUse // Accepts a SingleUseResponseReport marshalled to json -type SingleUseResponse func(responseReport []byte, err error) +type SingleUseResponse interface { + Callback(responseReport []byte, err error) +} /* CALLBACK WRAPPERS */ @@ -93,14 +138,14 @@ func (sl singleUseListener) Callback(req *single.Request, eid receptionID.Epheme EphID: eid, } - sl.scb(json.Marshal(scr)) + sl.scb.Callback(json.Marshal(scr)) } /* response struct */ // singleUseResponse is the private struct backing SingleUseResponse, which subscribes to the single.Response interface type singleUseResponse struct { - responseFunc SingleUseResponse + response SingleUseResponse } // Callback builds a SingleUseSendReport & passes the json marshalled version into the callback @@ -116,36 +161,5 @@ func (sr singleUseResponse) Callback(payload []byte, receptionID receptionID.Eph Payload: payload, Err: err, } - sr.responseFunc(json.Marshal(&sendReport)) -} - -/* PUBLIC WRAPPER METHODS */ - -// TransmitSingleUse accepts a marshalled recipient contact object, tag, payload, SingleUseResponse callback func & a -// Client. Transmits payload to recipient via single use -func TransmitSingleUse(recipient []byte, tag string, payload []byte, responseCB SingleUseResponse, cl *Client) ([]byte, error) { - recipientContact, err := contact.Unmarshal(recipient) - if err != nil { - return nil, err - } - - rcb := &singleUseResponse{responseFunc: responseCB} - - rids, eid, err := single.TransmitRequest(recipientContact, tag, payload, rcb, single.GetDefaultRequestParams(), cl.api.GetCmix(), cl.api.GetRng().GetStream(), cl.api.GetStorage().GetE2EGroup()) - - if err != nil { - return nil, err - } - sr := SingleUseSendReport{ - EphID: eid, - RoundsList: makeRoundsList(rids), - } - return json.Marshal(sr) -} - -// Listen starts a single use listener on a given tag using the passed in client and SingleUseCallback func -func Listen(tag string, cl *Client, cb SingleUseCallback) StopFunc { - listener := singleUseListener{scb: cb} - l := single.Listen(tag, cl.api.GetUser().ReceptionID, cl.api.GetUser().E2eDhPrivateKey, cl.api.GetCmix(), cl.api.GetStorage().GetE2EGroup(), listener) - return l.Stop + sr.response.Callback(json.Marshal(&sendReport)) }