implement bridge between muxrcp and http

This commit is contained in:
Henry 2021-03-24 16:52:58 +01:00
parent afa6bee285
commit 396961e5e8
13 changed files with 298 additions and 104 deletions

View File

@ -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,

1
go.mod
View File

@ -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

2
go.sum
View File

@ -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=

View File

@ -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
}

View File

@ -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")
}
}

View File

@ -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, &params); 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) {

View File

@ -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

View File

@ -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

View File

@ -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);
})
evtSource.addEventListener("failed", (e) => {
failed.textContent = e.data;
})
evtSource.addEventListener("success", (e) => {
console.log('trigger redirect!')
alert(e.data)
})

View File

@ -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")
}

View File

@ -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

View File

@ -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)

View File

@ -4,12 +4,14 @@
<h1 id="welcome" class="text-lg">{{i18n "AuthWithSSBWelcome"}}</h1>
</div>
<div>
<h3>Template Data</h3>
<pre>{{.}}</pre>
<h3>TODO: qr code of the code</h3>
<img src="{{.QRCodeURI}}" alt="QR-Code to pass challange to an App" />
<pre>{{.SSBURI}}</pre>
<h3>Server events</h3>
<ul id="event-list"></ul>
<p id="ping"></p>
<p id="failed" class="text-red-500"></p>
</div>
<div id="stream-name" stream="{{.StreamName}}"></div>
<div id="challange" ch="{{.ServerChallenge}}"></div>
<script src="/assets/events-demo.js"></script>
{{end}}