Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix rest api access when using script/develop_and_serve #23144

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"name": "Home Assistant Frontend",
"build": {
"dockerfile": "Dockerfile",
"context": ".."
"context": "..",
"options": ["--add-host=host.docker.internal:host-gateway"]
},
"appPort": "8124:8123",
"postCreateCommand": "sudo apt update && sudo apt upgrade -y && sudo apt install -y libpcap-dev",
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ build/
dist/
/hass_frontend/
/translations/
script/serve.key
script/serve.crt

# yarn
.yarn/*
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"postinstall": "husky",
"prepack": "pinst --disable",
"postpack": "pinst --enable",
"serve": "node ./script/serve.js",
"test": "vitest run --config test/vitest.config.ts",
"test:coverage": "vitest run --config test/vitest.config.ts --coverage"
},
Expand Down
72 changes: 27 additions & 45 deletions script/develop_and_serve
Original file line number Diff line number Diff line change
@@ -1,70 +1,52 @@
#!/bin/sh
#!/usr/bin/env bash
#
# This script can be used to develop and test the frontend without having to
# link the build in a running core instance through the frontend/development_repo setting.
#
# WARNING:
# If you have an active login session in the frontend. The core that was used
# as a backend during the time of the login remains used until you logout again.
# So if you reuse the url hosting the frontend, you will need to logout before
# it will actually start using the core backend configured by this script.
#
# If you run this script without parameters, the frontend will be accessible under http://localhost:8124.
# And it will use the core instance running under http://localhost:8123 as a backend.
# Note that from a devcontainer, the frontend will be accessible under port 8124 on the host container.
# Inside the devcontainer it will be accessible under port 8123 instead.
# The core instance endpoint remains the same in both cases, as this is resolved from the browser.
# If you run this script without parameters, the frontend will be accessible under
# http://localhost:8124. And it will use the core instance running under
# http://localhost:8123 as a backend. Note that if you're developing using a
# devcontainer, these are the url's on the host container
# (the ones used inside the devcontainer will be different)
#
# You can change the core instance the frontend connects to by passing the -c option.
# For example: script/develop_and_serve -c https://myhost.duckdns.org:8123
# This will also work for existing production core instances.
# It does not need to be a development version hosted locally.
# However note that if the core you connect to uses https, you will also need to use
# https to connect to your local development frontend. A self signed certificate
# for localhost is included and will be used when necessary. You just have to accept
# it as valid when browsing to your local development frontend.
#
# You can change the port the frontend is served on by passing the -p option.
# For example: script/develop_and_serve -p 8654
# Note that if you are running from a devcontainer, you will need to setup
# port forwarding as well if you want to access it from the container host.
#
# WARNING:
# If you change the core url in between runs but preserve the port on which the
# frontend runs, you should logout before you make the switch. If you do not
# the frontend will just keep loading and timeout as it has a session that is not
# valid for the server. You can fix this if you forgot it by clearing the cookies
# for your development frontend.

# Stop on errors
set -e

cd "$(dirname "$0")/.."

# parse input paramters
if [ -n "$DEVCONTAINER" ]; then
frontendPort=8123
else
frontendPort=8124
if [ ! -d "./hassio/build" ]; then
echo Building hassio
# the hassio rest app doesn't need the HASS_URL override,
# it will use the url from the current login session
hassio/script/build_hassio
fi

coreUrl=http://localhost:8123

while getopts p:c:h flag
do
case "${flag}" in
p) frontendPort=${OPTARG};;
c) coreUrl="${OPTARG}";;
h) echo Documentation can be found inside "$0" && exit 0;;
*) echo Documentation can be found inside "$0" && exit 1;;
esac
done

# display used settings
if [ -n "$DEVCONTAINER" ]; then
echo Frontend is available inside container as http://localhost:${frontendPort}
if [ 8123 -eq $frontendPort ]; then
echo Frontend is available on container host as http://localhost:8124
fi
else
echo Frontend is hosted on http://localhost:${frontendPort}
if [ ! -f "./script/serve.crt" ]; then
echo Generating self signed localhost certificate
openssl req -x509 -newkey rsa:4096 -keyout "./script/serve.key" -out "./script/serve.crt" -sha256 -days 3650 -nodes -subj "/CN=localhost"
fi
echo Core is used from ${coreUrl}

# build the frontend so it connects to the passed core
HASS_URL="$coreUrl" ./script/develop &

# serve the frontend
yarn dlx serve -l $frontendPort ./hass_frontend -s &
./script/develop &

# keep the script running while serving
wait
yarn serve "$@"
83 changes: 83 additions & 0 deletions script/serve.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { createProxyMiddleware } from "http-proxy-middleware";
import express from "express";
import { fileURLToPath } from "url";
import fs from "fs";
import http from "http";
import https from "https";
import minimist from "minimist";
import path from "path";

const inDevContainer = process.env.DEVCONTAINER !== undefined;
const parsedArguments = {
c: inDevContainer
? "http://host.docker.internal:8123"
: "http://localhost:8123",
p: inDevContainer ? "8123" : "8124",
...minimist(process.argv.slice(2)),
};
const coreUrl = parsedArguments.c;
const port = parseInt(parsedArguments.p);
const repoDir = path.join(fileURLToPath(import.meta.url), "../..");
// if the core uses https, also use https for serving to avoid problems
// with headers like Strict-Transport-Security
const useHttps = coreUrl.startsWith("https:");
const frontendBase = `http${useHttps ? "s" : ""}://localhost`;

console.log(`Frontend is hosted on ${frontendBase}:${port}`);
if (inDevContainer && port === 8123) {
console.log(
`Frontend is available on container host as ${frontendBase}:8124`
);
}
console.log(`Core is used from ${coreUrl}`);

const coreProxy = createProxyMiddleware({
target: coreUrl,
changeOrigin: true,
ws: true,
});
const app = express();
app.use("/", express.static(path.join(repoDir, "hass_frontend")));
app.use("/api/hassio/app", express.static(path.join(repoDir, "hassio/build")));
app.use("/api", coreProxy);
app.get("/auth/authorize", (req, res) => {
res.sendFile(path.join(repoDir, "hass_frontend/authorize.html"));
});
Dismissed Show dismissed Hide dismissed
app.use("/auth", coreProxy);
app.get("/onboarding", (req, res) => {
res.sendFile(path.join(repoDir, "hass_frontend/onboarding.html"));
});
Dismissed Show dismissed Hide dismissed
app.get("*", (req, res) => {
res.sendFile(path.join(repoDir, "hass_frontend/index.html"));
});
Fixed Show fixed Hide fixed
Fixed Show fixed Hide fixed
Dismissed Show dismissed Hide dismissed
Comment on lines +34 to +52
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const coreProxy = createProxyMiddleware({
target: coreUrl,
changeOrigin: true,
ws: true,
});
const app = express();
app.use("/", express.static(path.join(repoDir, "hass_frontend")));
app.use("/api/hassio/app", express.static(path.join(repoDir, "hassio/build")));
app.use("/api", coreProxy);
app.get("/auth/authorize", (req, res) => {
res.sendFile(path.join(repoDir, "hass_frontend/authorize.html"));
});
app.use("/auth", coreProxy);
app.get("/onboarding", (req, res) => {
res.sendFile(path.join(repoDir, "hass_frontend/onboarding.html"));
});
app.get("*", (req, res) => {
res.sendFile(path.join(repoDir, "hass_frontend/index.html"));
});
const indexHtml = fs.readFileSync(path.join(repoDir, "hass_frontend", "index.html"), "utf8")
.replace(/<\/body>/g, `<script>
const interval = setInterval(() => {
window.hassConnection.then(conn => {
conn.auth.data.hassUrl = "${coreUrl}";
clearInterval(interval);
});
}, 100);
</script>
</body>`);
const coreProxy = createProxyMiddleware({
target: coreUrl,
changeOrigin: true,
ws: true,
});
const app = express();
app.get("/index.html", (req, res) => {
res.send(indexHtml);
});
app.use(express.static(path.join(repoDir, "hass_frontend")));
app.use(express.static(path.join(repoDir, "hass_frontend", "frontend_latest")));
app.use("/api/hassio/app", express.static(path.join(repoDir, "hassio/build")));
app.use("/api", coreProxy);
app.get("/auth/authorize", (req, res) => {
res.sendFile(path.join(repoDir, "hass_frontend/authorize.html"));
});
app.use("/auth", coreProxy);
app.get("/onboarding", (req, res) => {
res.sendFile(path.join(repoDir, "hass_frontend/onboarding.html"));
});
app.get("*", (req, res) => {
res.send(indexHtml);
});

This works but is very dirty. The problem is that the API url comes from the hassConnection.auth.data.hassUrl

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we have to change the index, we should do it inside the template with a flag, also, why does it use an interval?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interval was the quickest dirty solution to test & show what needs to happen as hassConnection is not initialized when this script tag runs

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MindFreeze why was this extension needed? it worked just fine with proxying everything?

app.use("/", coreProxy);

const appServer = useHttps
? https.createServer(
{
key: fs.readFileSync(repoDir + "/script/serve.key"),
cert: fs.readFileSync(repoDir + "/script/serve.crt"),
},
app
)
: http.createServer(app);

const appListener = appServer.listen(port, () => {
console.log("Starting development server");
});

let connections = [];
appListener.on("connection", (connection) => {
connections.push(connection);
connection.on(
"close",
() => (connections = connections.filter((curr) => curr !== connection))
);
});

process.on("SIGINT", function () {
console.log("Shutting down development server");
appServer.close();
// websockets are not closed automatically
connections.forEach((c) => c.end());
});
Loading