Writing a Content Generator(translate)

原文:《The Apache Modules Book-Application Development with Apache》

原則上黄选,可以使用通用網(wǎng)關(guān)接口(CGI)進行任何操作称鳞。但CGI提供了一個很好的解決方案的問題的范圍要小得多伙菜!Apache中的內(nèi)容生成器也是如此。 它是處理請求和構(gòu)建Web應用程序的核心晌端。 實際上页徐,它可以擴展到基礎系統(tǒng)允許網(wǎng)絡服務器做任何事情解取。 內(nèi)容生成器是Apache中最基本的模塊愈案。 所有主要的傳統(tǒng)應用程序通常用作內(nèi)容生成器挺尾。 例如,由Apache代理的CGI站绪,PHP和應用程序服務器是內(nèi)容生成器遭铺。

5.1 HelloWorld模塊

在本章中,我們將開發(fā)一個簡單的內(nèi)容生成器恢准。 習慣的HelloWorld示例演示了模塊編程的基本概念魂挂,包括完整的模塊結(jié)構(gòu)以及處理程序回調(diào)和request_rec的使用。
在本章結(jié)尾馁筐,我們將擴展我們的HelloWorld模塊涂召,以報告請求和響應標頭,環(huán)境變量和任何發(fā)布到服務器的數(shù)據(jù)的完整信息敏沉,我們將配置好寫入內(nèi)容生成器模塊果正,在我們可能會使用CGI腳本或可比較的延期的情況下。

5.1.1 The Module Skeleton

每個Apache模塊通過導出模塊數(shù)據(jù)結(jié)構(gòu)來工作盟迟。 一般來說秋泳,Apache 2.x模塊采用以下形式:

module AP_MODULE_DECLARE_DATA some_module = {
    STANDARD20_MODULE_STUFF,
    some_dir_cfg, /* create per-directory config struct */創(chuàng)建每個目錄
    some_dir_merge, /* merge per-directory config struct */合并每個目錄
    some_svr_cfg, /* create per-host config struct */創(chuàng)建每個主機config struct 
    some_svr_merge, /* merge per-host config struct */ 合并每個主機config struct
    some_cmds, /* configuration directives for this module */此模塊的配置指令
    some_hooks /* register module's hooks/etc. with the core */注冊模塊的掛鉤等。 與核心
};

STANDARD20_MODULE_STUFF 宏擴展以提供版本信息攒菠,確保編譯后的模塊只有在完全二進制兼容時加載到服務器構(gòu)建中迫皱,以及文件名和保留字段。 大部分剩余字段涉及模塊配置; 他們將在第9章中詳細討論辖众。為了我們的HelloWorld模塊的目的卓起,我們只需要hook:

module AP_MODULE_DECLARE_DATA helloworld_module = {
    STANDARD20_MODULE_STUFF,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    helloworld_hooks
};

已經(jīng)聲明了模塊結(jié)構(gòu),現(xiàn)在我們需要實例化鉤子函數(shù)凹炸。 Apache 將在服務器啟動時運行該功能戏阅。 其目的是將模塊的處理功能注冊到服務器核心,以便隨后在適當時調(diào)用我們的模塊的功能还惠。 在 HelloWorld 的情況下饲握,我們只需要注冊一個簡單的內(nèi)容生成器或處理程序,這是我們可以在這里插入的許多功能之一蚕键。

static void helloworld_hooks(apr_pool_t *pool)
{
    ap_hook_handler(helloworld_handler, NULL, NULL, APR_HOOK_MIDDLE);
}

最后救欧,我們需要實現(xiàn)helloworld_handler。 這是一個回調(diào)函數(shù)锣光,Apache將在處理HTTP請求時在適當?shù)臅r候調(diào)用該函數(shù)笆怠。 它可以選擇處理或忽略請求。 如果處理請求誊爹,則該函數(shù)負責向客戶端發(fā)送有效的HTTP響應蹬刷,并確保讀绕白健(或丟棄)來自客戶機的任何數(shù)據(jù)。 這與CGI腳本的責任非常相似办成,或者實際上與整個Web服務器的責任相似泡态。
這里是我們最簡單的處理程序:

static int helloworld_handler(request_rec *r)
{
    if (!r->handler || (strcmp(r->handler, "helloworld") != 0)) {
        return DECLINED;
    }
    if (r->method_number != M_GET) {
        return HTTP_METHOD_NOT_ALLOWED;
    }
    ap_set_content_type(r, "text/html;charset=ascii");
    ap_rputs("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\">\n",
r);
    ap_rputs("<html><head><title>Apache HelloWorld "
"Module</title></head>", r);
    ap_rputs("<body><h1>Hello World!</h1>", r);
    ap_rputs("<p>This is the Apache HelloWorld module!</p>", r);
    ap_rputs("</body></html>", r);
    return OK;
}

這個回調(diào)函數(shù)從幾個基本的理智檢查開始。 首先迂卢,我們檢查r->handler程序來確定請求是否適用于我們某弦。 如果請求不適合我們,我們通過返回DECLINED來忽略它而克。 然后Apache將控制權(quán)傳給下一個處理程序靶壮。
其次,我們只想支持HTTP GET和HEAD方法员萍。 我們檢查這些情況腾降,如果合適,返回一個表示不允許該方法的HTTP錯誤代碼碎绎。 在此返回錯誤代碼將導致Apache將錯誤頁面返回給客戶端螃壤。 請注意,HTTP標準(見附錄C)將HEAD定義為與GET相同筋帖,除了在HEAD中省略的響應體映穗。 這兩種方法都包含在Apache的M_GET中,內(nèi)容生成器函數(shù)應該將它們視為相同幕随。
執(zhí)行這些檢查的順序很重要。 如果我們扭轉(zhuǎn)它們宿接,我們的模塊可能會導致Apache在諸如接受它們的CGI腳本之類的另一個處理程序的POST請求的情況下返回錯誤頁面赘淮。
一旦我們確信該請求是可接受的,并且適用于此處理程序睦霎,我們會生成實際的響應 - 在這種情況下梢卸,這是一個微不足道的HTML頁面。 完成后副女,我們返回OK蛤高,告訴Apache我們已經(jīng)處理了這個請求,并且它不應該調(diào)用任何其他處理程序碑幅。

5.1.2 Return Values

