2021-03-04 14:09:14 +00:00
|
|
|
package handlers
|
|
|
|
|
|
|
|
import (
|
2021-03-30 15:04:07 +00:00
|
|
|
"encoding/base64"
|
2021-03-26 19:08:13 +00:00
|
|
|
"encoding/json"
|
2021-03-04 14:09:14 +00:00
|
|
|
"errors"
|
2021-03-09 17:27:44 +00:00
|
|
|
"fmt"
|
2021-03-26 19:08:13 +00:00
|
|
|
"html/template"
|
2021-03-30 15:04:07 +00:00
|
|
|
"image/color"
|
2021-03-04 14:09:14 +00:00
|
|
|
"net/http"
|
2021-03-26 19:08:13 +00:00
|
|
|
"net/url"
|
2021-03-04 14:09:14 +00:00
|
|
|
|
2021-03-05 10:15:36 +00:00
|
|
|
"github.com/go-kit/kit/log/level"
|
2021-03-04 14:09:14 +00:00
|
|
|
"github.com/gorilla/csrf"
|
2021-03-30 15:04:07 +00:00
|
|
|
"github.com/skip2/go-qrcode"
|
2021-03-26 19:08:13 +00:00
|
|
|
"go.mindeco.de/http/render"
|
|
|
|
"go.mindeco.de/logging"
|
|
|
|
|
|
|
|
"github.com/ssb-ngi-pointer/go-ssb-room/internal/network"
|
2021-03-10 15:44:46 +00:00
|
|
|
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
|
2021-03-26 19:08:13 +00:00
|
|
|
"github.com/ssb-ngi-pointer/go-ssb-room/web"
|
2021-03-04 14:09:14 +00:00
|
|
|
weberrors "github.com/ssb-ngi-pointer/go-ssb-room/web/errors"
|
2021-03-26 19:08:13 +00:00
|
|
|
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
|
2021-03-04 14:09:14 +00:00
|
|
|
refs "go.mindeco.de/ssb-refs"
|
|
|
|
)
|
|
|
|
|
|
|
|
type inviteHandler struct {
|
2021-04-19 06:28:45 +00:00
|
|
|
render *render.Renderer
|
|
|
|
urlTo web.URLMaker
|
|
|
|
networkInfo network.ServerEndpointDetails
|
2021-03-26 19:08:13 +00:00
|
|
|
|
2021-03-30 15:04:07 +00:00
|
|
|
invites roomdb.InvitesService
|
|
|
|
pinnedNotices roomdb.PinnedNoticesService
|
2021-04-06 15:21:41 +00:00
|
|
|
config roomdb.RoomConfig
|
2021-04-07 09:08:43 +00:00
|
|
|
deniedKeys roomdb.DeniedKeysService
|
2021-03-04 14:09:14 +00:00
|
|
|
}
|
|
|
|
|
2021-04-19 06:28:45 +00:00
|
|
|
func (h inviteHandler) buildJoinRoomURI(token string) template.URL {
|
2021-03-26 19:08:13 +00:00
|
|
|
var joinRoomURI url.URL
|
|
|
|
joinRoomURI.Scheme = "ssb"
|
|
|
|
joinRoomURI.Opaque = "experimental"
|
|
|
|
|
|
|
|
queryVals := make(url.Values)
|
2021-04-22 08:37:57 +00:00
|
|
|
queryVals.Set("action", "claim-http-invite")
|
2021-03-26 19:08:13 +00:00
|
|
|
queryVals.Set("invite", token)
|
|
|
|
|
2021-04-19 06:28:45 +00:00
|
|
|
submissionURL := h.urlTo(router.CompleteInviteConsume)
|
2021-03-26 19:08:13 +00:00
|
|
|
queryVals.Set("postTo", submissionURL.String())
|
|
|
|
|
|
|
|
joinRoomURI.RawQuery = queryVals.Encode()
|
|
|
|
|
2021-04-06 10:12:11 +00:00
|
|
|
return template.URL(joinRoomURI.String())
|
2021-03-30 15:04:07 +00:00
|
|
|
}
|
|
|
|
|
2021-04-22 17:21:23 +00:00
|
|
|
// switch between JSON and HTML responses
|
|
|
|
func (h inviteHandler) presentFacade(rw http.ResponseWriter, req *http.Request) {
|
|
|
|
enc := req.URL.Query().Get("encoding")
|
|
|
|
if enc == "json" {
|
|
|
|
h.presentFacadeAsJSON(rw, req)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
h.render.HTML("invite/facade.tmpl", h.presentFacadeAsHTML)(rw, req)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h inviteHandler) presentFacadeAsJSON(rw http.ResponseWriter, req *http.Request) {
|
|
|
|
logger := logging.FromContext(req.Context())
|
|
|
|
|
|
|
|
rw.Header().Set("Content-Type", "application/json")
|
|
|
|
rw.WriteHeader(http.StatusOK)
|
|
|
|
|
|
|
|
enc := json.NewEncoder(rw)
|
|
|
|
|
|
|
|
// make sure token is still valid
|
|
|
|
token := req.URL.Query().Get("token")
|
|
|
|
_, err := h.invites.GetByToken(req.Context(), token)
|
|
|
|
if err != nil {
|
|
|
|
// send a json error back
|
|
|
|
data := struct {
|
|
|
|
Status string `json:"status"`
|
|
|
|
Error string `json:"error"`
|
|
|
|
}{"failed", err.Error()}
|
|
|
|
if err := enc.Encode(data); err != nil {
|
|
|
|
level.Warn(logger).Log("event", "sending json error failed", "err", err)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// send them on to the next step
|
|
|
|
postTo := h.urlTo(router.CompleteInviteConsume)
|
|
|
|
data := struct {
|
|
|
|
Status string `json:"status"`
|
|
|
|
Invite string `json:"invite"`
|
|
|
|
PostTo string `json:"postTo"`
|
|
|
|
}{"success", token, postTo.String()}
|
|
|
|
if err := enc.Encode(data); err != nil {
|
|
|
|
level.Warn(logger).Log("event", "sending json response failed", "err", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h inviteHandler) presentFacadeAsHTML(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
|
2021-03-30 15:04:07 +00:00
|
|
|
token := req.URL.Query().Get("token")
|
|
|
|
|
|
|
|
_, err := h.invites.GetByToken(req.Context(), token)
|
|
|
|
if err != nil {
|
|
|
|
if errors.Is(err, roomdb.ErrNotFound) {
|
|
|
|
return nil, weberrors.ErrNotFound{What: "invite"}
|
|
|
|
}
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
notice, err := h.pinnedNotices.Get(req.Context(), roomdb.NoticeDescription, "en-GB")
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to find room's description: %w", err)
|
|
|
|
}
|
|
|
|
|
2021-04-19 06:28:45 +00:00
|
|
|
joinRoomURI := h.buildJoinRoomURI(token)
|
2021-03-30 15:04:07 +00:00
|
|
|
|
2021-04-19 06:28:45 +00:00
|
|
|
fallbackURL := h.urlTo(router.CompleteInviteFacadeFallback, "token", token)
|
2021-03-30 15:04:07 +00:00
|
|
|
|
|
|
|
// generate a QR code with the token inside so that you can open it easily in a supporting mobile app
|
2021-04-14 13:10:25 +00:00
|
|
|
thisURL := req.URL
|
|
|
|
thisURL.Host = h.networkInfo.Domain
|
|
|
|
thisURL.Scheme = "https"
|
|
|
|
if h.networkInfo.Development {
|
|
|
|
thisURL.Scheme = "http"
|
|
|
|
thisURL.Host += fmt.Sprintf(":%d", h.networkInfo.PortHTTPS)
|
|
|
|
}
|
|
|
|
qrCode, err := qrcode.New(thisURL.String(), qrcode.Medium)
|
2021-03-30 15:04:07 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
qrCode.BackgroundColor = color.Transparent // transparent to fit into the page
|
|
|
|
qrCode.ForegroundColor = color.Black
|
|
|
|
|
|
|
|
qrCodeData, err := qrCode.PNG(-5)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
qrURI := "data:image/png;base64," + base64.StdEncoding.EncodeToString(qrCodeData)
|
|
|
|
|
|
|
|
return map[string]interface{}{
|
|
|
|
csrf.TemplateTag: csrf.TemplateField(req),
|
|
|
|
"RoomTitle": notice.Title,
|
2021-04-06 10:12:11 +00:00
|
|
|
"JoinRoomURI": joinRoomURI,
|
2021-03-30 15:04:07 +00:00
|
|
|
"FallbackURL": fallbackURL,
|
|
|
|
"QRCodeURI": template.URL(qrURI),
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h inviteHandler) presentFacadeFallback(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
|
|
token := req.URL.Query().Get("token")
|
|
|
|
|
|
|
|
_, err := h.invites.GetByToken(req.Context(), token)
|
|
|
|
if err != nil {
|
|
|
|
if errors.Is(err, roomdb.ErrNotFound) {
|
|
|
|
return nil, weberrors.ErrNotFound{What: "invite"}
|
|
|
|
}
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2021-04-19 06:28:45 +00:00
|
|
|
insertURL := h.urlTo(router.CompleteInviteInsertID, "token", token)
|
2021-03-30 15:04:07 +00:00
|
|
|
|
2021-03-04 14:09:14 +00:00
|
|
|
return map[string]interface{}{
|
2021-03-26 19:08:13 +00:00
|
|
|
csrf.TemplateTag: csrf.TemplateField(req),
|
2021-03-30 15:04:07 +00:00
|
|
|
"InsertURL": insertURL,
|
|
|
|
}, nil
|
|
|
|
}
|
2021-03-26 19:08:13 +00:00
|
|
|
|
2021-03-30 15:04:07 +00:00
|
|
|
func (h inviteHandler) presentInsert(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
|
|
token := req.URL.Query().Get("token")
|
2021-03-05 10:15:36 +00:00
|
|
|
|
2021-03-30 15:04:07 +00:00
|
|
|
_, err := h.invites.GetByToken(req.Context(), token)
|
|
|
|
if err != nil {
|
|
|
|
if errors.Is(err, roomdb.ErrNotFound) {
|
|
|
|
return nil, weberrors.ErrNotFound{What: "invite"}
|
|
|
|
}
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return map[string]interface{}{
|
|
|
|
csrf.TemplateTag: csrf.TemplateField(req),
|
|
|
|
"Token": token,
|
2021-03-04 14:09:14 +00:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2021-03-26 19:08:13 +00:00
|
|
|
type inviteConsumePayload struct {
|
|
|
|
ID refs.FeedRef `json:"id"`
|
|
|
|
Invite string `json:"invite"`
|
|
|
|
}
|
2021-03-04 14:09:14 +00:00
|
|
|
|
2021-03-26 19:08:13 +00:00
|
|
|
func (h inviteHandler) consume(rw http.ResponseWriter, req *http.Request) {
|
|
|
|
logger := logging.FromContext(req.Context())
|
2021-03-04 14:09:14 +00:00
|
|
|
|
2021-03-26 19:08:13 +00:00
|
|
|
var (
|
|
|
|
token string
|
|
|
|
newMember refs.FeedRef
|
2021-04-07 09:08:43 +00:00
|
|
|
resp inviteConsumeResponder
|
2021-03-26 19:08:13 +00:00
|
|
|
)
|
2021-04-07 09:08:43 +00:00
|
|
|
|
2021-03-26 19:08:13 +00:00
|
|
|
ct := req.Header.Get("Content-Type")
|
|
|
|
switch ct {
|
|
|
|
case "application/json":
|
|
|
|
resp = newinviteConsumeJSONResponder(rw)
|
|
|
|
|
|
|
|
var body inviteConsumePayload
|
|
|
|
|
|
|
|
level.Debug(logger).Log("event", "handling json body")
|
|
|
|
err := json.NewDecoder(req.Body).Decode(&body)
|
|
|
|
if err != nil {
|
|
|
|
err = fmt.Errorf("consume body contained invalid json: %w", err)
|
|
|
|
resp.SendError(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
newMember = body.ID
|
|
|
|
token = body.Invite
|
|
|
|
case "application/x-www-form-urlencoded":
|
|
|
|
resp = newinviteConsumeHTMLResponder(h.render, rw, req)
|
|
|
|
|
|
|
|
if err := req.ParseForm(); err != nil {
|
|
|
|
err = weberrors.ErrBadRequest{Where: "form data", Details: err}
|
|
|
|
resp.SendError(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
token = req.FormValue("invite")
|
|
|
|
|
|
|
|
parsedID, err := refs.ParseFeedRef(req.FormValue("id"))
|
|
|
|
if err != nil {
|
|
|
|
err = weberrors.ErrBadRequest{Where: "id", Details: err}
|
|
|
|
resp.SendError(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
newMember = *parsedID
|
|
|
|
default:
|
|
|
|
http.Error(rw, fmt.Sprintf("unhandled Content-Type (%q)", ct), http.StatusBadRequest)
|
|
|
|
return
|
2021-03-04 14:09:14 +00:00
|
|
|
}
|
2021-04-07 09:08:43 +00:00
|
|
|
|
|
|
|
// before consuming the invite: check if the invitee is banned
|
|
|
|
if h.deniedKeys.HasFeed(req.Context(), newMember) {
|
|
|
|
resp.SendError(weberrors.ErrDenied)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-03-26 19:08:13 +00:00
|
|
|
resp.UpdateMultiserverAddr(h.networkInfo.MultiserverAddress())
|
2021-03-04 14:09:14 +00:00
|
|
|
|
2021-03-26 19:08:13 +00:00
|
|
|
inv, err := h.invites.Consume(req.Context(), token, newMember)
|
2021-03-04 14:09:14 +00:00
|
|
|
if err != nil {
|
2021-03-10 15:44:46 +00:00
|
|
|
if errors.Is(err, roomdb.ErrNotFound) {
|
2021-03-26 19:08:13 +00:00
|
|
|
resp.SendError(weberrors.ErrNotFound{What: "invite"})
|
|
|
|
return
|
2021-03-04 14:09:14 +00:00
|
|
|
}
|
2021-03-26 19:08:13 +00:00
|
|
|
resp.SendError(err)
|
|
|
|
return
|
2021-03-04 14:09:14 +00:00
|
|
|
}
|
2021-03-05 10:15:36 +00:00
|
|
|
log := logging.FromContext(req.Context())
|
|
|
|
level.Info(log).Log("event", "invite consumed", "id", inv.ID, "ref", newMember.ShortRef())
|
|
|
|
|
2021-03-26 19:08:13 +00:00
|
|
|
resp.SendSuccess()
|
|
|
|
}
|
|
|
|
|
|
|
|
// inviteConsumeResponder is supposed to handle different encoding types transparently.
|
|
|
|
// It either sends the rooms multiaddress on success or an error.
|
|
|
|
type inviteConsumeResponder interface {
|
|
|
|
SendSuccess()
|
|
|
|
SendError(error)
|
|
|
|
|
|
|
|
UpdateMultiserverAddr(string)
|
|
|
|
}
|
|
|
|
|
|
|
|
// inviteConsumeJSONResponse dictates the field names and format of the JSON response for the inviteConsume web endpoint
|
|
|
|
type inviteConsumeJSONResponse struct {
|
|
|
|
Status string `json:"status"`
|
|
|
|
|
|
|
|
RoomAddress string `json:"multiserverAddress"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// handles JSON responses
|
|
|
|
type inviteConsumeJSONResponder struct {
|
|
|
|
enc *json.Encoder
|
|
|
|
|
|
|
|
multiservAddr string
|
|
|
|
}
|
|
|
|
|
|
|
|
func newinviteConsumeJSONResponder(rw http.ResponseWriter) inviteConsumeResponder {
|
|
|
|
rw.Header().Set("Content-Type", "application/json")
|
|
|
|
return &inviteConsumeJSONResponder{
|
|
|
|
enc: json.NewEncoder(rw),
|
2021-03-05 10:15:36 +00:00
|
|
|
}
|
2021-03-26 19:08:13 +00:00
|
|
|
}
|
2021-03-04 14:09:14 +00:00
|
|
|
|
2021-03-26 19:08:13 +00:00
|
|
|
func (json *inviteConsumeJSONResponder) UpdateMultiserverAddr(msaddr string) {
|
|
|
|
json.multiservAddr = msaddr
|
|
|
|
}
|
2021-03-09 17:27:44 +00:00
|
|
|
|
2021-03-26 19:08:13 +00:00
|
|
|
func (json inviteConsumeJSONResponder) SendSuccess() {
|
|
|
|
var resp = inviteConsumeJSONResponse{
|
|
|
|
Status: "successful",
|
|
|
|
RoomAddress: json.multiservAddr,
|
|
|
|
}
|
|
|
|
json.enc.Encode(resp)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (json inviteConsumeJSONResponder) SendError(err error) {
|
|
|
|
json.enc.Encode(struct {
|
|
|
|
Status string `json:"status"`
|
|
|
|
Error string `json:"error"`
|
|
|
|
}{"error", err.Error()})
|
|
|
|
}
|
|
|
|
|
|
|
|
// handles HTML responses
|
|
|
|
type inviteConsumeHTMLResponder struct {
|
|
|
|
renderer *render.Renderer
|
|
|
|
rw http.ResponseWriter
|
|
|
|
req *http.Request
|
|
|
|
|
|
|
|
multiservAddr string
|
|
|
|
}
|
|
|
|
|
|
|
|
func newinviteConsumeHTMLResponder(r *render.Renderer, rw http.ResponseWriter, req *http.Request) inviteConsumeResponder {
|
|
|
|
return &inviteConsumeHTMLResponder{
|
|
|
|
renderer: r,
|
|
|
|
rw: rw,
|
|
|
|
req: req,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (html *inviteConsumeHTMLResponder) UpdateMultiserverAddr(msaddr string) {
|
|
|
|
html.multiservAddr = msaddr
|
|
|
|
}
|
|
|
|
|
|
|
|
func (html inviteConsumeHTMLResponder) SendSuccess() {
|
|
|
|
err := html.renderer.Render(html.rw, html.req, "invite/consumed.tmpl", http.StatusOK, struct {
|
2021-03-30 15:04:07 +00:00
|
|
|
MultiserverAddress string
|
|
|
|
}{(html.multiservAddr)})
|
2021-03-26 19:08:13 +00:00
|
|
|
if err != nil {
|
|
|
|
logger := logging.FromContext(html.req.Context())
|
|
|
|
level.Warn(logger).Log("event", "render failed", "err", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (html inviteConsumeHTMLResponder) SendError(err error) {
|
|
|
|
html.renderer.Error(html.rw, html.req, http.StatusInternalServerError, err)
|
2021-03-04 14:09:14 +00:00
|
|
|
}
|
2021-05-14 13:11:29 +00:00
|
|
|
|
|
|
|
func (h inviteHandler) createOpenMode(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
|
|
ctx := req.Context()
|
|
|
|
|
|
|
|
token, err := h.invites.Create(ctx, -1)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
facadeURL := h.urlTo(router.CompleteInviteFacade, "token", token)
|
|
|
|
|
|
|
|
return map[string]interface{}{
|
|
|
|
"FacadeURL": facadeURL.String(),
|
|
|
|
}, nil
|
|
|
|
}
|