/****************************************************************************** * * Description : * * Handler of long polling clients. See also hdl_websock.go for web sockets and * hdl_grpc.go for gRPC * *****************************************************************************/ package main import ( "encoding/json" "errors" "io" "net/http" "time" "github.com/tinode/chat/server/logs" ) func (sess *Session) sendMessageLp(wrt http.ResponseWriter, msg any) bool { if len(sess.send) > sendQueueLimit { logs.Err.Println("longPoll: outbound queue limit exceeded", sess.sid) return false } statsInc("OutgoingMessagesLongpollTotal", 1) if err := lpWrite(wrt, msg); err != nil { logs.Err.Println("longPoll: writeOnce failed", sess.sid, err) return false } return true } func (sess *Session) writeOnce(wrt http.ResponseWriter, req *http.Request) { for { select { case msg, ok := <-sess.send: if !ok { return } switch v := msg.(type) { case *ServerComMessage: // single unserialized message w := sess.serializeAndUpdateStats(v) if !sess.sendMessageLp(wrt, w) { return } default: // serialized message if !sess.sendMessageLp(wrt, v) { return } } return case <-sess.bkgTimer.C: if sess.background { sess.background = false sess.onBackgroundTimer() } case msg := <-sess.stop: // Request to close the session. Make it unavailable. globals.sessionStore.Delete(sess) // Don't care if lpWrite fails. if msg != nil { lpWrite(wrt, msg) } return case topic := <-sess.detach: // Request to detach the session from a topic. sess.delSub(topic) // No 'return' statement here: continue waiting case <-time.After(pingPeriod): // just write an empty packet on timeout if _, err := wrt.Write([]byte{}); err != nil { logs.Err.Println("longPoll: writeOnce: timout", sess.sid, err) } return case <-req.Context().Done(): // HTTP request canceled or connection lost. return } } } func lpWrite(wrt http.ResponseWriter, msg any) error { // This will panic if msg is not []byte. This is intentional. wrt.Write(msg.([]byte)) return nil } func (sess *Session) readOnce(wrt http.ResponseWriter, req *http.Request) (int, error) { if req.ContentLength > globals.maxMessageSize { return http.StatusExpectationFailed, errors.New("request too large") } req.Body = http.MaxBytesReader(wrt, req.Body, globals.maxMessageSize) raw, err := io.ReadAll(req.Body) if err == nil { // Locking-unlocking is needed because the client may issue multiple requests in parallel. // Should not affect performance sess.lock.Lock() statsInc("IncomingMessagesLongpollTotal", 1) sess.dispatchRaw(raw) sess.lock.Unlock() return 0, nil } return 0, err } // serveLongPoll handles long poll connections when WebSocket is not available // Connection could be without sid or with sid: // - if sid is empty, create session, expect a login in the same request, respond and close // - if sid is not empty and there is an initialized session, payload is optional // - if no payload, perform long poll // - if payload exists, process it and close // - if sid is not empty but there is no session, report an error func serveLongPoll(wrt http.ResponseWriter, req *http.Request) { now := time.Now().UTC().Round(time.Millisecond) // Use the lowest common denominator - this is a legacy handler after all (otherwise would use application/json) wrt.Header().Set("Content-Type", "text/plain") if globals.tlsStrictMaxAge != "" { wrt.Header().Set("Strict-Transport-Security", "max-age"+globals.tlsStrictMaxAge) } enc := json.NewEncoder(wrt) if isValid, _ := checkAPIKey(getAPIKey(req)); !isValid { wrt.WriteHeader(http.StatusForbidden) enc.Encode(ErrAPIKeyRequired(now)) return } // TODO(gene): should it be configurable? // Currently any domain is allowed to get data from the chat server wrt.Header().Set("Access-Control-Allow-Origin", "*") // Ensure the response is not cached if req.ProtoAtLeast(1, 1) { wrt.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") // HTTP 1.1 } else { wrt.Header().Set("Pragma", "no-cache") // HTTP 1.0 } wrt.Header().Set("Expires", "0") // Proxies // TODO(gene): respond differently to valious HTTP methods // Get session id sid := req.FormValue("sid") var sess *Session if sid == "" { // New session var count int sess, count = globals.sessionStore.NewSession(wrt, "") sess.remoteAddr = getRemoteAddr(req) logs.Info.Println("longPoll: session started", sess.sid, sess.remoteAddr, count) wrt.WriteHeader(http.StatusCreated) pkt := NoErrCreated(req.FormValue("id"), "", now) pkt.Ctrl.Params = map[string]string{ "sid": sess.sid, } enc.Encode(pkt) return } // Existing session sess = globals.sessionStore.Get(sid) if sess == nil { logs.Warn.Println("longPoll: invalid or expired session id", sid) wrt.WriteHeader(http.StatusForbidden) enc.Encode(ErrSessionNotFound(now)) return } if addr := getRemoteAddr(req); sess.remoteAddr != addr { sess.remoteAddr = addr logs.Warn.Println("longPoll: remote address changed", sid, addr) } if req.ContentLength != 0 { // Read payload and send it for processing. if code, err := sess.readOnce(wrt, req); err != nil { logs.Warn.Println("longPoll: readOnce failed", sess.sid, err) // Failed to read request, report an error, if possible if code != 0 { wrt.WriteHeader(code) } else { wrt.WriteHeader(http.StatusBadRequest) } enc.Encode(ErrMalformed(req.FormValue("id"), "", now)) } return } sess.writeOnce(wrt, req) }