2018年9月16日 星期日

在 Fedora 下裝 id-utils

Fedora 似乎因為執行檔撞名,而沒有提供 id-utils 的套件,但這是使用 gj 的必要套件,只好自己編。從官網抓好 tarball,解開來編譯 (./configure && make)就是了。

但編譯後會遇到錯誤:

./stdio.h:1010:1: error: ‘gets’ undeclared here (not in a function)

這個 stdio.h 是 id-utils 產生的。id-utils 沒用到 gets,所以註解掉那行不必要的檢查即可編成功。

然後將需要用到的執行檔 gid、mkid 複製到自己的 PATH (例如 $HOME/bin),就可以用了。

2018年9月15日 星期六

makefile 雜記

備忘用到 makefile 的小技巧, 持續更新

Tip 1

$ make -n TARGET: dry run, 產生會執行的指令

Tip 2

問題: 用 make -n TARGET 產生指令 "CMD",執行 CMD 卻失敗。

解法: 可能是少了 makefile 定的環境變數,可在makefile 加上

shell:
        bash

然後用 "make shell" 透過 make 進入 bash,這樣就有一樣配置的環境變數,這時再執行 CMD 看看,若可以的話,表示問題出在少了環境變數。

2018年1月28日 星期日

C++ 能否用 memcpy 複製 class / struct 的資料?

答案是: POD (plain old data) type 可以。POD type 可和 C 互通, CPP Reference POD Type 的介紹:

Specifies that the type is POD (Plain Old Data) type. This means the type is compatible with the types used in the C programming language, can be manipulated using C library functions: it can be created with std::malloc, it can be copied with std::memmove, etc, and can be exchanged with C libraries directly, in its binary form.

C++11 開始可用 std::is_pod 判斷

在看 POD 定義的過程發現 Standard layout type 的定義也重要的:

Specifies that a type is standard layout type. Standard layout types are useful for communicating with code written in other programming languages.

符合 standard layout type 才能用 reinterpret_cast 轉型成第一個 member 的 type

不過最穩的最法是直接用 C++11 提供的 std::is_trivially_copyable 判斷, 成立的話就可以安心地用 memcpy。

如何讓 C++ STL, smart pointer 和 forward declaration

有時在 class 內訂義 member 用 std::vector 或 std::unique_ptr, 可以用 forward declaration 宣告 MyObject, 有時不行。原因在於是否有觸發 compiler inline vector / unique_ptr 的 ctor / dtor。

所以避免造成 inline ctor / dtor, 就可以用 forward declaration 宣告 MyObject, 降低程式之間不必要的 dependency, 減少重新編譯時間。

參考資料:

2017年9月3日 星期日

讀出 gcc/g++ 編譯的參數

從這裡看來的,官方文件說 -grecord-gcc-switches 預設有開,所以滿可靠的。

使用例子:

$ g++ f.cpp -std=c++11 -g -o f
$ readelf --debug-dump f | grep DW_AT_producer
       DW_AT_producer    : (indirect string, offset: 0xded): \
  GNU C++11 5.4.0 20160609 -mtune=generic -march=x86-64 -g -std=c++11 -fstack-protector-strong
    DW_AT_producer     DW_FORM_strp

但用 clang 編的資訊就沒存完整:

$ clang++ f.cpp -std=c++11 -g -o f
$ readelf --debug-dump f | grep DW_AT_producer
       DW_AT_producer    : \
  (indirect string, offset: 0x0): clang version 3.8.0-2ubuntu4 (tags/RELEASE_380/final)
    DW_AT_producer     DW_FORM_strp

2017年4月29日 星期六

(C/C++ ) 如何在 Linux 上使用自行編譯的第三方函式庫

以使用 LevelDB 為例。

抓好並編好相關檔案,編譯方式見第三方函式庫附的說明:

$ ls include/  # header files
leveldb/
$ ls out-shared/libleveldb.so*  # shared library
out-shared/libleveldb.so@  out-shared/libleveldb.so.1@  out-shared/libleveldb.so.1.20*

