rust web框架rocket指南——請(qǐng)求

請(qǐng)求

總之,為了路由的處理器能被調(diào)用,路由屬性和函數(shù)簽名指定路由必須是正確的。 你已經(jīng)看到了這樣一個(gè)例子:

#[get("/world")]
fn handler() { .. }

這個(gè)路由指定了它僅匹配到/worldGET請(qǐng)求。Rocket在調(diào)用處理器之前會(huì)驗(yàn)證這一點(diǎn)。當(dāng)然,你可以做的不僅僅是指定請(qǐng)求的路徑和方法。除了其它事情,Rocket還可以自動(dòng)地進(jìn)行數(shù)據(jù)驗(yàn)證:

  • 路徑動(dòng)態(tài)參數(shù)的類型。
  • 路由動(dòng)態(tài)參數(shù)的個(gè)數(shù)。
  • 請(qǐng)求體的數(shù)據(jù)類型。
  • 查詢參數(shù)、表單和表單值的類型。
  • 請(qǐng)求預(yù)期發(fā)出和接收的格式。
  • 任何用戶自己定義的安全驗(yàn)證規(guī)則。

路由屬性和函數(shù)簽名共同描述了這些驗(yàn)證規(guī)則。Rocket的代碼生成器實(shí)際擔(dān)任了驗(yàn)證這些數(shù)據(jù)的工作。這一節(jié)主要講怎樣使用Rocket來進(jìn)行這些數(shù)據(jù)驗(yàn)證和其他驗(yàn)證。

方法(Methods)

Rocket路由屬性可以是 get、put、post、delete、patchoptions中的任意一個(gè),或者任意一個(gè)和HTTP方法能匹配上的屬性。例如,下面的屬性匹配的是 根路徑的 POST方法的請(qǐng)求:

#[post("/")]

屬性的語法都的在正式的定義在rocket_codegen API文檔中

HEAD請(qǐng)求

當(dāng)存在一個(gè)GET路由時(shí),Rocket會(huì)自動(dòng)處理對(duì)應(yīng)路由的HEAD請(qǐng)求。如果能夠匹配的上,Rocket將原來的響應(yīng)體過濾掉,作為HEAD路由的響應(yīng)。你也可以為HEAD請(qǐng)求單獨(dú)聲明一個(gè)路由; Rocket并不會(huì)干涉你程序中對(duì)HEAD請(qǐng)求的處理。

重解析

因?yàn)闉g覽器只能發(fā)送GETPOST請(qǐng)求,Rocket在特定條件下會(huì)重解析請(qǐng)求的方法。如果POST請(qǐng)求的header里包含Content-Type:application/x-www-form-urlencoded,并且表單的第一個(gè)字段名為_method,值為HTTP請(qǐng)求的合法方法(例如"PUT"),Rocket將會(huì)以這個(gè)值中的方法,作為這次請(qǐng)求的方法。這會(huì)使Rocket應(yīng)用程序可以提交非POST的表單。例子todo 里面用了這個(gè)特性,通過網(wǎng)頁表單提交PUTDELETE請(qǐng)求。

動(dòng)態(tài)路徑參數(shù)

將變量名用尖括號(hào)括起來,放在路徑中,可設(shè)置動(dòng)態(tài)的路徑參數(shù)。例如,如果我們向任何事說hello!,不僅僅是world,我們可以這樣來定義路由:

#[get("/hello/<name>")]
fn hello(name: &RawStr) -> String {
    format!("Hello, {}!", name.as_str())
}

