Add redis support for distributed caching (#83)

* 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>
This commit is contained in:
2025-11-30 02:18:46 +00:00
committed by GitHub
co-authored by Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
parent 5fcbd54955
commit e64fc7f730
318 changed files with 100989 additions and 948 deletions
+2
View File
@@ -0,0 +1,2 @@
test:
go test
+60
View File
@@ -0,0 +1,60 @@
package proto
import (
"bufio"
"crypto/tls"
"net"
)
type Client struct {
c net.Conn
r *bufio.Reader
}
func Dial(addr string) (*Client, error) {
c, err := net.Dial("tcp", addr)
if err != nil {
return nil, err
}
return &Client{
c: c,
r: bufio.NewReader(c),
}, nil
}
func DialTLS(addr string, cfg *tls.Config) (*Client, error) {
c, err := tls.Dial("tcp", addr, cfg)
if err != nil {
return nil, err
}
return &Client{
c: c,
r: bufio.NewReader(c),
}, nil
}
func (c *Client) Close() error {
return c.c.Close()
}
func (c *Client) Do(cmd ...string) (string, error) {
if err := Write(c.c, cmd); err != nil {
return "", err
}
return Read(c.r)
}
func (c *Client) Read() (string, error) {
return Read(c.r)
}
// Do() + ReadStrings()
func (c *Client) DoStrings(cmd ...string) ([]string, error) {
res, err := c.Do(cmd...)
if err != nil {
return nil, err
}
return ReadStrings(res)
}
+288
View File
@@ -0,0 +1,288 @@
package proto
import (
"bufio"
"errors"
"fmt"
"io"
"strconv"
"strings"
)
var (
ErrProtocol = errors.New("unsupported protocol")
ErrUnexpected = errors.New("not what you asked for")
)
func readLine(r *bufio.Reader) (string, error) {
line, err := r.ReadString('\n')
if err != nil {
return "", err
}
if len(line) < 3 {
return "", ErrProtocol
}
return line, nil
}
// Read an array, with all elements are the raw redis commands
// Also reads sets and maps.
func ReadArray(b string) ([]string, error) {
r := bufio.NewReader(strings.NewReader(b))
line, err := readLine(r)
if err != nil {
return nil, err
}
elems := 0
switch line[0] {
default:
return nil, ErrUnexpected
case '*', '>', '~':
// *: array
// >: push data
// ~: set
length, err := strconv.Atoi(line[1 : len(line)-2])
if err != nil {
return nil, err
}
elems = length
case '%':
// we also read maps.
length, err := strconv.Atoi(line[1 : len(line)-2])
if err != nil {
return nil, err
}
elems = length * 2
}
var res []string
for i := 0; i < elems; i++ {
next, err := Read(r)
if err != nil {
return nil, err
}
res = append(res, next)
}
return res, nil
}
func ReadString(b string) (string, error) {
r := bufio.NewReader(strings.NewReader(b))
line, err := readLine(r)
if err != nil {
return "", err
}
switch line[0] {
default:
return "", ErrUnexpected
case '$':
// bulk strings are: `$5\r\nhello\r\n`
length, err := strconv.Atoi(line[1 : len(line)-2])
if err != nil {
return "", err
}
if length < 0 {
// -1 is a nil response
return line, nil
}
var (
buf = make([]byte, length+2)
pos = 0
)
for pos < length+2 {
n, err := r.Read(buf[pos:])
if err != nil {
return "", err
}
pos += n
}
return string(buf[:len(buf)-2]), nil
}
}
func readInline(b string) (string, error) {
if len(b) < 3 {
return "", ErrUnexpected
}
return b[1 : len(b)-2], nil
}
func ReadError(b string) (string, error) {
if len(b) < 1 {
return "", ErrUnexpected
}
switch b[0] {
default:
return "", ErrUnexpected
case '-':
return readInline(b)
}
}
func ReadStrings(b string) ([]string, error) {
elems, err := ReadArray(b)
if err != nil {
return nil, err
}
var res []string
for _, e := range elems {
s, err := ReadString(e)
if err != nil {
return nil, err
}
res = append(res, s)
}
return res, nil
}
// Read a single command, returning it raw. Used to read replies from redis.
// Understands RESP3 proto.
func Read(r *bufio.Reader) (string, error) {
line, err := readLine(r)
if err != nil {
return "", err
}
switch line[0] {
default:
return "", ErrProtocol
case '+', '-', ':', ',', '_':
// +: inline string
// -: errors
// :: integer
// ,: float
// _: null
// Simple line based replies.
return line, nil
case '$':
// bulk strings are: `$5\r\nhello\r\n`
length, err := strconv.Atoi(line[1 : len(line)-2])
if err != nil {
return "", err
}
if length < 0 {
// -1 is a nil response
return line, nil
}
var (
buf = make([]byte, length+2)
pos = 0
)
for pos < length+2 {
n, err := r.Read(buf[pos:])
if err != nil {
return "", err
}
pos += n
}
return line + string(buf), nil
case '*', '>', '~':
// arrays are: `*6\r\n...`
// pushdata is: `>6\r\n...`
// sets are: `~6\r\n...`
length, err := strconv.Atoi(line[1 : len(line)-2])
if err != nil {
return "", err
}
for i := 0; i < length; i++ {
next, err := Read(r)
if err != nil {
return "", err
}
line += next
}
return line, nil
case '%':
// maps are: `%3\r\n...`
length, err := strconv.Atoi(line[1 : len(line)-2])
if err != nil {
return "", err
}
for i := 0; i < length*2; i++ {
next, err := Read(r)
if err != nil {
return "", err
}
line += next
}
return line, nil
}
}
// Write a command in RESP3 proto. Used to write commands to redis.
// Currently only supports string arrays.
func Write(w io.Writer, cmd []string) error {
if _, err := fmt.Fprintf(w, "*%d\r\n", len(cmd)); err != nil {
return err
}
for _, c := range cmd {
if _, err := fmt.Fprintf(w, "$%d\r\n%s\r\n", len(c), c); err != nil {
return err
}
}
return nil
}
// Parse into interfaces. `b` must contain exactly a single command (which can be nested).
func Parse(b string) (interface{}, error) {
if len(b) < 1 {
return nil, ErrUnexpected
}
switch b[0] {
default:
return "", ErrProtocol
case '+':
return readInline(b)
case '-':
e, err := readInline(b)
if err != nil {
return nil, err
}
return errors.New(e), nil
case ':':
e, err := readInline(b)
if err != nil {
return nil, err
}
return strconv.Atoi(e)
case '$':
return ReadString(b)
case '*':
elems, err := ReadArray(b)
if err != nil {
return nil, err
}
var res []interface{}
for _, elem := range elems {
e, err := Parse(elem)
if err != nil {
return nil, err
}
res = append(res, e)
}
return res, nil
case '%':
elems, err := ReadArray(b)
if err != nil {
return nil, err
}
var res = map[interface{}]interface{}{}
for len(elems) > 1 {
key, err := Parse(elems[0])
if err != nil {
return nil, err
}
value, err := Parse(elems[1])
if err != nil {
return nil, err
}
res[key] = value
elems = elems[2:]
}
return res, nil
}
}
+102
View File
@@ -0,0 +1,102 @@
package proto
import (
"fmt"
"strings"
)
// Byte-safe string
func String(s string) string {
return fmt.Sprintf("$%d\r\n%s\r\n", len(s), s)
}
// Inline string
func Inline(s string) string {
return inline('+', s)
}
// Error
func Error(s string) string {
return inline('-', s)
}
func inline(r rune, s string) string {
return fmt.Sprintf("%s%s\r\n", string(r), s)
}
// Int
func Int(n int) string {
return fmt.Sprintf(":%d\r\n", n)
}
// Float
func Float(n float64) string {
return fmt.Sprintf(",%g\r\n", n)
}
const (
Nil = "$-1\r\n"
NilResp3 = "_\r\n"
NilList = "*-1\r\n"
)
// Array assembles the args in a list. Args should be raw redis commands.
// Example: Array(String("foo"), String("bar"))
func Array(args ...string) string {
return fmt.Sprintf("*%d\r\n", len(args)) + strings.Join(args, "")
}
// Push assembles the args for push-data. Args should be raw redis commands.
// Example: Push(String("foo"), String("bar"))
func Push(args ...string) string {
return fmt.Sprintf(">%d\r\n", len(args)) + strings.Join(args, "")
}
// Strings is a helper to build 1 dimensional string arrays.
func Strings(args ...string) string {
var strings []string
for _, a := range args {
strings = append(strings, String(a))
}
return Array(strings...)
}
// Ints is a helper to build 1 dimensional int arrays.
func Ints(args ...int) string {
var ints []string
for _, a := range args {
ints = append(ints, Int(a))
}
return Array(ints...)
}
// Map assembles the args in a map. Args should be raw redis commands.
// Must be an even number of arguments.
// Example: Map(String("foo"), String("bar"))
func Map(args ...string) string {
return fmt.Sprintf("%%%d\r\n", len(args)/2) + strings.Join(args, "")
}
// StringMap is is a wrapper to get a map of (bulk)strings.
func StringMap(args ...string) string {
var strings []string
for _, a := range args {
strings = append(strings, String(a))
}
return Map(strings...)
}
// Set assembles the args in a map. Args should be raw redis commands.
// Example: Set(String("foo"), String("bar"))
func Set(args ...string) string {
return fmt.Sprintf("~%d\r\n", len(args)) + strings.Join(args, "")
}
// StringSet is is a wrapper to get a set of (bulk)strings.
func StringSet(args ...string) string {
var strings []string
for _, a := range args {
strings = append(strings, String(a))
}
return Set(strings...)
}