以前說到, Dart 是個年輕的語言,SDK 還不夠成熟,使用中有很多坑。之前解決 了一個使用代理導致空指標的問題,這次又碰上了一個使用 Cookie 產生 FormatException 的問題。
問題描述
dio是Flutter中文網開源的一個強大的Dart Http請求庫,我使用這個庫寫了 一個訪問網站 Login 介面的 Demo。
Dio client = new Dio(options);
client.interceptors.add(CookieManager(CookieJar()));
FormData formData = new FormData.from(
{"id": 123456, "passwd": 456789, "CookieDate": "2"});
var response = await client.post<String>(
"/path/to/login.json",
data: formData,
);
複製程式碼
程式碼很簡單:
- 使用 dio 發起一個 Post 請求,包含使用者名稱和密碼,呼叫 web login 介面;
- 伺服器response 包含 set-cookie 設定登入資訊; 後續訪問需要攜帶 cookie.
- 客戶端使用 CookieManager(CookieJar()) 儲存 cookie資訊。
然而很不幸,簡單的程式碼遇上了如下錯誤:
DioError [DioErrorType.DEFAULT]: FormatException: Invalid character in cookie name, code unit: '91' (at character 5) main[UTMPUSERID]
抓出伺服器響應分析:
set-cookie: main[UTMPUSERID]=guest; path=/; domain=.****.net
set-cookie: main[UTMPKEY]=48990095; path=/; domain=.****.net
set-cookie: main[UTMPNUM]=79117; path=/; domain=.****.net
set-cookie 欄位包含了非法字元"["和"]",導致請求失敗。
問題已經定位,請伺服器兄弟吃頓燒烤,改一下 set-cookie 字串定義,問題解決。 ^_^
但是作為一個有追求的程式設計師,不能不跟蹤一下根本原因。
問題定位
協議中關於 set-cookie 的規定
HTTP 協議中,關於 set-cookie 可以使用的字元有明確規定:
RFC6265 中規定 cookie-name 是一個 token。
RFC2616 中定義了 token 就是 CHAR 排除掉分割符,因此"["和"]"確實是協議規定的非法字元。
貌似燒烤白請了 T_T,但是協議規定和現實有很大的距離。
StackOverFlow 這篇回答解釋了 set-cookie 合法字元的變化歷史,簡單來說,RFC6265協議定義了最新的標準, 新的網路介面都應該符合這個標準。但是有大量的歷史遺留問題,很多網站採用的就是最原始的 Netscape cookie_spec。 因此,從實際角度出發,伺服器端新增介面都要符合RFC6265,而客戶端最好能向前相容歷史標準。
Dart SDK 中的處理
回到Flutter程式碼中,Dio 通過 CookieManager 作為Intercepter 攔截所有 請求和響應,如果響應有set-cookie,就儲存在CookieJar中;發起請求時從CookieJar獲取當前 Coockie。
dio/lib/src/interceptors/cookie_mgr.dart
_saveCookies(Response response) {
......
cookieJar.saveFromResponse(
response.request.uri,
cookies.map((str) => Cookie.fromSetCookieValue(str)).toList(),
);
......
}
複製程式碼
因此,判斷Cookie欄位是否合法,程式碼包含在 Dart SDK => Cookie.fromSetCookieValue 中:
dart-sdk/lib/_http/http_headers.dart
void _validate() {
const separators = const [
"(",
")",
"<",
">",
"@",
",",
";",
":",
"\\",
'"',
"/",
"[",
"]",
"?",
"=",
"{",
"}"
];
for (int i = 0; i < name.length; i++) {
int codeUnit = name.codeUnits[i];
if (codeUnit <= 32 ||
codeUnit >= 127 ||
separators.indexOf(name[i]) >= 0) {
throw new FormatException(
"Invalid character in cookie name, code unit: '$codeUnit'",
name,
i);
}
}
// Per RFC 6265, consider surrounding "" as part of the value, but otherwise
// double quotes are not allowed.
int start = 0;
int end = value.length;
if (2 <= value.length &&
value.codeUnits[start] == 0x22 &&
value.codeUnits[end - 1] == 0x22) {
start++;
end--;
}
for (int i = start; i < end; i++) {
int codeUnit = value.codeUnits[i];
if (!(codeUnit == 0x21 ||
(codeUnit >= 0x23 && codeUnit <= 0x2B) ||
(codeUnit >= 0x2D && codeUnit <= 0x3A) ||
(codeUnit >= 0x3C && codeUnit <= 0x5B) ||
(codeUnit >= 0x5D && codeUnit <= 0x7E))) {
throw new FormatException(
"Invalid character in cookie value, code unit: '$codeUnit'",
value,
i);
}
}
}
複製程式碼
- 從上面程式碼我們可以看出,Dart SDK 嚴格實現了 RFC6265 標準,
- "(",")","<",">","@",",",";",":","\",'"',"/","[","]","?","=","{","}" 都是非法字元。
- 注意,Dart 2.1 之前的版本 cookie name 前後如果有雙引號,也會被判斷為非法字元, 後來提了 patch 才修正。
終端規避方案
由於伺服器程式碼祖傳,無法修改。我們在客戶端作相容,相容的方法就是,不使用 Dart SDK 提供的 Cookie,使用我們自定義的 Cookie。這樣我們可以自定義客戶端的合法字元。
Dio 建立自定義的CookieManager, PrivateCookieManager程式碼在 這個路徑 。
client.interceptors.add(PrivateCookieManager(CookieJar()));
......
class PrivateCookieManager extends CookieManager {
......
cookieJar.saveFromResponse(
response.request.uri,
cookies.map((str) => _Cookie.fromSetCookieValue(str)).toList(),
);
......
}
class _Cookie implements Cookie {
void _validate() {
const separators = const [
"(",
")",
"<",
">",
"@",
",",
";",
":",
"\\",
'"',
"/",
//******* [] is valid in this application ***********
// "[",
// "]",
"?",
"=",
"{",
"}"
];
}
}
複製程式碼
問題總結
跟蹤下來,Dart SDK的處理沒有問題,符合協議要求。只是處理的灰度不夠, 畢竟現在有大量的伺服器應用還是採用的原有定義。Cookie中的_validate是一個私有方法, 如果能暴露出來可以繼承修改,冗餘程式碼量會少很多。