diff --git a/cmd/server/main.go b/cmd/server/main.go index 6155f91..1603b7c 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -19,6 +19,8 @@ import ( "syscall" "time" + "github.com/ssb-ngi-pointer/go-ssb-room/internal/signinwithssb" + // debug "net/http" _ "net/http/pprof" @@ -203,11 +205,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 { @@ -250,6 +255,7 @@ func runroomsrv() error { }, roomsrv.StateManager, roomsrv.Network, + bridge, handlers.Databases{ Aliases: db.Aliases, AuthFallback: db.AuthFallback, @@ -286,7 +292,8 @@ 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, diff --git a/go.mod b/go.mod index 9dc63ad..dd04ef4 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( 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 // indirect github.com/stretchr/testify v1.6.1 github.com/unrolled/secure v1.0.8 github.com/vcraescu/go-paginator/v2 v2.0.0 diff --git a/go.sum b/go.sum index 83e0de8..85a8375 100644 --- a/go.sum +++ b/go.sum @@ -372,6 +372,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= diff --git a/internal/signinwithssb/bridge.go b/internal/signinwithssb/bridge.go new file mode 100644 index 0000000..dca602a --- /dev/null +++ b/internal/signinwithssb/bridge.go @@ -0,0 +1,88 @@ +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 + +type Event struct { + Worked bool + Token string +} + +// 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 channel from which future events can be read +// and the 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 challanges 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 +} + +func (sb *SignalBridge) GetEventChannel(sc string) (<-chan Event, bool) { + sb.mu.Lock() + defer sb.mu.Unlock() + ch, has := sb.sessions[sc] + return ch, has +} + +// CompleteSession uses the passed challange to send on and close the open channel. +// It will return an error if the session doesn't exist. +func (sb *SignalBridge) CompleteSession(sc string, success bool, token string) error { + sb.mu.Lock() + defer sb.mu.Unlock() + + ch, ok := sb.sessions[sc] + if !ok { + return fmt.Errorf("no such session") + } + + ch <- Event{ + Worked: success, + Token: token, + } + close(ch) + + // remove session + delete(sb.sessions, sc) + + return nil +} diff --git a/internal/signinwithssb/bridge_test.go b/internal/signinwithssb/bridge_test.go new file mode 100644 index 0000000..a72a8d1 --- /dev/null +++ b/internal/signinwithssb/bridge_test.go @@ -0,0 +1,39 @@ +package signinwithssb + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestBridge(t *testing.T) { + a := assert.New(t) + + sb := NewSignalBridge() + + // try to use a non-existant session + err := sb.CompleteSession("nope", false) + a.Error(err) + + // make a new session + updates, sc := sb.RegisterSession() + + b, err := DecodeChallengeString(sc) + a.NoError(err) + a.Len(b, challengeLength) + + go func() { + err := sb.CompleteSession(sc, true) + a.NoError(err) + }() + + time.Sleep(time.Second / 4) + + select { + case evt := <-updates: + a.True(evt.Worked) + default: + t.Error("no updates") + } +} diff --git a/muxrpc/handlers/signinwithssb/withssb.go b/muxrpc/handlers/signinwithssb/withssb.go index 059660c..1a0e9f8 100644 --- a/muxrpc/handlers/signinwithssb/withssb.go +++ b/muxrpc/handlers/signinwithssb/withssb.go @@ -10,6 +10,7 @@ import ( "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" @@ -23,6 +24,8 @@ type Handler struct { sessions roomdb.AuthWithSSBService members roomdb.MembersService + bridge *signinwithssb.SignalBridge + roomDomain string // the http(s) domain of the room to signal redirect addresses } @@ -30,9 +33,11 @@ type Handler struct { func New( log kitlog.Logger, self refs.FeedRef, - sessiondb roomdb.AuthWithSSBService, + roomDomain string, membersdb roomdb.MembersService, - roomDomain string) Handler { + sessiondb roomdb.AuthWithSSBService, + bridge *signinwithssb.SignalBridge, +) Handler { var h Handler h.self = self @@ -40,6 +45,7 @@ func New( h.logger = log h.sessions = sessiondb h.members = membersdb + h.bridge = bridge return h } @@ -50,6 +56,11 @@ func (h Handler) SendSolution(ctx context.Context, req *muxrpc.Request) (interfa 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 @@ -67,18 +78,22 @@ func (h Handler) SendSolution(ctx context.Context, req *muxrpc.Request) (interfa sig, err := base64.StdEncoding.DecodeString(params[2]) if err != nil { - return nil, fmt.Errorf("sc is not valid base64 data: %w", err) + h.bridge.CompleteSession(sol.ServerChallenge, false, "") + return nil, fmt.Errorf("signature is not valid base64 data: %w", err) } if !sol.Validate(sig) { + h.bridge.CompleteSession(sol.ServerChallenge, false, "") return nil, fmt.Errorf("not a valid solution") } - // TODO: - // h.challenges.Solved(sc) - // return true, nil + tok, err := h.sessions.CreateToken(ctx, member.ID) + if err != nil { + return nil, err + } - return nil, fmt.Errorf("TODO: update SSE") + h.bridge.CompleteSession(sol.ServerChallenge, true, tok) + return true, nil } func (h Handler) InvalidateAllSolutions(ctx context.Context, req *muxrpc.Request) (interface{}, error) { diff --git a/roomsrv/init_handlers.go b/roomsrv/init_handlers.go index fa0dc21..3597300 100644 --- a/roomsrv/init_handlers.go +++ b/roomsrv/init_handlers.go @@ -5,7 +5,6 @@ package roomsrv import ( kitlog "github.com/go-kit/kit/log" "github.com/ssb-ngi-pointer/go-ssb-room/muxrpc/handlers/signinwithssb" - "github.com/ssb-ngi-pointer/go-ssb-room/roomdb" muxrpc "go.cryptoscope.co/muxrpc/v2" "go.cryptoscope.co/muxrpc/v2/typemux" @@ -15,7 +14,7 @@ import ( ) // 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()) @@ -28,16 +27,17 @@ 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.authWithSSB, - s.Members, s.domain, + s.Members, + s.authWithSSB, + s.authWithSSBBridge, ) // register muxrpc commands diff --git a/roomsrv/server.go b/roomsrv/server.go index 4508e87..cad3233 100644 --- a/roomsrv/server.go +++ b/roomsrv/server.go @@ -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" @@ -67,7 +68,8 @@ type Server struct { Members roomdb.MembersService Aliases roomdb.AliasesService - authWithSSB roomdb.AuthWithSSBService + authWithSSB roomdb.AuthWithSSBService + authWithSSBBridge *signinwithssb.SignalBridge } func (s Server) Whoami() refs.FeedRef { @@ -78,6 +80,7 @@ func New( membersdb roomdb.MembersService, aliasdb roomdb.AliasesService, awsdb roomdb.AuthWithSSBService, + bridge *signinwithssb.SignalBridge, domainName string, opts ...Option, ) (*Server, error) { @@ -88,6 +91,7 @@ func New( s.Aliases = aliasdb s.authWithSSB = awsdb + s.authWithSSBBridge = bridge s.domain = domainName @@ -148,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 diff --git a/web/assets/events-demo.js b/web/assets/events-demo.js index 6a4d50c..0ee4337 100644 --- a/web/assets/events-demo.js +++ b/web/assets/events-demo.js @@ -1,13 +1,23 @@ -let streamName = document.querySelector("#stream-name").attributes.stream.value +// get the challange from out of the HTML +let sc = document.querySelector("#challange").attributes.ch.value +var evtSource = new EventSource(`/sse/events?sc=${sc}`); -var evtSource = new EventSource(`/sse/events?stream=${streamName}`); +var ping = document.querySelector('#ping'); +var failed = document.querySelector('#failed'); -var eventList = document.querySelector('#event-list'); +evtSource.onerror = (e) => { + failed.textContent = "Warning: The connection to the server was interupted." +} -evtSource.addEventListener("testing", (e) => { -// console.log(e) +evtSource.addEventListener("ping", (e) => { + ping.textContent = e.data; +}) - var newElement = document.createElement("li"); - newElement.textContent = `(${e.lastEventId}) message: ${e.data}`; - eventList.prepend(newElement); -}) \ No newline at end of file +evtSource.addEventListener("failed", (e) => { + failed.textContent = e.data; +}) + +evtSource.addEventListener("success", (e) => { + console.log('trigger redirect!') + alert(e.data) +}) diff --git a/web/handlers/auth/withssb.go b/web/handlers/auth/withssb.go index 8afb9a6..be969e3 100644 --- a/web/handlers/auth/withssb.go +++ b/web/handlers/auth/withssb.go @@ -5,23 +5,25 @@ package auth import ( "context" "encoding/base64" - "encoding/binary" "encoding/gob" "fmt" + "html/template" + "image/color" + "io" "net/http" + "net/url" "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/randutil" "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" @@ -41,8 +43,12 @@ type WithSSBHandler struct { cookieStore sessions.Store endpoints network.Endpoints + + bridge *signinwithssb.SignalBridge } +type registerToEventSourceMap map[string]<-chan signinwithssb.Event + func NewWithSSBHandler( m *mux.Router, r *render.Renderer, @@ -52,6 +58,7 @@ func NewWithSSBHandler( membersDB roomdb.MembersService, sessiondb roomdb.AuthWithSSBService, cookies sessions.Store, + bridge *signinwithssb.SignalBridge, ) *WithSSBHandler { var ssb WithSSBHandler @@ -61,11 +68,11 @@ func NewWithSSBHandler( ssb.endpoints = endpoints ssb.sessiondb = sessiondb ssb.cookieStore = cookies + ssb.bridge = bridge m.Get(router.AuthWithSSBSignIn).HandlerFunc(r.HTML("auth/withssb_sign_in.tmpl", ssb.login)) m.HandleFunc("/sse/login/{sc}", r.HTML("auth/withssb_server_start.tmpl", ssb.startWithServer)) - m.HandleFunc("/sse/events", ssb.eventSource) return &ssb @@ -267,48 +274,41 @@ func (h WithSSBHandler) Logout(w http.ResponseWriter, r *http.Request) { // server-sent-events stuff func (h WithSSBHandler) startWithServer(w http.ResponseWriter, req *http.Request) (interface{}, error) { - logger := logging.FromContext(req.Context()) + sc := h.bridge.RegisterSession() - streamName := randutil.String(20) - // h.events.CreateStream(streamName) + var queryParams = make(url.Values) + queryParams.Set("action", "start-http-auth") - logger = level.Debug(logger) - logger = kitlog.With(logger, "event", streamName) + var startAuthURI url.URL + startAuthURI.Scheme = "ssb" + startAuthURI.Opaque = "experimental" + startAuthURI.RawQuery = queryParams.Encode() - logger.Log("event", "started stream") + qrCode, err := qrcode.New(startAuthURI.String(), qrcode.High) + if err != nil { + return nil, err + } + qrCode.BackgroundColor = color.RGBA{R: 0xf9, G: 0xfa, B: 0xfb} + qrCode.ForegroundColor = color.Black - // tick := time.NewTicker(5 * time.Second) - // go func() { + qrCodeData, err := qrCode.PNG(-8) + if err != nil { + return nil, err + } - // var ( - // evtBuf = make([]byte, 4) - // evtID = uint32(0) - // ) - // for range tick.C { - // binary.BigEndian.PutUint32(evtBuf, evtID) - // evtID++ - // h.events.Publish(streamName, &sse.Event{ - // ID: evtBuf, - // Data: []byte(fmt.Sprintf("boring: %d", evtID)), - // Event: []byte("testing"), - // }) - // logger.Log("event", "sent", "id", evtID) - // } - // }() - - // go func() { - // time.Sleep(1 * time.Minute) - // tick.Stop() - // logger.Log("event", "stopped") - // }() + qrURI := "data:image/png;base64," + base64.StdEncoding.EncodeToString(qrCodeData) return struct { - StreamName string - }{streamName}, nil + SSBURI template.URL + QRCodeURI template.URL + ServerChallenge string + }{template.URL(startAuthURI.String()), template.URL(qrURI), sc}, nil } type event struct { - ID, Data, Event []byte + ID uint32 + Data string + Event string } func (h WithSSBHandler) eventSource(w http.ResponseWriter, r *http.Request) { @@ -317,37 +317,40 @@ func (h WithSSBHandler) eventSource(w http.ResponseWriter, r *http.Request) { http.Error(w, "Streaming unsupported!", http.StatusInternalServerError) return } + notifier, ok := w.(http.CloseNotifier) + if !ok { + http.Error(w, "cant notify about closed requests", http.StatusInternalServerError) + return + } + // setup server-sent events + // https://html.spec.whatwg.org/multipage/server-sent-events.html 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") // Get the StreamID from the URL - streamID := r.URL.Query().Get("stream") - if streamID == "" { - http.Error(w, "Please specify a stream!", http.StatusInternalServerError) + sc := r.URL.Query().Get("sc") + if sc == "" { + http.Error(w, "Please specify a stream!", http.StatusBadRequest) return } + logger := logging.FromContext(r.Context()) logger = level.Debug(logger) - logger = kitlog.With(logger, "stream", streamID) - + logger = kitlog.With(logger, "stream", sc) logger.Log("event", "stream opened") - // TODO: load map with channel + evtCh, has := h.bridge.GetEventChannel(sc) + if !has { + http.Error(w, "No such session!", http.StatusBadRequest) + return + } - // tick := time.NewTicker(time.Second / 4) - tick := time.NewTicker(time.Second) - notify := w.(http.CloseNotifier).CloseNotify() + tick := time.NewTicker(3 * time.Second) go func() { - <-notify - tick.Stop() - logger.Log("event", "request closed") - }() - - go func() { - time.Sleep(5 * time.Minute) + time.Sleep(3 * time.Minute) tick.Stop() logger.Log("event", "stopped") }() @@ -357,32 +360,52 @@ func (h WithSSBHandler) eventSource(w http.ResponseWriter, r *http.Request) { // Push events to client var ( - evtBuf = make([]byte, 4) - evtID = uint32(0) - evt event + evtID = uint32(0) ) - for range tick.C { - binary.BigEndian.PutUint32(evtBuf, evtID) + notify := notifier.CloseNotify() + for { + select { + + case <-notify: + logger.Log("event", "request closed") + return + + case <-tick.C: + msg := fmt.Sprintf("Waiting for solution (session age: %s)", time.Since(start)) + sendServerEvent(w, event{ + ID: evtID, + Data: msg, + Event: "ping", + }) + logger.Log("event", "sent", "ping", evtID) + + case update := <-evtCh: + evt := event{ + ID: evtID, + Data: "challange validation failed", + Event: "failed", + } + + if update.Worked { + evt.Event = "success" + evt.Data = update.Token + } + + sendServerEvent(w, evt) + + logger.Log("event", "sent", "worked", update.Worked) + } evtID++ - - evt = event{ - ID: []byte(fmt.Sprintf("%d", evtID)), - Data: []byte(fmt.Sprintf("age: %s", time.Since(start))), - Event: []byte("testing"), - } - - fmt.Fprintf(w, "id: %s\n", evt.ID) - fmt.Fprintf(w, "data: %s\n", evt.Data) - if len(evt.Event) > 0 { - fmt.Fprintf(w, "event: %s\n", evt.Event) - } - // if len(evt.Retry) > 0 { - // fmt.Fprintf(w, "retry: %s\n", evt.Retry) - // } - fmt.Fprint(w, "\n") flusher.Flush() - - logger.Log("event", "sent", "id", evtID) } } + +func sendServerEvent(w io.Writer, evt event) { + fmt.Fprintf(w, "id: %d\n", evt.ID) + fmt.Fprintf(w, "data: %s\n", evt.Data) + if len(evt.Event) > 0 { + fmt.Fprintf(w, "event: %s\n", evt.Event) + } + fmt.Fprint(w, "\n") +} diff --git a/web/handlers/http.go b/web/handlers/http.go index c8a74b1..c7b746c 100644 --- a/web/handlers/http.go +++ b/web/handlers/http.go @@ -20,6 +20,7 @@ import ( "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" @@ -71,6 +72,7 @@ func New( netInfo NetworkInfo, roomState *roomstate.Manager, roomEndpoints network.Endpoints, + bridge *signinwithssb.SignalBridge, dbs Databases, ) (http.Handler, error) { m := router.CompleteApp() @@ -237,6 +239,7 @@ func New( dbs.Members, dbs.AuthWithSSB, cookieStore, + bridge, ) // just hooks up the router to the handler diff --git a/web/members/helper.go b/web/members/helper.go index f20c96f..0dd66a7 100644 --- a/web/members/helper.go +++ b/web/members/helper.go @@ -25,7 +25,7 @@ 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.ErrBadRequest{}) + r.Error(w, req, http.StatusUnauthorized, weberrors.ErrNotAuthorized) return } next.ServeHTTP(w, req) diff --git a/web/templates/auth/withssb_server_start.tmpl b/web/templates/auth/withssb_server_start.tmpl index 61ec7a9..1fa829b 100644 --- a/web/templates/auth/withssb_server_start.tmpl +++ b/web/templates/auth/withssb_server_start.tmpl @@ -4,12 +4,14 @@

{{i18n "AuthWithSSBWelcome"}}

-

Template Data

-
{{.}}
+

TODO: qr code of the code

+ QR-Code to pass challange to an App +
{{.SSBURI}}

Server events

- +

+

-
+
{{end}} \ No newline at end of file