即使這個瑣碎的處理程序也有三個可能的返回值戴陡。 通常,模塊提供的處理程序可以返回

  • OK沟涨,表明處理程序已完全成功處理該請求恤批。 不需要進一步處理。
  • DECLINED裹赴,表示處理程序?qū)φ埱蟛桓信d趣喜庞,并拒絕處理該請求诀浪。 然后Apache會嘗試下一個處理程序。 默認的處理程序延都,它簡單地從本地磁盤返回文件(或錯誤頁面雷猪,如果失敗)晰房,永遠不會返回DECLINED求摇,所以請求總是由一些功能處理。
  • HTTP狀態(tài)代碼嫉你,用于指示錯誤月帝。 處理程序?qū)φ埱筘撠煟珶o法或不愿意完成幽污。

HTTP狀態(tài)代碼會轉(zhuǎn)移Apache內(nèi)的整個處理鏈嚷辅。 正常處理請求被中止,Apache設置內(nèi)部重定向到錯誤文檔距误,該文檔可能是Apache的預定義默認值之一簸搞,也可能是由服務器配置中的ErrorDocument指令指定的文檔或處理程序。請注意准潭,該轉(zhuǎn)移工作 只有當Apache還沒有開始將響應發(fā)送到客戶端時趁俊,這可能是處理錯誤的重要設計考慮因素。 為了確保正確的行為刑然,在編寫任何數(shù)據(jù)(我們的第一個ap_rputs語句)之前寺擂,必須進行任何這樣的轉(zhuǎn)移。在可能的情況下泼掠,最好在請求處理周期中處理錯誤怔软。 這個考慮在第6章進一步討論。

5.1.3 The Handler Field處理程序字段

檢查r->handler程序可能看起來違反直覺择镇,但是這一步通常在所有內(nèi)容生成器中都是必需的挡逼。 Apache將調(diào)用任何模塊注冊的所有內(nèi)容生成器,直到其中一個返回OK或HTTP狀態(tài)代碼腻豌。 因此家坎,每個模塊都需要檢查r->handler程序,它告訴模塊是否應該處理請求吝梅。
這個方案是通過實現(xiàn)Apache的hook(鉤子)而實現(xiàn)的虱疏,它們旨在使任何數(shù)量的函數(shù)(或沒有)在hook上運行。 內(nèi)容生成器在Apache的hook中是獨一無二的憔涉,因為只有一個內(nèi)容生成器函數(shù)必須對每個請求負責订框。 共享實現(xiàn)的其他hook具有不同的語義,我們將在第6章和第10章中看到兜叨。

5.1.4 The Complete Module

把它們放在一起并添加所需的headers穿扳,我們有一個完整的mod_helloworld.c源文件

/* The simplest HelloWorld module */
#include <httpd.h>
#include <http_protocol.h>
#include <http_config.h>
static int helloworld_handler(request_rec *r)
{
    if (!r->handler || strcmp(r->handler, "helloworld")) {
        return DECLINED;
    }
    if (r->method_number != M_GET) {
        return HTTP_METHOD_NOT_ALLOWED;
    }
    ap_set_content_type(r, "text/html;charset=ascii");
    ap_rputs("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\">\n",
r);
    ap_rputs("<html><head><title>Apache HelloWorld "
"Module</title></head>", r);
    ap_rputs("<body><h1>Hello World!</h1>", r);
    ap_rputs("<p>This is the Apache HelloWorld module!</p>", r);
    ap_rputs("</body></html>", r);
    return OK;
}
static void helloworld_hooks(apr_pool_t *pool)
{
    ap_hook_handler(helloworld_handler, NULL, NULL, APR_HOOK_MIDDLE);
}
module AP_MODULE_DECLARE_DATA helloworld_module = {
    STANDARD20_MODULE_STUFF,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    helloworld_hooks
} ;

這就是我們所需要的衩侥! 現(xiàn)在我們可以構(gòu)建模塊并將其插入到Apache中。 我們使用與Apache捆綁在一起的apxs實用程序矛物,用于確保編譯標志和路徑正確:
編譯這個模塊
$ apxs -c mod_helloworld.c
(以root身份登錄)安裝
# apxs -i mod_helloworld.la
然后將其配置為httpd.conf文件中的一個handler程序:

LoadModule helloworld_module modules/mod_helloworld.so
<Location /helloworld>
SetHandler helloworld
</Location>

此代碼導致任何在我們服務器上的/helloworld調(diào)用這個模塊作為其handler程序茫死。
請注意,helloworld_hooks和helloworld_handler函數(shù)都聲明為靜態(tài)履羞。 在Apache模塊中峦萎,這種做法是典型的 - 盡管不是很普遍。 通常忆首,僅導出模塊符號爱榔,并且其他所有內(nèi)容都保持為模塊本身的私有。 因此糙及,將所有函數(shù)聲明為靜態(tài)是一個很好的做法详幽。 當模塊導出其他模塊的服務或API時,可能會出現(xiàn)例外情況浸锨,如第10章所述唇聘。當模塊在多個源文件中實現(xiàn)并且需要某些符號對這些文件是共同的時,就會出現(xiàn)另一種情況柱搜。 在這種情況下應采用命名慣例迟郎,以避免符號空間污染。

5.1.5 Using the request_rec Object

正如我們剛剛看到的聪蘸,我們處理函數(shù)的單個參數(shù)是request_rec對象宪肖。 所有涉及到請求處理的hooks都使用相同的參數(shù)。
request_rec對象是表示HTTP請求的大型數(shù)據(jù)結(jié)構(gòu)健爬,并提供對處理請求所涉及的所有數(shù)據(jù)的訪問匈庭。 這也是許多較低級API調(diào)用的參數(shù)。 例如浑劳,在helloworld_handler中,它作為ap_set_content_type的參數(shù)和作為ap_rputs的I / O描述符的參數(shù)夭拌。
我們來看另一個例子魔熏。 假設我們要從本地文件系統(tǒng)提供文件,而不是固定的HTML頁面鸽扁。 為此蒜绽,我們將使用r-> filename參數(shù)來標識文件。 但是我們也可以使用文件統(tǒng)計信息來優(yōu)化發(fā)送文件的過程桶现。 我們可以發(fā)送文件本身躲雅,而不是使用ap_rwrite讀取文件并發(fā)送其內(nèi)容,從而允許APR利用可用的系統(tǒng)優(yōu)化:

