2021-03-17 09:46:05 +00:00
|
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
|
|
|
|
package auth
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"encoding/base64"
|
|
|
|
"encoding/gob"
|
2021-04-01 07:04:38 +00:00
|
|
|
"errors"
|
2021-03-17 09:46:05 +00:00
|
|
|
"fmt"
|
2021-03-24 15:52:58 +00:00
|
|
|
"html/template"
|
|
|
|
"image/color"
|
|
|
|
"io"
|
2021-03-17 09:46:05 +00:00
|
|
|
"net/http"
|
2021-03-24 15:52:58 +00:00
|
|
|
"net/url"
|
2021-03-24 17:31:37 +00:00
|
|
|
"strings"
|
2021-03-17 09:46:05 +00:00
|
|
|
"time"
|
|
|
|
|
2021-03-24 10:39:08 +00:00
|
|
|
kitlog "github.com/go-kit/kit/log"
|
|
|
|
"github.com/go-kit/kit/log/level"
|
2021-03-17 09:46:05 +00:00
|
|
|
"github.com/gorilla/mux"
|
|
|
|
"github.com/gorilla/sessions"
|
2021-03-24 15:52:58 +00:00
|
|
|
"github.com/skip2/go-qrcode"
|
2021-03-17 09:46:05 +00:00
|
|
|
"go.cryptoscope.co/muxrpc/v2"
|
|
|
|
"go.mindeco.de/http/render"
|
2021-03-24 10:39:08 +00:00
|
|
|
"go.mindeco.de/logging"
|
2021-03-17 09:46:05 +00:00
|
|
|
|
|
|
|
"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"
|
|
|
|
)
|
|
|
|
|
2021-03-26 08:47:28 +00:00
|
|
|
var HTMLTemplates = []string{
|
2021-03-26 15:30:57 +00:00
|
|
|
"auth/decide_method.tmpl",
|
2021-03-26 19:08:13 +00:00
|
|
|
"auth/fallback_sign_in.tmpl",
|
2021-03-26 08:47:28 +00:00
|
|
|
"auth/withssb_server_start.tmpl",
|
|
|
|
}
|
|
|
|
|
2021-03-25 17:38:21 +00:00
|
|
|
// 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
|
|
|
|
|
2021-03-17 09:46:05 +00:00
|
|
|
// WithSSBHandler implements the oauth-like challenge/response dance described in
|
2021-03-25 17:38:21 +00:00
|
|
|
// https://ssb-ngi-pointer.github.io/ssb-http-auth-spec
|
2021-03-17 09:46:05 +00:00
|
|
|
type WithSSBHandler struct {
|
2021-03-25 17:38:21 +00:00
|
|
|
render *render.Renderer
|
|
|
|
|
2021-03-29 10:23:11 +00:00
|
|
|
// roomID refs.FeedRef
|
|
|
|
// muxrpcHostAndPort string
|
|
|
|
|
|
|
|
netInfo network.ServerEndpointDetails
|
2021-03-17 09:46:05 +00:00
|
|
|
|
|
|
|
membersdb roomdb.MembersService
|
|
|
|
aliasesdb roomdb.AliasesService
|
|
|
|
sessiondb roomdb.AuthWithSSBService
|
|
|
|
|
|
|
|
cookieStore sessions.Store
|
|
|
|
|
|
|
|
endpoints network.Endpoints
|
2021-03-24 15:52:58 +00:00
|
|
|
|
|
|
|
bridge *signinwithssb.SignalBridge
|
2021-03-17 09:46:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func NewWithSSBHandler(
|
|
|
|
m *mux.Router,
|
|
|
|
r *render.Renderer,
|
2021-03-29 10:23:11 +00:00
|
|
|
netInfo network.ServerEndpointDetails,
|
2021-03-17 09:46:05 +00:00
|
|
|
endpoints network.Endpoints,
|
|
|
|
aliasDB roomdb.AliasesService,
|
|
|
|
membersDB roomdb.MembersService,
|
|
|
|
sessiondb roomdb.AuthWithSSBService,
|
|
|
|
cookies sessions.Store,
|
2021-03-24 15:52:58 +00:00
|
|
|
bridge *signinwithssb.SignalBridge,
|
2021-03-17 09:46:05 +00:00
|
|
|
) *WithSSBHandler {
|
|
|
|
|
|
|
|
var ssb WithSSBHandler
|
2021-03-25 17:38:21 +00:00
|
|
|
ssb.render = r
|
2021-03-29 10:23:11 +00:00
|
|
|
ssb.netInfo = netInfo
|
2021-03-17 09:46:05 +00:00
|
|
|
ssb.aliasesdb = aliasDB
|
|
|
|
ssb.membersdb = membersDB
|
|
|
|
ssb.endpoints = endpoints
|
|
|
|
ssb.sessiondb = sessiondb
|
|
|
|
ssb.cookieStore = cookies
|
2021-03-24 15:52:58 +00:00
|
|
|
ssb.bridge = bridge
|
2021-03-17 09:46:05 +00:00
|
|
|
|
2021-04-09 10:44:12 +00:00
|
|
|
m.Get(router.AuthWithSSBLogin).HandlerFunc(ssb.DecideMethod)
|
2021-03-26 10:50:16 +00:00
|
|
|
m.Get(router.AuthWithSSBServerEvents).HandlerFunc(ssb.eventSource)
|
|
|
|
m.Get(router.AuthWithSSBFinalize).HandlerFunc(ssb.finalizeCookie)
|
2021-03-24 10:39:08 +00:00
|
|
|
|
2021-03-17 09:46:05 +00:00
|
|
|
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.
|
2021-03-25 17:38:21 +00:00
|
|
|
// Otherwise it will return the member that belongs to the session.
|
2021-03-17 09:46:05 +00:00
|
|
|
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.
|
2021-03-24 17:31:37 +00:00
|
|
|
func (h WithSSBHandler) Logout(w http.ResponseWriter, r *http.Request) error {
|
2021-03-17 09:46:05 +00:00
|
|
|
session, err := h.cookieStore.Get(r, siwssbSessionName)
|
|
|
|
if err != nil {
|
2021-03-24 17:31:37 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
tokenVal, ok := session.Values[memberToken]
|
|
|
|
if !ok {
|
2021-03-25 17:38:21 +00:00
|
|
|
// not a ssb http auth session
|
2021-03-24 17:31:37 +00:00
|
|
|
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
|
2021-03-17 09:46:05 +00:00
|
|
|
}
|
|
|
|
|
2021-03-25 17:38:21 +00:00
|
|
|
session.Values[userTimeout] = time.Now().Add(-sessionLifetime)
|
2021-03-17 09:46:05 +00:00
|
|
|
session.Options.MaxAge = -1
|
|
|
|
if err := session.Save(r, w); err != nil {
|
2021-03-24 17:31:37 +00:00
|
|
|
return err
|
2021-03-17 09:46:05 +00:00
|
|
|
}
|
|
|
|
|
2021-03-24 17:31:37 +00:00
|
|
|
return nil
|
2021-03-17 09:46:05 +00:00
|
|
|
}
|
2021-03-24 10:39:08 +00:00
|
|
|
|
2021-03-25 17:38:21 +00:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2021-04-09 10:44:12 +00:00
|
|
|
// 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) {
|
2021-03-25 17:38:21 +00:00
|
|
|
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
|
2021-03-26 10:50:16 +00:00
|
|
|
|
|
|
|
_, err := h.membersdb.GetByFeed(req.Context(), *cid)
|
|
|
|
if err != nil {
|
2021-04-01 07:04:38 +00:00
|
|
|
if errors.Is(err, roomdb.ErrNotFound) {
|
|
|
|
h.render.Error(w, req, http.StatusForbidden, weberrors.ErrForbidden{Details: err})
|
2021-03-26 10:50:16 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
h.render.Error(w, req, http.StatusInternalServerError, err)
|
|
|
|
return
|
|
|
|
}
|
2021-03-25 17:38:21 +00:00
|
|
|
}
|
|
|
|
} 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
|
|
|
|
}
|
|
|
|
|
2021-03-26 10:50:16 +00:00
|
|
|
// 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)
|
2021-03-25 17:38:21 +00:00
|
|
|
}
|
|
|
|
|
2021-04-09 10:44:12 +00:00
|
|
|
// clientInitiated is called with a client challange (?cc=123) and calls back to
|
|
|
|
// the passed client using muxrpc to request a signed solution.
|
2021-03-25 17:38:21 +00:00
|
|
|
// 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()
|
|
|
|
|
2021-03-26 16:58:03 +00:00
|
|
|
var payload signinwithssb.ClientPayload
|
2021-03-29 10:23:11 +00:00
|
|
|
payload.ServerID = h.netInfo.RoomID // fill in the server
|
2021-03-25 17:38:21 +00:00
|
|
|
|
|
|
|
// validate and update client challenge
|
|
|
|
cc := queryParams.Get("cc")
|
2021-03-26 16:58:03 +00:00
|
|
|
payload.ClientChallenge = cc
|
2021-03-25 17:38:21 +00:00
|
|
|
|
|
|
|
// check that we have that member
|
|
|
|
member, err := h.membersdb.GetByFeed(req.Context(), client)
|
|
|
|
if err != nil {
|
2021-04-01 07:04:38 +00:00
|
|
|
if errors.Is(err, roomdb.ErrNotFound) {
|
|
|
|
errMsg := fmt.Errorf("ssb http auth: client isn't a member: %w", err)
|
2021-03-25 17:38:21 +00:00
|
|
|
return weberrors.ErrForbidden{Details: errMsg}
|
|
|
|
}
|
2021-04-01 07:04:38 +00:00
|
|
|
return err
|
2021-03-25 17:38:21 +00:00
|
|
|
}
|
2021-03-26 16:58:03 +00:00
|
|
|
payload.ClientID = client
|
2021-03-25 17:38:21 +00:00
|
|
|
|
|
|
|
// 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()
|
2021-03-26 16:58:03 +00:00
|
|
|
payload.ServerChallenge = sc
|
2021-03-25 17:38:21 +00:00
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2021-03-26 16:58:03 +00:00
|
|
|
if !payload.Validate(solutionBytes) {
|
2021-03-25 17:38:21 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2021-03-24 10:39:08 +00:00
|
|
|
// server-sent-events stuff
|
|
|
|
|
2021-03-26 10:50:16 +00:00
|
|
|
type templateData struct {
|
|
|
|
SSBURI template.URL
|
|
|
|
QRCodeURI template.URL
|
|
|
|
ServerChallenge string
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h WithSSBHandler) serverInitiated() (templateData, error) {
|
2021-03-24 15:52:58 +00:00
|
|
|
sc := h.bridge.RegisterSession()
|
2021-03-24 10:39:08 +00:00
|
|
|
|
2021-03-25 17:38:21 +00:00
|
|
|
// prepare the ssb-uri
|
|
|
|
// https://ssb-ngi-pointer.github.io/ssb-http-auth-spec/#list-of-new-ssb-uris
|
2021-03-24 15:52:58 +00:00
|
|
|
var queryParams = make(url.Values)
|
|
|
|
queryParams.Set("action", "start-http-auth")
|
2021-03-29 10:23:11 +00:00
|
|
|
queryParams.Set("sid", h.netInfo.RoomID.Ref())
|
2021-03-24 17:31:37 +00:00
|
|
|
queryParams.Set("sc", sc)
|
2021-03-29 10:23:11 +00:00
|
|
|
queryParams.Set("multiserverAddress", h.netInfo.MultiserverAddress())
|
2021-03-24 10:39:08 +00:00
|
|
|
|
2021-03-24 15:52:58 +00:00
|
|
|
var startAuthURI url.URL
|
|
|
|
startAuthURI.Scheme = "ssb"
|
|
|
|
startAuthURI.Opaque = "experimental"
|
|
|
|
startAuthURI.RawQuery = queryParams.Encode()
|
|
|
|
|
2021-03-24 17:31:37 +00:00
|
|
|
// 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)
|
2021-03-24 15:52:58 +00:00
|
|
|
if err != nil {
|
2021-03-26 10:50:16 +00:00
|
|
|
return templateData{}, err
|
2021-03-24 15:52:58 +00:00
|
|
|
}
|
2021-03-25 17:38:21 +00:00
|
|
|
|
|
|
|
qrCode.BackgroundColor = color.Transparent // transparent to fit into the page
|
2021-03-24 15:52:58 +00:00
|
|
|
qrCode.ForegroundColor = color.Black
|
|
|
|
|
2021-03-24 17:31:37 +00:00
|
|
|
qrCodeData, err := qrCode.PNG(-5)
|
2021-03-24 15:52:58 +00:00
|
|
|
if err != nil {
|
2021-03-26 10:50:16 +00:00
|
|
|
return templateData{}, err
|
2021-03-24 15:52:58 +00:00
|
|
|
}
|
|
|
|
qrURI := "data:image/png;base64," + base64.StdEncoding.EncodeToString(qrCodeData)
|
2021-03-24 10:39:08 +00:00
|
|
|
|
2021-03-25 17:38:21 +00:00
|
|
|
// template.URL signals the template engine that those aren't fishy and from a trusted source
|
2021-03-26 10:50:16 +00:00
|
|
|
|
2021-03-25 17:38:21 +00:00
|
|
|
data := templateData{
|
|
|
|
SSBURI: template.URL(startAuthURI.String()),
|
|
|
|
QRCodeURI: template.URL(qrURI),
|
|
|
|
|
|
|
|
ServerChallenge: sc,
|
|
|
|
}
|
|
|
|
return data, nil
|
2021-03-24 10:39:08 +00:00
|
|
|
}
|
|
|
|
|
2021-03-25 17:38:21 +00:00
|
|
|
// 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 {
|
2021-03-26 10:50:16 +00:00
|
|
|
http.Error(w, "invalid session token", http.StatusForbidden)
|
2021-03-25 17:38:21 +00:00
|
|
|
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)
|
2021-03-24 10:39:08 +00:00
|
|
|
}
|
|
|
|
|
2021-03-26 10:50:16 +00:00
|
|
|
// the time after which the SSE dance is considered failed
|
|
|
|
const sseTimeout = 3 * time.Minute
|
|
|
|
|
2021-03-25 17:38:21 +00:00
|
|
|
// eventSource is the server-side of our server-sent events (SSE) session
|
|
|
|
// https://html.spec.whatwg.org/multipage/server-sent-events.html
|
2021-03-24 10:39:08 +00:00
|
|
|
func (h WithSSBHandler) eventSource(w http.ResponseWriter, r *http.Request) {
|
|
|
|
flusher, err := w.(http.Flusher)
|
|
|
|
if !err {
|
2021-03-25 17:38:21 +00:00
|
|
|
http.Error(w, "ssb http auth: server-initiated method needs streaming support", http.StatusInternalServerError)
|
2021-03-24 10:39:08 +00:00
|
|
|
return
|
|
|
|
}
|
2021-03-26 10:50:16 +00:00
|
|
|
|
|
|
|
// closes when the http request is closed
|
|
|
|
var notify <-chan bool
|
|
|
|
|
2021-03-24 15:52:58 +00:00
|
|
|
notifier, ok := w.(http.CloseNotifier)
|
|
|
|
if !ok {
|
2021-03-26 10:50:16 +00:00
|
|
|
// 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()
|
2021-03-24 15:52:58 +00:00
|
|
|
}
|
2021-03-24 10:39:08 +00:00
|
|
|
|
2021-03-25 17:38:21 +00:00
|
|
|
// setup headers for SSE
|
2021-03-24 10:39:08 +00:00
|
|
|
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")
|
|
|
|
|
2021-03-24 15:52:58 +00:00
|
|
|
sc := r.URL.Query().Get("sc")
|
|
|
|
if sc == "" {
|
2021-03-25 17:38:21 +00:00
|
|
|
http.Error(w, "missing server challenge", http.StatusBadRequest)
|
2021-03-24 10:39:08 +00:00
|
|
|
return
|
|
|
|
}
|
2021-03-24 15:52:58 +00:00
|
|
|
|
2021-03-24 10:39:08 +00:00
|
|
|
logger := logging.FromContext(r.Context())
|
|
|
|
logger = level.Debug(logger)
|
2021-03-25 17:38:21 +00:00
|
|
|
logger = kitlog.With(logger, "stream", sc[:5])
|
2021-03-24 10:39:08 +00:00
|
|
|
logger.Log("event", "stream opened")
|
|
|
|
|
2021-03-24 15:52:58 +00:00
|
|
|
evtCh, has := h.bridge.GetEventChannel(sc)
|
|
|
|
if !has {
|
2021-03-25 17:38:21 +00:00
|
|
|
http.Error(w, "no such session!", http.StatusBadRequest)
|
2021-03-24 15:52:58 +00:00
|
|
|
return
|
|
|
|
}
|
2021-03-24 10:39:08 +00:00
|
|
|
|
2021-03-25 17:38:21 +00:00
|
|
|
sender := newEventSender(w)
|
|
|
|
|
|
|
|
// ping ticker
|
2021-03-24 15:52:58 +00:00
|
|
|
tick := time.NewTicker(3 * time.Second)
|
2021-03-24 10:39:08 +00:00
|
|
|
go func() {
|
2021-03-26 10:50:16 +00:00
|
|
|
time.Sleep(sseTimeout)
|
2021-03-24 10:39:08 +00:00
|
|
|
tick.Stop()
|
|
|
|
logger.Log("event", "stopped")
|
|
|
|
}()
|
|
|
|
|
|
|
|
start := time.Now()
|
|
|
|
flusher.Flush()
|
|
|
|
|
|
|
|
// Push events to client
|
2021-03-26 10:50:16 +00:00
|
|
|
|
2021-03-24 15:52:58 +00:00
|
|
|
for {
|
|
|
|
select {
|
2021-03-24 10:39:08 +00:00
|
|
|
|
2021-03-24 15:52:58 +00:00
|
|
|
case <-notify:
|
|
|
|
logger.Log("event", "request closed")
|
|
|
|
return
|
2021-03-24 10:39:08 +00:00
|
|
|
|
2021-03-24 15:52:58 +00:00
|
|
|
case <-tick.C:
|
2021-03-25 17:38:21 +00:00
|
|
|
sender.send("ping", fmt.Sprintf("Waiting for solution (session age: %s)", time.Since(start)))
|
|
|
|
logger.Log("event", "sent ping")
|
2021-03-24 15:52:58 +00:00
|
|
|
|
|
|
|
case update := <-evtCh:
|
2021-03-25 17:38:21 +00:00
|
|
|
var event, data string = "failed", "challenge validation failed"
|
2021-03-24 15:52:58 +00:00
|
|
|
|
|
|
|
if update.Worked {
|
2021-03-25 17:38:21 +00:00
|
|
|
event = "success"
|
|
|
|
data = update.Token
|
|
|
|
} else {
|
|
|
|
if update.Reason != nil {
|
|
|
|
data = update.Reason.Error()
|
|
|
|
}
|
2021-03-24 15:52:58 +00:00
|
|
|
}
|
|
|
|
|
2021-03-25 17:38:21 +00:00
|
|
|
sender.send(event, data)
|
2021-03-24 15:52:58 +00:00
|
|
|
logger.Log("event", "sent", "worked", update.Worked)
|
2021-03-25 14:17:30 +00:00
|
|
|
return
|
2021-03-24 10:39:08 +00:00
|
|
|
}
|
2021-03-25 17:38:21 +00:00
|
|
|
|
2021-03-24 10:39:08 +00:00
|
|
|
flusher.Flush()
|
2021-03-24 15:52:58 +00:00
|
|
|
}
|
|
|
|
}
|
2021-03-24 10:39:08 +00:00
|
|
|
|
2021-03-25 17:38:21 +00:00
|
|
|
// eventSender encapsulates the event ID and increases it with each send automatically
|
|
|
|
type eventSender struct {
|
|
|
|
w io.Writer
|
2021-03-25 14:17:30 +00:00
|
|
|
|
2021-03-25 17:38:21 +00:00
|
|
|
id uint32
|
|
|
|
}
|
2021-03-25 14:17:30 +00:00
|
|
|
|
2021-03-25 17:38:21 +00:00
|
|
|
func newEventSender(w io.Writer) eventSender {
|
|
|
|
return eventSender{w: w}
|
|
|
|
}
|
2021-03-25 14:17:30 +00:00
|
|
|
|
2021-03-25 17:38:21 +00:00
|
|
|
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++
|
2021-03-25 14:17:30 +00:00
|
|
|
}
|