Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
331925c197 | ||
|
|
0e51f745d3 | ||
|
|
d6f720d0e6 | ||
|
|
5324bf12d4 | ||
|
|
87cdcf28e0 | ||
|
|
441b181e79 | ||
|
|
eba639b59c | ||
|
|
da36283ad1 | ||
|
|
c050368782 | ||
|
|
82b50f0fd7 |
No files matched your search
@@ -33,6 +33,7 @@ body:
|
||||
options:
|
||||
# The below comment is used to insert a new version with on-release.yml
|
||||
#NEXT_VERSION#
|
||||
- 1.76.1
|
||||
- 1.76.0
|
||||
- 1.75.0
|
||||
- 1.74.0
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
## [1.76.2](https://github.com/homarr-labs/homarr/compare/v1.76.1...v1.76.2) (2026-08-27)
|
||||
|
||||
## [1.76.1](https://github.com/homarr-labs/homarr/compare/v1.76.0...v1.76.1) (2026-08-26)
|
||||
|
||||
## [1.76.0](https://github.com/homarr-labs/homarr/compare/v1.75.0...v1.76.0) (2026-08-21)
|
||||
|
||||
## [1.75.0](https://github.com/homarr-labs/homarr/compare/v1.74.0...v1.75.0) (2026-08-14)
|
||||
|
||||
+2
-2
@@ -9,7 +9,7 @@ http {
|
||||
|
||||
# Route websockets traffic to port 3001
|
||||
location /websockets {
|
||||
proxy_pass http://${HOSTNAME}:3001;
|
||||
proxy_pass http://127.0.0.1:3001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
@@ -18,7 +18,7 @@ http {
|
||||
|
||||
# Route all other traffic to port 3000
|
||||
location / {
|
||||
proxy_pass http://${HOSTNAME}:3000;
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "homarr",
|
||||
"version": "1.76.0",
|
||||
"version": "1.76.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "cross-env CI=true turbo build",
|
||||
|
||||
@@ -43,7 +43,7 @@ async function importCookiesAsync() {
|
||||
|
||||
function getBaseUrl() {
|
||||
if (typeof window !== "undefined") return window.location.origin;
|
||||
return `http://${process.env.HOSTNAME ?? "localhost"}:3000`;
|
||||
return "http://127.0.0.1:3000";
|
||||
}
|
||||
|
||||
export const trpcPath = "/api/trpc";
|
||||
|
||||
@@ -5,5 +5,4 @@ export {
|
||||
createCustomCheckServerIdentity,
|
||||
createHttpsAgentAsync,
|
||||
fetchWithTrustedCertificatesAsync,
|
||||
mergeHeadersWithUserAgent,
|
||||
} from "./request";
|
||||
@@ -13,19 +13,6 @@ import { UndiciHttpAgent } from "@homarr/core/infrastructure/http";
|
||||
|
||||
import type { TrustedCertificateHostname } from "../certificates/hostnames";
|
||||
import { withTimeoutAsync } from "./timeout";
|
||||
|
||||
import packageJson from "../../../../../package.json";
|
||||
|
||||
export const getDefaultUserAgent = () => `Homarr/${packageJson.version} (+https://homarr.dev)`;
|
||||
|
||||
export const mergeHeadersWithUserAgent = (headers: unknown): Headers => {
|
||||
const mergedHeaders = new Headers(headers as HeadersInit);
|
||||
if (!mergedHeaders.has("user-agent")) {
|
||||
mergedHeaders.set("user-agent", getDefaultUserAgent());
|
||||
}
|
||||
return mergedHeaders;
|
||||
};
|
||||
|
||||
export const createCustomCheckServerIdentity = (
|
||||
trustedHostnames: TrustedCertificateHostname[],
|
||||
): typeof checkServerIdentity => {
|
||||
@@ -85,18 +72,22 @@ export const fetchWithTrustedCertificatesAsync = async (
|
||||
undefined,
|
||||
options?.bodyTimeout !== undefined ? { bodyTimeout: options.bodyTimeout } : undefined,
|
||||
));
|
||||
const buildFetchOptions = (fetchOptions: Omit<RequestInit, "dispatcher" | "bodyTimeout" | "timeout">) => ({
|
||||
...fetchOptions,
|
||||
headers: mergeHeadersWithUserAgent(fetchOptions.headers) as unknown as RequestInit["headers"],
|
||||
});
|
||||
if (options?.timeout) {
|
||||
const { bodyTimeout: _bodyTimeout, dispatcher: _dispatcher, ...fetchOptions } = options;
|
||||
return await withTimeoutAsync(
|
||||
async (signal) => fetch(url, { ...buildFetchOptions(fetchOptions), signal, dispatcher: agent }),
|
||||
async (signal) =>
|
||||
fetch(url, {
|
||||
...fetchOptions,
|
||||
signal,
|
||||
dispatcher: agent,
|
||||
}),
|
||||
options.timeout,
|
||||
);
|
||||
}
|
||||
|
||||
const { bodyTimeout: _bodyTimeout, dispatcher: _dispatcher, ...fetchOptions } = options ?? {};
|
||||
return fetch(url, { ...buildFetchOptions(fetchOptions), dispatcher: agent });
|
||||
return fetch(url, {
|
||||
...fetchOptions,
|
||||
dispatcher: agent,
|
||||
});
|
||||
};
|
||||
@@ -1,120 +0,0 @@
|
||||
// @vitest-environment node
|
||||
|
||||
import { createServer } from "node:http";
|
||||
import type { Server } from "node:http";
|
||||
import type { AddressInfo } from "node:net";
|
||||
import type { RequestInit } from "undici";
|
||||
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
|
||||
import { fetchWithTrustedCertificatesAsync, mergeHeadersWithUserAgent } from "@homarr/core/infrastructure/http";
|
||||
|
||||
vi.mock("@homarr/core/infrastructure/certificates", () => ({
|
||||
getAllTrustedCertificatesAsync: vi.fn(async () => []),
|
||||
getTrustedCertificateHostnamesAsync: vi.fn(async () => []),
|
||||
}));
|
||||
|
||||
describe("mergeHeadersWithUserAgent", () => {
|
||||
test("should keep existing headers when given a Headers instance", () => {
|
||||
// Arrange
|
||||
const headers = new Headers({ "x-api-key": "some-api-key", Accept: "application/json" });
|
||||
|
||||
// Act
|
||||
const merged = mergeHeadersWithUserAgent(headers);
|
||||
|
||||
// Assert
|
||||
expect(merged.get("x-api-key")).toBe("some-api-key");
|
||||
expect(merged.get("accept")).toBe("application/json");
|
||||
expect(merged.get("user-agent")).toContain("Homarr/");
|
||||
});
|
||||
|
||||
test("should keep existing headers when given a record", () => {
|
||||
// Arrange
|
||||
const headers = { "x-api-key": "some-api-key", Accept: "application/json" };
|
||||
|
||||
// Act
|
||||
const merged = mergeHeadersWithUserAgent(headers);
|
||||
|
||||
// Assert
|
||||
expect(merged.get("x-api-key")).toBe("some-api-key");
|
||||
expect(merged.get("accept")).toBe("application/json");
|
||||
expect(merged.get("user-agent")).toContain("Homarr/");
|
||||
});
|
||||
|
||||
test("should keep existing headers when given a tuple array", () => {
|
||||
// Arrange
|
||||
const headers: [string, string][] = [
|
||||
["x-api-key", "some-api-key"],
|
||||
["Accept", "application/json"],
|
||||
];
|
||||
|
||||
// Act
|
||||
const merged = mergeHeadersWithUserAgent(headers);
|
||||
|
||||
// Assert
|
||||
expect(merged.get("x-api-key")).toBe("some-api-key");
|
||||
expect(merged.get("accept")).toBe("application/json");
|
||||
expect(merged.get("user-agent")).toContain("Homarr/");
|
||||
});
|
||||
|
||||
test("should not override an existing user-agent in any casing", () => {
|
||||
// Arrange
|
||||
const headers = new Headers({ "USER-AGENT": "custom-agent" });
|
||||
|
||||
// Act
|
||||
const merged = mergeHeadersWithUserAgent(headers);
|
||||
|
||||
// Assert
|
||||
expect(merged.get("user-agent")).toBe("custom-agent");
|
||||
});
|
||||
|
||||
test("should fall back to the default user agent when no headers are provided", () => {
|
||||
// Act
|
||||
const merged = mergeHeadersWithUserAgent(undefined);
|
||||
|
||||
// Assert
|
||||
expect(merged.get("user-agent")).toContain("Homarr/");
|
||||
});
|
||||
});
|
||||
|
||||
const startCaptureServerAsync = async (): Promise<{
|
||||
server: Server;
|
||||
getReceivedHeaders: () => Record<string, string | string[] | undefined>;
|
||||
port: number;
|
||||
}> => {
|
||||
let receivedHeaders: Record<string, string | string[] | undefined> = {};
|
||||
const server = createServer((req, res) => {
|
||||
receivedHeaders = req.headers;
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end("{}");
|
||||
});
|
||||
await new Promise<void>((resolve) => server.listen(0, resolve));
|
||||
const port = (server.address() as AddressInfo).port;
|
||||
return {
|
||||
server,
|
||||
port,
|
||||
getReceivedHeaders: () => receivedHeaders,
|
||||
};
|
||||
};
|
||||
|
||||
describe("fetchWithTrustedCertificatesAsync", () => {
|
||||
test("should deliver a Headers instance with all headers to the remote server", async () => {
|
||||
// Arrange - the @immich/sdk hands over a Headers instance after merging its defaults
|
||||
const { server, port, getReceivedHeaders } = await startCaptureServerAsync();
|
||||
const headers = new Headers({ "x-api-key": "immich-api-key", Accept: "application/json" });
|
||||
|
||||
// Act
|
||||
try {
|
||||
const response = await fetchWithTrustedCertificatesAsync(`http://127.0.0.1:${port}/api/server/statistics`, {
|
||||
headers: headers as unknown as RequestInit["headers"],
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(200);
|
||||
expect(getReceivedHeaders()["x-api-key"]).toBe("immich-api-key");
|
||||
expect(getReceivedHeaders()["user-agent"]).toContain("Homarr/");
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
+6
-5
@@ -21,20 +21,18 @@ fi
|
||||
export AUTH_SECRET=$(openssl rand -base64 32)
|
||||
|
||||
# Start nginx proxy
|
||||
# 1. Replace the HOSTNAME in the nginx template file
|
||||
# 2. Create the nginx configuration file from the template
|
||||
# 3. Start the nginx server
|
||||
# 1. Create the nginx configuration file from the template
|
||||
# 2. Start the nginx server
|
||||
# Only listen on IPv6 when the host actually has IPv6 configured (#4596).
|
||||
# Otherwise nginx aborts with "socket() [::]:7575 failed (97: Address family
|
||||
# not supported by protocol)". Check HOMARR_DISABLE_IPV6 first so users can
|
||||
# force IPv4-only even when the host supports IPv6, then probe for IPv6
|
||||
# addresses via grep (not -s: proc files always report size 0).
|
||||
export HOSTNAME
|
||||
export NGINX_LISTEN_IPV6=''
|
||||
if [ "${HOMARR_DISABLE_IPV6:-false}" != "true" ] && grep -q . /proc/net/if_inet6 2>/dev/null; then
|
||||
NGINX_LISTEN_IPV6='listen [::]:7575;'
|
||||
fi
|
||||
envsubst '${HOSTNAME} ${NGINX_LISTEN_IPV6}' < /etc/nginx/templates/nginx.conf > /etc/nginx/nginx.conf
|
||||
envsubst '${NGINX_LISTEN_IPV6}' < /etc/nginx/templates/nginx.conf > /etc/nginx/nginx.conf
|
||||
# Start services in the background and store their PIDs
|
||||
nginx -g 'daemon off;' &
|
||||
NGINX_PID=$!
|
||||
@@ -67,6 +65,9 @@ terminate() {
|
||||
|
||||
trap terminate TERM INT
|
||||
|
||||
# Next.js standalone uses HOSTNAME as its bind address. Docker's generated
|
||||
# hostname can be unresolvable, so bind explicitly while nginx uses loopback.
|
||||
export HOSTNAME=0.0.0.0
|
||||
node apps/nextjs/server.js &
|
||||
NEXTJS_PID=$!
|
||||
|
||||
|
||||
Reference in new issue
Block a user