在實作Web服務時,會需要去讀取客戶端傳送來的HTTP請求標頭中的User-Agent欄位,來判斷客戶端是使用什麼產品(如應用程式)和Web引擎來發送請求,以及客戶端環境的作業系統、CPU架構和所用的裝置等資訊。



Linux Mint上Firefox 60 ESR所發送的HTTP請求中的User-Agent欄位內容如下:

Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0

從以上內容,我們可以知道這個客戶端的作業系統是Ubuntu,瀏覽器是Firefox,Web引擎是Gecko,CPU架構是x86_64(amd64)。我們只要用一個正規表示式,就可以很輕易地用程式抓出這些資訊。

不過,事實上,User-Agent欄位的內容幾乎沒有格式的限制,例如Apple Watch發送出來的User-Agent欄位內容如下:

atc/1.0 watchOS/5.1.3 model/Watch3,4 hwp/t8004 build/16S535 (6; dt:156)

所以要對User-Agent欄位的內容進行完美解析是不可能的事情。還好,在GitHub上有一個uap-core開源專案,整理了用來解析已知(有流通)的User-Agent的正規表示式(都存在regexes.yaml檔案中),以及在各種場景下的測試案例。

然而,uap-core只有整理解析User-Agent中產品、作業系統和裝置的部份,而並沒有CPU架構和Web引擎。後面這兩個未支援的部份,在ua-parser-js這個開源專案是有支援的,而且也是使用正規表示式來解析。

也就是說,我們其實可以利用uap-coreua-parser-js提供的正規表示式,來在Rust程式語言中實現解析User-Agent欄位的功能。

User Agent Parser

「User Agent Parser」是筆者開發的套件,受到uap-coreua-parser-js的啟發,可以用來解析User-Agent欄位,從中獲得客戶端的產品、作業系統、裝置、CPU架構和Web引擎等資訊。

Crates.io

https://crates.io/crates/user-agent-parser

Cargo.toml

user-agent-parser = "*"

使用方法

建立regexes.yaml

以下是一個簡單的regexes.yaml範例:

user_agent_parsers:
  - regex: '(ESPN)[%20| ]+Radio/(\d+)\.(\d+)\.(\d+) CFNetwork'
  - regex: '(Namoroka|Shiretoko|Minefield)/(\d+)\.(\d+)\.(\d+(?:pre|))'
    family_replacement: 'Firefox ($1)'
  - regex: '(Android) Eclair'
    v1_replacement: '2'
    v2_replacement: '1'

os_parsers:
  - regex: 'Win(?:dows)? ?(95|98|3.1|NT|ME|2000|XP|Vista|7|CE)'
    os_replacement: 'Windows'
    os_v1_replacement: '$1'

device_parsers:
  - regex: '\bSmartWatch *\( *([^;]+) *; *([^;]+) *;'
    device_replacement: '$1 $2'
    brand_replacement: '$1'
    model_replacement: '$2'

regexes.yaml為一個Map結構,user_agent_parsers這個鍵值存放著解析產品所需的正規表示式和取代字串的樣本,os_parsers這個鍵值存放著解析作業系統所需的正規表示式和取代字串的樣本,device_parsers這個鍵值存放著解析裝置所需的正規表示式和取代字串的樣本。取代字串的樣本中,如果有以錢字號$來接著數字的部份,就會被這數字對應到的正規表示式群組所匹配到的字串來取代。

如果不需要自行撰寫regexes.yaml的話,可以到uap-core專案中取得標準的regexes.yaml

讀取regexes.yaml

建立好regexes.yaml,可以利用user_agent_parser這個crate所提供的UserAgentParser結構體的from_path或是from_str關聯函數來讀取,並建立出UserAgentParser結構實體。

from_path可以從檔案讀取regexes.yaml的資料,例如:

extern crate user_agent_parser;

use user_agent_parser::UserAgentParser;

let ua_parser = UserAgentParser::from_path("/path/to/regexes.yaml").unwrap();

「from_string」可以從記憶體讀取「servers.json」的資料,建議搭配「include_str」巨集一同使用,例如:

extern crate user_agent_parser;

use user_agent_parser::UserAgentParser;

