主机名 RAX3000M 型号 CMCC RAX3000M eMMC version 架构 AR...

Criado em: 17 de outubro de 2025

Respondido usando GPT-5 Thinking por Chat01

Pergunta

主机名 RAX3000M
型号 CMCC RAX3000M eMMC version
架构 ARMv8 Processor rev 4 (v8l) x 2
温度 CPU: 70.4°C, WiFi: 64°C
目标平台 mediatek/mt7981
固件版本 ImmortalWrt 21.02 build 20241217 / LuCI openwrt-21.02
/www/clipboard.html:

<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <title>📋 本地剪贴板中心 (RAX3000M)</title> <style> body { font-family: sans-serif; margin: 20px; background-color: #f4f4f4; } h2, h3 { color: #333; } textarea { width: 100%; height: 100px; box-sizing: border-box; border: 1px solid #ccc; border-radius: 4px; padding: 10px; } button { margin: 5px 2px; padding: 8px 12px; border: none; border-radius: 4px; background-color: #007bff; color: white; cursor: pointer; } button:hover { background-color: #0056b3; } input[type="file"] { margin-top: 10px; } #history { margin-top: 10px; background: white; padding: 10px; border-radius: 4px; border: 1px solid #ddd;} .history-item { display: flex; justify-content: space-between; align-items: center; padding: 8px; border-bottom: 1px solid #eee; } .history-item:last-child { border-bottom: none; } .history-item span { word-break: break-all; margin-right: 10px; } .history-item .buttons { flex-shrink: 0; } #images { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 10px; } .image-card { display: flex; flex-direction: column; align-items: center; border: 1px solid #ccc; padding: 10px; border-radius: 4px; background: white; } .image-card img { max-width: 150px; max-height: 150px; display: block; margin-bottom: 10px; cursor: pointer; } .image-card button { background-color: #dc3545; } .image-card button:hover { background-color: #c82333; } </style> </head> <body> <h2>📋 本地剪贴板中心 (RAX3000M)</h2> <h3>📝 文本剪贴板</h3> <textarea id="textArea" placeholder="在此输入或粘贴文本内容..."></textarea><br> <button onclick="addText()">保存到历史</button> <button onclick="copyTextArea()">复制当前</button> <button onclick="clearAllText()">清空所有</button> <h4>📜 历史记录</h4> <div id="history">(暂无记录)</div> <h3>📷 图片剪贴板</h3> <p style="color:red;"><b>重要提示:</b> 图片功能依赖WebDAV服务。如果无法使用,请确保路由器的WebDAV服务已开启,并且允许跨域请求(CORS)。</p> <input type="file" id="imgFile"> <button onclick="uploadImage()">上传图片</button> <button onclick="clearAllImages()">清空所有图片</button> <div id="images"></div> <script> const textApi = '/cgi-bin/clipboard.sh'; const webdavUrl = 'http://192.168.11.1:8081/rax3000m_emmc/clipboard/images/'; const webdavUser = 'asdf'; const webdavPass = 'asdf'; // -------- 通用函数 -------- /** * 兼容HTTP环境的文本复制函数 * @param {string} text 要复制的文本 */ function copyToClipboard(text) { // 优先使用 navigator.clipboard (适用于HTTPS) if (navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(text).then(() => { alert('已复制'); }).catch(err => { alert('复制失败: ' + err); }); } else { // 备用方案 (适用于HTTP) const textArea = document.createElement('textarea'); textArea.value = text; textArea.style.position = 'fixed'; // 防止屏幕滚动 textArea.style.opacity = '0'; document.body.appendChild(textArea); textArea.focus(); textArea.select(); try { document.execCommand('copy'); alert('已复制'); } catch (err) { alert('复制失败,请手动复制'); } document.body.removeChild(textArea); } } // -------- 文本剪贴板 -------- function loadHistory() { fetch(textApi) .then(r => { if (!r.ok) throw new Error('Network response was not ok'); return r.json(); }) .then(list => { const container = document.getElementById('history'); container.innerHTML = ''; if (!list || list.length === 0) { container.textContent = '(暂无记录)'; return; } list.reverse().forEach(item => { const div = document.createElement('div'); div.className = 'history-item'; const span = document.createElement('span'); span.textContent = item.text; const btnContainer = document.createElement('div'); btnContainer.className = 'buttons'; const copyBtn = document.createElement('button'); copyBtn.textContent = '复制'; copyBtn.onclick = () => copyToClipboard(item.text); const delBtn = document.createElement('button'); delBtn.textContent = '删除'; delBtn.style.backgroundColor = '#6c757d'; delBtn.onclick = () => { fetch(`${textApi}?id=${item.id}`, { method: 'DELETE' }) .then(() => loadHistory()); }; btnContainer.appendChild(copyBtn); btnContainer.appendChild(delBtn); div.appendChild(span); div.appendChild(btnContainer); container.appendChild(div); }); }) .catch(error => { document.getElementById('history').textContent = '加载历史记录失败,请检查后端脚本和JSON文件内容是否正确。错误: ' + error; console.error('Error loading history:', error); }); } function addText() { const val = document.getElementById('textArea').value; if (!val) return alert('请输入文本'); fetch(textApi, { method: 'POST', body: val }) .then(() => { loadHistory(); document.getElementById('textArea').value = ''; }); } function copyTextArea() { const text = document.getElementById('textArea').value; if (text) { copyToClipboard(text); } else { alert('当前无内容可复制'); } } function clearAllText() { if (confirm('确定要清空所有文本记录吗?')) { fetch(textApi, { method: 'DELETE' }).then(() => loadHistory()); } } // -------- 图片剪贴板 -------- // 注意:以下功能成功与否,强依赖于WebDAV服务器的CORS配置 function sendWebDAVRequest(method, path, data, callback) { const xhr = new XMLHttpRequest(); xhr.open(method, webdavUrl + path, true); xhr.setRequestHeader('Authorization', 'Basic ' + btoa(webdavUser + ':' + webdavPass)); xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) { callback(xhr); } else { alert(`操作失败: ${xhr.status} ${xhr.statusText}\n请检查WebDAV服务配置,特别是CORS跨域设置。`); } }; xhr.onerror = () => { alert('请求错误,无法连接到WebDAV服务器。请检查URL、端口或网络防火墙设置。'); }; xhr.send(data); } function uploadImage() { const file = document.getElementById('imgFile').files[0]; if (!file) return alert('请选择文件'); sendWebDAVRequest('PUT', file.name, file, () => { alert('上传完成'); loadImages(); document.getElementById('imgFile').value = ''; // 清空选择 }); } function loadImages() { sendWebDAVRequest('PROPFIND', '', null, (xhr) => { const parser = new DOMParser(); const xml = parser.parseFromString(xhr.responseText, 'text/xml'); const hrefs = Array.from(xml.getElementsByTagName('d:href')) .map(x => decodeURIComponent(x.textContent)) .filter(x => x.startsWith('/rax3000m_emmc/clipboard/images/')) // 确保是文件路径 .map(x => x.substring('/rax3000m_emmc/clipboard/images/'.length)) .filter(Boolean); // 过滤掉空字符串(目录本身) const container = document.getElementById('images'); container.innerHTML = ''; hrefs.forEach(name => { const card = document.createElement('div'); card.className = 'image-card'; const img = document.createElement('img'); img.src = webdavUrl + name; img.title = "点击下载"; img.onclick = () => window.open(webdavUrl + name, '_blank'); const del = document.createElement('button'); del.textContent = '🗑 删除'; del.onclick = () => deleteImage(name); card.appendChild(img); card.appendChild(del); container.appendChild(card); }); }); } function deleteImage(name) { if (confirm(`确定要删除图片 "${name}" 吗?`)) { sendWebDAVRequest('DELETE', name, null, () => loadImages()); } } function clearAllImages() { if(confirm('确定要删除所有图片吗?此操作不可恢复!')){ const imageCards = document.querySelectorAll('.image-card'); if (imageCards.length === 0) return; const fileNames = Array.from(imageCards).map(card => card.querySelector('img').src.split('/').pop()); let completed = 0; fileNames.forEach(name => { sendWebDAVRequest('DELETE', name, null, () => { completed++; if (completed === fileNames.length) { loadImages(); // 所有都删除完后刷新一次 } }); }); } } // 初始化 loadHistory(); loadImages(); </script> </body> </html>

root@RAX3000M:/etc/lighttpd/conf.d# cat ../lighttpd.conf

Documentation

https://wiki.lighttpd.net/

Configuration Syntax

https://wiki.lighttpd.net/Docs_Configuration

Configuration Options

https://wiki.lighttpd.net/Docs_ConfigurationOptions

Configuration Variables (potentially used in /etc/lighttpd/conf.d/*.conf)

var.log_root = "/var/log/lighttpd/"
var.server_root = "/www/"
var.state_dir = "/var/run/"
var.home_dir = "/var/lib/lighttpd/"
var.conf_dir = "/etc/lighttpd"
var.vhosts_dir = server_root + "/vhosts"
var.cache_dir = "/var/cache/lighttpd"
var.socket_dir = home_dir + "/sockets"

OpenWRT lighttpd base configuration

server.document-root = "/root"
server.upload-dirs = ( "/tmp" )
server.errorlog = log_root + "error.log"
server.pid-file = state_dir + "lighttpd.pid"
server.username = "http"
server.groupname = "www-data"

server.port=8081
$SERVER["socket"] == "[::]:8081" { }
dir-listing.encoding="utf-8"
server.dir-listing="enable"

historical; preserved for compatibility; should have been disabled by default

index-file.names = ( "index.php", "index.html",
"index.htm", "default.htm",
)

static-file.exclude-extensions = ( ".php", ".pl", ".fcgi" )

include "/etc/lighttpd/mime.conf"
include "/etc/lighttpd/conf.d/*.conf"

Customizations

customizations should generally be placed in separate files such as

/etc/lighttpd/conf.d/00_vars.conf # override variables for conf.d/*.conf

/etc/lighttpd/conf.d/zz_custom.conf # override other conf.d/*.conf settings

./20-auth.conf:
#######################################################################

Authentication Module

-----------------------

server.modules += ( "mod_auth" )

auth.backend = "plain"
auth.backend.plain.userfile = "/etc/lighttpd/lighttpd.user"

auth.require = ( "/" =>
(
"method" => "basic",
"realm" => "Server Status",
"require" => "valid-user"
),
)

cache passwords/digests in memory to reduce load on the backend

max-age is in seconds

cache is checked for expiration every 8 seconds, so actual

caching may be up to 8 seconds longer than configured max-age.

default: inactive (no caching)

#auth.cache = ("max-age" => "600")

#######################################################################
./20-authn_file.conf:
server.modules += ( "mod_authn_file" )
./30-webdav.conf:
#######################################################################

WebDAV Module

---------------

See https://redmine.lighttpd.net/projects/lighttpd/wiki/Docs_ModWebDAV

server.modules += ( "mod_webdav" )

#HTTP["url"]= "/dav(HTTP["url"] =~ "^/dav(|/)" {

enable webdav for this location

webdav.activate = "enable"

By default the webdav url is writable.

Uncomment the following line if you want to make it readonly.

webdav.is-readonly = "enable"

SQLite database for WebDAV properties and WebDAV locks

webdav.sqlite-db-name = "/root/rax3000m_emmc/webdav.db"

Log the XML Request bodies for debugging

#webdav.log-xml = "disable"

mod_webdav further tunables

See online doc:

https://redmine.lighttpd.net/projects/lighttpd/wiki/Docs_ModWebDAV

#webdav.opts = ( ... )
#}

#######################################################################

网页显示:
Access to XMLHttpRequest at 'http://192.168.11.1:8081/rax3000m_emmc/clipboard/images/' from origin 'http://192.168.11.1' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
clipboard.html:174 PROPFIND http://192.168.11.1:8081/rax3000m_emmc/clipboard/images/ net::ERR_FAILED
sendWebDAVRequest @ clipboard.html:174
loadImages @ clipboard.html:188
(anonymous) @ clipboard.html:246
favicon.ico:1 GET http://192.168.11.1/favicon.ico 404 (Not Found)

Request URL
http://192.168.11.1:8081/rax3000m_emmc/clipboard/images/
Referrer Policy
strict-origin-when-cross-origin

Request URL
http://192.168.11.1:8081/rax3000m_emmc/clipboard/images/
Request Method
OPTIONS
Status Code
404 Not Found
Remote Address
192.168.11.1:8081
Referrer Policy
strict-origin-when-cross-origin
如何解决?

Pergunta

主机名 RAX3000M
型号 CMCC RAX3000M eMMC version
架构 ARMv8 Processor rev 4 (v8l) x 2
温度 CPU: 70.4°C, WiFi: 64°C
目标平台 mediatek/mt7981
固件版本 ImmortalWrt 21.02 build 20241217 / LuCI openwrt-21.02
/www/clipboard.html:

<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <title>📋 本地剪贴板中心 (RAX3000M)</title> <style> body { font-family: sans-serif; margin: 20px; background-color: #f4f4f4; } h2, h3 { color: #333; } textarea { width: 100%; height: 100px; box-sizing: border-box; border: 1px solid #ccc; border-radius: 4px; padding: 10px; } button { margin: 5px 2px; padding: 8px 12px; border: none; border-radius: 4px; background-color: #007bff; color: white; cursor: pointer; } button:hover { background-color: #0056b3; } input[type="file"] { margin-top: 10px; } #history { margin-top: 10px; background: white; padding: 10px; border-radius: 4px; border: 1px solid #ddd;} .history-item { display: flex; justify-content: space-between; align-items: center; padding: 8px; border-bottom: 1px solid #eee; } .history-item:last-child { border-bottom: none; } .history-item span { word-break: break-all; margin-right: 10px; } .history-item .buttons { flex-shrink: 0; } #images { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 10px; } .image-card { display: flex; flex-direction: column; align-items: center; border: 1px solid #ccc; padding: 10px; border-radius: 4px; background: white; } .image-card img { max-width: 150px; max-height: 150px; display: block; margin-bottom: 10px; cursor: pointer; } .image-card button { background-color: #dc3545; } .image-card button:hover { background-color: #c82333; } </style> </head> <body> <h2>📋 本地剪贴板中心 (RAX3000M)</h2> <h3>📝 文本剪贴板</h3> <textarea id="textArea" placeholder="在此输入或粘贴文本内容..."></textarea><br> <button onclick="addText()">保存到历史</button> <button onclick="copyTextArea()">复制当前</button> <button onclick="clearAllText()">清空所有</button> <h4>📜 历史记录</h4> <div id="history">(暂无记录)</div> <h3>📷 图片剪贴板</h3> <p style="color:red;"><b>重要提示:</b> 图片功能依赖WebDAV服务。如果无法使用,请确保路由器的WebDAV服务已开启,并且允许跨域请求(CORS)。</p> <input type="file" id="imgFile"> <button onclick="uploadImage()">上传图片</button> <button onclick="clearAllImages()">清空所有图片</button> <div id="images"></div> <script> const textApi = '/cgi-bin/clipboard.sh'; const webdavUrl = 'http://192.168.11.1:8081/rax3000m_emmc/clipboard/images/'; const webdavUser = 'asdf'; const webdavPass = 'asdf'; // -------- 通用函数 -------- /** * 兼容HTTP环境的文本复制函数 * @param {string} text 要复制的文本 */ function copyToClipboard(text) { // 优先使用 navigator.clipboard (适用于HTTPS) if (navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(text).then(() => { alert('已复制'); }).catch(err => { alert('复制失败: ' + err); }); } else { // 备用方案 (适用于HTTP) const textArea = document.createElement('textarea'); textArea.value = text; textArea.style.position = 'fixed'; // 防止屏幕滚动 textArea.style.opacity = '0'; document.body.appendChild(textArea); textArea.focus(); textArea.select(); try { document.execCommand('copy'); alert('已复制'); } catch (err) { alert('复制失败,请手动复制'); } document.body.removeChild(textArea); } } // -------- 文本剪贴板 -------- function loadHistory() { fetch(textApi) .then(r => { if (!r.ok) throw new Error('Network response was not ok'); return r.json(); }) .then(list => { const container = document.getElementById('history'); container.innerHTML = ''; if (!list || list.length === 0) { container.textContent = '(暂无记录)'; return; } list.reverse().forEach(item => { const div = document.createElement('div'); div.className = 'history-item'; const span = document.createElement('span'); span.textContent = item.text; const btnContainer = document.createElement('div'); btnContainer.className = 'buttons'; const copyBtn = document.createElement('button'); copyBtn.textContent = '复制'; copyBtn.onclick = () => copyToClipboard(item.text); const delBtn = document.createElement('button'); delBtn.textContent = '删除'; delBtn.style.backgroundColor = '#6c757d'; delBtn.onclick = () => { fetch(`${textApi}?id=${item.id}`, { method: 'DELETE' }) .then(() => loadHistory()); }; btnContainer.appendChild(copyBtn); btnContainer.appendChild(delBtn); div.appendChild(span); div.appendChild(btnContainer); container.appendChild(div); }); }) .catch(error => { document.getElementById('history').textContent = '加载历史记录失败,请检查后端脚本和JSON文件内容是否正确。错误: ' + error; console.error('Error loading history:', error); }); } function addText() { const val = document.getElementById('textArea').value; if (!val) return alert('请输入文本'); fetch(textApi, { method: 'POST', body: val }) .then(() => { loadHistory(); document.getElementById('textArea').value = ''; }); } function copyTextArea() { const text = document.getElementById('textArea').value; if (text) { copyToClipboard(text); } else { alert('当前无内容可复制'); } } function clearAllText() { if (confirm('确定要清空所有文本记录吗?')) { fetch(textApi, { method: 'DELETE' }).then(() => loadHistory()); } } // -------- 图片剪贴板 -------- // 注意:以下功能成功与否,强依赖于WebDAV服务器的CORS配置 function sendWebDAVRequest(method, path, data, callback) { const xhr = new XMLHttpRequest(); xhr.open(method, webdavUrl + path, true); xhr.setRequestHeader('Authorization', 'Basic ' + btoa(webdavUser + ':' + webdavPass)); xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) { callback(xhr); } else { alert(`操作失败: ${xhr.status} ${xhr.statusText}\n请检查WebDAV服务配置,特别是CORS跨域设置。`); } }; xhr.onerror = () => { alert('请求错误,无法连接到WebDAV服务器。请检查URL、端口或网络防火墙设置。'); }; xhr.send(data); } function uploadImage() { const file = document.getElementById('imgFile').files[0]; if (!file) return alert('请选择文件'); sendWebDAVRequest('PUT', file.name, file, () => { alert('上传完成'); loadImages(); document.getElementById('imgFile').value = ''; // 清空选择 }); } function loadImages() { sendWebDAVRequest('PROPFIND', '', null, (xhr) => { const parser = new DOMParser(); const xml = parser.parseFromString(xhr.responseText, 'text/xml'); const hrefs = Array.from(xml.getElementsByTagName('d:href')) .map(x => decodeURIComponent(x.textContent)) .filter(x => x.startsWith('/rax3000m_emmc/clipboard/images/')) // 确保是文件路径 .map(x => x.substring('/rax3000m_emmc/clipboard/images/'.length)) .filter(Boolean); // 过滤掉空字符串(目录本身) const container = document.getElementById('images'); container.innerHTML = ''; hrefs.forEach(name => { const card = document.createElement('div'); card.className = 'image-card'; const img = document.createElement('img'); img.src = webdavUrl + name; img.title = "点击下载"; img.onclick = () => window.open(webdavUrl + name, '_blank'); const del = document.createElement('button'); del.textContent = '🗑 删除'; del.onclick = () => deleteImage(name); card.appendChild(img); card.appendChild(del); container.appendChild(card); }); }); } function deleteImage(name) { if (confirm(`确定要删除图片 "${name}" 吗?`)) { sendWebDAVRequest('DELETE', name, null, () => loadImages()); } } function clearAllImages() { if(confirm('确定要删除所有图片吗?此操作不可恢复!')){ const imageCards = document.querySelectorAll('.image-card'); if (imageCards.length === 0) return; const fileNames = Array.from(imageCards).map(card => card.querySelector('img').src.split('/').pop()); let completed = 0; fileNames.forEach(name => { sendWebDAVRequest('DELETE', name, null, () => { completed++; if (completed === fileNames.length) { loadImages(); // 所有都删除完后刷新一次 } }); }); } } // 初始化 loadHistory(); loadImages(); </script> </body> </html>

root@RAX3000M:/etc/lighttpd/conf.d# cat ../lighttpd.conf

Documentation

https://wiki.lighttpd.net/

Configuration Syntax

https://wiki.lighttpd.net/Docs_Configuration

Configuration Options

https://wiki.lighttpd.net/Docs_ConfigurationOptions

Configuration Variables (potentially used in /etc/lighttpd/conf.d/*.conf)

var.log_root = "/var/log/lighttpd/"
var.server_root = "/www/"
var.state_dir = "/var/run/"
var.home_dir = "/var/lib/lighttpd/"
var.conf_dir = "/etc/lighttpd"
var.vhosts_dir = server_root + "/vhosts"
var.cache_dir = "/var/cache/lighttpd"
var.socket_dir = home_dir + "/sockets"

OpenWRT lighttpd base configuration

server.document-root = "/root"
server.upload-dirs = ( "/tmp" )
server.errorlog = log_root + "error.log"
server.pid-file = state_dir + "lighttpd.pid"
server.username = "http"
server.groupname = "www-data"

server.port=8081
$SERVER["socket"] == "[::]:8081" { }
dir-listing.encoding="utf-8"
server.dir-listing="enable"

historical; preserved for compatibility; should have been disabled by default

index-file.names = ( "index.php", "index.html",
"index.htm", "default.htm",
)

static-file.exclude-extensions = ( ".php", ".pl", ".fcgi" )

include "/etc/lighttpd/mime.conf"
include "/etc/lighttpd/conf.d/*.conf"

Customizations

customizations should generally be placed in separate files such as

/etc/lighttpd/conf.d/00_vars.conf # override variables for conf.d/*.conf

/etc/lighttpd/conf.d/zz_custom.conf # override other conf.d/*.conf settings

./20-auth.conf:
#######################################################################

Authentication Module

-----------------------

server.modules += ( "mod_auth" )

auth.backend = "plain"
auth.backend.plain.userfile = "/etc/lighttpd/lighttpd.user"

auth.require = ( "/" =>
(
"method" => "basic",
"realm" => "Server Status",
"require" => "valid-user"
),
)

cache passwords/digests in memory to reduce load on the backend

max-age is in seconds

cache is checked for expiration every 8 seconds, so actual

caching may be up to 8 seconds longer than configured max-age.

default: inactive (no caching)

#auth.cache = ("max-age" => "600")

#######################################################################
./20-authn_file.conf:
server.modules += ( "mod_authn_file" )
./30-webdav.conf:
#######################################################################

WebDAV Module

---------------

See https://redmine.lighttpd.net/projects/lighttpd/wiki/Docs_ModWebDAV

server.modules += ( "mod_webdav" )

#HTTP["url"]= "/dav(HTTP["url"] =~ "^/dav(|/)" {

enable webdav for this location

webdav.activate = "enable"

By default the webdav url is writable.

Uncomment the following line if you want to make it readonly.

webdav.is-readonly = "enable"

SQLite database for WebDAV properties and WebDAV locks

webdav.sqlite-db-name = "/root/rax3000m_emmc/webdav.db"

Log the XML Request bodies for debugging

#webdav.log-xml = "disable"

mod_webdav further tunables

See online doc:

https://redmine.lighttpd.net/projects/lighttpd/wiki/Docs_ModWebDAV

#webdav.opts = ( ... )
#}

#######################################################################

网页显示:
Access to XMLHttpRequest at 'http://192.168.11.1:8081/rax3000m_emmc/clipboard/images/' from origin 'http://192.168.11.1' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
clipboard.html:174 PROPFIND http://192.168.11.1:8081/rax3000m_emmc/clipboard/images/ net::ERR_FAILED
sendWebDAVRequest @ clipboard.html:174
loadImages @ clipboard.html:188
(anonymous) @ clipboard.html:246
favicon.ico:1 GET http://192.168.11.1/favicon.ico 404 (Not Found)

Request URL
http://192.168.11.1:8081/rax3000m_emmc/clipboard/images/
Referrer Policy
strict-origin-when-cross-origin

Request URL
http://192.168.11.1:8081/rax3000m_emmc/clipboard/images/
Request Method
OPTIONS
Status Code
404 Not Found
Remote Address
192.168.11.1:8081
Referrer Policy
strict-origin-when-cross-origin
如何解决?

Pensamento

Thought for 1m 46s

Resposta

你这个报错本质是跨域(CORS) + 预检(OPTIONS) 请求 404,再叠加了几个配置问题:

  • 页面在 http://192.168.11.1(80 端口),WebDAV 在 http://192.168.11.1:8081(不同端口 ⇒ 不同源)
  • lighttpd 没返回 Access-Control-Allow-* 头;
  • 预检 OPTIONS 命中 404(WebDAV/路径/鉴权没有正确处理);
  • 你现在的 WebDAV 还是只读webdav.is-readonly = "enable"),即使过了 CORS,PUT/DELETE 也会失败。

下面给出两种可选的解决方案,任选其一即可。


方案 A(推荐,最省心):改成“同源”,彻底避免 CORS

clipboard.html8081 提供,并让前端请求同一端口的 WebDAV。

  1. 把页面放到 lighttpd 的根目录(你现在是 /root):
sh
mv /www/clipboard.html /root/clipboard.html
  1. 修改前端常量(避免跨域):
js
// 把 webdavUrl 改成相对路径(不含协议和端口) const webdavUrl = '/rax3000m_emmc/clipboard/images/';

之后用 http://192.168.11.1:8081/clipboard.html 打开页面即可。

注意:你的 textApi = '/cgi-bin/clipboard.sh' 仍然走 80 端口的 uhttpd(LuCI),会再次跨域。

  • 最简单:把 textApi 也写成绝对地址 http://192.168.11.1:8081/cgi-bin/clipboard.sh,并在 lighttpd 做反代(见下方“可选:反向代理”);
  • 或者把 CGI 脚本迁到 lighttpd(需要启用 cgi/fcgi,不展开)。

可选:在 lighttpd 给 /cgi-bin/ 做反向代理到 80 端口(这样依然同源 8081):

text
# /etc/lighttpd/conf.d/35-proxy.conf server.modules += ( "mod_proxy" ) proxy.server = ( "/cgi-bin/" => ( ( "host" => "127.0.0.1", "port" => 80 ) ) )

然后把前端里的 textApi 改为:

js
const textApi = '/cgi-bin/clipboard.sh';

方案 B:保留跨端口,正确开启 CORS(含预检 & 写权限)

1) 让目录真实存在并可写

sh
mkdir -p /root/rax3000m_emmc/clipboard/images chown -R http:www-data /root/rax3000m_emmc chmod -R 775 /root/rax3000m_emmc

2) 正确限定 WebDAV 路径、允许写、并返回 CORS 头

/etc/lighttpd/conf.d/30-webdav.conf 改成按路径生效,并加上 CORS。示例(可直接覆盖成下面这段):

text
####################################################################### ## WebDAV on /rax3000m_emmc/clipboard/images ####################################################################### server.modules += ( "mod_webdav", "mod_setenv" ) $HTTP["url"] =~ "^/rax3000m_emmc/clipboard/images($|/)" { webdav.activate = "enable" webdav.is-readonly = "disable" # 允许 PUT/DELETE webdav.sqlite-db-name = "/root/rax3000m_emmc/webdav.db" # CORS 响应头(按需要精确到你的来源) setenv.add-response-header = ( "Access-Control-Allow-Origin" => "http://192.168.11.1", "Access-Control-Allow-Credentials" => "true", "Access-Control-Allow-Methods" => "GET, PUT, DELETE, PROPFIND, MKCOL, OPTIONS", "Access-Control-Allow-Headers" => "Authorization, Depth, Content-Type, Destination, Overwrite", "Access-Control-Max-Age" => "86400" ) # 预检请求不做鉴权拦截,直接 200 $HTTP["request-method"] == "OPTIONS" { auth.require = () } }

说明:

  • mod_setenv 用于添加响应头;
  • OPTIONS 子块清空了 auth.require,否则浏览器的预检不会带凭据,会被 401/403 拦住;
  • webdav.is-readonly = "disable" 才能上传/删除。

3) 把全局鉴权改成“只保护需要的路径”

你当前 /etc/lighttpd/conf.d/20-auth.conf全站 "/" 都做了 Basic 认证,会影响预检。改成只在 WebDAV 路径上鉴权(或保持你需要的其它路径),并与上面的 OPTIONS 豁免配合:

text
server.modules += ( "mod_auth", "mod_authn_file" ) auth.backend = "plain" auth.backend.plain.userfile = "/etc/lighttpd/lighttpd.user" # 仅保护 WebDAV 目录(如需) $HTTP["url"] =~ "^/rax3000m_emmc/clipboard/images($|/)" { auth.require = ( "" => ( "method" => "basic", "realm" => "WebDAV", "require" => "valid-user" ) ) }

如果你还要保护别的目录,可按需再加对应的 $HTTP["url"] 块。不要再用对 "/" 的全局保护,否则还得额外豁免预检。

4) 重启并自检

sh
/etc/init.d/lighttpd restart

自检(在路由器上执行):

sh
# 预检(应返回 200 且含 Access-Control-Allow-*) curl -i -X OPTIONS \ 'http://192.168.11.1:8081/rax3000m_emmc/clipboard/images/' \ -H 'Origin: http://192.168.11.1' \ -H 'Access-Control-Request-Method: PROPFIND' \ -H 'Access-Control-Request-Headers: authorization, depth' # 列目录(PROPFIND) curl -i -X PROPFIND \ 'http://192.168.11.1:8081/rax3000m_emmc/clipboard/images/' \ -u asdf:asdf -H 'Depth: 1' # 上传(PUT) curl -i -X PUT --data-binary @/etc/banner \ 'http://192.168.11.1:8081/rax3000m_emmc/clipboard/images/test.txt' \ -u asdf:asdf

前端不改也能跑的小调整

你的 JS 已经手动加了 Authorization: Basic ...,这是可以的;只要服务器允许跨域并放行预检就行。
另外建议把允许的方法里包含 MKCOL(有些 WebDAV 客户端会用到)。


额外提示

  • 账号 asdf/asdf 太弱了,至少换个强密码;
  • 若开启了 Access-Control-Allow-Credentials: trueOrigin 不能用 *,要写成具体来源(上面已示范);
  • favicon.ico 的 404 无关紧要,想消除就放一个到 /root/favicon.ico 或在 HTML 里 <link rel="icon" href="data:,">
  • 如果仍返回 404,十有八九是目录不存在/路径不匹配或 WebDAV 没在该 URL 上激活,按上面的“路径正则 + 目录存在”检查即可。

一句话总结

要么把页面和 WebDAV 放到同一个端口(最简单),要么在 8081 上正确开启 CORS + 放行 OPTIONS + 允许写。上面的配置按步骤套上去即可解决你看到的 No 'Access-Control-Allow-Origin' 和预检 404。

Compartilhar esta Q&A