使用 nginx 代理 OpenBMC 前端
OpenBMC 的前端是用 Vue 2 实现的,由 bmcweb 后台提供静态资源服务,但是这种方式对于平台纳管以及前端开发而言都并不是太友好。

OpenBMC 的 webui 项目有两个,webui-vuephosphor-webui 的替代项目,其中 phosphor-webui 项目当前已归档。

The webui-vue repository will replace phosphor-webui once it is deprecated. Webui-vue uses the Vue.js framework to interact with the BMC via the Redfish API.

OpenBMC Web User Interface Development
https://github.com/openbmc/docs/blob/master/development/web-ui.md

auth

bmcweb 支持 basic auth(当然也支持多种其它类型的认证):

❯ curl -k -X GET https://192.168.1.60:2443/redfish/v1/Managers/bmc -H 'Authorization: Basic cm9vdDowcGVuQm1j'

其中 cm9vdDowcGVuQm1j 是 root:0penBmc 的 base64 编码。

AuthX
https://github.com/openbmc/bmcweb#authx

所有 bmc 认证相关的信息都记录在 /home/root/bmcweb_persistent_data.json 文件中。

systemd service

bmcweb 进程由 systemd 进行管理:

$ systemctl cat bmcweb
# /usr/lib/systemd/system/bmcweb.service
[Unit]
Description=Start bmcweb server

Wants=network.target
After=network.target

[Service]
ExecReload=kill -s HUP $MAINPID
ExecStart=/usr/bin/bmcweb
Type=simple
WorkingDirectory=/home/root

[Install]
WantedBy=network.target

static assets

webui 静态资源在如下位置:

$ ls /usr/share/www/
DMTF_Redfish_logo_2017.svg  favicon.ico.gz              img                         js                          redfish.css
css                         google                      index.html.gz               redfish                     styles

其中 css, img, js, favicon.ico.gz, index.html.gz 是通过 webui-vue 工程构建出来的。

tls

bmcweb 会自动生成默认的 HTTPS 证书,证书在如下位置:

$ cat /etc/ssl/certs/https/server.pem
-----BEGIN PRIVATE KEY-----
MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDADUHokzKpNvElYp1JN
X7R0txw0l3jQlOcAA5JhtGrfQXCNlAUrQ+mCv5TLLwUT90mhZANiAATi1SngpRxh
wo1lISVDDE78W6NFpLePX3SczBpq3NtpHFr2P7syuMh8JdZ4tnZ/pPDm/8kE+W7r
3dI6jcI/g/Kztdvyg1yNXqxLU/H2LhhEWDSITPUgnKjBSqElVIPJWrc=
-----END PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIICNzCCAb6gAwIBAgIEPR6/LDAKBggqhkjOPQQDAjAyMQswCQYDVQQGEwJVUzEQ
MA4GA1UECgwHT3BlbkJNQzERMA8GA1UEAwwIdGVzdGhvc3QwHhcNMjMxMTAzMDcw
MDM2WhcNMzMxMDMxMDcwMDM2WjAyMQswCQYDVQQGEwJVUzEQMA4GA1UECgwHT3Bl
bkJNQzERMA8GA1UEAwwIdGVzdGhvc3QwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATi
1SngpRxhwo1lISVDDE78W6NFpLePX3SczBpq3NtpHFr2P7syuMh8JdZ4tnZ/pPDm
/8kE+W7r3dI6jcI/g/Kztdvyg1yNXqxLU/H2LhhEWDSITPUgnKjBSqElVIPJWrej
gaQwgaEwDwYDVR0TAQH/BAUwAwEB/zATBgNVHREEDDAKggh0ZXN0aG9zdDAdBgNV
HQ4EFgQU6tqfW5Q+z2KegqzKINu3M08KHFcwCQYDVR0jBAIwADALBgNVHQ8EBAMC
BaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwLQYJYIZIAYb4QgENBCAWHkdlbmVyYXRl
ZCBmcm9tIE9wZW5CTUMgc2VydmljZTAKBggqhkjOPQQDAgNnADBkAjAvQjNK6rTd
Unq3RlSfoR0DPswa8Kk4gT7ui4oevvBefuIg+KOU4G1e479gcKOeqcACMDBtGtW7
WMhIGYnalbEsBJYFfRtLRt2IWqTTAHQrWwwn7Y876nzGBVTXBDcB8OzWgg==
-----END CERTIFICATE-----

因此如有需要,可以手工合并适用于 nginx tls 支持的私钥和证书为一个文件并进行替换。

build

❯ devtool modify webui-vue
❯ devtool build webui-vue

构建时可以设置 http_proxyhttps_proxy 环境变量,npm install 时会使用这两个环境变量:

// meta-phosphor/recipes-phosphor/webui/webui-vue_git.bb

do_compile[network] = "1"
do_compile () {
    cd ${S}
    rm -rf node_modules
    npm --loglevel info --proxy=${http_proxy} --https-proxy=${https_proxy} install
    npm run build ${EXTRA_OENPM}
}

