mirror of
https://github.com/strukturag/nextcloud-spreed-signaling
synced 2026-03-14 14:35:44 +01:00
Move common client code to separate package.
This commit is contained in:
parent
124c37108b
commit
3c41dcdbce
17 changed files with 1024 additions and 489 deletions
561
client/client.go
Normal file
561
client/client.go
Normal file
|
|
@ -0,0 +1,561 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2017 struktur AG
|
||||
*
|
||||
* @author Joachim Bauch <bauch@struktur.de>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/mailru/easyjson"
|
||||
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/api"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/geoip"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/internal"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/log"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/pool"
|
||||
)
|
||||
|
||||
const (
|
||||
// Time allowed to write a message to the peer.
|
||||
writeWait = 10 * time.Second
|
||||
|
||||
// Time allowed to read the next pong message from the peer.
|
||||
pongWait = 60 * time.Second
|
||||
|
||||
// Send pings to peer with this period. Must be less than pongWait.
|
||||
pingPeriod = (pongWait * 9) / 10
|
||||
|
||||
// Maximum message size allowed from peer.
|
||||
maxMessageSize = 64 * 1024
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterClientStats()
|
||||
}
|
||||
|
||||
var (
|
||||
InvalidFormat = api.NewError("invalid_format", "Invalid data format.")
|
||||
|
||||
bufferPool pool.BufferPool
|
||||
)
|
||||
|
||||
type WritableClientMessage interface {
|
||||
json.Marshaler
|
||||
|
||||
CloseAfterSend(session api.RoomAware) bool
|
||||
}
|
||||
|
||||
type HandlerClient interface {
|
||||
Context() context.Context
|
||||
RemoteAddr() string
|
||||
Country() geoip.Country
|
||||
UserAgent() string
|
||||
IsConnected() bool
|
||||
|
||||
SendError(e *api.Error) bool
|
||||
SendByeResponse(message *api.ClientMessage) bool
|
||||
SendByeResponseWithReason(message *api.ClientMessage, reason string) bool
|
||||
SendMessage(message WritableClientMessage) bool
|
||||
|
||||
Close()
|
||||
}
|
||||
|
||||
type Handler interface {
|
||||
GetSessionId() api.PublicSessionId
|
||||
|
||||
OnClosed()
|
||||
OnMessageReceived([]byte)
|
||||
OnRTTReceived(time.Duration)
|
||||
}
|
||||
|
||||
type GeoIpHandler interface {
|
||||
OnLookupCountry(addr string) geoip.Country
|
||||
}
|
||||
|
||||
type InRoomHandler interface {
|
||||
IsInRoom(string) bool
|
||||
}
|
||||
|
||||
type SessionCloserHandler interface {
|
||||
CloseSession()
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
logger log.Logger
|
||||
ctx context.Context
|
||||
// +checklocks:mu
|
||||
conn *websocket.Conn
|
||||
addr string
|
||||
agent string
|
||||
closed atomic.Int32
|
||||
country *geoip.Country
|
||||
logRTT bool
|
||||
|
||||
handlerMu sync.RWMutex
|
||||
// +checklocks:handlerMu
|
||||
handler Handler
|
||||
|
||||
sessionId atomic.Pointer[api.PublicSessionId]
|
||||
|
||||
mu sync.Mutex
|
||||
|
||||
closer *internal.Closer
|
||||
closeOnce sync.Once
|
||||
messagesDone chan struct{}
|
||||
messageChan chan *bytes.Buffer
|
||||
}
|
||||
|
||||
func (c *Client) SetConn(ctx context.Context, conn *websocket.Conn, remoteAddress string, agent string, logRTT bool, handler Handler) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.logger = log.LoggerFromContext(ctx)
|
||||
c.ctx = ctx
|
||||
c.conn = conn
|
||||
c.addr = remoteAddress
|
||||
c.agent = agent
|
||||
c.logRTT = logRTT
|
||||
c.SetHandler(handler)
|
||||
c.closer = internal.NewCloser()
|
||||
c.messageChan = make(chan *bytes.Buffer, 16)
|
||||
c.messagesDone = make(chan struct{})
|
||||
}
|
||||
|
||||
func (c *Client) GetConn() *websocket.Conn {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
return c.conn
|
||||
}
|
||||
|
||||
func (c *Client) SetHandler(handler Handler) {
|
||||
c.handlerMu.Lock()
|
||||
defer c.handlerMu.Unlock()
|
||||
c.handler = handler
|
||||
}
|
||||
|
||||
func (c *Client) getHandler() Handler {
|
||||
c.handlerMu.RLock()
|
||||
defer c.handlerMu.RUnlock()
|
||||
return c.handler
|
||||
}
|
||||
|
||||
func (c *Client) Context() context.Context {
|
||||
return c.ctx
|
||||
}
|
||||
|
||||
func (c *Client) IsConnected() bool {
|
||||
return c.closed.Load() == 0
|
||||
}
|
||||
|
||||
func (c *Client) SetSessionId(sessionId api.PublicSessionId) {
|
||||
c.sessionId.Store(&sessionId)
|
||||
}
|
||||
|
||||
func (c *Client) GetSessionId() api.PublicSessionId {
|
||||
sessionId := c.sessionId.Load()
|
||||
if sessionId == nil {
|
||||
sessionId := c.getHandler().GetSessionId()
|
||||
if sessionId == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
return sessionId
|
||||
}
|
||||
|
||||
return *sessionId
|
||||
}
|
||||
|
||||
func (c *Client) RemoteAddr() string {
|
||||
return c.addr
|
||||
}
|
||||
|
||||
func (c *Client) UserAgent() string {
|
||||
return c.agent
|
||||
}
|
||||
|
||||
func (c *Client) Country() geoip.Country {
|
||||
if c.country == nil {
|
||||
var country geoip.Country
|
||||
if handler, ok := c.getHandler().(GeoIpHandler); ok {
|
||||
country = handler.OnLookupCountry(c.addr)
|
||||
} else {
|
||||
country = geoip.UnknownCountry
|
||||
}
|
||||
c.country = &country
|
||||
}
|
||||
|
||||
return *c.country
|
||||
}
|
||||
|
||||
func (c *Client) IsInRoom(id string) bool {
|
||||
if handler, ok := c.getHandler().(InRoomHandler); ok {
|
||||
return handler.IsInRoom(id)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Client) Close() {
|
||||
if c.closed.Load() >= 2 {
|
||||
// Prevent reentrant call in case this was the second closing
|
||||
// step. Would otherwise deadlock in the "Once.Do" call path
|
||||
// through "Hub.processUnregister" (which calls "Close" again).
|
||||
return
|
||||
}
|
||||
|
||||
c.closeOnce.Do(func() {
|
||||
c.doClose()
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Client) doClose() {
|
||||
closed := c.closed.Add(1)
|
||||
switch closed {
|
||||
case 1:
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.conn != nil {
|
||||
c.conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) // nolint
|
||||
c.conn.Close()
|
||||
c.conn = nil
|
||||
}
|
||||
case 2:
|
||||
// Both the read pump and message processing must be finished before closing.
|
||||
c.closer.Close()
|
||||
<-c.messagesDone
|
||||
|
||||
c.getHandler().OnClosed()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) SendError(e *api.Error) bool {
|
||||
message := &api.ServerMessage{
|
||||
Type: "error",
|
||||
Error: e,
|
||||
}
|
||||
return c.SendMessage(message)
|
||||
}
|
||||
|
||||
func (c *Client) SendByeResponse(message *api.ClientMessage) bool {
|
||||
return c.SendByeResponseWithReason(message, "")
|
||||
}
|
||||
|
||||
func (c *Client) SendByeResponseWithReason(message *api.ClientMessage, reason string) bool {
|
||||
response := &api.ServerMessage{
|
||||
Type: "bye",
|
||||
}
|
||||
if message != nil {
|
||||
response.Id = message.Id
|
||||
}
|
||||
if reason != "" {
|
||||
if response.Bye == nil {
|
||||
response.Bye = &api.ByeServerMessage{}
|
||||
}
|
||||
response.Bye.Reason = reason
|
||||
}
|
||||
return c.SendMessage(response)
|
||||
}
|
||||
|
||||
func (c *Client) SendMessage(message WritableClientMessage) bool {
|
||||
return c.writeMessage(message)
|
||||
}
|
||||
|
||||
func (c *Client) ReadPump() {
|
||||
defer func() {
|
||||
close(c.messageChan)
|
||||
c.Close()
|
||||
}()
|
||||
|
||||
go c.processMessages()
|
||||
|
||||
addr := c.RemoteAddr()
|
||||
c.mu.Lock()
|
||||
conn := c.conn
|
||||
c.mu.Unlock()
|
||||
if conn == nil {
|
||||
c.logger.Printf("Connection from %s closed while starting readPump", addr)
|
||||
return
|
||||
}
|
||||
|
||||
conn.SetReadLimit(maxMessageSize)
|
||||
conn.SetPongHandler(func(msg string) error {
|
||||
now := time.Now()
|
||||
conn.SetReadDeadline(now.Add(pongWait)) // nolint
|
||||
if msg == "" {
|
||||
return nil
|
||||
}
|
||||
statsClientBytesTotal.WithLabelValues("incoming").Add(float64(len(msg)))
|
||||
if ts, err := strconv.ParseInt(msg, 10, 64); err == nil {
|
||||
rtt := now.Sub(time.Unix(0, ts))
|
||||
if c.logRTT {
|
||||
rtt_ms := rtt.Nanoseconds() / time.Millisecond.Nanoseconds()
|
||||
if sessionId := c.GetSessionId(); sessionId != "" {
|
||||
c.logger.Printf("Client %s has RTT of %d ms (%s)", sessionId, rtt_ms, rtt)
|
||||
} else {
|
||||
c.logger.Printf("Client from %s has RTT of %d ms (%s)", addr, rtt_ms, rtt)
|
||||
}
|
||||
}
|
||||
statsClientRTT.Observe(float64(rtt.Milliseconds()))
|
||||
c.getHandler().OnRTTReceived(rtt)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
for {
|
||||
conn.SetReadDeadline(time.Now().Add(pongWait)) // nolint
|
||||
messageType, reader, err := conn.NextReader()
|
||||
if err != nil {
|
||||
// Gorilla websocket hides the original net.Error, so also compare error messages
|
||||
if errors.Is(err, net.ErrClosed) || errors.Is(err, websocket.ErrCloseSent) || strings.Contains(err.Error(), net.ErrClosed.Error()) {
|
||||
break
|
||||
} else if _, ok := err.(*websocket.CloseError); !ok || websocket.IsUnexpectedCloseError(err,
|
||||
websocket.CloseNormalClosure,
|
||||
websocket.CloseGoingAway,
|
||||
websocket.CloseNoStatusReceived) {
|
||||
if sessionId := c.GetSessionId(); sessionId != "" {
|
||||
c.logger.Printf("Error reading from client %s: %v", sessionId, err)
|
||||
} else {
|
||||
c.logger.Printf("Error reading from %s: %v", addr, err)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if messageType != websocket.TextMessage {
|
||||
if sessionId := c.GetSessionId(); sessionId != "" {
|
||||
c.logger.Printf("Unsupported message type %v from client %s", messageType, sessionId)
|
||||
} else {
|
||||
c.logger.Printf("Unsupported message type %v from %s", messageType, addr)
|
||||
}
|
||||
c.SendError(InvalidFormat)
|
||||
continue
|
||||
}
|
||||
|
||||
decodeBuffer, err := bufferPool.ReadAll(reader)
|
||||
if err != nil {
|
||||
if sessionId := c.GetSessionId(); sessionId != "" {
|
||||
c.logger.Printf("Error reading message from client %s: %v", sessionId, err)
|
||||
} else {
|
||||
c.logger.Printf("Error reading message from %s: %v", addr, err)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// Stop processing if the client was closed.
|
||||
if !c.IsConnected() {
|
||||
bufferPool.Put(decodeBuffer)
|
||||
break
|
||||
}
|
||||
|
||||
statsClientBytesTotal.WithLabelValues("incoming").Add(float64(decodeBuffer.Len()))
|
||||
statsClientMessagesTotal.WithLabelValues("incoming").Inc()
|
||||
c.messageChan <- decodeBuffer
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) processMessages() {
|
||||
for {
|
||||
buffer := <-c.messageChan
|
||||
if buffer == nil {
|
||||
break
|
||||
}
|
||||
|
||||
c.getHandler().OnMessageReceived(buffer.Bytes())
|
||||
bufferPool.Put(buffer)
|
||||
}
|
||||
|
||||
close(c.messagesDone)
|
||||
c.doClose()
|
||||
}
|
||||
|
||||
type counterWriter struct {
|
||||
w io.Writer
|
||||
counter *int
|
||||
}
|
||||
|
||||
func (w *counterWriter) Write(p []byte) (int, error) {
|
||||
written, err := w.w.Write(p)
|
||||
if written > 0 {
|
||||
*w.counter += written
|
||||
}
|
||||
return written, err
|
||||
}
|
||||
|
||||
// +checklocks:c.mu
|
||||
func (c *Client) writeInternal(message json.Marshaler) bool {
|
||||
var closeData []byte
|
||||
|
||||
c.conn.SetWriteDeadline(time.Now().Add(writeWait)) // nolint
|
||||
writer, err := c.conn.NextWriter(websocket.TextMessage)
|
||||
var written int
|
||||
if err == nil {
|
||||
if m, ok := (any(message)).(easyjson.Marshaler); ok {
|
||||
written, err = easyjson.MarshalToWriter(m, writer)
|
||||
} else {
|
||||
err = json.NewEncoder(&counterWriter{
|
||||
w: writer,
|
||||
counter: &written,
|
||||
}).Encode(message)
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
err = writer.Close()
|
||||
}
|
||||
if err != nil {
|
||||
if err == websocket.ErrCloseSent {
|
||||
// Already sent a "close", won't be able to send anything else.
|
||||
return false
|
||||
}
|
||||
|
||||
if sessionId := c.GetSessionId(); sessionId != "" {
|
||||
c.logger.Printf("Could not send message %+v to client %s: %v", message, sessionId, err)
|
||||
} else {
|
||||
c.logger.Printf("Could not send message %+v to %s: %v", message, c.RemoteAddr(), err)
|
||||
}
|
||||
closeData = websocket.FormatCloseMessage(websocket.CloseInternalServerErr, "")
|
||||
goto close
|
||||
}
|
||||
|
||||
statsClientBytesTotal.WithLabelValues("outgoing").Add(float64(written))
|
||||
statsClientMessagesTotal.WithLabelValues("outgoing").Inc()
|
||||
return true
|
||||
|
||||
close:
|
||||
c.conn.SetWriteDeadline(time.Now().Add(writeWait)) // nolint
|
||||
if err := c.conn.WriteMessage(websocket.CloseMessage, closeData); err != nil {
|
||||
if sessionId := c.GetSessionId(); sessionId != "" {
|
||||
c.logger.Printf("Could not send close message to client %s: %v", sessionId, err)
|
||||
} else {
|
||||
c.logger.Printf("Could not send close message to %s: %v", c.RemoteAddr(), err)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Client) writeError(e error) bool { // nolint
|
||||
message := &api.ServerMessage{
|
||||
Type: "error",
|
||||
Error: api.NewError("internal_error", e.Error()),
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.conn == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if !c.writeMessageLocked(message) {
|
||||
return false
|
||||
}
|
||||
|
||||
closeData := websocket.FormatCloseMessage(websocket.CloseInternalServerErr, e.Error())
|
||||
c.conn.SetWriteDeadline(time.Now().Add(writeWait)) // nolint
|
||||
if err := c.conn.WriteMessage(websocket.CloseMessage, closeData); err != nil {
|
||||
if sessionId := c.GetSessionId(); sessionId != "" {
|
||||
c.logger.Printf("Could not send close message to client %s: %v", sessionId, err)
|
||||
} else {
|
||||
c.logger.Printf("Could not send close message to %s: %v", c.RemoteAddr(), err)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Client) writeMessage(message WritableClientMessage) bool {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.conn == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return c.writeMessageLocked(message)
|
||||
}
|
||||
|
||||
// +checklocks:c.mu
|
||||
func (c *Client) writeMessageLocked(message WritableClientMessage) bool {
|
||||
if !c.writeInternal(message) {
|
||||
return false
|
||||
}
|
||||
|
||||
if message.CloseAfterSend(c) {
|
||||
go func() {
|
||||
if sc, ok := c.getHandler().(SessionCloserHandler); ok {
|
||||
sc.CloseSession()
|
||||
}
|
||||
c.Close()
|
||||
}()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *Client) sendPing() bool {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.conn == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
now := time.Now().UnixNano()
|
||||
msg := strconv.FormatInt(now, 10)
|
||||
c.conn.SetWriteDeadline(time.Now().Add(writeWait)) // nolint
|
||||
if err := c.conn.WriteMessage(websocket.PingMessage, []byte(msg)); err != nil {
|
||||
if sessionId := c.GetSessionId(); sessionId != "" {
|
||||
c.logger.Printf("Could not send ping to client %s: %v", sessionId, err)
|
||||
} else {
|
||||
c.logger.Printf("Could not send ping to %s: %v", c.RemoteAddr(), err)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
statsClientBytesTotal.WithLabelValues("outgoing").Add(float64(len(msg)))
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *Client) WritePump() {
|
||||
ticker := time.NewTicker(pingPeriod)
|
||||
defer func() {
|
||||
ticker.Stop()
|
||||
}()
|
||||
|
||||
// Fetch initial RTT before any messages have been sent to the client.
|
||||
c.sendPing()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if !c.sendPing() {
|
||||
return
|
||||
}
|
||||
case <-c.closer.C:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
315
client/client_test.go
Normal file
315
client/client_test.go
Normal file
|
|
@ -0,0 +1,315 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2025 struktur AG
|
||||
*
|
||||
* @author Joachim Bauch <bauch@struktur.de>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/api"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/geoip"
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/log"
|
||||
)
|
||||
|
||||
func TestCounterWriter(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert := assert.New(t)
|
||||
|
||||
var b bytes.Buffer
|
||||
var written int
|
||||
w := &counterWriter{
|
||||
w: &b,
|
||||
counter: &written,
|
||||
}
|
||||
if count, err := w.Write(nil); assert.NoError(err) && assert.Equal(0, count) {
|
||||
assert.Equal(0, written)
|
||||
}
|
||||
if count, err := w.Write([]byte("foo")); assert.NoError(err) && assert.Equal(3, count) {
|
||||
assert.Equal(3, written)
|
||||
}
|
||||
}
|
||||
|
||||
type serverClient struct {
|
||||
Client
|
||||
|
||||
t *testing.T
|
||||
handler *testHandler
|
||||
|
||||
id string
|
||||
received atomic.Uint32
|
||||
sessionClosed atomic.Bool
|
||||
}
|
||||
|
||||
func newTestClient(h *testHandler, r *http.Request, conn *websocket.Conn, id uint64) *serverClient {
|
||||
result := &serverClient{
|
||||
t: h.t,
|
||||
handler: h,
|
||||
id: fmt.Sprintf("session-%d", id),
|
||||
}
|
||||
|
||||
addr := r.RemoteAddr
|
||||
if host, _, err := net.SplitHostPort(addr); err == nil {
|
||||
addr = host
|
||||
}
|
||||
|
||||
logger := log.NewLoggerForTest(h.t)
|
||||
ctx := log.NewLoggerContext(r.Context(), logger)
|
||||
result.SetConn(ctx, conn, addr, r.Header.Get("User-Agent"), false, result)
|
||||
return result
|
||||
}
|
||||
|
||||
func (c *serverClient) WaitReceived(ctx context.Context, count uint32) error {
|
||||
for {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
} else if c.received.Load() >= count {
|
||||
return nil
|
||||
}
|
||||
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *serverClient) GetSessionId() api.PublicSessionId {
|
||||
return api.PublicSessionId(c.id)
|
||||
}
|
||||
|
||||
func (c *serverClient) OnClosed() {
|
||||
c.handler.removeClient(c)
|
||||
}
|
||||
|
||||
func (c *serverClient) OnMessageReceived(message []byte) {
|
||||
switch c.received.Add(1) {
|
||||
case 1:
|
||||
var s string
|
||||
if err := json.Unmarshal(message, &s); assert.NoError(c.t, err) {
|
||||
assert.Equal(c.t, "Hello world!", s)
|
||||
c.SendMessage(&api.ServerMessage{
|
||||
Type: "welcome",
|
||||
Welcome: &api.WelcomeServerMessage{
|
||||
Version: "1.0",
|
||||
},
|
||||
})
|
||||
}
|
||||
case 2:
|
||||
var s string
|
||||
if err := json.Unmarshal(message, &s); assert.NoError(c.t, err) {
|
||||
assert.Equal(c.t, "Send error", s)
|
||||
c.SendError(api.NewError("test_error", "This is a test error."))
|
||||
}
|
||||
case 3:
|
||||
var s string
|
||||
if err := json.Unmarshal(message, &s); assert.NoError(c.t, err) {
|
||||
assert.Equal(c.t, "Send bye", s)
|
||||
c.SendByeResponseWithReason(nil, "Go away!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *serverClient) OnRTTReceived(rtt time.Duration) {
|
||||
|
||||
}
|
||||
|
||||
func (c *serverClient) OnLookupCountry(addr string) geoip.Country {
|
||||
return "DE"
|
||||
}
|
||||
|
||||
func (c *serverClient) IsInRoom(roomId string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *serverClient) CloseSession() {
|
||||
if c.sessionClosed.Swap(true) {
|
||||
assert.Fail(c.t, "session closed more than once")
|
||||
}
|
||||
}
|
||||
|
||||
type testHandler struct {
|
||||
mu sync.Mutex
|
||||
|
||||
t *testing.T
|
||||
|
||||
upgrader websocket.Upgrader
|
||||
|
||||
id atomic.Uint64
|
||||
// +checklocks:mu
|
||||
activeClients map[string]*serverClient
|
||||
// +checklocks:mu
|
||||
allClients []*serverClient
|
||||
}
|
||||
|
||||
func newTestHandler(t *testing.T) *testHandler {
|
||||
return &testHandler{
|
||||
t: t,
|
||||
activeClients: make(map[string]*serverClient),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *testHandler) addClient(client *serverClient) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
h.activeClients[client.id] = client
|
||||
h.allClients = append(h.allClients, client)
|
||||
}
|
||||
|
||||
func (h *testHandler) removeClient(client *serverClient) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
delete(h.activeClients, client.id)
|
||||
}
|
||||
|
||||
func (h *testHandler) getClients() []*serverClient {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
return slices.Clone(h.allClients)
|
||||
}
|
||||
|
||||
func (h *testHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := h.upgrader.Upgrade(w, r, nil)
|
||||
if !assert.NoError(h.t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
id := h.id.Add(1)
|
||||
client := newTestClient(h, r, conn, id)
|
||||
h.addClient(client)
|
||||
go client.WritePump()
|
||||
client.ReadPump()
|
||||
}
|
||||
|
||||
type localClient struct {
|
||||
t *testing.T
|
||||
|
||||
conn *websocket.Conn
|
||||
}
|
||||
|
||||
func newLocalClient(t *testing.T, url string) *localClient {
|
||||
t.Helper()
|
||||
|
||||
conn, _, err := websocket.DefaultDialer.DialContext(t.Context(), url, nil)
|
||||
require.NoError(t, err)
|
||||
return &localClient{
|
||||
t: t,
|
||||
|
||||
conn: conn,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *localClient) Close() error {
|
||||
err := c.conn.Close()
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
err = nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *localClient) WriteJSON(v any) error {
|
||||
return c.conn.WriteJSON(v)
|
||||
}
|
||||
|
||||
func (c *localClient) ReadJSON(v any) error {
|
||||
return c.conn.ReadJSON(v)
|
||||
}
|
||||
|
||||
func TestClient(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
require := require.New(t)
|
||||
assert := assert.New(t)
|
||||
|
||||
serverHandler := newTestHandler(t)
|
||||
|
||||
server := httptest.NewServer(serverHandler)
|
||||
t.Cleanup(func() {
|
||||
server.Close()
|
||||
})
|
||||
|
||||
client := newLocalClient(t, strings.ReplaceAll(server.URL, "http://", "ws://"))
|
||||
t.Cleanup(func() {
|
||||
assert.NoError(client.Close())
|
||||
})
|
||||
|
||||
var msg api.ServerMessage
|
||||
|
||||
require.NoError(client.WriteJSON("Hello world!"))
|
||||
if assert.NoError(client.ReadJSON(&msg)) &&
|
||||
assert.Equal("welcome", msg.Type) &&
|
||||
assert.NotNil(msg.Welcome) {
|
||||
assert.Equal("1.0", msg.Welcome.Version)
|
||||
}
|
||||
if clients := serverHandler.getClients(); assert.Len(clients, 1) {
|
||||
assert.False(clients[0].sessionClosed.Load())
|
||||
assert.EqualValues(1, clients[0].received.Load())
|
||||
}
|
||||
|
||||
require.NoError(client.WriteJSON("Send error"))
|
||||
if assert.NoError(client.ReadJSON(&msg)) &&
|
||||
assert.Equal("error", msg.Type) &&
|
||||
assert.NotNil(msg.Error) {
|
||||
assert.Equal("test_error", msg.Error.Code)
|
||||
assert.Equal("This is a test error.", msg.Error.Message)
|
||||
}
|
||||
if clients := serverHandler.getClients(); assert.Len(clients, 1) {
|
||||
assert.False(clients[0].sessionClosed.Load())
|
||||
assert.EqualValues(2, clients[0].received.Load())
|
||||
}
|
||||
|
||||
require.NoError(client.WriteJSON("Send bye"))
|
||||
if assert.NoError(client.ReadJSON(&msg)) &&
|
||||
assert.Equal("bye", msg.Type) &&
|
||||
assert.NotNil(msg.Bye) {
|
||||
assert.Equal("Go away!", msg.Bye.Reason)
|
||||
}
|
||||
if clients := serverHandler.getClients(); assert.Len(clients, 1) {
|
||||
assert.EqualValues(3, clients[0].received.Load())
|
||||
}
|
||||
|
||||
// Sending a "bye" will close the connection.
|
||||
var we *websocket.CloseError
|
||||
if err := client.ReadJSON(&msg); assert.ErrorAs(err, &we) {
|
||||
assert.Equal(websocket.CloseNormalClosure, we.Code)
|
||||
assert.Empty(we.Text)
|
||||
}
|
||||
if clients := serverHandler.getClients(); assert.Len(clients, 1) {
|
||||
assert.True(clients[0].sessionClosed.Load())
|
||||
assert.EqualValues(3, clients[0].received.Load())
|
||||
}
|
||||
}
|
||||
93
client/ip.go
Normal file
93
client/ip.go
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2026 struktur AG
|
||||
*
|
||||
* @author Joachim Bauch <bauch@struktur.de>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package client
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/container"
|
||||
)
|
||||
|
||||
var (
|
||||
DefaultTrustedProxies = container.DefaultPrivateIPs()
|
||||
)
|
||||
|
||||
func GetRealUserIP(r *http.Request, trusted *container.IPList) string {
|
||||
addr := r.RemoteAddr
|
||||
if host, _, err := net.SplitHostPort(addr); err == nil {
|
||||
addr = host
|
||||
}
|
||||
|
||||
ip := net.ParseIP(addr)
|
||||
if len(ip) == 0 {
|
||||
return addr
|
||||
}
|
||||
|
||||
// Don't check any headers if the server can be reached by untrusted clients directly.
|
||||
if trusted == nil || !trusted.Contains(ip) {
|
||||
return addr
|
||||
}
|
||||
|
||||
if realIP := r.Header.Get("X-Real-IP"); realIP != "" {
|
||||
if ip := net.ParseIP(realIP); len(ip) > 0 {
|
||||
return realIP
|
||||
}
|
||||
}
|
||||
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For#selecting_an_ip_address
|
||||
forwarded := strings.Split(strings.Join(r.Header.Values("X-Forwarded-For"), ","), ",")
|
||||
if len(forwarded) > 0 {
|
||||
slices.Reverse(forwarded)
|
||||
var lastTrusted string
|
||||
for _, hop := range forwarded {
|
||||
hop = strings.TrimSpace(hop)
|
||||
// Make sure to remove any port.
|
||||
if host, _, err := net.SplitHostPort(hop); err == nil {
|
||||
hop = host
|
||||
}
|
||||
|
||||
ip := net.ParseIP(hop)
|
||||
if len(ip) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if trusted.Contains(ip) {
|
||||
lastTrusted = hop
|
||||
continue
|
||||
}
|
||||
|
||||
return hop
|
||||
}
|
||||
|
||||
// If all entries in the "X-Forwarded-For" list are trusted, the left-most
|
||||
// will be the client IP. This can happen if a subnet is trusted and the
|
||||
// client also has an IP from this subnet.
|
||||
if lastTrusted != "" {
|
||||
return lastTrusted
|
||||
}
|
||||
}
|
||||
|
||||
return addr
|
||||
}
|
||||
278
client/ip_test.go
Normal file
278
client/ip_test.go
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2026 struktur AG
|
||||
*
|
||||
* @author Joachim Bauch <bauch@struktur.de>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package client
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/container"
|
||||
)
|
||||
|
||||
func TestGetRealUserIP(t *testing.T) {
|
||||
t.Parallel()
|
||||
testcases := []struct {
|
||||
expected string
|
||||
headers http.Header
|
||||
trusted string
|
||||
addr string
|
||||
}{
|
||||
{
|
||||
"192.168.1.2",
|
||||
nil,
|
||||
"192.168.0.0/16",
|
||||
"192.168.1.2:23456",
|
||||
},
|
||||
{
|
||||
"invalid-ip",
|
||||
nil,
|
||||
"192.168.0.0/16",
|
||||
"invalid-ip",
|
||||
},
|
||||
{
|
||||
"invalid-ip",
|
||||
nil,
|
||||
"192.168.0.0/16",
|
||||
"invalid-ip:12345",
|
||||
},
|
||||
{
|
||||
"10.11.12.13",
|
||||
nil,
|
||||
"192.168.0.0/16",
|
||||
"10.11.12.13:23456",
|
||||
},
|
||||
{
|
||||
"10.11.12.13",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-real-ip"): []string{"10.11.12.13"},
|
||||
},
|
||||
"192.168.0.0/16",
|
||||
"192.168.1.2:23456",
|
||||
},
|
||||
{
|
||||
"2002:db8::1",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-real-ip"): []string{"2002:db8::1"},
|
||||
},
|
||||
"192.168.0.0/16",
|
||||
"192.168.1.2:23456",
|
||||
},
|
||||
{
|
||||
"11.12.13.14",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-forwarded-for"): []string{"11.12.13.14, 192.168.30.32"},
|
||||
},
|
||||
"192.168.0.0/16",
|
||||
"192.168.1.2:23456",
|
||||
},
|
||||
{
|
||||
"11.12.13.14",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-forwarded-for"): []string{"11.12.13.14:1234, 192.168.30.32:2345"},
|
||||
},
|
||||
"192.168.0.0/16",
|
||||
"192.168.1.2:23456",
|
||||
},
|
||||
{
|
||||
"10.11.12.13",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-real-ip"): []string{"10.11.12.13"},
|
||||
},
|
||||
"2001:db8::/48",
|
||||
"[2001:db8::1]:23456",
|
||||
},
|
||||
{
|
||||
"2002:db8::1",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-real-ip"): []string{"2002:db8::1"},
|
||||
},
|
||||
"2001:db8::/48",
|
||||
"[2001:db8::1]:23456",
|
||||
},
|
||||
{
|
||||
"2002:db8::1",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-forwarded-for"): []string{"2002:db8::1, 192.168.30.32"},
|
||||
},
|
||||
"192.168.0.0/16",
|
||||
"192.168.1.2:23456",
|
||||
},
|
||||
{
|
||||
"2002:db8::1",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-forwarded-for"): []string{"2002:db8::1, 2001:db8::1"},
|
||||
},
|
||||
"192.168.0.0/16, 2001:db8::/48",
|
||||
"192.168.1.2:23456",
|
||||
},
|
||||
{
|
||||
"2002:db8::1",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-forwarded-for"): []string{"2002:db8::1, 192.168.30.32"},
|
||||
},
|
||||
"192.168.0.0/16, 2001:db8::/48",
|
||||
"[2001:db8::1]:23456",
|
||||
},
|
||||
{
|
||||
"2002:db8::1",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-forwarded-for"): []string{"2002:db8::1, 2001:db8::2"},
|
||||
},
|
||||
"2001:db8::/48",
|
||||
"[2001:db8::1]:23456",
|
||||
},
|
||||
// "X-Real-IP" has preference before "X-Forwarded-For"
|
||||
{
|
||||
"10.11.12.13",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-real-ip"): []string{"10.11.12.13"},
|
||||
http.CanonicalHeaderKey("x-forwarded-for"): []string{"11.12.13.14, 192.168.30.32"},
|
||||
},
|
||||
"192.168.0.0/16",
|
||||
"192.168.1.2:23456",
|
||||
},
|
||||
// Multiple "X-Forwarded-For" headers are merged.
|
||||
{
|
||||
"11.12.13.14",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-forwarded-for"): []string{"11.12.13.14", "192.168.30.32"},
|
||||
},
|
||||
"192.168.0.0/16",
|
||||
"192.168.1.2:23456",
|
||||
},
|
||||
{
|
||||
"11.12.13.14",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-forwarded-for"): []string{"1.2.3.4", "11.12.13.14", "192.168.30.32"},
|
||||
},
|
||||
"192.168.0.0/16",
|
||||
"192.168.1.2:23456",
|
||||
},
|
||||
{
|
||||
"11.12.13.14",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-forwarded-for"): []string{"1.2.3.4", "2.3.4.5", "11.12.13.14", "192.168.31.32", "192.168.30.32"},
|
||||
},
|
||||
"192.168.0.0/16",
|
||||
"192.168.1.2:23456",
|
||||
},
|
||||
// Headers are ignored if coming from untrusted clients.
|
||||
{
|
||||
"10.11.12.13",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-real-ip"): []string{"11.12.13.14"},
|
||||
},
|
||||
"192.168.0.0/16",
|
||||
"10.11.12.13:23456",
|
||||
},
|
||||
{
|
||||
"10.11.12.13",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-forwarded-for"): []string{"11.12.13.14, 192.168.30.32"},
|
||||
},
|
||||
"192.168.0.0/16",
|
||||
"10.11.12.13:23456",
|
||||
},
|
||||
// X-Forwarded-For is filtered for trusted proxies.
|
||||
{
|
||||
"1.2.3.4",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-forwarded-for"): []string{"11.12.13.14, 1.2.3.4"},
|
||||
},
|
||||
"192.168.0.0/16",
|
||||
"192.168.1.2:23456",
|
||||
},
|
||||
{
|
||||
"1.2.3.4",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-forwarded-for"): []string{"11.12.13.14, 1.2.3.4, 192.168.2.3"},
|
||||
},
|
||||
"192.168.0.0/16",
|
||||
"192.168.1.2:23456",
|
||||
},
|
||||
{
|
||||
"10.11.12.13",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-forwarded-for"): []string{"11.12.13.14, 1.2.3.4"},
|
||||
},
|
||||
"192.168.0.0/16",
|
||||
"10.11.12.13:23456",
|
||||
},
|
||||
// Invalid IPs are ignored.
|
||||
{
|
||||
"192.168.1.2",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-real-ip"): []string{"this-is-not-an-ip"},
|
||||
},
|
||||
"192.168.0.0/16",
|
||||
"192.168.1.2:23456",
|
||||
},
|
||||
{
|
||||
"11.12.13.14",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-real-ip"): []string{"this-is-not-an-ip"},
|
||||
http.CanonicalHeaderKey("x-forwarded-for"): []string{"11.12.13.14, 192.168.30.32"},
|
||||
},
|
||||
"192.168.0.0/16",
|
||||
"192.168.1.2:23456",
|
||||
},
|
||||
{
|
||||
"11.12.13.14",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-real-ip"): []string{"this-is-not-an-ip"},
|
||||
http.CanonicalHeaderKey("x-forwarded-for"): []string{"11.12.13.14, 192.168.30.32, proxy1"},
|
||||
},
|
||||
"192.168.0.0/16",
|
||||
"192.168.1.2:23456",
|
||||
},
|
||||
{
|
||||
"192.168.1.2",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-forwarded-for"): []string{"this-is-not-an-ip"},
|
||||
},
|
||||
"192.168.0.0/16",
|
||||
"192.168.1.2:23456",
|
||||
},
|
||||
{
|
||||
"192.168.2.3",
|
||||
http.Header{
|
||||
http.CanonicalHeaderKey("x-forwarded-for"): []string{"this-is-not-an-ip, 192.168.2.3"},
|
||||
},
|
||||
"192.168.0.0/16",
|
||||
"192.168.1.2:23456",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
trustedProxies, err := container.ParseIPList(tc.trusted)
|
||||
if !assert.NoError(t, err, "invalid trusted proxies in %+v", tc) {
|
||||
continue
|
||||
}
|
||||
request := &http.Request{
|
||||
RemoteAddr: tc.addr,
|
||||
Header: tc.headers,
|
||||
}
|
||||
assert.Equal(t, tc.expected, GetRealUserIP(request, trustedProxies), "failed for %+v", tc)
|
||||
}
|
||||
}
|
||||
60
client/stats_prometheus.go
Normal file
60
client/stats_prometheus.go
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
/**
|
||||
* Standalone signaling server for the Nextcloud Spreed app.
|
||||
* Copyright (C) 2021 struktur AG
|
||||
*
|
||||
* @author Joachim Bauch <bauch@struktur.de>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package client
|
||||
|
||||
import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
"github.com/strukturag/nextcloud-spreed-signaling/metrics"
|
||||
)
|
||||
|
||||
var (
|
||||
statsClientRTT = prometheus.NewHistogram(prometheus.HistogramOpts{
|
||||
Namespace: "signaling",
|
||||
Subsystem: "client",
|
||||
Name: "rtt",
|
||||
Help: "The roundtrip time of WebSocket ping messages in milliseconds",
|
||||
Buckets: prometheus.ExponentialBucketsRange(1, 30000, 50),
|
||||
})
|
||||
statsClientBytesTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: "signaling",
|
||||
Subsystem: "client",
|
||||
Name: "bytes_total",
|
||||
Help: "The total number of bytes sent to or received by clients",
|
||||
}, []string{"direction"})
|
||||
statsClientMessagesTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: "signaling",
|
||||
Subsystem: "client",
|
||||
Name: "messages_total",
|
||||
Help: "The total number of messages sent to or received by clients",
|
||||
}, []string{"direction"})
|
||||
|
||||
clientStats = []prometheus.Collector{
|
||||
statsClientRTT,
|
||||
statsClientBytesTotal,
|
||||
statsClientMessagesTotal,
|
||||
}
|
||||
)
|
||||
|
||||
func RegisterClientStats() {
|
||||
metrics.RegisterAll(clientStats...)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue