2017年8月19日 星期六

FreeBSD 上安裝 golang & websocket 使用 Let’s Encrypt

FreeBSD 中文教學相對現在很冷門...正體中文幾乎已經沒有人在寫新文件資料了...
golang 請直接上 golang 官網找最新版本對應改一下喔!!
用套件管理工具 pkg 安裝的很可能不是最新版所以直接上官網抓會比較好...

wget https://storage.googleapis.com/golang/go1.8.3.freebsd-amd64.tar.gz
tar zxvf go1.8.3.freebsd-amd64.tar.gz
sudo mv go /usr/local/
mkdir gowork
vi .cshrc
ADD
setenv  GOROOT  /usr/local/go
setenv  GOPATH  $HOME/gowork
set path = (/sbin /bin /usr/sbin /usr/bin /usr/local/sbin /usr/local/bin $HOME/bin $GOROOT/bin)
source .cshrc

go

已經可以用了!!

golang 在各平台上幾乎都能簡易的解壓縮設定環境變數後就可以用了... 同一套 source code 搬過去直接 go build .... 就可以編出一個可執行檔... 某些應用上是蠻好用的


抓取安裝 websocket 程式庫
go get github.com/gorilla/websocket
cd `go list -f '{{.Dir}}' github.com/gorilla/websocket/examples/command`

command 光用這個範例就可以延伸想像做許多有趣 or 邪惡應用... XDD

但為了方便測試會用 echo 這 server client 的範例 code
cd `go list -f '{{.Dir}}' github.com/gorilla/websocket/examples/command` && cd ../echo

但現在應用上會碰到的問題就是 https 下跨接 ws 由 SSL 加密模式接 未加密的任何東西時瀏覽器會擋...當然可以設 header by pass 的規則但還是不完整的做法
所以必須要能使用 wss 也是相同加密模式的 websocket

由G大神找到 wss 也就是加密傳輸的 websocket 範例中都沒有完整的寫到 直接可以 run 起來的問題點
rootPEM
拼湊了幾個後得到

server_wss.go
// Copyright 2015 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// +build ignore

package main

import (
 "flag"
 "html/template"
 "log"
 "net/http"

 "github.com/gorilla/websocket"
)

var addr = flag.String("addr", "localhost:8080", "http service address")

var upgrader = websocket.Upgrader{} // use default options

func echo(w http.ResponseWriter, r *http.Request) {
 c, err := upgrader.Upgrade(w, r, nil)
 if err != nil {
  log.Print("upgrade:", err)
  return
 }
 defer c.Close()
 for {
  mt, message, err := c.ReadMessage()
  if err != nil {
   log.Println("read:", err)
   break
  }
  log.Printf("recv: %s", message)
  err = c.WriteMessage(mt, message)
  if err != nil {
   log.Println("write:", err)
   break
  }
 }
}

func home(w http.ResponseWriter, r *http.Request) {
 homeTemplate.Execute(w, "wss://"+r.Host+"/echo")
}

func main() {
 flag.Parse()
 log.SetFlags(0)
 http.HandleFunc("/echo", echo)
 http.HandleFunc("/", home)
 //log.Fatal(http.ListenAndServe(*addr, nil))
 log.Fatal(http.ListenAndServeTLS(*addr, "cert.pem", "privkey.pem", nil))
}

var homeTemplate = template.Must(template.New("").Parse(`
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script>  
window.addEventListener("load", function(evt) {

    var output = document.getElementById("output");
    var input = document.getElementById("input");
    var ws;

    var print = function(message) {
        var d = document.createElement("div");
        d.innerHTML = message;
        output.appendChild(d);
    };

    document.getElementById("open").onclick = function(evt) {
        if (ws) {
            return false;
        }
        ws = new WebSocket("{{.}}");
        ws.onopen = function(evt) {
            print("OPEN");
        }
        ws.onclose = function(evt) {
            print("CLOSE");
            ws = null;
        }
        ws.onmessage = function(evt) {
            print("RESPONSE: " + evt.data);
        }
        ws.onerror = function(evt) {
            print("ERROR: " + evt.data);
        }
        return false;
    };

    document.getElementById("send").onclick = function(evt) {
        if (!ws) {
            return false;
        }
        print("SEND: " + input.value);
        ws.send(input.value);
        return false;
    };

    document.getElementById("close").onclick = function(evt) {
        if (!ws) {
            return false;
        }
        ws.close();
        return false;
    };

});
</script>
</head>
<body>
<table>
<tr><td valign="top" width="50%">
<p>Click "Open" to create a connection to the server, 
"Send" to send a message to the server and "Close" to close the connection. 
You can change the message and send multiple times.
<p>
<form>
<button id="open">Open</button>
<button id="close">Close</button>
<p><input id="input" type="text" value="Hello world!">
<button id="send">Send</button>
</form>
</td><td valign="top" width="50%">
<div id="output"></div>
</td></tr></table>
</body>
</html>
`))


