Publish current app and server

This commit is contained in:
ponzischeme89
2026-08-02 22:10:19 +12:00
parent a265636139
commit 1ed180c739
203 changed files with 23933 additions and 2788 deletions
+206
View File
@@ -0,0 +1,206 @@
package api
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"net/http"
"net/url"
"strings"
"time"
"github.com/ponzischeme89/memby/server/internal/emby"
)
const (
installerCookieName = "memby_installer"
installerSessionTTL = 30 * time.Minute
installerDeviceID = "memby-web-installer"
installerDeviceName = "Memby Web Installer"
)
func (s *Server) installerSecret() []byte {
if s.cfg.ReleasePublishToken == "" {
return nil
}
// Domain separation means a cookie/signature is not the release publisher token and
// cannot be presented to the upload endpoint.
sum := sha256.Sum256([]byte("memby installer access v1\x00" + s.cfg.ReleasePublishToken))
return sum[:]
}
func (s *Server) signInstallerValue(purpose string, payload []byte) []byte {
mac := hmac.New(sha256.New, s.installerSecret())
_, _ = mac.Write([]byte(purpose))
_, _ = mac.Write([]byte{0})
_, _ = mac.Write(payload)
return mac.Sum(nil)
}
func (s *Server) newInstallerSession() (string, error) {
payload := make([]byte, 8+16)
binary.BigEndian.PutUint64(payload[:8], uint64(time.Now().Add(installerSessionTTL).Unix()))
if _, err := rand.Read(payload[8:]); err != nil {
return "", err
}
signature := s.signInstallerValue("session", payload)
return base64.RawURLEncoding.EncodeToString(payload) + "." +
base64.RawURLEncoding.EncodeToString(signature), nil
}
func (s *Server) validInstallerSession(r *http.Request) bool {
if len(s.installerSecret()) == 0 {
return false
}
cookie, err := r.Cookie(installerCookieName)
if err != nil {
return false
}
parts := strings.Split(cookie.Value, ".")
if len(parts) != 2 {
return false
}
payload, err := base64.RawURLEncoding.DecodeString(parts[0])
if err != nil || len(payload) != 24 {
return false
}
signature, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil || !hmac.Equal(signature, s.signInstallerValue("session", payload)) {
return false
}
expires := int64(binary.BigEndian.Uint64(payload[:8]))
now := time.Now().Unix()
return expires > now && expires <= now+int64(installerSessionTTL/time.Second)+60
}
func (s *Server) setInstallerCookie(w http.ResponseWriter, value string) {
http.SetCookie(w, &http.Cookie{
Name: installerCookieName,
Value: value,
Path: "/",
MaxAge: int(installerSessionTTL / time.Second),
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteStrictMode,
})
}
func (s *Server) clearInstallerCookie(w http.ResponseWriter) {
http.SetCookie(w, &http.Cookie{
Name: installerCookieName,
Path: "/",
MaxAge: -1,
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteStrictMode,
})
}
func (s *Server) releaseAccessToken(filename string) string {
if len(s.installerSecret()) == 0 || !releaseFilenamePattern.MatchString(filename) {
return ""
}
return base64.RawURLEncoding.EncodeToString(
s.signInstallerValue("release", []byte(filename)),
)
}
func (s *Server) signedReleasePath(filename string) string {
token := s.releaseAccessToken(filename)
if token == "" {
return ""
}
return "/updates/" + filename + "?access=" + url.QueryEscape(token)
}
func (s *Server) allowedReleaseDownload(r *http.Request, filename string) bool {
if s.validInstallerSession(r) {
return true
}
expected := s.releaseAccessToken(filename)
presented := strings.TrimSpace(r.URL.Query().Get("access"))
return expected != "" && hmac.Equal([]byte(presented), []byte(expected))
}
func (s *Server) handleInstallLogin(w http.ResponseWriter, r *http.Request) {
if len(s.installerSecret()) == 0 {
http.NotFound(w, r)
return
}
// Password authentication necessarily registers a device with Emby. Do not start
// it unless the service credential needed to remove that temporary record exists.
if s.cfg.SyncAPIKey == "" {
s.log.Error("installer login unavailable: MEMBY_SYNC_API_KEY is not configured")
s.renderAccessLogin(w, r, "Sign-in is temporarily unavailable.",
http.StatusServiceUnavailable, "/install")
return
}
r.Body = http.MaxBytesReader(w, r.Body, 8<<10)
if err := r.ParseForm(); err != nil {
s.renderAccessLogin(w, r, "Sign-in failed.", http.StatusBadRequest, "/install")
return
}
next := cleanInstallerDestination(r.FormValue("next"))
username := strings.TrimSpace(r.FormValue("username"))
password := r.FormValue("password")
if username == "" || password == "" {
s.renderAccessLogin(w, r, "Enter your username and password.", http.StatusBadRequest, next)
return
}
auth, err := s.emby.Authenticate(
r.Context(), username, password, installerDeviceID, installerDeviceName,
)
if err != nil {
s.log.Warn("installer Emby authentication failed", "username", username)
s.renderAccessLogin(w, r, "Sign-in failed.", http.StatusUnauthorized, next)
return
}
// Authentication creates an Emby access token. The installer needs only proof that
// it succeeded, so retire the upstream session immediately and never persist it.
if err := s.emby.Logout(r.Context(), emby.Credentials{
UserID: auth.User.ID, Token: auth.AccessToken,
DeviceID: installerDeviceID, DeviceName: installerDeviceName,
}); err != nil {
s.log.Warn("installer Emby session cleanup failed", "error", err)
}
if err := s.emby.DeleteDevice(r.Context(), emby.Credentials{
UserID: s.cfg.SyncUserID, Token: s.cfg.SyncAPIKey,
DeviceID: "memby-gateway", DeviceName: "Memby Gateway",
}, installerDeviceID); err != nil {
s.log.Error("installer Emby device cleanup failed", "error", err)
s.renderAccessLogin(w, r, "Sign-in is temporarily unavailable.",
http.StatusBadGateway, next)
return
}
session, err := s.newInstallerSession()
if err != nil {
s.log.Error("installer session generation failed", "error", err)
writeError(w, http.StatusInternalServerError, "could not start installer session")
return
}
s.setInstallerCookie(w, session)
http.Redirect(w, r, next, http.StatusSeeOther)
}
func (s *Server) handleInstallLogout(w http.ResponseWriter, r *http.Request) {
s.clearInstallerCookie(w)
http.Redirect(w, r, "/install", http.StatusSeeOther)
}
func cleanInstallerDestination(value string) string {
value = strings.TrimSpace(value)
if value == "/admin/" {
return "/admin/"
}
if strings.HasPrefix(value, "/admin/") {
page := strings.TrimPrefix(value, "/admin/")
if adminPages[page] {
return value
}
}
return "/install"
}