Shambhala API 備註

以下是關於 Shambhala API 和您必須處理的資料結構的部份備註等。這些備註還不完整,但希望它們能夠幫助您了解重點。

此處包含關於一般教學風格的部份備註。為求簡潔,此處中的所有結構宣告並不完整 --- 真正的宣告會包含更多時段,在此並未提及。這些宣告大部分保留給伺服器核心的一個或多個元件,模組應該審慎地進行變更。不過,在某些情況下,確實是我尚未完成的部份。歡迎使用這個最新技術。

最後,以下是綱要,提供給您後續內容的概略及順序

基本觀念。

以下是 Shambhala API 背後基本觀念的概觀,以及這些觀念在程式碼中的呈現方式。

處理常式、模組和要求

Shambhala 將要求處理分解成一系列的步驟,處理方式與 Netscape Server API 大致相同 (儘管 Shambhala 有比 NetSite 多一些階段,因為對於一些架構而言,我不知道它們在未來是否會很有用)。這些步驟如下這些階段會透過檢視一系列模組來處理,檢視每個模組是否針對這個階段準備了處理常式,並在準備了處理常式的狀況下嘗試執行它。處理常式通常可以執行下列三項動作之一大部分階段會在它們被處理的第一個模組中終止;然而,對於登錄、“修復”和非存取身分驗證檢查,所有處理程式皆會持續執行(除非遇到錯誤)。而且,回應階段的獨特之處在於模組可以針對它,通過一個將 MIME 種類作為鍵值的調度表,宣告多個處理程式。模組可以宣告一個回應階段處理程式,它可以透過賦予它 */*(也就是說,一個萬用字元 MIME 種類規格)鍵值,來處理任何要求。然而,只有在伺服器已經嘗試過而且未能尋找一個更具體回應處理程式,來處理所要求物件的 MIME 種類時,萬用字元處理程式才會被呼叫(代表沒有該處理程式或它們全部拒絕)。

處理程式本身是一個單一參數(也就是一個 request_rec 結構。詳見下文)的函數,它傳回一個整數,如上所述。

模組簡介

在這個時候,我們需要說明一個模組的結構。我們選擇的將會是一個較為混亂的模組 CGI 模組 --- 它同時處理 CGI 指令碼和 ScriptAlias 設定檔指令命令。它實際上比大部分模組還要複雜許多,但如果我們只有一個範例,它也可能是介入所有事務的那一個模組。

讓我們從處理檔案開始。為了處理 CGI 指令碼,模組為它們宣告了一個回應用處理程式。由於 ScriptAlias,它也有用來處理名稱轉換階段(辨認 ScriptAlias 的 URI)的處理程式,用來處理類型檢查階段(任何 ScriptAlias 的要求會被分類為一個 CGI 指令碼)。

模組需要維護一些關於(虛擬)伺服器的資訊,也就是生效的 ScriptAlias;因此模組結構包含指向一個建構這些結構的函數的指標,以及指向另一個結合兩個結構(如果主伺服器和一個虛擬伺服器都有宣告的 ScriptAlias)的指標。

最後,這個模組包含一個用來處理 ScriptAlias 命令本身的程式碼。這個特定的模組只宣告一個命令,但可能會還有更多,所以模組有宣告它們的命令的命令表格,並描述允許它們在哪裡以及它們將如何被呼叫。

關於某些命令參數的宣告類型之最終說明:pool 是指向資源群組結構的指標;伺服器用於追蹤已配置的記憶體、已開啟的檔案等等,可能是服務特定要求或處理其自身的組態程序中。這個方式下,當要求結束(或對組態群組來說,當伺服器重新啟動時),記憶體便可以一併釋放,檔案也可以一併關閉,而不需要任何人寫出明確的程式碼來追蹤並處理它們。此外,cmd_parms 結構包含關於正在讀取設定檔的各種資訊,以及其他有時對處理設定檔命令的功能(例如 ScriptAlias)有用的狀態資訊。言歸正傳,模組本身

/* Declarations of handlers. */

int translate_scriptalias (request_rec *);
int type_scriptalias (request_rec *);
int cgi_handler (request_rec *);

/* Subsdiary dispatch table for response-phase handlers, by MIME type */

handler_rec cgi_handlers[] = {
{ "application/x-httpd-cgi", cgi_handler },
{ NULL }
};

/* Declarations of routines to manipulate the module's configuration
 * info.  Note that these are returned, and passed in, as void *'s;
 * the server core keeps track of them, but it doesn't, and can't,
 * know their internal structure.
 */

void *make_cgi_server_config (pool *);
void *merge_cgi_server_config (pool *, void *, void *);

/* Declarations of routines to handle config-file commands */

char *script_alias (cmd_parms *, void *per_dir_config, char *fake, char *real);

command_rec cgi_cmds[] = {
{ "ScriptAlias", script_alias, NULL, RSRC_CONF, TAKE2,
    "a fakename and a realname"},
{ NULL }
};

module cgi_module = {
   STANDARD_MODULE_STUFF,
   NULL,			/* initializer */
   NULL,			/* dir config creater */
   NULL,			/* dir merger --- default is to override */
   make_cgi_server_config,      /* server config */
   merge_cgi_server_config,	/* merge server config */
   cgi_cmds,			/* command table */
   cgi_handlers,		/* handlers */
   translate_scriptalias,	/* filename translation */
   NULL,			/* check_user_id */
   NULL,			/* check auth */
   NULL,			/* check access */
   type_scriptalias,		/* type_checker */
   NULL,			/* fixups */
   NULL				/* logger */
};

處理常式的運作方式

處理函式的單一輸入參數是 request_rec 結構。這個結構說明已對伺服器提出的特定要求,代表某個客戶端提出。在多數情況下,與客戶端每連線只會產生一個 request_rec 結構。

request_rec 的簡介

request_rec 包含指向伺服器處理完要求後會清除的資源群組的指標;指向包含伺服器和連線資訊的結構,最重要的,還有要求本身的資訊。

這些資訊中最重要的是一組簡短的字元串,說明要求物件的屬性,包括它的 URI、檔名、內容類型和內容編碼(分別由處理要求的翻譯和類型檢查處理函式填入)。

其他經常使用的資料項目是表格,提供客戶端原始要求的 MIME 標頭,MIME 標頭會在回應時回傳(模組可以任意加入),以及在服務要求過程中建立的所有子程序的環境變數。這些表格使用 table_gettable_set 常式來操作。

最後,有指向兩個資料結構的指標,而這些資料結構又指向各模組組態結構。特別是,這些指標指向模組為描述它已建構的方式來操作特定目錄(透過 .htaccess 檔案或 <Directory> 區段)而建立的資料結構,以及在服務要求過程中建立的私人資料(因此模組的處理函式在一個階段可以將「備忘錄」傳送給它們在其他階段的處理函式)。在 request_rec 指向的 server_rec 資料結構中,還有另一個此類組態向量,它包含每組(虛擬)伺服器組態資料。

以下是一個精簡的宣告,提供最常使用欄位

struct request_rec {

  pool *pool;
  conn_rec *connection;
  server_rec *server;

  /* What object is being requested */
  
  char *uri;
  char *filename;
  char *path_info;
  char *args;			/* QUERY_ARGS, if any */
  struct stat finfo;		/* Set by server core;
                                 * st_mode set to zero if no such file */
  
  char *content_type;
  char *content_encoding;
  
  /* MIME header environments, in and out.  Also, an array containing
   * environment variables to be passed to subprocesses, so people can
   * write modules to add to that environment.
   *
   * The difference between headers_out and err_headers_out is that the
   * latter are printed even on error, and persist across internal redirects
   * (so the headers printed for ErrorDocument handlers will have them).
   */
  
  table *headers_in;
  table *headers_out;
  table *err_headers_out;
  table *subprocess_env;

  /* Info about the request itself... */
  
  int header_only;		/* HEAD request, as opposed to GET */
  char *protocol;		/* Protocol, as given to us, or HTTP/0.9 */
  char *method;			/* GET, HEAD, POST, etc. */
  int method_number;		/* M_GET, M_POST, etc. */

  /* Info for logging */

  char *the_request;
  int bytes_sent;

  /* A flag which modules can set, to indicate that the data being
   * returned is volatile, and clients should be told not to cache it.
   */

  int no_cache;

  /* Various other config info which may change with .htaccess files
   * These are config vectors, with one void* pointer for each module
   * (the thing pointed to being the module's business).
   */
  
  void *per_dir_config;		/* Options set in config files, etc. */
  void *request_config;		/* Notes on *this* request */
  
};

request_rec 結構的來源

多數 request_rec 結構是透過從客戶端讀取 HTTP 要求並填入欄位來建立的。不過還是有些例外

處理要求、拒絕和傳回錯誤碼

如同上面所討論的,每個處理常式在呼叫來處理特定 request_rec 時,必須傳回一個 int 來表示發生了什麼事。這可以是請注意,如果傳回的錯誤碼為 REDIRECT,則組件應將 Location 放入請求的 headers_out,以表示應該將客戶端重新導向何處。

