# 用Nginx架設local端https應用 ###### tags: `Nginx` `Ubuntu` `WSL` `WebApp` [TOC] ## 背景 某天閒來無事的我寫了一個Web App,想要分享給別人使用,剛好實驗室有固定IP的電腦可以用,為了省錢(X)就決定直接開在上面。一開始一切都很美好,用Docker開好了MySQL server,Node.js的server也順利開起來連上DB,localhost測起來相安無事,於是乎,拿起我的小筆電,連連看實驗室IP,恩,可以看到網頁,但...好像有某個功能壞了(?) {%hackmd 2q2OKXhpSca0en6d0b1GEg %} 原本在WebApp中有個功能是可以直接複製文字到剪貼簿中,用到了[Clipboard API](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/clipboard),在localhost中測試時一切都好好的,也可以順利地要求剪貼簿權限,同意後就有相關API可以call,但這時卻連權限要求的通知都沒有跳出來,從網址列點進去權限面板看看發現,不對啊!怎麼剪貼簿權限連開都沒辦法開,正常來講應該要像下面這張圖片一樣,Clipboard那邊是可以選擇要不要給權限的 ![](https://i.imgur.com/f7E0doX.png) <center class="description"> <small> 在Edge上面看到的關於網頁各種權限的管理>在Edge上面看到的關於網頁各種權限的管理 </small> </center> <br/> 但我點進去卻發現那個selector完全是disabled的,不給按QQ仔細看了敘述之後才發現,應該是因為網站是用http所以才會被擋掉權限,但這也太狠了,連我想要把自記置身於危險之中都不讓,不過其實早就有傳言Chrome會加強安全性,之後連http的網站都沒辦法訪問,用Chromium核心的Edge自然也是跟上這股歪風(X) ![](https://i.imgur.com/JAAbhPT.png) <center class="description"> <small> 不給按 </small> </center> <br/> 雖然只是個不起眼的小功能,但不能用了還是會感到很阿雜,手動選取文字再按 `ctrl+c` 就是很麻煩(懶),所以決定要幫這個小專案加上https。 ## 實作 當然這個只是個個人用的小project,不太可能為了它去買個網域+SSL憑證,所以想說可以自己sign一個憑證來用用,雖然瀏覽器會跳警告說這是不安全的憑證,但至少還算是有個https可以用,於是我們就開做吧! ### Web App 這個部分就沒甚麼好講的了,基本上任何的APP都可以用這個方法賦予它https,這次我是用nest-next開了一個前+後端的APP在windows上的port 8081上面,連線到localhost:8081確定APP可以正常運行,這時候如果從外部連到http://xxx.xxx.xxx.xxx:8081是可以順利看到運行中的APP的,但如果切https://xxx.xxx.xxx.xxx:8081則完全沒有畫面 ### 建立SSL(Secure Sockets Layer)憑證 這部分我其實沒有很熟悉,主要是參考[這篇](https://juejin.cn/post/7081627429646630942)一步一步做的,而SSL的原理可以看[這裡](https://support.unethost.com/index.php?rp=/knowledgebase/82/SSLSSL-certificate.html),簡言之SSL會加密browser與server間傳輸的通訊內容,使傳遞的資料在傳輸過程中,不會被第三方(如proxy、router、攔截)看見明文,為了達到這個目的,server會生成一對可以相互加解密的公鑰(public key)與私鑰(private key),並將public key工愾讓所有連線的browser都可以取得,在傳輸資料前,瀏覽器會以這支公鑰加密內容,server收到data後,再以自己保有的私鑰解密。 但瀏覽器怎麼知道這隻公鑰是不是真的從server發出來的呢?如果有惡意人士替換掉了傳給browser的公鑰,那麼他就有辦法從中攔截回傳的data並解密出明文。所以現行server發出來的公鑰,必須經過權威第三方,也就是CA(Certificate Authority)去簽發認證你是這個公鑰的擁有者,這個簽發過的公鑰就被稱為憑證(certificate)。 ![certificate](https://d1smxttentwwqu.cloudfront.net/wp-content/uploads/2019/07/ca-diagram-b.png) <center class="description"> <small> 憑證生成流程圖 </small> </center> <br/> 為了方便管理等一下生成的檔案,我在`/home/{username}`新建一個名為`CA`的資料夾,以下的指令都是在該資料夾中執行。第一步我們先生成一個rsa非對稱加密演算法的2048位元長的密鑰,這隻密鑰等一下會同時作為上圖中的private key(Application)與private key(CA)的用途 ```bash openssl genrsa -out privatekey.pem 2048 ``` 接者以這支密鑰建立憑證簽章請求檔案,這邊會有一輪身家調查,但我們也不是正經(?)的公司,所以就意思意思回答一下即可 ```bash openssl req -new -key privatekey.pem -out private-csr.pem ``` 最後再以同一隻密鑰來簽發憑證,這裡面的參數`-days`可以調久一點,就不會需要常常回來更新憑證 ```bash openssl x509 -req -days 365 -in private-csr.pem -signkey privatekey.pem -out certificate.pem ``` 簡單的三個步驟我們久有了一組~~免費~~自製的SSL憑證了,接下來就要把這些憑證掛到Nginx的server上面。 ### 安裝Nginx :::success Nginx是非同步框架的網頁伺服器,也可以用作反向代理、負載平衡器和HTTP快取。該軟體由俄羅斯程式設計師伊戈爾·賽索耶夫開發並於2004年首次公開發布。2011年成立同名公司以提供支援服務。2019年3月11日,Nginx公司被F5網路公司以6.7億美元收購。 <span style="float:right">------ 維基百科</span> ::: 所以我們的目標就是開啟Nginx的反向代理,把80 port跟443 port的流量都導向8081 port,然後再掛一個SSL憑證來啟用https,說起來簡單做起來難,好在網路上都已經有各路大神給出的詳細指示,照著做準沒錯(吧 但我從來沒有去過Nginx的官網,之前都是直接下指令在Linux上安裝的,那要怎麼在Windows上面裝呢?很簡單,不要裝在Windows上面XDD這時候我們的好朋友WSL(Windows Subsystem for Linux)就派上用場啦!在Terminal裡面打開WSL,輸入以下指令一鍵安裝 ```bash sudo apt-install nginx ``` 等它安裝好了之後,再輸入以下指令檢查,應該就會顯示版號了 ```bash nginx -v nginx version: nginx/1.18.0 (Ubuntu) ``` 接者要在Ubuntu上面把Nginx打開 ```bash sudo service nginx start * Starting nginx nginx ``` 他預設會在80 port上面開啟自帶的網頁,這時候到localhost去看就可以看到Nginx自帶的首頁 ### Nginx反向代理(Reverse proxy) 正向的代理(proxy)是指我們在瀏覽網頁的時候,不直接用我們的電腦去跟server溝通,而是將請求發到proxy server,再由後者去向server request資料,好處是client端的IP位址可以被隱藏起來,server只會知道proxy server的IP,另外就是可以用proxy server來做一些cache,讓瀏覽時的反應速度更好。 而反向代理則是網路上的用戶在瀏覽我們的時候,不能直接跟server request資料,而是統一向reverse proxy server要求,後者根據要求的資料來將流量導至背後不同的server,好處是我們可以在同一台電腦上開多個server在不同的port,並由同一個proxy server對外接口,根據user request的網址來決定要將流量導至哪一個server,另外也可以做到load balance的功能,所謂load balance就是同樣的server我有好幾台,在request叫到proxy server的時候,去分流到不同的機器上面,降低每一台機器再同意時間的負擔,從而提供比較流暢的體驗。 ![reverse proxy](https://www.asustor.com/images/admv2/reverse_proxy/revise_proxy_1) <center class="description"> <small> Reverse proxy示意圖 <a href="https://www.asustor.com/admv2?type=2&subject=8&sub=153&lan=en" target="_blank">source</a> </small> </center> <br/> 要在Nginx中做反向代理也不難,只要修改相關config檔就可以了,這邊我直接修改default的config,首先開啟設定檔,我這邊是直接用vim修改,但其實升級WSL2之後可以支援WSLg,就可以安裝一些GUI的編輯器來開啟Linux上的檔案 ```bash sudo vim /etc/nginx/sites-available/default ``` :::warning /etc/nginx/sites-available/default預設是readonly的檔案,若沒有修改權限不加sudo就直接編輯,最後會無法寫入檔案 ::: 設定檔打開後大概會長得像是這樣 ```nginx server { listen 80; listen [::]:80 ; # SSL configuration listen 443 ssl ; listen [::]:443 ssl ; server_name www.example.com; root /var/www/nginx-default/; index index.html location / { # [...] } location /foo { # [...] } location /bar { root /some/other/place; # [...] } } ``` 每一個block會用大括號括起來,block裡面的參數則是每一行以`key value;`的形式去設定,如果要下comment就用`#`放在一行的最開始。簡單介紹一下這裡面出現的一些參數: - `server` 是一個基礎的block,nginx會依照block裡面的設定去開一台server - `listen` 則是設定這台server要監聽那些port,第二列的`[::]:80`是設定IPv6的port - `server_name` 是設定這台sever的網域名稱,這裡如果沒有網域可以設定為`_`代表任何網域 - `root` 是設定這台server的根目錄,如果有要serve靜態檔案的話會從這裡面找 - `index` 是設定沒有path name的時候要回傳的首頁檔案 - `location` 可以個別設定server在收到不同路徑的request時要套用的設定 接著來設定proxy,這邊想做的是將連進來的請求,全部導向我們的Web App,上面已經設定好監聽80與443 port了,剩下要做的就是重新導向,將`location /`裡面的內容清空,設定為以下這樣 ```nginx location / { proxy_pass http://172.27.224.1; proxy_redirect off; proxy_set_header Host $proxy_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } ``` 其中 - `proxy_pass` 是用來指定proxy的目的地,就是我們App所在的位置,這邊因為App是開在windows上所以IP位置是填WSL所看到的windows網卡接口 - `proxy_redirect` 是用來改寫server回傳的301或302(redirect)中header的字串,這邊目前還用不到,所以先設置為off - `proxy_set_header` 則是加一些額外的header到請求的封包裡面 :::info 要如何知道windows主機在WSL上面的ip位置?可以在windows下輸入指令`ipconfig`找到裡面`Ethernet adapter vEthernet (WSL)`底下的ip就可以了 ::: 記得要將其他的location block給刪掉或是註解起來,才不會有些路徑被導向別的地方。config檔存好了之後,要重啟nginx才能套用變更,輸入以下指令 ```bash sudo service nginx restart ``` 這時候如果連線到http://localhost,應該會發現沒有辦法順利看到我們的APP,為甚麼呢?雖然我們已經順利把流量導回windows了,但是在windows中,WSL所在的區域網路(172.27.224.1),跟localhost(127.0.0.1)所在的區域網路是不同的,所以沒辦法直接互通,我們需要將這個流量再forword過去。 ### Port forwording 前面提到,不同的區域網路是不能互通的,好在windows有內建portproxy的指令,讓我們可以把流量導過去,首先用**管理員權限**開啟terminal,輸入以下指令 ```ps netsh interface portproxy add v4tov4 listenport=80 listenaddress=172.27.224.1 connectport=8081 connectaddress=127.0.0.1 ``` 整串指令的意思是:「我們要修改網卡介面的設定,增加一個port proxy,從IPv4到IPv4,將所有進來172.27.224.1 port 80的流量都導向127.0.0.1的port 8081」 可以輸入以下指令確認是否有添加成功 ```ps netsh interface portproxy show all ``` 這樣就可以從http://localhost不加port,看到我們的Web App了,但這時候還沒有設定SSL的憑證,所以https是沒辦法連線的 :::info 如果要刪除這個portproxy可以用以下指令 ```bash netsh interface portproxy delete v4tov4 listenport=80 listenaddress=172.27.224.1 ``` ::: :::warning 記得將上面指令中的`172.27.224.1`改為你電腦上的WSL端口IP ::: ### 設定SSL憑證 現在我們nginx server也架好了,SSL憑證也簽完了,萬事俱備只欠東風,最後就只要把這兩個東西串在一起就大功告成,而串在一起也非常的簡單,就只需要在剛剛的config檔裡面,添加兩行設定,讓nginx server知道我們憑證與私鑰的檔案所在 ```nginx server { # ... ssl_certificate /home/{username}/CA/certificate.pem; ssl_certificate_key /home/{username}/CA/privatekey.pem; } ``` 存檔後,一樣再執行一次重啟nginx的指令,就可以連線到https://localhost了,但由於我們的憑證是自己簽署的,所以瀏覽器會把它檔下來,但只要在進階選項中,點繼續前往就可以了 ![](https://i.imgur.com/dNg4u2U.png) <center class="description"> <small> 被擋下來了 </small> </center> <br/> ## 疑難雜症 當然,事情不會一帆風順,一定會有什麼小小的問題藏在細節裡面。當上面一切都設定好了之後,也測試了http跟https都可以看到我們的APP,但...localhost可以連到,為什麼改用固定IP就連不上了呢?!! ### localhost可以連線,IP無法連線 這個問題其實跟前面的[Port Forwording](#Port-forwording)類似,從IP位址連進來的流量跟localhost是在不同的網路介面上,所以無法互通,解法就是如同前述指令,但將WSL的IP改為固定IP,監聽的port要有80(http)與443(https),都導向127.0.0.1的80與443,就可以用IP連線了 ```ps $IP={固定IP} netsh interface portproxy add v4tov4 listenport=80 listenaddress=$IP connectport=80 connectaddress= 127.0.0.1 netsh interface portproxy add v4tov4 listenport=443 listenaddress=$IP connectport=443 connectaddress= 127.0.0.1 ``` ### 用其他電腦沒辦法連進來固定IP 明明ping也ping的到,但為甚麼從瀏覽器連進來的時候卻是一片虛無呢?原因大概是 >防火牆沒開 解法也不難,打開防火牆設定(可以用windows內建的搜尋功能),點左上角的`Inbounce Rules`(不知道中文要翻成啥XD) ![](https://i.imgur.com/J94Qnod.png) 再點右上的新增規則 ![](https://i.imgur.com/28l0JNF.png) Type選擇Port ![](https://i.imgur.com/TyjCVLb.png) 開啟TCP的port 80與443 ![](https://i.imgur.com/MnDRWJn.png) 允許所有連線 ![](https://i.imgur.com/FsivGPe.png) 套用在所有類型的網路 ![](https://i.imgur.com/gRJJpfE.png) 設定一個名稱 ![](https://i.imgur.com/HLbcEoy.png) 最後按確定就可以從外面連線進來啦 ## 結語 如果一路從上面看下來可能會發先有一個地方怪怪的,我們要大費周章地把WSL裡面的流量forword回windows,可是開在WSL上的nginx server卻一啟用就可以從localhost連上,這其實是WSL在設計的時候,windows是可以直接訪問到WSL內的localhost的,詳細的情況我沒有很清楚,但windows上的localhost與WSL內的是同一個,但如果要從WSL連回windows,就必須要用interface的IP去連,所以我們將IP進來的流量導向windows上的localhost,回直接被WSL中的nginx server抓住,但nginx在做proxy的時候,沒辦法指回windows中的localhost,才會需要先導向172.27.224.1在從windows中forword到127.0.0.1。 這麼看來一開始好像應該直接在windows上裝nginx就不會需要這麼麻煩的轉來轉去了,但也是因為這一番操作,對WSL跟一些linux上的指令更熟悉了一點,好像也不是壞事(?)。 其實為了寫這一篇出來,雖然整個網站都可以用了,但還是去查了很多SSL或nginx的細節,比起只是讓它可以work,仔細了解背後的原理,我覺得更能夠加深記憶,才不會每次都是複製貼上了事XDD 儘管如此,Nginx還有許多這次沒用到所以不熟悉的部分,像是load balance,在AWS上只要點幾個鍵就可以設定好的東東,到底背後的細節是甚麼?這要等之後有機會再來研究了(大概率不會