mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-14 04:07:46 +00:00
* Add redis support for distributed caching * Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! fixup! fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * ... and another all nighter. * fixup! ... and another all nighter. * fixup! fixup! ... and another all nighter. * fixup! fixup! fixup! ... and another all nighter. * Resolve issue #85 by adding ability to set custom claims in JWT tokens * Remove redundant validation in auth middleware ( issue #89 ) * Add ability to set cookie prefix for session cookies ( #87 ) * fixup! Add ability to set cookie prefix for session cookies ( #87 ) * Add ability to set cookie max age - issue #91 * Potential fix for code scanning alert no. 10: Size computation for allocation may overflow Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * fixup! Merge main into 0.8.0-redis: resolve conflicts --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
491 lines
9.1 KiB
Go
491 lines
9.1 KiB
Go
package server
|
|
|
|
import (
|
|
"bufio"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"net"
|
|
"strings"
|
|
"sync"
|
|
"unicode"
|
|
|
|
"github.com/alicebob/miniredis/v2/fpconv"
|
|
)
|
|
|
|
func errUnknownCommand(cmd string, args []string) string {
|
|
s := fmt.Sprintf("ERR unknown command `%s`, with args beginning with: ", cmd)
|
|
if len(args) > 20 {
|
|
args = args[:20]
|
|
}
|
|
for _, a := range args {
|
|
s += fmt.Sprintf("`%s`, ", a)
|
|
}
|
|
return s
|
|
}
|
|
|
|
// Cmd is what Register expects
|
|
type Cmd func(c *Peer, cmd string, args []string)
|
|
|
|
type DisconnectHandler func(c *Peer)
|
|
|
|
// Hook is can be added to run before every cmd. Return true if the command is done.
|
|
type Hook func(*Peer, string, ...string) bool
|
|
|
|
// Server is a simple redis server
|
|
type Server struct {
|
|
l net.Listener
|
|
cmds map[string]Cmd
|
|
preHook Hook
|
|
peers map[net.Conn]struct{}
|
|
mu sync.Mutex
|
|
wg sync.WaitGroup
|
|
infoConns int
|
|
infoCmds int
|
|
}
|
|
|
|
// NewServer makes a server listening on addr. Close with .Close().
|
|
func NewServer(addr string) (*Server, error) {
|
|
l, err := net.Listen("tcp", addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return newServer(l), nil
|
|
}
|
|
|
|
func NewServerTLS(addr string, cfg *tls.Config) (*Server, error) {
|
|
l, err := tls.Listen("tcp", addr, cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return newServer(l), nil
|
|
}
|
|
|
|
func newServer(l net.Listener) *Server {
|
|
s := Server{
|
|
cmds: map[string]Cmd{},
|
|
peers: map[net.Conn]struct{}{},
|
|
l: l,
|
|
}
|
|
|
|
s.wg.Add(1)
|
|
go func() {
|
|
defer s.wg.Done()
|
|
s.serve(l)
|
|
|
|
s.mu.Lock()
|
|
for c := range s.peers {
|
|
c.Close()
|
|
}
|
|
s.mu.Unlock()
|
|
}()
|
|
return &s
|
|
}
|
|
|
|
// (un)set a hook which is ran before every call. It returns true if the command is done.
|
|
func (s *Server) SetPreHook(h Hook) {
|
|
s.mu.Lock()
|
|
s.preHook = h
|
|
s.mu.Unlock()
|
|
}
|
|
|
|
func (s *Server) serve(l net.Listener) {
|
|
for {
|
|
conn, err := l.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
s.ServeConn(conn)
|
|
}
|
|
}
|
|
|
|
// ServeConn handles a net.Conn. Nice with net.Pipe()
|
|
func (s *Server) ServeConn(conn net.Conn) {
|
|
s.wg.Add(1)
|
|
s.mu.Lock()
|
|
s.peers[conn] = struct{}{}
|
|
s.infoConns++
|
|
s.mu.Unlock()
|
|
|
|
go func() {
|
|
defer s.wg.Done()
|
|
defer conn.Close()
|
|
|
|
s.servePeer(conn)
|
|
|
|
s.mu.Lock()
|
|
delete(s.peers, conn)
|
|
s.mu.Unlock()
|
|
}()
|
|
}
|
|
|
|
// Addr has the net.Addr struct
|
|
func (s *Server) Addr() *net.TCPAddr {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if s.l == nil {
|
|
return nil
|
|
}
|
|
return s.l.Addr().(*net.TCPAddr)
|
|
}
|
|
|
|
// Close a server started with NewServer. It will wait until all clients are
|
|
// closed.
|
|
func (s *Server) Close() {
|
|
s.mu.Lock()
|
|
if s.l != nil {
|
|
s.l.Close()
|
|
}
|
|
s.l = nil
|
|
s.mu.Unlock()
|
|
|
|
s.wg.Wait()
|
|
}
|
|
|
|
// Register a command. It can't have been registered before. Safe to call on a
|
|
// running server.
|
|
func (s *Server) Register(cmd string, f Cmd) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
cmd = strings.ToUpper(cmd)
|
|
if _, ok := s.cmds[cmd]; ok {
|
|
return fmt.Errorf("command already registered: %s", cmd)
|
|
}
|
|
s.cmds[cmd] = f
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) servePeer(c net.Conn) {
|
|
r := bufio.NewReader(c)
|
|
peer := &Peer{
|
|
w: bufio.NewWriter(c),
|
|
}
|
|
|
|
defer func() {
|
|
for _, f := range peer.onDisconnect {
|
|
f()
|
|
}
|
|
}()
|
|
|
|
readCh := make(chan []string)
|
|
|
|
go func() {
|
|
defer close(readCh)
|
|
|
|
for {
|
|
args, err := readArray(r)
|
|
if err != nil {
|
|
peer.Close()
|
|
return
|
|
}
|
|
|
|
readCh <- args
|
|
}
|
|
}()
|
|
|
|
for args := range readCh {
|
|
s.Dispatch(peer, args)
|
|
peer.Flush()
|
|
|
|
if peer.Closed() {
|
|
c.Close()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Server) Dispatch(c *Peer, args []string) {
|
|
cmd, args := args[0], args[1:]
|
|
cmdUp := strings.ToUpper(cmd)
|
|
s.mu.Lock()
|
|
h := s.preHook
|
|
s.mu.Unlock()
|
|
if h != nil {
|
|
if h(c, cmdUp, args...) {
|
|
return
|
|
}
|
|
}
|
|
|
|
s.mu.Lock()
|
|
cb, ok := s.cmds[cmdUp]
|
|
s.mu.Unlock()
|
|
if !ok {
|
|
c.WriteError(errUnknownCommand(cmd, args))
|
|
return
|
|
}
|
|
|
|
s.mu.Lock()
|
|
s.infoCmds++
|
|
s.mu.Unlock()
|
|
cb(c, cmdUp, args)
|
|
if c.SwitchResp3 != nil {
|
|
c.Resp3 = *c.SwitchResp3
|
|
c.SwitchResp3 = nil
|
|
}
|
|
}
|
|
|
|
// TotalCommands is total (known) commands since this the server started
|
|
func (s *Server) TotalCommands() int {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
return s.infoCmds
|
|
}
|
|
|
|
// ClientsLen gives the number of connected clients right now
|
|
func (s *Server) ClientsLen() int {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
return len(s.peers)
|
|
}
|
|
|
|
// TotalConnections give the number of clients connected since the server
|
|
// started, including the currently connected ones
|
|
func (s *Server) TotalConnections() int {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
return s.infoConns
|
|
}
|
|
|
|
// Peer is a client connected to the server
|
|
type Peer struct {
|
|
w *bufio.Writer
|
|
closed bool
|
|
Resp3 bool
|
|
SwitchResp3 *bool // we'll switch to this version _after_ the command
|
|
Ctx interface{} // anything goes, server won't touch this
|
|
onDisconnect []func() // list of callbacks
|
|
mu sync.Mutex // for Block()
|
|
ClientName string // client name set by CLIENT SETNAME
|
|
}
|
|
|
|
func NewPeer(w *bufio.Writer) *Peer {
|
|
return &Peer{
|
|
w: w,
|
|
}
|
|
}
|
|
|
|
// Flush the write buffer. Called automatically after every redis command
|
|
func (c *Peer) Flush() {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.w.Flush()
|
|
}
|
|
|
|
// Close the client connection after the current command is done.
|
|
func (c *Peer) Close() {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.closed = true
|
|
}
|
|
|
|
// Return true if the peer connection closed.
|
|
func (c *Peer) Closed() bool {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
return c.closed
|
|
}
|
|
|
|
// Register a function to execute on disconnect. There can be multiple
|
|
// functions registered.
|
|
func (c *Peer) OnDisconnect(f func()) {
|
|
c.onDisconnect = append(c.onDisconnect, f)
|
|
}
|
|
|
|
// issue multiple calls, guarded with a mutex
|
|
func (c *Peer) Block(f func(*Writer)) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
f(&Writer{c.w, c.Resp3})
|
|
}
|
|
|
|
// WriteError writes a redis 'Error'
|
|
func (c *Peer) WriteError(e string) {
|
|
c.Block(func(w *Writer) {
|
|
w.WriteError(e)
|
|
})
|
|
}
|
|
|
|
// WriteInline writes a redis inline string
|
|
func (c *Peer) WriteInline(s string) {
|
|
c.Block(func(w *Writer) {
|
|
w.WriteInline(s)
|
|
})
|
|
}
|
|
|
|
// WriteOK write the inline string `OK`
|
|
func (c *Peer) WriteOK() {
|
|
c.WriteInline("OK")
|
|
}
|
|
|
|
// WriteBulk writes a bulk string
|
|
func (c *Peer) WriteBulk(s string) {
|
|
c.Block(func(w *Writer) {
|
|
w.WriteBulk(s)
|
|
})
|
|
}
|
|
|
|
// WriteNull writes a redis Null element
|
|
func (c *Peer) WriteNull() {
|
|
c.Block(func(w *Writer) {
|
|
w.WriteNull()
|
|
})
|
|
}
|
|
|
|
// WriteLen starts an array with the given length
|
|
func (c *Peer) WriteLen(n int) {
|
|
c.Block(func(w *Writer) {
|
|
w.WriteLen(n)
|
|
})
|
|
}
|
|
|
|
// WriteMapLen starts a map with the given length (number of keys)
|
|
func (c *Peer) WriteMapLen(n int) {
|
|
c.Block(func(w *Writer) {
|
|
w.WriteMapLen(n)
|
|
})
|
|
}
|
|
|
|
// WriteSetLen starts a set with the given length (number of elements)
|
|
func (c *Peer) WriteSetLen(n int) {
|
|
c.Block(func(w *Writer) {
|
|
w.WriteSetLen(n)
|
|
})
|
|
}
|
|
|
|
// WritePushLen starts a push-data array with the given length
|
|
func (c *Peer) WritePushLen(n int) {
|
|
c.Block(func(w *Writer) {
|
|
w.WritePushLen(n)
|
|
})
|
|
}
|
|
|
|
// WriteInt writes an integer
|
|
func (c *Peer) WriteInt(n int) {
|
|
c.Block(func(w *Writer) {
|
|
w.WriteInt(n)
|
|
})
|
|
}
|
|
|
|
// WriteFloat writes a float
|
|
func (c *Peer) WriteFloat(n float64) {
|
|
c.Block(func(w *Writer) {
|
|
w.WriteFloat(n)
|
|
})
|
|
}
|
|
|
|
// WriteRaw writes a raw redis response
|
|
func (c *Peer) WriteRaw(s string) {
|
|
c.Block(func(w *Writer) {
|
|
w.WriteRaw(s)
|
|
})
|
|
}
|
|
|
|
// WriteStrings is a helper to (bulk)write a string list
|
|
func (c *Peer) WriteStrings(strs []string) {
|
|
c.Block(func(w *Writer) {
|
|
w.WriteStrings(strs)
|
|
})
|
|
}
|
|
|
|
func toInline(s string) string {
|
|
return strings.Map(func(r rune) rune {
|
|
if unicode.IsSpace(r) {
|
|
return ' '
|
|
}
|
|
return r
|
|
}, s)
|
|
}
|
|
|
|
// A Writer is given to the callback in Block()
|
|
type Writer struct {
|
|
w *bufio.Writer
|
|
resp3 bool
|
|
}
|
|
|
|
// WriteError writes a redis 'Error'
|
|
func (w *Writer) WriteError(e string) {
|
|
fmt.Fprintf(w.w, "-%s\r\n", toInline(e))
|
|
}
|
|
|
|
func (w *Writer) WriteLen(n int) {
|
|
fmt.Fprintf(w.w, "*%d\r\n", n)
|
|
}
|
|
|
|
func (w *Writer) WriteMapLen(n int) {
|
|
if w.resp3 {
|
|
fmt.Fprintf(w.w, "%%%d\r\n", n)
|
|
return
|
|
}
|
|
w.WriteLen(n * 2)
|
|
}
|
|
|
|
func (w *Writer) WriteSetLen(n int) {
|
|
if w.resp3 {
|
|
fmt.Fprintf(w.w, "~%d\r\n", n)
|
|
return
|
|
}
|
|
w.WriteLen(n)
|
|
}
|
|
|
|
func (w *Writer) WritePushLen(n int) {
|
|
if w.resp3 {
|
|
fmt.Fprintf(w.w, ">%d\r\n", n)
|
|
return
|
|
}
|
|
w.WriteLen(n)
|
|
}
|
|
|
|
// WriteBulk writes a bulk string
|
|
func (w *Writer) WriteBulk(s string) {
|
|
fmt.Fprintf(w.w, "$%d\r\n%s\r\n", len(s), s)
|
|
}
|
|
|
|
// WriteStrings writes a list of strings (bulk)
|
|
func (w *Writer) WriteStrings(strs []string) {
|
|
w.WriteLen(len(strs))
|
|
for _, s := range strs {
|
|
w.WriteBulk(s)
|
|
}
|
|
}
|
|
|
|
// WriteInt writes an integer
|
|
func (w *Writer) WriteInt(n int) {
|
|
fmt.Fprintf(w.w, ":%d\r\n", n)
|
|
}
|
|
|
|
// WriteFloat writes a float
|
|
func (w *Writer) WriteFloat(n float64) {
|
|
if w.resp3 {
|
|
fmt.Fprintf(w.w, ",%s\r\n", formatFloat(n))
|
|
return
|
|
}
|
|
w.WriteBulk(formatFloat(n))
|
|
}
|
|
|
|
// WriteNull writes a redis Null element
|
|
func (w *Writer) WriteNull() {
|
|
if w.resp3 {
|
|
fmt.Fprint(w.w, "_\r\n")
|
|
return
|
|
}
|
|
fmt.Fprintf(w.w, "$-1\r\n")
|
|
}
|
|
|
|
// WriteInline writes a redis inline string
|
|
func (w *Writer) WriteInline(s string) {
|
|
fmt.Fprintf(w.w, "+%s\r\n", toInline(s))
|
|
}
|
|
|
|
// WriteRaw writes a raw redis response
|
|
func (w *Writer) WriteRaw(s string) {
|
|
fmt.Fprint(w.w, s)
|
|
}
|
|
|
|
func (w *Writer) Flush() {
|
|
w.w.Flush()
|
|
}
|
|
|
|
// formatFloat formats a float the way redis does.
|
|
// Redis uses a method called "grisu2", which we ported from C.
|
|
func formatFloat(v float64) string {
|
|
return fpconv.Dtoa(v)
|
|
}
|