ngrok 不求人:自己搭一個窮人版的 ngrok 服務
在開發聊天機器人的過程中,可以透過 ngrok 這個服務來快速測試我們在開發中的本地程式碼,本篇文章將介紹如何用 nginx + ssh reverse tunnel 來自己架設一個窮人版的偽 ngrok,並且研究一下其中的原理。
ngrok 服務簡介
(圖片來源: ngrok 官網)
簡單的來說,ngrok 就是可以讓內網伺服器與外界溝通的一個服務。 一般來說我們本地開發時,會使用開瀏覽器連到 http://localhost
或 http://127.0.0.1
的本地端伺服器預覽成果,因為大部分所在的網路環境都是由 NAT 分配虛擬 ip 給你,因此頂多是跟你同樣網段的同事可以連線到你電腦上的 web server 看看有沒有搞砸什麼。
但是有些開發的場景就一定需要由外網可以連線到你的本地 server,常見的例如:
- Chatbot:聊天機器人通常會設定一個
webhook url
,讓服務端(如 Line、Facebook)可以將訊息發佈到你的伺服器上面。 - 金流系統:在串接金流的時候,通常都會需要有個
ReturnUrl
回傳網址,在金流付款成功後將結果送到你的伺服器上面。 - 其他各式各樣的 api callback
一定要 Demo 給客戶看但是還沒部署的時候
一般來說我們比較難取得固定的 public ip,當然在家你可以使用光世代申請一組固定的 ip 使用,在公司的話你可以求網管把公司對外唯一組 public ip 的 80
port 或 443
port 設定 port forwording 給你(不過我勸你不要嘗試這麼做,應該會被罵),或者是每次要測試的時候一定要部署到 server 上,但這樣在開發上又沒效率。
於是 ngrok 這個服務就是幫我們解決這個問題。使用 ngrok 的方式大概就是:
- 註冊一個 ngrok 帳號
- 在你的電腦上安裝 ngrok client,並且啟用token
- 在 console 中輸入
ngrok http 3000
來連線到 ngrok proxy server,你就會得到一串網址。 - 接著你就可以用上述的網址連線到你的 local server
localhost:3000
這串 ngrok 提供的 url 就可以讓我們貼在任何需要輸入 webhook 網址的地方,非常方便!詳細的使用方式可以參考官網的說明。
自己來架一個窮人版 ngrok 服務
免費版的 ngrok 有一個小小小小不方便的地方,就是每次重新連線的時候他都會配給你不一樣的 url,這樣還必須去 webhook 的服務端更改你重新取得的 ngrok url。早期 ngrok 有開放原始碼讓大家可以自行架設服務,但在 1.7 版就不再開源了。
所以說了這麼多終於要進入我們的主題:「自己來架個窮人版 ngrok 服務」
這裡要準備的材料有:
- 租一個 domain name
- 租一個 vps 或任何的雲端主機服務,安裝一個喜歡的 linux 系統
- 並且安裝好 ngnix
等等,不是說窮人版嗎? 其實忘了說,本篇的對象是鎖定「已經有自己的 domain name 和 vps 空間的窮人」(身為一個工程師隨身攜帶一個 vps 不為過吧)…不過既然你都看到這邊了,那就繼續看下去吧!
以下將以 Ubuntu 16.4 作業系統示範
並且已經將 ngrok.yourdomain 設定好 dns 服務指向你的雲端主機
nginx 的設定
在 etc/nginx/sites-available
的目錄下新增一個 ngrok
的伺服器設定檔
server {
server_name ngrok.yourdomain.com;
# 這邊設定你要設定的 url
location / {
proxy_pass http://localhost:3333/;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https; # 如果有設定 https 要加上這行
}
接著切換到 etc/nginx/sites-enable
加上剛剛 server 設定檔的 soft link。最後記得重啟 nginx
$ cd /etc/nginx/sites-enable
$ sudo ln -s ../sites-available/ngrok .
$ service nginx restart
本地端建立 ssh tunnel
接著到自己的電腦上,隨便找一個自己可以動的 rails 專案開啟 server
$ my_app/> rails s
rails server 預設是開啟 port 3000
,我們要跟那遠端的 nginx server 建立 reverse tunnel,將本地的 3000
port 連接到 server 上的 3333
port
$ ssh -R 3333:localhost:3000 your_account@your_server_ip
後面 your_account@your.server.com 就是用來登入你主機的位址和帳號啦!
完成了,你可以試著打開 http://ngrok.yourdomain.com
看看是否可以連線到你的 rails app!
如果要 https 怎辦
因為很多服務的 webhook 都強調必須使用 https
,這時候可以使用 certbot 來讓你的 nginx 使用 Let’s Encrypt 的免費憑證,因為安裝實在太簡單了,大家可以直接參考官網的說明。
nginx revers proxy
接著我們來了解一下上面 nginx 設定檔中寫了些什麼
proxy_pass http://localhost:3333/;
nginx 可以作為靜態 web server 外,本身也是一個強大的 reverse proxy,這行指令就是在指定接收到 web request 後要轉傳到哪個伺服器上面,我們在此設定轉送到 server 自己的 3333 port
。
順道一提 proxy_pass
的指令除了可以讓我們轉送請求到其他 server 外,最常見的就是搭配 upstream
模組設定 load balancer,例如:
upstream webapp {
server yourserver_1.tw;
server yourserver_2.tw;
}
server {
listen 80;
server_name your_domain.com;
location / {
proxy_pass http://webapp; # 指定給 upstream
}
}
接著我們來了解一下 proxy_set_header
設定的一連串指令。主要目的是幫我們把要轉送的 http 封包加上一些 request header。
proxy_set_header X-Real-IP $remote_addr;
# 把真實的 client ip 放到 X-Real-IP
proxy_set_header Host $host;
# 將 Host 指定為 ngrok.yourdomain.com
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 將 X-Forwarded-For 加上這台 nginx,記錄所有轉送的伺服器
proxy_set_header X-Forwarded-Proto https;
# 設定 nginx 與 你的 local server 間使用的 http 協議
你可以試著把以下兩行註解掉:
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Host $host;
這個時候會發現透過我們自己做架設的偽 ngrok 有可能會有些問題。
我們以一個 rails 程式為例。雖然刪掉那兩行 nginx 設定後看起來可以正常連線,但是如果在瀏覽器上發送 post 行為(例如送出表單),你會發現會你的 rails 程式會噴錯:
HTTP Origin header (https://ngrok.yourdomain.com) didn't match request.base_url (http://localhost:3333)
Completed 422 Unprocessable Entity in 1ms (ActiveRecord: 0.0ms)
ActionController::InvalidAuthenticityToken (ActionController::InvalidAuthenticityToken):
造成個錯誤的原因是:
request.base_url
會使用 request header出一個完整的網址 (例如 https://ngrok.yourdomain.com) ,base_url
會變成 nginx 預設要轉送的網址http://localhost:3333
。- http header
Origin
是你在開啟瀏覽器的時候上面那串網址,通常在POST
行為或跨站請求的時候會被放進去 - rails 會去檢查這兩個 request header 是否一樣來判斷是否為合法的 origin。
如果有興趣的話,試著在你的 rails controller 中加上以下的程式碼,印出 rails 收到的 request header 內容:
class YourController < ApplicationController
before_action :puts_request
# ...
def puts_request
puts "Origin: #{request.headers['origin']}"
puts "base_url: #{request.base_url}"
# 這裡可以把 HTTP 開頭的 header 印出來
puts request.headers.env.select{|k, _| k =~ /^HTTP_/}
end
另外也可以研究一下 Rack 如何組成 base_url 和怎麼判斷 scheme
ssh tunnel
nginx 將 https://ngrok.yourdoman.com
的 request 經過 reverse proxy 的設定,轉送到我們遠端 server 上的 http://localhost:3333
,而事實上在 3333 port 並沒有任何一台 web server 在聆聽請求,因此我們在本機上建立了 ssh tunnel 來將請求導向我們本地端電腦的 http://localhost:3000
$ ssh -R [remote_port]:locahost:[local_port] your_accout@yourdomain.com
一般來說我們使用 ssh your_accout@yourdomain.com
來連線到遠端 server 上的 ssh 服務,而 -R
能讓 yourdomain.com
上面的 [remote_port]
反向的導入你本機上的 [local_port]
。所以上面的範例來說,遠端 server 的 3333
port 所接收到的封包,都會導向我們本機開啟的 web server 所聆聽的 3000
port。
那如果我們有個大膽的想法,當在本地電腦瀏覽器連線到 http://localhost:3000
的時候可以看到遠端 server 80 port 的 web server,可以使用 -L
來做正向的 tunnel
$ ssh -L [local_port]:localhost:[remote_port]
此外,在研究 ssh tunnel 的時候發現它 貼近我們生活 的使用場景也不少
- 例如公司的網管鎖東鎖西,讓你沒辦法在上班看股票、滑臉書,可以在家裡弄一個 ssh proxy server 來當跳板跳出去。
- 例如想要在家裡連回公司內網的 server 作業。
簡單來說就是可以當做個跳板,相關的教學網路上蠻多的,這邊就不特別介紹了。
小記
會得知 ngrok 是因為在某次公司的午餐時間與同事卡米哥聊天,得知可以透過它來快速測試本地程式碼,過沒幾天午餐時間得知同事西瓜用了 nginx 自己架了一個 ngrok 心裡覺得非常的神奇,自己試做了一次發現並不會太難,但研究其中的原理也蠻有趣的,除了讓我更瞭解 web 的運作細節,也藉這個機會去看 rack 程式在處理 request 封包的一些方法。