如果我們把這個(gè)路由掛載到根路徑(.mount("/", routes![hello]),任何一個(gè)以hello開頭且不為空的兩部分的路由都會(huì)分發(fā)的這個(gè)hello路由。例如,如果我們訪問/hello/John,程序會(huì)響應(yīng)Hello, John!。

動(dòng)態(tài)路徑參數(shù)允許任意數(shù)量。路徑參數(shù)也可以是任意類型,包括自定義類型,但是自定義類型需要實(shí)現(xiàn)FromParam特性。Rocket已經(jīng)對(duì)標(biāo)準(zhǔn)庫中的許多類型和幾個(gè)特殊的Rocket類型實(shí)現(xiàn)了FromParam。提供的全部類型列表,請(qǐng)看FromParamAPI docs。下面的完整路由可以說明各種用法:

#[get("/hello/<name>/<age>/<cool>")]
fn hello(name: String, age: u8, cool: bool) -> String {
    if cool {
        format!("You're a cool {} year old, {}!", age, name)
    } else {
        format!("{}, we need to talk about your coolness.", name)
    }
}

原始字符串

你可能在上面例子的代碼里注意到了一個(gè)不熟悉的類型 RawStr 。這是Rocket提供的特殊類型,表示直接從HTTP信息中獲取的不明確的,沒有驗(yàn)證的,沒有解碼的,原始字符串。String,&strCow<str>表示的驗(yàn)證過的字符串,他們的區(qū)別是,&RawStr用來獲取未經(jīng)驗(yàn)證的輸入。它提供了的方法很方便的可以將未驗(yàn)證的字符串轉(zhuǎn)化為驗(yàn)證過的字符串。

&RawStr實(shí)現(xiàn)了FromParam特性,因此在上面的例子中,它可以作為路徑動(dòng)態(tài)參數(shù)的類型。當(dāng)作為路徑動(dòng)態(tài)參數(shù)的類型時(shí),RawStr指向一個(gè)潛在的未解碼的字符串。 相比之下,String可以保證是解碼之后的。使用哪一個(gè),取決于你的目的,如果允許不安全的訪問則使用&RawStr,反之則使用String

匹配規(guī)則

讓我們認(rèn)真看一看上面最后一個(gè)例子的屬性和簽名:

#[get("/hello/<name>/<age>/<cool>")]
fn hello(name: String, age: u8, cool: bool) -> String { ... }

如果cool不是一個(gè)布爾類型呢?如果age不是u8類型呢?如果出現(xiàn)參數(shù)類型匹配不上的情況,Rocket會(huì)將請(qǐng)求轉(zhuǎn)向下一個(gè)匹配的路由(如果存在的話)。Rocket會(huì)一直匹配直到完全匹配或者所有的路由都不能匹配。如果所有的路由都匹配不上,就會(huì)返回一個(gè)可自定義的404 error。

路由會(huì)根據(jù)一個(gè)升序規(guī)則做嘗試匹配。Rocket的默認(rèn)排序?yàn)?4到-1,詳細(xì)規(guī)則會(huì)在下一節(jié)講,所有路由的排序都可以通過rank屬性手動(dòng)設(shè)定。請(qǐng)看下面的例子:

#[get("/user/<id>")]
fn user(id: usize) -> T { ... }

#[get("/user/<id>", rank = 2)]
fn user_int(id: isize) -> T { ... }

#[get("/user/<id>", rank = 3)]
fn user_str(id: &RawStr) -> T { ... }

可以看到在函數(shù)user_intuser_str都設(shè)置了rank參數(shù)。如果我們把這幾個(gè)路由掛載到根路徑下,運(yùn)行程序之后,向/user/<id>的請(qǐng)求,會(huì)按照一下的規(guī)則去匹配:

  1. user函數(shù)的路由會(huì)最先匹配。如果在<id>位置的字符串是一個(gè)無符號(hào)的整形數(shù)字,那么user函數(shù)就會(huì)被調(diào)用。如果不是,則請(qǐng)求就會(huì)被轉(zhuǎn)向下一個(gè)路由:user_int。
  2. user_int的路由會(huì)是第二個(gè)進(jìn)行匹配。如果<id>是有符號(hào)的整形,則user_int函數(shù)被調(diào)用,反之請(qǐng)求會(huì)被轉(zhuǎn)發(fā)到下一個(gè)路由。
  3. user_str的路由最后一個(gè)進(jìn)行匹配。因?yàn)?code><id>肯定是一個(gè)字符串,所以到達(dá)這個(gè)路由的請(qǐng)求都會(huì)被匹配。函數(shù)user_str就會(huì)被調(diào)用。

路徑動(dòng)態(tài)參數(shù)可以為ResultOption類型。例如,如果在user函數(shù)中參數(shù)idResult<usize, &RawStr>,那么所有的請(qǐng)求都會(huì)被user處理,不再轉(zhuǎn)發(fā)到下一個(gè)。Ok狀態(tài)則表示<id>是一個(gè)有效的usize,然而Err狀態(tài)則表示<id>并不是有效的usizeErr的值則會(huì)轉(zhuǎn)換不成usize的那個(gè)原始字符串。

值得注意的是,如果將user_struser_int路由中的rank參數(shù)去掉,Rocket會(huì)在啟動(dòng)程序的時(shí)候發(fā)出一個(gè)error,表示路由沖突,或者是路由匹配了相似的請(qǐng)求。rank參數(shù)就是解決這個(gè)沖突的。

默認(rèn)排序

如果沒有顯示的指定排序,Rocket會(huì)默認(rèn)的分配排序。默認(rèn)情況下,靜態(tài)路由和含有查詢參數(shù)的路由排序較?。▋?yōu)先匹配),動(dòng)態(tài)路徑參數(shù)路由和沒有查詢參數(shù)的路由排序較大(滯后匹配)。下面的表格顯示了各種路由的默認(rèn)排序。

static path query string rank example
yes yes -4 /hello?world=true
yes no -3 /hello
no yes -2 /<hi>?world=true
no no -1 /<hi>

多段參數(shù)

在路由中使用<param..>可以使用多段路徑參數(shù)。這些參數(shù)的類型被叫做多段參數(shù)(segments),必須實(shí)現(xiàn)FromSegments。多段參數(shù)必須放在路徑的最后面:如果在多段參數(shù)后面還有任何文本,則會(huì)產(chǎn)生編譯錯(cuò)誤。

下面的例子中,路由會(huì)匹配所有以/page/開頭的請(qǐng)求:

#[get("/page/<path..>")]
fn get_page(path: PathBuf) -> T { ... }

/page/后面的路徑都會(huì)有效的傳入path參數(shù)。PathBuf實(shí)現(xiàn)了FromSegments防止了多段參數(shù)受到路徑遍歷攻擊。因此,一個(gè)安全、穩(wěn)定的靜態(tài)穩(wěn)定建服務(wù)器只用四行代碼就可以實(shí)現(xiàn):

#[get("/<file..>")]
fn files(file: PathBuf) -> Option<NamedFile> {
    NamedFile::open(Path::new("static/").join(file)).ok()
}

格式

路由可以通過format參數(shù)指定接受的request請(qǐng)求或者返回的response的數(shù)據(jù)格式。參數(shù)的值為指定HTTP媒體類型的一個(gè)字符串。例如,JSON數(shù)據(jù)參數(shù)值為application/json

當(dāng)路由指定的方法為帶有請(qǐng)求體的方法(PUT, POST, DELETE, 和 PATCH),指定format屬性之后,Rockt就會(huì)檢測新來的請(qǐng)求的header中的Content-Type。只有請(qǐng)求的Content-Type和參數(shù)format中的值一直的時(shí)候才能匹配該路由。

請(qǐng)思考下面的例子:

#[post("/user", format = "application/json", data = "<user>")]
fn new_user(user: Json<User>) -> T { ... }

post屬性中format參數(shù)聲明了,新來的請(qǐng)求中,僅僅為Content-Type: application/json才能匹配new_user路由。(參數(shù)data會(huì)在下一節(jié)中講到)。

當(dāng)路由指定的是沒有請(qǐng)求體的方法(GET, HEAD, 和 OPTIONS),指定format參數(shù)之后,Rocket會(huì)檢測新來的請(qǐng)求中header中的Accept。僅僅在header的Accept中指定的希望收到的媒體類型和format參數(shù)指定的一致的請(qǐng)求才會(huì)匹配。

請(qǐng)思考下面的例子:

#[get("/user/<id>", format = "application/json")]
fn user(id: usize) -> Json<User> { ... }

get屬性中的format參數(shù)指明了,在新來的請(qǐng)求中header中的Accept指定的媒體類型為application/json時(shí)才能匹配user。

請(qǐng)求警衛(wèi)

請(qǐng)求警衛(wèi)是Rocket最強(qiáng)大的工具之一。按它名字的意思,請(qǐng)求警衛(wèi)的作用是,根據(jù)請(qǐng)求包含的數(shù)據(jù)防止處理器被錯(cuò)誤的調(diào)用。更確切的說,請(qǐng)求警衛(wèi)是一個(gè)表示任意驗(yàn)證策略的類型。驗(yàn)策略則通過實(shí)現(xiàn)FromRequest特性來時(shí)先。任意一種實(shí)現(xiàn)了FromRequest的類型都是一個(gè)請(qǐng)求警衛(wèi)。

請(qǐng)求警衛(wèi)在向處理器中傳參的時(shí)候起作用。作為路由處理器的參數(shù),請(qǐng)求警衛(wèi)可以設(shè)置任意數(shù)量。在調(diào)用處理器之前,Rocket會(huì)調(diào)用請(qǐng)求警衛(wèi)對(duì)FromRequest 的實(shí)現(xiàn)。只有當(dāng)請(qǐng)求通過所有的警衛(wèi)的時(shí)候,Rocket才會(huì)調(diào)用處理器處理請(qǐng)求。

來看下面的例子,下面虛擬的處理器函數(shù)用了三個(gè)請(qǐng)求警衛(wèi):A,BC。 處理器函數(shù)的參數(shù),不是路徑路徑參數(shù)的情況下才會(huì)被認(rèn)為是請(qǐng)求警衛(wèi)。因此param并不是請(qǐng)求警衛(wèi)。

#[get("/<param>")]
fn index(param: isize, a: A, b: B, c: C) -> ... { ... }

請(qǐng)求警衛(wèi)的執(zhí)行順序是從左到右。上面的例子中執(zhí)行順序是A B C。失敗是短路的;如果一個(gè)警衛(wèi)失敗,剩下的就不會(huì)執(zhí)行。了解更多關(guān)于請(qǐng)求警衛(wèi)的信息以及實(shí)現(xiàn)請(qǐng)求警衛(wèi),請(qǐng)看FromRequest 文檔。

自定義警衛(wèi)

你可以為你自己的類型實(shí)現(xiàn)FromRequest。如下面的例子,你可以創(chuàng)建一個(gè)ApiKey類型,并為其實(shí)現(xiàn)FromRequest, 然后將其作為請(qǐng)求警衛(wèi)。只有在請(qǐng)求頭中存在ApiKey的時(shí)候,路由sensitive才會(huì)運(yùn)行。

#[get("/sensitive")]
fn sensitive(key: ApiKey) -> &'static str { ... }

你也可以為AdminUser類型實(shí)現(xiàn)FromRequest,用來從cookies中認(rèn)證管理員用戶。因此,任何含有AdminUserApikey參數(shù)的處理器,只有在請(qǐng)求符合預(yù)期條件的時(shí)候才會(huì)被調(diào)用。
請(qǐng)求保護(hù)將規(guī)則集中起來,使程序更簡單,更穩(wěn)定,更安全。

警衛(wèi)規(guī)則

請(qǐng)求警衛(wèi)和匹配規(guī)則是強(qiáng)大的校驗(yàn)組合。為了說明,我們考慮一個(gè)簡單的鑒權(quán)功能是怎么實(shí)現(xiàn)的。
我們以兩個(gè)請(qǐng)求警衛(wèi)開始:

  • User:普通的授權(quán)用戶。
    UserFromRequest實(shí)現(xiàn)會(huì)檢測含有用戶認(rèn)證信息的cookie,如果用戶可以被認(rèn)證則返回一個(gè)有效的User,如果認(rèn)證失敗,則轉(zhuǎn)向下一個(gè)路由。

  • AdminUser: 管理員用戶。
    AdminUserFromRequest實(shí)現(xiàn)會(huì)檢測含有管理員認(rèn)證信息的cookie,如果用戶可以被認(rèn)證則返回一個(gè)有效的AdminUser,如果認(rèn)證失敗,則轉(zhuǎn)向下一個(gè)路由。

現(xiàn)在我們將兩個(gè)警衛(wèi)和請(qǐng)求匹配規(guī)則組合起來實(shí)現(xiàn)三個(gè)路由,每個(gè)路由都指向/admin的認(rèn)證控制處面板。

#[get("/admin")]
fn admin_panel(admin: AdminUser) -> &'static str {
    "Hello, administrator. This is the admin panel!"
}

#[get("/admin", rank = 2)]
fn admin_panel_user(user: User) -> &'static str {
    "Sorry, you must be an administrator to access this page."
}

#[get("/admin", rank = 3)]
fn admin_panel_redirect() -> Redirect {
    Redirect::to("/login")
}

上述三條路由定制了認(rèn)證和授權(quán)。 admin_panel的路由僅在管理員登錄時(shí)才會(huì)執(zhí)行。之后才會(huì)顯示管理面板。 如果用戶不是管理員,路由則會(huì)匹配下一個(gè)。接下來會(huì)嘗試順序?yàn)榈诙?code>admin_panel_user路由。 如果任意用戶為登陸狀態(tài),則會(huì)執(zhí)行此路由,并顯示“對(duì)不起,你沒有管理員權(quán)限”。 最后,如果用戶未登錄,則嘗試admin_panel_redirect路由。 由于這個(gè)路由沒有警衛(wèi),所以總會(huì)成功執(zhí)行。用戶重新返回到登錄頁面。

Cookies

Cookies 是一個(gè)重要的內(nèi)建的請(qǐng)求警衛(wèi):你可以獲取,設(shè)置,和刪除cookies。因?yàn)?code>Cookies是一個(gè)請(qǐng)求警衛(wèi),因此Cookies的類型可以作為處理器的參數(shù):

use rocket::http::Cookies;

#[get("/")]
fn index(cookies: Cookies) -> Option<String> {
    cookies.get("message")
        .map(|value| format!("Message: {}", value))
}

因此cookise可以在處理器中使用。上面的例子中,獲取了cookies中的message信息。Cookies警衛(wèi)也可以設(shè)置或者刪除cookies信息。GitHub上的cookies例子說明了更多是用Cookies類型操作cookies的方法,同時(shí)Cookies 文檔包含了所有的使用方法。

加密Cookies

通過Cookies::add() 方法添加cookies是“顯而易見的”,所有的值都能被客戶端看到。對(duì)于敏感數(shù)據(jù),Pocket提供了加密cookies。

加密cookies和常規(guī)的cookies類似,只是經(jīng)過了認(rèn)證模式加密,認(rèn)證模式加密同時(shí)提供了機(jī)密性,完成行,和真實(shí)性。這意味著加密cookies不能被客戶檢查,篡改或制造。 如果您愿意,可以將加密cookies視為簽名和加密。

加密cookies的獲取,添加,和刪除的API和常規(guī)的相同,只是方法末尾多了_private。分別是:get_private,add_private,和 remove_private。使用的例子如下:

/// Retrieve the user's ID, if any.
#[get("/user_id")]
fn user_id(cookies: Cookies) -> Option<String> {
    cookies.get_private("user_id")
        .map(|cookie| format!("User ID: {}", cookie.value()))
}

/// Remove the `user_id` cookie.
#[post("/logout")]
fn logout(mut cookies: Cookies) -> Flash<Redirect> {
    cookies.remove_private(Cookie::named("user_id"));
    Flash::success(Redirect::to("/"), "Successfully logged out.")
}

密匙

Rocket使用256bit的密匙加密cookies,密匙在配置參數(shù)secret_key中指定。如果不指定,Rocket會(huì)自動(dòng)生成一個(gè)新密匙。需要注意的是,加密cookie的解密密匙必須和加密密匙相同才能解密。因此,如果當(dāng)程序重啟之后還要正確解密之前加密的cookie,就必須在配置中指定secret_key。如果在正式環(huán)境中程序啟動(dòng)時(shí)發(fā)現(xiàn)配置中沒有指定secret_key,Rocket會(huì)發(fā)出一個(gè)警告。

通常使用openssl之類的工具來生成合適的secret_keyopenssl生成一個(gè)256bit的base64密匙使用命令openssl rand -base64 32。

關(guān)于配置的更多信息,請(qǐng)看本指南的配置(Configuration) 這一節(jié)。

一次一個(gè)

為了安全起見,目前Rocket要求在同一時(shí)間最多只能有一個(gè)活躍的Cookies實(shí)例。多個(gè)Cookies實(shí)例的情況并不常見,但是一旦遇到,處理器就會(huì)不知所措。