回應處理常式的特別考量

多數階段的處理常式只需設定 request_rec 結構中的幾個欄位 (或者,對於存取檢查器而言,只需傳回正確的錯誤碼) 就可完成工作。然而,回應處理常式實際上必須發送請求回到客戶端。

他們應透過函式 send_http_header 開始傳送 HTTP 回應標頭。(您不必採取任何特殊措施來略過為 HTTP/0.9 請求傳送標頭;函式會自行判斷不應執行任何動作)。如果請求標記為 header_only,則這便是他們應該執行的所有動作;他們應該在之後傳回,而不嘗試任何進一步的輸出。

否則,他們應產生一個請求主體來適當回應客戶端。必要的基礎架構包含 rputcrprintf,用於內部產生的輸出,以及 send_fd,用於將某些 FILE * 的內容直接複製到客戶端。

最後一個考量事項:執行對客戶端的 I/O 時,有進一步延遲的可能性。因此,在對客戶端發起 I/O 之前,事先設定逾時非常重要。

此時,您應該多多少少了解以下這段程式碼,它是處理沒有更具體處理常式的 GET 請求的處理常式。它也展示如何在特定回應處理常式中進行處理時,如何執行有條件的 GET。(函式 pfopenpfclose 將回傳到資源池機制中的 FILE *,因此即使請求被中斷,它仍會被關閉)。

int default_handler (request_rec *r)
{
    int errstatus;
    FILE *f;
    
    if (r->method_number != M_GET) return DECLINED;
    if (r->finfo.st_mode == 0) return NOT_FOUND;
	
    if ((errstatus = set_content_length (r, r->finfo.st_size))
	|| (errstatus = set_last_modified (r, r->finfo.st_mtime)))
        return errstatus;
    
    f = pfopen (r->pool, r->filename, "r");

    if (f == NULL) {
        log_reason("file permissions deny server access", r->filename, r);
        return FORBIDDEN;
    }
      
    register_timeout ("send", r);
    send_http_header (r);

    if (!r->header_only) {
	send_fd (f, r);
    }
    
    kill_timeout(r);
    pfclose (r->pool, f);
    return OK;
}
最後,如果上述內容全部超出能力範圍,則有幾種脫身方法。首先,如上所示,尚未產生任何輸出結果的回應處理常式,可以僅回傳錯誤碼,在這種情況下,伺服器將自動產生錯誤回應。其次,它可以透過呼叫 internal_redirect 轉移到其他處理常式,這是如上所述的內部重新導向機制呼叫方式。已執行內部重新導向的回應處理常式應始終回傳 OK

(並非回應處理常式的處理常式呼叫 internal_redirect 將導致混淆)。

驗證處理常式的特別考量

應在此詳細討論的內容

記錄處理常式的特別考量

當請求已內部重新導向時,會產生記錄什麼內容的問題。Shambhala 會透過將整個重新導向鏈串入 request_rec 結構列表(這些結構透過 r->prevr->next 指標執行),來處理此問題。在這種情況下,傳遞至記錄處理常式的 request_rec 結構,是最初為客戶端初始請求所建置的。請注意,只有鏈中的最後一個請求(即實際傳送回應的那個請求)才會正確顯示 bytes_sent 欄位。

資源配置和資源池

撰寫和設計伺服器池伺服器的問題之一,是防止資源外洩,亦即分配資源(記憶體、開啟檔案等),但在隨後沒有釋出它們。資源池機制旨在防止此問題。應在此詳細討論的內容

組態、指令等

此伺服器其中一個設計目標是維護與 NCSA 1.3 伺服器相容的外觀 --- 也即是讀取相同的組態檔、正確地處理其中的所有指令,並普遍成為 NCSA 替換的 drop-in。另一方面,另一個設計目標是將越多的伺服器功能移到模組中,這些模組與整體伺服器核心關係越少越好。要調和這些目標的唯一方式是將大多數指令的處理從中央伺服器移到模組中。

然而,只給予模組指令表格不足以將它們與伺服器核心完全分離。伺服器必須記住指令以便稍後對其採取動作。這涉及維護對模組私有的資料,這些資料可以是每個伺服器專屬的,或是每個目錄專屬的。大多數都是每個目錄專屬的,特別包括存取控制和授權資訊,以及如何從字尾決定檔案類型的資訊,這些資訊會受到 AddTypeDefaultType 指令變更,以此類推。普遍的規範哲學是,可透過目錄組態的所有內容都應這麼做;每個伺服器資訊通常在資訊標準模組集中使用,例如 AliasRedirect 等在請求連結到基礎檔案系統特定位置前就開始運作的資訊。

