func (h *handler) sendContinuousChangesByWebSocket(inChannels base.Set, options db.ChangesOptions) error { handler := func(conn *websocket.Conn) { h.logStatus(101, "Upgraded to WebSocket protocol") defer func() { conn.Close() base.LogTo("HTTP+", "#%03d: --> WebSocket closed", h.serialNumber) }() // Read changes-feed options from an initial incoming WebSocket message in JSON format: if msg, err := readWebSocketMessage(conn); err != nil { return } else { var channelNames []string var err error if _, options, _, channelNames, err = h.readChangesOptionsFromJSON(msg); err != nil { return } if channelNames != nil { inChannels, _ = channels.SetFromArray(channelNames, channels.ExpandStar) } } options.Terminator = make(chan bool) defer close(options.Terminator) caughtUp := false h.generateContinuousChanges(inChannels, options, func(changes []*db.ChangeEntry) error { var data []byte if changes != nil { data, _ = json.Marshal(changes) } else if !caughtUp { caughtUp = true data, _ = json.Marshal([]*db.ChangeEntry{}) } else { data = []byte{} } _, err := conn.Write(data) return err }) } server := websocket.Server{ Handshake: func(*websocket.Config, *http.Request) error { return nil }, Handler: handler, } server.ServeHTTP(h.response, h.rq) return nil }
// Top-level handler for _changes feed requests. Accepts GET or POST requests. func (h *handler) handleChanges() error { // http://wiki.apache.org/couchdb/HTTP_database_API#Changes // http://docs.couchdb.org/en/latest/api/database/changes.html restExpvars.Add("changesFeeds_total", 1) restExpvars.Add("changesFeeds_active", 1) defer restExpvars.Add("changesFeeds_active", -1) var feed string var options db.ChangesOptions var filter string var channelsArray []string var docIdsArray []string if h.rq.Method == "GET" { // GET request has parameters in URL: feed = h.getQuery("feed") var err error if options.Since, err = h.db.ParseSequenceID(h.getJSONStringQuery("since")); err != nil { return err } options.Limit = int(h.getIntQuery("limit", 0)) options.Conflicts = (h.getQuery("style") == "all_docs") options.ActiveOnly = h.getBoolQuery("active_only") options.IncludeDocs = (h.getBoolQuery("include_docs")) filter = h.getQuery("filter") channelsParam := h.getQuery("channels") if channelsParam != "" { channelsArray = strings.Split(channelsParam, ",") } docidsParam := h.getQuery("doc_ids") if docidsParam != "" { var docidKeys []string err := json.Unmarshal([]byte(docidsParam), &docidKeys) if err != nil { err = base.HTTPErrorf(http.StatusBadRequest, "Bad doc id's") } if len(docidKeys) > 0 { docIdsArray = docidKeys } } options.HeartbeatMs = getRestrictedIntQuery( h.rq.URL.Query(), "heartbeat", kDefaultHeartbeatMS, kMinHeartbeatMS, h.server.config.MaxHeartbeat*1000, true, ) options.TimeoutMs = getRestrictedIntQuery( h.rq.URL.Query(), "timeout", kDefaultTimeoutMS, 0, kMaxTimeoutMS, true, ) } else { // POST request has parameters in JSON body: body, err := h.readBody() if err != nil { return err } feed, options, filter, channelsArray, docIdsArray, _, err = h.readChangesOptionsFromJSON(body) if err != nil { return err } } // Get the channels as parameters to an imaginary "bychannel" filter. // The default is all channels the user can access. userChannels := channels.SetOf(channels.AllChannelWildcard) if filter != "" { if filter == "sync_gateway/bychannel" { if channelsArray == nil { return base.HTTPErrorf(http.StatusBadRequest, "Missing 'channels' filter parameter") } var err error userChannels, err = channels.SetFromArray(channelsArray, channels.ExpandStar) if err != nil { return err } if len(userChannels) == 0 { return base.HTTPErrorf(http.StatusBadRequest, "Empty channel list") } } else if filter == "_doc_ids" { if docIdsArray == nil { return base.HTTPErrorf(http.StatusBadRequest, "Missing 'doc_ids' filter parameter") } if len(docIdsArray) == 0 { return base.HTTPErrorf(http.StatusBadRequest, "Empty doc_ids list") } } else { return base.HTTPErrorf(http.StatusBadRequest, "Unknown filter; try sync_gateway/bychannel or _doc_ids") } } h.db.ChangesClientStats.Increment() defer h.db.ChangesClientStats.Decrement() options.Terminator = make(chan bool) var err error forceClose := false switch feed { case "normal", "": if filter == "_doc_ids" { err, forceClose = h.sendChangesForDocIds(userChannels, docIdsArray, options) } else { err, forceClose = h.sendSimpleChanges(userChannels, options) } case "longpoll": options.Wait = true err, forceClose = h.sendSimpleChanges(userChannels, options) case "continuous": err, forceClose = h.sendContinuousChangesByHTTP(userChannels, options) case "websocket": err, forceClose = h.sendContinuousChangesByWebSocket(userChannels, options) default: err = base.HTTPErrorf(http.StatusBadRequest, "Unknown feed type") forceClose = false } close(options.Terminator) if forceClose && h.user != nil { h.db.DatabaseContext.NotifyUser(h.user.Name()) } return err }
func (h *handler) sendContinuousChangesByWebSocket(inChannels base.Set, options db.ChangesOptions) (error, bool) { forceClose := false handler := func(conn *websocket.Conn) { h.logStatus(101, "Upgraded to WebSocket protocol") defer func() { conn.Close() base.LogTo("HTTP+", "#%03d: --> WebSocket closed", h.serialNumber) }() // Read changes-feed options from an initial incoming WebSocket message in JSON format: var wsoptions db.ChangesOptions var compress bool if msg, err := readWebSocketMessage(conn); err != nil { return } else { var channelNames []string var err error if _, wsoptions, _, channelNames, _, compress, err = h.readChangesOptionsFromJSON(msg); err != nil { return } if channelNames != nil { inChannels, _ = ch.SetFromArray(channelNames, ch.ExpandStar) } } //Copy options.Terminator to new WebSocket options //options.Terminator will be closed automatically when //changes feed completes wsoptions.Terminator = options.Terminator // Set up GZip compression var writer *bytes.Buffer var zipWriter *gzip.Writer if compress { writer = bytes.NewBuffer(nil) zipWriter = GetGZipWriter(writer) } caughtUp := false _, forceClose = h.generateContinuousChanges(inChannels, wsoptions, func(changes []*db.ChangeEntry) error { var data []byte if changes != nil { data, _ = json.Marshal(changes) } else if !caughtUp { caughtUp = true data, _ = json.Marshal([]*db.ChangeEntry{}) } else { data = []byte{} } if compress && len(data) > 8 { // Compress JSON, using same GZip context, and send as binary msg: zipWriter.Write(data) zipWriter.Flush() data = writer.Bytes() writer.Reset() conn.PayloadType = websocket.BinaryFrame } else { conn.PayloadType = websocket.TextFrame } _, err := conn.Write(data) return err }) if zipWriter != nil { ReturnGZipWriter(zipWriter) } } server := websocket.Server{ Handshake: func(*websocket.Config, *http.Request) error { return nil }, Handler: handler, } server.ServeHTTP(h.response, h.rq) return nil, forceClose }
// Top-level handler for _changes feed requests. Accepts GET or POST requests. func (h *handler) handleChanges() error { // http://wiki.apache.org/couchdb/HTTP_database_API#Changes // http://docs.couchdb.org/en/latest/api/database/changes.html restExpvars.Add("changesFeeds_total", 1) restExpvars.Add("changesFeeds_active", 1) defer restExpvars.Add("changesFeeds_active", -1) var feed string var options db.ChangesOptions var filter string var channelsArray []string if h.rq.Method == "GET" { // GET request has parameters in URL: feed = h.getQuery("feed") var err error if options.Since, err = db.ParseSequenceID(h.getQuery("since")); err != nil { return err } options.Limit = int(h.getIntQuery("limit", 0)) options.Conflicts = (h.getQuery("style") == "all_docs") options.IncludeDocs = (h.getBoolQuery("include_docs")) filter = h.getQuery("filter") channelsParam := h.getQuery("channels") if channelsParam != "" { channelsArray = strings.Split(channelsParam, ",") } options.HeartbeatMs = getRestrictedIntQuery(h.rq.URL.Query(), "heartbeat", 0, kMinHeartbeatMS, 0) options.TimeoutMs = getRestrictedIntQuery(h.rq.URL.Query(), "timeout", kDefaultTimeoutMS, 0, kMaxTimeoutMS) } else { // POST request has parameters in JSON body: body, err := h.readBody() if err != nil { return err } feed, options, filter, channelsArray, err = readChangesOptionsFromJSON(body) if err != nil { return err } } // Get the channels as parameters to an imaginary "bychannel" filter. // The default is all channels the user can access. userChannels := channels.SetOf(channels.AllChannelWildcard) if filter != "" { if filter != "sync_gateway/bychannel" { return base.HTTPErrorf(http.StatusBadRequest, "Unknown filter; try sync_gateway/bychannel") } if channelsArray == nil { return base.HTTPErrorf(http.StatusBadRequest, "Missing 'channels' filter parameter") } var err error userChannels, err = channels.SetFromArray(channelsArray, channels.ExpandStar) if err != nil { return err } if len(userChannels) == 0 { return base.HTTPErrorf(http.StatusBadRequest, "Empty channel list") } } h.db.ChangesClientStats.Increment() defer h.db.ChangesClientStats.Decrement() options.Terminator = make(chan bool) defer close(options.Terminator) switch feed { case "normal", "": return h.sendSimpleChanges(userChannels, options) case "longpoll": options.Wait = true return h.sendSimpleChanges(userChannels, options) case "continuous": return h.sendContinuousChangesByHTTP(userChannels, options) case "websocket": return h.sendContinuousChangesByWebSocket(userChannels, options) default: return base.HTTPErrorf(http.StatusBadRequest, "Unknown feed type") } }