/* CreateDocument - Creates a fresh Binder for a new document, which is subsequently stored, returns an error if either the document ID is already currently in use, or if there is a problem storing the new document. May require authentication, if so a userID is supplied. */ func (c *Curator) CreateDocument(token string, userID string, doc store.Document) (BinderPortal, error) { c.log.Debugf("Creating new document with token %v\n", token) if !c.authenticator.AuthoriseCreate(token, userID) { c.stats.Incr("curator.create.rejected_client", 1) return BinderPortal{}, fmt.Errorf("failed to gain permission to create with token: %v\n", token) } c.stats.Incr("curator.create.accepted_client", 1) // Always generate a fresh ID doc.ID = util.GenerateStampedUUID() if err := c.store.Create(doc.ID, doc); err != nil { c.stats.Incr("curator.create_new.failed", 1) c.log.Errorf("Failed to create new document: %v\n", err) return BinderPortal{}, err } binder, err := NewBinder(doc.ID, c.store, c.config.BinderConfig, c.errorChan, c.log, c.stats) if err != nil { c.stats.Incr("curator.bind_new.failed", 1) c.log.Errorf("Failed to bind to new document: %v\n", err) return BinderPortal{}, err } c.binderMutex.Lock() c.openBinders[doc.ID] = binder c.binderMutex.Unlock() c.stats.Incr("curator.open_binders", 1) return binder.Subscribe(token), nil }
/* CreateDocument - Creates a fresh Binder for a new document, which is subsequently stored, returns an error if either the document ID is already currently in use, or if there is a problem storing the new document. May require authentication, if so a userID is supplied. */ func (c *impl) CreateDocument( userID, token string, doc store.Document, timeout time.Duration, ) (binder.Portal, error) { c.log.Debugf("Creating new document with userID %v token %v\n", userID, token) if c.auth.Authenticate(userID, token, "") < acl.CreateAccess { c.stats.Incr("curator.create.rejected_client", 1) return nil, fmt.Errorf("failed to gain permission to create with token: %v\n", token) } c.stats.Incr("curator.create.accepted_client", 1) // Always generate a fresh ID doc.ID = util.GenerateStampedUUID() if err := c.store.Create(doc); err != nil { c.stats.Incr("curator.create_new.failed", 1) c.log.Errorf("Failed to create new document: %v\n", err) return nil, err } openBinder, err := binder.New( doc.ID, c.store, c.config.BinderConfig, c.errorChan, c.log, c.stats, ) if err != nil { c.stats.Incr("curator.bind_new.failed", 1) c.log.Errorf("Failed to bind to new document: %v\n", err) return nil, err } c.binderMutex.Lock() c.openBinders[doc.ID] = openBinder c.binderMutex.Unlock() c.stats.Incr("curator.open_binders", 1) return openBinder.Subscribe(userID, timeout) }
/* NewDocument - Create a fresh leap document with a title, description, type and the initial content. */ func NewDocument(content string) (*Document, error) { doc := &Document{ ID: util.GenerateStampedUUID(), Content: content, } return doc, nil }
func (h *HTTP) createGenerateTokenHandler(tokens tokensMap) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { http.Error(w, "POST endpoint only", http.StatusMethodNotAllowed) return } bytes, err := ioutil.ReadAll(r.Body) if err != nil { h.logger.Errorf("Failed to read request body: %v\n", err) http.Error(w, "Bad request: could not read body", http.StatusBadRequest) return } var bodyObj struct { Key string `json:"key_value"` } if err = json.Unmarshal(bytes, &bodyObj); err != nil { h.logger.Errorf("Failed to parse request body: %v\n", err) http.Error(w, "Bad request: could not parse body", http.StatusBadRequest) return } if 0 == len(bodyObj.Key) { h.logger.Errorln("User ID not found in request body") http.Error(w, "Bad request: no user id found", http.StatusBadRequest) return } token := util.GenerateStampedUUID() h.mutex.Lock() tokens[token] = tokenMapValue{ value: bodyObj.Key, expires: time.Now().Add(time.Second * time.Duration(h.config.HTTPConfig.ExpiryPeriod)), } h.mutex.Unlock() resBytes, err := json.Marshal(struct { Token string `json:"token"` }{ Token: token, }) if err != nil { h.logger.Errorf("Failed to generate JSON response: %v\n", err) http.Error(w, "Failed to generate response", http.StatusInternalServerError) return } w.Write(resBytes) w.Header().Add("Content-Type", "application/json") h.clearExpiredTokens(tokens) } }
/* Subscribe - Returns a BinderPortal, which represents a contract between a client and the binder. If the subscription was unsuccessful the BinderPortal will contain an error. */ func (b *Binder) Subscribe(token string) BinderPortal { if len(token) == 0 { token = util.GenerateStampedUUID() } retChan := make(chan BinderPortal, 1) bundle := BinderSubscribeBundle{ PortalRcvChan: retChan, Token: token, } b.subscribeChan <- bundle return <-retChan }
/* SubscribeReadOnly - Returns a BinderPortal, which represents a contract between a client and the binder. If the subscription was unsuccessful the BinderPortal will contain an error. This is a read only version of a BinderPortal and means transforms will be received but cannot be submitted. */ func (b *Binder) SubscribeReadOnly(token string) BinderPortal { if len(token) == 0 { token = util.GenerateStampedUUID() } retChan := make(chan BinderPortal, 1) bundle := BinderSubscribeBundle{ PortalRcvChan: retChan, Token: token, } b.subscribeChan <- bundle portal := <-retChan portal.TransformSndChan = nil return portal }
/* processSubscriber - Processes a prospective client wishing to subscribe to this binder. This involves flushing the OTBuffer in order to obtain a clean version of the document, if this fails we return false to flag the binder loop that we should shut down. */ func (b *impl) processSubscriber(request subscribeRequest) error { transformSndChan := make(chan text.OTransform, 1) updateSndChan := make(chan ClientUpdate, 1) var err error var doc store.Document // We need to read the full document here anyway, so might as well flush. doc, err = b.flush() if err != nil { select { case request.errChan <- err: default: } return err } client := binderClient{ userID: request.userID, sessionID: util.GenerateStampedUUID(), transformChan: transformSndChan, updateChan: updateSndChan, } portal := portalImpl{ client: &client, version: b.otBuffer.GetVersion(), document: doc, transformRcvChan: transformSndChan, updateRcvChan: updateSndChan, transformSndChan: b.transformChan, messageSndChan: b.messageChan, exitChan: b.exitChan, } select { case request.portalChan <- &portal: b.stats.Incr("binder.subscribed_clients", 1) b.log.Debugf("Subscribed new client %v\n", request.userID) b.clients = append(b.clients, &client) case <-time.After(time.Duration(b.config.ClientKickPeriod) * time.Millisecond): /* We're not bothered if you suck, you just don't get enrolled, and this isn't * considered an error. Deal with it. */ b.stats.Incr("binder.rejected_client", 1) b.log.Infof("Rejected client request %v\n", request.userID) } return nil }
func TestUpdatesSameUserID(t *testing.T) { errChan := make(chan Error) doc, _ := store.NewDocument("hello world") logger, stats := loggerAndStats() binder, err := New( doc.ID, &testStore{documents: map[string]store.Document{doc.ID: *doc}}, NewConfig(), errChan, logger, stats, ) if err != nil { t.Errorf("error: %v", err) return } go func() { for e := range errChan { t.Errorf("From error channel: %v", e.Err) } }() userID := util.GenerateStampedUUID() portal1, _ := binder.Subscribe(userID, time.Second) portal2, _ := binder.Subscribe(userID, time.Second) if userID != portal1.UserID() { t.Errorf("Binder portal wrong user ID: %v != %v", userID, portal1.UserID()) } if userID != portal2.UserID() { t.Errorf("Binder portal wrong user ID: %v != %v", userID, portal2.UserID()) } for i := 0; i < 100; i++ { portal1.SendMessage(Message{}) message := <-portal2.UpdateReadChan() if message.ClientInfo.UserID != userID { t.Errorf( "Received incorrect user ID: %v != %v", message.ClientInfo.UserID, portal1.UserID(), ) } if message.ClientInfo.SessionID != portal1.SessionID() { t.Errorf( "Received incorrect session ID: %v != %v", message.ClientInfo.SessionID, portal1.SessionID(), ) } portal2.SendMessage(Message{}) message2 := <-portal1.UpdateReadChan() if message2.ClientInfo.UserID != userID { t.Errorf( "Received incorrect token: %v != %v", message2.ClientInfo.UserID, portal2.UserID(), ) } if message2.ClientInfo.SessionID != portal2.SessionID() { t.Errorf( "Received incorrect session ID: %v != %v", message2.ClientInfo.SessionID, portal2.SessionID(), ) } } }
func TestClientAdminTasks(t *testing.T) { errChan := make(chan Error, 10) logger, stats := loggerAndStats() doc, _ := store.NewDocument("hello world") store := testStore{documents: map[string]store.Document{ "KILL_ME": *doc, }} binder, err := New("KILL_ME", &store, NewConfig(), errChan, logger, stats) if err != nil { t.Errorf("Error: %v", err) return } nClients := 10 portals := make([]Portal, nClients) clientIDs := make([]string, nClients) for i := 0; i < nClients; i++ { clientIDs[i] = util.GenerateStampedUUID() var err error portals[i], err = binder.Subscribe(clientIDs[i], time.Second) if err != nil { t.Errorf("Subscribe error: %v\n", err) return } } for i := 0; i < nClients; i++ { remainingClients, err := binder.GetUsers(time.Second) if err != nil { t.Errorf("Get users error: %v\n", err) return } if len(remainingClients) != len(clientIDs) { t.Errorf("Wrong number of remaining clients: %v != %v\n", len(remainingClients), len(clientIDs)) return } for _, val := range clientIDs { found := false for _, c := range remainingClients { if val == c { found = true break } } if !found { t.Errorf("Client not found in binder: %v\n", val) return } } killID := clientIDs[0] clientIDs = clientIDs[1:] if err := binder.KickUser(killID, time.Second); err != nil { t.Errorf("Kick user error: %v\n", err) return } } binder.Close() }
func TestBadKeys(t *testing.T) { config := NewConfig() config.AllowCreate = true config.HTTPConfig.Path = "/test" config.HTTPConfig.ExpiryPeriod = 300 log, stats := loggerAndStats() httpAuth := NewHTTP(config, log, stats) for i := 0; i < 1000; i++ { uuid := util.GenerateStampedUUID() bodyReader := bytes.NewReader([]byte(fmt.Sprintf(`{"key_value":"%v"}`, uuid))) req, _ := http.NewRequest("POST", "http://localhost:8001/test/create", bodyReader) dWriter := dummyWriter{header: http.Header{}, Token: ""} switch i % 3 { case 0: httpAuth.joinHandler(&dWriter, req) if _, ok := httpAuth.tokensJoin[dWriter.Token]; !ok { t.Errorf("Token not added: %v", dWriter.Token) } case 1: httpAuth.createHandler(&dWriter, req) if _, ok := httpAuth.tokensCreate[dWriter.Token]; !ok { t.Errorf("Token not added: %v", dWriter.Token) } default: httpAuth.readOnlyHandler(&dWriter, req) if _, ok := httpAuth.tokensReadOnly[dWriter.Token]; !ok { t.Errorf("Token not added: %v", dWriter.Token) } } } // Check existing tokens with random values for token, key := range httpAuth.tokensJoin { randomKey := util.GenerateStampedUUID() if httpAuth.AuthoriseJoin(token, randomKey) { if key.value != randomKey { t.Errorf("Authorised join random key: %v %v", token, randomKey) } } if httpAuth.AuthoriseCreate(token, randomKey) { if key.value != randomKey { t.Errorf("Authorised create random key: %v %v", token, randomKey) } } if httpAuth.AuthoriseReadOnly(token, randomKey) { if key.value != randomKey { t.Errorf("Authorised create random key: %v %v", token, randomKey) } } } // Check random tokens and values for i := 0; i < 1000; i++ { randomToken := util.GenerateStampedUUID() randomKey := util.GenerateStampedUUID() if httpAuth.AuthoriseJoin(randomToken, randomKey) { t.Errorf("Authorised join random key/token: %v %v", randomToken, randomKey) } if httpAuth.AuthoriseCreate(randomToken, randomKey) { t.Errorf("Authorised create random key/token: %v %v", randomToken, randomKey) } if httpAuth.AuthoriseReadOnly(randomToken, randomKey) { t.Errorf("Authorised create random key/token: %v %v", randomToken, randomKey) } } }
func senderClient(id, url string, feeds <-chan text.OTransform, t *testing.T) { origin := "http://127.0.0.1/" userID := util.GenerateStampedUUID() ws, err := websocket.Dial(url, "", origin) if err != nil { t.Errorf("client connect error: %v", err) return } if err = findDocument(id, userID, ws); err != nil { t.Errorf("%v", err) return } rcvChan := make(chan []text.OTransform, 5) crctChan := make(chan bool) go func() { for { var serverMsg leapSocketServerMessage if err := websocket.JSON.Receive(ws, &serverMsg); err == nil { switch serverMsg.Type { case "correction": crctChan <- true case "transforms": rcvChan <- serverMsg.Transforms case "update": case "error": t.Errorf("Server returned error: %v", serverMsg.Error) default: t.Errorf("unexpected message type from server: %v", serverMsg.Type) } } else { close(rcvChan) return } } }() for { select { case feed, open := <-feeds: if !open { return } websocket.JSON.Send(ws, leapSocketClientMessage{ Command: "submit", Transform: &feed, }) websocket.JSON.Send(ws, leapSocketClientMessage{ Command: "ping", }) select { case <-crctChan: case <-time.After(1 * time.Second): t.Errorf("Timed out waiting for correction") return } case _, open := <-rcvChan: if !open { return } t.Errorf("sender client received a transform") case <-time.After(8 * time.Second): t.Errorf("Sender client timeout occured") return } } }
func goodStoryClient(id, url string, bstory *binderStory, wg *sync.WaitGroup, t *testing.T) { origin := "http://127.0.0.1/" userID := util.GenerateStampedUUID() ws, err := websocket.Dial(url, "", origin) if err != nil { t.Errorf("client connect error: %v", err) wg.Done() return } if err = findDocument(id, userID, ws); err != nil { t.Errorf("%v", err) wg.Done() return } rcvChan := make(chan []text.OTransform, 5) go func() { for { var serverMsg leapSocketServerMessage if err := websocket.JSON.Receive(ws, &serverMsg); err == nil { switch serverMsg.Type { case "correction": t.Errorf("listening client received correction") case "transforms": rcvChan <- serverMsg.Transforms case "update": // Ignore case "error": t.Errorf("Server returned error: %v", serverMsg.Error) default: t.Errorf("unexpected message type from server: %v", serverMsg.Type) } } else { close(rcvChan) return } } }() tformIndex, lenCorrected := 0, len(bstory.TCorrected) for { select { case ret, open := <-rcvChan: if !open { t.Errorf("channel was closed before receiving last change") wg.Done() return } for _, tform := range ret { if tform.Version != bstory.TCorrected[tformIndex].Version || tform.Insert != bstory.TCorrected[tformIndex].Insert || tform.Delete != bstory.TCorrected[tformIndex].Delete || tform.Position != bstory.TCorrected[tformIndex].Position { t.Errorf("Transform (%v) not expected, %v != %v", tformIndex, tform, bstory.TCorrected[tformIndex]) } tformIndex++ if tformIndex == lenCorrected { wg.Done() return } } case <-time.After(8 * time.Second): t.Errorf("Timeout occured") wg.Done() return } } }