如果真的出現(xiàn),Roocket會(huì)在console里輸出如下信息:

=> Error: Multiple `Cookies` instances are active at once.
=> An instance of `Cookies` must be dropped before another can be retrieved.
=> Warning: The retrieved `Cookies` instance will be empty.

當(dāng)違反這個(gè)規(guī)則調(diào)用處理器時(shí),就會(huì)輸出上述日志。解決這個(gè)問題只能是調(diào)用處理器的時(shí)候,保證統(tǒng)一時(shí)間只能有一個(gè)Cookies。大家共同容易犯的一個(gè)錯(cuò)誤是,同時(shí)使用Cookies警衛(wèi)和Custom警衛(wèi),并且通過Custom警衛(wèi)又獲取了一次Cookies。如下:

#[get("/")]
fn bad(cookies: Cookies, custom: Custom) { .. }

因?yàn)槭紫闰?yàn)證Cookies警衛(wèi),之后在Custom警衛(wèi)里再次獲取Cookies實(shí)例的時(shí)候,已經(jīng)存在一個(gè)Cookies了。
這個(gè)方案可以簡單的通過調(diào)換警衛(wèi)的順序?qū)崿F(xiàn):

#[get("/")]
fn good(custom: Custom, cookies: Cookies) { .. }

請(qǐng)求體

和Rocket大部分時(shí)候一樣,請(qǐng)求體數(shù)據(jù)處理是明確數(shù)據(jù)類型的。 標(biāo)明handler希望的數(shù)據(jù)類型,需要聲明data="<param>", 這里的param就是handler的一個(gè)參數(shù)。
這個(gè)參數(shù)的類型必須實(shí)現(xiàn)了FromData trait。看起來類似下面的例子,T就假定實(shí)現(xiàn)了FromData:

#[post("/", data = "<input>")]
fn new(input: T) { /* .. */ }

任何實(shí)現(xiàn)了 FromData 的類型,同時(shí)也實(shí)現(xiàn)了 類型限制。

表單

在web應(yīng)用程序中,表單是處理的最多的類型。Rocket 處理表單 很容易。假設(shè)應(yīng)用程序需要處理一個(gè)todo Task表單, 表單 包含兩個(gè)字段:完成狀態(tài),一個(gè)復(fù)選框和一個(gè)描述、一個(gè)文字字段。像下面的例子,在Rocket里你可以很容易的處理表單請(qǐng)求:

use rocket::request::Form;

#[derive(FromForm)]
struct Task {
    complete: bool,
    description: String,
}

#[post("/todo", data = "<task>")]
fn new(task: Form<Task>) { /* .. */ }

松散的解析

Rocket 的FromForm 默認(rèn)是嚴(yán)格的解析。Form<T> 只有在form精確的包含T的所有字段才會(huì)解析成功。如果form字段缺少或者多于Form<T>就會(huì)解析失敗。比如,提交的form 含有 "a", "b", "c" 三個(gè)字段 但是 T只含有 "a"和"c"兩個(gè)字段,from就沒辦法解析成Form<T>。

Rocket 可以選擇不使用嚴(yán)格模式解析。使用LenientForm<T>類型實(shí)現(xiàn)。 當(dāng)提交的form 含有T的字段的超集的時(shí)候, LenientForm<T> 就會(huì)自動(dòng)過濾掉多余的字段,解析成功。比如,當(dāng)提交的 form含有“a”,"b","c"三個(gè)字段,并且T只含有“a”,"c"兩個(gè)字段,這個(gè)form能夠成功解析成LenientForm<T>

You can use a LenientForm anywhere you'd use a Form. Its generic parameter is also required to implement FromForm. For instance, we can simply replace Form with LenientForm above to get lenient parsing:
在任何使用Form的地方都可以使用LenientForm。他的泛型參數(shù)一樣需要實(shí)現(xiàn)FromForm。 例如,我們吧上面的例子中的Form簡單的替換成LenientForm就可以使用松散的解析了。

use rocket::request::LenientForm;

#[derive(FromForm)]
struct Task {
    /* .. */
}

#[post("/todo", data = "<task>")]
fn new(task: LenientForm<Task>) { /* .. */ }

Rocket系列 >>

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容