2022年8月25日 星期四

C 開發筆記 - 使用 libwebsockets 製作簡易的 WebSocket Client

已經有用過 Golang 開發小工具,以及其他現成工具如 websocat 等,做 WebSocket Server 的測試。這次回過頭來再仔細看看 libwebsockets 這套,寫一點 C 語言。

程式架構不會太難,比較難的反而是一些小東西,像是跟 ws.ptt.cc 連線時,PTT 會做 Request Header(Origin欄位) 檢查,當作簡單的管控,然而 libwebsockets 則是要設定  LWS_SERVER_OPTION_JUST_USE_RAW_ORIGIN 才能不加料 XD

#include <include/libwebsockets/lws-context-vhost.h>

For backwards-compatibility reasons, by default lws prepends "http://" to the origin you give in the client connection info struct. If you give this flag when you create the context, only the string you give in the client connect info for .origin (if any) will be used directly.

大概在這種地方耗掉半小時以上。另外,還有做 wss 連線時,也須設定 LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT 等等,以及如果想要略過 wss 的 SSL 憑證檢查,有滿多設定項目可以試試:LCCSCF_USE_SSL | LCCSCF_ALLOW_SELFSIGNED | LCCSCF_SKIP_SERVER_CERT_HOSTNAME_CHECK | LCCSCF_ALLOW_INSECURE;

其他則是依照 libwebsockets 的架構,要設計連線時的 callback 處理機制,算是體驗了一下。未來就多一個 C 語言工具可以做 WebSocket Server 連線測試。


常見的錯誤訊息:
  • E: SSL_new failed: error:00000063:lib(0)::reason(99)
  • W: [wsicli|0|WS/h1/default/ws.ptt.cc]: lws_client_ws_upgrade: got bad HTTP response '403'
  • E: lws_ssl_client_bio_create: Unable to get hostname
執行 logs:

% make
gcc -g -Wall main.c `pkg-config libwebsockets --libs --cflags`

% ./a.out   
Usage> ./a.out url
./a.out ws://localhost:8000/
./a.out wss://ws.ptt.cc/bbs app://cmd
./a.out wss://ws.ptt.cc/bbs https://term.ptt.cc