下面的例子用 clang++ 編譯,這裡用到的參數和 g++ 一樣。

問題一:找不到 header

$ clang++ sample.cpp
sample.cpp:5:10: fatal error: 'leveldb/db.h' file not found
#include "leveldb/db.h"
         ^
1 error generated.

解法:用 -I 指定 header 位置

問題二:找不到 shared library

$ clang++ sample.cpp -I include/
/tmp/sample-2e7dd8.o: In function `main':
sample.cpp:(.text+0x1e): undefined reference to `leveldb::Options::Options()'
sample.cpp:(.text+0x6f): undefined reference to `leveldb::DB::Open(leveldb::Options const&, std::string const&, leveldb::DB**)'
sample.cpp:(.text+0x10c): undefined reference to `leveldb::Status::ToString() const'
sample.cpp:(.text+0x7d0): undefined reference to `leveldb::Status::ToString() const'
clang: error: linker command failed with exit code 1 (use -v to see invocation)

要求 linker 連結 libleveldb.so (linker 的參數由 clang++ / g++ 轉傳):

$ clang++ sample.cpp -I include/ -l leveldb
/usr/bin/ld: cannot find -lleveldb
clang: error: linker command failed with exit code 1 (use -v to see invocation)

但 compiler 說找不到要連結的 library

補上 libleveldb 的位置:

$ clang++ sample.cpp -I include/ -l leveldb -L out-shared/

問題三:執行時找不到 shared library

$ ./a.out
./a.out: error while loading shared libraries: libleveldb.so.1: cannot open shared object file: No such file or directory

編出 executable file 或 shared library 表示 static linker 成功,但執行時會用到 dynamic linker 載入函式庫。這錯誤訊息是dynamic linker 回報的。

用 ldd 可以檢查 shared library 的路徑是否正確:

$ ldd a.out | grep leveldb
        libleveldb.so.1 => not found

幾種解法:

1. 用 LD_LIBRARY_PATH 指定位置 (man ld.so 查看細節)

$ LD_LIBRARY_PATH=`pwd`/out-shared ./a.out

若 out-shared 的位置有固定的話,可以在 /.bashrc 加上

export LD_LIBRARY_PATH=/path/to/out-shared 

2. 將 library path 寫到 executable 裡 (man ld.sh 查看細節):

$ clang++ sample.cpp -I include/ -l leveldb -L out-shared/ -Wl,-rpath,`pwd`/out-shared 
$ objdump -p a.out | grep PATH  # 確認有記錄
  RPATH                /home/fcamel/dev/study/leveldb/out-shared
$ ldd a.out  | grep leveldb  # 也可用 ldd 確認
        libleveldb.so.1 => /home/fcamel/dev/study/leveldb/out-shared/libleveldb.so.1 (0x00007fc1f091e000)

這裡我用絕對路徑減少潛在的問題。

3. 搬到系統函式庫

$ ldd a.out  | grep leveldb
        libleveldb.so.1 => not found
$ sudo su
$ cp out-shared/libleveldb.so* /usr/lib
$ ldd a.out  | grep leveldb
        libleveldb.so.1 => /usr/lib/libleveldb.so.1 (0x00007f1717026000)

但這樣和系統內建的混在一起,不好維護。改放到 /usr/local/lib/leveldb/ 下:

$ mkdir /usr/local/lib/leveldb
$ cp --preserve=links out-shared/libleveldb.so* /usr/local/lib/leveldb/
$ echo "/usr/local/lib/leveldb" > /etc/ld.so.conf.d/leveldb.conf
$ ldconfig # Update ldconfig's cache
$ ldd a.out | grep leveldb
        libleveldb.so.1 => /usr/local/lib/leveldb/libleveldb.so.1 (0x00007f0314b32000)

由 man ldconfig 得知 ldconfig 會讀 /etc/ld.so.conf。我在 Ubuntu 14.04 看到的設定如下:

$ cat /etc/ld.so.conf
include /etc/ld.so.conf.d/*.conf

所以在 /etc/ld.so.conf.d/ 建新檔案寫入 /usr/local/lib/leveldb,然後更新 ldconfig cache 即可。

參考資料

  1. The Linux Programming Interface ch41: Fundamentals of Shared Libraries
  2. man ld.so
  3. man ldconfig

2017年3月7日 星期二

除錯技巧:在 Ubuntu 上找出第三方函式庫的程式碼

這篇藉由一個小例子說明如何使用 gdb, locate, apt-file, apt-get 找出問題原因。藉由取得第三方函式庫的程式碼,可以減少瞎猜的時間。

問題

我發現某個程式會不預期的結束。但它不是 crash,沒有 core dump 可看。

用 gdb 找出怎麼結束的

先用 gdb attach 程式,繼續操作。程式結束時 gdb 顯示是呼叫 exit() 結束的。

於是再執行一次,這次用 gdb 在 exit 設中斷點再繼續執行。

取得的 backtrace 如下:

#0  __GI_exit (...) at exit.c:104
#1  0x00007fdd27f60408 in _XDefaultError (...) at ../../src/XlibInt.c:1414
#2  0x00007fdd27f6054b in _XError (...) at ../../src/XlibInt.c:1463
#3  0x00007fdd27f5d5e7 in handle_error ...) at ../../src/xcb_io.c:213
#4  0x00007fdd27f5e687 in _XReply (...) at ../../src/xcb_io.c:699
#5  0x00007fdd27f45346 in XGetWindowProperty (...) at ../../src/GetProp.c:69
#6  0x00007fdd2825db30 in XmuClientWindow () from /usr/lib/x86_64-linux-gnu/libXmu.so.6
...

看 backtrace 沒什麼頭緒,都是第三方函式庫的程式。照 XmuClientWindow() 的說明,它可能會失敗,但它不該直接呼叫 exit()。先找 XmuClientWindow 的原始碼,看看有什麼線索。

用 Ubuntu 的 package 系統找出原始碼

首先用 apt-file 找出 libXmu.so.6 在那個套件 (第一次執行需先跑 apt-file update 更新索引):

$ apt-file search /usr/lib/x86_64-linux-gnu/libXmu.so.6
libxmu6: /usr/lib/x86_64-linux-gnu/libXmu.so.6
libxmu6: /usr/lib/x86_64-linux-gnu/libXmu.so.6.2.0
libxmu6-dbg: /usr/lib/debug/usr/lib/x86_64-linux-gnu/libXmu.so.6.2.0

然後用 apt-get source 取得原始碼:

$ apt-get source libxmu6

在 src/ClientWin.c 裡找到 XmuClientWindow [*1]:

Window
XmuClientWindow(Display *dpy, Window win)
{
    ...
    XGetWindowProperty(dpy, win, WM_STATE, 0, 0, False, AnyPropertyType,
               &type, &format, &nitems, &after, &data);
    ....
}

沒什麼有用資訊,繼續找 XGetWindowProperty 的程式碼。從 backtrace 裡知道它在 ../../src/GetProp.c 裡,但我不知道 GetProp.c 是什麼套件的程式。用 Google 搜尋 GetProp.c 也許會有線索,不過這裡我用另一個作法。

從 header 找出原始碼

src/ClientWin.c 引入的 header 不多,只有這些:

#include <X11/Xlib.h>
#include <X11/Xatom.h>

#include <X11/Xmu/WinUtil.h>

先用 locate 查出完整路徑:

$ locate X11/Xlib.h
/usr/include/X11/Xlib.h

再用 apt-file 找套件名稱:

$ apt-file search /usr/include/X11/Xlib.h
libx11-dev: /usr/include/X11/Xlib.h

取得原始碼:

$ apt-get source libx11-dev

附帶一提,可透過 gcc -E 展開 #include 驗證 XGetWindowProperty 確實出自 X11/Xlib.h:

$ echo "#include " | gcc - -E | grep XGetWindowProperty
extern int XGetWindowProperty(

header 太多的話,直接編譯原始檔比較快:

$ gcc -E src/ClientWin.c > t
得到的資訊如下:
1674 # 1303 "/usr/include/X11/Xlib.h" 3 4
...
3060 extern int XGetWindowProperty(

找到目標後,往回找第一個 header 路徑就是來源了。如果找不到的話,可能是少指定 include header 的來源目錄。可以從 make 產生的結果得知完整的編譯訊息:

$ ./configure
$ make V=1 | grep ClientWin.c
/bin/bash ../libtool  --tag=CC   --mode=compile gcc -std=gnu99 -DHAVE_CONFIG_H \
-I. -I..  -I../include -I../include/X11/Xmu < ... 略 ...> -c -o ClientWin.lo ClientWin.c
...

找出 -I 的部份,和 -E 一樣加在 gcc 的參數上展開 include。

關於如何讓 make 顯示編譯時的參數 (cflags),參見這裡。

繼續追踪程式碼

在 src/GetProp.c 裡發現:

 69     if (!_XReply (dpy, (xReply *) &reply, 0, xFalse)) {

這和前面的 backtrace 一致:

#0  __GI_exit (...) at exit.c:104
#1  0x00007fdd27f60408 in _XDefaultError (...) at ../../src/XlibInt.c:1414
#2  0x00007fdd27f6054b in _XError (...) at ../../src/XlibInt.c:1463
#3  0x00007fdd27f5d5e7 in handle_error ...) at ../../src/xcb_io.c:213
#4  0x00007fdd27f5e687 in _XReply (...) at ../../src/xcb_io.c:699
#5  0x00007fdd27f45346 in XGetWindowProperty (...) at ../../src/GetProp.c:69
#6  0x00007fdd2825db30 in XmuClientWindow () from /usr/lib/x86_64-linux-gnu/libXmu.so.6

表示尋找的方向正確。再繼續看 src/xcb_io.c 和 src/XlibInt.c 內相關函式,得知:

// src/XlibInt.c
1463     rtn_val = (*_XErrorFunction)(dpy, (XErrorEvent *)&event); /* upcall */

對照 backtrace 得知執行 _XErrorFunction 後,是呼叫 _XDefaultError。_XDefaultError 程式如下:

1409 int _XDefaultError(
1410     Display *dpy,
1411     XErrorEvent *event)
1412 {
1413     if (_XPrintDefaultError (dpy, event, stderr) == 0) return 0;
1414     exit(1);
1415     /*NOTREACHED*/
1416 }

確認是這裡呼叫 exit()。所以換掉 _XErrorFunction 可以解決問題。

在程式碼裡搜尋 _XErrorFunction 發現可用 XSetErrorHandler() 設值。這樣至少不會造成程式結束了。

備註

1. 可用 grep 尋找,我是用 gj,找起來更有效率。

2016年6月26日 星期日

NAT64 和 DNS 64

參考資料

IPv4 要用完了 (喊了十幾年, 最近是玩真的了), 在骨幹和 server 都轉成 IPv6 的過渡期, 用戶端也要轉換, 不然 server 是轉心酸的。但是大量 server 還是用 IPv4, 所以要讓 client 能同時支援 IPv4 和 IPv6才行。

如果你是網管, 希望讓用戶可以同時連 IPv4 和 IPv6 的位置, 一個可能的作法是建置「純 IPv6 環境」, 永遠拿到 IPv6 位置, 永遠連 IPv6 的位置。但要讓 IPv6 client 也能連 IPv4 位置, 需要 NAT64+DNS64 幫忙處理。

原理是 DNS64 會同時查 IPv6 和 IPv4 的位置, 有 IPv6 就用 IPv6, 沒有就轉換 IPv4 為 IPv6, prefix 配合 NAT64 的設定, 讓 client 連往該 IP 的封包會先經過 NAT64 router。第二份 slide 有詳細的流程圖。

然後 NAT64 會再轉換 IPv6 的封包成 IPv4 出去, 無縫接上用 IPv4 位置的 server。至於 client 收到 DNS64 回應的正常 IPv6 位置, 就透過一般 router 直接出去, 不用經過 NAT 64。

對設計後端 server 的人來說, 有愈多用戶可以連 IPv6 位置, 有愈高的機會考慮用 IPv6 位置。

對 app 開發者來說, 了解有些用戶的網路環境會用 DNS64+NAT64 的配置, 所以要用 domain name 連上自家 server, 這樣自家 server 用 IPv4 或 IPv6 都會通。

2016年4月16日 星期六

查詢 Web 標準是否有支援

備忘待補更多來源:

如何 git merge 更改檔名的檔案

參考資料:

如果在 branch 裡有 rename file X 為 Y, 並且 master 和 branch 都有改 X ( Y ) 的內容。那麼, merge master 的時候, git 可能會回報 master 的 X 被刪除了, 造成 merge conflict (deleted in ... and modified in ...)。

解法是提高 merge.renameLimit, 比方說 git config merge.renameLimit 999999999。merge 時間會久一點, 但 git 會找出 X 在 branch 裡已被 rename 成 Y。

用 vimdiff 作 git merge

參考文章

以前我用 kdiff3, 後來需要在 terminal merge, 改試 vimdiff, 也很好用。

設定:

$ git config --global merge.tool vimdiff

執行 (在 git merge 有 unsolved conflict 後):

$ git mergetool

UI 顯示:

  • 左上: LOCAL
  • 中上: BASE
  • 右上: REMOTE
  • 下面: 編輯區

vim 指令:

  • [c, ]c: 在 hunk 間移動, 和 vim-gitgutter 一樣
  • 搜 "<<<<" 找 conflict
  • :diffget LOCAL: 目前的 hunk 選用 LOCAL
  • :diffget REMOTE: 目前的 hunk 選用 REMOTE
  • :diffupdate: 更新 diff 畫面

修改後存檔離開, 會自動 resolved conflict。無修改離開會跳過 merge, 維持原本狀態。

其它

查看 merge 狀態:

$ git status

如果整份檔案都要選用 LOCAL, 改用 git 指令:

$ git checkout --ours

全用 REMOTE:

$ git checkout --theirs

2016年1月10日 星期日

2015年11月21日 星期六

multiple thread 存取共享資料的小技巧

最近看到不錯的寫法, 備忘一下。

通常我們不希望 main thread 被 block [*1], 所以會另外開 worker thread 處理耗 CPU 的工作。主要的需求是避免 block main thread, 其次是縮短完成工作的時間。

由於 main thread 和 worker thread 需要互相傳遞一些資料, 有可能造成 race condition, 導致程式行為有時不正確或是掛掉。這裡討論幾種避免 race condition 的作法。

用一個 "global" lock 保護整個 thread 的資料

worker thread 全部函式都用同一個 lock 保護。

  • 優點: 容易實作。
  • 缺點: main thread 在 worker thread 忙的時候存取共享資料, 還是會被 worker thread block。

不同群共享的資料用不同的 lock 保護

和上個作法類似, 不過資料分群用不同 lock 保護。

  • 優點: 減少 main thread 被 block 的機會。
  • 缺點: 實作有些複雜, 可能會漏保護到新加的變數。並且 main thread 仍有可能被 block。

避免共享資料

如果資料只有一個 thread 會用到, 就直接轉交給另一個 thread (改變 ownership)。若兩個 thread 都會用到, 先複製一份再轉交給另一個 thread。沒有共享的資料, 就不需用 lock 保護了 (但仍需透過 message loop 或 queue 傳遞資料到另一個 thread)。

  • 優點: 複製的時間不長, 且可以掌控占據 main thread 的時間, 完全不受 worker thread 影響。
  • 缺點: 需要仔細設計複製和傳遞資料的機製, 確保沒有共享資料。

需要同步資料的時候, 記得只能允許一個方向的 block。比方說 main thread 可以透過 conditional variable 來 block worker thread, 但不允許反過來的操作, 這樣就不會有 dead lock。

不用 lock 的注意事項

每個函式都要放 assert 確保函式在正確的 thread 執行。若一個 class 有兩份資料分別在兩個 thread 下執行的話, 可以考慮訂兩個 struct, 比方說 MainData 和 WorkerData, 然後透過 private method 存取資料, 藉此確保 thread safe:

// C++
class MyClass
{
public:
   ...

private:
  MainData& GetMainData();
  WorkerData& GetWorkerData();

  struct MainData {
    ...
  };
  struct WorkerData {
    ...
  };
  MainData m_mainData;
  WorkerData m_workerData;
};

MainData& MyClass::GetMainData()
{
  assert(InMainThread());
  return m_mainData;
}

WorkerData& MyClass::GetWorkerData()
{
  assert(InWorkerThread());
  return m_workerData;
}

這樣程式寫錯時會造成 assert failed, 可以很快地修正。

備註

*1 比方說 daemon 的 main thread 會不斷收新的連線, GUI 的 main thread 會收使用者的輸入。main thread 被 block 而沒有反應的話, 使用者會覺得軟體有問題。

2015年10月3日 星期六

node.js 開發備忘

前陣子用 node.js 寫了一個上線的服務, 沒有意外的話接下來有一大段時間不會碰 node.js。備忘一下有用到和以後可能會用到的東西。

何時以及為什麼要用 node.js?

node.js 開發

主要弱點

其它

  • coding style: callback 函式的第一個參數總是放 error, 沒有 error 時傳入 null。
  • 用 nodemon 在更新程式後自動更新。
  • logger: 有很多家, 我是用 winston, 還不錯用, 寫入 JSON log, 再用 jq 讀。
  • 用 posix 使用缺少的 POSIX API (如 setrlimit), 還有可以參考它如何包 C/C++ 程式給 node.js 用。
  • web server: 大家都用 express, 要用 HTTPS 或 POST 需要作些設定。看起來彈性很大的 web server。
  • 用舊版 node.js (0.10以前) 記得設 http max socket 提高對每個 domain 送出的連線數量。

2015年10月2日 星期五

2015年9月29日 星期二

觀察 HTTP/HTTPS 傳輸的內容以及防範 man-in-the-middle attack

看到許多強者推薦 mitmproxy (MITM 是 man-in-the-middle 的意思), 試用了一下的確超好用的。它用 man-in-the-middle attack 的方式觀察 (甚至更改) HTTP/HTTPS 傳輸的內容。

How To: Use mitmproxy to read and modify HTTPS traffic 介紹原理和用法, 設定很簡單。

官方文件也很清楚, 可交叉參考:

這裡摘要一下 mitmproxy 作為 Transparent Proxy 的方式和原理:

  1. 備好一台機器 R 作為觀察目標裝置 D (如手機) 的 gateway, 這樣就完成 man-in-the-middle 的設置。
  2. 設定 R 的 firewall, 重導往外連往 port 80 和 443 的連線到在 R 上執行的 mitmproxy。
  3. 在 D 上安裝 mitmproxy 的 certificate, 這樣 D 會相信 mitmproxy 幫自己簽的 certificate。

測試 Android 手機在 MITM 情況下 App 的反應

我是用 ASUS Zenfone 2, 不過 App 的測試結果應該和手機無關。

結果如下:

  • Android OS 有出現訊息說網路可能被監控了, 但清掉訊息後就沒再出現了, 使用者應該不會注意到。
  • 沒裝 mitmproxy certificate 的情況, Google Play 會出現連線錯誤; 裝 certificate 後可以用 Google Play 安裝 app。
  • 沒裝 mitmproxy certificate 的情況, Chrome 會警告有問題; 有裝 certificate 會放行。
  • 沒裝 mitmproxy certificate 的情況, Gmail 會如常運作, 看不到 HTTPS 傳的內容。裝 certificate 的後可以看到 HTTPS 傳的內容。雖然 Gmail 用自己訂的資料格式, 在搜尋信件時, 可看到搜尋字、回傳信件的標題、內文前面幾句的文字。
  • 另外在有裝 mitmproxy certificate 的情況測了 Hangouts, Facebook, Messenger, Skype, Line。它們的主要傳輸內容都不是用 HTTPS, 改天再用 SSLsplit 看看它們傳什麼。
    • Line 會說 Wi-Fi 有問題無法用需要連線的功能 (訊息、電話、桌機 QR Code 登入), 剩下的 App 都可以用。
    • Facebook 有部份內容用 HTTPS 傳, 並且用 JSON 作為內文編碼。
    • Facebook 和 Messenger 有時候會顯示連線問題, 但放一陣子又通了, 也許它們有偵測網路情況用不同方式連線吧。
    • Hangouts 意外地都沒有安全警告, 有沒有裝 mitmproxy certificate 都一樣, 可以正常使用。

其它 Web Proxy

這些也是很有名的 Web Proxy, 有看到朋友推薦。不確定作為 transparent proxy 的效果如何, 至少監控本機的訊息應該滿方便的, 留著備忘。

防範 man-in-the-middle attack

聽 Scott 提到可用 Certificate Pinning 防 MITM attack。原理是只相信自己指定的 certificate (或幫你簽 certificate 的 certificate), 這樣就會忽視攻擊者裝到作業系統的 root CA。當 SSL handshake 的 certificate 被替換的時候, 會變成 handshake error。Certificate Pinning 似乎有些不同的作法, 先記住關鍵字, 之後再來研究。

另一個好像可行但可能有問題的解法是不用 OS 提供的 certificate, 直接用自己包在 app 裡的 certificate (放 server 用的 certificate 或簽它的 CA 的 certificate, 意思一樣)。雖然這樣不用擔心裝置被裝了有問題的 root certificate, 但和 self-signed certificate 一樣, 需要處理另一個問題 (原本的問題): 如何確定 app 內裝了正確的 certificate? 這個難題原本是由 OS 處理, 假設 OS 沒安裝有問題的 root certificate, 所以可以用它的 root certificate。

2015-10-01 更新

經 Victor 告知, 發現 Gmail 應該有檢查 certificate, 我又測了一下, 並更正內文。

Gmail 和 Google Play 有檢查 certificate, 只是使用者從系統刪除信任的 certificate 時, app 沒有同步更新, 仍會相信這個 certificate。另一方面, Chrome 有同步更新。

HTTP library 支援 gzip response body 的情況

為了節省 HTTP response 的傳輸量, HTTP 有提供 request header Accept-Encoding: gzip 要求 server 「可以的話, 用 gzip 壓縮 body 再傳回來」, 還有 response header Content-Encoding: gzip 告知 client 「response body 有用 gzip 壓縮」。

在這樣的規定下, 推測 HTTP library 可能會有兩種實作方式:

  • 不處理。使用者自己加 request header "Accept-Encoding: gzip", 然後自己解 response body
  • 有個 flag 開啟後會自動處理。自動加上 "Accept-Encoding: gzip", 然後自動解 response body 並且移掉 response header "Content-Encoding: gzip"。另外要註明 API 取得的 content length 是否會受 gzip 影響, 結果如何變化。

不過實際情況是:

  • iOS NSURLConnection 文件沒有提到這件事, 但是會自動加 request header 並自動解 response body, 不過沒有移掉 response header "Content-Encoding: gzip" (Ref., 實測結果確實如此, 不需手動加 Accept-Encoding: gzip 就有作用)
  • Android HttpURLConnection 指明預設就會作, 並會清掉 Content-Encoding 和 Content-Length。可以用 Accept-Encoding: identity 覆寫預設值 (Ref.)。
  • curl 開啟 flag CURLOPT_ACCEPT_ENCODING 後會支援, 也是自動加 request header 並自動解 response body, 不過沒有移掉 response header "Content-Encoding: gzip" (Ref.)

結果只有 Android 的實作符合預期。使用前還是要先測過才準啊。

附帶一提, http://httpbin.org/ 會回傳 request/response header, 用來測試 HTTP library 很方便。或是用 mitmproxy 作 transparent proxy 也可以, 相對之下費工一點, 不過看得最清楚。

2015年9月21日 星期一

Android 線上原始碼

偵測網路環境是否有 Captive Portal (使用 Wi-Fi 時會先導到登入網頁)

Captive Portal 是指在可以正常用網路前, 先導到一個登入網頁, 登入後才可以正常上網。通常用在需要付費上網的地方, 或是飯店提供免費但需帳號登入的 Wi-Fi。不過不限於 Wi-Fi 連線

這樣作的好處是使用者可以透過網頁看到比較詳細的說明, 像是「使用者同意書」、「使用限制」、「收費標準」等。對於 Wi-Fi 連線的供應者, 在連上 Wi-Fi 時登入 Wi-Fi 帳號和密碼, 就無法提供這些資訊。

但缺點是使用裝置會誤以為有網路可用 (因為 Wi-Fi 層級有通), 但其實還不能用。如果用的 app 沒有作進一步的偵測, 不會開啟登入網頁, 就無法上網了。使用者會覺得「Wi-Fi 有通 app 卻不能用」, 而覺得是 app 的問題。

Android 4.0.1 開始有內建一套偵測機制 [*1], 發現有 Captive Portal 時會通知使用者:

private boolean isCaptivePortal(InetAddress server) {
    HttpURLConnection urlConnection = null;
    if (!mIsCaptivePortalCheckEnabled) return false;

    mUrl = "http://" + server.getHostAddress() + "/generate_204";
    if (DBG) log("Checking " + mUrl);
    try {
        URL url = new URL(mUrl);
        urlConnection = (HttpURLConnection) url.openConnection();
        urlConnection.setInstanceFollowRedirects(false);
        urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS);
        urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS);
        urlConnection.setUseCaches(false);
        urlConnection.getInputStream();
        // we got a valid response, but not from the real google
        return urlConnection.getResponseCode() != 204;
    } catch (IOException e) {
        if (DBG) log("Probably not a portal: exception " + e);
        return false;
    } finally {
        if (urlConnection != null) {
            urlConnection.disconnect();
        }
    }
}

作法是送 HTTP request 到 http://clients3.google.com/generate_204 [*2], 然後看 HTTP response code 是否為 204 No Content。另一個作法是連 http://www.google.com/blank.html, 然後檢查 Content-Length 是否為 0。

不過當使用者用 HTTPS 上網時, 這招就不管用了, 連線可能出現 certificate error 或是沒反應。原因是 HTTPS 會先加密連線才傳 HTTP, 提供 Wi-Fi 的 AP 無法偵測到有 HTTP 連線, 自然也就無法重導到登入頁面。這正是 HTTPS 要的結果: 使用者送出的連線不該被任意地導到其它地方 (會讓使用者誤以為那是他連往的頁面)。所以....這招應該會在 HTTPS 普及後而愈來愈少人用吧。另一方面也沒有更好的作法可以讓 Wi-Fi 供給者先顯示訊息再讓使用者登入使用。

備註

1. 5.1.1 r6的程式碼有點不同, 變得更一般化, 可自訂測試用的網址還有將 200 + 無資料視為 204, 不過概念一樣。

2. 原始碼是用 http://clients3.google.com/generate_204, 不過用 http://www.google.com/generate_204 也通

參考資料

在 Fedora 下裝 id-utils

Fedora 似乎因為執行檔撞名,而沒有提供 id-utils 的套件 ,但這是使用 gj 的必要套件,只好自己編。從官網抓好 tarball ,解開來編譯 (./configure && make)就是了。 但編譯後會遇到錯誤: ./stdio.h:10...