模擬 NCSA 伺服器的另一個需求在於能夠處理每個目錄的組態檔,通常稱為 .htaccess 檔,儘管這些檔案即使在 NCSA 伺服器裡也可能包含與存取控制無關的指令。於是,在 URI -> 檔名轉換之後但在執行任何其他階段之前,伺服器會沿著基礎檔案系統的目錄階層向下移動,追蹤已轉換路徑名稱,以讀取可能存在的任何 .htaccess 檔案。接著必須將讀取進來的資訊與伺服器本身設定檔的適用資訊進行合併(不是來自 access.conf 中的 <Directory> 區段,就是來自 srm.conf 中的預設值,而後者幾乎在所有方面都近乎完全像 <Directory /> 一樣運作)。

最後,在提供涉及讀取 .htaccess 檔案的請求後,我們需要清除為處理它們而配置的儲存。其解決方式與其他有類似問題的地方相同,將這些結構連結到每個交易資源池即可。

每個目錄的組態結構

讓我們來看看這一切是如何在 mod_mime.c 中發揮作用的,這個檔案定義了檔案類型處理函式,它模擬了 NCSA 伺服器根據字尾來判斷檔案類型的行為。我們在此要探討的,就是實作 AddTypeAddEncoding 指令的程式碼。這些指令會出現在 .htaccess 檔案中,所以它們必須在模組的私有 per-directory 資料中處理,它實際上包含兩個用於 MIME 類型和編碼資訊的獨立 table,並宣告如下
typedef struct {
    table *forced_types;	/* Additional AddTyped stuff */
    table *encoding_types;	/* Added with AddEncoding... */
} mime_dir_config;
當伺服器讀取包含 MIME 模組指令之一的組態檔案或 <Directory> 區塊時,它需要建立一個 mime_dir_config 結構,讓這些指令能夠操作。它透過執行模組「製造 per-dir 配置槽」中找到的函式來達成這件事,帶兩個參數:要套用這個組態資訊的目錄名稱(或 srm.confNULL),以及應執行配置的資源池指標。

(如果我們讀取 .htaccess 檔案,那資源池就是該請求的 per-request 資源池;否則,它就是用於組態資料的資源池,並在重新啟動時清除。在任一種情況下,建立的結構在清除資源池時消失,非常重要,必要時必須在資源池中註冊清除機制)。

對於 MIME 模組,per-dir 配置建立函式只會將上述結構 palloc,並建立幾個 table 來填滿它。它看起來像這個樣子

void *create_mime_dir_config (pool *p, char *dummy)
{
    mime_dir_config *new =
      (mime_dir_config *) palloc (p, sizeof(mime_dir_config));

    new->forced_types = make_table (p, 4);
    new->encoding_types = make_table (p, 4);
    
    return new;
}
現在,假設我們剛讀取一個 .htaccess 檔案。我們已經有了階層中下一個目錄的 per-directory 組態結構。如果我們剛讀取的 .htaccess 檔案沒有任何 AddTypeAddEncoding 指令,則它對 MIME 模組的 per-directory 配置結構仍然有效,我們可以使用它。否則,我們需要以某種方式合併兩個結構。

為此,伺服器會呼叫模組的 per-directory 配置合併函式(如果存在)。該函式採用三個參數:要合併的兩個結構和在其中配置結果的資源池。對於 MIME 模組,所有需要做的就是將新的 per-directory 配置結構中的表格疊加在父結構的表格上

void *merge_mime_dir_configs (pool *p, void *parent_dirv, void *subdirv)
{
    mime_dir_config *parent_dir = (mime_dir_config *)parent_dirv;
    mime_dir_config *subdir = (mime_dir_config *)subdirv;
    mime_dir_config *new =
      (mime_dir_config *)palloc (p, sizeof(mime_dir_config));

    new->forced_types = overlay_tables (p, subdir->forced_types,
					parent_dir->forced_types);
    new->encoding_types = overlay_tables (p, subdir->encoding_types,
					  parent_dir->encoding_types);

    return new;
}
注意事項 --- 如果沒有 per-directory 合併函式,伺服器只會使用子目錄的組態資訊,而忽略父目錄的組態資訊。對於某些模組,這樣做沒問題(例如,包含模組,其 per-directory 組態資訊僅包含 XBITHACK 的狀態),對於這些模組,您只需不宣告一個,並將模組中對應的結構槽留為 NULL

