App v0.2.26 and gateway 0.1.20
Client: seek controls, Bazarr subtitle download and cast panel in the player; MDBList ratings strip; episode and schedule detail pages; series pace estimate; what's new panel; install-permission onboarding step; synced per-profile preferences; Emby outage banner. Gateway: rebuilt admin console (one fragment per page), preference history and restore, merged Continue Watching, Emby health probe, subtitle selection and Bazarr download, structured request logging with per-request identity, and embedded build version. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
2675e6d82b
commit
4a4df7a73c
@@ -19,6 +19,11 @@ const (
|
||||
installerSessionTTL = 30 * time.Minute
|
||||
installerDeviceID = "memby-web-installer"
|
||||
installerDeviceName = "Memby Web Installer"
|
||||
|
||||
// installerRenewWithin is how close to expiry a session must be before an operator's
|
||||
// own request re-issues it. Half the TTL, so a cookie is rewritten at most once every
|
||||
// fifteen minutes rather than on every request of a working session.
|
||||
installerRenewWithin = installerSessionTTL / 2
|
||||
)
|
||||
|
||||
func (s *Server) installerSecret() []byte {
|
||||
@@ -50,29 +55,58 @@ func (s *Server) newInstallerSession() (string, error) {
|
||||
base64.RawURLEncoding.EncodeToString(signature), nil
|
||||
}
|
||||
|
||||
func (s *Server) validInstallerSession(r *http.Request) bool {
|
||||
// installerSessionExpiry reports when the request's session runs out. A cookie that is
|
||||
// missing, malformed, forged or already expired is reported the same way: no session.
|
||||
func (s *Server) installerSessionExpiry(r *http.Request) (time.Time, bool) {
|
||||
if len(s.installerSecret()) == 0 {
|
||||
return false
|
||||
return time.Time{}, false
|
||||
}
|
||||
cookie, err := r.Cookie(installerCookieName)
|
||||
if err != nil {
|
||||
return false
|
||||
return time.Time{}, false
|
||||
}
|
||||
parts := strings.Split(cookie.Value, ".")
|
||||
if len(parts) != 2 {
|
||||
return false
|
||||
return time.Time{}, false
|
||||
}
|
||||
payload, err := base64.RawURLEncoding.DecodeString(parts[0])
|
||||
if err != nil || len(payload) != 24 {
|
||||
return false
|
||||
return time.Time{}, false
|
||||
}
|
||||
signature, err := base64.RawURLEncoding.DecodeString(parts[1])
|
||||
if err != nil || !hmac.Equal(signature, s.signInstallerValue("session", payload)) {
|
||||
return false
|
||||
return time.Time{}, false
|
||||
}
|
||||
expires := int64(binary.BigEndian.Uint64(payload[:8]))
|
||||
now := time.Now().Unix()
|
||||
return expires > now && expires <= now+int64(installerSessionTTL/time.Second)+60
|
||||
if expires <= now || expires > now+int64(installerSessionTTL/time.Second)+60 {
|
||||
return time.Time{}, false
|
||||
}
|
||||
return time.Unix(expires, 0), true
|
||||
}
|
||||
|
||||
func (s *Server) validInstallerSession(r *http.Request) bool {
|
||||
_, ok := s.installerSessionExpiry(r)
|
||||
return ok
|
||||
}
|
||||
|
||||
// renewInstallerSession slides a valid session's expiry forward. The TTL was absolute and
|
||||
// nothing extended it, so an operator working the admin console was signed out from under
|
||||
// themselves after thirty minutes and the page's poll became a permanent "invalid admin
|
||||
// token" banner with no sign-in to return to. Callers must only reach here for a request
|
||||
// an operator actually made — see operatorPresent — or an abandoned tab's own polling
|
||||
// would keep the session alive indefinitely, which is what the TTL exists to stop.
|
||||
func (s *Server) renewInstallerSession(w http.ResponseWriter, r *http.Request) {
|
||||
expires, ok := s.installerSessionExpiry(r)
|
||||
if !ok || time.Until(expires) > installerRenewWithin {
|
||||
return
|
||||
}
|
||||
session, err := s.newInstallerSession()
|
||||
if err != nil {
|
||||
s.loggerFor(r.Context()).Error("installer session renewal failed", "error", err)
|
||||
return
|
||||
}
|
||||
s.setInstallerCookie(w, session)
|
||||
}
|
||||
|
||||
func (s *Server) setInstallerCookie(w http.ResponseWriter, value string) {
|
||||
@@ -132,7 +166,7 @@ func (s *Server) handleInstallLogin(w http.ResponseWriter, r *http.Request) {
|
||||
// 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.loggerFor(r.Context()).Error("installer login unavailable: MEMBY_SYNC_API_KEY is not configured")
|
||||
s.renderAccessLogin(w, r, "Sign-in is temporarily unavailable.",
|
||||
http.StatusServiceUnavailable, "/install")
|
||||
return
|
||||
@@ -151,10 +185,10 @@ func (s *Server) handleInstallLogin(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
auth, err := s.emby.Authenticate(
|
||||
r.Context(), username, password, installerDeviceID, installerDeviceName,
|
||||
r.Context(), username, password, installerDeviceID, installerDeviceName, "",
|
||||
)
|
||||
if err != nil {
|
||||
s.log.Warn("installer Emby authentication failed", "username", username)
|
||||
s.loggerFor(r.Context()).Warn("installer Emby authentication failed", "username", username)
|
||||
s.renderAccessLogin(w, r, "Sign-in failed.", http.StatusUnauthorized, next)
|
||||
return
|
||||
}
|
||||
@@ -164,13 +198,13 @@ func (s *Server) handleInstallLogin(w http.ResponseWriter, r *http.Request) {
|
||||
UserID: auth.User.ID, Token: auth.AccessToken,
|
||||
DeviceID: installerDeviceID, DeviceName: installerDeviceName,
|
||||
}); err != nil {
|
||||
s.log.Warn("installer Emby session cleanup failed", "error", err)
|
||||
s.loggerFor(r.Context()).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.loggerFor(r.Context()).Error("installer Emby device cleanup failed", "error", err)
|
||||
s.renderAccessLogin(w, r, "Sign-in is temporarily unavailable.",
|
||||
http.StatusBadGateway, next)
|
||||
return
|
||||
@@ -178,7 +212,7 @@ func (s *Server) handleInstallLogin(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
session, err := s.newInstallerSession()
|
||||
if err != nil {
|
||||
s.log.Error("installer session generation failed", "error", err)
|
||||
s.loggerFor(r.Context()).Error("installer session generation failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "could not start installer session")
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user