client_wss.go
// Copyright 2015 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// +build ignore

package main

import (
 "crypto/tls"
 "crypto/x509"
 "flag"
 "log"
 "net/url"
 "os"
 "os/signal"
 "time"
 "io/ioutil"

 "github.com/gorilla/websocket"
)

var addr = flag.String("addr", "localhost:8080", "http service address")

func main() {
 flag.Parse()
 log.SetFlags(0)

 interrupt := make(chan os.Signal, 1)
 signal.Notify(interrupt, os.Interrupt)

 u := url.URL{Scheme: "wss", Host: *addr, Path: "/echo"}
 log.Printf("connecting to %s", u.String())

 //c, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
 rootPEM, err := ioutil.ReadFile("root.pem")
 if err != nil || rootPEM == nil {
  log.Fatal("dial:", err)
 }
 roots := x509.NewCertPool()
 ok := roots.AppendCertsFromPEM(rootPEM)
 if !ok {
  log.Fatal("failed to parse root certificate")
 }
 d := websocket.Dialer{TLSClientConfig: &tls.Config{RootCAs: roots}}
 c, _, err := d.Dial(u.String(), nil)
 if err != nil {
  log.Fatal("dial:", err)
 }
 defer c.Close()

 done := make(chan struct{})

 go func() {
  defer c.Close()
  defer close(done)
  for {
   _, message, err := c.ReadMessage()
   if err != nil {
    log.Println("read:", err)
    return
   }
   log.Printf("recv: %s", message)
  }
 }()

 ticker := time.NewTicker(time.Second)
 defer ticker.Stop()

 for {
  select {
  case t := <-ticker.C:
   err := c.WriteMessage(websocket.TextMessage, []byte(t.String()))
   if err != nil {
    log.Println("write:", err)
    return
   }
  case <-interrupt:
   log.Println("interrupt")
   // To cleanly close a connection, a client should send a close
   // frame and wait for the server to close the connection.
   err := c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
   if err != nil {
    log.Println("write close:", err)
    return
   }
   select {
   case <-done:
   case <-time.After(time.Second):
   }
   c.Close()
   return
  }
 }
}

server_wss 大概比較沒什麼問題 只需要記得改一下 localhost:8080 to 0.0.0.0:8080 這樣外部才連的到 or 只是區網內就改成你 Server 內部的虛擬 IP... 跟要使用的 Port
另外
log.Fatal(http.ListenAndServeTLS(*addr, "cert.pem", "privkey.pem", nil))
cert.pem 跟 privkey.pem 憑證的部分
我是直接

ln -s /etc/dehydrated/certs/cert.pem
ln -s /etc/dehydrated/certs/privkey.pem

此時 go run server_wss.go 需要 root 權限!! 否則憑證無法讀取
PS: 這就看你自己憑證路徑是什麼在哪嘍~ 我是用 dehydrated 來取得 Let’s Encrypt 憑證
其他還有相仿的官方的...路徑不盡相同嘍



client_wss 基本上同上 localhost:8080 記得改成你 Domain Name

主要會碰到的問題回到 rootPEM !!!
這到底要給什麼??

由於我是用 Let’s Encrypt
所以上 https://letsencrypt.org/certificates/ 找 Active
Let’s Encrypt Authority X3 (IdenTrust cross-signed)
將它存成 root.pem

至於用其他家產的憑證就必須自己去找看看對應的來試試看嘍