问题在于 当我在家里的 NAS 上跑了一个 Web 服务,家宽的 Web 服务端口被封禁了,可我不想访问的时候带上端口(很丑)怎么办?
反向代理 通常情况下我们会找一个中间服务器来做反代,内网穿透或者自己找台VPS都可以实现。这么做的问题是什么?
NAS 和反代服务器哪个带宽低哪个就是瓶颈。
现在数据要多走点距离了,访问会变慢。
URL转发 许多 DNS 服务提供商都允许你设置一个特殊的记录,只要把正经的域名转发到 NAS 上那个带端口的 URL 即可。这么做的问题是什么?
如果你设置显性转发,那么用户还是会在地址栏看到那奇丑无比的 URL。
如果你设置隐性转发,大部分 DNS 服务提供商就不会允许配置 https。这就意味着用户虽然能看到好看的 URL,却会在 URL 旁边看到一个奇丑无比的 不安全
标识。
解决方案 直接说结论,我们找一台有公网IP的机子(下称代理机),域名解析过去,然后在这台机子上摆一页 Html,用 iframe
把源站(没有公网IP的机子)页面嵌进去,然后用 js 对站点标题和图标进行同步。
优势
流量不经过代理机,直接到源站。
代理机负载极低,只是提供小型静态 Html 文件,所以配置可以很低,你甚至可以用 Github Pages 。
用户不会看到实际的 URL,同时保持正确的图标和标题(需修改源站代码)。
劣势
源站必须允许跨域。
iframe
可能导致加载速度略慢。
可能出现查询字符串以及一些莫名其妙的问题。
极其不利于 SEO。
总的来说,这方法适合个人博客或 NAS 上的 Jellyfin、MT Photos 之类的。对于大型网站、动态内容多的社交平台或 API 密集型应用,不建议使用,图个好看而带来一堆问题,得不偿失啦。
步骤 根据需求修改下方的代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 <!DOCTYPE html > <html lang ="zh" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > Loading...</title > <link id ="favicon" rel ="icon" href ="favicon.png" type ="image/png" > <style > body , html { margin : 0 ; padding : 0 ; overflow : hidden; width : 100% ; height : 100% ; } iframe { border : none; width : 100% ; height : 100% ; } </style > </head > <body > <iframe id ="contentFrame" src ="" > </iframe > <script > const hostMap = { 'example.com' : 'https://source.top:1234' , 'xxx.example.com' : 'https://source.top:5678' , }; const currentHost = window .location .host ; const frameSource = hostMap[currentHost]; if (frameSource) { const iframe = document .getElementById ('contentFrame' ); const urlParams = window .location .search + window .location .hash ; iframe.src = frameSource + urlParams; let currentFavicon = 'favicon.png' ; let currentTitle = 'Loading...' ; function updateFaviconAndTitle (favicon, title ) { if (favicon && favicon !== currentFavicon) { document .getElementById ('favicon' ).href = favicon; currentFavicon = favicon; } if (title && title !== currentTitle) { document .title = title; currentTitle = title; } } window .addEventListener ('message' , function (event ) { if (event.origin === frameSource) { const { favicon, title } = event.data ; console .log ('[Dynamic Iframe Meta Sync] Got favicon:' , favicon); console .log ('[Dynamic Iframe Meta Sync] Got title:' , title); updateFaviconAndTitle (favicon, title); } }, false ); const updateUrl = (event ) => { const newUrl = new URL (event.target .contentWindow .location .href ); const newHost = newUrl.host ; if (newHost !== currentHost) { window .history .pushState ({}, '' , newUrl.origin + newUrl.pathname + urlParams); } }; iframe.onload = () => { const iframeWindow = iframe.contentWindow ; iframeWindow.addEventListener ('popstate' , updateUrl); iframeWindow.addEventListener ('hashchange' , updateUrl); }; } else { document .body .innerHTML = '<h1>无效的访问地址</h1>' ; } </script > </body > </html >
修改后保存为 index.html
,然后丢到有公网IP的服务器上,或者上传到 Github Pages 之类的地方托管也可以,确保可以通过你设置的 URL 访问到这个文件。
接下来由于跨域问题,代理站无法获取源站的图标和标题,因此我们为源站添加一个 js,来告知代理站当前的标题和图标。
将下面的文件保存为 dynamic-iframe-agent.js
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 (function ( ) { let originalTitle = document .title ; let originalSetTitle; let intervalId; function sendMetaInfo ( ) { try { const favicon = document .querySelector ('link[rel="icon"]' ) || document .querySelector ('link[rel="shortcut icon"]' ); const faviconUrl = favicon ? favicon.href : '' ; let title = document .title ; if (!title.trim ()) { title = document .querySelector ('title' )?.textContent || '' ; } if (!title.trim () && !intervalId) { intervalId = setInterval (checkVisibleTitle, 1000 ); } else if (title.trim () && intervalId) { clearInterval (intervalId); intervalId = null ; } if (window .parent !== window ) { console .log ('[Dynamic Iframe Meta Sync] Sent favicon:' , faviconUrl); console .log ('[Dynamic Iframe Meta Sync] Sent title:' , title); window .parent .postMessage ({ favicon : faviconUrl, title : title }, '*' ); } } catch (error) { console .error ('Error in sendMetaInfo:' , error); } } function checkVisibleTitle ( ) { const visibleTitle = document .title || document .querySelector ('title' )?.textContent || '' ; if (visibleTitle.trim ()) { sendMetaInfo (); clearInterval (intervalId); intervalId = null ; } } function init ( ) { const titleDescriptor = Object .getOwnPropertyDescriptor (Document .prototype , 'title' ); originalSetTitle = titleDescriptor.set ; Object .defineProperty (document , 'title' , { set : function (newTitle ) { if (originalSetTitle) { originalSetTitle.call (this , newTitle); } originalTitle = newTitle; sendMetaInfo (); }, get : function ( ) { return originalTitle; } }); sendMetaInfo (); const faviconObserver = new MutationObserver (function (mutations ) { mutations.forEach (function (mutation ) { if (mutation.type === 'attributes' && mutation.attributeName === 'href' ) { sendMetaInfo (); } }); }); const favicon = document .querySelector ('link[rel="icon"]' ) || document .querySelector ('link[rel="shortcut icon"]' ); if (favicon) { faviconObserver.observe (favicon, { attributes : true }); } const titleObserver = new MutationObserver (function (mutations ) { mutations.forEach (function (mutation ) { if (mutation.type === 'childList' || mutation.type === 'characterData' ) { sendMetaInfo (); } }); }); const titleElement = document .querySelector ('title' ); if (titleElement) { titleObserver.observe (titleElement, { childList : true , characterData : true , subtree : true }); } } if (document .readyState === 'loading' ) { document .addEventListener ('DOMContentLoaded' , init); } else { init (); } const headObserver = new MutationObserver (function (mutations ) { mutations.forEach (function (mutation ) { if (mutation.type === 'childList' ) { mutation.addedNodes .forEach (function (node ) { if (node.nodeName === 'LINK' && (node.rel === 'icon' || node.rel === 'shortcut icon' )) { sendMetaInfo (); faviconObserver.observe (node, { attributes : true }); } else if (node.nodeName === 'TITLE' ) { sendMetaInfo (); titleObserver.observe (node, { childList : true , characterData : true , subtree : true }); } }); } }); }); headObserver.observe (document .head , { childList : true , subtree : true }); })();
如果源站的页面不多,你可以手动在每个页面引用 dynamic-iframe-agent.js
;而如果是动态站点或比较复杂的应用,我推荐在反代程序上配置引用 dynamic-iframe-agent.js
,这里以 Nginx 为例,其它反代程序请自行查找修改响应内容的方法。
手动引用 Nginx
在原站根目录下添加 dynamic-iframe-agent.js
文件,确保文件名正确且大小写无误。
在源站的每个 Html 文件里的 </body>
标签前添加一行代码:
1 <script src ="/dynamic-iframe-agent.js" > </script >
首先为了确保你的站点能够访问这个文件,在该站点配置中添加以下代码。
1 2 3 4 location /dynamic-iframe-agent.js { alias path/to/dynamic-iframe-agent.js; add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0' ; }
记得将 path/to/dynamic-iframe-agent.js
修改为你的 dynamic-iframe-agent.js
文件的实际路径。
第二行设置 Cache-Control
是调试用的,施工完毕后确保无误可以删除这一行。
确保你的 Nginx 安装了 sub_filter 模块(通常是自带的),如果你不确定,使用 nginx -v
指令确定是否包含 --with-http_sub_module
字样。
随后在你的站点配置里添加以下代码。
1 2 3 4 proxy_set_header Accept-Encoding "" ;sub_filter '</body>' '<script src="/dynamic-iframe-agent.js"></script></body>' ;sub_filter_once on ;
设置 Accept-Encoding
是因为 sub_module
是通过替换文件里的指定字样以达到修改目的的,如果源站开启了压缩则无法生效。
这里提供一份完整的站点配置示例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 server { listen 2488 ssl; server_name server.leever.cn; proxy_set_header Host 127.0.0.1 ; add_header 'Access-Control-Allow-Origin' 'https://rain.leever.cn' ; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' ; add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range' ; add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' ; add_header Content-Security-Policy "frame-ancestors 'self' https://rain.leever.cn" ; location /dynamic-iframe-agent.js { alias D:/Software/Nginx/sub_filter/dynamic-iframe-agent.js; } location / { proxy_pass http://127.0.0.1:8778; proxy_set_header Accept-Encoding "" ; sub_filter '</body>' '<script src="/dynamic-iframe-agent.js"></script></body>' ; sub_filter_once on ; } }
最后,你需要确认源站是支持跨域的,否则页面无法正常显示,你会在控制台看到 Uncaught DOMException: Blocked a frame with origin "https://example.com" from accessing a cross-origin frame.
之类的字样。
顺带一提:代理站和源站需要同时使用 Https,或者同时使用 Http,两者协议不同会导致 iframe
不加载。