% ./a.out wss://ws.ptt.cc/bbs https://term.ptt.cc
[2022/08/25 20:45:29:8530] N: lws_create_context: LWS: 4.3.2-no_hash, NET CLI SRV H1 H2 WS ConMon IPV6-on
[2022/08/25 20:45:29:8539] N: __lws_lc_tag:  ++ [wsi|0|pipe] (1)
[2022/08/25 20:45:29:8964] N: __lws_lc_tag:  ++ [vh|0|default||-1] (1)
wsCallback - LWS_CALLBACK_OPENSSL_LOAD_EXTRA_CLIENT_VERIFY_CERTS
[INFO] libwebsockets version: 4.3.2
[INFO] lws_parse_uri - Server: ws.ptt.cc:443
[INFO] lws_parse_uri - URLScheme: wss
[INFO] lws_parse_uri - URLPath: bbs
[INFO] clientConnectInfo.addres: ws.ptt.cc
[INFO] clientConnectInfo.port: 443
[INFO] clientConnectInfo.path: /bbs
[INFO] clientConnectInfo.host: ws.ptt.cc
[INFO] clientConnectInfo.origin: https://term.ptt.cc
[INFO] clientConnectInfo.ssl_connection: 109
[2022/08/25 20:45:29:9789] N: __lws_lc_tag:  ++ [wsicli|0|WS/h1/default/ws.ptt.cc] (1)
call lws_client_connect_via_info done, webSocket is NULL(NO)
noop
[2022/08/25 20:45:30:0229] N: lws_gate_accepts: on = 0
[2022/08/25 20:45:30:0575] N: lws_gate_accepts: on = 0
wsCallback - LWS_CALLBACK_CLIENT_ESTABLISHED
wsCallback - LWS_CALLBACK_CLIENT_WRITEABLE
wsCallback - LWS_CALLBACK_CLIENT_RECEIVE
RECEIVER: HTTP/1.1 200 OK

     ??      PTT                ?P   ??        ?P  ??      ???i?i?i?i?i?i??
             140.112.172.11 ?P     ???i???i??            ???i?i?i?i?i
  ?z?w?{     ?????~?{        ???d?i?i???i??   ?P   ???i?i?i?i?i??  ?P
  ?x?V?|?{   ptt.cc            ???i?i?i?i?i???i??    ???i?i?i?i?i
  ?x?V  ?x                   ???i?i?i???i?i?i????  ???i?i?i?i?i??  ??  ?P
?w?}    ?x?z?w?w?{  ?P       ????      ?i?i?i?i?????i?i?i?i    ?P
        ?|?t  ?V?x      ?P           ???i?i?i?i???i?i?i??            ?P
                ?x?z?w?w?w?{         ?i?i?i?i?i?h?h?g?g?f?f?e?e?d?c?b
            ?z?w?r?}?V?V  ?| ???i?i?i?i?i?h?h?g?g?f?f?e?e?d?d?c?c?b
            ?x?V?V           ???i?i
wsCallback - LWS_CALLBACK_CLIENT_RECEIVE
RECEIVER:       ?i?h?h?g?g?f?f?e?e?d?c?b

wsCallback - LWS_CALLBACK_CLIENT_RECEIVE
RECEIVER:            ?f?d?d?e?g?f?e         ?e?d?g??     ???âg?d?e       ????            
               ?g?h  ?h         ?e?d         ?b      ???c?g?d                   
?п?J?N???A?ΥH guest ???[?A?ΥH new ???U:               
^C   ?b?c?e?f?e?g?h?g?f?d    ?n  ??   ??  ???n?l?? ??  ???? ?l?k  ?h            

使用 curl 指定 Server IP 檢驗 SSL 憑證 (SKIP DNS LOOKUP)

原先是想要做 curl -I https://IP 檢驗時,在想怎樣處理憑證檢驗,像是多塞 "Host: test.com" 是不是有用,最後查了一下,要反過來想,可以多增加 --resolve 參數去指定,如此就可以測試指定的機器了

% man curl 
...
       --resolve <[+]host:port:addr[,addr]...>
              Provide a custom address for a specific host and port pair. Using this, you can make the curl requests(s) use a specified
              address and prevent the otherwise normally resolved address to be used. Consider it a sort of /etc/hosts alternative
              provided on the command line. The port number should be the number used for the specific protocol the host will be used for.
              It means you need several entries if you want to provide address for the same host but different ports.

              By specifying '*' as host you can tell curl to resolve any host and specific port pair to the specified address. Wildcard is
              resolved last so any --resolve with a specific host and port will be used first.

              The provided address set by this option will be used even if --ipv4 or --ipv6 is set to make curl use another IP version.

              By prefixing the host with a '+' you can make the entry time out after curl's default timeout (1 minute). Note that this
              will only make sense for long running parallel transfers with a lot of files. In such cases, if this option is used curl
              will try to resolve the host as it normally would once the timeout has expired.

              Support for providing the IP address within [brackets] was added in 7.57.0.

              Support for providing multiple IP addresses per entry was added in 7.59.0.

              Support for resolving with wildcard was added in 7.64.0.

              Support for the '+' prefix was was added in 7.75.0.

              This option can be used many times to add many host names to resolve.

              Example:
               curl --resolve example.com:443:127.0.0.1 https://example.com

              See also --connect-to and --alt-svc.

這用法可以用在 load balancer 或 GeoDNS 後面的機器群測試,也能拿來測試某機器的 SSL 憑證是否設定正常。

2022年8月23日 星期二

Cloudflare 1.1.1.1 FOR FAMILIES 提供阻擋惡意網站與成人網站的 DNS 查詢服務


最近跟朋友聊天,才發現 Cloudflare 除了提供 1.1.1.1 反應速度極快的 DNS 查詢服務外,還有提供阻擋惡意網站、成人網站的 DNS 查詢服務!當發現是惡意網站時,直接給予 0.0.0.0 (查不到) 的回饋,如此使用者在瀏覽網站時,不小心點擊到的惡意連結也不會瀏覽到,對於家裡有長輩、小孩的環境來說,真的很不錯。

大概五六年前在幫公司做 使用者生成內容 (User-generated content) 時,再加上規劃權限機制,打算從大家公開的資料中,整理出熱門的網站/影片連結出來,這時就會踩到有人擺了色情網站等問題,那時則是做了一套 URL Checker/Filter (SPAM) 機制,透過網域黑名單做的。現在,單純用 Cloudflare 提供的 DNS 來查詢也能直接達到類似的效果。

Cloudflare 1.1.1.1 FOR FAMILIES: https://one.one.one.one/family/
  • 1.1.1.1
    • 與 Google 8.8.8.8, 8.8.4.4 同樣效果但標榜更快、更注重隱私
    • 2018.04.01 開始的服務
  • 1.1.1.2, 1.0.0.2
    • 可阻擋惡意網站: Malware Blocking Only
  • 1.1.1.3, 1.0.0.3
    • 可阻擋惡意網站+成人網站: Malware and Adult Content Blocking Together
如此,在疫情後常態在家上課時,小孩拿手機平板連到家裡的 WIFI AP ,這時在 WIFI AP 的 DHCP 設置 DNS = 1.1.1.3 , 1.0.0.3 後,就可以簡單防護到所有連到此 WIFI AP 的設備,可防止小孩誤點、誤連到惡意網站和成人網站內容,畢竟有不少惡意網站/成人網站仍會買流量的,立即能避開一堆奇怪廣告。當然,對長輩來說也有一樣的效果。

圖:TP-Link WIFI AP 之 DHCP 設定 主要 DNS / 次要 DNS

其他:

2022年8月20日 星期六

Go 開發筆記 - 簡易 WebSocket Client 練習,WSS 與 Skip SSL Check

公司的 WebSocket Server 使用 node.js Primus 建構服務,最近正在調整架構,弄隻 WebSocket Client 交叉比對,這時就想到 Golang 可以練一下 XD 預設先以 PTT 的 WebSocket 為例 wss://ws.ptt.cc/bbs 練習,而 PTT WebSocket 連線時,有檢查 Request Header 的 Origin 欄位的,完整的用法請直接使用 https://term.ptt.cc 。

實作上,直接用 Gorilla WebSocket 即可,用他建制 WebSocket server 跟 client。

此外,對於 WebSocket over SSL (wss) 連線瑣事上,若要處理 SKIP SSL Check 這件事,可以這樣處理:

// https://pkg.go.dev/crypto/tls
type zeroSource struct{}
func (zeroSource) Read(b []byte) (n int, err error) {
    for i := range b { 
        b[i] = 0 
    }   
    return len(b), nil 
}

func main() {

// ...

    var dialer ws.Dialer
    if *skipSSLCheck {
        fmt.Println("[IFNO] Skip SSL check")
        // https://github.com/gorilla/websocket/blob/master/client.go
        // https://github.com/gorilla/websocket/blob/master/tls_handshake.go
        dialer = ws.Dialer{
            Subprotocols: []string{},
            ReadBufferSize: 2048,
            WriteBufferSize: 2048,
            TLSClientConfig: &tls.Config{
                Rand: zeroSource{},
                InsecureSkipVerify: true,
            },
        }
    } else {
        dialer = ws.Dialer{
            Subprotocols: []string{},
            ReadBufferSize: 2048,
            WriteBufferSize: 2048,
        }
    }
    fmt.Println("[IFNO] try to connect to: ", connectToURL)

// ...

}

至於跟 node.js Primus WebSocket server 溝通時,會有 PING/PONG 的 heartbeat 檢驗,目前偷懶等收到 server 詢問時才回,此外,實際上可以靠 messageType (Control Messages) 來判斷:

    go func() {
        defer func() {
            wg.Done()
        }()
        for {
            _, p, err := conn.ReadMessage()
            if err == nil {
                cmd := string(p)
                fmt.Println("SERVER> ", cmd)

                    if strings.Index(cmd, "\"primus::ping::") >= 0 {
                        text := fmt.Sprintf("\"primus::ping::%d\"", time.Now().UnixMilli())
                        fmt.Println("CLIENT:AUTO> [",text,"]")
                        conn.WriteMessage(ws.TextMessage, []byte(text))
                    }

            } else {
                fmt.Println("SERVER:ERROR> ", err)
                break
            }
        }
        fmt.Println("[IFNO] server error")
    }()

更多完整的資訊,就直接參考 github.com/changyy/go-ws-client

2022年8月8日 星期一

PHP 開發筆記 - 在 Windows 10 上配置 cmd line 開發環境


這個故事說來話長,我本身不會在 Windows 開發,但為了同事只好配置環境出來,交叉比對運作的問題。就筆記一下,這次要做的事就弄到純 cmd line 開發環境:

安裝:

如此,在 cmd 就可以靠純指令工作,像是 composer install ,而在 cmd line 中有 vim 編輯器,可以快速編輯小東西。