fix(bskyweb): jpeg avatars, base64 wallet creds, require token secret
This commit is contained in:
@@ -7,9 +7,12 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
_ "image/gif"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -73,6 +76,9 @@ func (srv *Server) RegisterInvitePassRoutes() {
|
||||
}
|
||||
|
||||
func (srv *Server) WebInvitePassURL(c echo.Context) error {
|
||||
if len(srv.cfg.InvitePass.TokenSecret) == 0 {
|
||||
return c.JSON(http.StatusServiceUnavailable, echo.Map{"error": "InvitePassDisabled"})
|
||||
}
|
||||
did, _, err := srv.authenticator.Authenticate(c)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusUnauthorized, echo.Map{"error": "AuthMissing"})
|
||||
@@ -91,6 +97,9 @@ func (srv *Server) WebInvitePassURL(c echo.Context) error {
|
||||
}
|
||||
|
||||
func (srv *Server) WebInvitePassPkpass(c echo.Context) error {
|
||||
if len(srv.cfg.InvitePass.TokenSecret) == 0 {
|
||||
return c.JSON(http.StatusServiceUnavailable, echo.Map{"error": "InvitePassDisabled"})
|
||||
}
|
||||
theme := CoerceTheme(c.QueryParam("theme"))
|
||||
tok := c.QueryParam("t")
|
||||
did, tokTheme, err := VerifyPassToken(srv.cfg.InvitePass.TokenSecret, tok, time.Now())
|
||||
@@ -165,7 +174,7 @@ func (srv *Server) WebInviteWalletHero(c echo.Context) error {
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, echo.Map{"error": "StripLoadFailed"})
|
||||
}
|
||||
avatarImg, _ := decodePNG(avatarBytes)
|
||||
avatarImg, _ := decodeImage(avatarBytes)
|
||||
out, err := CompositeStrip(base, avatarImg, handle, srv.cfg.InvitePass.FontFace)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, echo.Map{"error": "StripBuildFailed"})
|
||||
@@ -190,13 +199,19 @@ func (srv *Server) buildPassAssets(theme, handle string, avatarBytes []byte) ([]
|
||||
logo2, _ := readFS(srv.cfg.InvitePass.StripFS, "passes/logo@2x.png")
|
||||
logo3, _ := readFS(srv.cfg.InvitePass.StripFS, "passes/logo@3x.png")
|
||||
|
||||
avatarImg, _ := decodePNG(avatarBytes)
|
||||
avatarImg, _ := decodeImage(avatarBytes)
|
||||
strip1Bytes, err := buildStripAtDensity(srv.cfg.InvitePass.StripFS, theme, 1, avatarImg, handle, srv.cfg.InvitePass.FontFace)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("strip@1x: %w", err)
|
||||
}
|
||||
strip2Bytes, _ := buildStripAtDensity(srv.cfg.InvitePass.StripFS, theme, 2, avatarImg, handle, srv.cfg.InvitePass.FontFace)
|
||||
strip3Bytes, _ := buildStripAtDensity(srv.cfg.InvitePass.StripFS, theme, 3, avatarImg, handle, srv.cfg.InvitePass.FontFace)
|
||||
strip2Bytes, err := buildStripAtDensity(srv.cfg.InvitePass.StripFS, theme, 2, avatarImg, handle, srv.cfg.InvitePass.FontFace)
|
||||
if err != nil {
|
||||
slog.Debug("invite pass: strip composite failed", "scale", 2, "err", err)
|
||||
}
|
||||
strip3Bytes, err := buildStripAtDensity(srv.cfg.InvitePass.StripFS, theme, 3, avatarImg, handle, srv.cfg.InvitePass.FontFace)
|
||||
if err != nil {
|
||||
slog.Debug("invite pass: strip composite failed", "scale", 3, "err", err)
|
||||
}
|
||||
|
||||
assets := []PassAsset{
|
||||
{Name: "icon.png", Data: icon1},
|
||||
@@ -241,7 +256,7 @@ func readFS(staticFS fs.FS, name string) ([]byte, error) {
|
||||
return io.ReadAll(f)
|
||||
}
|
||||
|
||||
func decodePNG(data []byte) (image.Image, error) {
|
||||
func decodeImage(data []byte) (image.Image, error) {
|
||||
if len(data) == 0 {
|
||||
return nil, errors.New("empty")
|
||||
}
|
||||
|
||||
@@ -91,3 +91,44 @@ func TestWebInvitePassPkpass_ThemeMismatchInToken(t *testing.T) {
|
||||
t.Fatalf("status = %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebInvitePassURL_NoTokenSecret(t *testing.T) {
|
||||
authStub := &stubAuth{did: "did:plc:abc", handle: "alice.bsky.social"}
|
||||
srv := &Server{
|
||||
echo: echo.New(),
|
||||
cfg: &Config{
|
||||
InvitePass: InvitePassConfig{
|
||||
TokenSecret: nil,
|
||||
},
|
||||
},
|
||||
authenticator: authStub,
|
||||
}
|
||||
srv.RegisterInvitePassRoutes()
|
||||
req := httptest.NewRequest(http.MethodPost, "/invite/pass.url", bytes.NewReader([]byte(`{"theme":"dusk"}`)))
|
||||
req.Header.Set("Authorization", "Bearer fake")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
srv.echo.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusServiceUnavailable {
|
||||
t.Fatalf("status = %d, want 503", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebInvitePassPkpass_NoTokenSecret(t *testing.T) {
|
||||
srv := &Server{
|
||||
echo: echo.New(),
|
||||
cfg: &Config{
|
||||
InvitePass: InvitePassConfig{
|
||||
TokenSecret: nil,
|
||||
},
|
||||
},
|
||||
authenticator: &stubAuth{},
|
||||
}
|
||||
srv.RegisterInvitePassRoutes()
|
||||
req := httptest.NewRequest(http.MethodGet, "/invite/pass.pkpass?theme=dusk&t=anytoken", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
srv.echo.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusServiceUnavailable {
|
||||
t.Fatalf("status = %d, want 503", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,7 +138,11 @@ func serve(cctx *cli.Context) error {
|
||||
}
|
||||
}
|
||||
if saJSON, issuer := os.Getenv("GOOGLE_WALLET_SA_JSON"), os.Getenv("GOOGLE_WALLET_ISSUER_ID"); saJSON != "" && issuer != "" {
|
||||
wcfg, werr := LoadWalletConfig([]byte(saJSON), issuer,
|
||||
saBytes := []byte(saJSON)
|
||||
if decoded, err := base64.StdEncoding.DecodeString(saJSON); err == nil {
|
||||
saBytes = decoded
|
||||
}
|
||||
wcfg, werr := LoadWalletConfig(saBytes, issuer,
|
||||
os.Getenv("GOOGLE_WALLET_HERO_BASE_URL"),
|
||||
os.Getenv("GOOGLE_WALLET_LOGO_URL"))
|
||||
if werr != nil {
|
||||
@@ -154,6 +158,9 @@ func serve(cctx *cli.Context) error {
|
||||
invitePass.StripFS = stripFS
|
||||
invitePass.FontFace = basicfont.Face7x13
|
||||
invitePass.BaseURL = "https://bsky.app"
|
||||
if invitePass.Signer != nil && len(invitePass.TokenSecret) == 0 {
|
||||
slog.Warn("invite pass: Apple cert loaded but INVITE_PASS_TOKEN_SECRET is empty; pass endpoints disabled")
|
||||
}
|
||||
|
||||
//
|
||||
// server
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ APPLE_PASS_KEY_PEM=
|
||||
APPLE_PASS_WWDR_PEM=
|
||||
# Invite passes (Google Wallet)
|
||||
GOOGLE_WALLET_ISSUER_ID=
|
||||
GOOGLE_WALLET_SA_JSON=
|
||||
GOOGLE_WALLET_SA_JSON= # raw JSON or base64-encoded JSON; server tries base64 first, falls back to raw
|
||||
GOOGLE_WALLET_HERO_BASE_URL=https://bsky.app/invite/wallet/hero
|
||||
GOOGLE_WALLET_LOGO_URL=https://web-cdn.bsky.app/passes/logo.png
|
||||
# HMAC key for the iOS download token
|
||||
|
||||
Reference in New Issue
Block a user