Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1419309a2d |
Generated
+187
-8
@@ -2,6 +2,12 @@
|
|||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "adler2"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.1.4"
|
version = "1.1.4"
|
||||||
@@ -84,6 +90,12 @@ version = "1.1.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "autocfg"
|
||||||
|
version = "1.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum"
|
name = "axum"
|
||||||
version = "0.7.9"
|
version = "0.7.9"
|
||||||
@@ -212,12 +224,24 @@ version = "3.20.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytemuck"
|
||||||
|
version = "1.25.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder"
|
name = "byteorder"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "byteorder-lite"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytes"
|
name = "bytes"
|
||||||
version = "1.11.1"
|
version = "1.11.1"
|
||||||
@@ -280,6 +304,12 @@ version = "1.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "color_quant"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "colorchoice"
|
name = "colorchoice"
|
||||||
version = "1.0.5"
|
version = "1.0.5"
|
||||||
@@ -295,6 +325,15 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crc32fast"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crypto-common"
|
name = "crypto-common"
|
||||||
version = "0.1.7"
|
version = "0.1.7"
|
||||||
@@ -406,6 +445,25 @@ version = "2.4.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
|
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fdeflate"
|
||||||
|
version = "0.3.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
|
||||||
|
dependencies = [
|
||||||
|
"simd-adler32",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flate2"
|
||||||
|
version = "1.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
|
||||||
|
dependencies = [
|
||||||
|
"crc32fast",
|
||||||
|
"miniz_oxide",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fnv"
|
name = "fnv"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
@@ -549,6 +607,16 @@ dependencies = [
|
|||||||
"wasip3",
|
"wasip3",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gif"
|
||||||
|
version = "0.14.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159"
|
||||||
|
dependencies = [
|
||||||
|
"color_quant",
|
||||||
|
"weezl",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "globset"
|
name = "globset"
|
||||||
version = "0.4.18"
|
version = "0.4.18"
|
||||||
@@ -821,6 +889,34 @@ dependencies = [
|
|||||||
"icu_properties",
|
"icu_properties",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "image"
|
||||||
|
version = "0.25.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
|
||||||
|
dependencies = [
|
||||||
|
"bytemuck",
|
||||||
|
"byteorder-lite",
|
||||||
|
"color_quant",
|
||||||
|
"gif",
|
||||||
|
"image-webp",
|
||||||
|
"moxcms",
|
||||||
|
"num-traits",
|
||||||
|
"png",
|
||||||
|
"zune-core",
|
||||||
|
"zune-jpeg",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "image-webp"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder-lite",
|
||||||
|
"quick-error",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.14.0"
|
version = "2.14.0"
|
||||||
@@ -958,6 +1054,16 @@ dependencies = [
|
|||||||
"unicase",
|
"unicase",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "miniz_oxide"
|
||||||
|
version = "0.8.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||||
|
dependencies = [
|
||||||
|
"adler2",
|
||||||
|
"simd-adler32",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@@ -969,6 +1075,16 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "moxcms"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
"pxfm",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "multer"
|
name = "multer"
|
||||||
version = "3.1.0"
|
version = "3.1.0"
|
||||||
@@ -1001,6 +1117,15 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967"
|
checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-traits"
|
||||||
|
version = "0.2.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.4"
|
version = "1.21.4"
|
||||||
@@ -1015,7 +1140,7 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openpxe"
|
name = "openpxe"
|
||||||
version = "0.4.6"
|
version = "0.4.61"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -1037,7 +1162,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openpxe-core"
|
name = "openpxe-core"
|
||||||
version = "0.4.6"
|
version = "0.4.61"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bcrypt",
|
"bcrypt",
|
||||||
@@ -1056,7 +1181,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openpxe-dhcp-proxy"
|
name = "openpxe-dhcp-proxy"
|
||||||
version = "0.4.6"
|
version = "0.4.61"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -1070,13 +1195,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openpxe-http-api"
|
name = "openpxe-http-api"
|
||||||
version = "0.4.6"
|
version = "0.4.61"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures",
|
"futures",
|
||||||
"hyper",
|
"hyper",
|
||||||
|
"image",
|
||||||
"mime",
|
"mime",
|
||||||
"mime_guess",
|
"mime_guess",
|
||||||
"openpxe-core",
|
"openpxe-core",
|
||||||
@@ -1100,7 +1226,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openpxe-ipxe-assets"
|
name = "openpxe-ipxe-assets"
|
||||||
version = "0.4.6"
|
version = "0.4.61"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"openpxe-core",
|
"openpxe-core",
|
||||||
"rust-embed",
|
"rust-embed",
|
||||||
@@ -1110,12 +1236,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openpxe-iso-store"
|
name = "openpxe-iso-store"
|
||||||
version = "0.4.6"
|
version = "0.4.61"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bcrypt",
|
"bcrypt",
|
||||||
"bytes",
|
"bytes",
|
||||||
"hex",
|
"hex",
|
||||||
|
"image",
|
||||||
"libc",
|
"libc",
|
||||||
"openpxe-core",
|
"openpxe-core",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
@@ -1133,7 +1260,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openpxe-tftp"
|
name = "openpxe-tftp"
|
||||||
version = "0.4.6"
|
version = "0.4.61"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -1147,7 +1274,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openpxe-webui"
|
name = "openpxe-webui"
|
||||||
version = "0.4.6"
|
version = "0.4.61"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
@@ -1184,6 +1311,19 @@ version = "0.2.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "png"
|
||||||
|
version = "0.18.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"crc32fast",
|
||||||
|
"fdeflate",
|
||||||
|
"flate2",
|
||||||
|
"miniz_oxide",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "potential_utf"
|
name = "potential_utf"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
@@ -1227,6 +1367,18 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pxfm"
|
||||||
|
version = "0.1.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quick-error"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.45"
|
version = "1.0.45"
|
||||||
@@ -1484,6 +1636,12 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simd-adler32"
|
||||||
|
version = "0.3.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.12"
|
version = "0.4.12"
|
||||||
@@ -2124,6 +2282,12 @@ dependencies = [
|
|||||||
"semver",
|
"semver",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "weezl"
|
||||||
|
version = "0.1.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi-util"
|
name = "winapi-util"
|
||||||
version = "0.1.11"
|
version = "0.1.11"
|
||||||
@@ -2438,3 +2602,18 @@ name = "zmij"
|
|||||||
version = "1.0.21"
|
version = "1.0.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zune-core"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zune-jpeg"
|
||||||
|
version = "0.5.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296"
|
||||||
|
dependencies = [
|
||||||
|
"zune-core",
|
||||||
|
]
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@ members = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.4.6"
|
version = "0.4.61"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.95"
|
rust-version = "1.95"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
|
|||||||
@@ -41,3 +41,6 @@ tower = { workspace = true }
|
|||||||
tempfile = "3.12"
|
tempfile = "3.12"
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
time = { workspace = true }
|
time = { workspace = true }
|
||||||
|
# v0.4.61: integration tests need to generate real PNG bytes for the
|
||||||
|
# `/branding/pxe-logo` compositor; hand-rolled CRCs are too fragile.
|
||||||
|
image = { version = "0.25", default-features = false, features = ["png"] }
|
||||||
|
|||||||
+108
-44
@@ -193,23 +193,47 @@ async fn api_sso_put(State(state): State<AppState>, Json(body): Json<SsoConfig>)
|
|||||||
// ─── UI ────────────────────────────────────────────────────────────────────
|
// ─── UI ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async fn index(State(state): State<AppState>) -> Response {
|
async fn index(State(state): State<AppState>) -> Response {
|
||||||
let html = openpxe_webui::index_html(&state.public_base_url);
|
// The asset version pin in index.html (`?v=…`) is what makes
|
||||||
|
// browsers re-fetch JS/CSS after an upgrade. We use the OpenPXE
|
||||||
|
// binary version — every release ships a new value, every release
|
||||||
|
// forces a fresh URL on each asset.
|
||||||
|
let html = openpxe_webui::index_html(&state.public_base_url, env!("CARGO_PKG_VERSION"));
|
||||||
(
|
(
|
||||||
[(
|
[
|
||||||
header::CONTENT_TYPE,
|
(
|
||||||
HeaderValue::from_static("text/html; charset=utf-8"),
|
header::CONTENT_TYPE,
|
||||||
)],
|
HeaderValue::from_static("text/html; charset=utf-8"),
|
||||||
|
),
|
||||||
|
// index.html itself must never be cached — that's how the
|
||||||
|
// browser learns about a new `?v=…` value for the assets.
|
||||||
|
(
|
||||||
|
header::CACHE_CONTROL,
|
||||||
|
HeaderValue::from_static("no-cache, must-revalidate"),
|
||||||
|
),
|
||||||
|
],
|
||||||
html,
|
html,
|
||||||
)
|
)
|
||||||
.into_response()
|
.into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Cache-Control header value used for the bundled JS/CSS/SVG assets.
|
||||||
|
/// We pin a 1-day TTL so a long-lived deployment doesn't re-fetch the
|
||||||
|
/// same bytes on every page-load, but require revalidation — combined
|
||||||
|
/// with the `?v=<version>` query string in index.html, the practical
|
||||||
|
/// upper bound on caching across an upgrade is "until the operator
|
||||||
|
/// reloads".
|
||||||
|
const ASSET_CACHE_CONTROL: HeaderValue =
|
||||||
|
HeaderValue::from_static("no-cache, must-revalidate");
|
||||||
|
|
||||||
async fn ui_js() -> Response {
|
async fn ui_js() -> Response {
|
||||||
(
|
(
|
||||||
[(
|
[
|
||||||
header::CONTENT_TYPE,
|
(
|
||||||
HeaderValue::from_static("application/javascript"),
|
header::CONTENT_TYPE,
|
||||||
)],
|
HeaderValue::from_static("application/javascript"),
|
||||||
|
),
|
||||||
|
(header::CACHE_CONTROL, ASSET_CACHE_CONTROL),
|
||||||
|
],
|
||||||
openpxe_webui::app_js(),
|
openpxe_webui::app_js(),
|
||||||
)
|
)
|
||||||
.into_response()
|
.into_response()
|
||||||
@@ -217,7 +241,10 @@ async fn ui_js() -> Response {
|
|||||||
|
|
||||||
async fn ui_css() -> Response {
|
async fn ui_css() -> Response {
|
||||||
(
|
(
|
||||||
[(header::CONTENT_TYPE, HeaderValue::from_static("text/css"))],
|
[
|
||||||
|
(header::CONTENT_TYPE, HeaderValue::from_static("text/css")),
|
||||||
|
(header::CACHE_CONTROL, ASSET_CACHE_CONTROL),
|
||||||
|
],
|
||||||
openpxe_webui::app_css(),
|
openpxe_webui::app_css(),
|
||||||
)
|
)
|
||||||
.into_response()
|
.into_response()
|
||||||
@@ -261,20 +288,29 @@ async fn ui_logo(State(state): State<AppState>) -> Response {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
(
|
(
|
||||||
[(
|
[
|
||||||
header::CONTENT_TYPE,
|
(
|
||||||
HeaderValue::from_static("image/svg+xml"),
|
header::CONTENT_TYPE,
|
||||||
)],
|
HeaderValue::from_static("image/svg+xml"),
|
||||||
|
),
|
||||||
|
(header::CACHE_CONTROL, ASSET_CACHE_CONTROL),
|
||||||
|
],
|
||||||
openpxe_webui::logo_svg(),
|
openpxe_webui::logo_svg(),
|
||||||
)
|
)
|
||||||
.into_response()
|
.into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// v0.4.6: raster-only logo endpoint for the iPXE menu's
|
/// v0.4.61: PXE menu logo composed for the iPXE `console --picture`
|
||||||
/// `console --picture`. iPXE can't rasterize SVG, so SVG uploads 404
|
/// call. The operator can upload any raster image (PNG / JPEG / WebP /
|
||||||
/// here — the ASCII OpenPXE wordmark in `render_menu` already gives
|
/// GIF) of any aspect ratio; this handler decodes it, draws it
|
||||||
/// the operator a polished default. No bundled PNG fallback by design:
|
/// centered-top onto a fixed 1024×768 canvas, and returns PNG bytes.
|
||||||
/// either the operator's raster logo paints, or the text stands in.
|
/// That gives the same look as iVentoy regardless of what the operator
|
||||||
|
/// uploaded — a portrait logo, a wide wordmark, a square monogram all
|
||||||
|
/// land in the same place on the boot screen.
|
||||||
|
///
|
||||||
|
/// SVG uploads still 404 here — iPXE can't rasterize SVG, and rather
|
||||||
|
/// than haul in `resvg` we ask the operator to provide a raster when
|
||||||
|
/// they want a custom PXE-side logo. (The WebUI keeps using the SVG.)
|
||||||
async fn ui_pxe_logo(State(state): State<AppState>) -> Response {
|
async fn ui_pxe_logo(State(state): State<AppState>) -> Response {
|
||||||
let Some(path) = state.branding.logo_path() else {
|
let Some(path) = state.branding.logo_path() else {
|
||||||
return (StatusCode::NOT_FOUND, "no custom logo configured").into_response();
|
return (StatusCode::NOT_FOUND, "no custom logo configured").into_response();
|
||||||
@@ -285,40 +321,68 @@ async fn ui_pxe_logo(State(state): State<AppState>) -> Response {
|
|||||||
if mime == "image/svg+xml" {
|
if mime == "image/svg+xml" {
|
||||||
return (
|
return (
|
||||||
StatusCode::NOT_FOUND,
|
StatusCode::NOT_FOUND,
|
||||||
"operator-uploaded logo is SVG; iPXE menu falls back to the bundled ASCII wordmark",
|
"operator-uploaded logo is SVG; PXE menu requires a raster (PNG / JPEG / WebP / GIF)",
|
||||||
)
|
)
|
||||||
.into_response();
|
.into_response();
|
||||||
}
|
}
|
||||||
match tokio::fs::read(&path).await {
|
let bytes = match tokio::fs::read(&path).await {
|
||||||
Ok(bytes) => {
|
Ok(b) => b,
|
||||||
let ct = HeaderValue::from_str(&mime)
|
Err(e) => {
|
||||||
.unwrap_or_else(|_| HeaderValue::from_static("application/octet-stream"));
|
return (
|
||||||
(
|
StatusCode::NOT_FOUND,
|
||||||
[
|
format!("custom logo unreadable: {e}"),
|
||||||
(header::CONTENT_TYPE, ct),
|
|
||||||
(
|
|
||||||
header::CACHE_CONTROL,
|
|
||||||
HeaderValue::from_static("no-cache, max-age=0"),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
bytes,
|
|
||||||
)
|
)
|
||||||
.into_response()
|
.into_response();
|
||||||
}
|
}
|
||||||
Err(e) => (
|
};
|
||||||
StatusCode::NOT_FOUND,
|
// Compose to a fixed 1024×768 PNG so the PXE menu paints the logo
|
||||||
format!("custom logo unreadable: {e}"),
|
// centered-top regardless of the operator's source dimensions. The
|
||||||
)
|
// `image` crate is pure-Rust + sync; offload to a blocking task
|
||||||
.into_response(),
|
// because Lanczos resampling on a 4K source can take tens of
|
||||||
}
|
// milliseconds and we don't want to block the executor.
|
||||||
|
let composed =
|
||||||
|
match tokio::task::spawn_blocking(move || openpxe_iso_store::pxe_logo::compose_pxe_logo(&bytes))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(Ok(png)) => png,
|
||||||
|
Ok(Err(e)) => {
|
||||||
|
tracing::warn!(
|
||||||
|
target: "openpxe::http::branding",
|
||||||
|
error = %e, "failed to compose PXE logo PNG"
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!("failed to compose PXE logo: {e}"),
|
||||||
|
)
|
||||||
|
.into_response();
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
return (
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!("pxe-logo task failed: {e}"),
|
||||||
|
)
|
||||||
|
.into_response();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
(
|
||||||
|
[
|
||||||
|
(header::CONTENT_TYPE, HeaderValue::from_static("image/png")),
|
||||||
|
(header::CACHE_CONTROL, ASSET_CACHE_CONTROL),
|
||||||
|
],
|
||||||
|
composed,
|
||||||
|
)
|
||||||
|
.into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn ui_loader() -> Response {
|
async fn ui_loader() -> Response {
|
||||||
(
|
(
|
||||||
[(
|
[
|
||||||
header::CONTENT_TYPE,
|
(
|
||||||
HeaderValue::from_static("image/svg+xml"),
|
header::CONTENT_TYPE,
|
||||||
)],
|
HeaderValue::from_static("image/svg+xml"),
|
||||||
|
),
|
||||||
|
(header::CACHE_CONTROL, ASSET_CACHE_CONTROL),
|
||||||
|
],
|
||||||
openpxe_webui::loader_svg(),
|
openpxe_webui::loader_svg(),
|
||||||
)
|
)
|
||||||
.into_response()
|
.into_response()
|
||||||
|
|||||||
@@ -77,17 +77,15 @@ pub fn render_menu(isos: &[IsoMeta], settings: &Settings, base_url: &str) -> Str
|
|||||||
);
|
);
|
||||||
let _ = writeln!(s, ":menu");
|
let _ = writeln!(s, ":menu");
|
||||||
let _ = writeln!(s, "menu OpenPXE - network boot menu");
|
let _ = writeln!(s, "menu OpenPXE - network boot menu");
|
||||||
// Centered ASCII wordmark. iPXE menus are ~76 columns wide on the
|
// v0.4.61: we used to draw an ASCII OpenPXE wordmark here. Now
|
||||||
// default VGA text console; the lines below are padded to sit
|
// that the bundled iPXE is built with `IMAGE_PNG`, the
|
||||||
// approximately centered. `item --gap -- <text>` emits text without
|
// `console --picture` line at the top of this script paints the
|
||||||
// a selectable hotkey.
|
// operator's actual logo (composed server-side into a 1024×768
|
||||||
let _ = writeln!(s, "item --gap");
|
// canvas with the logo centered at the top) — the ASCII banner
|
||||||
let _ = writeln!(s, "item --gap -- ___ ___ __ __ ___");
|
// became visual noise *on top* of the real image. Old iPXE
|
||||||
let _ = writeln!(s, "item --gap -- / _ \\ _ __ ___ _ _ | _ \\ \\/ / | __|");
|
// builds without PNG fall through the `|| console` clause and
|
||||||
let _ = writeln!(s, "item --gap -- | (_) | '_ \\/ -_) ' \\ | _/ \\ / | _|");
|
// simply show the menu without a logo, which is the correct
|
||||||
let _ = writeln!(s, "item --gap -- \\___/| .__/\\___|_||_| |_| /_/\\_\\ |___|");
|
// graceful-degradation outcome.
|
||||||
let _ = writeln!(s, "item --gap -- |_|");
|
|
||||||
let _ = writeln!(s, "item --gap");
|
|
||||||
let _ = writeln!(
|
let _ = writeln!(
|
||||||
s,
|
s,
|
||||||
"item --gap -- ------------------------- Default -------------------------"
|
"item --gap -- ------------------------- Default -------------------------"
|
||||||
@@ -628,10 +626,12 @@ mod password_tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn top_menu_has_polished_branding_and_arch_footer() {
|
fn top_menu_has_polished_branding_and_arch_footer() {
|
||||||
// v0.4.6 polish: a `console --picture` line for operator
|
// v0.4.6 polish + v0.4.61 image upgrade: the menu emits a
|
||||||
// logos, an ASCII OpenPXE wordmark visible across iPXE
|
// `console --picture` line that the bundled iPXE (built with
|
||||||
// builds (graphics or not), and a single-line footer carrying
|
// `IMAGE_PNG`) honours, plus an arch-resolved footer carrying
|
||||||
// the current OpenPXE version + the resolved arch label.
|
// the current OpenPXE version. The ASCII wordmark that used
|
||||||
|
// to live here was dropped in v0.4.61 — it duplicated the now-
|
||||||
|
// working image.
|
||||||
let settings = Settings::default();
|
let settings = Settings::default();
|
||||||
let s = render_menu(&[], &settings, "http://10.0.0.5");
|
let s = render_menu(&[], &settings, "http://10.0.0.5");
|
||||||
assert!(
|
assert!(
|
||||||
@@ -641,11 +641,11 @@ mod password_tests {
|
|||||||
// Picture-or-text-console must be a single statement so older
|
// Picture-or-text-console must be a single statement so older
|
||||||
// iPXE parsers don't choke on the chain.
|
// iPXE parsers don't choke on the chain.
|
||||||
assert!(s.contains("|| console"), "missing graceful fallback:\n{s}");
|
assert!(s.contains("|| console"), "missing graceful fallback:\n{s}");
|
||||||
// ASCII wordmark — at least one of the banner lines must
|
// No ASCII wordmark — once the real PNG paints, the ASCII
|
||||||
// contain the trailing pipe segment, plus the leading "_"s.
|
// banner would duplicate the operator's logo visually.
|
||||||
assert!(
|
assert!(
|
||||||
s.contains("___ ___ __ __ ___"),
|
!s.contains("___ ___ __ __ ___"),
|
||||||
"ascii banner missing first row:\n{s}"
|
"ASCII banner shouldn't be emitted in v0.4.61+:\n{s}"
|
||||||
);
|
);
|
||||||
// Footer with version + arch interpolation. The version comes
|
// Footer with version + arch interpolation. The version comes
|
||||||
// from CARGO_PKG_VERSION at compile time.
|
// from CARGO_PKG_VERSION at compile time.
|
||||||
|
|||||||
@@ -1755,11 +1755,26 @@ async fn pxe_logo_404_when_no_custom_logo_configured() {
|
|||||||
assert!(text.contains("no custom logo"), "got: {text}");
|
assert!(text.contains("no custom logo"), "got: {text}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build a tiny valid PNG via the `image` crate. The v0.4.61 PXE-logo
|
||||||
|
/// compositor decodes whatever the operator uploaded — hand-rolled
|
||||||
|
/// PNGs with handwritten CRCs are too easy to break; let the encoder
|
||||||
|
/// produce something it can later decode.
|
||||||
|
fn tiny_png() -> Vec<u8> {
|
||||||
|
use image::{DynamicImage, ImageBuffer, ImageFormat, Rgb};
|
||||||
|
use std::io::Cursor;
|
||||||
|
let buf: ImageBuffer<Rgb<u8>, Vec<u8>> = ImageBuffer::from_pixel(8, 8, Rgb([0, 180, 220]));
|
||||||
|
let mut out = Vec::with_capacity(256);
|
||||||
|
DynamicImage::ImageRgb8(buf)
|
||||||
|
.write_to(&mut Cursor::new(&mut out), ImageFormat::Png)
|
||||||
|
.unwrap();
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn pxe_logo_404_when_uploaded_logo_is_svg() {
|
async fn pxe_logo_404_when_uploaded_logo_is_svg() {
|
||||||
// iPXE can't rasterize SVG, so an SVG upload deliberately doesn't
|
// iPXE can't rasterize SVG, so an SVG upload deliberately doesn't
|
||||||
// light up the PXE menu's `console --picture` overlay — the ASCII
|
// light up the PXE menu's `console --picture` overlay — the menu
|
||||||
// wordmark in render_menu stands in instead.
|
// simply paints without a logo.
|
||||||
let (state, _dir) = build_state().await;
|
let (state, _dir) = build_state().await;
|
||||||
state
|
state
|
||||||
.branding
|
.branding
|
||||||
@@ -1777,11 +1792,15 @@ async fn pxe_logo_404_when_uploaded_logo_is_svg() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn pxe_logo_serves_raster_with_correct_mime() {
|
async fn pxe_logo_composes_to_1024x768_png() {
|
||||||
|
// v0.4.61: the endpoint no longer serves the raw upload — it
|
||||||
|
// composes the operator's logo into a fixed 1024×768 canvas so
|
||||||
|
// the iPXE menu always paints at consistent dimensions.
|
||||||
let (state, _dir) = build_state().await;
|
let (state, _dir) = build_state().await;
|
||||||
|
let png = tiny_png();
|
||||||
state
|
state
|
||||||
.branding
|
.branding
|
||||||
.set_logo("image/png", "png", b"\x89PNG\r\n\x1a\nfake-png-bytes")
|
.set_logo("image/png", "png", &png)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let app = build_router(state);
|
let app = build_router(state);
|
||||||
let res = app
|
let res = app
|
||||||
@@ -1805,7 +1824,14 @@ async fn pxe_logo_serves_raster_with_correct_mime() {
|
|||||||
let body = axum::body::to_bytes(res.into_body(), usize::MAX)
|
let body = axum::body::to_bytes(res.into_body(), usize::MAX)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
// PNG signature.
|
||||||
assert!(body.starts_with(b"\x89PNG"), "PNG header missing");
|
assert!(body.starts_with(b"\x89PNG"), "PNG header missing");
|
||||||
|
// IHDR chunk lives at bytes 8..29; width is bytes 16..20, height
|
||||||
|
// 20..24 in big-endian u32. The composed canvas should be 1024×768.
|
||||||
|
let width = u32::from_be_bytes([body[16], body[17], body[18], body[19]]);
|
||||||
|
let height = u32::from_be_bytes([body[20], body[21], body[22], body[23]]);
|
||||||
|
assert_eq!(width, 1024, "compose should pin width to 1024");
|
||||||
|
assert_eq!(height, 768, "compose should pin height to 768");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -1814,9 +1840,10 @@ async fn pxe_logo_endpoint_is_public_after_admin_setup() {
|
|||||||
// must stay reachable once the admin has been bootstrapped. The
|
// must stay reachable once the admin has been bootstrapped. The
|
||||||
// auth allowlist gates `/api/*` only.
|
// auth allowlist gates `/api/*` only.
|
||||||
let (state, _dir) = build_state().await;
|
let (state, _dir) = build_state().await;
|
||||||
|
let png = tiny_png();
|
||||||
state
|
state
|
||||||
.branding
|
.branding
|
||||||
.set_logo("image/png", "png", b"\x89PNG\r\n\x1a\nfake")
|
.set_logo("image/png", "png", &png)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let app = build_router(state);
|
let app = build_router(state);
|
||||||
// Configure an admin so the middleware kicks in.
|
// Configure an admin so the middleware kicks in.
|
||||||
|
|||||||
@@ -27,6 +27,13 @@ parking_lot.workspace = true
|
|||||||
bytes.workspace = true
|
bytes.workspace = true
|
||||||
tempfile = "3.12"
|
tempfile = "3.12"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
|
# v0.4.61: server-side compose of the operator's uploaded raster into a
|
||||||
|
# fixed 1024x768 canvas so the PXE menu always gets a consistently-sized
|
||||||
|
# PNG regardless of what the operator uploaded. We use the bare-bones
|
||||||
|
# `image` crate (no default features) and explicitly enable only the
|
||||||
|
# decoders we accept on upload (PNG/JPEG/WebP/GIF) plus the PNG
|
||||||
|
# encoder. Keeps the build slim — no JPEG2000, TIFF, BMP, etc.
|
||||||
|
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3.12"
|
tempfile = "3.12"
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
pub mod entry;
|
pub mod entry;
|
||||||
pub mod introspect;
|
pub mod introspect;
|
||||||
pub mod nfs;
|
pub mod nfs;
|
||||||
|
pub mod pxe_logo;
|
||||||
pub mod smb;
|
pub mod smb;
|
||||||
pub mod store;
|
pub mod store;
|
||||||
pub mod windows;
|
pub mod windows;
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
//! Operator-logo compositor for the iPXE menu.
|
||||||
|
//!
|
||||||
|
//! The brief: match iVentoy's polished centered-logo PXE chrome with
|
||||||
|
//! whatever raster the operator drops onto Settings → Branding. A wide
|
||||||
|
//! wordmark, a portrait stack, a square monogram — all three should
|
||||||
|
//! land in roughly the same place on the boot screen.
|
||||||
|
//!
|
||||||
|
//! Approach: decode the operator's upload, fit it into a fixed
|
||||||
|
//! 1024×768 canvas with the logo horizontally centered and pinned a
|
||||||
|
//! short margin from the top, re-encode as PNG, return the bytes. iPXE
|
||||||
|
//! built with `IMAGE_PNG` paints the result via `console --picture`.
|
||||||
|
//!
|
||||||
|
//! The 1024×768 size matches the default VESA framebuffer iPXE picks
|
||||||
|
//! on most BIOS/UEFI consoles. Operators uploading 4K logos get
|
||||||
|
//! correctly downscaled; tiny icons get drawn at their native size,
|
||||||
|
//! centered, with transparent margins.
|
||||||
|
//!
|
||||||
|
//! We deliberately don't ship `resvg` for SVG support — keeping the
|
||||||
|
//! dependency surface narrow matters more than supporting SVG-only
|
||||||
|
//! brand assets. The WebUI's logo stays SVG-native (the browser
|
||||||
|
//! rasterizes it); the PXE menu wants a raster regardless.
|
||||||
|
|
||||||
|
use image::imageops::FilterType;
|
||||||
|
use image::{DynamicImage, ImageError, ImageFormat, Rgba, RgbaImage};
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
/// Canvas dimensions used for the composed PXE logo. Picked to match
|
||||||
|
/// the framebuffer dimensions iPXE picks on most BIOS/UEFI consoles —
|
||||||
|
/// gives a 1:1 paint with no scaling at the firmware layer.
|
||||||
|
pub const CANVAS_W: u32 = 1024;
|
||||||
|
pub const CANVAS_H: u32 = 768;
|
||||||
|
|
||||||
|
/// Maximum dimensions for the operator's logo inside the canvas. Any
|
||||||
|
/// upload larger than this in either axis is downscaled (preserving
|
||||||
|
/// aspect ratio) to fit. Smaller uploads paint at native size.
|
||||||
|
const LOGO_MAX_W: u32 = 600;
|
||||||
|
const LOGO_MAX_H: u32 = 200;
|
||||||
|
|
||||||
|
/// Top margin in pixels from the canvas's top edge to the logo's top
|
||||||
|
/// edge. Matches the visual rhythm of iVentoy's screen (logo at top,
|
||||||
|
/// menu below).
|
||||||
|
const LOGO_TOP_MARGIN: u32 = 64;
|
||||||
|
|
||||||
|
/// Compose `src_bytes` (any PNG/JPEG/WebP/GIF) into a centered-top
|
||||||
|
/// 1024×768 PNG and return the encoded bytes.
|
||||||
|
///
|
||||||
|
/// Errors when the source can't be decoded or the encoded buffer can't
|
||||||
|
/// be written (only really fires on out-of-memory; the encoder itself
|
||||||
|
/// is infallible for well-formed inputs).
|
||||||
|
pub fn compose_pxe_logo(src_bytes: &[u8]) -> Result<Vec<u8>, ImageError> {
|
||||||
|
let logo = image::load_from_memory(src_bytes)?;
|
||||||
|
// Resize-fit if the upload exceeds our bounding box. `Lanczos3`
|
||||||
|
// keeps the antialiasing crisp on the framebuffer console; it's a
|
||||||
|
// touch slower than `Triangle` but the operator hits this endpoint
|
||||||
|
// once per boot at most.
|
||||||
|
let logo = downscale_to_fit(logo, LOGO_MAX_W, LOGO_MAX_H);
|
||||||
|
let logo_rgba = logo.to_rgba8();
|
||||||
|
|
||||||
|
// Transparent canvas. iPXE 1.21+ honours alpha-channel transparency
|
||||||
|
// on framebuffer consoles; older builds simply draw the alpha as
|
||||||
|
// black, which still gives a sensible look.
|
||||||
|
let mut canvas: RgbaImage = RgbaImage::from_pixel(CANVAS_W, CANVAS_H, Rgba([0, 0, 0, 0]));
|
||||||
|
let logo_w = logo_rgba.width();
|
||||||
|
let logo_h = logo_rgba.height();
|
||||||
|
// Horizontal center, top-margin from the top. Saturating math
|
||||||
|
// means a logo wider than CANVAS_W (shouldn't happen after the
|
||||||
|
// downscale above, but defensive) just sits flush-left.
|
||||||
|
let off_x = CANVAS_W.saturating_sub(logo_w) / 2;
|
||||||
|
let off_y = LOGO_TOP_MARGIN.min(CANVAS_H.saturating_sub(logo_h));
|
||||||
|
image::imageops::overlay(&mut canvas, &logo_rgba, off_x.into(), off_y.into());
|
||||||
|
|
||||||
|
let mut out = Vec::with_capacity(64 * 1024);
|
||||||
|
DynamicImage::ImageRgba8(canvas).write_to(&mut Cursor::new(&mut out), ImageFormat::Png)?;
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn downscale_to_fit(img: DynamicImage, max_w: u32, max_h: u32) -> DynamicImage {
|
||||||
|
let (w, h) = (img.width(), img.height());
|
||||||
|
if w <= max_w && h <= max_h {
|
||||||
|
return img;
|
||||||
|
}
|
||||||
|
// Preserve aspect ratio. `resize` clamps to the smaller of the
|
||||||
|
// two scale factors so we never overshoot the bounding box.
|
||||||
|
img.resize(max_w, max_h, FilterType::Lanczos3)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use image::{ImageBuffer, Rgb};
|
||||||
|
|
||||||
|
fn solid_png(w: u32, h: u32, rgb: [u8; 3]) -> Vec<u8> {
|
||||||
|
let img: ImageBuffer<Rgb<u8>, Vec<u8>> = ImageBuffer::from_pixel(w, h, Rgb(rgb));
|
||||||
|
let mut out = Vec::with_capacity(4096);
|
||||||
|
DynamicImage::ImageRgb8(img)
|
||||||
|
.write_to(&mut Cursor::new(&mut out), ImageFormat::Png)
|
||||||
|
.unwrap();
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn compose_emits_canvas_sized_png() {
|
||||||
|
let src = solid_png(120, 60, [200, 50, 50]);
|
||||||
|
let out = compose_pxe_logo(&src).unwrap();
|
||||||
|
// Round-trip the output and confirm dimensions.
|
||||||
|
let img = image::load_from_memory(&out).unwrap();
|
||||||
|
assert_eq!(img.width(), CANVAS_W);
|
||||||
|
assert_eq!(img.height(), CANVAS_H);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn small_logo_centered_at_top_margin() {
|
||||||
|
let src = solid_png(100, 40, [10, 200, 10]);
|
||||||
|
let out = compose_pxe_logo(&src).unwrap();
|
||||||
|
let canvas = image::load_from_memory(&out).unwrap().to_rgba8();
|
||||||
|
// Pixel just inside the logo box should match the source color
|
||||||
|
// (alpha=255). Pixel near a far corner of the canvas should be
|
||||||
|
// the transparent background.
|
||||||
|
let cx = (CANVAS_W - 100) / 2;
|
||||||
|
let cy = LOGO_TOP_MARGIN;
|
||||||
|
let inside = canvas.get_pixel(cx + 10, cy + 10);
|
||||||
|
assert_eq!(inside.0[3], 255, "logo pixel should be opaque");
|
||||||
|
assert!(inside.0[0] < 100 && inside.0[1] > 100 && inside.0[2] < 100, "color mismatch: {inside:?}");
|
||||||
|
let corner = canvas.get_pixel(CANVAS_W - 1, CANVAS_H - 1);
|
||||||
|
assert_eq!(corner.0[3], 0, "canvas corner should be transparent");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn oversize_logo_is_downscaled_to_bounding_box() {
|
||||||
|
// 4000×800 image — bigger than LOGO_MAX_W and LOGO_MAX_H in
|
||||||
|
// both axes. After downscale the output must fit; we re-decode
|
||||||
|
// the canvas, count non-transparent pixels, and confirm none
|
||||||
|
// sit outside the expected band.
|
||||||
|
let src = solid_png(4000, 800, [50, 50, 200]);
|
||||||
|
let out = compose_pxe_logo(&src).unwrap();
|
||||||
|
let canvas = image::load_from_memory(&out).unwrap().to_rgba8();
|
||||||
|
// Span row at the top margin should have non-transparent
|
||||||
|
// pixels somewhere; rows past the LOGO_TOP_MARGIN + LOGO_MAX_H
|
||||||
|
// should be entirely transparent.
|
||||||
|
let bottom_band_y = LOGO_TOP_MARGIN + LOGO_MAX_H + 10;
|
||||||
|
for x in 0..CANVAS_W {
|
||||||
|
let p = canvas.get_pixel(x, bottom_band_y);
|
||||||
|
assert_eq!(p.0[3], 0, "row {bottom_band_y} should be transparent at x={x}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unsupported_bytes_returns_error_not_panic() {
|
||||||
|
let r = compose_pxe_logo(b"\xde\xad\xbe\xef not an image");
|
||||||
|
assert!(r.is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,8 +5,15 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="color-scheme" content="dark light" />
|
<meta name="color-scheme" content="dark light" />
|
||||||
<title>OpenPXE</title>
|
<title>OpenPXE</title>
|
||||||
<link rel="stylesheet" href="/assets/app.css" />
|
<!-- v0.4.61: the `?v=…` query string is replaced by the server at
|
||||||
<link rel="icon" type="image/svg+xml" href="/assets/logo.svg" />
|
request time with the running OpenPXE version. That guarantees a
|
||||||
|
fresh URL on every upgrade so browsers (and intermediary proxies)
|
||||||
|
can't keep serving stale JS / CSS / branding from before the
|
||||||
|
deploy. Combined with `Cache-Control: no-cache, must-revalidate`
|
||||||
|
on the asset handlers, the practical caching window is one
|
||||||
|
version. -->
|
||||||
|
<link rel="stylesheet" href="/assets/app.css?v={{ASSET_VERSION}}" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/assets/logo.svg?v={{ASSET_VERSION}}" />
|
||||||
<!-- Theme is read from localStorage *before* paint to avoid the
|
<!-- Theme is read from localStorage *before* paint to avoid the
|
||||||
dark→light flash on every navigation. Falls back to the OS
|
dark→light flash on every navigation. Falls back to the OS
|
||||||
preference and finally to dark. -->
|
preference and finally to dark. -->
|
||||||
@@ -26,7 +33,7 @@
|
|||||||
<div class="shell">
|
<div class="shell">
|
||||||
<aside class="sidebar">
|
<aside class="sidebar">
|
||||||
<div class="brand">
|
<div class="brand">
|
||||||
<img src="/assets/logo.svg" alt="OpenPXE" />
|
<img src="/assets/logo.svg?v={{ASSET_VERSION}}" alt="OpenPXE" />
|
||||||
<strong>OpenPXE</strong>
|
<strong>OpenPXE</strong>
|
||||||
</div>
|
</div>
|
||||||
<nav>
|
<nav>
|
||||||
@@ -59,7 +66,7 @@
|
|||||||
<!-- The brand badge at the top can be overridden by operator-uploaded
|
<!-- The brand badge at the top can be overridden by operator-uploaded
|
||||||
logos; keep "OpenPXE v…" pinned in the footer so the backend
|
logos; keep "OpenPXE v…" pinned in the footer so the backend
|
||||||
identity is always visible regardless of branding. -->
|
identity is always visible regardless of branding. -->
|
||||||
<div class="footer-version">OpenPXE v<span data-bind="version">0.4.6</span></div>
|
<div class="footer-version">OpenPXE v<span data-bind="version">0.4.61</span></div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
@@ -115,6 +122,6 @@
|
|||||||
<main class="main" id="view-root"></main>
|
<main class="main" id="view-root"></main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/assets/app.js"></script>
|
<script src="/assets/app.js?v={{ASSET_VERSION}}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+14
-4
@@ -7,11 +7,21 @@
|
|||||||
//! nav, top bar with secondary tabs, card-dense content panels.
|
//! nav, top bar with secondary tabs, card-dense content panels.
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
/// Render the top-level page. `base_url` is interpolated into the footer
|
/// Render the top-level page.
|
||||||
/// so operators can see at a glance what URL clients are PXE-booting from.
|
///
|
||||||
|
/// * `base_url` is interpolated into the footer so operators can see at
|
||||||
|
/// a glance what URL clients are PXE-booting from.
|
||||||
|
/// * `asset_version` is appended as `?v=…` to every asset URL so each
|
||||||
|
/// release ships with brand-new asset URLs — browsers (and any
|
||||||
|
/// intermediary proxy) can't keep serving last release's `app.js`
|
||||||
|
/// when we know the new one is incompatible. Combined with
|
||||||
|
/// `Cache-Control: no-cache, must-revalidate` on the asset handlers,
|
||||||
|
/// the worst-case caching window is one version.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn index_html(base_url: &str) -> String {
|
pub fn index_html(base_url: &str, asset_version: &str) -> String {
|
||||||
INDEX_HTML.replace("{{BASE_URL}}", base_url)
|
INDEX_HTML
|
||||||
|
.replace("{{BASE_URL}}", base_url)
|
||||||
|
.replace("{{ASSET_VERSION}}", asset_version)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
|
|||||||
@@ -16,13 +16,79 @@
|
|||||||
|
|
||||||
ARG RUST_VERSION=1.95
|
ARG RUST_VERSION=1.95
|
||||||
|
|
||||||
########## fetch iPXE binaries ##########
|
########## fetch wimboot (and a sanity-check fetch of upstream iPXE) ##########
|
||||||
|
# v0.4.61: we no longer ship the boot.ipxe.org iPXE binaries directly;
|
||||||
|
# instead we build iPXE from source with IMAGE_PNG enabled (see the
|
||||||
|
# ipxe-build stage below). The fetch stage still pulls wimboot (a
|
||||||
|
# pre-signed binary from ipxe/wimboot's GitHub release) since that's
|
||||||
|
# unrelated to the PNG concern.
|
||||||
FROM debian:12-slim AS fetch
|
FROM debian:12-slim AS fetch
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates \
|
RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY scripts/fetch-ipxe.sh scripts/fetch-ipxe.sh
|
RUN mkdir -p assets/ipxe && \
|
||||||
RUN mkdir -p assets/ipxe && bash scripts/fetch-ipxe.sh
|
curl --fail --silent --show-error --location \
|
||||||
|
-o assets/ipxe/wimboot \
|
||||||
|
https://github.com/ipxe/wimboot/releases/latest/download/wimboot \
|
||||||
|
|| echo "wimboot fetch failed; Windows toggle will stay disabled"
|
||||||
|
|
||||||
|
########## build iPXE from source with IMAGE_PNG enabled ##########
|
||||||
|
# This stage replaces the old "grab pre-built binaries from
|
||||||
|
# boot.ipxe.org" path. The shipped binaries there are built with the
|
||||||
|
# default config which omits `IMAGE_PNG`, so the `console --picture`
|
||||||
|
# call in render_menu silently no-ops — operator logos never paint.
|
||||||
|
# Building from source lets us flip the one flag we need.
|
||||||
|
#
|
||||||
|
# Cross-compilation: x86_64 + i386 use the native toolchain that ships
|
||||||
|
# in the rust:bookworm base; arm64 uses gcc-aarch64-linux-gnu. The four
|
||||||
|
# output binaries match the names openpxe-ipxe-assets expects in
|
||||||
|
# assets/ipxe/.
|
||||||
|
FROM rust:${RUST_VERSION}-bookworm AS ipxe-build
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
git build-essential liblzma-dev mtools genisoimage syslinux \
|
||||||
|
gcc-aarch64-linux-gnu \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
WORKDIR /build
|
||||||
|
# Pin to a recent iPXE master tip via shallow clone. iPXE doesn't tag
|
||||||
|
# releases; pinning the SHA in source would be a periodic chore. The
|
||||||
|
# tradeoff is that "rebuild the container" silently picks up upstream
|
||||||
|
# patches — for a boot loader this is the right side of the
|
||||||
|
# pin-vs-fresh tradeoff (we want CVE fixes ASAP and the PXE chain is
|
||||||
|
# the trusted base).
|
||||||
|
RUN git clone --depth=1 https://github.com/ipxe/ipxe.git ipxe
|
||||||
|
WORKDIR /build/ipxe/src
|
||||||
|
# Feature flags landed via the `config/local/` override files iPXE's
|
||||||
|
# config system reads after `config/general.h`. We enable just the
|
||||||
|
# image format + framebuffer console plumbing — everything else stays
|
||||||
|
# at the upstream default. `keep-debug` is off; `parserrors` is off; we
|
||||||
|
# pin a small set of useful tweaks.
|
||||||
|
RUN mkdir -p config/local \
|
||||||
|
&& printf '%s\n' \
|
||||||
|
'#define IMAGE_PNG' \
|
||||||
|
'#define CONSOLE_FRAMEBUFFER' \
|
||||||
|
'#define CONSOLE_VESAFB' \
|
||||||
|
'#define DOWNLOAD_PROTO_HTTPS' \
|
||||||
|
'#define NSLOOKUP_CMD' \
|
||||||
|
'#define NTP_CMD' \
|
||||||
|
> config/local/general.h
|
||||||
|
# Each arch builds to its own `bin-*` directory. We copy the four
|
||||||
|
# output binaries into /out/ with the names openpxe-ipxe-assets
|
||||||
|
# expects. Stripping the binaries saves ~30% — they go into the rust
|
||||||
|
# binary via include_bytes! so the savings ripple through the final
|
||||||
|
# image.
|
||||||
|
RUN mkdir -p /out && \
|
||||||
|
make -j"$(nproc)" bin/undionly.kpxe && \
|
||||||
|
cp bin/undionly.kpxe /out/undionly.kpxe && \
|
||||||
|
make -j"$(nproc)" bin-x86_64-efi/snponly.efi && \
|
||||||
|
cp bin-x86_64-efi/snponly.efi /out/snponly.efi && \
|
||||||
|
make -j"$(nproc)" bin-x86_64-efi/ipxe.efi && \
|
||||||
|
cp bin-x86_64-efi/ipxe.efi /out/ipxe.efi && \
|
||||||
|
make -j"$(nproc)" bin-i386-efi/snponly.efi && \
|
||||||
|
cp bin-i386-efi/snponly.efi /out/snponly-i386.efi && \
|
||||||
|
make -j"$(nproc)" CROSS_COMPILE=aarch64-linux-gnu- bin-arm64-efi/snponly.efi && \
|
||||||
|
cp bin-arm64-efi/snponly.efi /out/snponly-arm64.efi && \
|
||||||
|
ls -lh /out/
|
||||||
|
|
||||||
########## build openpxe ##########
|
########## build openpxe ##########
|
||||||
FROM rust:${RUST_VERSION}-bookworm AS build
|
FROM rust:${RUST_VERSION}-bookworm AS build
|
||||||
@@ -56,7 +122,11 @@ RUN apt-get update \
|
|||||||
# `cargo build`, which is slow and can exhaust small Colima/CI disks.
|
# `cargo build`, which is slow and can exhaust small Colima/CI disks.
|
||||||
COPY Cargo.toml Cargo.lock ./
|
COPY Cargo.toml Cargo.lock ./
|
||||||
COPY crates/ crates/
|
COPY crates/ crates/
|
||||||
COPY --from=fetch /src/assets/ipxe /src/assets/ipxe
|
# v0.4.61: iPXE binaries come from our own source-built stage with
|
||||||
|
# IMAGE_PNG enabled. wimboot still comes from the fetch stage (it's
|
||||||
|
# from ipxe/wimboot's GitHub release, separately signed).
|
||||||
|
COPY --from=ipxe-build /out/ /src/assets/ipxe/
|
||||||
|
COPY --from=fetch /src/assets/ipxe/wimboot /src/assets/ipxe/wimboot
|
||||||
|
|
||||||
# Cache cargo registry + target across builds. The mtime touch is
|
# Cache cargo registry + target across builds. The mtime touch is
|
||||||
# belt-and-suspenders: cargo occasionally misses mtime-only changes on
|
# belt-and-suspenders: cargo occasionally misses mtime-only changes on
|
||||||
|
|||||||
Reference in New Issue
Block a user