static int helloworld_handler(request_rec *r)
{
    apr_file_t *fd;
    apr_size_t sz;
    apr_status_t rv;
    /* "Is it for us?" checks omitted for brevity */
    /* It's an error if r->filename and finfo haven't been set for us.
    * We could omit this check if we make certain assumptions concerning
    * use of our module, but if 'normal' processing is prevented by
    * some other module, then r->filename might be null, and we don't
    * want to risk a segfault!
    */
    if (r->filename == NULL) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,"Incomplete request_rec!") ;
        return HTTP_INTERNAL_SERVER_ERROR ;
    }
    ap_set_content_type(r, "text/html;charset=ascii");
    /* Now we can usefully set some additional headers from file info
    * (1) Content-Length
    * (2) Last-Modified
    */
    ap_set_content_length(r, r->finfo.size);
    if (r->finfo.mtime) {
        char *datestring = apr_palloc(r->pool, APR_RFC822_DATE_LEN);
        apr_rfc822_date(datestring, r->finfo.mtime);
        apr_table_setn(r->headers_out, "Last-Modified", datestring);
    }
    rv = apr_file_open(&fd, r->filename,
          APR_READ|APR_SHARELOCK|APR_SENDFILE_ENABLED,APR_OS_DEFAULT, r->pool);
    if (rv != APR_SUCCESS) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "can't open %s", r->filename);
        return HTTP_NOT_FOUND ;
    }
    ap_send_fd(fd, r, 0, r->finfo.size, &sz);
    /* file_close here is purely optional. If we omit it, APR will close
    * the file for us when r is destroyed, because apr_file_open
    * registered a close on r->pool.
    */
    apr_file_close(fd);
    return OK;
}

5.2 The Request, the Response, and the Environment

將這個小的轉(zhuǎn)移放在文件系統(tǒng)中骡和,HelloWorld模塊還有什么有用的功能相赁?
那么相寇,模塊可以按照與Apache捆綁在一起的printenv CGI腳本程序的方式來報告一般信息。 Apache模塊中最常用(有用的)三個信息組中有三個本別是是請求頭钮科,響應頭和內(nèi)部環(huán)境變量唤衫。 讓我們更新原來的HelloWorld模塊,在響應頁面打印绵脯。
這些信息集合中的每一個都保存在作為request_rec對象的一部分的APR表中佳励。 我們可以遍歷表,使用apr_table_do和回調(diào)來打印完整的內(nèi)容蛆挫。 我們將使用HTML表來表示這些Apache表赃承。
首先,這是一個回調(diào)悴侵,將表項打印為HTML行瞧剖。 當然,我們需要轉(zhuǎn)義HTML的數(shù)據(jù):

static int printitem(void *rec, const char *key, const char *value)
{
    /* rec is a user data pointer. We'll pass the request_rec in it. */
    request_rec *r = rec;
    ap_rprintf(r, "<tr><th scope=\"row\">%s</th><td>%s</td></tr>\n",
                ap_escape_html(r->pool, key),
                ap_escape_html(r->pool, value));
    /* Zero would stop iterating; any other return value continues */
    return 1;
}

其次畜挨,我們提供一個使用回調(diào)打印整個表的功能:

static void printtable(request_rec *r, apr_table_t *t,
                    const char *caption, const char *keyhead,
                    const char *valhead)
{
    /* Print a table header */
    ap_rprintf(r, "<table<caption>%s</caption><thead>"
                "<tr><th scope=\"col\">%s</th><th scope=\"col\">%s"
                "</th></tr></thead><tbody>", caption, keyhead, valhead);
    /* Print the data: apr_table_do iterates over entries with
    * our callback
    */
    apr_table_do(printitem, r, t, NULL);
    /* Finish the table */
    ap_rputs("</tbody></table>\n", r);
}

現(xiàn)在我們可以在HelloWorld處理程序中包裝這個功能:

static int helloworld_handler(request_rec *r)
{
    if (!r->handler || (strcmp(r->handler, "helloworld") != 0)) {
        return DECLINED ;
    }
    if (r->method_number != M_GET) {
        return HTTP_METHOD_NOT_ALLOWED;
    }
    ap_set_content_type(r, "text/html;charset=ascii");
    ap_rputs("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\">\n"
            "<html><head><title>Apache HelloWorld Module</title></head>"
            "<body><h1>Hello World!</h1>"
            "<p>This is the Apache HelloWorld module!</p>", r);
    /* Print the tables */
    printtable(r, r->headers_in, "Request Headers", "Header", "Value");
    printtable(r, r->headers_out, "Response Headers", "Header", "Value");
    printtable(r, r->subprocess_env, "Environment", "Variable", "Value");
    ap_rputs("</body></html>", r);
    return OK;
}
5.2.1 Module I/O

我們的HelloWorld模塊使用類似stdio的函數(shù)系列生成輸出:ap_rputc筒繁,ap_rputs,ap_rwrite巴元,ap_rvputs毡咏,ap_vrprintf,ap_rprintf和ap_rflush逮刨。 我們也看到了“發(fā)送文件”調(diào)用ap_send_file呕缭。 這個簡單的高級API最初是從較早的Apache版本繼承而來,它仍然適用于許多內(nèi)容生成器修己。 它在http_protocol.h中定義恢总。
由于引入了過濾器鏈,產(chǎn)生輸出的基本機制是基于buckets和brigades睬愤,如第3章和第8章所述片仿。過濾器模塊采用不同的機制來產(chǎn)生輸出,這些機制也可用于或者說有時適用于內(nèi)容處理程序尤辱。
在過濾器中處理或生成輸出有兩種根本不同的方法:

  • 直接操縱bucket(鏟斗)和brigades(旅)
  • 使用另一個類似stdio的API(這是比ap_r * API更好的選擇砂豌,因為向后兼容性不是問題)

我們將在第8章中詳細描述這些機制。現(xiàn)在我們來看一下在內(nèi)容生成器中使用面向過濾器的I / O的基本機制光督。
使用過濾器I / O進行輸出有三個步驟:

  1. 創(chuàng)建一個斗旅阳距。
  2. 使用我們正在寫的數(shù)據(jù)填寫旅。
  3. 將旅轉(zhuǎn)移到堆棧上的第一個輸出過濾器(r-> output_filters)结借。

