Skip to content
Snippets Groups Projects
Select Git revision
  • 2732eebc625cdab4c34c2907663c09d65d2c2082
  • main default protected
  • dev protected
  • hotfixes-oct-2022
  • refactor/avatar-cell
  • 1.1.5
  • 1.1.4
  • 1.1.3
  • 1.1
  • 1.0.8
  • 1.0.7
  • 1.0.6
12 results

readme.md

Blame
  • ud.go 21.03 KiB
    ////////////////////////////////////////////////////////////////////////////////
    // Copyright © 2022 xx foundation                                             //
    //                                                                            //
    // Use of this source code is governed by a license that can be found in the  //
    // LICENSE file.                                                              //
    ////////////////////////////////////////////////////////////////////////////////
    
    package bindings
    
    import (
    	"encoding/json"
    	"fmt"
    	"github.com/pkg/errors"
    	jww "github.com/spf13/jwalterweatherman"
    	"gitlab.com/elixxir/client/single"
    	"gitlab.com/elixxir/client/ud"
    	"gitlab.com/elixxir/client/xxdk"
    	"gitlab.com/elixxir/crypto/contact"
    	"gitlab.com/elixxir/primitives/fact"
    	"gitlab.com/xx_network/primitives/id"
    	"sync"
    )
    
    ////////////////////////////////////////////////////////////////////////////////
    // Singleton Tracker                                                          //
    ////////////////////////////////////////////////////////////////////////////////
    
    // udTrackerSingleton is used to track UserDiscovery objects so that they can be
    // referenced by ID back over the bindings.
    var udTrackerSingleton = &udTracker{
    	tracked: make(map[int]*UserDiscovery),
    	count:   0,
    }
    
    // udTracker is a singleton used to keep track of extant UserDiscovery objects,
    // preventing race conditions created by passing it over the bindings.
    type udTracker struct {
    	tracked map[int]*UserDiscovery
    	count   int
    	mux     sync.RWMutex
    }
    
    // make create a UserDiscovery from an ud.Manager, assigns it a unique ID, and
    // adds it to the udTracker.
    func (ut *udTracker) make(u *ud.Manager) *UserDiscovery {
    	ut.mux.Lock()
    	defer ut.mux.Unlock()
    
    	id := ut.count
    	ut.count++
    
    	ut.tracked[id] = &UserDiscovery{
    		api: u,
    		id:  id,
    	}
    
    	return ut.tracked[id]
    }
    
    // get an UserDiscovery from the udTracker given its ID.
    func (ut *udTracker) get(id int) (*UserDiscovery, error) {
    	ut.mux.RLock()
    	defer ut.mux.RUnlock()
    
    	c, exist := ut.tracked[id]
    	if !exist {
    		return nil, errors.Errorf(
    			"Cannot get UserDiscovery for ID %d, does not exist", id)
    	}
    
    	return c, nil
    }
    
    // delete removes a UserDiscovery from the udTracker.
    func (ut *udTracker) delete(id int) {
    	ut.mux.Lock()
    	defer ut.mux.Unlock()
    
    	delete(ut.tracked, id)
    }
    
    ////////////////////////////////////////////////////////////////////////////////
    // Structs and Interfaces                                                     //
    ////////////////////////////////////////////////////////////////////////////////
    
    // UserDiscovery is a bindings-layer struct that wraps an ud.Manager interface.
    type UserDiscovery struct {
    	api *ud.Manager
    	id  int
    }
    
    // GetID returns the udTracker ID for the UserDiscovery object.
    func (ud *UserDiscovery) GetID() int {
    	return ud.id
    }
    
    // UdNetworkStatus contains the UdNetworkStatus, which is a bindings-level
    // interface for ud.udNetworkStatus.
    type UdNetworkStatus interface {
    	// UdNetworkStatus returns:
    	// - int - a xxdk.Status int
    	UdNetworkStatus() int
    }
    
    ////////////////////////////////////////////////////////////////////////////////
    // Manager functions                                                          //
    ////////////////////////////////////////////////////////////////////////////////
    
    // IsRegisteredWithUD is a function which checks the internal state
    // files to determine if a user has registered with UD in the past.
    //
    // Parameters:
    //  - e2eID -  REQUIRED. The tracked e2e object ID. This can be retrieved using [E2e.GetID].
    //
    // Returns:
    //   - bool - A boolean representing true if the user has been registered with UD already
    //            or false if it has not been registered already.
    //  - error - An error should only be returned if the internal tracker failed to retrieve an
    //            E2e object given the e2eId. If an error was returned, the registration state check
    //            was not performed properly, and the boolean returned should be ignored.
    func IsRegisteredWithUD(e2eId int) (bool, error) {
    
    	// Get user from singleton
    	user, err := e2eTrackerSingleton.get(e2eId)
    	if err != nil {
    		return false, err
    	}
    
    	return ud.IsRegistered(user.api.GetStorage().GetKV()), nil
    }
    
    // NewOrLoadUd loads an existing UserDiscovery from storage or creates a new
    // UserDiscovery if there is no storage data. Regardless of storage state,
    // the UserDiscovery object returned will be registered with the
    // User Discovery service. If the user is not already registered, a call
    // to register will occur internally. If the user is already registered,
    // this call will simply load state and return to you a UserDiscovery object.
    // Some parameters are required for registering with the service, but are not required
    // if the user is already registered. These will be noted in the parameters section as
    // "SEMI-REQUIRED".
    //
    // Certain parameters are required every call to this function. These parameters are listed below
    // as "REQUIRED". For example, parameters need be provided to specify how to connect to the
    // User Discovery service. These parameters specifically may be used to contact either the UD
    // server hosted by the xx network team or a custom third-party operated server. For the former,
    // all the information may be fetched from the NDF using the bindings. These fetch
    // methods are detailed in the parameters section.
    //
    // Params
    //  - e2eID -  REQUIRED. The tracked e2e object ID. This is returned by [E2e.GetID].
    //  - follower - REQUIRED. Network follower function. This will check if the network
    //    follower is running.
    //  - username - SEMI-REQUIRED. The username the user wants to register with UD.
    //    If the user is already registered, this field may be blank. If the user is not
    //    already registered, these field must be populated with a username that meets the
    //    requirements of the UD service. For example, in the xx network's UD service,
    //    the username must not be registered by another user.
    //  - registrationValidationSignature - SEMI-REQUIRED. A signature provided by the xx network
    //    (i.e. the client registrar). If the user is not already registered, this field is required
    //    in order to register with the xx network. This may be nil if the user is already registered
    //    or connecting to a third-party UD service unassociated with the xx network.
    //  - cert - REQUIRED. The TLS certificate for the UD server this call will connect with.
    //    If this is nil, you may not contact the UD server hosted by the xx network.
    //    Third-party services may vary.
    //    You may use the UD server run by the xx network team by using [E2e.GetUdCertFromNdf].
    //  - contactFile - REQUIRED. The data within a marshalled [contact.Contact]. This represents the
    //    contact file of the server this call will connect with.
    //    If this is nil, you may not contact the UD server hosted by the xx network.
    //    Third-party services may vary.
    //    You may use the UD server run by the xx network team by using [E2e.GetUdContactFromNdf].
    //  - address - REQUIRED. The IP address of the UD server this call will connect with.
    //    You may use the UD server run by the xx network team by using [E2e.GetUdAddressFromNdf].
    //    If this is nil, you may not contact the UD server hosted by the xx network.
    //    Third-party services may vary.
    //
    // Returns
    //  - A Manager object which is registered to the specified UD service.
    func NewOrLoadUd(e2eID int, follower UdNetworkStatus, username string,
    	registrationValidationSignature, cert, contactFile []byte, address string) (
    	*UserDiscovery, error) {
    
    	// Get user from singleton
    	user, err := e2eTrackerSingleton.get(e2eID)
    	if err != nil {
    		return nil, err
    	}
    
    	// Construct callback
    	UdNetworkStatusFn := func() xxdk.Status {
    		return xxdk.Status(follower.UdNetworkStatus())
    	}
    
    	// Build manager
    	u, err := ud.NewOrLoad(user.api, user.api.GetComms(),
    		UdNetworkStatusFn, username, registrationValidationSignature,
    		cert, contactFile, address)
    	if err != nil {
    		return nil, err
    	}
    
    	// Track and return manager
    	return udTrackerSingleton.make(u), nil
    }
    
    // NewUdManagerFromBackup builds a new user discover manager from a backup. It
    // will construct a manager that is already registered. Confirmed facts have
    // already been restored via the call NewCmixFromBackup.
    //
    // Parameters:
    //  - e2eID - e2e object ID in the tracker
    //  - follower - network follower func wrapped in UdNetworkStatus
    //  - cert - the TLS certificate for the UD server this call will connect with.
    //    You may use the UD server run by the xx network team by using
    //    [E2e.GetUdCertFromNdf].
    //  - contactFile - the data within a marshalled [contact.Contact]. This
    //    represents the contact file of the server this call will connect with. You
    //    may use the UD server run by the xx network team by using
    //    [E2e.GetUdContactFromNdf].
    //  - address - the IP address of the UD server this call will connect with. You
    //    may use the UD server run by the xx network team by using
    //    [E2e.GetUdAddressFromNdf].
    func NewUdManagerFromBackup(e2eID int, follower UdNetworkStatus,
    	cert, contactFile []byte, address string) (*UserDiscovery, error) {
    
    	// Get user from singleton
    	user, err := e2eTrackerSingleton.get(e2eID)
    	if err != nil {
    		return nil, err
    	}
    
    	UdNetworkStatusFn := func() xxdk.Status {
    		return xxdk.Status(follower.UdNetworkStatus())
    	}
    
    	u, err := ud.NewManagerFromBackup(user.api, user.api.GetComms(),
    		UdNetworkStatusFn, cert, contactFile, address)
    	if err != nil {
    		return nil, err
    	}
    
    	return udTrackerSingleton.make(u), nil
    }
    
    // GetFacts returns a JSON marshalled list of [fact.Fact] objects that exist
    // within the Store's registeredFacts map.
    func (ud *UserDiscovery) GetFacts() []byte {
    	jsonData, err := json.Marshal(ud.api.GetFacts())
    	if err != nil {
    		jww.FATAL.Panicf("Failed to JSON marshal fact list: %+v", err)
    	}
    	return jsonData
    }
    
    // GetContact returns the marshalled bytes of the contact.Contact for UD as
    // retrieved from the NDF.
    func (ud *UserDiscovery) GetContact() ([]byte, error) {
    	return ud.api.GetContact().Marshal(), nil
    }
    
    // ConfirmFact confirms a fact first registered via SendRegisterFact. The
    // confirmation ID comes from SendRegisterFact while the code will come over the
    // associated communications system.
    func (ud *UserDiscovery) ConfirmFact(confirmationID, code string) error {
    	return ud.api.ConfirmFact(confirmationID, code)
    }
    
    // SendRegisterFact adds a fact for the user to user discovery. Will only
    // succeed if the user is already registered and the system does not have the
    // fact currently registered for any user.
    //
    // This does not complete the fact registration process, it returns a
    // confirmation ID instead. Over the communications system the fact is
    // associated with, a code will be sent. This confirmation ID needs to be called
    // along with the code to finalize the fact.
    //
    // Parameters:
    //  - factJson - a JSON marshalled [fact.Fact]
    func (ud *UserDiscovery) SendRegisterFact(factJson []byte) (string, error) {
    	var f fact.Fact
    	err := json.Unmarshal(factJson, &f)
    	if err != nil {
    		return "", err
    	}
    
    	return ud.api.SendRegisterFact(f)
    }
    
    // PermanentDeleteAccount removes the username associated with this user from
    // the UD service. This will only take a username type fact, and the fact must
    // be associated with this user.
    //
    // Parameters:
    //  - factJson - a JSON marshalled [fact.Fact]
    func (ud *UserDiscovery) PermanentDeleteAccount(factJson []byte) error {
    	var f fact.Fact
    	err := json.Unmarshal(factJson, &f)
    	if err != nil {
    		return err
    	}
    
    	return ud.api.PermanentDeleteAccount(f)
    }
    
    // RemoveFact removes a previously confirmed fact. This will fail if the fact
    // passed in is not UD service does not associate this fact with this user.
    //
    // Parameters:
    //  - factJson - a JSON marshalled [fact.Fact]
    func (ud *UserDiscovery) RemoveFact(factJson []byte) error {
    	var f fact.Fact
    	err := json.Unmarshal(factJson, &f)
    	if err != nil {
    		return err
    	}
    
    	return ud.api.RemoveFact(f)
    }
    
    ////////////////////////////////////////////////////////////////////////////////
    // User Discovery Lookup                                                      //
    ////////////////////////////////////////////////////////////////////////////////
    
    // UdLookupCallback contains the callback called by LookupUD that returns the
    // contact that matches the passed in ID.
    //
    // Parameters:
    //  - contactBytes - the marshalled bytes of contact.Contact returned from the
    //    lookup, or nil if an error occurs
    //  - err - any errors that occurred in the lookup
    type UdLookupCallback interface {
    	Callback(contactBytes []byte, err error)
    }
    
    // LookupUD returns the public key of the passed ID as known by the user
    // discovery system or returns by the timeout.
    //
    // Parameters:
    //  - e2eID - e2e object ID in the tracker
    //  - udContact - the marshalled bytes of the contact.Contact object
    //  - lookupId - the marshalled bytes of the id.ID object for the user that
    //    LookupUD will look up.
    //  - singleRequestParams - the JSON marshalled bytes of single.RequestParams
    //
    // Returns:
    //  - []byte - the JSON marshalled bytes of the SingleUseSendReport object,
    //    which can be passed into Cmix.WaitForRoundResult to see if the send
    //    succeeded.
    func LookupUD(e2eID int, udContact []byte, cb UdLookupCallback,
    	lookupId []byte, singleRequestParamsJSON []byte) ([]byte, error) {
    
    	// Get user from singleton
    	user, err := e2eTrackerSingleton.get(e2eID)
    	if err != nil {
    		return nil, err
    	}
    
    	c, err := contact.Unmarshal(udContact)
    	if err != nil {
    		return nil, err
    	}
    
    	uid, err := id.Unmarshal(lookupId)
    	if err != nil {
    		return nil, err
    	}
    
    	var p single.RequestParams
    	err = json.Unmarshal(singleRequestParamsJSON, &p)
    	if err != nil {
    		return nil, err
    	}
    
    	callback := func(c contact.Contact, err error) {
    		cb.Callback(c.Marshal(), err)
    	}
    
    	rids, eid, err := ud.Lookup(user.api, c, callback, uid, p)
    	if err != nil {
    		return nil, err
    	}
    
    	sr := SingleUseSendReport{
    		EphID:       eid.EphId.Int64(),
    		ReceptionID: eid.Source,
    		RoundsList:  makeRoundsList(rids...),
    		RoundURL:    getRoundURL(rids[0]),
    	}
    
    	return json.Marshal(sr)
    }
    
    ////////////////////////////////////////////////////////////////////////////////
    // User Discovery MultiLookup                                                      //
    ////////////////////////////////////////////////////////////////////////////////
    
    // UdMultiLookupCallback contains the callback called by MultiLookupUD that returns the
    // contacts which match the passed in IDs.
    //
    // Parameters:
    //  - contactListJSON - the JSON marshalled bytes of []contact.Contact, or nil
    //    if an error occurs.
    //
    //   JSON Example:
    //   {
    //  	"<xxc(2)F8dL9EC6gy+RMJuk3R+Au6eGExo02Wfio5cacjBcJRwDEgB7Ugdw/BAr6RkCABkWAFV1c2VybmFtZTA7c4LzV05sG+DMt+rFB0NIJg==xxc>",
    //  	"<xxc(2)eMhAi/pYkW5jCmvKE5ZaTglQb+fTo1D8NxVitr5CCFADEgB7Ugdw/BAr6RoCABkWAFV1c2VybmFtZTE7fElAa7z3IcrYrrkwNjMS2w==xxc>",
    //  	"<xxc(2)d7RJTu61Vy1lDThDMn8rYIiKSe1uXA/RCvvcIhq5Yg4DEgB7Ugdw/BAr6RsCABkWAFV1c2VybmFtZTI7N3XWrxIUpR29atpFMkcR6A==xxc>"
    //	}
    //  - failedIDs - JSON marshalled list of []*id.ID objects which failed lookup
    //  - err - any errors that occurred in the multilookup.
    type UdMultiLookupCallback interface {
    	Callback(contactListJSON []byte, failedIDs []byte, err error)
    }
    
    type lookupResp struct {
    	Id      *id.ID
    	Contact contact.Contact
    	Err     error
    }
    
    // MultiLookupUD returns the public key of all passed in IDs as known by the
    // user discovery system or returns by the timeout.
    //
    // Parameters:
    //  - e2eID - e2e object ID in the tracker
    //  - udContact - the marshalled bytes of the contact.Contact object
    //  - lookupIds - JSON marshalled list of []*id.ID object for the users that
    //    MultiLookupUD will look up.
    //  - singleRequestParams - the JSON marshalled bytes of single.RequestParams
    //
    // Returns:
    //  - []byte - the JSON marshalled bytes of the SingleUseSendReport object,
    //    which can be passed into Cmix.WaitForRoundResult to see if the send
    //    succeeded.
    func MultiLookupUD(e2eID int, udContact []byte, cb UdMultiLookupCallback,
    	lookupIds []byte, singleRequestParamsJSON []byte) error {
    
    	// Get user from singleton
    	user, err := e2eTrackerSingleton.get(e2eID)
    	if err != nil {
    		return err
    	}
    
    	c, err := contact.Unmarshal(udContact)
    	if err != nil {
    		return err
    	}
    
    	var idList []*id.ID
    	err = json.Unmarshal(lookupIds, &idList)
    	if err != nil {
    		return err
    	}
    
    	var p single.RequestParams
    	err = json.Unmarshal(singleRequestParamsJSON, &p)
    	if err != nil {
    		return err
    	}
    
    	jww.INFO.Printf("ud.MultiLookupUD(%s, %s)", idList, p.Timeout)
    
    	respCh := make(chan lookupResp, len(idList))
    	for _, uid := range idList {
    		callback := func(c contact.Contact, err error) {
    			respCh <- lookupResp{
    				Id:      uid,
    				Contact: c,
    				Err:     err,
    			}
    		}
    		go func(localID *id.ID) {
    			_, _, err := ud.Lookup(user.api, c, callback, localID, p)
    			if err != nil {
    				respCh <- lookupResp{
    					Id:      localID,
    					Contact: contact.Contact{},
    					Err:     err,
    				}
    			}
    		}(uid.DeepCopy())
    
    	}
    
    	go func() {
    		marshaledContactList := make([][]byte, 0)
    		var failedIDs []*id.ID
    		var errorString string
    		for numReturned := 0; numReturned < len(idList); numReturned++ {
    			response := <-respCh
    			if response.Err != nil {
    				failedIDs = append(failedIDs, response.Id)
    				errorString = errorString +
    					fmt.Sprintf("Failed to lookup id %s: %+v",
    						response.Id, response.Err)
    			} else {
    				marshaledContactList = append(
    					marshaledContactList, response.Contact.Marshal())
    			}
    		}
    
    		marshalledFailedIds, err := json.Marshal(failedIDs)
    		if err != nil {
    			cb.Callback(nil, nil,
    				errors.WithMessage(err,
    					"Failed to marshal failed IDs"))
    		}
    
    		contactListJSON, err := json.Marshal(marshaledContactList)
    		if err != nil {
    			jww.FATAL.Panicf(
    				"Failed to marshal list of contact.Contact: %+v", err)
    		}
    		cb.Callback(contactListJSON, marshalledFailedIds, errors.New(errorString))
    	}()
    
    	return nil
    }
    
    ////////////////////////////////////////////////////////////////////////////////
    // User Discovery Search                                                      //
    ////////////////////////////////////////////////////////////////////////////////
    
    // UdSearchCallback contains the callback called by SearchUD that returns a list
    // of contact.Contact objects  that match the list of facts passed into
    // SearchUD.
    //
    // Parameters:
    //  - contactListJSON - the JSON marshalled bytes of []contact.Contact, or nil
    //    if an error occurs.
    //
    //   JSON Example:
    //   {
    //  	"<xxc(2)F8dL9EC6gy+RMJuk3R+Au6eGExo02Wfio5cacjBcJRwDEgB7Ugdw/BAr6RkCABkWAFV1c2VybmFtZTA7c4LzV05sG+DMt+rFB0NIJg==xxc>",
    //  	"<xxc(2)eMhAi/pYkW5jCmvKE5ZaTglQb+fTo1D8NxVitr5CCFADEgB7Ugdw/BAr6RoCABkWAFV1c2VybmFtZTE7fElAa7z3IcrYrrkwNjMS2w==xxc>",
    //  	"<xxc(2)d7RJTu61Vy1lDThDMn8rYIiKSe1uXA/RCvvcIhq5Yg4DEgB7Ugdw/BAr6RsCABkWAFV1c2VybmFtZTI7N3XWrxIUpR29atpFMkcR6A==xxc>"
    //	}
    //  - err - any errors that occurred in the search.
    type UdSearchCallback interface {
    	Callback(contactListJSON []byte, err error)
    }
    
    // SearchUD searches user discovery for the passed Facts. The searchCallback
    // will return a list of contacts, each having the facts it hit against. This is
    // NOT intended to be used to search for multiple users at once; that can have a
    // privacy reduction. Instead, it is intended to be used to search for a user
    // where multiple pieces of information is known.
    //
    // Parameters:
    //  - e2eID - e2e object ID in the tracker
    //  - udContact - the marshalled bytes of the contact.Contact for the user
    //    discovery server
    //  - factListJSON - the JSON marshalled bytes of [fact.FactList]
    //  - singleRequestParams - the JSON marshalled bytes of single.RequestParams
    //
    // Returns:
    //  - []byte - the JSON marshalled bytes of the SingleUseSendReport object,
    //    which can be passed into Cmix.WaitForRoundResult to see if the send
    //    operation succeeded.
    func SearchUD(e2eID int, udContact []byte, cb UdSearchCallback,
    	factListJSON, singleRequestParamsJSON []byte) ([]byte, error) {
    
    	// Get user from singleton
    	user, err := e2eTrackerSingleton.get(e2eID)
    	if err != nil {
    		return nil, err
    	}
    
    	c, err := contact.Unmarshal(udContact)
    	if err != nil {
    		return nil, err
    	}
    
    	var list fact.FactList
    	err = json.Unmarshal(factListJSON, &list)
    	if err != nil {
    		return nil, err
    	}
    
    	var p single.RequestParams
    	err = json.Unmarshal(singleRequestParamsJSON, &p)
    	if err != nil {
    		return nil, err
    	}
    
    	callback := func(contactList []contact.Contact, err error) {
    		marshaledContactList := make([][]byte, 0)
    		// fixme: it may be wiser to change this callback interface
    		//   to simply do the work below when parsing the response from UD.
    		//   that would change ud/search.go in two places:
    		//    - searchCallback
    		//    - parseContacts
    		//  I avoid doing that as it changes interfaces w/o approval
    		for i := range contactList {
    			con := contactList[i]
    			marshaledContactList = append(
    				marshaledContactList, con.Marshal())
    		}
    
    		contactListJSON, err2 := json.Marshal(marshaledContactList)
    		if err2 != nil {
    			jww.FATAL.Panicf(
    				"Failed to marshal list of contact.Contact: %+v", err2)
    		}
    
    		cb.Callback(contactListJSON, err)
    	}
    
    	rids, eid, err := ud.Search(user.api, c, callback, list, p)
    	if err != nil {
    		return nil, err
    	}
    
    	sr := SingleUseSendReport{
    		EphID:       eid.EphId.Int64(),
    		ReceptionID: eid.Source,
    		RoundsList:  makeRoundsList(rids...),
    		RoundURL:    getRoundURL(rids[0]),
    	}
    
    	return json.Marshal(sr)
    }