指令處理

現在我們有了這些結構,我們需要找出如何填滿它們。這涉及處理實際的 AddTypeAddEncoding 命令。為命令尋找,伺服器會在模組的 command table 中尋找。該表包含有關命令需要多少個引數、以及用什麼格式、在哪裡允許等等的資訊。那些資訊足夠讓伺服器使用已準備好的引數呼叫大多數命令處理功能。不多介紹,讓我們看看 AddType 命令處理常式,如下所示(AddEncoding 命令看起來基本上相同,這裡不會示範)
char *add_type(cmd_parms *cmd, mime_dir_config *m, char *ct, char *ext)
{
    if (*ext == '.') ++ext;
    table_set (m->forced_types, ext, ct);
    return NULL;
}
這個命令處理程序異常簡單。如您所見,它需要四個引數,其中兩個是準備好的引數,另一個是相關模組的每個目錄配置結構,第四個是 cmd_parms 結構的指標。該結構包含許多引數,經常在一些但並非所有的命令中使用,包括資源池(可從中配置記憶體,可用來進行清理)和正在設定的(虛擬)伺服器,如果需要,可以從中取得模組的每個伺服器設定資料。

這個特定命令處理常式異常簡單的另一方法是不會有任何錯誤的錯誤條件。如果有的話,它會傳回錯誤訊息,而不是輸出 NULL;這會導致錯誤訊息印在伺服器的 stderr 上,然後快速離開,如果錯誤訊息在主設定檔中;對於 .htaccess 檔,語法錯誤會記錄在伺服器錯誤記錄中(還會紀錄錯誤訊息從何而來),且請求會以伺服器錯誤回應(HTTP 錯誤狀態,代碼 500)彈回。

MIME 模組的命令表有這些命令的項目,看起來像這樣

command_rec mime_cmds[] = {
{ "AddType", add_type, NULL, OR_FILEINFO, TAKE2, 
    "a mime type followed by a file extension" },
{ "AddEncoding", add_encoding, NULL, OR_FILEINFO, TAKE2, 
    "an encoding (e.g., gzip), followed by a file extension" },
{ NULL }
};
這些表中的項目是最後,在設定完所有這些值後,我們必須使用它。這最終會在模組的處理程式執行,特別是針對其檔案輸入類型處理程式,其外觀或多或少像這樣;請注意,每個目錄組態結構會使用 get_module_config 函數從 request_rec 的每個目錄組態向量中提取。
int find_ct(request_rec *r)
{
    int i;
    char *fn = pstrdup (r->pool, r->filename);
    mime_dir_config *conf =
      (mime_dir_config *)get_module_config(r->per_dir_config, &mime_module);
    char *type;

    if (S_ISDIR(r->finfo.st_mode)) {
        r->content_type = DIR_MAGIC_TYPE;
	return OK;
    }
    
    if((i=rind(fn,'.')) < 0) return DECLINED;
    ++i;

    if ((type = table_get (conf->encoding_types, &fn[i])))
    {
        r->content_encoding = type;

	/* go back to previous extension to try to use it as a type */
	
        fn[i-1] = '\0';
	if((i=rind(fn,'.')) < 0) return OK;
	++i;
    }

    if ((type = table_get (conf->forced_types, &fn[i])))
    {
        r->content_type = type;
    }
    
    return OK;
}

旁注 --- 每個伺服器的組態、虛擬伺服器等

每個伺服器模組組態背後的基本概念,基本上和其他每個目錄組態的概念相同;有一個建立函數和合併函數,後者是在虛擬伺服器部分覆寫基礎伺服器組態時呼叫,且必須計算合併結構。(與每個目錄組態相同,如果未指定合併函數,且模組在某些虛擬伺服器中進行組態,則基礎組態只會被忽略)。

唯一實質上的差異是,當命令需要組態每個伺服器的私人模組資料時,必須先前往 cmd_parms 資料才能取得它。以下是一個來自別名模組的範例,它也指出如何傳回語法錯誤(請注意,命令處理程式的每個目錄組態引數宣告為偽造值,因為模組實際上沒有每個目錄的組態資料)

char *add_redirect(cmd_parms *cmd, void *dummy, char *f, char *url)
{
    server_rec *s = cmd->server;
    alias_server_conf *conf =
        (alias_server_conf *)get_module_config(s->module_config,&alias_module);
    alias_entry *new = push_array (conf->redirects);

    if (!is_url (url)) return "Redirect to non-URL";
    
    new->fake = f; new->real = url;
    return NULL;
}