通過創(chuàng)建一個新的brigade或者重新使用一個brigade筐摘,這些步驟可以根據(jù)需要重復多次。 如果響應大和/或生成速度較慢,我們可能希望將其沿較小的塊中的過濾器鏈傳遞咖熟。 然后圃酵,響應可以通過過濾器傳遞給客戶端,從而為我們提供了一個有效的管道球恤,并避免了緩沖整個響應的開銷辜昵。 過濾器模塊是非常有用的目標。
對于我們的HelloWorld模塊咽斧,我們所需要做的就是創(chuàng)建brigade堪置,然后使用util_filter.h中定義的替代stdio類API替換ap_r *系列調(diào)用:ap_fflush,ap_fwrite张惹,ap_fputs舀锨,ap_fputc,ap_fputstrs和ap_fprintf宛逗。 這些調(diào)用有一個稍微不同的原型:而不是將request_rec作為文件描述符傳遞坎匿,我們必須通過我們正在寫入的目標過濾器和bucket bridage。 我們將在第8章中看到這個方案的例子雷激。

5.2.1.1 Output

這是我們第一個使用面向過濾器輸出的簡單的HelloWorld處理程序替蔬。 這個較低級別的API比簡單的類似stdio的緩沖I / O復雜一點,有時可以實現(xiàn)模塊的優(yōu)化(盡管在這種情況下屎暇,任何差異都可以忽略不計)承桥。 我們還可以通過明確處理輸出錯誤來利用稍微更精細的控制。

static int helloworld_handler(request_rec *r)
{
    static const char *const helloworld =
        "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\">\n"
        "<html><head><title>Apache HelloWorld Module</title></head>"
        "<body><h1>Hello World!</h1>"
        "<p>This is the Apache HelloWorld module!</p>"
        "</body></html>";
    apr_status_t rv;
    apr_bucket_brigade *bb;
    apr_bucket *b;
    if (!r->handler || strcmp(r->handler,"helloworld")) {
        return DECLINED;
    }
    if (r->method_number != M_GET) {
        return HTTP_METHOD_NOT_ALLOWED;
    }
    bb = apr_brigade_create(r->pool, r->connection->bucket_alloc);
    ap_set_content_type(r, "text/html;charset=ascii");
    /* We could instead use the stdio-like filter API calls like
    * ap_fputs(r->filters_out, bb, helloworld);
    * which is basically the same as using ap_rputs and family.
    *
    * Alternatively, we can wrap our output in a bucket, append an
    * EOS, and pass it down the filter chain.
    */
    b = apr_bucket_immortal_create(helloworld, strlen(helloworld),
    bb->bucket_alloc);
    APR_BRIGADE_INSERT_TAIL(bb, b);
    APR_BRIGADE_INSERT_TAIL(bb, apr_bucket_eos_create(bb->bucket_alloc));
    rv = ap_pass_brigade(r->filters_out, bb);
    if (rv != APR_SUCCESS) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, "Output Error");
        return HTTP_INTERNAL_SERVER_ERROR;
    }
    return OK;
}
5.2.1.2 Input

模塊輸入略有不同根悼。 再次凶异,我們擁有一種從Apache 1.x繼承的遺留方法,但現(xiàn)在大多數(shù)開發(fā)人員都將其視為已棄用(盡管該方法仍然支持)挤巡。 在大多數(shù)情況下剩彬,我們更愿意直接在新的代碼中使用輸入過濾器鏈:

  1. 創(chuàng)建一個bucket brigade。
  2. 將數(shù)據(jù)從第一個輸入濾波器(r-> input_filters)拉到brigade中矿卑。
  3. 讀取我們的數(shù)據(jù)buckests中的數(shù)據(jù)喉恋,并使用它。

這兩種輸入法通常在現(xiàn)有模塊中找到母廷,包括Apache 2.x的模塊瀑晒。 我們將依次介紹我們的HelloWorld模塊。 我們將更新模塊以支持POST并計算POSTed的字節(jié)數(shù)(請注意徘意,此操作通常但不總是在Content-Length請求標頭中可用)。 我們不會解碼或顯示實際數(shù)據(jù); 雖然我們可以這樣做轩褐,但是這個任務通常最好由一個輸入過濾器(或者一個庫椎咧,比如libapreq)來處理。 我們在這里使用的功能記錄在http_protocol.h中:

#define BUFLEN 8192
static int check_postdata_old_method(request_rec *r)
{
    char buf[BUFLEN];
    size_t bytes, count = 0;
    /* Decide how to treat input */
    if (ap_setup_client_block(r, REQUEST_CHUNKED_DECHUNK) != OK) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Bad request body!");
        ap_rputs("<p>Bad request body.</p>\n", r);
        return HTTP_BAD_REQUEST;
    }
    if (ap_should_client_block(r)) {
        for (bytes = ap_get_client_block(r, buf, BUFLEN); bytes > 0;
        bytes = ap_get_client_block(r, buf, BUFLEN)) {
        count += bytes;
    }
    ap_rprintf(r, "<p>Got %d bytes of request body data.</p>\n", count);
    } else {
        ap_rputs("<p>No request body.</p>\n", r);
    }
    return OK;
}
static int helloworld_handler(request_rec *r)
{
    if (!r->handler || strcmp(r->handler, "helloworld")) {
        return DECLINED;
    }
    /* We could be just slightly sloppy and drop this altogether,
    * but it's good practice to reject anything that's not explicitly
    * allowed. It cuts off *potential* exploits for someone trying
    * to compromise the server.
    */
    if ((r->method_number != M_GET) && (r->method_number != M_POST)) {
        return HTTP_METHOD_NOT_ALLOWED;
    }
    ap_set_content_type(r, "text/html;charset=ascii");
    ap_rputs("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\">\n"
            "<html><head><title>Apache HelloWorld Module</title></head>"
            "<body><h1>Hello World!</h1>"
            "<p>This is the Apache HelloWorld module!</p>", r);
    /* Print the tables */
    printtable(r, r->headers_in, "Request Headers", "Header", "Value");
    printtable(r, r->headers_out, "Response Headers", "Header", "Value");
    printtable(r, r->subprocess_env, "Environment", "Variable", "Value");
    /* Ignore the return value -– it's too late to bail out now
    * even if there's an error
    */
    check_postdata_old_method(r);
    ap_rputs("</body></html>", r);
    return OK ;
}

最后,最后勤讽,使用使用util_filter.h中記錄的函數(shù)的直接訪問輸入過濾器的首選方法是check_postdata蟋座。
我們創(chuàng)建一個brigade,然后循環(huán)直到EOS脚牍,從輸入過濾器填充brigade向臀。我們將在第8章再次看到這種技術(shù)。

