Skip to content

Commit 2ebc01d

Browse files
committed
Add a session middleware
1 parent 13495e8 commit 2ebc01d

10 files changed

+830
-0
lines changed

go.mod

+3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ go 1.22.5
44

55
require (
66
github.com/go-co-op/gocron/v2 v2.11.0
7+
github.com/gorilla/securecookie v1.1.2
8+
github.com/gorilla/sessions v1.3.0
79
github.com/stretchr/testify v1.9.0
810
github.com/zeromicro/go-zero v1.7.0
911
xorm.io/xorm v1.3.9
@@ -18,6 +20,7 @@ require (
1820
github.com/fatih/color v1.17.0 // indirect
1921
github.com/go-logr/logr v1.4.2 // indirect
2022
github.com/go-logr/stdr v1.2.2 // indirect
23+
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
2124
github.com/golang/snappy v0.0.4 // indirect
2225
github.com/google/uuid v1.6.0 // indirect
2326
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect

go.sum

+8
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,22 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
2929
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
3030
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
3131
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
32+
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
33+
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
3234
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
3335
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
3436
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
3537
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
3638
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
3739
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
40+
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
41+
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
3842
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
3943
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
44+
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
45+
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
46+
github.com/gorilla/sessions v1.3.0 h1:XYlkq7KcpOB2ZhHBPv5WpjMIxrQosiZanfoy1HLZFzg=
47+
github.com/gorilla/sessions v1.3.0/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
4048
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
4149
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
4250
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=

session/config.go

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package session
2+
3+
//lint:file-ignore SA5008 Use gozero config tags
4+
//nolint:staticcheck
5+
type SessionConfig struct {
6+
SessionSecret string // used to authenticate session cookies using HMAC
7+
SessionStorageNamespace string `json:",default=sessions"`
8+
SessionCookieName string `json:",default=SID"`
9+
SessionCookiePath string `json:",default=/"`
10+
SessionCookieDomain string `json:",optional"`
11+
// The duration in seconds that the session cookie/token is valid,
12+
// and also how long users stay logged-in to the App.
13+
SessionCookieTTL int `json:",default=600,range=[60:]"`
14+
SessionCookieSameSite string `json:",default=Lax,options=Strict|Lax|None"`
15+
SessionCookieSecure bool `json:",default=false"`
16+
// The session storage TTL is derived from its max age plus this grace period.
17+
SessionStorageGracePeriod int `json:",default=10,range=[1:60]"`
18+
SessionStorageUnauthenticatedTTL int `json:",default=60,range=[0:600]"`
19+
SessionStorageInjectedAuthenticationTTL int `json:",default=0,range=[0:60]"`
20+
}

session/const.go

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package session
2+
3+
import (
4+
"net"
5+
"net/http"
6+
"net/textproto"
7+
"strings"
8+
)
9+
10+
// These are transient session keys
11+
const (
12+
// Is the session authenticated: 0: no; 1: yes
13+
Authenticated = "authenticated"
14+
// The user's ID
15+
UserID = "user_id"
16+
// The user's name
17+
Username = "username"
18+
// The user's type: admin|<nil>
19+
UserType = "user_type"
20+
// The time when the session is created
21+
Created = "created"
22+
// The time when the session is last updated
23+
Updated = "updated"
24+
// The path where the session is last updated
25+
Path = "path"
26+
// User-Agent request header
27+
UserAgent = "user_agent"
28+
// The user's IP address
29+
UserIPAddr = "user_ipaddr"
30+
)
31+
32+
// GetUserAddr from the http request, considering X-Forwarded-For, Forwarded
33+
func GetUserAddr(r *http.Request) string {
34+
if xFwd := r.Header.Get("X-Forwarded-For"); xFwd != "" {
35+
client, _, _ := strings.Cut(xFwd, ",")
36+
return textproto.TrimString(client)
37+
}
38+
if fwd := r.Header.Get("Forwarded"); fwd != "" {
39+
client, _, _ := strings.Cut(fwd, ",")
40+
client = readCookieValue(client, "for")
41+
return stripPort(client)
42+
}
43+
return stripPort(r.RemoteAddr)
44+
}
45+
46+
func stripPort(addr string) string {
47+
if host, _, err := net.SplitHostPort(addr); err == nil {
48+
return host
49+
}
50+
return addr
51+
}
52+
53+
func readCookieValue(line, filter string) string {
54+
line = textproto.TrimString(line)
55+
56+
var part string
57+
for len(line) > 0 { // continue since we have rest
58+
part, line, _ = strings.Cut(line, ";")
59+
part = textproto.TrimString(part)
60+
if part == "" {
61+
continue
62+
}
63+
name, val, _ := strings.Cut(part, "=")
64+
name = textproto.TrimString(name)
65+
if !isCookieNameValid(name) {
66+
continue
67+
}
68+
if filter != "" && !strings.EqualFold(filter, name) {
69+
continue
70+
}
71+
val, ok := parseCookieValue(val, true)
72+
if !ok {
73+
continue
74+
}
75+
return val
76+
}
77+
78+
return ""
79+
}

session/const_test.go

+133
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package session
2+
3+
import (
4+
"net/http"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestGetUserAddr(t *testing.T) {
11+
tests := []struct {
12+
name string
13+
input http.Request
14+
expect string
15+
}{
16+
{
17+
name: "Test0_X-Forwarded-For",
18+
input: http.Request{
19+
RemoteAddr: "1.2.3.4:1234",
20+
Header: http.Header{
21+
"X-Forwarded-For": []string{"203.0.113.195"},
22+
},
23+
},
24+
expect: "203.0.113.195",
25+
},
26+
{
27+
name: "Test1_X-Forwarded-For",
28+
input: http.Request{
29+
RemoteAddr: "1.2.3.4:1234",
30+
Header: http.Header{
31+
"X-Forwarded-For": []string{"2001:db8:85a3:8d3:1319:8a2e:370:7348"},
32+
},
33+
},
34+
expect: "2001:db8:85a3:8d3:1319:8a2e:370:7348",
35+
},
36+
{
37+
name: "Test2_X-Forwarded-For",
38+
input: http.Request{
39+
RemoteAddr: "1.2.3.4:1234",
40+
Header: http.Header{
41+
"X-Forwarded-For": []string{"203.0.113.195, 2001:db8:85a3:8d3:1319:8a2e:370:7348"},
42+
},
43+
},
44+
expect: "203.0.113.195",
45+
},
46+
{
47+
name: "Test3_X-Forwarded-For",
48+
input: http.Request{
49+
RemoteAddr: "1.2.3.4:1234",
50+
Header: http.Header{
51+
"X-Forwarded-For": []string{"203.0.113.195,2001:db8:85a3:8d3:1319:8a2e:370:7348,198.51.100.178"},
52+
},
53+
},
54+
expect: "203.0.113.195",
55+
},
56+
{
57+
name: "Test0_Forwarded",
58+
input: http.Request{
59+
RemoteAddr: "1.2.3.4:1234",
60+
Header: http.Header{
61+
"Forwarded": []string{`for="_mdn"`},
62+
},
63+
},
64+
expect: "_mdn",
65+
},
66+
{
67+
name: "Test1_Forwarded",
68+
input: http.Request{
69+
RemoteAddr: "1.2.3.4:1234",
70+
Header: http.Header{
71+
"Forwarded": []string{`For="[2001:db8:cafe::17]:4711"`},
72+
},
73+
},
74+
expect: "2001:db8:cafe::17",
75+
},
76+
{
77+
name: "Test2_Forwarded",
78+
input: http.Request{
79+
RemoteAddr: "1.2.3.4:1234",
80+
Header: http.Header{
81+
"Forwarded": []string{`for=192.0.2.60;proto=http;by=203.0.113.43`},
82+
},
83+
},
84+
expect: "192.0.2.60",
85+
},
86+
{
87+
name: "Test3_Forwarded",
88+
input: http.Request{
89+
RemoteAddr: "1.2.3.4:1234",
90+
Header: http.Header{
91+
"Forwarded": []string{`for=192.0.2.43, for=198.51.100.17`},
92+
},
93+
},
94+
expect: "192.0.2.43",
95+
},
96+
{
97+
name: "Test0_Mixed",
98+
input: http.Request{
99+
RemoteAddr: "1.2.3.4:1234",
100+
Header: http.Header{
101+
"Forwarded": []string{`for=192.0.2.172`},
102+
"X-Forwarded-For": []string{"192.0.2.172"},
103+
},
104+
},
105+
expect: "192.0.2.172",
106+
},
107+
{
108+
name: "Test1_Mixed",
109+
input: http.Request{
110+
RemoteAddr: "1.2.3.4:1234",
111+
Header: http.Header{
112+
"Forwarded": []string{`for=192.0.2.43, for="[2001:db8:cafe::17]"`},
113+
"X-Forwarded-For": []string{"192.0.2.43, 2001:db8:cafe::17"},
114+
},
115+
},
116+
expect: "192.0.2.43",
117+
},
118+
{
119+
name: "Test0_RemoteAddr",
120+
input: http.Request{
121+
RemoteAddr: "1.2.3.4:1234",
122+
},
123+
expect: "1.2.3.4",
124+
},
125+
}
126+
127+
for _, test := range tests {
128+
t.Run(test.name, func(t *testing.T) {
129+
addr := GetUserAddr(&test.input)
130+
assert.Exactly(t, test.expect, addr)
131+
})
132+
}
133+
}

0 commit comments

Comments
 (0)