do_install () {
   # create directory structure
   install -d ${D}${datadir}/www
   cp -r ${S}/dist/** ${D}${datadir}/www
   find ${D}${datadir}/www -type f -exec chmod a=r,u+w '{}' +
   find ${D}${datadir}/www -type d -exec chmod a=rx,u+w '{}' +
}

构建出来的内容见如下目录(还有其它一些目录,可以通过 oe-workdir/temp 目录下的日志文件进行了解):

❯ ls build/romulus/workspace/sources/webui-vue/oe-workdir/image/usr/share/www
css  favicon.ico.gz  img  index.html.gz  js

❯ ls build/romulus/workspace/sources/webui-vue/oe-workdir/package/usr/share/www
css  favicon.ico.gz  img  index.html.gz  js

debug logging

日志输出默认为 disabled 级别,因此所有日志打印都无法输出。

option(
    'bmcweb-logging',
    type: 'combo',
    choices : [ 'disabled', 'enabled', 'debug', 'info', 'warning', 'error', 'critical' ],
    value: 'disabled',
    description: '''Enable output the extended logging level.
                    - disabled: disable bmcweb log traces.
                    - enabled: treated as 'debug'
                    - For the other logging level option, see DEVELOPING.md.'''
)
❯ vi build/romulus/workspace/appends/bmcweb_git.bbappend
EXTRA_OEMESON:append = " -Dbmcweb-logging=debug"

bmcweb 的日志可以通过 journactl 进行查看:

$ sudo journalctl -f -u bmcweb
Dec 02 14:20:23 romulus bmcweb[518]: [INFO webserver_main.cpp:46] Starting webserver on socket handle 3
Dec 02 14:20:23 romulus bmcweb[518]: [INFO webserver_main.cpp:142] Start Hostname Monitor Service...

websocket

/subscribe websocket 由 dbus_monitor.hpp 注册,需要使能 rest 特性:

option(
    'rest',
    type: 'feature',
    value: 'disabled',
    description: '''Enable Phosphor REST (D-Bus) APIs. Paths directly map
                    Phosphor D-Bus object paths, for example,
                    /xyz/openbmc_project/logging/entry/enumerate. See
                    https://github.com/openbmc/docs/blob/master/rest-api.md.'''
)
❯ vi build/romulus/workspace/appends/bmcweb_git.bbappend
EXTRA_OEMESON:append = " -Drest=enabled"

需要注意的是,整个 webui 有多处 websocket 请求,包括:/subscribe, /kvm, /console, /vm/vm 实际上是 WebSocketEndpoint,即服务端动态生成的路径),搜索 wss 前缀即可。

nginx reverse proxy

需求

nginx 使用 HTTP 对外提供服务,通过 nginx 反向代理 bmcweb,且静态资源仍然由 bmcweb 后端提供服务。

bmcweb 后端和 webui-vue 前端都需要进行修改,相应改动如下。

bmcweb 后端

增加如下构建选项:

❯ vi build/romulus/workspace/appends/bmcweb_git.bbappend
EXTRA_OEMESON:append = " -Dinsecure-disable-xss=enabled"

增加该构建选项是为了支持 OpenBMC 页面通过 iframe 嵌入。

修改如下代码:

diff --git a/include/login_routes.hpp b/include/login_routes.hpp
index ae99757e..ffc793de 100644
--- a/include/login_routes.hpp
+++ b/include/login_routes.hpp
@@ -200,8 +200,8 @@ inline void handleLogout(const crow::Request& req,
                                  "SESSION="
                                  "; SameSite=Strict; Secure; HttpOnly; "
                                  "expires=Thu, 01 Jan 1970 00:00:00 GMT");
-        asyncResp->res.addHeader("Clear-Site-Data",
-                                 R"("cache","cookies","storage")");
+        // asyncResp->res.addHeader("Clear-Site-Data",
+        //                          R"("cache","cookies","storage")");
         persistent_data::SessionStore::getInstance().removeSession(session);
     }
 }
diff --git a/include/security_headers.hpp b/include/security_headers.hpp
index 1b9e984d..7cf1dbbc 100644
--- a/include/security_headers.hpp
+++ b/include/security_headers.hpp
@@ -60,7 +60,7 @@ inline void addSecurityHeaders(const crow::Request& req [[maybe_unused]],
     res.addHeader("X-Permitted-Cross-Domain-Policies", "none");
 
     res.addHeader("Cross-Origin-Embedder-Policy", "require-corp");
-    res.addHeader("Cross-Origin-Opener-Policy", "same-origin");
+    res.addHeader("Cross-Origin-Opener-Policy", "unsafe-none");
     res.addHeader("Cross-Origin-Resource-Policy", "same-origin");
 
     if (bmcwebInsecureDisableXssPrevention == 0)
@@ -85,7 +85,7 @@ inline void addSecurityHeaders(const crow::Request& req [[maybe_unused]],
         // If XSS is disabled, we need to allow loading from addresses other
         // than self, as the BMC will be hosted elsewhere.
         res.addHeader("Content-Security-Policy", "default-src 'none'; "
-                                                 "img-src *; "
+                                                 "img-src * data:; "
                                                  "font-src *; "
                                                  "style-src *; "
                                                  "script-src *; "

Clear-Site-Data 的修改是为了避免如下错误:

Clear-Site-Data header on 'http://192.168.1.60/bmcweb/logout': Not supported for insecure origins.

Cross-Origin-Opener-Policy 的修改是为了避免如下错误:

The Cross-Origin-Opener-Policy header has been ignored, because the URL's 
origin was untrustworthy.
It was defined either in the final response or a redirect. Please deliver 
the response using the HTTPS protocol.

Content-Security-Policy 的修改是为了避免如下错误:

Refused to load the image 'data:image/svg+xml;' because it violates the 
following Content Security Policy directive: "img-src *".
Note that '*' matches only URLs with network schemes ('http', 'https', 
'ws', 'wss'), or URLs whose scheme matches `self`'s scheme.
The scheme 'data:' must be added explicitly.

webui-vue 前端

修改 webui-vue 前端根路径为 /bmcweb

diff --git a/vue.config.js b/vue.config.js
index de0ad12..1bd8fa0 100644
--- a/vue.config.js
+++ b/vue.config.js
@@ -93,6 +93,8 @@ module.exports = {
       );
     }
   },
+  publicPath: '/bmcweb',
+  chainWebpack: (config) => config.optimization.minimize(false),
   pluginOptions: {
     i18n: {
       localeDir: 'locales',

注意 chainWebpack 的配置只是为了方便调试而已。

同时设置 axios 的 baseURL/bmcweb

diff --git a/src/store/api.js b/src/store/api.js
index 9fd900d..dca09e9 100644
--- a/src/store/api.js
+++ b/src/store/api.js
@@ -9,6 +9,7 @@ Axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
 
 const api = Axios.create({
   withCredentials: true,
+  baseURL: '/bmcweb',
 });
 
 api.interceptors.response.use(undefined, (error) => {

同时搜索 webui-vue 工程,将 wss:// 修改成 ws://,同时为 path 增加 /bmcweb/ws 前缀,如:

diff --git a/src/store/plugins/WebSocketPlugin.js b/src/store/plugins/WebSocketPlugin.js
index cbdc932..4713c6b 100644
--- a/src/store/plugins/WebSocketPlugin.js
+++ b/src/store/plugins/WebSocketPlugin.js
@@ -22,7 +22,8 @@ const WebSocketPlugin = (store) => {
       process.env.VUE_APP_SUBSCRIBE_SOCKET_DISABLED === 'true' ? true : false;
     if (socketDisabled) return;
     const token = store.getters['authentication/token'];
-    ws = new WebSocket(`wss://${window.location.host}/subscribe`, [token]);
+    // eslint-disable-next-line
+    ws = new WebSocket(`ws://${window.location.host}/bmcweb/ws/subscribe`, [token]);
     ws.onopen = () => {
       ws.send(JSON.stringify(data));
     };

最终 URL 静态资源的前缀为 /bmcweb,json api 的前缀为 /bmcweb,websocket 的前缀为 /bmcweb/ws

nginx conf

nginx 的反向代理规则相应的改变如下(注意静态文件仍然由 bmcweb 后台进程直接提供服务):

# basic auth
set $authorization "Basic cm9vdDowcGVuQm1j";

# serve index.html & set cookie for / only
location ~ ^/bmcweb/?$ {
	rewrite ^.*$ / break;
	proxy_pass https://192.168.1.60:2443;
	# fool the webui-vue, do not redirect to login page since we
	# have set Authorization header explicitly
	add_header Set-Cookie "IsAuthenticated=true; Path=/bmcweb";
}

# serve static assets & json api
location /bmcweb {
	rewrite ^/bmcweb(.*)$ $1 break;
	proxy_pass https://192.168.1.60:2443;
	proxy_http_version 1.1;
	# delete Secure flag
	proxy_cookie_flags ~ nosecure;
	proxy_set_header Authorization $authorization;
}

# serve websocket
location /bmcweb/ws {
	rewrite ^/bmcweb/ws(.*)$ $1 break;
	proxy_pass https://192.168.1.60:2443;
	proxy_http_version 1.1;
	proxy_set_header Upgrade $http_upgrade;
	proxy_set_header Connection "Upgrade";
	proxy_set_header Authorization $authorization;
}

不过需要注意的是,即使通过 nginx 隐式传递了 Authorization 请求头,但是由于 webui-vue 前端页面需要在浏览器 Local storage 里读取 storedUsername 属性,因此仍然需要手工在浏览器中设置该属性为 root(与 Basic cm9vdDowcGVuQm1j 对应)。

Set-Cookie 显式指定 Path=/bmcweb 属性,以避免如下的情况:

nginx serve static assets

需求

nginx 使用 HTTP 对外提供服务,通过 nginx 反向代理 bmcweb,且使用 nginx 为 webui-vue 提供静态资源服务。

bmcweb 后端和 webui-vue 前端都需要进行修改(相比上一需求的改动,改动点稍有不同),有两种解决方案,相应改动如下。

解决方案一

最终 URL 静态资源的前缀为 /bmcweb,json api 的前缀为 /bmcweb/api,websocket 的前缀为 /bmcweb/ws

1. bmcweb 后端

修改如下代码:

diff --git a/include/login_routes.hpp b/include/login_routes.hpp
index ae99757e..1fae5810 100644
--- a/include/login_routes.hpp
+++ b/include/login_routes.hpp
@@ -169,10 +169,10 @@ inline void handleLogin(const crow::Request& req,
 
             asyncResp->res.addHeader(boost::beast::http::field::set_cookie,
                                      "XSRF-TOKEN=" + session->csrfToken +
-                                         "; SameSite=Strict; Secure");
+                                         "; SameSite=Strict; Secure; Path=/bmcweb");
             asyncResp->res.addHeader(boost::beast::http::field::set_cookie,
                                      "SESSION=" + session->sessionToken +
-                                         "; SameSite=Strict; Secure; HttpOnly");
+                                         "; SameSite=Strict; Secure; HttpOnly; Path=/bmcweb");
 
             // if content type is json, assume json token
             asyncResp->res.jsonValue["token"] = session->sessionToken;
@@ -200,8 +200,8 @@ inline void handleLogout(const crow::Request& req,
                                  "SESSION="
                                  "; SameSite=Strict; Secure; HttpOnly; "
                                  "expires=Thu, 01 Jan 1970 00:00:00 GMT");
-        asyncResp->res.addHeader("Clear-Site-Data",
-                                 R"("cache","cookies","storage")");
+        // asyncResp->res.addHeader("Clear-Site-Data",
+        //                          R"("cache","cookies","storage")");
         persistent_data::SessionStore::getInstance().removeSession(session);
     }
 }
diff --git a/include/security_headers.hpp b/include/security_headers.hpp
index 1b9e984d..44009c51 100644
--- a/include/security_headers.hpp
+++ b/include/security_headers.hpp
@@ -60,7 +60,7 @@ inline void addSecurityHeaders(const crow::Request& req [[maybe_unused]],
     res.addHeader("X-Permitted-Cross-Domain-Policies", "none");
 
     res.addHeader("Cross-Origin-Embedder-Policy", "require-corp");
-    res.addHeader("Cross-Origin-Opener-Policy", "same-origin");
+    res.addHeader("Cross-Origin-Opener-Policy", "unsafe-none");
     res.addHeader("Cross-Origin-Resource-Policy", "same-origin");
 
     if (bmcwebInsecureDisableXssPrevention == 0)

Clear-Site-Data 的修改是为了避免如下错误:

Clear-Site-Data header on 'http://192.168.1.60/bmcweb/api/logout': Not supported for insecure origins.

Cross-Origin-Opener-Policy 的修改是为了避免如下错误:

The Cross-Origin-Opener-Policy header has been ignored, because the URL's 
origin was untrustworthy.
It was defined either in the final response or a redirect. Please deliver 
the response using the HTTPS protocol.

注意这里为 XSRF-TOKENSESSION 两个 cookie 都显式设置了 Path 属性,这是因为接下来配置 nginx 反向代理时 json api 的前缀为 /bmcweb/api,因此生成的 cookie Path 属性为 Path=/bmcweb/api,会导致前端页面无法访问 cookie。

1. webui-vue 前端

修改 webui-vue 前端根路径为 /bmcweb

diff --git a/vue.config.js b/vue.config.js
index de0ad12..1bd8fa0 100644
--- a/vue.config.js
+++ b/vue.config.js
@@ -93,6 +93,8 @@ module.exports = {
       );
     }
   },
+  publicPath: '/bmcweb',
+  chainWebpack: (config) => config.optimization.minimize(false),
   pluginOptions: {
     i18n: {
       localeDir: 'locales',

注意 chainWebpack 的配置只是为了方便调试而已。

同时设置 axios 的 baseURL/bmcweb/api

diff --git a/src/store/api.js b/src/store/api.js
index 9fd900d..4f41625 100644
--- a/src/store/api.js
+++ b/src/store/api.js
@@ -9,6 +9,7 @@ Axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
 
 const api = Axios.create({
   withCredentials: true,
+  baseURL: '/bmcweb/api',
 });
 
 api.interceptors.response.use(undefined, (error) => {
@@ -17,7 +18,7 @@ api.interceptors.response.use(undefined, (error) => {
   // TODO: Provide user with a notification and way to keep system active
   if (response.status == 401) {
     if (response.config.url != '/login') {
-      window.location = '/login';
+      window.location = '/bmcweb/#/login';
       // Commit logout to remove XSRF-TOKEN cookie
       store.commit('authentication/logout');
     }

对路由处理的修改是因为跳转路径错误(可在加载过程中执行 logout 操作进行复现),需要注意对 response.config.url 的判断不能加上 baseURL,此外 location 的值与 vue-router 的模式相关,webui-vue 默认使用的 hash 模式。

由于 cookie 的 Path 属性为 /bmcweb,因此,相应的删除操作需要进行调整(否则无法删除):

diff --git a/src/store/modules/Authentication/AuthenticanStore.js b/src/store/modules/Authentication/AuthenticanStore.js
index 0dca183..218ab03 100644
--- a/src/store/modules/Authentication/AuthenticanStore.js
+++ b/src/store/modules/Authentication/AuthenticanStore.js
@@ -29,7 +29,7 @@ const AuthenticationStore = {
       state.authError = authError;
     },
     logout(state) {
-      Cookies.remove('XSRF-TOKEN');
+      Cookies.remove('XSRF-TOKEN', { path: '/bmcweb' });
       Cookies.remove('IsAuthenticated');
       localStorage.removeItem('storedUsername');
       state.xsrfCookie = undefined;

同时搜索 webui-vue 工程,将 wss:// 修改成 ws://,同时为 path 增加 /bmcweb/ws 前缀,如:

diff --git a/src/store/plugins/WebSocketPlugin.js b/src/store/plugins/WebSocketPlugin.js
index cbdc932..4713c6b 100644
--- a/src/store/plugins/WebSocketPlugin.js
+++ b/src/store/plugins/WebSocketPlugin.js
@@ -22,7 +22,8 @@ const WebSocketPlugin = (store) => {
       process.env.VUE_APP_SUBSCRIBE_SOCKET_DISABLED === 'true' ? true : false;
     if (socketDisabled) return;
     const token = store.getters['authentication/token'];
-    ws = new WebSocket(`wss://${window.location.host}/subscribe`, [token]);
+    // eslint-disable-next-line
+    ws = new WebSocket(`ws://${window.location.host}/bmcweb/ws/subscribe`, [token]);
     ws.onopen = () => {
       ws.send(JSON.stringify(data));
     };

1. nginx conf

set $authorization "Basic cm9vdDowcGVuQm1j";

# serve index.html
location ~ ^/bmcweb/?$ {
	rewrite ^.*$ /bmcweb/index.html last;
}

# serve static assets
location /bmcweb {
	gzip_static always;
	alias /home/runsisi/working/bmc/webui-vue/dist/;
	# set cookie for / only
	if ($uri = /bmcweb/index.html) {
		# fool the webui-vue, do not redirect to login page since we
		# have set Authorization header explicitly
		add_header Set-Cookie "IsAuthenticated=true; Path=/bmcweb";
	}
}

# serve json api
location /bmcweb/api {
	rewrite ^/bmcweb/api(.*)$ $1 break;
	proxy_pass https://192.168.1.60:2443;
	proxy_http_version 1.1;
	# delete Secure flag
	proxy_cookie_flags ~ nosecure;
	proxy_set_header Authorization $authorization;
}

# serve websocket
location /bmcweb/ws {
	rewrite ^/bmcweb/ws(.*)$ $1 break;
	proxy_pass https://192.168.1.60:2443;
	proxy_http_version 1.1;
	proxy_set_header Upgrade $http_upgrade;
	proxy_set_header Connection "Upgrade";
	proxy_set_header Authorization $authorization;
}

解决方案二

最终 URL 静态资源的前缀为 /bmcweb,json api 的前缀为 /bmcweb,websocket 的前缀为 /bmcweb/ws

2. bmcweb 后端

修改如下代码:

diff --git a/include/login_routes.hpp b/include/login_routes.hpp
index ae99757e..ffc793de 100644
--- a/include/login_routes.hpp
+++ b/include/login_routes.hpp
@@ -200,8 +200,8 @@ inline void handleLogout(const crow::Request& req,
                                  "SESSION="
                                  "; SameSite=Strict; Secure; HttpOnly; "
                                  "expires=Thu, 01 Jan 1970 00:00:00 GMT");
-        asyncResp->res.addHeader("Clear-Site-Data",
-                                 R"("cache","cookies","storage")");
+        // asyncResp->res.addHeader("Clear-Site-Data",
+        //                          R"("cache","cookies","storage")");
         persistent_data::SessionStore::getInstance().removeSession(session);
     }
 }
diff --git a/include/security_headers.hpp b/include/security_headers.hpp
index 1b9e984d..44009c51 100644
--- a/include/security_headers.hpp
+++ b/include/security_headers.hpp
@@ -60,7 +60,7 @@ inline void addSecurityHeaders(const crow::Request& req [[maybe_unused]],
     res.addHeader("X-Permitted-Cross-Domain-Policies", "none");
 
     res.addHeader("Cross-Origin-Embedder-Policy", "require-corp");
-    res.addHeader("Cross-Origin-Opener-Policy", "same-origin");
+    res.addHeader("Cross-Origin-Opener-Policy", "unsafe-none");
     res.addHeader("Cross-Origin-Resource-Policy", "same-origin");
 
     if (bmcwebInsecureDisableXssPrevention == 0)

Clear-Site-Data 的修改是为了避免如下错误:

Clear-Site-Data header on 'http://192.168.1.60/bmcweb/logout': Not supported for insecure origins.

Cross-Origin-Opener-Policy 的修改是为了避免如下错误:

The Cross-Origin-Opener-Policy header has been ignored, because the URL's 
origin was untrustworthy.
It was defined either in the final response or a redirect. Please deliver 
the response using the HTTPS protocol.

2. webui-vue 前端

修改 webui-vue 前端根路径为 /bmcweb

diff --git a/vue.config.js b/vue.config.js
index de0ad12..1bd8fa0 100644
--- a/vue.config.js
+++ b/vue.config.js
@@ -93,6 +93,8 @@ module.exports = {
       );
     }
   },
+  publicPath: '/bmcweb',
+  chainWebpack: (config) => config.optimization.minimize(false),
   pluginOptions: {
     i18n: {
       localeDir: 'locales',

注意 chainWebpack 的配置只是为了方便调试而已。

同时设置 axios 的 baseURL/bmcweb

diff --git a/src/store/api.js b/src/store/api.js
index 9fd900d..4f41625 100644
--- a/src/store/api.js
+++ b/src/store/api.js
@@ -9,6 +9,7 @@ Axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
 
 const api = Axios.create({
   withCredentials: true,
+  baseURL: '/bmcweb',
 });
 
 api.interceptors.response.use(undefined, (error) => {
@@ -17,7 +18,7 @@ api.interceptors.response.use(undefined, (error) => {
   // TODO: Provide user with a notification and way to keep system active
   if (response.status == 401) {
     if (response.config.url != '/login') {
-      window.location = '/login';
+      window.location = '/bmcweb/#/login';
       // Commit logout to remove XSRF-TOKEN cookie
       store.commit('authentication/logout');
     }

对路由处理的修改是因为跳转路径错误(可在加载过程中执行 logout 操作进行复现),需要注意对 response.config.url 的判断不能加上 baseURL,此外 location 的值与 vue-router 的模式相关,webui-vue 默认使用的 hash 模式。

由于 bmcweb 后端生成的 cookie 没有设置 Path 属性,默认 Path 属性为 /bmcweb,因此,相应的删除操作需要进行调整(否则无法删除):

diff --git a/src/store/modules/Authentication/AuthenticanStore.js b/src/store/modules/Authentication/AuthenticanStore.js
index 0dca183..218ab03 100644
--- a/src/store/modules/Authentication/AuthenticanStore.js
+++ b/src/store/modules/Authentication/AuthenticanStore.js
@@ -29,7 +29,7 @@ const AuthenticationStore = {
       state.authError = authError;
     },
     logout(state) {
-      Cookies.remove('XSRF-TOKEN');
+      Cookies.remove('XSRF-TOKEN', { path: '/bmcweb' });
       Cookies.remove('IsAuthenticated');
       localStorage.removeItem('storedUsername');
       state.xsrfCookie = undefined;

同时搜索 webui-vue 工程,将 wss:// 修改成 ws://,同时为 path 增加 /bmcweb/ws 前缀,如:

diff --git a/src/store/plugins/WebSocketPlugin.js b/src/store/plugins/WebSocketPlugin.js
index cbdc932..4713c6b 100644
--- a/src/store/plugins/WebSocketPlugin.js
+++ b/src/store/plugins/WebSocketPlugin.js
@@ -22,7 +22,8 @@ const WebSocketPlugin = (store) => {
       process.env.VUE_APP_SUBSCRIBE_SOCKET_DISABLED === 'true' ? true : false;
     if (socketDisabled) return;
     const token = store.getters['authentication/token'];
-    ws = new WebSocket(`wss://${window.location.host}/subscribe`, [token]);
+    // eslint-disable-next-line
+    ws = new WebSocket(`ws://${window.location.host}/bmcweb/ws/subscribe`, [token]);
     ws.onopen = () => {
       ws.send(JSON.stringify(data));
     };

2. nginx conf

set $authorization "Basic cm9vdDowcGVuQm1j";

# serve index.html
location ~ ^/bmcweb/?$ {
	rewrite ^.*$ /bmcweb/index.html last;
}

# serve index.html
location = /bmcweb/index.html {
	gzip_static always;
	alias /home/runsisi/working/bmc/webui-vue/dist/index.html;
	# set cookie for / only
	# fool the webui-vue, do not redirect to login page since we
	# have set Authorization header explicitly
	add_header Set-Cookie "IsAuthenticated=true; Path=/bmcweb";
}

# serve static assets
location ~ ^/bmcweb/(.+\.(?:css|js|svg|ico))$ {
	gzip_static always;
	alias /home/runsisi/working/bmc/webui-vue/dist/$1;
}

# serve json api
location /bmcweb {
	rewrite ^/bmcweb(.*)$ $1 break;
	proxy_pass https://192.168.1.60:2443;
	proxy_http_version 1.1;
	# delete Secure flag
	proxy_cookie_flags ~ nosecure;
	proxy_set_header Authorization $authorization;
}

# serve websocket
location /bmcweb/ws {
	rewrite ^/bmcweb/ws(.*)$ $1 break;
	proxy_pass https://192.168.1.60:2443;
	proxy_http_version 1.1;
	proxy_set_header Upgrade $http_upgrade;
	proxy_set_header Connection "Upgrade";
	proxy_set_header Authorization $authorization;
}

2.1 nginx conf

由于后端服务可以自动忽略不支持的 Upgrade 请求头,因此也可以合并 json api 和 websocket 的处理(wss:// 相应的 path 前缀改成 /bmcweb)。

最终 URL 静态资源的前缀为 /bmcweb,json api 的前缀为 /bmcweb,websocket 的前缀为 /bmcweb

nginx 相应的配置如下:

set $authorization "Basic cm9vdDowcGVuQm1j";

# serve index.html
location ~ ^/bmcweb/?$ {
	rewrite ^.*$ /bmcweb/index.html last;
}

# serve index.html
location = /bmcweb/index.html {
	gzip_static always;
	alias /home/runsisi/working/bmc/webui-vue/dist/index.html;
	# set cookie for / only
	# fool the webui-vue, do not redirect to login page since we
	# have set Authorization header explicitly
	add_header Set-Cookie "IsAuthenticated=true; Path=/bmcweb";
}

# serve static assets
location ~ ^/bmcweb/(.+\.(?:css|js|svg|ico))$ {
	gzip_static always;
	alias /home/runsisi/working/bmc/webui-vue/dist/$1;
}

# serve json api & websocket
location /bmcweb {
	rewrite ^/bmcweb(.*)$ $1 break;
	proxy_pass https://192.168.1.60:2443;
	proxy_http_version 1.1;
	proxy_set_header Upgrade $http_upgrade;
	proxy_set_header Connection "Upgrade";
	proxy_set_header Authorization $authorization;
	# delete Secure flag
	proxy_cookie_flags ~ nosecure;
}

解决方案三

最终 URL 静态资源的前缀为 /bmcweb,json api 的前缀为 /bmcweb/api,websocket 的前缀为 /bmcweb/api

3. bmcweb 后端

修改如下代码:

diff --git a/include/login_routes.hpp b/include/login_routes.hpp
index ae99757e..1fae5810 100644
--- a/include/login_routes.hpp
+++ b/include/login_routes.hpp
@@ -169,10 +169,10 @@ inline void handleLogin(const crow::Request& req,
 
             asyncResp->res.addHeader(boost::beast::http::field::set_cookie,
                                      "XSRF-TOKEN=" + session->csrfToken +
-                                         "; SameSite=Strict; Secure");
+                                         "; SameSite=Strict; Secure; Path=/bmcweb");
             asyncResp->res.addHeader(boost::beast::http::field::set_cookie,
                                      "SESSION=" + session->sessionToken +
-                                         "; SameSite=Strict; Secure; HttpOnly");
+                                         "; SameSite=Strict; Secure; HttpOnly; Path=/bmcweb");
 
             // if content type is json, assume json token
             asyncResp->res.jsonValue["token"] = session->sessionToken;
@@ -200,8 +200,8 @@ inline void handleLogout(const crow::Request& req,
                                  "SESSION="
                                  "; SameSite=Strict; Secure; HttpOnly; "
                                  "expires=Thu, 01 Jan 1970 00:00:00 GMT");
-        asyncResp->res.addHeader("Clear-Site-Data",
-                                 R"("cache","cookies","storage")");
+        // asyncResp->res.addHeader("Clear-Site-Data",
+        //                          R"("cache","cookies","storage")");
         persistent_data::SessionStore::getInstance().removeSession(session);
     }
 }
diff --git a/include/security_headers.hpp b/include/security_headers.hpp
index 1b9e984d..44009c51 100644
--- a/include/security_headers.hpp
+++ b/include/security_headers.hpp
@@ -60,7 +60,7 @@ inline void addSecurityHeaders(const crow::Request& req [[maybe_unused]],
     res.addHeader("X-Permitted-Cross-Domain-Policies", "none");
 
     res.addHeader("Cross-Origin-Embedder-Policy", "require-corp");
-    res.addHeader("Cross-Origin-Opener-Policy", "same-origin");
+    res.addHeader("Cross-Origin-Opener-Policy", "unsafe-none");
     res.addHeader("Cross-Origin-Resource-Policy", "same-origin");
 
     if (bmcwebInsecureDisableXssPrevention == 0)

Clear-Site-Data 的修改是为了避免如下错误:

Clear-Site-Data header on 'http://192.168.1.60/bmcweb/api/logout': Not supported for insecure origins.

Cross-Origin-Opener-Policy 的修改是为了避免如下错误:

The Cross-Origin-Opener-Policy header has been ignored, because the URL's 
origin was untrustworthy.
It was defined either in the final response or a redirect. Please deliver 
the response using the HTTPS protocol.

注意这里为 XSRF-TOKENSESSION 两个 cookie 都显式设置了 Path 属性,这是因为接下来配置 nginx 反向代理时 json api 的前缀为 /bmcweb/api,因此生成的 cookie Path 属性为 Path=/bmcweb/api,会导致前端页面无法访问 cookie。

3. webui-vue 前端

修改 webui-vue 前端根路径为 /bmcweb

diff --git a/vue.config.js b/vue.config.js
index de0ad12..7112321 100644
--- a/vue.config.js
+++ b/vue.config.js
@@ -88,11 +88,14 @@ module.exports = {
     if (process.env.NODE_ENV === 'production') {
       config.plugins.push(
         new CompressionPlugin({
-          deleteOriginalAssets: true,
+          deleteOriginalAssets: false,
         })
       );
     }
   },
+  outputDir: 'bmcweb',
+  publicPath: '/bmcweb',
+  chainWebpack: (config) => config.optimization.minimize(false),
   pluginOptions: {
     i18n: {
       localeDir: 'locales',

deleteOriginalAssets 的修改是为了保留原始的未进行 gzip 压缩的资源文件,因为 nginx try_files 不支持 gzip_static,必须要有源文件的存在,对 outputDir 的修改是为了将资源文件输出到 bmcweb 目录,方便 nginx 使用。

注意 chainWebpack 的配置只是为了方便调试而已。

同时设置 axios 的 baseURL/bmcweb/api

diff --git a/src/store/api.js b/src/store/api.js
index 9fd900d..4f41625 100644
--- a/src/store/api.js
+++ b/src/store/api.js
@@ -9,6 +9,7 @@ Axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
 
 const api = Axios.create({
   withCredentials: true,
+  baseURL: '/bmcweb/api',
 });
 
 api.interceptors.response.use(undefined, (error) => {
@@ -17,7 +18,7 @@ api.interceptors.response.use(undefined, (error) => {
   // TODO: Provide user with a notification and way to keep system active
   if (response.status == 401) {
     if (response.config.url != '/login') {
-      window.location = '/login';
+      window.location = '/bmcweb/#/login';
       // Commit logout to remove XSRF-TOKEN cookie
       store.commit('authentication/logout');
     }

对路由处理的修改是因为跳转路径错误(可在加载过程中执行 logout 操作进行复现),需要注意对 response.config.url 的判断不能加上 baseURL,此外 location 的值与 vue-router 的模式相关,webui-vue 默认使用的 hash 模式。

由于 cookie Path 属性为 /bmcweb,因此,相应的删除操作需要进行调整(否则无法删除):

diff --git a/src/store/modules/Authentication/AuthenticanStore.js b/src/store/modules/Authentication/AuthenticanStore.js
index 0dca183..218ab03 100644
--- a/src/store/modules/Authentication/AuthenticanStore.js
+++ b/src/store/modules/Authentication/AuthenticanStore.js
@@ -29,7 +29,7 @@ const AuthenticationStore = {
       state.authError = authError;
     },
     logout(state) {
-      Cookies.remove('XSRF-TOKEN');
+      Cookies.remove('XSRF-TOKEN', { path: '/bmcweb' });
       Cookies.remove('IsAuthenticated');
       localStorage.removeItem('storedUsername');
       state.xsrfCookie = undefined;

同时搜索 webui-vue 工程,将 wss:// 修改成 ws://,同时为 path 增加 /bmcweb/api 前缀,如:

diff --git a/src/store/plugins/WebSocketPlugin.js b/src/store/plugins/WebSocketPlugin.js
index cbdc932..575086c 100644
--- a/src/store/plugins/WebSocketPlugin.js
+++ b/src/store/plugins/WebSocketPlugin.js
@@ -22,7 +22,8 @@ const WebSocketPlugin = (store) => {
       process.env.VUE_APP_SUBSCRIBE_SOCKET_DISABLED === 'true' ? true : false;
     if (socketDisabled) return;
     const token = store.getters['authentication/token'];
-    ws = new WebSocket(`wss://${window.location.host}/subscribe`, [token]);
+    // eslint-disable-next-line
+    ws = new WebSocket(`ws://${window.location.host}/bmcweb/api/subscribe`, [token]);
     ws.onopen = () => {
       ws.send(JSON.stringify(data));
     };

3. nginx conf

vue-router 为 hash 模式时的 nginx 配置:

location = /bmcweb {
	return 301 /bmcweb/;
}

location ~ ^/bmcweb(.*)/index.html$ {
	return 301 /bmcweb$1/;
}

# serve static assets
location /bmcweb/ {
	root /home/runsisi/working/bmc/webui-vue;
	try_files $uri $uri/index.html @bmcweb-index;
}

location @bmcweb-index {
	return 301 /bmcweb/;
}

# serve json api & websocket
location /bmcweb/api {
	rewrite ^/bmcweb/api(.*)$ $1 break;
	proxy_pass https://192.168.1.60:2443;
	proxy_http_version 1.1;
	proxy_set_header Upgrade $http_upgrade;
	proxy_set_header Connection "Upgrade";
	# delete Secure flag
	proxy_cookie_flags ~ nosecure;
}

如果更严谨一点,hash 模式下 nginx 配置如下:

location = /bmcweb {
	return 301 /bmcweb/;
}

location ~ ^/bmcweb(.*)/index.html$ {
	return 301 /bmcweb$1/;
}

# serve index.html
location = /bmcweb/ {
	root /home/runsisi/working/bmc/webui-vue;
	try_files /bmcweb/index.html =404;
}

# serve static assets
location /bmcweb/ {
	root /home/runsisi/working/bmc/webui-vue;
	try_files $uri @bmcweb-index;
}

location @bmcweb-index {
	return 301 /bmcweb/;
}

# serve json api & websocket
location /bmcweb/api {
	rewrite ^/bmcweb/api(.*)$ $1 break;
	proxy_pass https://192.168.1.60:2443;
	proxy_http_version 1.1;
	proxy_set_header Upgrade $http_upgrade;
	proxy_set_header Connection "Upgrade";
	# delete Secure flag
	proxy_cookie_flags ~ nosecure;
}

vue-router 为 history 模式时的 nginx 配置:

location = /bmcweb {
	return 301 /bmcweb/;
}

location ~ ^/bmcweb(.*)/index.html$ {
	return 301 /bmcweb$1/;
}

# serve static assets
location /bmcweb/ {
	root /home/runsisi/working/bmc/webui-vue;
	try_files $uri /bmcweb/index.html =404;
}

# serve json api & websocket
location /bmcweb/api {
	rewrite ^/bmcweb/api(.*)$ $1 break;
	proxy_pass https://192.168.1.60:2443;
	proxy_http_version 1.1;
	proxy_set_header Upgrade $http_upgrade;
	proxy_set_header Connection "Upgrade";
	# delete Secure flag
	proxy_cookie_flags ~ nosecure;
}

这里 nginx 的配置相比前面的方案去掉了 Authorization 头的处理,此外由于 vue 的 outputDir 进行了修改,因此相应的将 alias 修改为 root


最后修改于 2023-12-06