static int check_postdata_new_method(request_rec *r)
{
    apr_status_t status;
    int end = 0;
    apr_size_t bytes, count = 0;
    const char *buf;
    apr_bucket *b;
    apr_bucket_brigade *bb;
    /* Check whether there's any input to read. A client can tell
    * us that fact by using Content-Length or Transfer-Encoding.
    */
    int has_input = 0;
    const char *hdr = apr_table_get(r->headers_in, "Content-Length");
    if (hdr) {
        has_input = 1;
    }
    hdr = apr_table_get(r->headers_in, "Transfer-Encoding");
    if (hdr) {
        if (strcasecmp(hdr, "chunked") == 0) {
            has_input = 1;
        }else {
            ap_rprintf(r, "<p>Unsupported Transfer Encoding: %s</p>",
            ap_escape_html(r->pool, hdr));
            return OK; /* we allow this, but just refuse to handle it */
        }
    }
    if (!has_input) {
        ap_rputs("<p>No request body.</p>\n", r);
        return OK;
    }
    /* OK, we have some input data. Now read and count it. */
    /* Create a brigade to put the data into. */
    bb = apr_brigade_create(r->pool, r->connection->bucket_alloc);
    /* Loop until we get an EOS on the input */
    do {
        /* Read a chunk of input into bb */
        status = ap_get_brigade(r->input_filters, bb, AP_MODE_READBYTES,APR_BLOCK_READ, BUFLEN);
        if ( status == APR_SUCCESS ) {
            /* Loop over the contents of bb */
            for (b = APR_BRIGADE_FIRST(bb);
                b != APR_BRIGADE_SENTINEL(bb);
                b = APR_BUCKET_NEXT(b) ) {
                /* Check for EOS */
                if (APR_BUCKET_IS_EOS(b)) {
                    end = 1;
                    break;
                }
                /* Ignore other metadata */
                else if (APR_BUCKET_IS_METADATA(b)) {
                    continue;
                }
                /* To get the actual length, we need to read the data */
                bytes = BUFLEN;
                status = apr_bucket_read(b, &buf, &bytes,
APR_BLOCK_READ);
                count += bytes;
            }
        }
        /* Discard data we're finished with */
        apr_brigade_cleanup(bb);
    } while (!end && (status == APR_SUCCESS));
    if (status == APR_SUCCESS) {
        ap_rprintf(r, "<p>Got %d bytes of request body data.</p>\n",
count);
        return OK;
    }
    else {
       ap_rputs("<p>Error reading request body.</p>", r);
        return OK; /* Just send the above message and ignore the data */
    }
}
5.2.1.3 I/O Errors

當我們得到I / O錯誤時會發(fā)生什么诸狭?
過濾器(第8章所述)通過返回APR錯誤代碼表示錯誤給我們; 他們也可以設置r->狀態(tài)楣号。 我們的處理程序可以通過檢查ap_pass_brigade和ap_get_brigade的返回值來檢測這樣的事件轧铁,如前面的例子。 正常的行為是停止處理并返回適當?shù)腍TTP錯誤代碼。 此行為會導致Apache向客戶端發(fā)送錯誤文檔(在第6章中討論)鄙早。 我們還應該記錄錯誤消息,從而幫助系統(tǒng)管理員診斷問題宏多。
但是如果錯誤是客戶端連接被終止了怎么辦瑞凑? 這是浪費時間,試圖將錯誤文檔發(fā)送給已經(jīng)消失的客戶端陡叠。 我們可以通過檢查r-> connection->aborted檢測到這種斷開連接玩郊,如本章末尾的默認處理程序所示。

5.2.2 Reading Form Data

我們現(xiàn)在有讀取輸入數(shù)據(jù)的基礎枉阵。 但是译红,只有當我們知道如何處理這些數(shù)據(jù)時,數(shù)據(jù)才有用岭妖。 我們需要在網(wǎng)上處理的最常見的數(shù)據(jù)形式是通過提交HTML表單的Web瀏覽器發(fā)送給我們的數(shù)據(jù)临庇。 此類數(shù)據(jù)遵循通用瀏覽器支持的兩種標準格式之一,并由enctype屬性控制為HTML中的<form>元素:

  • application / x-www-form-urlencoded(通過POST或GET提交的普通Web表單)
  • 多部分/表單數(shù)據(jù)(Netscape的文件上傳表單的多部分格式)

歷史上昵慌,以這些格式中的任何一種解碼形式的數(shù)據(jù)是應用程序的責任假夺。 例如,任何CGI庫或腳本模塊都包含處理此任務的代碼斋攀。 Apache本身不包括此功能作為標準已卷,但由第三方模塊(如mod_form和mod_upload)提供。
分析表單數(shù)據(jù)
標準表單數(shù)據(jù)(application/x-www-form-urlencoded)的格式是一系列的鍵/值對淳蔼,由&號(“&”)分隔侧蘸。 任何字符都可以使用%nn序列進行轉(zhuǎn)義,其中nn是字節(jié)的十六進制表示形式鹉梨,某些字符必須被轉(zhuǎn)義讳癌。 鍵數(shù)據(jù)并不總是唯一的,解析數(shù)據(jù)是復雜的 例如存皂,HTML <select multiple>元素可以提交密鑰的幾個值晌坤。
表示這些數(shù)據(jù)的天然結(jié)構(gòu)是一張行李表逢艘。 這個結(jié)構(gòu)可以在Apache中表示為apr_array_header_t *(數(shù)組)值的apr_hash_t *(哈希表)。 我們可以將輸入數(shù)據(jù)解析成該表示骤菠,如下所示:

/* Parse form data from a string. The input string is NOT preserved. */
static apr_hash_t *parse_form_from_string(request_rec *r, char *args)
{
    apr_hash_t *form;
    apr_array_header_t *values;
    char *pair;
    char *eq;
    const char *delim = "&";
    char *last;
    char **ptr;
    if (args == NULL) {
        return NULL;
    }
    form = apr_hash_make(r->pool);
    /* Split the input on '&' */
    for (pair = apr_strtok(args, delim, &last); pair != NULL;pair = apr_strtok(NULL, delim, &last)) {
        for (eq = pair; *eq; ++eq) {
            if (*eq == '+') {
                *eq = ' ';
            }
        }
        /* split into Key / Value and unescape it */
        eq = strchr(pair, '=');
        if (eq) {
            *eq++ = '\0';
            ap_unescape_url(pair);
            ap_unescape_url(eq);
        }
        else {
            eq = "";
            ap_unescape_url(pair);
        }
        /* Store key/value pair in our form       hash. Given that there
        * may be many values for the same key, we store values
        * in an array (which we'll have to create the first
        * time we encounter the key in question).
        */
        values = apr_hash_get(form, pair, APR_HASH_KEY_STRING);
        if (values == NULL) {
            values = apr_array_make(r->pool, 1, sizeof(const char*));
            apr_hash_set(form, pair, APR_HASH_KEY_STRING, values);
        }
        ptr = apr_array_push(values);
        *ptr = apr_pstrdup(r->pool, eq);
     }
     return form;
}

該方案基于從單個輸入緩沖區(qū)解析整個輸入數(shù)據(jù)它改。 在表單提交的總大小相當小的情況下,正常網(wǎng)絡表單的情況通常是正常的商乎。 我們應該通過限制接受的輸入的大小來限制拒絕服務(DoS)攻擊(由服務器管理員指定的數(shù)據(jù)的最大數(shù)量)央拖。 涉及流解析的替代方法可能適用于較大的形式,特別是那些涉及文件上傳的方法鹉戚,可能涉及到兆字節(jié)或甚至千兆字節(jié)的數(shù)據(jù)鲜戒。 mod_upload3模塊提供了更適合大型上傳的解析器。
我們可以使用我們剛剛定義的函數(shù)來解析GET提交的數(shù)據(jù):

static apr_hash_t* parse_form_from_GET(request_rec *r)
{
    return parse_form_from_string(r, r->args);
}

解析由POST提交的數(shù)據(jù)更多的工作崩瓤,因為我們必須讀取數(shù)據(jù):

/* Get POSTed data. Assume we have already checked that the
* content type is application/x-www-form-urlencoded.
* Assume *form is null on entry.
*/
static int parse_form_from_POST(request_rec *r, apr_hash_t **form)
{
int bytes, eos;
apr_size_t count;
apr_status_t rv;
apr_bucket_brigade *bb;
apr_bucket_brigade *bbin;
char *buf;
apr_bucket *b;
const char *clen = apr_table_get(r->headers_in, "Content-Length");
if (clen != NULL) {
bytes = strtol(clen, NULL, 0);
if (bytes >= MAX_SIZE) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
"Request too big (%d bytes; limit %d)",
bytes, MAX_SIZE);
return HTTP_REQUEST_ENTITY_TOO_LARGE;
}
}
else {
bytes = MAX_SIZE;
}
bb = apr_brigade_create(r->pool, r->connection->bucket_alloc);
bbin = apr_brigade_create(r->pool, r->connection->bucket_alloc);
count = 0;
do {
rv = ap_get_brigade(r->input_filters, bbin, AP_MODE_READBYTES,
APR_BLOCK_READ, bytes);
if (rv != APR_SUCCESS) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
"failed to read form input");
return HTTP_INTERNAL_SERVER_ERROR;
}
for (b = APR_BRIGADE_FIRST(bbin);
b != APR_BRIGADE_SENTINEL(bbin);
b = APR_BUCKET_NEXT(b) ) {
if (APR_BUCKET_IS_EOS(b)) {
eos = 1;
}
if (!APR_BUCKET_IS_METADATA(b)) {
if (b->length != (apr_size_t)(-1)) {
count += b->length;
if (count > MAX_SIZE) {
/* This is more data than we accept, so we're
* going to kill the request. But we have to
* mop it up first.
*/
apr_bucket_delete(b);
}
}
}
if (count <= MAX_SIZE) {
APR_BUCKET_REMOVE(b);
APR_BRIGADE_INSERT_TAIL(bb, b);
}
}
} while (!eos);
/* OK, done with the data. Kill the request if we got too much data. */
if (count > MAX_SIZE) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
"Request too big (%d bytes; limit %s)",
bytes, MAX_SIZE);
return HTTP_REQUEST_ENTITY_TOO_LARGE;
}
/* We've got all the data. Now put it in a buffer and parse it. */
buf = apr_palloc(r->pool, count+1);
rv = apr_brigade_flatten(bb, buf, &count);
if (rv != APR_SUCCESS) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
"Error (flatten) reading form data");
return HTTP_INTERNAL_SERVER_ERROR;
}
buf[count] = '\0';
*form = parse_form_from_string(r, buf);
return OK;
}

在這一點上袍啡,我們?yōu)榇_保容易訪問表單數(shù)據(jù)奠定了基礎,我們可以提供一些訪問器功能却桶。 mod_form執(zhí)行類似的功能境输,但使用我們尚未遇到的技術(shù)來提供更清潔的API,其中處理程序模塊不需要關(guān)注哈希颖系。
以下示例顯示了一個函數(shù)嗅剖,它以逗號分隔的字符串形式返回鍵的所有值,該表達式對Perl(使用CGI.pm)或PHP等腳本環(huán)境的用戶來說很熟悉嘁扼。 其他高級別的訪問者現(xiàn)在也很容易寫信粮。

char *form_value(apr_pool_t *pool, apr_hash_t *form, const char *key)
{
apr_array_header_t *v_arr = apr_hash_get(form, key,
APR_HASH_KEY_STRING);
/* Caveat: this is ambiguous because values may contain commas */
return apr_array_pstrcat(pool, v_arr, ',');
}

結(jié)合這些功能,我們可以更新我們的HelloWorld處理程序來顯示表單數(shù)據(jù)趁啸。 我們假設表單數(shù)據(jù)由ASCII輸入和任何非ASCII字符的替代問號組成:

static int helloworld_handler(request_rec *r)
{
apr_hash_t *formdata = NULL;
int rv = OK;
if (!r->handler || (strcmp(r->handler, "helloworld") != 0)) {
return DECLINED;
}
/* We could be just slightly sloppy and drop this altogether,
* but it's good practice to reject anything that's not explicitly
* allowed. It cuts off *potential* exploits for someone trying
* to compromise the server.
*/
if ((r->method_number != M_GET) && (r->method_number != M_POST)) {
return HTTP_METHOD_NOT_ALLOWED;
}
ap_set_content_type(r, "text/html;charset=ascii");
ap_rputs("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\">\n"
"<html><head><title>Apache HelloWorld Module</title></head>"
"<body><h1>Hello World!</h1>"
"<p>This is the Apache HelloWorld module!</p>", r);
/* Print the tables */
printtable(r, r->headers_in, "Request Headers", "Header", "Value");
printtable(r, r->headers_out, "Response Headers", "Header", "Value");
printtable(r, r->subprocess_env, "Environment", "Variable", "Value");
/* Display the form data */
if (r->method_number == M_GET) {
formdata = parse_form_from_GET(r);
}
else if (r->method_number == M_POST) {
const char* ctype = apr_table_get(r->headers_in, "Content-Type");
if (ctype && (strcasecmp(ctype,
"application/x-www-form-urlencoded")
== 0)) {
rv = parse_form_from_POST(r, &formdata);
}
}
if (rv != OK) {
ap_rputs("<p>Error reading form data!</p>", r);
}
else if (formdata == NULL) {
ap_rputs("<p>No form data found.</p>", r);
}
else {
/* Parsed the form successfully, so we have data to display */
apr_array_header_t *arr;
char *key;
apr_ssize_t klen;
apr_hash_index_t *index;
char *val;
char *p;
ap_rprintf(r, "<h2>Form data supplied by method %s</h2>\n<dl>",
r->method) ;
for (index = apr_hash_first(r->pool, formdata); index != NULL;
index = apr_hash_next(index)) {
apr_hash_this(index, (void**)&key, &klen, (void**)&arr);
ap_rprintf(r, "<dt>%s</dt>\n",ap_escape_html(r->pool, key));
for (val = apr_array_pop(arr); val != NULL;
val = apr_array_pop(arr)) {
for (p = val; *p != '\0'; ++p) {
if (!isascii(*p)) {
*p = '?';
}
}
ap_rprintf(r, "<dd>%s</dd>\n",
ap_escape_html(r->pool, val));
}
}
ap_rputs("</dl>", r) ;
}
ap_rputs("</body></html>", r) ;
return OK ;
}

5.3 The Default Handler

到目前為止强缘,我們在簡單的處理程序中介紹了簡單的變體,并強調(diào)了開發(fā)與正常CGI或PHP腳本相同的內(nèi)容處理程序所需的工具不傅。 總結(jié)本章旅掂,我們將介紹Apache的默認處理程序。 雖然它服務于服務器文件系統(tǒng)中的文件访娶,但這個處理程序與我們早期的功能不同商虐,因為它執(zhí)行了更多的內(nèi)務管理,從而說明了更多的核心API崖疤。 Apache的默認處理程序比前面示例中顯示的處理程序更為先進秘车,您可能希望在第一次閱讀時略過。

static int default_handler(request_rec *r)
{
conn_rec *c = r->connection;
apr_bucket_brigade *bb;
apr_bucket *e;
core_dir_config *d;
int errstatus;
apr_file_t *fd = NULL;
apr_status_t status;
int bld_content_md5;

ap_get_module_config檢索模塊的配置(第9章):

d = (core_dir_config *)ap_get_module_config(r->per_dir_config,
&core_module);

如果我們的系統(tǒng)被配置為這樣做劫哼,我們可以計算一個MD5哈希值叮趴,但只有當沒有一個過濾器會轉(zhuǎn)換內(nèi)容并使我們的哈希無效時。

bld_content_md5 = (d->content_md5 & 1)
&& r->output_filters->frec->ftype != AP_FTYPE_RESOURCE;

因為這是最后的處理程序权烧,如果我們不要求請求眯亦,我們不能返回DECLINED咳蔚。

ap_allow_standard_methods(r, MERGE_ALLOW,
M_GET, M_OPTIONS, M_POST, -1);

下一個檢查執(zhí)行內(nèi)務處理任務。 這不是真的必要搔驼,因為如果在銷毀請求時未使用的輸入仍然存在,Apache將為我們執(zhí)行這些任務侈询。

/* If filters intend to consume the request body, they must
* register an InputFilter to slurp the contents of the POST
* data from the POST input stream. It no longer exists when
* the output filters are invoked by the default handler.
*/
if ((errstatus = ap_discard_request_body(r)) != OK) {
return errstatus;
}
if (r->method_number == M_GET || r->method_number == M_POST) {
if (r->finfo.filetype == 0) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
"File does not exist: %s", r->filename);
return HTTP_NOT_FOUND;
}

此處理程序僅提供普通文件; Apache以不同的方式處理目錄舌涨。 如果一個目錄的請求到達這個處理程序,那是一個配置錯誤扔字。

/* Don't try to serve a directory. Some OSs do weird things
* with raw I/O on a directory.
*/
if (r->finfo.filetype == APR_DIR) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
"Attempt to serve directory: %s", r->filename);
return HTTP_NOT_FOUND;
}

處理請求URI結(jié)尾處的任何額外的垃圾囊嘉。

if ((r->used_path_info != AP_REQ_ACCEPT_PATH_INFO) &&
r->path_info && *r->path_info)
{
/* default to reject */
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
"File does not exist: %s",
apr_pstrcat(r->pool, r->filename,
r->path_info, NULL));
return HTTP_NOT_FOUND;
}
/* We understood the (non-GET) method, but it might not be
legal for this particular resource. Check whether the
'deliver_script' flag is set. If so, then go ahead
and deliver the file because
it isn't really content (only GET normally returns content).
Note: The only possible non-GET method
at this point is POST. In the future, we should enable
script delivery for all methods. */
if (r->method_number != M_GET) {
core_request_config *req_cfg;
req_cfg = ap_get_module_config(r->request_config,
&core_module);
if (!req_cfg->deliver_script) {
/* The flag hasn't been set for this request. Punt. */
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
"This resource does not accept the %s method.",
r->method);
return HTTP_METHOD_NOT_ALLOWED;
}
}
if ((status = apr_file_open(&fd, r->filename,APR_READ|APR_BINARY
#if APR_HAS_SENDFILE
| ((d->enable_sendfile == ENABLE_SENDFILE_OFF)
? 0 : APR_SENDFILE_ENABLED)
#endif
, 0, r->pool)) != APR_SUCCESS) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, status, r,
"file permissions deny server access: %s", r->filename);
return HTTP_FORBIDDEN;
}

現(xiàn)在我們再設置一些標準頭文件:

ap_update_mtime(r, r->finfo.mtime);
ap_set_last_modified(r);
ap_set_etag(r);
apr_table_setn(r->headers_out, "Accept-Ranges", "bytes");
ap_set_content_length(r, r->finfo.size);
bb = apr_brigade_create(r->pool, c->bucket_alloc);

ap_meets_conditions執(zhí)行一些有用的檢查,將文件信息交叉引用到請求標頭革为,以確定我們是否真的需要發(fā)送文件或僅確認客戶端緩存副本的有效性扭粱。 在特殊情況下,可能會確定我們的文件對客戶端是無用的震檩,應該被丟棄琢蛤。

if ((errstatus = ap_meets_conditions(r)) != OK) {
apr_file_close(fd);
r->status = errstatus;
}
else {
if (bld_content_md5) {
apr_table_setn(r->headers_out, "Content-MD5",
ap_md5digest(r->pool, fd));
}
/* For platforms where the size of the file may be larger
* than can be stored in a single bucket (where the
* length field is an apr_size_t), split it into several
* buckets */
if (sizeof(apr_off_t) > sizeof(apr_size_t)
&& r->finfo.size > AP_MAX_SENDFILE) {
apr_off_t fsize = r->finfo.size;
e = apr_bucket_file_create(fd, 0, AP_MAX_SENDFILE,
r->pool, c->bucket_alloc);
while (fsize > AP_MAX_SENDFILE) {
apr_bucket *ce;
apr_bucket_copy(e, &ce);
APR_BRIGADE_INSERT_TAIL(bb, ce);
e->start += AP_MAX_SENDFILE;
fsize -= AP_MAX_SENDFILE;
}
e->length = (apr_size_t)fsize;
/* Resize just the last bucket */
}
else {
e = apr_bucket_file_create(fd, 0,
(apr_size_t)r->finfo.size,
r->pool, c->bucket_alloc);
}
#if APR_HAS_MMAP
if (d->enable_mmap == ENABLE_MMAP_OFF) {
(void)apr_bucket_file_enable_mmap(e, 0);
}
#endif
APR_BRIGADE_INSERT_TAIL(bb, e);
}
e = apr_bucket_eos_create(c->bucket_alloc);
APR_BRIGADE_INSERT_TAIL(bb, e);
status = ap_pass_brigade(r->output_filters, bb);
if (status == APR_SUCCESS
|| r->status != HTTP_OK
|| c->aborted) {
return OK;
}
else {
/* No way to know what type of error occurred */
ap_log_rerror(APLOG_MARK, APLOG_DEBUG, status, r,
"default_handler: ap_pass_brigade returned %i",
status);
return HTTP_INTERNAL_SERVER_ERROR;
}
}
else { /* unusual method (not GET or POST) */
if (r->method_number == M_INVALID) {
ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
"Invalid method in request %s", r->the_request);
return HTTP_NOT_IMPLEMENTED;
}

另一個API調(diào)用支持OPTIONS方法:

if (r->method_number == M_OPTIONS) {
return ap_send_http_options(r);
}
return HTTP_METHOD_NOT_ALLOWED;
}
}

5.4 Summary

本章討論內(nèi)容生成器和相關(guān)主題:

  • 介紹了Apache模塊結(jié)構(gòu)。
  • 它顯示了一個模塊如何使用內(nèi)核注冊一個處理函數(shù)抛虏。
  • 描述了基本的處理程序API博其。
  • 它描述了內(nèi)容生成器模塊的作用,并開發(fā)了一個簡單的模塊迂猴。
  • 它顯示內(nèi)容生成器如何與request_rec對象一起使用慕淡,以獲取諸如標題和環(huán)境變量的信息,以執(zhí)行I / O以及訪問表單數(shù)據(jù)沸毁。
  • 它演示了基本的錯誤處理峰髓。
  • 描述了模塊中常見的基本管理。
  • 它引入了Apache的默認處理程序息尺,展示了稍微更先進的技術(shù)來高效地提供靜態(tài)文件携兵,并適當關(guān)注HTTP協(xié)議。

此時掷倔,您應該可以將應用程序編寫為模塊或?qū)GI腳本重寫為模塊眉孩。 雖然我們介紹了一個模塊的整體結(jié)構(gòu)框架,但是我們的覆蓋面已經(jīng)有幾個空白勒葱。 模塊結(jié)構(gòu)的其余部分涉及配置; 他們將在第9章中討論浪汪。鉤子的含義及其注冊在第10章中討論。接下來凛虽,第6章死遭,第7章和第8章通過引入請求處理周期,訪問和身份驗證以及 過濾鏈凯旋。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末呀潭,一起剝皮案震驚了整個濱河市钉迷,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌钠署,老刑警劉巖糠聪,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異谐鼎,居然都是意外死亡舰蟆,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進店門狸棍,熙熙樓的掌柜王于貴愁眉苦臉地迎上來身害,“玉大人,你說我怎么就攤上這事草戈∷欤” “怎么了?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵唐片,是天一觀的道長丙猬。 經(jīng)常有香客問我,道長牵触,這世上最難降的妖魔是什么淮悼? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮揽思,結(jié)果婚禮上袜腥,老公的妹妹穿的比我還像新娘。我一直安慰自己钉汗,他們只是感情好羹令,可當我...
    茶點故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著损痰,像睡著了一般福侈。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上卢未,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天肪凛,我揣著相機與錄音,去河邊找鬼辽社。 笑死伟墙,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的滴铅。 我是一名探鬼主播戳葵,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼汉匙!你這毒婦竟也來了拱烁?” 一聲冷哼從身側(cè)響起生蚁,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎戏自,沒想到半個月后邦投,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡擅笔,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年尼摹,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片剂娄。...
    茶點故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖玄呛,靈堂內(nèi)的尸體忽然破棺而出阅懦,到底是詐尸還是另有隱情,我是刑警寧澤徘铝,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布耳胎,位于F島的核電站,受9級特大地震影響惕它,放射性物質(zhì)發(fā)生泄漏怕午。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一淹魄、第九天 我趴在偏房一處隱蔽的房頂上張望郁惜。 院中可真熱鬧,春花似錦甲锡、人聲如沸兆蕉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽虎韵。三九已至,卻和暖如春缸废,著一層夾襖步出監(jiān)牢的瞬間包蓝,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工企量, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留测萎,地道東北人。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓梁钾,卻偏偏與公主長得像绳泉,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子姆泻,可洞房花燭夜當晚...
    茶點故事閱讀 44,619評論 2 354

推薦閱讀更多精彩內(nèi)容