let ua_parser = UserAgentParser::from_str(include_str!("/path/to/regexes.yaml")).unwrap();
解析

利用UserAgentParser提供的名稱以parse_為開頭的方法,來解析透過參數傳入的User-Agent欄位內容的字串。

例如:

extern crate user_agent_parser;

use user_agent_parser::UserAgentParser;

let ua_parser = UserAgentParser::from_path("/path/to/regexes.yaml").unwrap();

let user_agent = "Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20100101 Firefox/10.0 [FBAN/FBIOS;FBAV/8.0.0.28.18;FBBV/1665515;FBDV/iPhone4,1;FBMD/iPhone;FBSN/iPhone OS;FBSV/7.0.4;FBSS/2; FBCR/Telekom.de;FBID/phone;FBLC/de_DE;FBOP/5]";

let product = ua_parser.parse_product(user_agent);

println!("{:#?}", product);

//    Product {
//        name: Some(
//            "Facebook",
//        ),
//        major: Some(
//            "8",
//        ),
//        minor: Some(
//            "0",
//        ),
//        patch: Some(
//            "0",
//        ),
//    }

let os = ua_parser.parse_os(user_agent);

println!("{:#?}", os);

//    OS {
//        name: Some(
//            "iOS",
//        ),
//        major: None,
//        minor: None,
//        patch: None,
//        patch_minor: None,
//    }

let device = ua_parser.parse_device(user_agent);

println!("{:#?}", device);

//    Device {
//        name: Some(
//            "iPhone",
//        ),
//        brand: Some(
//            "Apple",
//        ),
//        model: Some(
//            "iPhone4,1",
//        ),
//    }

let cpu = ua_parser.parse_cpu(user_agent);

println!("{:#?}", cpu);

//    CPU {
//        architecture: Some(
//            "amd64",
//        ),
//    }

let engine = ua_parser.parse_engine(user_agent);

println!("{:#?}", engine);

//    Engine {
//        name: Some(
//            "Gecko",
//        ),
//        major: Some(
//            "10",
//        ),
//        minor: Some(
//            "0",
//        ),
//        patch: None,
//    }
生命周期

parse_*方法所回傳的資料模型實體,其生命周期相依於User-Agent欄位內容的字串和UserAgentParser結構實體。如果要讓模型沒有生命周期相依性,可以使用其提供的into_owned方法。

extern crate user_agent_parser;

use user_agent_parser::UserAgentParser;

let ua_parser = UserAgentParser::from_path("/path/to/regexes.yaml").unwrap();

let product = ua_parser.parse_product("Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.12) Gecko/20101027 Ubuntu/10.04 (lucid) Firefox/3.6.12").into_owned();

Rocket框架支援

Rocket是Rust的一個Web框架(Web Framework),如果想要讓Rocket能夠解析User-Agent欄位的話,可以考慮與這個User Agent Parser一同使用。首先要啟用User Agent Parser的rocketly特色,Cargo.toml設定檔的寫法如下:

[dependencies.user-agent-parser]
version = "*"
features = ["rocketly"]

接著那些parse_*方法所回傳的資料模型,也就是ProductProductOSDeviceCPUEngine結構體,就可以直接被當作請求守衛來用了!另外,如果不想解析User-Agent欄位的內容,也可以使用UserAgent這個請求守衛。不過要記得將一個UserAgentParser結構實體註冊給Rocket使用。

程式範例如下:

#![feature(proc_macro_hygiene, decl_macro)]

#[macro_use]
extern crate rocket;

extern crate user_agent_parser;

use user_agent_parser::{UserAgentParser, UserAgent, Product, OS, Device, CPU, Engine};

#[get("/")]
fn index(user_agent: UserAgent, product: Product, os: OS, device: Device, cpu: CPU, engine: Engine) -> String {
    format!("{user_agent:#?}\n{product:#?}\n{os:#?}\n{device:#?}\n{cpu:#?}\n{engine:#?}",
            user_agent = user_agent,
            product = product,
            os = os,
            device = device,
            cpu = cpu,
            engine = engine,
    )
}

fn main() {
    rocket::ignite()
        .manage(UserAgentParser::from_path("/path/to/regexes.yaml").unwrap())
        .mount("/", routes![index])
        .launch();
}