Proxy Plex through stemserve (/plex/) so HTTPS pages avoid mixed-content blocks
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4b53a720c4
commit
7ca9d0374d
2 files changed
+33
-3
No files matched your search
@@ -16,6 +16,7 @@ Endpoints (all GET unless noted; add ?token=... or X-Stem-Token when TOKEN is se
|
|||||||
POST /api/prepare?key=<key>&fmt=opus&br=160 start transcodes in the background
|
POST /api/prepare?key=<key>&fmt=opus&br=160 start transcodes in the background
|
||||||
POST /api/stem {"paths": [...]} "Stem Now": stemd separates these Plex files next
|
POST /api/stem {"paths": [...]} "Stem Now": stemd separates these Plex files next
|
||||||
/file/<key>/<file>[?fmt=opus&br=160] the stem itself, with Range support
|
/file/<key>/<file>[?fmt=opus&br=160] the stem itself, with Range support
|
||||||
|
/plex/... proxy to PLEX_URL (needs TOKEN or the Plex token)
|
||||||
/api/config Plex URL and token from PLEX_URL / PLEX_TOKEN, if set
|
/api/config Plex URL and token from PLEX_URL / PLEX_TOKEN, if set
|
||||||
/ the web player (web/, or WEB_DIR); no token needed
|
/ the web player (web/, or WEB_DIR); no token needed
|
||||||
|
|
||||||
@@ -33,7 +34,9 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
import urllib.error
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
from array import array
|
from array import array
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
@@ -263,6 +266,31 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
return True
|
return True
|
||||||
return TOKEN in (self.headers.get("X-Stem-Token"), query.get("token", [None])[0])
|
return TOKEN in (self.headers.get("X-Stem-Token"), query.get("token", [None])[0])
|
||||||
|
|
||||||
|
def _plex_proxy(self, path, rawquery, query):
|
||||||
|
"""Forward to PLEX_URL so an HTTPS page can reach an HTTP Plex. Needs the stem or Plex token."""
|
||||||
|
given = self.headers.get("X-Plex-Token") or query.get("X-Plex-Token", [None])[0]
|
||||||
|
if not (self._authorized(query) or (PLEX_TOKEN and given == PLEX_TOKEN)):
|
||||||
|
return self._error(HTTPStatus.UNAUTHORIZED, "bad token")
|
||||||
|
pairs = [(k, v) for k, v in urllib.parse.parse_qsl(rawquery, keep_blank_values=True) if k not in ("token", "X-Plex-Token")]
|
||||||
|
req = urllib.request.Request(PLEX_URL + path + ("?" + urllib.parse.urlencode(pairs) if pairs else ""))
|
||||||
|
req.add_header("X-Plex-Token", PLEX_TOKEN or given or "")
|
||||||
|
for h in ("Range", "Accept", "X-Plex-Client-Identifier", "X-Plex-Product", "If-None-Match"):
|
||||||
|
if self.headers.get(h):
|
||||||
|
req.add_header(h, self.headers[h])
|
||||||
|
try:
|
||||||
|
r = urllib.request.urlopen(req, timeout=30)
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
r = e
|
||||||
|
except OSError as e:
|
||||||
|
return self._error(502, f"plex unreachable: {e}")
|
||||||
|
self.send_response(r.status if hasattr(r, "status") else r.code)
|
||||||
|
for h in ("Content-Type", "Content-Length", "Content-Range", "Accept-Ranges", "ETag", "Cache-Control"):
|
||||||
|
if r.headers.get(h):
|
||||||
|
self.send_header(h, r.headers[h])
|
||||||
|
self.end_headers()
|
||||||
|
while chunk := r.read(65536):
|
||||||
|
self.wfile.write(chunk)
|
||||||
|
|
||||||
def _send(self, status, body=b"", ctype="application/json", headers=None):
|
def _send(self, status, body=b"", ctype="application/json", headers=None):
|
||||||
self.send_response(status)
|
self.send_response(status)
|
||||||
self.send_header("Content-Type", ctype)
|
self.send_header("Content-Type", ctype)
|
||||||
@@ -299,6 +327,8 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
url = urllib.parse.urlsplit(self.path)
|
url = urllib.parse.urlsplit(self.path)
|
||||||
query = urllib.parse.parse_qs(url.query)
|
query = urllib.parse.parse_qs(url.query)
|
||||||
path = urllib.parse.unquote(url.path)
|
path = urllib.parse.unquote(url.path)
|
||||||
|
if path.startswith("/plex/") and PLEX_URL:
|
||||||
|
return self._plex_proxy(path[len("/plex"):], url.query, query)
|
||||||
if not path.startswith(("/api/", "/file/")):
|
if not path.startswith(("/api/", "/file/")):
|
||||||
return self._web(path)
|
return self._web(path)
|
||||||
if not self._authorized(query):
|
if not self._authorized(query):
|
||||||
|
|||||||
+3
-3
@@ -60,7 +60,7 @@ const plexHeaders = token => ({
|
|||||||
|
|
||||||
async function px(path, params = {}) {
|
async function px(path, params = {}) {
|
||||||
if (!S.plex) throw new Error("Not signed in to Plex");
|
if (!S.plex) throw new Error("Not signed in to Plex");
|
||||||
const u = new URL(S.plex.url + path);
|
const u = new URL(S.plex.url + path, location.href);
|
||||||
for (const [k, v] of Object.entries(params)) u.searchParams.set(k, v);
|
for (const [k, v] of Object.entries(params)) u.searchParams.set(k, v);
|
||||||
const r = await fetch(u, { headers: plexHeaders(S.plex.token) });
|
const r = await fetch(u, { headers: plexHeaders(S.plex.token) });
|
||||||
if (!r.ok) throw new Error(`Plex ${r.status}`);
|
if (!r.ok) throw new Error(`Plex ${r.status}`);
|
||||||
@@ -782,7 +782,7 @@ const routes = {
|
|||||||
if (S.plex) secs = ((await px("/library/sections").catch(() => ({}))).Directory || []).filter(d => d.type === "artist");
|
if (S.plex) secs = ((await px("/library/sections").catch(() => ({}))).Directory || []).filter(d => d.type === "artist");
|
||||||
view.innerHTML = `<h1>Settings</h1>
|
view.innerHTML = `<h1>Settings</h1>
|
||||||
<h2>Plex</h2><div class="form">
|
<h2>Plex</h2><div class="form">
|
||||||
<p class="note">${S.plex ? `Plex: <b>${esc(S.plex.name && S.plex.name !== S.plex.url ? S.plex.name + " (" + S.plex.url + ")" : S.plex.url)}</b>${S.plex.env ? " (set by the server)" : ""}` : "Not signed in."}</p>
|
<p class="note">${S.plex ? `Plex: <b>${esc(S.plex.env ? "via this server" : S.plex.name && S.plex.name !== S.plex.url ? S.plex.name + " (" + S.plex.url + ")" : S.plex.url)}</b>${S.plex.env ? " (set by the server)" : ""}` : "Not signed in."}</p>
|
||||||
<div class="row"><button class="btn primary" id="signin">Sign in with Plex</button>${S.plex ? `<button class="btn" id="signout">Sign out</button>` : ""}<span class="note" id="sstatus"></span></div>
|
<div class="row"><button class="btn primary" id="signin">Sign in with Plex</button>${S.plex ? `<button class="btn" id="signout">Sign out</button>` : ""}<span class="note" id="sstatus"></span></div>
|
||||||
${secs.length > 1 ? `<label>Music library<select id="sec">${secs.map(d => `<option value="${d.key}" ${d.key == S.section ? "selected" : ""}>${esc(d.title)}</option>`).join("")}</select></label>` : ""}
|
${secs.length > 1 ? `<label>Music library<select id="sec">${secs.map(d => `<option value="${d.key}" ${d.key == S.section ? "selected" : ""}>${esc(d.title)}</option>`).join("")}</select></label>` : ""}
|
||||||
<details><summary class="note">Enter a server and token by hand</summary><div class="form" style="margin-top:12px">
|
<details><summary class="note">Enter a server and token by hand</summary><div class="form" style="margin-top:12px">
|
||||||
@@ -850,7 +850,7 @@ async function autoConnect() {
|
|||||||
if (!S.plex || S.plex.env) {
|
if (!S.plex || S.plex.env) {
|
||||||
try {
|
try {
|
||||||
const c = await fetch(stemBase() + "/api/config" + stemQuery()).then(r => r.ok ? r.json() : null);
|
const c = await fetch(stemBase() + "/api/config" + stemQuery()).then(r => r.ok ? r.json() : null);
|
||||||
if (c?.plex) S.plex = { ...c.plex, name: "", env: true };
|
if (c?.plex) S.plex = { token: c.plex.token, url: stemBase() + "/plex", name: "", env: true };
|
||||||
else if (S.plex?.env) S.plex = null;
|
else if (S.plex?.env) S.plex = null;
|
||||||
save();
|
save();
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|||||||
Reference in new issue
Block a user