ngrok 不求人:自己搭一個窮人版的 ngrok 服務

在開發聊天機器人的過程中,可以透過 ngrok 這個服務來快速測試我們在開發中的本地程式碼,本篇文章將介紹如何用 nginx + ssh reverse tunnel 來自己架設一個窮人版的偽 ngrok,並且研究一下其中的原理。

ngrok 服務簡介

ngrok

(圖片來源: ngrok 官網)

簡單的來說,ngrok 就是可以讓內網伺服器與外界溝通的一個服務。 一般來說我們本地開發時,會使用開瀏覽器連到 http://localhosthttp://127.0.0.1 的本地端伺服器預覽成果,因為大部分所在的網路環境都是由 NAT 分配虛擬 ip 給你,因此頂多是跟你同樣網段的同事可以連線到你電腦上的 web server 看看有沒有搞砸什麼

但是有些開發的場景就一定需要由外網可以連線到你的本地 server,常見的例如:

  1. Chatbot:聊天機器人通常會設定一個 webhook url,讓服務端(如 Line、Facebook)可以將訊息發佈到你的伺服器上面。
  2. 金流系統:在串接金流的時候,通常都會需要有個 ReturnUrl 回傳網址,在金流付款成功後將結果送到你的伺服器上面。
  3. 其他各式各樣的 api callback
  4. 一定要 Demo 給客戶看但是還沒部署的時候

一般來說我們比較難取得固定的 public ip,當然在家你可以使用光世代申請一組固定的 ip 使用,在公司的話你可以求網管把公司對外唯一組 public ip 的 80 port 或 443 port 設定 port forwording 給你(不過我勸你不要嘗試這麼做,應該會被罵),或者是每次要測試的時候一定要部署到 server 上,但這樣在開發上又沒效率。

於是 ngrok 這個服務就是幫我們解決這個問題。使用 ngrok 的方式大概就是:

  1. 註冊一個 ngrok 帳號
  2. 在你的電腦上安裝 ngrok client,並且啟用token
  3. 在 console 中輸入 ngrok http 3000 來連線到 ngrok proxy server,你就會得到一串網址。
  4. 接著你就可以用上述的網址連線到你的 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 封包的一些方法。