Merge pull request #90 from ssb-ngi-pointer/sign-in-with-ssb
go side of sign-in with ssb
This commit is contained in:
commit
8821b99e27
|
@ -26,10 +26,13 @@ import (
|
|||
kitlog "github.com/go-kit/kit/log"
|
||||
"github.com/go-kit/kit/log/level"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/throttled/throttled/v2"
|
||||
"github.com/throttled/throttled/v2/store/memstore"
|
||||
"github.com/unrolled/secure"
|
||||
"go.cryptoscope.co/muxrpc/v2/debug"
|
||||
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/repo"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/signinwithssb"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb/sqlite"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomsrv"
|
||||
mksrv "github.com/ssb-ngi-pointer/go-ssb-room/roomsrv"
|
||||
|
@ -203,10 +206,14 @@ func runroomsrv() error {
|
|||
return fmt.Errorf("failed to initiate database: %w", err)
|
||||
}
|
||||
|
||||
bridge := signinwithssb.NewSignalBridge()
|
||||
|
||||
// create the shs+muxrpc server
|
||||
roomsrv, err := mksrv.New(
|
||||
db.Members,
|
||||
db.Aliases,
|
||||
db.AuthWithSSB,
|
||||
bridge,
|
||||
httpsDomain,
|
||||
opts...)
|
||||
if err != nil {
|
||||
|
@ -238,7 +245,7 @@ func runroomsrv() error {
|
|||
}()
|
||||
|
||||
// setup web dashboard handlers
|
||||
dashboardH, err := handlers.New(
|
||||
webHandler, err := handlers.New(
|
||||
kitlog.With(log, "package", "web"),
|
||||
repo.New(repoDir),
|
||||
handlers.NetworkInfo{
|
||||
|
@ -248,9 +255,12 @@ func runroomsrv() error {
|
|||
RoomID: roomsrv.Whoami(),
|
||||
},
|
||||
roomsrv.StateManager,
|
||||
roomsrv.Network,
|
||||
bridge,
|
||||
handlers.Databases{
|
||||
Aliases: db.Aliases,
|
||||
AuthFallback: db.AuthFallback,
|
||||
AuthWithSSB: db.AuthWithSSB,
|
||||
DeniedKeys: db.DeniedKeys,
|
||||
Invites: db.Invites,
|
||||
Notices: db.Notices,
|
||||
|
@ -283,13 +293,39 @@ func runroomsrv() error {
|
|||
STSIncludeSubdomains: false,
|
||||
|
||||
// See for more https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
|
||||
ContentSecurityPolicy: "default-src 'self'", // enforce no external content
|
||||
// helpful: https://report-uri.com/home/generate
|
||||
ContentSecurityPolicy: "default-src 'self'; img-src 'self' data:", // enforce no external content
|
||||
|
||||
BrowserXssFilter: true,
|
||||
FrameDeny: true,
|
||||
//ContentTypeNosniff: true, // TODO: fix Content-Type headers served from assets
|
||||
})
|
||||
|
||||
// HTTP rate limiter
|
||||
throttleStore, err := memstore.New(65536) // 64k different combinations of limitByPathAndAddr
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to init HTTP rate limiter store: %w", err)
|
||||
}
|
||||
quota := throttled.RateQuota{
|
||||
MaxRate: throttled.PerSec(5), // different requests per second per VaryBy
|
||||
MaxBurst: 25,
|
||||
}
|
||||
limiter, err := throttled.NewGCRARateLimiter(throttleStore, quota)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to init HTTP rate limiter: %w", err)
|
||||
}
|
||||
|
||||
httpRateLimiter := throttled.HTTPRateLimiter{
|
||||
RateLimiter: limiter,
|
||||
VaryBy: limitByPathAndAddr{},
|
||||
}
|
||||
|
||||
// wrap dashboard/alias/invite handler in ratlimiter and security middleware
|
||||
var httpHandler http.Handler
|
||||
httpHandler = httpRateLimiter.RateLimit(webHandler)
|
||||
httpHandler = secureMiddleware.Handler(httpHandler)
|
||||
|
||||
// all init was successfull
|
||||
level.Info(log).Log(
|
||||
"event", "serving",
|
||||
"ID", roomsrv.Whoami().Ref(),
|
||||
|
@ -305,11 +341,12 @@ func runroomsrv() error {
|
|||
Addr: httpLis.Addr().String(),
|
||||
|
||||
// Good practice to set timeouts to avoid Slowloris attacks.
|
||||
WriteTimeout: time.Second * 15,
|
||||
ReadTimeout: time.Second * 15,
|
||||
IdleTimeout: time.Second * 60,
|
||||
// Keep in mind that the SSE stuff for "sign-in with ssb" can take a moment, thou
|
||||
ReadHeaderTimeout: time.Second * 15,
|
||||
WriteTimeout: time.Minute * 3,
|
||||
IdleTimeout: time.Minute * 3,
|
||||
|
||||
Handler: secureMiddleware.Handler(dashboardH),
|
||||
Handler: httpHandler,
|
||||
}
|
||||
|
||||
err = srv.Serve(httpLis)
|
||||
|
@ -342,3 +379,20 @@ func main() {
|
|||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
type limitByPathAndAddr struct{}
|
||||
|
||||
func (limitByPathAndAddr) Key(r *http.Request) string {
|
||||
var k strings.Builder
|
||||
|
||||
k.WriteString(r.URL.Path)
|
||||
k.WriteString("\n")
|
||||
|
||||
remoteIP := r.Header.Get("X-Forwarded-For")
|
||||
if remoteIP == "" {
|
||||
remoteIP = r.RemoteAddr
|
||||
}
|
||||
k.WriteString(remoteIP)
|
||||
|
||||
return k.String()
|
||||
}
|
||||
|
|
8
go.mod
8
go.mod
|
@ -5,7 +5,7 @@ go 1.16
|
|||
require (
|
||||
github.com/BurntSushi/toml v0.3.1
|
||||
github.com/PuerkitoBio/goquery v1.5.0
|
||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.0
|
||||
github.com/friendsofgo/errors v0.9.2
|
||||
github.com/go-kit/kit v0.10.0
|
||||
github.com/gofrs/uuid v4.0.0+incompatible // indirect
|
||||
|
@ -15,13 +15,14 @@ require (
|
|||
github.com/gorilla/sessions v1.2.1
|
||||
github.com/gorilla/websocket v1.4.2
|
||||
github.com/keks/nocomment v0.0.0-20181007001506-30c6dcb4a472
|
||||
github.com/kevinburke/go-bindata v3.21.0+incompatible // indirect
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible
|
||||
github.com/nicksnyder/go-i18n/v2 v2.1.2
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/rubenv/sql-migrate v0.0.0-20200616145509-8d140a17f351
|
||||
github.com/russross/blackfriday/v2 v2.1.0
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/stretchr/testify v1.6.1
|
||||
github.com/throttled/throttled/v2 v2.7.1
|
||||
github.com/unrolled/secure v1.0.8
|
||||
github.com/vcraescu/go-paginator/v2 v2.0.0
|
||||
github.com/volatiletech/sqlboiler-sqlite3 v0.0.0-20210314195744-a1c697a68aef // indirect
|
||||
|
@ -30,9 +31,10 @@ require (
|
|||
go.cryptoscope.co/muxrpc/v2 v2.0.0-beta.1.0.20210308090127-5f1f5f9cbb59
|
||||
go.cryptoscope.co/netwrap v0.1.1
|
||||
go.cryptoscope.co/secretstream v1.2.2
|
||||
go.mindeco.de v1.8.0
|
||||
go.mindeco.de v1.9.0
|
||||
go.mindeco.de/ssb-refs v0.1.1-0.20210108133850-cf1f44fea870
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad
|
||||
golang.org/x/net v0.0.0-20191116160921-f9c825593386 // indirect
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect
|
||||
golang.org/x/text v0.3.5
|
||||
|
|
46
go.sum
46
go.sum
|
@ -111,6 +111,7 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9
|
|||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-redis/redis v6.15.8+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
|
||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
||||
|
@ -145,13 +146,22 @@ github.com/golang/protobuf v1.0.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
|
|||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
|
||||
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
|
@ -197,6 +207,8 @@ github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09
|
|||
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
|
||||
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
|
@ -232,7 +244,6 @@ github.com/keks/nocomment v0.0.0-20181007001506-30c6dcb4a472 h1:6nrO82kszcc+rcKP
|
|||
github.com/keks/nocomment v0.0.0-20181007001506-30c6dcb4a472/go.mod h1:oLLUlGld/axGHThR36o8bADQUHG+TKSUdoKqCvnoQB4=
|
||||
github.com/keks/persist v0.0.0-20180731151133-9546f7b3f97e/go.mod h1:KMIOJFEE+0E/mYfYExA9vOpCFDz4TQfzk6mCOtCXR9k=
|
||||
github.com/keks/persist v0.0.0-20181029214439-3af502dad70b/go.mod h1:KMIOJFEE+0E/mYfYExA9vOpCFDz4TQfzk6mCOtCXR9k=
|
||||
github.com/kevinburke/go-bindata v3.21.0+incompatible/go.mod h1:/pEEZ72flUW2p0yi30bslSp9YqD9pysLxunQDdb2CPM=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
|
@ -298,7 +309,9 @@ github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXW
|
|||
github.com/olekukonko/tablewriter v0.0.2/go.mod h1:rSAaSIOAGT9odnlyGlUfAJaoc5w2fSBUmeGDbRWPxyQ=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
||||
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
|
||||
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
|
||||
|
@ -373,6 +386,8 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV
|
|||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
|
@ -408,6 +423,8 @@ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd
|
|||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/throttled/throttled/v2 v2.7.1 h1:FnBysDX4Sok55bvfDMI0l2Y71V1vM2wi7O79OW7fNtw=
|
||||
github.com/throttled/throttled/v2 v2.7.1/go.mod h1:fuOeyK9fmnA+LQnsBbfT/mmPHjmkdogRBQxaD8YsgZ8=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/ugorji/go v1.1.1/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ=
|
||||
|
@ -427,18 +444,14 @@ github.com/vcraescu/go-paginator/v2 v2.0.0 h1:m9If0wF7pSjYfocrJZcyWNiWn7OfIeLFVQ
|
|||
github.com/vcraescu/go-paginator/v2 v2.0.0/go.mod h1:qsrC8+/YgRL0LfurxeY3gCAtsN7oOthkIbmBdqpMX9U=
|
||||
github.com/volatiletech/inflect v0.0.1 h1:2a6FcMQyhmPZcLa+uet3VJ8gLn/9svWhJxJYwvE8KsU=
|
||||
github.com/volatiletech/inflect v0.0.1/go.mod h1:IBti31tG6phkHitLlr5j7shC5SOo//x0AjDzaJU1PLA=
|
||||
github.com/volatiletech/null/v8 v8.1.0 h1:eAO3I31A5R04usY5SKMMfDcOCnEGyT/T4wRI0JVGp4U=
|
||||
github.com/volatiletech/null/v8 v8.1.0/go.mod h1:98DbwNoKEpRrYtGjWFctievIfm4n4MxG0A6EBUcoS5g=
|
||||
github.com/volatiletech/null/v8 v8.1.2 h1:kiTiX1PpwvuugKwfvUNX/SU/5A2KGZMXfGD0DUHdKEI=
|
||||
github.com/volatiletech/null/v8 v8.1.2/go.mod h1:98DbwNoKEpRrYtGjWFctievIfm4n4MxG0A6EBUcoS5g=
|
||||
github.com/volatiletech/randomize v0.0.1 h1:eE5yajattWqTB2/eN8df4dw+8jwAzBtbdo5sbWC4nMk=
|
||||
github.com/volatiletech/randomize v0.0.1/go.mod h1:GN3U0QYqfZ9FOJ67bzax1cqZ5q2xuj2mXrXBjWaRTlY=
|
||||
github.com/volatiletech/sqlboiler-sqlite3 v0.0.0-20200618013359-a93887c09a14 h1:2PCMsnM/GVptZVyB8s0vTIFCPjl6f5rhz5Ry5MNShMQ=
|
||||
github.com/volatiletech/sqlboiler-sqlite3 v0.0.0-20200618013359-a93887c09a14/go.mod h1:fmZQG/eGdD2vdjWZjrVq4v2sTQ+Alz/I09chjYWWUVw=
|
||||
github.com/volatiletech/sqlboiler-sqlite3 v0.0.0-20210314195744-a1c697a68aef h1:XjoYLjR/XToxGQY9O6WKA8l6t0hIWjPwazVQcQEctFM=
|
||||
github.com/volatiletech/sqlboiler-sqlite3 v0.0.0-20210314195744-a1c697a68aef/go.mod h1:fmZQG/eGdD2vdjWZjrVq4v2sTQ+Alz/I09chjYWWUVw=
|
||||
github.com/volatiletech/sqlboiler/v4 v4.0.0/go.mod h1:U0Z5K4y+twWgHxh364G45QyzyNssSbBqNWtXGHVTlgM=
|
||||
github.com/volatiletech/sqlboiler/v4 v4.4.0 h1:aSlvHidRBuxHHQZNX3ZLGgzNVPVPzWqsC3lhcLbV/b0=
|
||||
github.com/volatiletech/sqlboiler/v4 v4.4.0/go.mod h1:h4RBAO6QbwMP3ezGmtfGljRms7S27cFIgF3rKgPKstE=
|
||||
github.com/volatiletech/sqlboiler/v4 v4.5.0 h1:oJ3YXEvv0c48S9W/3TuPLxJxefIkewpub2qZioXXlUY=
|
||||
github.com/volatiletech/sqlboiler/v4 v4.5.0/go.mod h1:tQgF5zxwqrjR6Wydc5rRylI6puDOO1WvBC70/5up+Hg=
|
||||
github.com/volatiletech/strmangle v0.0.1 h1:UKQoHmY6be/R3tSvD2nQYrH41k43OJkidwEiC74KIzk=
|
||||
|
@ -461,8 +474,6 @@ go.cryptoscope.co/margaret v0.0.5/go.mod h1:W+Q6lvzHIrF8+Yt3dItHHsx2R9/Xvj/NkJGM
|
|||
go.cryptoscope.co/margaret v0.0.8/go.mod h1:VbP0bqavqW5osTmdNvgukrqtmqvZC5sXyPSBUW5rzv8=
|
||||
go.cryptoscope.co/margaret v0.0.12-0.20190912103626-34323ad497f4 h1:gLSldWRujtUOfdnpA1XKD71xcCp3Wz1URMnT6xpUPV4=
|
||||
go.cryptoscope.co/margaret v0.0.12-0.20190912103626-34323ad497f4/go.mod h1:3rt+RmZTFZEgfvFxz0ZPDBIWtLJOouWtzV6YbBl6sek=
|
||||
go.cryptoscope.co/muxrpc/v2 v2.0.0-20210202162901-fe642d405dc6 h1:p135TwijE3DbmklGygc7++MMRRVlujmjqed8kEOmwLs=
|
||||
go.cryptoscope.co/muxrpc/v2 v2.0.0-20210202162901-fe642d405dc6/go.mod h1:MgaeojIkWY3lLuoNw1mlMT3b3jiZwOj/fgsoGZp/VNA=
|
||||
go.cryptoscope.co/muxrpc/v2 v2.0.0-beta.1.0.20210308090127-5f1f5f9cbb59 h1:Gv5pKkvHYJNc12uRZ/jMCsR17G7v6oFLLCrGAUVxhvo=
|
||||
go.cryptoscope.co/muxrpc/v2 v2.0.0-beta.1.0.20210308090127-5f1f5f9cbb59/go.mod h1:MgaeojIkWY3lLuoNw1mlMT3b3jiZwOj/fgsoGZp/VNA=
|
||||
go.cryptoscope.co/netwrap v0.1.0/go.mod h1:7zcYswCa4CT+ct54e9uH9+IIbYYETEMHKDNpzl8Ukew=
|
||||
|
@ -473,8 +484,8 @@ go.cryptoscope.co/secretstream v1.2.2/go.mod h1:7nRGZ7fTqSgQAnv2Y4m8xQsS3MFxvB7I
|
|||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
|
||||
go.mindeco.de v1.8.0 h1:Vxob3XaDz85aD4wq8VbQxtradpHbmjciG2eSJLGaFV0=
|
||||
go.mindeco.de v1.8.0/go.mod h1:ePOcyktbpqzhMPRBDv2gUaDd3h8QtT+DUU1DK+VbQZE=
|
||||
go.mindeco.de v1.9.0 h1:/xli02DkzpIUZxp/rp1nj8z/OZ9MHvkMIr9TfDVcmBg=
|
||||
go.mindeco.de v1.9.0/go.mod h1:ePOcyktbpqzhMPRBDv2gUaDd3h8QtT+DUU1DK+VbQZE=
|
||||
go.mindeco.de/ssb-refs v0.1.1-0.20210108133850-cf1f44fea870 h1:TCI3AefMAaOYECvppn30+CfEB0Fn8IES1SKvvacc3/c=
|
||||
go.mindeco.de/ssb-refs v0.1.1-0.20210108133850-cf1f44fea870/go.mod h1:OnBnV02ux4lLsZ39LID6yYLqSDp+dqTHb/3miYPkQFs=
|
||||
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||
|
@ -516,8 +527,10 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR
|
|||
golang.org/x/net v0.0.0-20190607181551-461777fb6f67/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 h1:fHDIZ2oxGnUZRN6WgWFCbYBjH9uqVPRCUVUDhs0wnbA=
|
||||
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191116160921-f9c825593386 h1:ktbWvQrW08Txdxno1PiDpSxPXG6ndGsfnJjRRtkM0LQ=
|
||||
golang.org/x/net v0.0.0-20191116160921-f9c825593386/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
@ -549,6 +562,7 @@ golang.org/x/sys v0.0.0-20190610200419-93c9922d18ae/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191007154456-ef33b2fb2c41/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
@ -599,10 +613,17 @@ google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac
|
|||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
|
@ -620,6 +641,7 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
|
|
|
@ -24,13 +24,22 @@ type EndpointStat struct {
|
|||
Endpoint muxrpc.Endpoint
|
||||
}
|
||||
|
||||
//go:generate counterfeiter -o mocked/endpoints.go . Endpoints
|
||||
|
||||
// Endpoints returns the connected endpoint for the passed feed,
|
||||
// or false if there is none.
|
||||
type Endpoints interface {
|
||||
GetEndpointFor(refs.FeedRef) (muxrpc.Endpoint, bool)
|
||||
}
|
||||
|
||||
// Network supplies all network related functionalitiy
|
||||
type Network interface {
|
||||
Connect(ctx context.Context, addr net.Addr) error
|
||||
Serve(context.Context, ...muxrpc.HandlerWrapper) error
|
||||
GetListenAddr() net.Addr
|
||||
|
||||
GetAllEndpoints() []EndpointStat
|
||||
GetEndpointFor(refs.FeedRef) (muxrpc.Endpoint, bool)
|
||||
Endpoints
|
||||
|
||||
GetConnTracker() ConnTracker
|
||||
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
// Code generated by counterfeiter. DO NOT EDIT.
|
||||
package mocked
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/network"
|
||||
muxrpc "go.cryptoscope.co/muxrpc/v2"
|
||||
refs "go.mindeco.de/ssb-refs"
|
||||
)
|
||||
|
||||
type FakeEndpoints struct {
|
||||
GetEndpointForStub func(refs.FeedRef) (muxrpc.Endpoint, bool)
|
||||
getEndpointForMutex sync.RWMutex
|
||||
getEndpointForArgsForCall []struct {
|
||||
arg1 refs.FeedRef
|
||||
}
|
||||
getEndpointForReturns struct {
|
||||
result1 muxrpc.Endpoint
|
||||
result2 bool
|
||||
}
|
||||
getEndpointForReturnsOnCall map[int]struct {
|
||||
result1 muxrpc.Endpoint
|
||||
result2 bool
|
||||
}
|
||||
invocations map[string][][]interface{}
|
||||
invocationsMutex sync.RWMutex
|
||||
}
|
||||
|
||||
func (fake *FakeEndpoints) GetEndpointFor(arg1 refs.FeedRef) (muxrpc.Endpoint, bool) {
|
||||
fake.getEndpointForMutex.Lock()
|
||||
ret, specificReturn := fake.getEndpointForReturnsOnCall[len(fake.getEndpointForArgsForCall)]
|
||||
fake.getEndpointForArgsForCall = append(fake.getEndpointForArgsForCall, struct {
|
||||
arg1 refs.FeedRef
|
||||
}{arg1})
|
||||
stub := fake.GetEndpointForStub
|
||||
fakeReturns := fake.getEndpointForReturns
|
||||
fake.recordInvocation("GetEndpointFor", []interface{}{arg1})
|
||||
fake.getEndpointForMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1, ret.result2
|
||||
}
|
||||
return fakeReturns.result1, fakeReturns.result2
|
||||
}
|
||||
|
||||
func (fake *FakeEndpoints) GetEndpointForCallCount() int {
|
||||
fake.getEndpointForMutex.RLock()
|
||||
defer fake.getEndpointForMutex.RUnlock()
|
||||
return len(fake.getEndpointForArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeEndpoints) GetEndpointForCalls(stub func(refs.FeedRef) (muxrpc.Endpoint, bool)) {
|
||||
fake.getEndpointForMutex.Lock()
|
||||
defer fake.getEndpointForMutex.Unlock()
|
||||
fake.GetEndpointForStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeEndpoints) GetEndpointForArgsForCall(i int) refs.FeedRef {
|
||||
fake.getEndpointForMutex.RLock()
|
||||
defer fake.getEndpointForMutex.RUnlock()
|
||||
argsForCall := fake.getEndpointForArgsForCall[i]
|
||||
return argsForCall.arg1
|
||||
}
|
||||
|
||||
func (fake *FakeEndpoints) GetEndpointForReturns(result1 muxrpc.Endpoint, result2 bool) {
|
||||
fake.getEndpointForMutex.Lock()
|
||||
defer fake.getEndpointForMutex.Unlock()
|
||||
fake.GetEndpointForStub = nil
|
||||
fake.getEndpointForReturns = struct {
|
||||
result1 muxrpc.Endpoint
|
||||
result2 bool
|
||||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeEndpoints) GetEndpointForReturnsOnCall(i int, result1 muxrpc.Endpoint, result2 bool) {
|
||||
fake.getEndpointForMutex.Lock()
|
||||
defer fake.getEndpointForMutex.Unlock()
|
||||
fake.GetEndpointForStub = nil
|
||||
if fake.getEndpointForReturnsOnCall == nil {
|
||||
fake.getEndpointForReturnsOnCall = make(map[int]struct {
|
||||
result1 muxrpc.Endpoint
|
||||
result2 bool
|
||||
})
|
||||
}
|
||||
fake.getEndpointForReturnsOnCall[i] = struct {
|
||||
result1 muxrpc.Endpoint
|
||||
result2 bool
|
||||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeEndpoints) Invocations() map[string][][]interface{} {
|
||||
fake.invocationsMutex.RLock()
|
||||
defer fake.invocationsMutex.RUnlock()
|
||||
fake.getEndpointForMutex.RLock()
|
||||
defer fake.getEndpointForMutex.RUnlock()
|
||||
copiedInvocations := map[string][][]interface{}{}
|
||||
for key, value := range fake.invocations {
|
||||
copiedInvocations[key] = value
|
||||
}
|
||||
return copiedInvocations
|
||||
}
|
||||
|
||||
func (fake *FakeEndpoints) recordInvocation(key string, args []interface{}) {
|
||||
fake.invocationsMutex.Lock()
|
||||
defer fake.invocationsMutex.Unlock()
|
||||
if fake.invocations == nil {
|
||||
fake.invocations = map[string][][]interface{}{}
|
||||
}
|
||||
if fake.invocations[key] == nil {
|
||||
fake.invocations[key] = [][]interface{}{}
|
||||
}
|
||||
fake.invocations[key] = append(fake.invocations[key], args)
|
||||
}
|
||||
|
||||
var _ network.Endpoints = new(FakeEndpoints)
|
|
@ -0,0 +1,16 @@
|
|||
package randutil
|
||||
|
||||
import "math/rand"
|
||||
|
||||
var alphabet = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
|
||||
|
||||
// String returns a random string of length n, using the alphnum character set (a-z, A-Z, 0-9)
|
||||
func String(n int) string {
|
||||
s := make([]rune, n)
|
||||
|
||||
for i := range s {
|
||||
s[i] = alphabet[rand.Intn(len(alphabet))]
|
||||
}
|
||||
|
||||
return string(s)
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package signinwithssb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SignalBridge implements a way for muxrpc and http handlers to communicate about SIWSSB events
|
||||
type SignalBridge struct {
|
||||
mu *sync.Mutex
|
||||
|
||||
sessions sessionMap
|
||||
}
|
||||
|
||||
type sessionMap map[string]chan Event
|
||||
|
||||
// Event is the unit of information that is sent over the bridge.
|
||||
type Event struct {
|
||||
Worked bool
|
||||
|
||||
// the token value if it did work
|
||||
Token string
|
||||
|
||||
// reason why it didn't work
|
||||
Reason error
|
||||
}
|
||||
|
||||
// NewSignalBridge returns a new SignalBridge
|
||||
func NewSignalBridge() *SignalBridge {
|
||||
return &SignalBridge{
|
||||
mu: new(sync.Mutex),
|
||||
sessions: make(sessionMap),
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterSession registers a new session on the bridge.
|
||||
// It returns a fresh server challenge, which acts as the session key.
|
||||
func (sb *SignalBridge) RegisterSession() string {
|
||||
sb.mu.Lock()
|
||||
defer sb.mu.Unlock()
|
||||
|
||||
c := GenerateChallenge()
|
||||
_, used := sb.sessions[c]
|
||||
if used {
|
||||
for used { // generate new challenges until we have an un-used one
|
||||
c = GenerateChallenge()
|
||||
_, used = sb.sessions[c]
|
||||
}
|
||||
}
|
||||
|
||||
evtCh := make(chan Event)
|
||||
sb.sessions[c] = evtCh
|
||||
|
||||
go func() { // make sure the session doesn't go stale and collect dust (ie unused memory)
|
||||
time.Sleep(10 * time.Minute)
|
||||
sb.mu.Lock()
|
||||
defer sb.mu.Unlock()
|
||||
delete(sb.sessions, c)
|
||||
}()
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// GetEventChannel returns the channel for the passed challenge from which future events can be read.
|
||||
// If sc doesn't exist, the 2nd argument is false.
|
||||
func (sb *SignalBridge) GetEventChannel(sc string) (<-chan Event, bool) {
|
||||
sb.mu.Lock()
|
||||
defer sb.mu.Unlock()
|
||||
ch, has := sb.sessions[sc]
|
||||
return ch, has
|
||||
}
|
||||
|
||||
// SessionWorked uses the passed challenge to send on and close the open channel.
|
||||
// It will return an error if the session doesn't exist.
|
||||
func (sb *SignalBridge) SessionWorked(sc string, token string) error {
|
||||
return sb.sendAndClose(sc, Event{
|
||||
Worked: true,
|
||||
Token: token,
|
||||
})
|
||||
}
|
||||
|
||||
// SessionFailed uses the passed challenge to send on and close the open channel.
|
||||
// It will return an error if the session doesn't exist.
|
||||
func (sb *SignalBridge) SessionFailed(sc string, reason error) error {
|
||||
return sb.sendAndClose(sc, Event{
|
||||
Worked: false,
|
||||
Reason: reason,
|
||||
})
|
||||
}
|
||||
|
||||
func (sb *SignalBridge) sendAndClose(sc string, evt Event) error {
|
||||
sb.mu.Lock()
|
||||
defer sb.mu.Unlock()
|
||||
|
||||
ch, ok := sb.sessions[sc]
|
||||
if !ok {
|
||||
return fmt.Errorf("no such session")
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
timeout = time.NewTimer(2 * time.Minute)
|
||||
)
|
||||
|
||||
// handle what happens if the sse client isn't connected
|
||||
select {
|
||||
case <-timeout.C:
|
||||
err = fmt.Errorf("faled to send completed session")
|
||||
|
||||
case ch <- evt:
|
||||
timeout.Stop()
|
||||
}
|
||||
|
||||
// session is finalized either way
|
||||
close(ch)
|
||||
delete(sb.sessions, sc)
|
||||
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package signinwithssb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBridgeWorked(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
a := assert.New(t)
|
||||
|
||||
sb := NewSignalBridge()
|
||||
|
||||
// try to use a non-existant session
|
||||
err := sb.SessionWorked("nope", "just a test")
|
||||
a.Error(err)
|
||||
|
||||
// make a new session
|
||||
sc := sb.RegisterSession()
|
||||
|
||||
b, err := DecodeChallengeString(sc)
|
||||
a.NoError(err)
|
||||
a.Len(b, challengeLength)
|
||||
|
||||
updates, has := sb.GetEventChannel(sc)
|
||||
a.True(has)
|
||||
|
||||
go func() {
|
||||
err := sb.SessionWorked(sc, "a token")
|
||||
a.NoError(err)
|
||||
}()
|
||||
|
||||
time.Sleep(time.Second / 4)
|
||||
|
||||
select {
|
||||
case evt := <-updates:
|
||||
a.True(evt.Worked)
|
||||
a.Equal("a token", evt.Token)
|
||||
a.Nil(evt.Reason)
|
||||
default:
|
||||
t.Error("no updates")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBridgeFailed(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
a := assert.New(t)
|
||||
|
||||
sb := NewSignalBridge()
|
||||
|
||||
// try to use a non-existant session
|
||||
testReason := fmt.Errorf("just an error")
|
||||
err := sb.SessionFailed("nope", testReason)
|
||||
a.Error(err)
|
||||
|
||||
// make a new session
|
||||
sc := sb.RegisterSession()
|
||||
|
||||
b, err := DecodeChallengeString(sc)
|
||||
a.NoError(err)
|
||||
a.Len(b, challengeLength)
|
||||
|
||||
updates, has := sb.GetEventChannel(sc)
|
||||
a.True(has)
|
||||
|
||||
go func() {
|
||||
err := sb.SessionFailed(sc, testReason)
|
||||
a.NoError(err)
|
||||
}()
|
||||
|
||||
time.Sleep(time.Second / 4)
|
||||
|
||||
select {
|
||||
case evt := <-updates:
|
||||
a.False(evt.Worked)
|
||||
a.Equal("", evt.Token)
|
||||
a.EqualError(testReason, evt.Reason.Error())
|
||||
default:
|
||||
t.Error("no updates")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package signinwithssb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/crypto/ed25519"
|
||||
|
||||
refs "go.mindeco.de/ssb-refs"
|
||||
)
|
||||
|
||||
// sign-in with ssb uses 256-bit nonces
|
||||
const challengeLength = 32
|
||||
|
||||
// DecodeChallengeString accepts base64 encoded strings and decodes them,
|
||||
// checks their length to be equal to challengeLength,
|
||||
// and returns the decoded bytes
|
||||
func DecodeChallengeString(c string) ([]byte, error) {
|
||||
challengeBytes, err := base64.URLEncoding.DecodeString(c)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid challenge encoding: %w", err)
|
||||
}
|
||||
|
||||
if n := len(challengeBytes); n != challengeLength {
|
||||
return nil, fmt.Errorf("invalid challenge length: expected %d but got %d", challengeLength, n)
|
||||
}
|
||||
|
||||
return challengeBytes, nil
|
||||
}
|
||||
|
||||
// GenerateChallenge returs a base64 encoded string
|
||||
// with challangeLength bytes of random data
|
||||
func GenerateChallenge() string {
|
||||
buf := make([]byte, challengeLength)
|
||||
rand.Read(buf)
|
||||
return base64.URLEncoding.EncodeToString(buf)
|
||||
}
|
||||
|
||||
// ClientPayload is used to create and verify solutions
|
||||
type ClientPayload struct {
|
||||
ClientID, ServerID refs.FeedRef
|
||||
|
||||
ClientChallenge string
|
||||
ServerChallenge string
|
||||
}
|
||||
|
||||
// recreate the signed message
|
||||
func (cr ClientPayload) createMessage() []byte {
|
||||
var msg bytes.Buffer
|
||||
msg.WriteString("=http-auth-sign-in:")
|
||||
msg.WriteString(cr.ServerID.Ref())
|
||||
msg.WriteString(":")
|
||||
msg.WriteString(cr.ClientID.Ref())
|
||||
msg.WriteString(":")
|
||||
msg.WriteString(cr.ServerChallenge)
|
||||
msg.WriteString(":")
|
||||
msg.WriteString(cr.ClientChallenge)
|
||||
return msg.Bytes()
|
||||
}
|
||||
|
||||
// Sign returns the signature created with the passed privateKey
|
||||
func (cr ClientPayload) Sign(privateKey ed25519.PrivateKey) []byte {
|
||||
msg := cr.createMessage()
|
||||
return ed25519.Sign(privateKey, msg)
|
||||
}
|
||||
|
||||
// Validate checks the signature by calling createMessage() and ed25519.Verify()
|
||||
// together with the ClientID public key.
|
||||
func (cr ClientPayload) Validate(signature []byte) bool {
|
||||
msg := cr.createMessage()
|
||||
return ed25519.Verify(cr.ClientID.PubKey(), msg, signature)
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package signinwithssb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
refs "go.mindeco.de/ssb-refs"
|
||||
)
|
||||
|
||||
func TestPayloadString(t *testing.T) {
|
||||
|
||||
server := refs.FeedRef{ID: bytes.Repeat([]byte{1}, 32), Algo: "test"}
|
||||
client := refs.FeedRef{ID: bytes.Repeat([]byte{2}, 32), Algo: "test"}
|
||||
|
||||
var req ClientPayload
|
||||
|
||||
req.ServerID = server
|
||||
req.ClientID = client
|
||||
|
||||
req.ServerChallenge = "fooo"
|
||||
req.ClientChallenge = "barr"
|
||||
|
||||
want := "=http-auth-sign-in:@AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=.test:@AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI=.test:fooo:barr"
|
||||
|
||||
got := req.createMessage()
|
||||
assert.Equal(t, want, string(got))
|
||||
}
|
||||
|
||||
func TestGenerateAndDecode(t *testing.T) {
|
||||
r := require.New(t)
|
||||
|
||||
b, err := DecodeChallengeString(GenerateChallenge())
|
||||
r.NoError(err)
|
||||
r.Len(b, challengeLength)
|
||||
|
||||
b, err = DecodeChallengeString("toshort")
|
||||
r.Error(err)
|
||||
r.Nil(b)
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package gossip
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"go.cryptoscope.co/muxrpc/v2"
|
||||
"go.mindeco.de/encodedTime"
|
||||
)
|
||||
|
||||
// Ping implements the server side of gossip.ping.
|
||||
// it's idea is mentioned here https://github.com/ssbc/ssb-gossip/#ping-duplex
|
||||
// and implemented by https://github.com/dominictarr/pull-ping/
|
||||
//
|
||||
func Ping(ctx context.Context, req *muxrpc.Request, peerSrc *muxrpc.ByteSource, peerSnk *muxrpc.ByteSink) error {
|
||||
type arg struct {
|
||||
Timeout int
|
||||
}
|
||||
|
||||
var args []arg
|
||||
err := json.Unmarshal(req.RawArgs, &args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// var timeout = time.Minute * 5
|
||||
// if len(args) == 1 {
|
||||
// timeout = time.Minute * time.Duration(args[0].Timeout/(60*1000))
|
||||
// }
|
||||
|
||||
go func() {
|
||||
peerSnk.SetEncoding(muxrpc.TypeJSON)
|
||||
enc := json.NewEncoder(peerSnk)
|
||||
|
||||
tick := time.NewTicker(5 * time.Second)
|
||||
defer tick.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-tick.C:
|
||||
}
|
||||
|
||||
var pong = encodedTime.Millisecs(time.Now())
|
||||
err = enc.Encode(pong)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for peerSrc.Next(ctx) {
|
||||
var ping encodedTime.Millisecs
|
||||
err := peerSrc.Reader(func(rd io.Reader) error {
|
||||
return json.NewDecoder(rd).Decode(&ping)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// when := time.Time(ping)
|
||||
// fmt.Printf("got ping: %s - age: %s\n", when.String(), time.Since(when))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// this is how it should work, i think, but it leads to disconnects...
|
||||
// From the code it's hard to see but the client sends a timestamp in milliseconds (Date.now() in javascript/json)
|
||||
// and the other side responds with it's own timestamp.
|
||||
func actualPingPong(ctx context.Context, peerSrc *muxrpc.ByteSource, peerSnk *muxrpc.ByteSink) error {
|
||||
peerSnk.SetEncoding(muxrpc.TypeJSON)
|
||||
enc := json.NewEncoder(peerSnk)
|
||||
|
||||
for peerSrc.Next(ctx) {
|
||||
var ping encodedTime.Millisecs
|
||||
err := peerSrc.Reader(func(rd io.Reader) error {
|
||||
return json.NewDecoder(rd).Decode(&ping)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
when := time.Time(ping)
|
||||
fmt.Printf("got ping: %s - age: %s\n", when.String(), time.Since(when))
|
||||
|
||||
pong := encodedTime.Millisecs(time.Now())
|
||||
err = enc.Encode(pong)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// time.Sleep(timeout)
|
||||
}
|
||||
|
||||
return peerSrc.Err()
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package signinwithssb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
kitlog "github.com/go-kit/kit/log"
|
||||
"go.cryptoscope.co/muxrpc/v2"
|
||||
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/network"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/signinwithssb"
|
||||
validate "github.com/ssb-ngi-pointer/go-ssb-room/internal/signinwithssb"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
|
||||
refs "go.mindeco.de/ssb-refs"
|
||||
)
|
||||
|
||||
// Handler implements the muxrpc methods for the "Sign-in with SSB" calls. SendSolution and InvalidateAllSolutions.
|
||||
type Handler struct {
|
||||
logger kitlog.Logger
|
||||
self refs.FeedRef
|
||||
|
||||
sessions roomdb.AuthWithSSBService
|
||||
members roomdb.MembersService
|
||||
|
||||
bridge *signinwithssb.SignalBridge
|
||||
|
||||
roomDomain string // the http(s) domain of the room to signal redirect addresses
|
||||
}
|
||||
|
||||
// New returns the muxrpc handler for Sign-in with SSB
|
||||
func New(
|
||||
log kitlog.Logger,
|
||||
self refs.FeedRef,
|
||||
roomDomain string,
|
||||
membersdb roomdb.MembersService,
|
||||
sessiondb roomdb.AuthWithSSBService,
|
||||
bridge *signinwithssb.SignalBridge,
|
||||
) Handler {
|
||||
|
||||
var h Handler
|
||||
h.self = self
|
||||
h.roomDomain = roomDomain
|
||||
h.logger = log
|
||||
h.sessions = sessiondb
|
||||
h.members = membersdb
|
||||
h.bridge = bridge
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// SendSolution implements the receiving end of httpAuth.sendSolution.
|
||||
// It recevies three parameters [sc, cc, sol], does the validation and if it passes creates a token
|
||||
// and signals the created token to the SSE HTTP handler using the signal bridge.
|
||||
func (h Handler) SendSolution(ctx context.Context, req *muxrpc.Request) (interface{}, error) {
|
||||
clientID, err := network.GetFeedRefFromAddr(req.RemoteAddr())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
member, err := h.members.GetByFeed(ctx, *clientID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("client is not a room member")
|
||||
}
|
||||
|
||||
var params []string
|
||||
if err := json.Unmarshal(req.RawArgs, ¶ms); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if n := len(params); n != 3 {
|
||||
return nil, fmt.Errorf("expected 3 arguments (sc, cc, sol) but got %d", n)
|
||||
}
|
||||
|
||||
var payload validate.ClientPayload
|
||||
payload.ServerID = h.self
|
||||
payload.ServerChallenge = params[0]
|
||||
payload.ClientID = *clientID
|
||||
payload.ClientChallenge = params[1]
|
||||
|
||||
sig, err := base64.StdEncoding.DecodeString(strings.TrimSuffix(params[2], ".sig.ed25519"))
|
||||
if err != nil {
|
||||
h.bridge.SessionFailed(payload.ServerChallenge, err)
|
||||
return nil, fmt.Errorf("signature is not valid base64 data: %w", err)
|
||||
}
|
||||
|
||||
if !payload.Validate(sig) {
|
||||
err = fmt.Errorf("not a valid solution")
|
||||
h.bridge.SessionFailed(payload.ServerChallenge, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tok, err := h.sessions.CreateToken(ctx, member.ID)
|
||||
if err != nil {
|
||||
h.bridge.SessionFailed(payload.ServerChallenge, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = h.bridge.SessionWorked(payload.ServerChallenge, tok)
|
||||
if err != nil {
|
||||
h.sessions.RemoveToken(ctx, tok)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// InvalidateAllSolutions implements the muxrpc call httpAuth.invalidateAllSolutions
|
||||
func (h Handler) InvalidateAllSolutions(ctx context.Context, req *muxrpc.Request) (interface{}, error) {
|
||||
// get the feed from the muxrpc connection
|
||||
clientID, err := network.GetFeedRefFromAddr(req.RemoteAddr())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// lookup the member
|
||||
member, err := h.members.GetByFeed(ctx, *clientID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// delete all SIWSSB sessions of that member
|
||||
err = h.sessions.WipeTokensForMember(ctx, member.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
|
@ -10,6 +10,8 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.cryptoscope.co/muxrpc/v2"
|
||||
|
@ -106,7 +108,10 @@ func TestAliasRegister(t *testing.T) {
|
|||
r.NoError(err)
|
||||
t.Log("got URL:", resolveURL)
|
||||
a.Equal("srv", resolveURL.Host)
|
||||
a.Equal("/bob", resolveURL.Path)
|
||||
|
||||
wantURL, err := router.CompleteApp().Get(router.CompleteAliasResolve).URL("alias", "bob")
|
||||
r.NoError(err)
|
||||
a.Equal(wantURL.Path, resolveURL.Path)
|
||||
|
||||
// server should have the alias now
|
||||
alias, err := serv.Aliases.Resolve(ctx, "bob")
|
||||
|
|
|
@ -23,6 +23,7 @@ import (
|
|||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/maybemod/testutils"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/network"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/repo"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/signinwithssb"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb/sqlite"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomsrv"
|
||||
)
|
||||
|
@ -88,7 +89,8 @@ func makeNamedTestBot(t testing.TB, name string, opts []roomsrv.Option) (roomdb.
|
|||
t.Log("db close failed: ", err)
|
||||
}
|
||||
})
|
||||
theBot, err := roomsrv.New(db.Members, db.Aliases, name, botOptions...)
|
||||
sb := signinwithssb.NewSignalBridge()
|
||||
theBot, err := roomsrv.New(db.Members, db.Aliases, db.AuthWithSSB, sb, name, botOptions...)
|
||||
r.NoError(err)
|
||||
return db.Members, theBot
|
||||
}
|
||||
|
|
|
@ -28,7 +28,9 @@ import (
|
|||
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/maybemod/testutils"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/network"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/signinwithssb"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb/mockdb"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomsrv"
|
||||
refs "go.mindeco.de/ssb-refs"
|
||||
)
|
||||
|
@ -117,7 +119,11 @@ func (ts *testSession) startGoServer(
|
|||
}),
|
||||
)
|
||||
|
||||
srv, err := roomsrv.New(membersDB, aliasDB, "go.test.room.server", opts...)
|
||||
// not needed for testing yet
|
||||
sb := signinwithssb.NewSignalBridge()
|
||||
authSessionsDB := new(mockdb.FakeAuthWithSSBService)
|
||||
|
||||
srv, err := roomsrv.New(membersDB, aliasDB, authSessionsDB, sb, "go.test.room.server", opts...)
|
||||
r.NoError(err, "failed to init tees a server")
|
||||
ts.t.Logf("go server: %s", srv.Whoami().Ref())
|
||||
ts.t.Cleanup(func() {
|
||||
|
|
|
@ -32,9 +32,24 @@ type AuthFallbackService interface {
|
|||
// Remove(pwid)
|
||||
}
|
||||
|
||||
// needed?! not sure we need to hold the challanges
|
||||
// AuthWithSSBService defines functions needed for the challange/response system of sign-in with ssb
|
||||
type AuthWithSSBService interface{}
|
||||
// AuthWithSSBService defines utility functions for the challenge/response system of sign-in with ssb
|
||||
// They are particualarly of service to check valid sessions (after the client provided a solution for a challenge)
|
||||
// And to log out valid sessions from the clients device.
|
||||
type AuthWithSSBService interface {
|
||||
|
||||
// CreateToken is used to generate a token that is stored inside a cookie.
|
||||
// It is used after a valid solution for a challenge was provided.
|
||||
CreateToken(ctx context.Context, memberID int64) (string, error)
|
||||
|
||||
// CheckToken checks if the passed token is still valid and returns the member id if so
|
||||
CheckToken(ctx context.Context, token string) (int64, error)
|
||||
|
||||
// RemoveToken removes a single token from the database
|
||||
RemoveToken(ctx context.Context, token string) error
|
||||
|
||||
// WipeTokensForMember deletes all tokens currently held for that member
|
||||
WipeTokensForMember(ctx context.Context, memberID int64) error
|
||||
}
|
||||
|
||||
// MembersService stores and retreives the list of internal users (members, mods and admins).
|
||||
type MembersService interface {
|
||||
|
|
|
@ -2,19 +2,334 @@
|
|||
package mockdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
|
||||
)
|
||||
|
||||
type FakeAuthWithSSBService struct {
|
||||
CheckTokenStub func(context.Context, string) (int64, error)
|
||||
checkTokenMutex sync.RWMutex
|
||||
checkTokenArgsForCall []struct {
|
||||
arg1 context.Context
|
||||
arg2 string
|
||||
}
|
||||
checkTokenReturns struct {
|
||||
result1 int64
|
||||
result2 error
|
||||
}
|
||||
checkTokenReturnsOnCall map[int]struct {
|
||||
result1 int64
|
||||
result2 error
|
||||
}
|
||||
CreateTokenStub func(context.Context, int64) (string, error)
|
||||
createTokenMutex sync.RWMutex
|
||||
createTokenArgsForCall []struct {
|
||||
arg1 context.Context
|
||||
arg2 int64
|
||||
}
|
||||
createTokenReturns struct {
|
||||
result1 string
|
||||
result2 error
|
||||
}
|
||||
createTokenReturnsOnCall map[int]struct {
|
||||
result1 string
|
||||
result2 error
|
||||
}
|
||||
RemoveTokenStub func(context.Context, string) error
|
||||
removeTokenMutex sync.RWMutex
|
||||
removeTokenArgsForCall []struct {
|
||||
arg1 context.Context
|
||||
arg2 string
|
||||
}
|
||||
removeTokenReturns struct {
|
||||
result1 error
|
||||
}
|
||||
removeTokenReturnsOnCall map[int]struct {
|
||||
result1 error
|
||||
}
|
||||
WipeTokensForMemberStub func(context.Context, int64) error
|
||||
wipeTokensForMemberMutex sync.RWMutex
|
||||
wipeTokensForMemberArgsForCall []struct {
|
||||
arg1 context.Context
|
||||
arg2 int64
|
||||
}
|
||||
wipeTokensForMemberReturns struct {
|
||||
result1 error
|
||||
}
|
||||
wipeTokensForMemberReturnsOnCall map[int]struct {
|
||||
result1 error
|
||||
}
|
||||
invocations map[string][][]interface{}
|
||||
invocationsMutex sync.RWMutex
|
||||
}
|
||||
|
||||
func (fake *FakeAuthWithSSBService) CheckToken(arg1 context.Context, arg2 string) (int64, error) {
|
||||
fake.checkTokenMutex.Lock()
|
||||
ret, specificReturn := fake.checkTokenReturnsOnCall[len(fake.checkTokenArgsForCall)]
|
||||
fake.checkTokenArgsForCall = append(fake.checkTokenArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
arg2 string
|
||||
}{arg1, arg2})
|
||||
stub := fake.CheckTokenStub
|
||||
fakeReturns := fake.checkTokenReturns
|
||||
fake.recordInvocation("CheckToken", []interface{}{arg1, arg2})
|
||||
fake.checkTokenMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1, ret.result2
|
||||
}
|
||||
return fakeReturns.result1, fakeReturns.result2
|
||||
}
|
||||
|
||||
func (fake *FakeAuthWithSSBService) CheckTokenCallCount() int {
|
||||
fake.checkTokenMutex.RLock()
|
||||
defer fake.checkTokenMutex.RUnlock()
|
||||
return len(fake.checkTokenArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeAuthWithSSBService) CheckTokenCalls(stub func(context.Context, string) (int64, error)) {
|
||||
fake.checkTokenMutex.Lock()
|
||||
defer fake.checkTokenMutex.Unlock()
|
||||
fake.CheckTokenStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeAuthWithSSBService) CheckTokenArgsForCall(i int) (context.Context, string) {
|
||||
fake.checkTokenMutex.RLock()
|
||||
defer fake.checkTokenMutex.RUnlock()
|
||||
argsForCall := fake.checkTokenArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2
|
||||
}
|
||||
|
||||
func (fake *FakeAuthWithSSBService) CheckTokenReturns(result1 int64, result2 error) {
|
||||
fake.checkTokenMutex.Lock()
|
||||
defer fake.checkTokenMutex.Unlock()
|
||||
fake.CheckTokenStub = nil
|
||||
fake.checkTokenReturns = struct {
|
||||
result1 int64
|
||||
result2 error
|
||||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeAuthWithSSBService) CheckTokenReturnsOnCall(i int, result1 int64, result2 error) {
|
||||
fake.checkTokenMutex.Lock()
|
||||
defer fake.checkTokenMutex.Unlock()
|
||||
fake.CheckTokenStub = nil
|
||||
if fake.checkTokenReturnsOnCall == nil {
|
||||
fake.checkTokenReturnsOnCall = make(map[int]struct {
|
||||
result1 int64
|
||||
result2 error
|
||||
})
|
||||
}
|
||||
fake.checkTokenReturnsOnCall[i] = struct {
|
||||
result1 int64
|
||||
result2 error
|
||||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeAuthWithSSBService) CreateToken(arg1 context.Context, arg2 int64) (string, error) {
|
||||
fake.createTokenMutex.Lock()
|
||||
ret, specificReturn := fake.createTokenReturnsOnCall[len(fake.createTokenArgsForCall)]
|
||||
fake.createTokenArgsForCall = append(fake.createTokenArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
arg2 int64
|
||||
}{arg1, arg2})
|
||||
stub := fake.CreateTokenStub
|
||||
fakeReturns := fake.createTokenReturns
|
||||
fake.recordInvocation("CreateToken", []interface{}{arg1, arg2})
|
||||
fake.createTokenMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1, ret.result2
|
||||
}
|
||||
return fakeReturns.result1, fakeReturns.result2
|
||||
}
|
||||
|
||||
func (fake *FakeAuthWithSSBService) CreateTokenCallCount() int {
|
||||
fake.createTokenMutex.RLock()
|
||||
defer fake.createTokenMutex.RUnlock()
|
||||
return len(fake.createTokenArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeAuthWithSSBService) CreateTokenCalls(stub func(context.Context, int64) (string, error)) {
|
||||
fake.createTokenMutex.Lock()
|
||||
defer fake.createTokenMutex.Unlock()
|
||||
fake.CreateTokenStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeAuthWithSSBService) CreateTokenArgsForCall(i int) (context.Context, int64) {
|
||||
fake.createTokenMutex.RLock()
|
||||
defer fake.createTokenMutex.RUnlock()
|
||||
argsForCall := fake.createTokenArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2
|
||||
}
|
||||
|
||||
func (fake *FakeAuthWithSSBService) CreateTokenReturns(result1 string, result2 error) {
|
||||
fake.createTokenMutex.Lock()
|
||||
defer fake.createTokenMutex.Unlock()
|
||||
fake.CreateTokenStub = nil
|
||||
fake.createTokenReturns = struct {
|
||||
result1 string
|
||||
result2 error
|
||||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeAuthWithSSBService) CreateTokenReturnsOnCall(i int, result1 string, result2 error) {
|
||||
fake.createTokenMutex.Lock()
|
||||
defer fake.createTokenMutex.Unlock()
|
||||
fake.CreateTokenStub = nil
|
||||
if fake.createTokenReturnsOnCall == nil {
|
||||
fake.createTokenReturnsOnCall = make(map[int]struct {
|
||||
result1 string
|
||||
result2 error
|
||||
})
|
||||
}
|
||||
fake.createTokenReturnsOnCall[i] = struct {
|
||||
result1 string
|
||||
result2 error
|
||||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeAuthWithSSBService) RemoveToken(arg1 context.Context, arg2 string) error {
|
||||
fake.removeTokenMutex.Lock()
|
||||
ret, specificReturn := fake.removeTokenReturnsOnCall[len(fake.removeTokenArgsForCall)]
|
||||
fake.removeTokenArgsForCall = append(fake.removeTokenArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
arg2 string
|
||||
}{arg1, arg2})
|
||||
stub := fake.RemoveTokenStub
|
||||
fakeReturns := fake.removeTokenReturns
|
||||
fake.recordInvocation("RemoveToken", []interface{}{arg1, arg2})
|
||||
fake.removeTokenMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeAuthWithSSBService) RemoveTokenCallCount() int {
|
||||
fake.removeTokenMutex.RLock()
|
||||
defer fake.removeTokenMutex.RUnlock()
|
||||
return len(fake.removeTokenArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeAuthWithSSBService) RemoveTokenCalls(stub func(context.Context, string) error) {
|
||||
fake.removeTokenMutex.Lock()
|
||||
defer fake.removeTokenMutex.Unlock()
|
||||
fake.RemoveTokenStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeAuthWithSSBService) RemoveTokenArgsForCall(i int) (context.Context, string) {
|
||||
fake.removeTokenMutex.RLock()
|
||||
defer fake.removeTokenMutex.RUnlock()
|
||||
argsForCall := fake.removeTokenArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2
|
||||
}
|
||||
|
||||
func (fake *FakeAuthWithSSBService) RemoveTokenReturns(result1 error) {
|
||||
fake.removeTokenMutex.Lock()
|
||||
defer fake.removeTokenMutex.Unlock()
|
||||
fake.RemoveTokenStub = nil
|
||||
fake.removeTokenReturns = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeAuthWithSSBService) RemoveTokenReturnsOnCall(i int, result1 error) {
|
||||
fake.removeTokenMutex.Lock()
|
||||
defer fake.removeTokenMutex.Unlock()
|
||||
fake.RemoveTokenStub = nil
|
||||
if fake.removeTokenReturnsOnCall == nil {
|
||||
fake.removeTokenReturnsOnCall = make(map[int]struct {
|
||||
result1 error
|
||||
})
|
||||
}
|
||||
fake.removeTokenReturnsOnCall[i] = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeAuthWithSSBService) WipeTokensForMember(arg1 context.Context, arg2 int64) error {
|
||||
fake.wipeTokensForMemberMutex.Lock()
|
||||
ret, specificReturn := fake.wipeTokensForMemberReturnsOnCall[len(fake.wipeTokensForMemberArgsForCall)]
|
||||
fake.wipeTokensForMemberArgsForCall = append(fake.wipeTokensForMemberArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
arg2 int64
|
||||
}{arg1, arg2})
|
||||
stub := fake.WipeTokensForMemberStub
|
||||
fakeReturns := fake.wipeTokensForMemberReturns
|
||||
fake.recordInvocation("WipeTokensForMember", []interface{}{arg1, arg2})
|
||||
fake.wipeTokensForMemberMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeAuthWithSSBService) WipeTokensForMemberCallCount() int {
|
||||
fake.wipeTokensForMemberMutex.RLock()
|
||||
defer fake.wipeTokensForMemberMutex.RUnlock()
|
||||
return len(fake.wipeTokensForMemberArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeAuthWithSSBService) WipeTokensForMemberCalls(stub func(context.Context, int64) error) {
|
||||
fake.wipeTokensForMemberMutex.Lock()
|
||||
defer fake.wipeTokensForMemberMutex.Unlock()
|
||||
fake.WipeTokensForMemberStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeAuthWithSSBService) WipeTokensForMemberArgsForCall(i int) (context.Context, int64) {
|
||||
fake.wipeTokensForMemberMutex.RLock()
|
||||
defer fake.wipeTokensForMemberMutex.RUnlock()
|
||||
argsForCall := fake.wipeTokensForMemberArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2
|
||||
}
|
||||
|
||||
func (fake *FakeAuthWithSSBService) WipeTokensForMemberReturns(result1 error) {
|
||||
fake.wipeTokensForMemberMutex.Lock()
|
||||
defer fake.wipeTokensForMemberMutex.Unlock()
|
||||
fake.WipeTokensForMemberStub = nil
|
||||
fake.wipeTokensForMemberReturns = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeAuthWithSSBService) WipeTokensForMemberReturnsOnCall(i int, result1 error) {
|
||||
fake.wipeTokensForMemberMutex.Lock()
|
||||
defer fake.wipeTokensForMemberMutex.Unlock()
|
||||
fake.WipeTokensForMemberStub = nil
|
||||
if fake.wipeTokensForMemberReturnsOnCall == nil {
|
||||
fake.wipeTokensForMemberReturnsOnCall = make(map[int]struct {
|
||||
result1 error
|
||||
})
|
||||
}
|
||||
fake.wipeTokensForMemberReturnsOnCall[i] = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeAuthWithSSBService) Invocations() map[string][][]interface{} {
|
||||
fake.invocationsMutex.RLock()
|
||||
defer fake.invocationsMutex.RUnlock()
|
||||
fake.checkTokenMutex.RLock()
|
||||
defer fake.checkTokenMutex.RUnlock()
|
||||
fake.createTokenMutex.RLock()
|
||||
defer fake.createTokenMutex.RUnlock()
|
||||
fake.removeTokenMutex.RLock()
|
||||
defer fake.removeTokenMutex.RUnlock()
|
||||
fake.wipeTokensForMemberMutex.RLock()
|
||||
defer fake.wipeTokensForMemberMutex.RUnlock()
|
||||
copiedInvocations := map[string][][]interface{}{}
|
||||
for key, value := range fake.invocations {
|
||||
copiedInvocations[key] = value
|
||||
|
|
|
@ -26,19 +26,6 @@ type FakeMembersService struct {
|
|||
result1 int64
|
||||
result2 error
|
||||
}
|
||||
ChangeRoleStub func(context.Context, int64, roomdb.Role) error
|
||||
changeRoleMutex sync.RWMutex
|
||||
changeRoleArgsForCall []struct {
|
||||
arg1 context.Context
|
||||
arg2 int64
|
||||
arg3 roomdb.Role
|
||||
}
|
||||
changeRoleReturns struct {
|
||||
result1 error
|
||||
}
|
||||
changeRoleReturnsOnCall map[int]struct {
|
||||
result1 error
|
||||
}
|
||||
GetByFeedStub func(context.Context, refs.FeedRef) (roomdb.Member, error)
|
||||
getByFeedMutex sync.RWMutex
|
||||
getByFeedArgsForCall []struct {
|
||||
|
@ -188,69 +175,6 @@ func (fake *FakeMembersService) AddReturnsOnCall(i int, result1 int64, result2 e
|
|||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeMembersService) ChangeRole(arg1 context.Context, arg2 int64, arg3 roomdb.Role) error {
|
||||
fake.changeRoleMutex.Lock()
|
||||
ret, specificReturn := fake.changeRoleReturnsOnCall[len(fake.changeRoleArgsForCall)]
|
||||
fake.changeRoleArgsForCall = append(fake.changeRoleArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
arg2 int64
|
||||
arg3 roomdb.Role
|
||||
}{arg1, arg2, arg3})
|
||||
stub := fake.ChangeRoleStub
|
||||
fakeReturns := fake.changeRoleReturns
|
||||
fake.recordInvocation("ChangeRole", []interface{}{arg1, arg2, arg3})
|
||||
fake.changeRoleMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2, arg3)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeMembersService) ChangeRoleCallCount() int {
|
||||
fake.changeRoleMutex.RLock()
|
||||
defer fake.changeRoleMutex.RUnlock()
|
||||
return len(fake.changeRoleArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeMembersService) ChangeRoleCalls(stub func(context.Context, int64, roomdb.Role) error) {
|
||||
fake.changeRoleMutex.Lock()
|
||||
defer fake.changeRoleMutex.Unlock()
|
||||
fake.ChangeRoleStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeMembersService) ChangeRoleArgsForCall(i int) (context.Context, int64, roomdb.Role) {
|
||||
fake.changeRoleMutex.RLock()
|
||||
defer fake.changeRoleMutex.RUnlock()
|
||||
argsForCall := fake.changeRoleArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
|
||||
}
|
||||
|
||||
func (fake *FakeMembersService) ChangeRoleReturns(result1 error) {
|
||||
fake.changeRoleMutex.Lock()
|
||||
defer fake.changeRoleMutex.Unlock()
|
||||
fake.ChangeRoleStub = nil
|
||||
fake.changeRoleReturns = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeMembersService) ChangeRoleReturnsOnCall(i int, result1 error) {
|
||||
fake.changeRoleMutex.Lock()
|
||||
defer fake.changeRoleMutex.Unlock()
|
||||
fake.ChangeRoleStub = nil
|
||||
if fake.changeRoleReturnsOnCall == nil {
|
||||
fake.changeRoleReturnsOnCall = make(map[int]struct {
|
||||
result1 error
|
||||
})
|
||||
}
|
||||
fake.changeRoleReturnsOnCall[i] = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeMembersService) GetByFeed(arg1 context.Context, arg2 refs.FeedRef) (roomdb.Member, error) {
|
||||
fake.getByFeedMutex.Lock()
|
||||
ret, specificReturn := fake.getByFeedReturnsOnCall[len(fake.getByFeedArgsForCall)]
|
||||
|
@ -637,8 +561,6 @@ func (fake *FakeMembersService) Invocations() map[string][][]interface{} {
|
|||
defer fake.invocationsMutex.RUnlock()
|
||||
fake.addMutex.RLock()
|
||||
defer fake.addMutex.RUnlock()
|
||||
fake.changeRoleMutex.RLock()
|
||||
defer fake.changeRoleMutex.RUnlock()
|
||||
fake.getByFeedMutex.RLock()
|
||||
defer fake.getByFeedMutex.RUnlock()
|
||||
fake.getByIDMutex.RLock()
|
||||
|
|
|
@ -3,9 +3,18 @@
|
|||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"github.com/friendsofgo/errors"
|
||||
"github.com/mattn/go-sqlite3"
|
||||
"github.com/volatiletech/sqlboiler/v4/boil"
|
||||
"github.com/volatiletech/sqlboiler/v4/queries/qm"
|
||||
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/randutil"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb/sqlite/models"
|
||||
)
|
||||
|
||||
// compiler assertion to ensure the struct fullfills the interface
|
||||
|
@ -14,3 +23,113 @@ var _ roomdb.AuthWithSSBService = (*AuthWithSSB)(nil)
|
|||
type AuthWithSSB struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
const siwssbTokenLength = 32
|
||||
|
||||
// CreateToken is used to generate a token that is stored inside a cookie.
|
||||
// It is used after a valid solution for a challenge was provided.
|
||||
func (a AuthWithSSB) CreateToken(ctx context.Context, memberID int64) (string, error) {
|
||||
|
||||
var newToken = models.SIWSSBSession{
|
||||
MemberID: memberID,
|
||||
}
|
||||
|
||||
err := transact(a.db, func(tx *sql.Tx) error {
|
||||
|
||||
// check the member is registerd
|
||||
if _, err := models.FindMember(ctx, tx, newToken.MemberID); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return roomdb.ErrNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
inserted := false
|
||||
trying: // keep trying until we inserted in unused token
|
||||
for tries := 100; tries > 0; tries-- {
|
||||
|
||||
// generate an new token
|
||||
newToken.Token = randutil.String(siwssbTokenLength)
|
||||
|
||||
// insert the new token
|
||||
cols := boil.Whitelist(models.SIWSSBSessionColumns.Token, models.SIWSSBSessionColumns.MemberID)
|
||||
err := newToken.Insert(ctx, tx, cols)
|
||||
if err != nil {
|
||||
var sqlErr sqlite3.Error
|
||||
if errors.As(err, &sqlErr) && sqlErr.ExtendedCode == sqlite3.ErrConstraintUnique {
|
||||
// generated an existing token, retry
|
||||
continue trying
|
||||
}
|
||||
return err
|
||||
}
|
||||
inserted = true
|
||||
break // no error means it worked!
|
||||
}
|
||||
|
||||
if !inserted {
|
||||
return errors.New("admindb: failed to generate a fresh token in a reasonable amount of time")
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return newToken.Token, nil
|
||||
}
|
||||
|
||||
const sessionTimeout = time.Hour * 24
|
||||
|
||||
// CheckToken checks if the passed token is still valid and returns the member id if so
|
||||
func (a AuthWithSSB) CheckToken(ctx context.Context, token string) (int64, error) {
|
||||
var memberID int64
|
||||
|
||||
err := transact(a.db, func(tx *sql.Tx) error {
|
||||
session, err := models.SIWSSBSessions(qm.Where("token = ?", token)).One(ctx, a.db)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return roomdb.ErrNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if time.Since(session.CreatedAt) > sessionTimeout {
|
||||
_, err = session.Delete(ctx, tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return errors.New("sign-in with ssb: session expired")
|
||||
}
|
||||
|
||||
memberID = session.MemberID
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
return memberID, nil
|
||||
}
|
||||
|
||||
// RemoveToken removes a single token from the database
|
||||
func (a AuthWithSSB) RemoveToken(ctx context.Context, token string) error {
|
||||
_, err := models.SIWSSBSessions(qm.Where("token = ?", token)).DeleteAll(ctx, a.db)
|
||||
return err
|
||||
}
|
||||
|
||||
// WipeTokensForMember deletes all tokens currently held for that member
|
||||
func (a AuthWithSSB) WipeTokensForMember(ctx context.Context, memberID int64) error {
|
||||
return transact(a.db, func(tx *sql.Tx) error {
|
||||
_, err := models.SIWSSBSessions(qm.Where("member_id = ?", memberID)).DeleteAll(ctx, tx)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return roomdb.ErrNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
|
|
@ -33,14 +33,14 @@ type Invites struct {
|
|||
// Create creates a new invite for a new member. It returns the token or an error.
|
||||
// createdBy is user ID of the admin or moderator who created it.
|
||||
// aliasSuggestion is optional (empty string is fine) but can be used to disambiguate open invites. (See https://github.com/ssb-ngi-pointer/rooms2/issues/21)
|
||||
// The returned token is base64 URL encoded and has tokenLength when decoded.
|
||||
// The returned token is base64 URL encoded and has inviteTokenLength when decoded.
|
||||
func (i Invites) Create(ctx context.Context, createdBy int64, aliasSuggestion string) (string, error) {
|
||||
var newInvite = models.Invite{
|
||||
CreatedBy: createdBy,
|
||||
AliasSuggestion: aliasSuggestion,
|
||||
}
|
||||
|
||||
tokenBytes := make([]byte, tokenLength)
|
||||
tokenBytes := make([]byte, inviteTokenLength)
|
||||
|
||||
err := transact(i.db, func(tx *sql.Tx) error {
|
||||
|
||||
|
@ -88,7 +88,7 @@ func (i Invites) Create(ctx context.Context, createdBy int64, aliasSuggestion st
|
|||
|
||||
// Consume checks if the passed token is still valid. If it is it adds newMember to the members of the room and invalidates the token.
|
||||
// If the token isn't valid, it returns an error.
|
||||
// Tokens need to be base64 URL encoded and when decoded be of tokenLength.
|
||||
// Tokens need to be base64 URL encoded and when decoded be of inviteTokenLength.
|
||||
func (i Invites) Consume(ctx context.Context, token string, newMember refs.FeedRef) (roomdb.Invite, error) {
|
||||
var inv roomdb.Invite
|
||||
|
||||
|
@ -263,7 +263,7 @@ func (i Invites) Revoke(ctx context.Context, id int64) error {
|
|||
})
|
||||
}
|
||||
|
||||
const tokenLength = 50
|
||||
const inviteTokenLength = 50
|
||||
|
||||
func getHashedToken(b64tok string) (string, error) {
|
||||
tokenBytes, err := base64.URLEncoding.DecodeString(b64tok)
|
||||
|
@ -271,7 +271,7 @@ func getHashedToken(b64tok string) (string, error) {
|
|||
return "", err
|
||||
}
|
||||
|
||||
if n := len(tokenBytes); n != tokenLength {
|
||||
if n := len(tokenBytes); n != inviteTokenLength {
|
||||
return "", fmt.Errorf("admindb: invalid invite token length (only got %d bytes)", n)
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ CREATE TABLE fallback_passwords (
|
|||
|
||||
member_id INTEGER NOT NULL,
|
||||
|
||||
FOREIGN KEY ( member_id ) REFERENCES members( "id" )
|
||||
FOREIGN KEY ( member_id ) REFERENCES members( "id" ) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX fallback_passwords_by_login ON fallback_passwords(login);
|
||||
|
||||
|
@ -32,7 +32,7 @@ CREATE TABLE invites (
|
|||
alias_suggestion TEXT NOT NULL DEFAULT "", -- optional
|
||||
active boolean NOT NULL DEFAULT TRUE,
|
||||
|
||||
FOREIGN KEY ( created_by ) REFERENCES members( "id" )
|
||||
FOREIGN KEY ( created_by ) REFERENCES members( "id" ) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX invite_active_ids ON invites(id) WHERE active=TRUE;
|
||||
CREATE UNIQUE INDEX invite_active_tokens ON invites(hashed_token) WHERE active=TRUE;
|
||||
|
@ -45,7 +45,7 @@ CREATE TABLE aliases (
|
|||
member_id INTEGER NOT NULL,
|
||||
signature BLOB NOT NULL,
|
||||
|
||||
FOREIGN KEY ( member_id ) REFERENCES members( "id" )
|
||||
FOREIGN KEY ( member_id ) REFERENCES members( "id" ) ON DELETE CASCADE
|
||||
);
|
||||
CREATE UNIQUE INDEX aliases_ids ON aliases(id);
|
||||
CREATE UNIQUE INDEX aliases_names ON aliases(name);
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
-- +migrate Up
|
||||
-- SIWSSB stands for sign-in with ssb
|
||||
CREATE TABLE SIWSSB_sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
token TEXT UNIQUE NOT NULL,
|
||||
member_id INTEGER NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
FOREIGN KEY ( member_id ) REFERENCES members( "id" ) ON DELETE CASCADE
|
||||
);
|
||||
CREATE UNIQUE INDEX SIWSSB_by_token ON SIWSSB_sessions(token);
|
||||
CREATE INDEX SIWSSB_by_member ON SIWSSB_sessions(member_id);
|
||||
|
||||
-- +migrate Down
|
||||
DROP TABLE SIWSSB_sessions;
|
||||
DROP INDEX SIWSSB_by_token;
|
||||
DROP INDEX SIWSSB_by_member;
|
File diff suppressed because it is too large
Load Diff
|
@ -45,52 +45,6 @@ var AliasColumns = struct {
|
|||
|
||||
// Generated where
|
||||
|
||||
type whereHelperint64 struct{ field string }
|
||||
|
||||
func (w whereHelperint64) EQ(x int64) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.EQ, x) }
|
||||
func (w whereHelperint64) NEQ(x int64) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.NEQ, x) }
|
||||
func (w whereHelperint64) LT(x int64) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.LT, x) }
|
||||
func (w whereHelperint64) LTE(x int64) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.LTE, x) }
|
||||
func (w whereHelperint64) GT(x int64) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.GT, x) }
|
||||
func (w whereHelperint64) GTE(x int64) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.GTE, x) }
|
||||
func (w whereHelperint64) IN(slice []int64) qm.QueryMod {
|
||||
values := make([]interface{}, 0, len(slice))
|
||||
for _, value := range slice {
|
||||
values = append(values, value)
|
||||
}
|
||||
return qm.WhereIn(fmt.Sprintf("%s IN ?", w.field), values...)
|
||||
}
|
||||
func (w whereHelperint64) NIN(slice []int64) qm.QueryMod {
|
||||
values := make([]interface{}, 0, len(slice))
|
||||
for _, value := range slice {
|
||||
values = append(values, value)
|
||||
}
|
||||
return qm.WhereNotIn(fmt.Sprintf("%s NOT IN ?", w.field), values...)
|
||||
}
|
||||
|
||||
type whereHelperstring struct{ field string }
|
||||
|
||||
func (w whereHelperstring) EQ(x string) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.EQ, x) }
|
||||
func (w whereHelperstring) NEQ(x string) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.NEQ, x) }
|
||||
func (w whereHelperstring) LT(x string) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.LT, x) }
|
||||
func (w whereHelperstring) LTE(x string) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.LTE, x) }
|
||||
func (w whereHelperstring) GT(x string) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.GT, x) }
|
||||
func (w whereHelperstring) GTE(x string) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.GTE, x) }
|
||||
func (w whereHelperstring) IN(slice []string) qm.QueryMod {
|
||||
values := make([]interface{}, 0, len(slice))
|
||||
for _, value := range slice {
|
||||
values = append(values, value)
|
||||
}
|
||||
return qm.WhereIn(fmt.Sprintf("%s IN ?", w.field), values...)
|
||||
}
|
||||
func (w whereHelperstring) NIN(slice []string) qm.QueryMod {
|
||||
values := make([]interface{}, 0, len(slice))
|
||||
for _, value := range slice {
|
||||
values = append(values, value)
|
||||
}
|
||||
return qm.WhereNotIn(fmt.Sprintf("%s NOT IN ?", w.field), values...)
|
||||
}
|
||||
|
||||
type whereHelper__byte struct{ field string }
|
||||
|
||||
func (w whereHelper__byte) EQ(x []byte) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.EQ, x) }
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
package models
|
||||
|
||||
var TableNames = struct {
|
||||
SIWSSBSessions string
|
||||
Aliases string
|
||||
DeniedKeys string
|
||||
FallbackPasswords string
|
||||
|
@ -13,6 +14,7 @@ var TableNames = struct {
|
|||
PinNotices string
|
||||
Pins string
|
||||
}{
|
||||
SIWSSBSessions: "SIWSSB_sessions",
|
||||
Aliases: "aliases",
|
||||
DeniedKeys: "denied_keys",
|
||||
FallbackPasswords: "fallback_passwords",
|
||||
|
|
|
@ -67,27 +67,6 @@ func (w whereHelperroomdb_DBFeedRef) GTE(x roomdb.DBFeedRef) qm.QueryMod {
|
|||
return qmhelper.Where(w.field, qmhelper.GTE, x)
|
||||
}
|
||||
|
||||
type whereHelpertime_Time struct{ field string }
|
||||
|
||||
func (w whereHelpertime_Time) EQ(x time.Time) qm.QueryMod {
|
||||
return qmhelper.Where(w.field, qmhelper.EQ, x)
|
||||
}
|
||||
func (w whereHelpertime_Time) NEQ(x time.Time) qm.QueryMod {
|
||||
return qmhelper.Where(w.field, qmhelper.NEQ, x)
|
||||
}
|
||||
func (w whereHelpertime_Time) LT(x time.Time) qm.QueryMod {
|
||||
return qmhelper.Where(w.field, qmhelper.LT, x)
|
||||
}
|
||||
func (w whereHelpertime_Time) LTE(x time.Time) qm.QueryMod {
|
||||
return qmhelper.Where(w.field, qmhelper.LTE, x)
|
||||
}
|
||||
func (w whereHelpertime_Time) GT(x time.Time) qm.QueryMod {
|
||||
return qmhelper.Where(w.field, qmhelper.GT, x)
|
||||
}
|
||||
func (w whereHelpertime_Time) GTE(x time.Time) qm.QueryMod {
|
||||
return qmhelper.Where(w.field, qmhelper.GTE, x)
|
||||
}
|
||||
|
||||
var DeniedKeyWhere = struct {
|
||||
ID whereHelperint64
|
||||
PubKey whereHelperroomdb_DBFeedRef
|
||||
|
|
|
@ -60,10 +60,12 @@ var MemberWhere = struct {
|
|||
|
||||
// MemberRels is where relationship names are stored.
|
||||
var MemberRels = struct {
|
||||
SIWSSBSessions string
|
||||
Aliases string
|
||||
FallbackPasswords string
|
||||
CreatedByInvites string
|
||||
}{
|
||||
SIWSSBSessions: "SIWSSBSessions",
|
||||
Aliases: "Aliases",
|
||||
FallbackPasswords: "FallbackPasswords",
|
||||
CreatedByInvites: "CreatedByInvites",
|
||||
|
@ -71,6 +73,7 @@ var MemberRels = struct {
|
|||
|
||||
// memberR is where relationships are stored.
|
||||
type memberR struct {
|
||||
SIWSSBSessions SIWSSBSessionSlice `boil:"SIWSSBSessions" json:"SIWSSBSessions" toml:"SIWSSBSessions" yaml:"SIWSSBSessions"`
|
||||
Aliases AliasSlice `boil:"Aliases" json:"Aliases" toml:"Aliases" yaml:"Aliases"`
|
||||
FallbackPasswords FallbackPasswordSlice `boil:"FallbackPasswords" json:"FallbackPasswords" toml:"FallbackPasswords" yaml:"FallbackPasswords"`
|
||||
CreatedByInvites InviteSlice `boil:"CreatedByInvites" json:"CreatedByInvites" toml:"CreatedByInvites" yaml:"CreatedByInvites"`
|
||||
|
@ -366,6 +369,27 @@ func (q memberQuery) Exists(ctx context.Context, exec boil.ContextExecutor) (boo
|
|||
return count > 0, nil
|
||||
}
|
||||
|
||||
// SIWSSBSessions retrieves all the SIWSSB_session's SIWSSBSessions with an executor.
|
||||
func (o *Member) SIWSSBSessions(mods ...qm.QueryMod) sIWSSBSessionQuery {
|
||||
var queryMods []qm.QueryMod
|
||||
if len(mods) != 0 {
|
||||
queryMods = append(queryMods, mods...)
|
||||
}
|
||||
|
||||
queryMods = append(queryMods,
|
||||
qm.Where("\"SIWSSB_sessions\".\"member_id\"=?", o.ID),
|
||||
)
|
||||
|
||||
query := SIWSSBSessions(queryMods...)
|
||||
queries.SetFrom(query.Query, "\"SIWSSB_sessions\"")
|
||||
|
||||
if len(queries.GetSelect(query.Query)) == 0 {
|
||||
queries.SetSelect(query.Query, []string{"\"SIWSSB_sessions\".*"})
|
||||
}
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
// Aliases retrieves all the alias's Aliases with an executor.
|
||||
func (o *Member) Aliases(mods ...qm.QueryMod) aliasQuery {
|
||||
var queryMods []qm.QueryMod
|
||||
|
@ -429,6 +453,104 @@ func (o *Member) CreatedByInvites(mods ...qm.QueryMod) inviteQuery {
|
|||
return query
|
||||
}
|
||||
|
||||
// LoadSIWSSBSessions allows an eager lookup of values, cached into the
|
||||
// loaded structs of the objects. This is for a 1-M or N-M relationship.
|
||||
func (memberL) LoadSIWSSBSessions(ctx context.Context, e boil.ContextExecutor, singular bool, maybeMember interface{}, mods queries.Applicator) error {
|
||||
var slice []*Member
|
||||
var object *Member
|
||||
|
||||
if singular {
|
||||
object = maybeMember.(*Member)
|
||||
} else {
|
||||
slice = *maybeMember.(*[]*Member)
|
||||
}
|
||||
|
||||
args := make([]interface{}, 0, 1)
|
||||
if singular {
|
||||
if object.R == nil {
|
||||
object.R = &memberR{}
|
||||
}
|
||||
args = append(args, object.ID)
|
||||
} else {
|
||||
Outer:
|
||||
for _, obj := range slice {
|
||||
if obj.R == nil {
|
||||
obj.R = &memberR{}
|
||||
}
|
||||
|
||||
for _, a := range args {
|
||||
if a == obj.ID {
|
||||
continue Outer
|
||||
}
|
||||
}
|
||||
|
||||
args = append(args, obj.ID)
|
||||
}
|
||||
}
|
||||
|
||||
if len(args) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
query := NewQuery(
|
||||
qm.From(`SIWSSB_sessions`),
|
||||
qm.WhereIn(`SIWSSB_sessions.member_id in ?`, args...),
|
||||
)
|
||||
if mods != nil {
|
||||
mods.Apply(query)
|
||||
}
|
||||
|
||||
results, err := query.QueryContext(ctx, e)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to eager load SIWSSB_sessions")
|
||||
}
|
||||
|
||||
var resultSlice []*SIWSSBSession
|
||||
if err = queries.Bind(results, &resultSlice); err != nil {
|
||||
return errors.Wrap(err, "failed to bind eager loaded slice SIWSSB_sessions")
|
||||
}
|
||||
|
||||
if err = results.Close(); err != nil {
|
||||
return errors.Wrap(err, "failed to close results in eager load on SIWSSB_sessions")
|
||||
}
|
||||
if err = results.Err(); err != nil {
|
||||
return errors.Wrap(err, "error occurred during iteration of eager loaded relations for SIWSSB_sessions")
|
||||
}
|
||||
|
||||
if len(sIWSSBSessionAfterSelectHooks) != 0 {
|
||||
for _, obj := range resultSlice {
|
||||
if err := obj.doAfterSelectHooks(ctx, e); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if singular {
|
||||
object.R.SIWSSBSessions = resultSlice
|
||||
for _, foreign := range resultSlice {
|
||||
if foreign.R == nil {
|
||||
foreign.R = &sIWSSBSessionR{}
|
||||
}
|
||||
foreign.R.Member = object
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, foreign := range resultSlice {
|
||||
for _, local := range slice {
|
||||
if local.ID == foreign.MemberID {
|
||||
local.R.SIWSSBSessions = append(local.R.SIWSSBSessions, foreign)
|
||||
if foreign.R == nil {
|
||||
foreign.R = &sIWSSBSessionR{}
|
||||
}
|
||||
foreign.R.Member = local
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadAliases allows an eager lookup of values, cached into the
|
||||
// loaded structs of the objects. This is for a 1-M or N-M relationship.
|
||||
func (memberL) LoadAliases(ctx context.Context, e boil.ContextExecutor, singular bool, maybeMember interface{}, mods queries.Applicator) error {
|
||||
|
@ -723,6 +845,59 @@ func (memberL) LoadCreatedByInvites(ctx context.Context, e boil.ContextExecutor,
|
|||
return nil
|
||||
}
|
||||
|
||||
// AddSIWSSBSessions adds the given related objects to the existing relationships
|
||||
// of the member, optionally inserting them as new records.
|
||||
// Appends related to o.R.SIWSSBSessions.
|
||||
// Sets related.R.Member appropriately.
|
||||
func (o *Member) AddSIWSSBSessions(ctx context.Context, exec boil.ContextExecutor, insert bool, related ...*SIWSSBSession) error {
|
||||
var err error
|
||||
for _, rel := range related {
|
||||
if insert {
|
||||
rel.MemberID = o.ID
|
||||
if err = rel.Insert(ctx, exec, boil.Infer()); err != nil {
|
||||
return errors.Wrap(err, "failed to insert into foreign table")
|
||||
}
|
||||
} else {
|
||||
updateQuery := fmt.Sprintf(
|
||||
"UPDATE \"SIWSSB_sessions\" SET %s WHERE %s",
|
||||
strmangle.SetParamNames("\"", "\"", 0, []string{"member_id"}),
|
||||
strmangle.WhereClause("\"", "\"", 0, sIWSSBSessionPrimaryKeyColumns),
|
||||
)
|
||||
values := []interface{}{o.ID, rel.ID}
|
||||
|
||||
if boil.IsDebug(ctx) {
|
||||
writer := boil.DebugWriterFrom(ctx)
|
||||
fmt.Fprintln(writer, updateQuery)
|
||||
fmt.Fprintln(writer, values)
|
||||
}
|
||||
if _, err = exec.ExecContext(ctx, updateQuery, values...); err != nil {
|
||||
return errors.Wrap(err, "failed to update foreign table")
|
||||
}
|
||||
|
||||
rel.MemberID = o.ID
|
||||
}
|
||||
}
|
||||
|
||||
if o.R == nil {
|
||||
o.R = &memberR{
|
||||
SIWSSBSessions: related,
|
||||
}
|
||||
} else {
|
||||
o.R.SIWSSBSessions = append(o.R.SIWSSBSessions, related...)
|
||||
}
|
||||
|
||||
for _, rel := range related {
|
||||
if rel.R == nil {
|
||||
rel.R = &sIWSSBSessionR{
|
||||
Member: o,
|
||||
}
|
||||
} else {
|
||||
rel.R.Member = o
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddAliases adds the given related objects to the existing relationships
|
||||
// of the member, optionally inserting them as new records.
|
||||
// Appends related to o.R.Aliases.
|
||||
|
|
|
@ -27,13 +27,13 @@ import (
|
|||
migrate "github.com/rubenv/sql-migrate"
|
||||
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/repo"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
|
||||
)
|
||||
|
||||
type Database struct {
|
||||
db *sql.DB
|
||||
|
||||
AuthFallback roomdb.AuthFallbackService
|
||||
AuthFallback AuthFallback
|
||||
AuthWithSSB AuthWithSSB
|
||||
|
||||
Members Members
|
||||
Aliases Aliases
|
||||
|
@ -102,6 +102,7 @@ func Open(r repo.Interface) (*Database, error) {
|
|||
|
||||
Aliases: Aliases{db},
|
||||
AuthFallback: AuthFallback{db},
|
||||
AuthWithSSB: AuthWithSSB{db},
|
||||
DeniedKeys: DeniedKeys{db},
|
||||
Invites: Invites{db: db, members: ml},
|
||||
Notices: Notices{db},
|
||||
|
|
|
@ -8,9 +8,10 @@ import (
|
|||
"testing"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/repo"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
|
||||
"github.com/stretchr/testify/require"
|
||||
refs "go.mindeco.de/ssb-refs"
|
||||
)
|
||||
|
||||
|
|
|
@ -4,17 +4,18 @@ package roomsrv
|
|||
|
||||
import (
|
||||
kitlog "github.com/go-kit/kit/log"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
|
||||
muxrpc "go.cryptoscope.co/muxrpc/v2"
|
||||
"go.cryptoscope.co/muxrpc/v2/typemux"
|
||||
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/muxrpc/handlers/alias"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/muxrpc/handlers/gossip"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/muxrpc/handlers/signinwithssb"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/muxrpc/handlers/tunnel/server"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/muxrpc/handlers/whoami"
|
||||
)
|
||||
|
||||
// instantiate and register the muxrpc handlers
|
||||
func (s *Server) initHandlers(aliasDB roomdb.AliasesService) {
|
||||
func (s *Server) initHandlers() {
|
||||
// inistaniate handler packages
|
||||
whoami := whoami.New(s.Whoami())
|
||||
|
||||
|
@ -27,10 +28,19 @@ func (s *Server) initHandlers(aliasDB roomdb.AliasesService) {
|
|||
aliasHandler := alias.New(
|
||||
kitlog.With(s.logger, "unit", "aliases"),
|
||||
s.Whoami(),
|
||||
aliasDB,
|
||||
s.Aliases,
|
||||
s.domain,
|
||||
)
|
||||
|
||||
siwssbHandler := signinwithssb.New(
|
||||
kitlog.With(s.logger, "unit", "auth-with-ssb"),
|
||||
s.Whoami(),
|
||||
s.domain,
|
||||
s.Members,
|
||||
s.authWithSSB,
|
||||
s.authWithSSBBridge,
|
||||
)
|
||||
|
||||
// register muxrpc commands
|
||||
registries := []typemux.HandlerMux{s.public, s.master}
|
||||
|
||||
|
@ -47,5 +57,12 @@ func (s *Server) initHandlers(aliasDB roomdb.AliasesService) {
|
|||
|
||||
mux.RegisterAsync(append(method, "registerAlias"), typemux.AsyncFunc(aliasHandler.Register))
|
||||
mux.RegisterAsync(append(method, "revokeAlias"), typemux.AsyncFunc(aliasHandler.Revoke))
|
||||
|
||||
method = muxrpc.Method{"httpAuth"}
|
||||
mux.RegisterAsync(append(method, "invalidateAllSolutions"), typemux.AsyncFunc(siwssbHandler.InvalidateAllSolutions))
|
||||
mux.RegisterAsync(append(method, "sendSolution"), typemux.AsyncFunc(siwssbHandler.SendSolution))
|
||||
|
||||
method = muxrpc.Method{"gossip"}
|
||||
mux.RegisterDuplex(append(method, "ping"), typemux.DuplexFunc(gossip.Ping))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ import (
|
|||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/maybemod/multicloser"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/network"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/repo"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/signinwithssb"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomstate"
|
||||
refs "go.mindeco.de/ssb-refs"
|
||||
|
@ -66,6 +67,9 @@ type Server struct {
|
|||
|
||||
Members roomdb.MembersService
|
||||
Aliases roomdb.AliasesService
|
||||
|
||||
authWithSSB roomdb.AuthWithSSBService
|
||||
authWithSSBBridge *signinwithssb.SignalBridge
|
||||
}
|
||||
|
||||
func (s Server) Whoami() refs.FeedRef {
|
||||
|
@ -75,6 +79,8 @@ func (s Server) Whoami() refs.FeedRef {
|
|||
func New(
|
||||
membersdb roomdb.MembersService,
|
||||
aliasdb roomdb.AliasesService,
|
||||
awsdb roomdb.AuthWithSSBService,
|
||||
bridge *signinwithssb.SignalBridge,
|
||||
domainName string,
|
||||
opts ...Option,
|
||||
) (*Server, error) {
|
||||
|
@ -84,6 +90,9 @@ func New(
|
|||
s.Members = membersdb
|
||||
s.Aliases = aliasdb
|
||||
|
||||
s.authWithSSB = awsdb
|
||||
s.authWithSSBBridge = bridge
|
||||
|
||||
s.domain = domainName
|
||||
|
||||
for i, opt := range opts {
|
||||
|
@ -143,7 +152,7 @@ func New(
|
|||
|
||||
s.StateManager = roomstate.NewManager(s.rootCtx, s.logger)
|
||||
|
||||
s.initHandlers(aliasdb)
|
||||
s.initHandlers()
|
||||
|
||||
if err := s.initNetwork(); err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
const ssbUriLink = document.querySelector('#start-auth-uri');
|
||||
const waitingElem = document.querySelector('#waiting');
|
||||
const errorElem = document.querySelector('#failed');
|
||||
const challengeElem = document.querySelector('#challenge');
|
||||
|
||||
const sc = challengeElem.dataset.sc;
|
||||
const evtSource = new EventSource(`/withssb/events?sc=${sc}`);
|
||||
|
||||
ssbUriLink.addEventListener('click', (e) => {
|
||||
errorElem.classList.add('hidden');
|
||||
waitingElem.classList.remove('hidden');
|
||||
});
|
||||
|
||||
evtSource.onerror = (e) => {
|
||||
waitingElem.classList.add('hidden');
|
||||
errorElem.classList.remove('hidden');
|
||||
console.error(e.data);
|
||||
};
|
||||
|
||||
evtSource.addEventListener('failed', (e) => {
|
||||
waitingElem.classList.add('hidden');
|
||||
errorElem.classList.remove('hidden');
|
||||
console.error(e.data);
|
||||
});
|
||||
|
||||
evtSource.addEventListener('success', (e) => {
|
||||
waitingElem.classList.add('hidden');
|
||||
evtSource.close();
|
||||
window.location = `/withssb/finalize?token=${e.data}`;
|
||||
});
|
|
@ -4,6 +4,7 @@
|
|||
package errors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
|
@ -23,3 +24,13 @@ type ErrBadRequest struct {
|
|||
func (br ErrBadRequest) Error() string {
|
||||
return fmt.Sprintf("rooms/web: bad request error: %s", br.Details)
|
||||
}
|
||||
|
||||
type ErrForbidden struct {
|
||||
Details error
|
||||
}
|
||||
|
||||
func (f ErrForbidden) Error() string {
|
||||
return fmt.Sprintf("rooms/web: access denied: %s", f.Details)
|
||||
}
|
||||
|
||||
var ErrNotAuthorized = errors.New("rooms/web: not authorized")
|
||||
|
|
|
@ -4,7 +4,6 @@ package admin
|
|||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
@ -16,6 +15,7 @@ import (
|
|||
"go.mindeco.de/http/tester"
|
||||
"go.mindeco.de/logging/logtest"
|
||||
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/randutil"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb/mockdb"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomstate"
|
||||
|
@ -60,7 +60,7 @@ func newSession(t *testing.T) *testSession {
|
|||
|
||||
ts.Router = router.CompleteApp()
|
||||
|
||||
ts.Domain = randomString(10)
|
||||
ts.Domain = randutil.String(10)
|
||||
|
||||
// fake user
|
||||
ts.User = roomdb.Member{
|
||||
|
@ -120,15 +120,3 @@ func newSession(t *testing.T) *testSession {
|
|||
|
||||
return &ts
|
||||
}
|
||||
|
||||
// utils
|
||||
|
||||
func randomString(n int) string {
|
||||
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
|
||||
|
||||
s := make([]rune, n)
|
||||
for i := range s {
|
||||
s[i] = letters[rand.Intn(len(letters))]
|
||||
}
|
||||
return string(s)
|
||||
}
|
||||
|
|
|
@ -4,8 +4,10 @@ import (
|
|||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"go.mindeco.de/http/render"
|
||||
|
@ -46,7 +48,7 @@ func (a aliasHandler) resolve(rw http.ResponseWriter, req *http.Request) {
|
|||
|
||||
alias, err := a.db.Resolve(req.Context(), name)
|
||||
if err != nil {
|
||||
ar.SendError(err)
|
||||
ar.SendError(fmt.Errorf("aliases: failed to resolve name %q: %w", name, err))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -139,11 +141,29 @@ func (html *aliasHTMLResponder) UpdateRoomInfo(hostAndPort string, roomID refs.F
|
|||
}
|
||||
|
||||
func (html aliasHTMLResponder) SendConfirmation(alias roomdb.Alias) {
|
||||
|
||||
// construct the ssb:experimental?action=consume-alias&... uri for linking into apps
|
||||
queryParams := url.Values{}
|
||||
queryParams.Set("action", "consume-alias")
|
||||
queryParams.Set("roomId", html.roomID.Ref())
|
||||
queryParams.Set("alias", alias.Name)
|
||||
queryParams.Set("userId", alias.Feed.Ref())
|
||||
queryParams.Set("signature", base64.URLEncoding.EncodeToString(alias.Signature))
|
||||
queryParams.Set("multiserverAddress", html.multiservAddr)
|
||||
|
||||
// html.multiservAddr
|
||||
ssbURI := url.URL{
|
||||
Scheme: "ssb",
|
||||
Opaque: "experimental",
|
||||
|
||||
RawQuery: queryParams.Encode(),
|
||||
}
|
||||
|
||||
err := html.renderer.Render(html.rw, html.req, "aliases-resolved.html", http.StatusOK, struct {
|
||||
Alias roomdb.Alias
|
||||
|
||||
RoomAddr string
|
||||
}{alias, html.multiservAddr})
|
||||
SSBURI template.URL
|
||||
}{alias, template.URL(ssbURI.String())})
|
||||
if err != nil {
|
||||
log.Println("alias-resolve render errr:", err)
|
||||
}
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/csrf"
|
||||
"github.com/gorilla/mux"
|
||||
"go.mindeco.de/http/auth"
|
||||
"go.mindeco.de/http/render"
|
||||
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
|
||||
)
|
||||
|
||||
var HTMLTemplates = []string{
|
||||
"auth/fallback_sign_in.tmpl",
|
||||
}
|
||||
|
||||
func Handler(m *mux.Router, r *render.Renderer, a *auth.Handler) http.Handler {
|
||||
if m == nil {
|
||||
m = router.Auth(nil)
|
||||
}
|
||||
|
||||
m.Get(router.AuthFallbackSignInForm).Handler(r.HTML("auth/fallback_sign_in.tmpl", func(w http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||
return map[string]interface{}{
|
||||
csrf.TemplateTag: csrf.TemplateField(req),
|
||||
}, nil
|
||||
}))
|
||||
|
||||
// hook up the auth handler to the router
|
||||
m.Get(router.AuthFallbackSignIn).HandlerFunc(a.Authorize)
|
||||
m.Get(router.AuthFallbackSignOut).HandlerFunc(a.Logout)
|
||||
|
||||
return m
|
||||
}
|
|
@ -0,0 +1,514 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"image/color"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
kitlog "github.com/go-kit/kit/log"
|
||||
"github.com/go-kit/kit/log/level"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/skip2/go-qrcode"
|
||||
"go.cryptoscope.co/muxrpc/v2"
|
||||
"go.mindeco.de/http/render"
|
||||
"go.mindeco.de/logging"
|
||||
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/network"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/signinwithssb"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
|
||||
weberrors "github.com/ssb-ngi-pointer/go-ssb-room/web/errors"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
|
||||
refs "go.mindeco.de/ssb-refs"
|
||||
)
|
||||
|
||||
var HTMLTemplates = []string{
|
||||
"auth/decide_method.tmpl",
|
||||
"auth/withssb_server_start.tmpl",
|
||||
}
|
||||
|
||||
// custom sessionKey type to prevent collision
|
||||
type sessionKey uint
|
||||
|
||||
func init() {
|
||||
// need to register our Key with gob so gorilla/sessions can (de)serialize it
|
||||
gob.Register(memberToken)
|
||||
gob.Register(time.Time{})
|
||||
}
|
||||
|
||||
const (
|
||||
siwssbSessionName = "AuthWithSSBSession"
|
||||
|
||||
memberToken sessionKey = iota
|
||||
userTimeout
|
||||
)
|
||||
|
||||
const sessionLifetime = time.Hour * 24
|
||||
|
||||
// WithSSBHandler implements the oauth-like challenge/response dance described in
|
||||
// https://ssb-ngi-pointer.github.io/ssb-http-auth-spec
|
||||
type WithSSBHandler struct {
|
||||
render *render.Renderer
|
||||
|
||||
roomID refs.FeedRef
|
||||
|
||||
membersdb roomdb.MembersService
|
||||
aliasesdb roomdb.AliasesService
|
||||
sessiondb roomdb.AuthWithSSBService
|
||||
|
||||
cookieStore sessions.Store
|
||||
|
||||
endpoints network.Endpoints
|
||||
|
||||
bridge *signinwithssb.SignalBridge
|
||||
}
|
||||
|
||||
func NewWithSSBHandler(
|
||||
m *mux.Router,
|
||||
r *render.Renderer,
|
||||
roomID refs.FeedRef,
|
||||
endpoints network.Endpoints,
|
||||
aliasDB roomdb.AliasesService,
|
||||
membersDB roomdb.MembersService,
|
||||
sessiondb roomdb.AuthWithSSBService,
|
||||
cookies sessions.Store,
|
||||
bridge *signinwithssb.SignalBridge,
|
||||
) *WithSSBHandler {
|
||||
|
||||
var ssb WithSSBHandler
|
||||
ssb.render = r
|
||||
ssb.roomID = roomID
|
||||
ssb.aliasesdb = aliasDB
|
||||
ssb.membersdb = membersDB
|
||||
ssb.endpoints = endpoints
|
||||
ssb.sessiondb = sessiondb
|
||||
ssb.cookieStore = cookies
|
||||
ssb.bridge = bridge
|
||||
|
||||
m.Get(router.AuthWithSSBLogin).HandlerFunc(ssb.decideMethod)
|
||||
m.Get(router.AuthWithSSBServerEvents).HandlerFunc(ssb.eventSource)
|
||||
m.Get(router.AuthWithSSBFinalize).HandlerFunc(ssb.finalizeCookie)
|
||||
|
||||
return &ssb
|
||||
}
|
||||
|
||||
// AuthenticateRequest uses the passed request to load and return the session data that was stored previously.
|
||||
// If it is invalid or there is no session, it will return ErrNotAuthorized.
|
||||
// Otherwise it will return the member that belongs to the session.
|
||||
func (h WithSSBHandler) AuthenticateRequest(r *http.Request) (*roomdb.Member, error) {
|
||||
session, err := h.cookieStore.Get(r, siwssbSessionName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if session.IsNew {
|
||||
return nil, weberrors.ErrNotAuthorized
|
||||
}
|
||||
|
||||
tokenVal, ok := session.Values[memberToken]
|
||||
if !ok {
|
||||
return nil, weberrors.ErrNotAuthorized
|
||||
}
|
||||
|
||||
t, ok := session.Values[userTimeout]
|
||||
if !ok {
|
||||
return nil, weberrors.ErrNotAuthorized
|
||||
}
|
||||
|
||||
tout, ok := t.(time.Time)
|
||||
if !ok {
|
||||
return nil, weberrors.ErrNotAuthorized
|
||||
}
|
||||
|
||||
if time.Now().After(tout) {
|
||||
return nil, weberrors.ErrNotAuthorized
|
||||
}
|
||||
|
||||
token, ok := tokenVal.(string)
|
||||
if !ok {
|
||||
return nil, weberrors.ErrNotAuthorized
|
||||
}
|
||||
|
||||
memberID, err := h.sessiondb.CheckToken(r.Context(), token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
member, err := h.membersdb.GetByID(r.Context(), memberID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &member, nil
|
||||
}
|
||||
|
||||
// Logout destroys the session data and updates the cookie with an invalidated one.
|
||||
func (h WithSSBHandler) Logout(w http.ResponseWriter, r *http.Request) error {
|
||||
session, err := h.cookieStore.Get(r, siwssbSessionName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tokenVal, ok := session.Values[memberToken]
|
||||
if !ok {
|
||||
// not a ssb http auth session
|
||||
return nil
|
||||
}
|
||||
|
||||
token, ok := tokenVal.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("wrong token type: %T", tokenVal)
|
||||
}
|
||||
|
||||
err = h.sessiondb.RemoveToken(r.Context(), token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
session.Values[userTimeout] = time.Now().Add(-sessionLifetime)
|
||||
session.Options.MaxAge = -1
|
||||
if err := session.Save(r, w); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// saveCookie is a utility function that stores the passed token inside the cookie
|
||||
func (h WithSSBHandler) saveCookie(w http.ResponseWriter, req *http.Request, token string) error {
|
||||
session, err := h.cookieStore.Get(req, siwssbSessionName)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("ssb http auth: failed to load cookie session: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
session.Values[memberToken] = token
|
||||
session.Values[userTimeout] = time.Now().Add(sessionLifetime)
|
||||
if err := session.Save(req, w); err != nil {
|
||||
err = fmt.Errorf("ssb http auth: failed to update cookie session: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// this is the /login landing page which branches out to the different methods based on the query parameters that are present
|
||||
func (h WithSSBHandler) decideMethod(w http.ResponseWriter, req *http.Request) {
|
||||
queryVals := req.URL.Query()
|
||||
|
||||
var (
|
||||
alias string = queryVals.Get("alias")
|
||||
cid *refs.FeedRef
|
||||
)
|
||||
|
||||
if cidString := queryVals.Get("cid"); cidString != "" {
|
||||
parsedCID, err := refs.ParseFeedRef(cidString)
|
||||
if err == nil {
|
||||
cid = parsedCID
|
||||
|
||||
_, err := h.membersdb.GetByFeed(req.Context(), *cid)
|
||||
if err != nil {
|
||||
if err == roomdb.ErrNotFound {
|
||||
errMsg := fmt.Errorf("ssb http auth: client isn't a member: %w", err)
|
||||
h.render.Error(w, req, http.StatusForbidden, errMsg)
|
||||
return
|
||||
}
|
||||
h.render.Error(w, req, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
aliasEntry, err := h.aliasesdb.Resolve(req.Context(), alias)
|
||||
if err == nil {
|
||||
cid = &aliasEntry.Feed
|
||||
}
|
||||
}
|
||||
|
||||
// ?cid=CID&cc=CC does client-initiated http-auth
|
||||
if cc := queryVals.Get("cc"); cc != "" && cid != nil {
|
||||
err := h.clientInitiated(w, req, *cid)
|
||||
if err != nil {
|
||||
h.render.Error(w, req, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// assume server-init sse dance
|
||||
data, err := h.serverInitiated()
|
||||
if err != nil {
|
||||
h.render.Error(w, req, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
h.render.Render(w, req, "auth/withssb_server_start.tmpl", http.StatusOK, data)
|
||||
}
|
||||
|
||||
// clientInitiated is called with a client challange (?cc=123) and calls back to the passed client using muxrpc to request a signed solution
|
||||
// if everything checks out it redirects to the admin dashboard
|
||||
func (h WithSSBHandler) clientInitiated(w http.ResponseWriter, req *http.Request, client refs.FeedRef) error {
|
||||
queryParams := req.URL.Query()
|
||||
|
||||
var payload signinwithssb.ClientPayload
|
||||
payload.ServerID = h.roomID // fill in the server
|
||||
|
||||
// validate and update client challenge
|
||||
cc := queryParams.Get("cc")
|
||||
payload.ClientChallenge = cc
|
||||
|
||||
// check that we have that member
|
||||
member, err := h.membersdb.GetByFeed(req.Context(), client)
|
||||
if err != nil {
|
||||
errMsg := fmt.Errorf("ssb http auth: client isn't a member: %w", err)
|
||||
if err == roomdb.ErrNotFound {
|
||||
return weberrors.ErrForbidden{Details: errMsg}
|
||||
}
|
||||
return errMsg
|
||||
}
|
||||
payload.ClientID = client
|
||||
|
||||
// get the connected client for that member
|
||||
edp, connected := h.endpoints.GetEndpointFor(client)
|
||||
if !connected {
|
||||
return weberrors.ErrForbidden{Details: fmt.Errorf("ssb http auth: client not connected to room")}
|
||||
}
|
||||
|
||||
// roll a Challenge from the server
|
||||
sc := signinwithssb.GenerateChallenge()
|
||||
payload.ServerChallenge = sc
|
||||
|
||||
ctx, cancel := context.WithTimeout(req.Context(), 1*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
// request the signed solution over muxrpc
|
||||
var solution string
|
||||
err = edp.Async(ctx, &solution, muxrpc.TypeString, muxrpc.Method{"httpAuth", "requestSolution"}, sc, cc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ssb http auth: could not request solution from client: %w", err)
|
||||
}
|
||||
|
||||
// decode and validate the response
|
||||
solution = strings.TrimSuffix(solution, ".sig.ed25519")
|
||||
solutionBytes, err := base64.StdEncoding.DecodeString(solution)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ssb http auth: failed to decode solution: %w", err)
|
||||
}
|
||||
|
||||
if !payload.Validate(solutionBytes) {
|
||||
return fmt.Errorf("ssb http auth: validation of client solution failed")
|
||||
}
|
||||
|
||||
// create a session for invalidation
|
||||
tok, err := h.sessiondb.CreateToken(req.Context(), member.ID)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("ssb http auth: could not create token: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := h.saveCookie(w, req, tok); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// go to the dashboard
|
||||
dashboardURL, err := router.CompleteApp().Get(router.AdminDashboard).URL()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
http.Redirect(w, req, dashboardURL.Path, http.StatusTemporaryRedirect)
|
||||
return nil
|
||||
}
|
||||
|
||||
// server-sent-events stuff
|
||||
|
||||
type templateData struct {
|
||||
SSBURI template.URL
|
||||
QRCodeURI template.URL
|
||||
ServerChallenge string
|
||||
}
|
||||
|
||||
func (h WithSSBHandler) serverInitiated() (templateData, error) {
|
||||
sc := h.bridge.RegisterSession()
|
||||
|
||||
// prepare the ssb-uri
|
||||
// https://ssb-ngi-pointer.github.io/ssb-http-auth-spec/#list-of-new-ssb-uris
|
||||
var queryParams = make(url.Values)
|
||||
queryParams.Set("action", "start-http-auth")
|
||||
queryParams.Set("sid", h.roomID.Ref())
|
||||
queryParams.Set("sc", sc)
|
||||
|
||||
var startAuthURI url.URL
|
||||
startAuthURI.Scheme = "ssb"
|
||||
startAuthURI.Opaque = "experimental"
|
||||
startAuthURI.RawQuery = queryParams.Encode()
|
||||
|
||||
// generate a QR code with the token inside so that you can open it easily in a supporting mobile app
|
||||
qrCode, err := qrcode.New(startAuthURI.String(), qrcode.Medium)
|
||||
if err != nil {
|
||||
return templateData{}, err
|
||||
}
|
||||
|
||||
qrCode.BackgroundColor = color.Transparent // transparent to fit into the page
|
||||
qrCode.ForegroundColor = color.Black
|
||||
|
||||
qrCodeData, err := qrCode.PNG(-5)
|
||||
if err != nil {
|
||||
return templateData{}, err
|
||||
}
|
||||
qrURI := "data:image/png;base64," + base64.StdEncoding.EncodeToString(qrCodeData)
|
||||
|
||||
// template.URL signals the template engine that those aren't fishy and from a trusted source
|
||||
|
||||
data := templateData{
|
||||
SSBURI: template.URL(startAuthURI.String()),
|
||||
QRCodeURI: template.URL(qrURI),
|
||||
|
||||
ServerChallenge: sc,
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// finalizeCookie is called with a redirect from the js sse client if everything worked
|
||||
func (h WithSSBHandler) finalizeCookie(w http.ResponseWriter, r *http.Request) {
|
||||
tok := r.URL.Query().Get("token")
|
||||
|
||||
// check the token is correct
|
||||
if _, err := h.sessiondb.CheckToken(r.Context(), tok); err != nil {
|
||||
http.Error(w, "invalid session token", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.saveCookie(w, r, tok); err != nil {
|
||||
http.Error(w, "failed to save cookie", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
||||
}
|
||||
|
||||
// the time after which the SSE dance is considered failed
|
||||
const sseTimeout = 3 * time.Minute
|
||||
|
||||
// eventSource is the server-side of our server-sent events (SSE) session
|
||||
// https://html.spec.whatwg.org/multipage/server-sent-events.html
|
||||
func (h WithSSBHandler) eventSource(w http.ResponseWriter, r *http.Request) {
|
||||
flusher, err := w.(http.Flusher)
|
||||
if !err {
|
||||
http.Error(w, "ssb http auth: server-initiated method needs streaming support", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// closes when the http request is closed
|
||||
var notify <-chan bool
|
||||
|
||||
notifier, ok := w.(http.CloseNotifier)
|
||||
if !ok {
|
||||
// testing hack
|
||||
// http.Error(w, "ssb http auth: cant notify about closed requests", http.StatusInternalServerError)
|
||||
// return
|
||||
ch := make(chan bool)
|
||||
go func() {
|
||||
time.Sleep(sseTimeout)
|
||||
close(ch)
|
||||
}()
|
||||
notify = ch
|
||||
} else {
|
||||
notify = notifier.CloseNotify()
|
||||
}
|
||||
|
||||
// setup headers for SSE
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
w.Header().Set("Transfer-Encoding", "chunked")
|
||||
|
||||
sc := r.URL.Query().Get("sc")
|
||||
if sc == "" {
|
||||
http.Error(w, "missing server challenge", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
logger := logging.FromContext(r.Context())
|
||||
logger = level.Debug(logger)
|
||||
logger = kitlog.With(logger, "stream", sc[:5])
|
||||
logger.Log("event", "stream opened")
|
||||
|
||||
evtCh, has := h.bridge.GetEventChannel(sc)
|
||||
if !has {
|
||||
http.Error(w, "no such session!", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
sender := newEventSender(w)
|
||||
|
||||
// ping ticker
|
||||
tick := time.NewTicker(3 * time.Second)
|
||||
go func() {
|
||||
time.Sleep(sseTimeout)
|
||||
tick.Stop()
|
||||
logger.Log("event", "stopped")
|
||||
}()
|
||||
|
||||
start := time.Now()
|
||||
flusher.Flush()
|
||||
|
||||
// Push events to client
|
||||
|
||||
for {
|
||||
select {
|
||||
|
||||
case <-notify:
|
||||
logger.Log("event", "request closed")
|
||||
return
|
||||
|
||||
case <-tick.C:
|
||||
sender.send("ping", fmt.Sprintf("Waiting for solution (session age: %s)", time.Since(start)))
|
||||
logger.Log("event", "sent ping")
|
||||
|
||||
case update := <-evtCh:
|
||||
var event, data string = "failed", "challenge validation failed"
|
||||
|
||||
if update.Worked {
|
||||
event = "success"
|
||||
data = update.Token
|
||||
} else {
|
||||
if update.Reason != nil {
|
||||
data = update.Reason.Error()
|
||||
}
|
||||
}
|
||||
|
||||
sender.send(event, data)
|
||||
logger.Log("event", "sent", "worked", update.Worked)
|
||||
return
|
||||
}
|
||||
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
// eventSender encapsulates the event ID and increases it with each send automatically
|
||||
type eventSender struct {
|
||||
w io.Writer
|
||||
|
||||
id uint32
|
||||
}
|
||||
|
||||
func newEventSender(w io.Writer) eventSender {
|
||||
return eventSender{w: w}
|
||||
}
|
||||
|
||||
func (es *eventSender) send(event, data string) {
|
||||
fmt.Fprintf(es.w, "id: %d\n", es.id)
|
||||
fmt.Fprintf(es.w, "data: %s\n", data)
|
||||
fmt.Fprintf(es.w, "event: %s\n", event)
|
||||
fmt.Fprint(es.w, "\n")
|
||||
es.id++
|
||||
}
|
|
@ -1,15 +1,27 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.cryptoscope.co/muxrpc/v2"
|
||||
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/maybemod/keys"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/signinwithssb"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/web"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/web/webassert"
|
||||
refs "go.mindeco.de/ssb-refs"
|
||||
|
@ -38,14 +50,14 @@ func TestLoginForm(t *testing.T) {
|
|||
|
||||
a, r := assert.New(t), require.New(t)
|
||||
|
||||
url, err := ts.Router.Get(router.AuthFallbackSignInForm).URL()
|
||||
url, err := ts.Router.Get(router.AuthFallbackLogin).URL()
|
||||
r.Nil(err)
|
||||
html, resp := ts.Client.GetHTML(url.String())
|
||||
a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code")
|
||||
|
||||
webassert.Localized(t, html, []webassert.LocalizedElement{
|
||||
{"title", "AuthTitle"},
|
||||
{"#welcome", "AuthFallbackWelcome"},
|
||||
{"title", "AuthFallbackTitle"},
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -53,11 +65,11 @@ func TestFallbackAuth(t *testing.T) {
|
|||
ts := setup(t)
|
||||
a, r := assert.New(t), require.New(t)
|
||||
|
||||
// very cheap client session
|
||||
// very cheap "browser" client session
|
||||
jar, err := cookiejar.New(nil)
|
||||
r.NoError(err)
|
||||
|
||||
signInFormURL, err := ts.Router.Get(router.AuthFallbackSignInForm).URL()
|
||||
signInFormURL, err := ts.Router.Get(router.AuthFallbackLogin).URL()
|
||||
r.Nil(err)
|
||||
signInFormURL.Host = "localhost"
|
||||
signInFormURL.Scheme = "https"
|
||||
|
@ -70,9 +82,10 @@ func TestFallbackAuth(t *testing.T) {
|
|||
|
||||
jar.SetCookies(signInFormURL, csrfCookie)
|
||||
|
||||
webassert.CSRFTokenPresent(t, doc.Find("form"))
|
||||
passwordForm := doc.Find("#password-fallback")
|
||||
webassert.CSRFTokenPresent(t, passwordForm)
|
||||
|
||||
csrfTokenElem := doc.Find("input[type=hidden]")
|
||||
csrfTokenElem := passwordForm.Find("input[type=hidden]")
|
||||
a.Equal(1, csrfTokenElem.Length())
|
||||
|
||||
csrfName, has := csrfTokenElem.Attr("name")
|
||||
|
@ -89,7 +102,7 @@ func TestFallbackAuth(t *testing.T) {
|
|||
}
|
||||
ts.AuthFallbackDB.CheckReturns(int64(23), nil)
|
||||
|
||||
signInURL, err := ts.Router.Get(router.AuthFallbackSignIn).URL()
|
||||
signInURL, err := ts.Router.Get(router.AuthFallbackFinalize).URL()
|
||||
r.Nil(err)
|
||||
|
||||
signInURL.Host = "localhost"
|
||||
|
@ -112,6 +125,7 @@ func TestFallbackAuth(t *testing.T) {
|
|||
sessionCookie := resp.Result().Cookies()
|
||||
jar.SetCookies(signInURL, sessionCookie)
|
||||
|
||||
// now request the protected dashboard page
|
||||
dashboardURL, err := ts.Router.Get(router.AdminDashboard).URL()
|
||||
r.Nil(err)
|
||||
dashboardURL.Host = "localhost"
|
||||
|
@ -169,3 +183,382 @@ func TestFallbackAuth(t *testing.T) {
|
|||
{"#roomCount", "AdminRoomCountPlural"},
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthWithSSBClientInitNotConnected(t *testing.T) {
|
||||
ts := setup(t)
|
||||
a, r := assert.New(t), require.New(t)
|
||||
|
||||
// the client is a member but not connected right now
|
||||
ts.MembersDB.GetByFeedReturns(roomdb.Member{ID: 1234, Nickname: "test-member"}, nil)
|
||||
ts.MockedEndpoints.GetEndpointForReturns(nil, false)
|
||||
|
||||
client, err := keys.NewKeyPair(nil)
|
||||
r.NoError(err)
|
||||
|
||||
cc := signinwithssb.GenerateChallenge()
|
||||
|
||||
urlTo := web.NewURLTo(ts.Router)
|
||||
|
||||
signInStartURL := urlTo(router.AuthWithSSBLogin,
|
||||
"cid", client.Feed.Ref(),
|
||||
"cc", cc,
|
||||
)
|
||||
r.NotNil(signInStartURL)
|
||||
|
||||
t.Log(signInStartURL.String())
|
||||
doc, resp := ts.Client.GetHTML(signInStartURL.String())
|
||||
a.Equal(http.StatusInternalServerError, resp.Code) // TODO: StatusForbidden
|
||||
|
||||
webassert.Localized(t, doc, []webassert.LocalizedElement{
|
||||
// {"#welcome", "AuthWithSSBWelcome"},
|
||||
// {"title", "AuthWithSSBTitle"},
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthWithSSBClientInitNotAllowed(t *testing.T) {
|
||||
ts := setup(t)
|
||||
a, r := assert.New(t), require.New(t)
|
||||
|
||||
// the client isnt a member
|
||||
ts.MembersDB.GetByFeedReturns(roomdb.Member{}, roomdb.ErrNotFound)
|
||||
ts.MockedEndpoints.GetEndpointForReturns(nil, false)
|
||||
|
||||
client, err := keys.NewKeyPair(nil)
|
||||
r.NoError(err)
|
||||
|
||||
cc := signinwithssb.GenerateChallenge()
|
||||
|
||||
urlTo := web.NewURLTo(ts.Router)
|
||||
|
||||
signInStartURL := urlTo(router.AuthWithSSBLogin,
|
||||
"cid", client.Feed.Ref(),
|
||||
"cc", cc,
|
||||
)
|
||||
r.NotNil(signInStartURL)
|
||||
|
||||
t.Log(signInStartURL.String())
|
||||
doc, resp := ts.Client.GetHTML(signInStartURL.String())
|
||||
a.Equal(http.StatusForbidden, resp.Code)
|
||||
|
||||
webassert.Localized(t, doc, []webassert.LocalizedElement{
|
||||
// {"#welcome", "AuthWithSSBWelcome"},
|
||||
// {"title", "AuthWithSSBTitle"},
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthWithSSBClientInitHasClient(t *testing.T) {
|
||||
ts := setup(t)
|
||||
a, r := assert.New(t), require.New(t)
|
||||
|
||||
// very cheap "browser" client session
|
||||
jar, err := cookiejar.New(nil)
|
||||
r.NoError(err)
|
||||
|
||||
// the request to be signed later
|
||||
var payload signinwithssb.ClientPayload
|
||||
payload.ServerID = ts.NetworkInfo.RoomID
|
||||
|
||||
// the keypair for our client
|
||||
testMember := roomdb.Member{ID: 1234, Nickname: "test-member"}
|
||||
client, err := keys.NewKeyPair(nil)
|
||||
r.NoError(err)
|
||||
testMember.PubKey = client.Feed
|
||||
|
||||
// setup the mocked database
|
||||
ts.MembersDB.GetByFeedReturns(testMember, nil)
|
||||
ts.AuthWithSSB.CreateTokenReturns("abcdefgh", nil)
|
||||
ts.AuthWithSSB.CheckTokenReturns(testMember.ID, nil)
|
||||
ts.MembersDB.GetByIDReturns(testMember, nil)
|
||||
|
||||
// fill the basic infos of the request
|
||||
payload.ClientID = client.Feed
|
||||
|
||||
// this is our fake "connected" client
|
||||
var edp muxrpc.FakeEndpoint
|
||||
|
||||
// setup a mocked muxrpc call that asserts the arguments and returns the needed signature
|
||||
edp.AsyncCalls(func(_ context.Context, ret interface{}, encoding muxrpc.RequestEncoding, method muxrpc.Method, args ...interface{}) error {
|
||||
a.Equal(muxrpc.TypeString, encoding)
|
||||
a.Equal("httpAuth.requestSolution", method.String())
|
||||
|
||||
r.Len(args, 2, "expected two args")
|
||||
|
||||
serverChallenge, ok := args[0].(string)
|
||||
r.True(ok, "argument[0] is not a string: %T", args[0])
|
||||
a.NotEqual("", serverChallenge)
|
||||
// update the challenge
|
||||
payload.ServerChallenge = serverChallenge
|
||||
|
||||
clientChallenge, ok := args[1].(string)
|
||||
r.True(ok, "argument[1] is not a string: %T", args[1])
|
||||
a.Equal(payload.ClientChallenge, clientChallenge)
|
||||
|
||||
strptr, ok := ret.(*string)
|
||||
r.True(ok, "return is not a string pointer: %T", ret)
|
||||
|
||||
// sign the request now that we have the sc
|
||||
clientSig := payload.Sign(client.Pair.Secret)
|
||||
|
||||
*strptr = base64.StdEncoding.EncodeToString(clientSig)
|
||||
return nil
|
||||
})
|
||||
|
||||
// setup the fake client endpoint
|
||||
ts.MockedEndpoints.GetEndpointForReturns(&edp, true)
|
||||
|
||||
cc := signinwithssb.GenerateChallenge()
|
||||
// update the challenge
|
||||
payload.ClientChallenge = cc
|
||||
|
||||
// prepare the url
|
||||
urlTo := web.NewURLTo(ts.Router)
|
||||
signInStartURL := urlTo(router.AuthWithSSBLogin,
|
||||
"cid", client.Feed.Ref(),
|
||||
"cc", cc,
|
||||
)
|
||||
signInStartURL.Host = "localhost"
|
||||
signInStartURL.Scheme = "https"
|
||||
|
||||
r.NotNil(signInStartURL)
|
||||
|
||||
t.Log(signInStartURL.String())
|
||||
doc, resp := ts.Client.GetHTML(signInStartURL.String())
|
||||
a.Equal(http.StatusTemporaryRedirect, resp.Code)
|
||||
|
||||
dashboardURL, err := ts.Router.Get(router.AdminDashboard).URL()
|
||||
r.Nil(err)
|
||||
a.Equal(dashboardURL.Path, resp.Header().Get("Location"))
|
||||
|
||||
webassert.Localized(t, doc, []webassert.LocalizedElement{
|
||||
// {"#welcome", "AuthWithSSBWelcome"},
|
||||
// {"title", "AuthWithSSBTitle"},
|
||||
})
|
||||
|
||||
// analyse the endpoints call
|
||||
a.Equal(1, ts.MockedEndpoints.GetEndpointForCallCount())
|
||||
edpRef := ts.MockedEndpoints.GetEndpointForArgsForCall(0)
|
||||
a.Equal(client.Feed.Ref(), edpRef.Ref())
|
||||
|
||||
// check the mock was called
|
||||
a.Equal(1, edp.AsyncCallCount())
|
||||
|
||||
// check that we have a new cookie
|
||||
sessionCookie := resp.Result().Cookies()
|
||||
r.True(len(sessionCookie) > 0, "expecting one cookie!")
|
||||
jar.SetCookies(signInStartURL, sessionCookie)
|
||||
|
||||
// now request the protected dashboard page
|
||||
dashboardURL.Host = "localhost"
|
||||
dashboardURL.Scheme = "https"
|
||||
|
||||
// load the cookie for the dashboard
|
||||
cs := jar.Cookies(dashboardURL)
|
||||
r.True(len(cs) > 0, "expecting one cookie!")
|
||||
|
||||
var sessionHeader = http.Header(map[string][]string{})
|
||||
for _, c := range cs {
|
||||
theCookie := c.String()
|
||||
a.NotEqual("", theCookie, "should have a new cookie")
|
||||
sessionHeader.Add("Cookie", theCookie)
|
||||
}
|
||||
|
||||
durl := dashboardURL.String()
|
||||
t.Log(durl)
|
||||
|
||||
// update headers
|
||||
ts.Client.ClearHeaders()
|
||||
ts.Client.SetHeaders(sessionHeader)
|
||||
|
||||
html, resp := ts.Client.GetHTML(durl)
|
||||
if !a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code for dashboard") {
|
||||
t.Log(html.Find("body").Text())
|
||||
}
|
||||
|
||||
webassert.Localized(t, html, []webassert.LocalizedElement{
|
||||
{"#welcome", "AdminDashboardWelcome"},
|
||||
{"title", "AdminDashboardTitle"},
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthWithSSBServerInitHappyPath(t *testing.T) {
|
||||
ts := setup(t)
|
||||
a, r := assert.New(t), require.New(t)
|
||||
|
||||
// the keypair for our client
|
||||
testMember := roomdb.Member{ID: 1234, Nickname: "test-member"}
|
||||
client, err := keys.NewKeyPair(nil)
|
||||
r.NoError(err)
|
||||
testMember.PubKey = client.Feed
|
||||
|
||||
// setup the mocked database
|
||||
ts.MembersDB.GetByFeedReturns(testMember, nil)
|
||||
|
||||
// prepare the url
|
||||
urlTo := web.NewURLTo(ts.Router)
|
||||
signInStartURL := urlTo(router.AuthWithSSBLogin,
|
||||
"cid", client.Feed.Ref(),
|
||||
)
|
||||
r.NotNil(signInStartURL)
|
||||
|
||||
html, resp := ts.Client.GetHTML(signInStartURL.String())
|
||||
if !a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code for dashboard") {
|
||||
t.Log(html.Find("body").Text())
|
||||
}
|
||||
|
||||
webassert.Localized(t, html, []webassert.LocalizedElement{
|
||||
{"title", "AuthWithSSBTitle"},
|
||||
{"#welcome", "AuthWithSSBWelcome"},
|
||||
})
|
||||
|
||||
jsFile, has := html.Find("script").Attr("src")
|
||||
a.True(has, "should have client code")
|
||||
a.Equal("/assets/login-events.js", jsFile)
|
||||
|
||||
serverChallenge, has := html.Find("#challenge").Attr("data-sc")
|
||||
a.True(has, "should have server challenge")
|
||||
a.NotEqual("", serverChallenge)
|
||||
|
||||
ssbURI, has := html.Find("#start-auth-uri").Attr("href")
|
||||
a.True(has, "should have an ssb:experimental uri")
|
||||
a.True(strings.HasPrefix(ssbURI, "ssb:experimental?"), "not an ssb-uri? %s", ssbURI)
|
||||
|
||||
parsedURI, err := url.Parse(ssbURI)
|
||||
r.NoError(err)
|
||||
a.Equal("ssb", parsedURI.Scheme)
|
||||
a.Equal("experimental", parsedURI.Opaque)
|
||||
|
||||
qry := parsedURI.Query()
|
||||
a.Equal("start-http-auth", qry.Get("action"))
|
||||
a.Equal(serverChallenge, qry.Get("sc"))
|
||||
a.Equal(ts.NetworkInfo.RoomID.Ref(), qry.Get("sid"))
|
||||
|
||||
qrCode, has := html.Find("#start-auth-qrcode").Attr("src")
|
||||
a.True(has, "should have the inline image data")
|
||||
a.True(strings.HasPrefix(qrCode, "data:image/png;base64,"))
|
||||
|
||||
// TODO: decode image data and check qr code(?)
|
||||
|
||||
// simulate muxrpc client
|
||||
testToken := "our-test-token"
|
||||
ts.AuthWithSSB.CheckTokenReturns(23, nil)
|
||||
go func() {
|
||||
time.Sleep(4 * time.Second)
|
||||
err = ts.SignalBridge.SessionWorked(serverChallenge, testToken)
|
||||
r.NoError(err)
|
||||
}()
|
||||
|
||||
// start reading sse
|
||||
sseURL := urlTo(router.AuthWithSSBServerEvents, "sc", serverChallenge)
|
||||
resp = ts.Client.GetBody(sseURL.String())
|
||||
a.Equal(http.StatusOK, resp.Result().StatusCode)
|
||||
|
||||
// check contents of sse channel
|
||||
sseBody := resp.Body.String()
|
||||
|
||||
a.True(strings.Contains(sseBody, "data: Waiting for solution"), "ping data")
|
||||
a.True(strings.Contains(sseBody, "event: ping\n"), "ping event")
|
||||
|
||||
wantDataToken := fmt.Sprintf("data: %s\n", testToken)
|
||||
a.True(strings.Contains(sseBody, wantDataToken), "token data")
|
||||
a.True(strings.Contains(sseBody, "event: success\n"), "success event")
|
||||
|
||||
// use the token and go to /withssb/finalize and get a cookie
|
||||
// (this happens in the browser engine via login-events.js)
|
||||
finalizeURL := urlTo(router.AuthWithSSBFinalize, "token", testToken)
|
||||
finalizeURL.Host = "localhost"
|
||||
finalizeURL.Scheme = "https"
|
||||
|
||||
resp = ts.Client.GetBody(finalizeURL.String())
|
||||
|
||||
csrfCookie := resp.Result().Cookies()
|
||||
a.Len(csrfCookie, 2, "csrf and session cookie")
|
||||
|
||||
// very cheap "browser" client session
|
||||
jar, err := cookiejar.New(nil)
|
||||
r.NoError(err)
|
||||
jar.SetCookies(finalizeURL, csrfCookie)
|
||||
|
||||
// now request the protected dashboard page
|
||||
dashboardURL, err := ts.Router.Get(router.AdminDashboard).URL()
|
||||
r.Nil(err)
|
||||
dashboardURL.Host = "localhost"
|
||||
dashboardURL.Scheme = "https"
|
||||
|
||||
// load the cookie for the dashboard
|
||||
cs := jar.Cookies(dashboardURL)
|
||||
r.True(len(cs) > 0, "expecting one cookie!")
|
||||
var sessionHeader = http.Header(map[string][]string{})
|
||||
for _, c := range cs {
|
||||
theCookie := c.String()
|
||||
a.NotEqual("", theCookie, "should have a new cookie")
|
||||
sessionHeader.Add("Cookie", theCookie)
|
||||
}
|
||||
ts.Client.SetHeaders(sessionHeader)
|
||||
|
||||
html, resp = ts.Client.GetHTML(dashboardURL.String())
|
||||
if !a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code for dashboard") {
|
||||
t.Log(html.Find("body").Text())
|
||||
}
|
||||
|
||||
webassert.Localized(t, html, []webassert.LocalizedElement{
|
||||
{"#welcome", "AdminDashboardWelcome"},
|
||||
{"title", "AdminDashboardTitle"},
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthWithSSBServerInitWrongSolution(t *testing.T) {
|
||||
ts := setup(t)
|
||||
a, r := assert.New(t), require.New(t)
|
||||
|
||||
// the keypair for our client
|
||||
testMember := roomdb.Member{ID: 1234, Nickname: "test-member"}
|
||||
client, err := keys.NewKeyPair(nil)
|
||||
r.NoError(err)
|
||||
testMember.PubKey = client.Feed
|
||||
|
||||
// setup the mocked database
|
||||
ts.MembersDB.GetByFeedReturns(testMember, nil)
|
||||
|
||||
// prepare the url
|
||||
urlTo := web.NewURLTo(ts.Router)
|
||||
signInStartURL := urlTo(router.AuthWithSSBLogin,
|
||||
"cid", client.Feed.Ref(),
|
||||
)
|
||||
r.NotNil(signInStartURL)
|
||||
|
||||
html, resp := ts.Client.GetHTML(signInStartURL.String())
|
||||
if !a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code for dashboard") {
|
||||
t.Log(html.Find("body").Text())
|
||||
}
|
||||
|
||||
serverChallenge, has := html.Find("#challenge").Attr("data-sc")
|
||||
a.True(has, "should have server challenge")
|
||||
a.NotEqual("", serverChallenge)
|
||||
|
||||
// simulate muxrpc client
|
||||
ts.AuthWithSSB.CheckTokenReturns(-1, roomdb.ErrNotFound)
|
||||
go func() {
|
||||
time.Sleep(4 * time.Second)
|
||||
err = ts.SignalBridge.SessionFailed(serverChallenge, fmt.Errorf("wrong solution"))
|
||||
r.NoError(err)
|
||||
}()
|
||||
|
||||
// start reading sse
|
||||
sseURL := urlTo(router.AuthWithSSBServerEvents, "sc", serverChallenge)
|
||||
resp = ts.Client.GetBody(sseURL.String())
|
||||
a.Equal(http.StatusOK, resp.Result().StatusCode)
|
||||
|
||||
// check contents of sse channel
|
||||
sseBody := resp.Body.String()
|
||||
|
||||
a.True(strings.Contains(sseBody, "data: Waiting for solution"), "ping data")
|
||||
a.True(strings.Contains(sseBody, "event: ping\n"), "ping event")
|
||||
|
||||
a.True(strings.Contains(sseBody, "data: wrong solution\n"), "reason data")
|
||||
a.True(strings.Contains(sseBody, "event: failed\n"), "success event")
|
||||
|
||||
// use an invalid token
|
||||
finalizeURL := urlTo(router.AuthWithSSBFinalize, "token", "wrong")
|
||||
resp = ts.Client.GetBody(finalizeURL.String())
|
||||
a.Equal(http.StatusForbidden, resp.Result().StatusCode)
|
||||
}
|
||||
|
|
|
@ -11,8 +11,7 @@ import (
|
|||
"strconv"
|
||||
"time"
|
||||
|
||||
refs "go.mindeco.de/ssb-refs"
|
||||
|
||||
"github.com/go-kit/kit/log/level"
|
||||
"github.com/gorilla/csrf"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/russross/blackfriday/v2"
|
||||
|
@ -20,7 +19,9 @@ import (
|
|||
"go.mindeco.de/http/render"
|
||||
"go.mindeco.de/logging"
|
||||
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/network"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/repo"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/signinwithssb"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomstate"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/web"
|
||||
|
@ -29,6 +30,7 @@ import (
|
|||
"github.com/ssb-ngi-pointer/go-ssb-room/web/i18n"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/web/members"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
|
||||
refs "go.mindeco.de/ssb-refs"
|
||||
)
|
||||
|
||||
var HTMLTemplates = []string{
|
||||
|
@ -37,6 +39,7 @@ var HTMLTemplates = []string{
|
|||
"aliases-resolved.html",
|
||||
"invite/accept.tmpl",
|
||||
"invite/consumed.tmpl",
|
||||
"auth/fallback_sign_in.tmpl",
|
||||
"notice/list.tmpl",
|
||||
"notice/show.tmpl",
|
||||
"error.tmpl",
|
||||
|
@ -46,6 +49,7 @@ var HTMLTemplates = []string{
|
|||
type Databases struct {
|
||||
Aliases roomdb.AliasesService
|
||||
AuthFallback roomdb.AuthFallbackService
|
||||
AuthWithSSB roomdb.AuthWithSSBService
|
||||
DeniedKeys roomdb.DeniedKeysService
|
||||
Invites roomdb.InvitesService
|
||||
Notices roomdb.NoticesService
|
||||
|
@ -69,8 +73,9 @@ func New(
|
|||
repo repo.Interface,
|
||||
netInfo NetworkInfo,
|
||||
roomState *roomstate.Manager,
|
||||
roomEndpoints network.Endpoints,
|
||||
bridge *signinwithssb.SignalBridge,
|
||||
dbs Databases,
|
||||
|
||||
) (http.Handler, error) {
|
||||
m := router.CompleteApp()
|
||||
|
||||
|
@ -147,7 +152,7 @@ func New(
|
|||
return nil, err
|
||||
}
|
||||
|
||||
store := &sessions.CookieStore{
|
||||
cookieStore := &sessions.CookieStore{
|
||||
Codecs: cookieCodec,
|
||||
Options: &sessions.Options{
|
||||
Path: "/",
|
||||
|
@ -197,8 +202,8 @@ func New(
|
|||
}, nil
|
||||
})
|
||||
|
||||
a, err := auth.NewHandler(dbs.AuthFallback,
|
||||
auth.SetStore(store),
|
||||
authWithPassword, err := auth.NewHandler(dbs.AuthFallback,
|
||||
auth.SetStore(cookieStore),
|
||||
auth.SetErrorHandler(authErrH),
|
||||
auth.SetNotAuthorizedHandler(notAuthorizedH),
|
||||
auth.SetLifetime(2*time.Hour), // TODO: configure
|
||||
|
@ -225,8 +230,37 @@ func New(
|
|||
// TODO: explain problem between gorilla/mux named routers and authentication
|
||||
mainMux := &http.ServeMux{}
|
||||
|
||||
// hookup handlers to the router
|
||||
roomsAuth.Handler(m, r, a)
|
||||
// start hooking up handlers to the router
|
||||
|
||||
authWithSSB := roomsAuth.NewWithSSBHandler(
|
||||
m,
|
||||
r,
|
||||
netInfo.RoomID,
|
||||
roomEndpoints,
|
||||
dbs.Aliases,
|
||||
dbs.Members,
|
||||
dbs.AuthWithSSB,
|
||||
cookieStore,
|
||||
bridge,
|
||||
)
|
||||
|
||||
m.Get(router.AuthLogin).Handler(r.StaticHTML("auth/decide_method.tmpl"))
|
||||
|
||||
m.Get(router.AuthFallbackFinalize).HandlerFunc(authWithPassword.Authorize)
|
||||
|
||||
m.Get(router.AuthFallbackLogin).Handler(r.HTML("auth/fallback_sign_in.tmpl", func(w http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||
return map[string]interface{}{
|
||||
csrf.TemplateTag: csrf.TemplateField(req),
|
||||
}, nil
|
||||
}))
|
||||
|
||||
m.Get(router.AuthLogout).HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
err = authWithSSB.Logout(w, req)
|
||||
if err != nil {
|
||||
level.Warn(logging.FromContext(req.Context())).Log("err", err)
|
||||
}
|
||||
authWithPassword.Logout(w, req)
|
||||
})
|
||||
|
||||
adminHandler := admin.Handler(
|
||||
netInfo.Domain,
|
||||
|
@ -241,7 +275,7 @@ func New(
|
|||
PinnedNotices: dbs.PinnedNotices,
|
||||
},
|
||||
)
|
||||
mainMux.Handle("/admin/", a.Authenticate(adminHandler))
|
||||
mainMux.Handle("/admin/", members.AuthenticateFromContext(r)(adminHandler))
|
||||
|
||||
m.Get(router.CompleteIndex).Handler(r.HTML("landing/index.tmpl", func(w http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||
notice, err := dbs.PinnedNotices.Get(req.Context(), roomdb.NoticeDescription, "en-GB")
|
||||
|
@ -297,7 +331,7 @@ func New(
|
|||
// apply HTTP middleware
|
||||
middlewares := []func(http.Handler) http.Handler{
|
||||
logging.InjectHandler(logger),
|
||||
members.ContextInjecter(dbs.Members, a),
|
||||
members.ContextInjecter(dbs.Members, authWithPassword, authWithSSB),
|
||||
CSRF,
|
||||
}
|
||||
|
||||
|
|
|
@ -49,12 +49,11 @@ func (h inviteHandler) consume(rw http.ResponseWriter, req *http.Request) (inter
|
|||
}
|
||||
|
||||
alias := req.FormValue("alias")
|
||||
|
||||
token := req.FormValue("token")
|
||||
|
||||
newMember, err := refs.ParseFeedRef(req.FormValue("new_member"))
|
||||
if err != nil {
|
||||
return nil, weberrors.ErrBadRequest{Where: "form data", Details: err}
|
||||
return nil, weberrors.ErrBadRequest{Where: "new_member", Details: err}
|
||||
}
|
||||
|
||||
inv, err := h.invites.Consume(req.Context(), token, *newMember)
|
||||
|
|
|
@ -61,6 +61,8 @@ func TestNoticesEditButtonVisible(t *testing.T) {
|
|||
|
||||
urlTo := web.NewURLTo(ts.Router)
|
||||
|
||||
ts.AliasesDB.ResolveReturns(roomdb.Alias{}, roomdb.ErrNotFound)
|
||||
|
||||
noticeData := roomdb.Notice{
|
||||
ID: 42,
|
||||
Title: "Welcome!",
|
||||
|
@ -89,7 +91,7 @@ func TestNoticesEditButtonVisible(t *testing.T) {
|
|||
|
||||
// when dealing with cookies we also need to have an Host and URL-Scheme
|
||||
// for the jar to save and load them correctly
|
||||
formEndpoint := urlTo(router.AuthFallbackSignInForm)
|
||||
formEndpoint := urlTo(router.AuthFallbackLogin)
|
||||
r.NotNil(formEndpoint)
|
||||
formEndpoint.Host = "localhost"
|
||||
formEndpoint.Scheme = "https"
|
||||
|
@ -126,7 +128,7 @@ func TestNoticesEditButtonVisible(t *testing.T) {
|
|||
ts.AuthFallbackDB.CheckReturns(testUser.ID, nil)
|
||||
ts.MembersDB.GetByIDReturns(testUser, nil)
|
||||
|
||||
postEndpoint, err := ts.Router.Get(router.AuthFallbackSignIn).URL()
|
||||
postEndpoint, err := ts.Router.Get(router.AuthFallbackFinalize).URL()
|
||||
r.Nil(err)
|
||||
postEndpoint.Host = "localhost"
|
||||
postEndpoint.Scheme = "https"
|
||||
|
|
|
@ -16,14 +16,16 @@ import (
|
|||
"github.com/gorilla/mux"
|
||||
"go.mindeco.de/http/tester"
|
||||
"go.mindeco.de/logging/logtest"
|
||||
refs "go.mindeco.de/ssb-refs"
|
||||
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/network/mocked"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/repo"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/signinwithssb"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb/mockdb"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomstate"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/web/i18n"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
|
||||
refs "go.mindeco.de/ssb-refs"
|
||||
)
|
||||
|
||||
type testSession struct {
|
||||
|
@ -34,6 +36,7 @@ type testSession struct {
|
|||
// mocked dbs
|
||||
AuthDB *mockdb.FakeAuthWithSSBService
|
||||
AuthFallbackDB *mockdb.FakeAuthFallbackService
|
||||
AuthWithSSB *mockdb.FakeAuthWithSSBService
|
||||
AliasesDB *mockdb.FakeAliasesService
|
||||
MembersDB *mockdb.FakeMembersService
|
||||
InvitesDB *mockdb.FakeInvitesService
|
||||
|
@ -42,12 +45,17 @@ type testSession struct {
|
|||
|
||||
RoomState *roomstate.Manager
|
||||
|
||||
MockedEndpoints *mocked.FakeEndpoints
|
||||
|
||||
SignalBridge *signinwithssb.SignalBridge
|
||||
|
||||
NetworkInfo NetworkInfo
|
||||
}
|
||||
|
||||
var testI18N = justTheKeys()
|
||||
|
||||
func setup(t *testing.T) *testSession {
|
||||
t.Parallel()
|
||||
var ts testSession
|
||||
|
||||
testRepoPath := filepath.Join("testrun", t.Name())
|
||||
|
@ -63,6 +71,7 @@ func setup(t *testing.T) *testSession {
|
|||
|
||||
ts.AuthDB = new(mockdb.FakeAuthWithSSBService)
|
||||
ts.AuthFallbackDB = new(mockdb.FakeAuthFallbackService)
|
||||
ts.AuthWithSSB = new(mockdb.FakeAuthWithSSBService)
|
||||
ts.AliasesDB = new(mockdb.FakeAliasesService)
|
||||
ts.MembersDB = new(mockdb.FakeMembersService)
|
||||
ts.InvitesDB = new(mockdb.FakeInvitesService)
|
||||
|
@ -74,6 +83,8 @@ func setup(t *testing.T) *testSession {
|
|||
ts.PinnedDB.GetReturns(defaultNotice, nil)
|
||||
ts.NoticeDB = new(mockdb.FakeNoticesService)
|
||||
|
||||
ts.MockedEndpoints = new(mocked.FakeEndpoints)
|
||||
|
||||
ts.NetworkInfo = NetworkInfo{
|
||||
Domain: "localhost",
|
||||
PortMUXRPC: 8008,
|
||||
|
@ -91,14 +102,19 @@ func setup(t *testing.T) *testSession {
|
|||
|
||||
ts.Router = router.CompleteApp()
|
||||
|
||||
ts.SignalBridge = signinwithssb.NewSignalBridge()
|
||||
|
||||
h, err := New(
|
||||
log,
|
||||
testRepo,
|
||||
ts.NetworkInfo,
|
||||
ts.RoomState,
|
||||
ts.MockedEndpoints,
|
||||
ts.SignalBridge,
|
||||
Databases{
|
||||
Aliases: ts.AliasesDB,
|
||||
AuthFallback: ts.AuthFallbackDB,
|
||||
AuthWithSSB: ts.AuthWithSSB,
|
||||
Members: ts.MembersDB,
|
||||
Invites: ts.InvitesDB,
|
||||
Notices: ts.NoticeDB,
|
||||
|
|
|
@ -4,6 +4,7 @@ GenericSave = "Save"
|
|||
GenericCreate = "Create"
|
||||
GenericPreview = "Preview"
|
||||
GenericLanguage = "Language"
|
||||
GenericOpenLink = "Open Link"
|
||||
|
||||
PageNotFound = "The requested page was not found."
|
||||
|
||||
|
@ -14,11 +15,22 @@ RoleAdmin = "Admin"
|
|||
LandingTitle = "ohai my room"
|
||||
LandingWelcome = "Landing welcome here"
|
||||
|
||||
AuthFallbackWelcome = "You really shouldn't be here.... Let's get you through this."
|
||||
AuthFallbackTitle = "The place of last resort"
|
||||
AuthSignIn = "Sign in"
|
||||
AuthSignOut = "Sign out"
|
||||
|
||||
AuthTitle = "Member Authentication"
|
||||
AuthWelcome = "If you are a member of this room, you can access the internal dashboard. Click on your preferred sign-in method below:"
|
||||
|
||||
AuthWithSSBTitle = "Sign in with SSB"
|
||||
AuthWithSSBInstruct = "Easy and secure method, if your SSB app supports it."
|
||||
AuthWithSSBWelcome = "To sign-in with your SSB identity stored on this device, press the button below which will open a compatible SSB app, if it's installed."
|
||||
AuthWithSSBInstructQR = "If your SSB app is on another device, you can scan the following QR code to sign-in with that device's SSB identity."
|
||||
AuthWithSSBError = "Sign-in failed. Please make sure you use an SSB app that supports this method of login, and click the button above within a minute after this page was opened."
|
||||
|
||||
AuthFallbackTitle = "Password sign-in"
|
||||
AuthFallbackWelcome = "Signing in with username and password is only possible if the administrator has given you one, because we do not support user registration."
|
||||
AuthFallbackInstruct = "This method is an acceptable fallback, if you have a username and password."
|
||||
|
||||
AdminDashboardWelcome = "Welcome to your dashboard"
|
||||
AdminDashboardTitle = "Room Admin Dashboard"
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package members implements helpers for accessing the currently logged in admin or moderator of an active request.
|
||||
package members
|
||||
|
||||
|
@ -5,45 +7,80 @@ import (
|
|||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
|
||||
"go.mindeco.de/http/auth"
|
||||
"go.mindeco.de/http/render"
|
||||
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
|
||||
weberrors "github.com/ssb-ngi-pointer/go-ssb-room/web/errors"
|
||||
authWithSSB "github.com/ssb-ngi-pointer/go-ssb-room/web/handlers/auth"
|
||||
)
|
||||
|
||||
type roomMemberContextKeyType string
|
||||
|
||||
var roomMemberContextKey roomMemberContextKeyType = "ssb:room:httpcontext:member"
|
||||
|
||||
type Middleware func(next http.Handler) http.Handler
|
||||
|
||||
// AuthenticateFromContext calls the next http handler if there is a member stored in the context
|
||||
// otherwise it will call r.Error
|
||||
func AuthenticateFromContext(r *render.Renderer) Middleware {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
if FromContext(req.Context()) == nil {
|
||||
r.Error(w, req, http.StatusUnauthorized, weberrors.ErrNotAuthorized)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, req)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// FromContext returns the member or nil if not logged in
|
||||
func FromContext(ctx context.Context) *roomdb.Member {
|
||||
v := ctx.Value(roomMemberContextKey)
|
||||
|
||||
m, ok := v.(roomdb.Member)
|
||||
m, ok := v.(*roomdb.Member)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &m
|
||||
return m
|
||||
}
|
||||
|
||||
// ContextInjecter returns middleware for injecting a member into the context of the request.
|
||||
// Retreive it using FromContext(ctx)
|
||||
func ContextInjecter(mdb roomdb.MembersService, a *auth.Handler) func(http.Handler) http.Handler {
|
||||
func ContextInjecter(mdb roomdb.MembersService, withPassword *auth.Handler, withSSB *authWithSSB.WithSSBHandler) Middleware {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
v, err := a.AuthenticateRequest(req)
|
||||
if err != nil {
|
||||
next.ServeHTTP(w, req)
|
||||
return
|
||||
var (
|
||||
member *roomdb.Member
|
||||
|
||||
errWithPassword, errWithSSB error
|
||||
)
|
||||
|
||||
v, errWithPassword := withPassword.AuthenticateRequest(req)
|
||||
if errWithPassword == nil {
|
||||
mid, ok := v.(int64)
|
||||
if !ok {
|
||||
next.ServeHTTP(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
m, err := mdb.GetByID(req.Context(), mid)
|
||||
if err != nil {
|
||||
next.ServeHTTP(w, req)
|
||||
return
|
||||
}
|
||||
member = &m
|
||||
}
|
||||
|
||||
mid, ok := v.(int64)
|
||||
if !ok {
|
||||
next.ServeHTTP(w, req)
|
||||
return
|
||||
m, errWithSSB := withSSB.AuthenticateRequest(req)
|
||||
if errWithSSB == nil {
|
||||
member = m
|
||||
}
|
||||
|
||||
member, err := mdb.GetByID(req.Context(), mid)
|
||||
if err != nil {
|
||||
// if both methods failed, don't update the context
|
||||
if errWithPassword != nil && errWithSSB != nil {
|
||||
next.ServeHTTP(w, req)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package members
|
||||
|
||||
import (
|
||||
|
@ -14,7 +16,7 @@ import (
|
|||
func MiddlewareForTests(m roomdb.Member) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
ctx := context.WithValue(req.Context(), roomMemberContextKey, m)
|
||||
ctx := context.WithValue(req.Context(), roomMemberContextKey, &m)
|
||||
next.ServeHTTP(w, req.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -6,27 +6,32 @@ import "github.com/gorilla/mux"
|
|||
|
||||
// constant names for the named routes
|
||||
const (
|
||||
AuthFallbackSignInForm = "auth:fallback:signin:form"
|
||||
AuthFallbackSignIn = "auth:fallback:signin"
|
||||
AuthFallbackSignOut = "auth:fallback:logout"
|
||||
AuthLogin = "auth:login"
|
||||
AuthLogout = "auth:logout"
|
||||
|
||||
AuthWithSSBSignIn = "auth:ssb:signin"
|
||||
AuthWithSSBSignOut = "auth:ssb:logout"
|
||||
AuthFallbackLogin = "auth:fallback:login"
|
||||
AuthFallbackFinalize = "auth:fallback:finalize"
|
||||
|
||||
AuthWithSSBLogin = "auth:withssb:login"
|
||||
AuthWithSSBServerEvents = "auth:withssb:sse"
|
||||
AuthWithSSBFinalize = "auth:withssb:finalize"
|
||||
)
|
||||
|
||||
// NewSignin constructs a mux.Router containing the routes for sign-in and -out
|
||||
// Auth constructs a mux.Router containing the routes for sign-in and -out
|
||||
func Auth(m *mux.Router) *mux.Router {
|
||||
if m == nil {
|
||||
m = mux.NewRouter()
|
||||
}
|
||||
|
||||
// register fallback
|
||||
m.Path("/fallback/signin").Methods("GET").Name(AuthFallbackSignInForm)
|
||||
m.Path("/fallback/signin").Methods("POST").Name(AuthFallbackSignIn)
|
||||
m.Path("/fallback/logout").Methods("GET").Name(AuthFallbackSignOut)
|
||||
m.Path("/login").Methods("GET").Name(AuthLogin)
|
||||
m.Path("/logout").Methods("GET").Name(AuthLogout)
|
||||
|
||||
m.Path("/withssb/signin").Methods("GET").Name(AuthWithSSBSignIn)
|
||||
m.Path("/withssb/logout").Methods("GET").Name(AuthWithSSBSignOut)
|
||||
m.Path("/fallback/login").Methods("GET").Name(AuthFallbackLogin)
|
||||
m.Path("/fallback/finalize").Methods("POST").Name(AuthFallbackFinalize)
|
||||
|
||||
m.Path("/withssb/login").Methods("GET").Name(AuthWithSSBLogin)
|
||||
m.Path("/withssb/events").Methods("GET").Name(AuthWithSSBServerEvents)
|
||||
m.Path("/withssb/finalize").Methods("GET").Name(AuthWithSSBFinalize)
|
||||
|
||||
return m
|
||||
}
|
||||
|
|
|
@ -24,13 +24,13 @@ const (
|
|||
func CompleteApp() *mux.Router {
|
||||
m := mux.NewRouter()
|
||||
|
||||
Auth(m.PathPrefix("/auth").Subrouter())
|
||||
Auth(m)
|
||||
Admin(m.PathPrefix("/admin").Subrouter())
|
||||
|
||||
m.Path("/").Methods("GET").Name(CompleteIndex)
|
||||
m.Path("/about").Methods("GET").Name(CompleteAbout)
|
||||
|
||||
m.Path("/{alias}").Methods("GET").Name(CompleteAliasResolve)
|
||||
m.Path("/alias/{alias}").Methods("GET").Name(CompleteAliasResolve)
|
||||
|
||||
m.Path("/invite/accept").Methods("GET").Name(CompleteInviteAccept)
|
||||
m.Path("/invite/consume").Methods("POST").Name(CompleteInviteConsume)
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
{{ define "title" }}{{i18n "AdminAllowListRemoveConfirmTitle"}}{{ end }}
|
||||
{{ define "title" }}{{i18n "AdminMembersRemoveConfirmTitle"}}{{ end }}
|
||||
{{ define "content" }}
|
||||
<div class="flex flex-col justify-center items-center h-64">
|
||||
|
||||
<span
|
||||
id="welcome"
|
||||
class="text-center"
|
||||
>{{i18n "AdminAllowListRemoveConfirmWelcome"}}</span>
|
||||
>{{i18n "AdminMembersRemoveConfirmWelcome"}}</span>
|
||||
|
||||
<pre
|
||||
id="verify"
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<div>
|
||||
<h1>{{.Alias.Name}}</h1>
|
||||
|
||||
<pre>{{.RoomAddr}}</pre>
|
||||
<p class="color-red-600">TODO: ssb-uri</p>
|
||||
<pre>{{.SSBURI}}</pre>
|
||||
<a href="{{.SSBURI}}">Consume</a>
|
||||
</div>
|
||||
{{end}}
|
|
@ -0,0 +1,30 @@
|
|||
{{ define "title" }}{{i18n "AuthTitle"}}{{ end }}
|
||||
{{ define "content" }}
|
||||
<div class="flex flex-col justify-center items-center self-center max-w-lg">
|
||||
<span class="text-center mt-8">{{i18n "AuthWelcome"}}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row justify-center items-center sm:items-stretch">
|
||||
<a
|
||||
href="{{urlTo "auth:withssb:login"}}"
|
||||
class="w-64 sm:mr-4 my-6 py-10 border-green-200 border-2 rounded-3xl flex flex-col justify-start items-center hover:border-green-400 hover:shadow-xl transition"
|
||||
>
|
||||
<svg class="w-12 h-12 text-green-500 mb-4" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M12.66 13.67C12.32 14 11.93 14.29 11.5 14.5V21L9.5 23L7.5 21L9.5 19.29L8 18L9.5 16.71L7.5 15V14.5C6 13.77 5 12.26 5 10.5C5 8 7 6 9.5 6C9.54 6 9.58 6 9.61 6C9.59 6.07 9.54 6.12 9.5 6.18C9.23 6.79 9.08 7.43 9.03 8.08C8.43 8.28 8 8.84 8 9.5C8 10.33 8.67 11 9.5 11C9.53 11 9.57 11 9.6 11C10.24 12.25 11.34 13.2 12.66 13.67M16 6C16 5.37 15.9 4.75 15.72 4.18C17.06 4.56 18.21 5.55 18.73 6.96C19.33 8.62 18.89 10.39 17.75 11.59L20 17.68L18.78 20.25L16.22 19.05L17.5 16.76L15.66 16.06L16.63 14.34L14.16 13.41L14 12.95C12.36 12.77 10.88 11.7 10.27 10.04C9.42 7.71 10.63 5.12 12.96 4.27C13.14 4.21 13.33 4.17 13.5 4.13C12.84 2.87 11.53 2 10 2C7.79 2 6 3.79 6 6C6 6.09 6 6.17 6.03 6.26C5.7 6.53 5.4 6.82 5.15 7.15C5.06 6.78 5 6.4 5 6C5 3.24 7.24 1 10 1S15 3.24 15 6C15 7.16 14.6 8.21 13.94 9.06C16.08 8.88 16 6 16 6M12.81 8.1C12.87 8.27 12.96 8.41 13.06 8.54C13.62 7.88 13.97 7.04 14 6.11C13.89 6.13 13.8 6.15 13.7 6.18C12.92 6.47 12.5 7.33 12.81 8.1Z" />
|
||||
</svg>
|
||||
<h1 class="text-xl font-bold text-green-500">{{i18n "AuthWithSSBTitle"}}</h1>
|
||||
<span class="mx-3 mt-2 text-center text-sm">{{i18n "AuthWithSSBInstruct"}}</span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="{{urlTo "auth:fallback:login"}}"
|
||||
class="w-64 sm:ml-4 my-6 py-10 border-gray-200 border-2 rounded-3xl flex flex-col justify-start items-center hover:border-gray-400 hover:shadow-xl transition"
|
||||
>
|
||||
<svg class="w-12 h-12 text-gray-500 mb-4" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M17,7H22V17H17V19A1,1 0 0,0 18,20H20V22H17.5C16.95,22 16,21.55 16,21C16,21.55 15.05,22 14.5,22H12V20H14A1,1 0 0,0 15,19V5A1,1 0 0,0 14,4H12V2H14.5C15.05,2 16,2.45 16,3C16,2.45 16.95,2 17.5,2H20V4H18A1,1 0 0,0 17,5V7M2,7H13V9H4V15H13V17H2V7M20,15V9H17V15H20M8.5,12A1.5,1.5 0 0,0 7,10.5A1.5,1.5 0 0,0 5.5,12A1.5,1.5 0 0,0 7,13.5A1.5,1.5 0 0,0 8.5,12M13,10.89C12.39,10.33 11.44,10.38 10.88,11C10.32,11.6 10.37,12.55 11,13.11C11.55,13.63 12.43,13.63 13,13.11V10.89Z" />
|
||||
</svg>
|
||||
<h1 class="text-xl font-bold text-gray-500">{{i18n "AuthFallbackTitle"}}</h1>
|
||||
<span class="mx-3 mt-2 text-center text-sm">{{i18n "AuthFallbackInstruct"}}</span>
|
||||
</a>
|
||||
</div>
|
||||
{{end}}
|
|
@ -1,29 +1,25 @@
|
|||
{{ define "title" }}{{i18n "AuthFallbackTitle"}}{{ end }}
|
||||
{{ define "title" }}{{i18n "AuthTitle"}}{{ end }}
|
||||
{{ define "content" }}
|
||||
<div id="page-header">
|
||||
<h1 id="welcome" class="text-lg">{{i18n "AuthFallbackWelcome"}}</h1>
|
||||
</div>
|
||||
<div>
|
||||
<form method="POST" action={{urlTo "auth:fallback:signin" }} class="flex flex-row items-end">
|
||||
{{ .csrfField }}
|
||||
<div class="w-96 grid grid-cols-2 gap-x-4 gap-y-1 mr-4">
|
||||
<label>Username</label>
|
||||
<label>Password</label>
|
||||
<input
|
||||
type="text"
|
||||
name="user"
|
||||
class="shadow rounded border border-transparent h-8 p-1 focus:outline-none focus:ring-2 focus:ring-pink-400 focus:border-transparent"
|
||||
>
|
||||
<input
|
||||
type="password"
|
||||
name="pass"
|
||||
class="shadow rounded border border-transparent h-8 p-1 focus:outline-none focus:ring-2 focus:ring-pink-400 focus:border-transparent"
|
||||
>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
class="shadow rounded px-4 h-8 text-gray-100 bg-pink-600 hover:bg-pink-700 focus:outline-none focus:ring-2 focus:ring-pink-600 focus:ring-opacity-50"
|
||||
>Enter</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="flex flex-col justify-center items-center self-center max-w-lg">
|
||||
<span id="welcome" class="text-center mt-8">{{i18n "AuthFallbackWelcome"}}</span>
|
||||
|
||||
<form
|
||||
id="password-fallback"
|
||||
method="POST"
|
||||
action="{{urlTo "auth:fallback:finalize"}}"
|
||||
class="flex flex-row items-end"
|
||||
>
|
||||
{{ .csrfField }}
|
||||
<div class="flex flex-col w-48">
|
||||
<label class="mt-8 text-sm text-gray-600">Username</label>
|
||||
<input type="text" name="user"
|
||||
class="shadow rounded border border-transparent h-8 p-1 focus:outline-none focus:ring-2 focus:ring-green-400 focus:border-transparent">
|
||||
<label class="mt-8 text-sm text-gray-600">Password</label>
|
||||
<input type="password" name="pass"
|
||||
class="shadow rounded border border-transparent h-8 p-1 focus:outline-none focus:ring-2 focus:ring-green-400 focus:border-transparent">
|
||||
<button type="submit"
|
||||
class="my-8 shadow rounded px-4 h-8 text-gray-100 bg-green-500 hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-600 focus:ring-opacity-50">Enter</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{{end}}
|
|
@ -0,0 +1,33 @@
|
|||
{{ define "title" }}{{i18n "AuthWithSSBTitle"}}{{ end }}
|
||||
{{ define "content" }}
|
||||
<div class="flex flex-col justify-center items-center self-center max-w-lg">
|
||||
<span id="welcome" class="text-center mt-8">{{i18n "AuthWithSSBWelcome"}}</span>
|
||||
|
||||
<a
|
||||
id="start-auth-uri"
|
||||
href="{{.SSBURI}}"
|
||||
target="_blank"
|
||||
class="shadow rounded flex flex-row justify-center items-center mt-8 px-4 h-8 text-gray-100 bg-green-500 hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-600 focus:ring-opacity-50"
|
||||
>{{i18n "AuthWithSSBTitle"}}</a>
|
||||
|
||||
<p id="waiting" class="hidden mt-8 animate-pulse text-green-500">Waiting for confirmation</p>
|
||||
|
||||
<p id="failed" class="hidden mt-8 text-red-700 text-center">{{i18n "AuthWithSSBError"}}</p>
|
||||
|
||||
<hr class="mt-8 w-64 h-px bg-gray-200"></hr>
|
||||
|
||||
<span class="text-center mt-8">{{i18n "AuthWithSSBInstructQR"}}</span>
|
||||
|
||||
<img
|
||||
id="start-auth-qrcode"
|
||||
src="{{.QRCodeURI}}"
|
||||
alt="QR-Code to pass the challenge to an App"
|
||||
width="160"
|
||||
height="160"
|
||||
class="mt-8"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="challenge" class="hidden" data-sc="{{.ServerChallenge}}"></div>
|
||||
<script src="/assets/login-events.js"></script>
|
||||
{{end}}
|
|
@ -31,17 +31,17 @@
|
|||
<svg class="text-green-500 w-4 h-4 mr-1" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M22,18V22H18V19H15V16H12L9.74,13.74C9.19,13.91 8.61,14 8,14A6,6 0 0,1 2,8A6,6 0 0,1 8,2A6,6 0 0,1 14,8C14,8.61 13.91,9.19 13.74,9.74L22,18M7,5A2,2 0 0,0 5,7A2,2 0 0,0 7,9A2,2 0 0,0 9,7A2,2 0 0,0 7,5Z" />
|
||||
</svg>
|
||||
<span class="text-green-500 text-sm">{{$user.Nickname}}</span>
|
||||
<span class="text-green-500 text-sm truncate w-32">{{$user.Nickname}} {{$user.PubKey.Ref}}</span>
|
||||
</div>
|
||||
<a
|
||||
href="{{urlTo "auth:fallback:logout"}}"
|
||||
href="{{urlTo "auth:logout"}}"
|
||||
class="pl-3 pr-4 py-2 sm:py-1 font-semibold text-sm text-gray-500 hover:text-red-600"
|
||||
>{{i18n "AuthSignOut"}}</a>
|
||||
</span>
|
||||
{{else}}
|
||||
<a
|
||||
href="{{urlTo "auth:fallback:signin:form"}}"
|
||||
class="pl-3 pr-4 py-2 sm:py-1 font-semibold text-sm text-gray-500 hover:text-green-600"
|
||||
href="{{urlTo "auth:login"}}"
|
||||
class="pl-3 pr-4 py-2 sm:py-1 font-semibold text-sm text-gray-500 hover:text-green-500"
|
||||
>{{i18n "AuthSignIn"}}</a>
|
||||
{{end}}
|
||||
</div>
|
||||
|
|
|
@ -18,7 +18,7 @@ type LocalizedElement struct {
|
|||
func Localized(t *testing.T, html *goquery.Document, elems []LocalizedElement) {
|
||||
a := assert.New(t)
|
||||
for i, pair := range elems {
|
||||
a.Equal(pair.Label, html.Find(pair.Selector).Text(), "localized pair %d failed", i+1)
|
||||
a.Equal(pair.Label, html.Find(pair.Selector).Text(), "localized pair %d failed (selector: %s)", i+1, pair.Selector)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue