Adolph
Engineer
Engineer
  • UID623
  • Fans2
  • Follows1
  • Posts72
Reads:1156Replies:0

Exploring FastCGI protocol and its implementation in PHP

Created#
More Posted time:Sep 18, 2016 13:35 PM
Analysis on traditional CGI principle
After the client accesses a URL, the data is committed by GET/POST/PUT, and a request is sent to the Web server through HTTP protocol. The server HTTP Daemon will pass the information described in the HTTP request through the standard input stdin and environment variables to the CGI program designated by the homepage, and start the application for processing (including processing on the database). The results are returned to HTTP Daemon through the standard output stdout, then to the client through the HTTP protocol by the HTTP Daemon process.
The above paragraph may be abstract to comprehend. Next we will illustrate it through a GET request.



The following code implements the function described in the figure. Web server starts a socket listener service, and then executes the CGI program locally. A more detailed interpretation of the code can be found in the latter part.
Web server code
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
    
#define SERV_PORT 9003
 
char* str_join(char *str1, char *str2);
char* html_response(char *res, char *buf);
  
int main(void)
{
    int lfd, cfd;
    struct sockaddr_in serv_addr,clin_addr;
    socklen_t clin_len;
    char buf[1024],web_result[1024];
    int len;
    FILE *cin;
  
    if((lfd = socket(AF_INET,SOCK_STREAM,0)) == -1){
        perror("create socket failed");
        exit(1);
    }
      
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(SERV_PORT);
  
    if(bind(lfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1)
    {
        perror("bind error");
        exit(1);
    }
  
    if(listen(lfd, 128) == -1)
    {
        perror("listen error");
        exit(1);
    }
    
    signal(SIGCLD,SIG_IGN);
  
    while(1)
    {
        clin_len = sizeof(clin_addr);
        if ((cfd = accept(lfd, (struct sockaddr *)&clin_addr, &clin_len)) == -1)
        {
            perror("receive error\n");
            continue;
        }
 
        cin = fdopen(cfd, "r");
        setbuf(cin, (char *)0);
        fgets(buf,1024,cin); //Read the first line
        printf("\n%s", buf);
 
        //============================ CGI environment variable settings demo ============================
        
        // For example,  "GET /user.cgi?id=1 HTTP/1.1";
 
        char *delim = " ";
        char *p;
        char *method, *filename, *query_string;
        char *query_string_pre = "QUERY_STRING=";
 
        method = strtok(buf,delim);         // GET
        p = strtok(NULL,delim);             // /user.cgi?id=1
        filename = strtok(p,"?");           // /user.cgi
        
        if (strcmp(filename,"/favicon.ico") == 0)
        {
            continue;
        }
 
        query_string = strtok(NULL,"?");    // id=1
        putenv(str_join(query_string_pre,query_string));
 
        //============================ CGI environment variable settings demo ============================
 
        int pid = fork();
  
        if (pid > 0)
        {
            close(cfd);
        }
        else if (pid == 0)
        {
            close(lfd);
            FILE *stream = popen(str_join(".",filename),"r");
            fread(buf,sizeof(char),sizeof(buf),stream);
            html_response(web_result,buf);
            write(cfd,web_result,sizeof(web_result));
            pclose(stream);
            close(cfd);
            exit(0);
        }
        else
        {
            perror("fork error");
            exit(1);
        }
    }
  
    close(lfd);
      
    return 0;
}
 
char* str_join(char *str1, char *str2)
{
    char *result = malloc(strlen(str1)+strlen(str2)+1);
    if (result == NULL) exit (1);
    strcpy(result, str1);
    strcat(result, str2);
  
    return result;
}
 
char* html_response(char *res, char *buf)
{
    char *html_response_template = "HTTP/1.1 200 OK\r\nContent-Type:text/html\r\nContent-Length: %d\r\nServer: mengkang\r\n\r\n%s";
 
    sprintf(res,html_response_template,strlen(buf),buf);
    
    return res;
}


Key of the code above:
Lines 66-81 serve to find the relative path of CGI program (for convenience, we defined the root directory as the current directory of the Web program), so that you can execute the CGI program in the sub-process. At the same time, it sets the environment variables for the CGI program to conveniently read them;
Lines 94-95 write the standard output results of the CGI program to the cache of the Web server daemon;
Line 97 writes the packaged HTML results to the client socket descriptor and returns it to the client connected to the Web server.
CGI program (user.c)
#include <stdio.h>
#include <stdlib.h>
// Query user information through the ID obtained
int main(void){

        //============================ Mock database ============================
        typedef struct
        {
                int  id;
                char *username;
                int  age;
        } user;

        user users[] = {
                {},
                {
                        1,
                        "mengkang.zhou",
                        18
                }
        };
        //============================ Mock database ============================


        char *query_string;
        int id;

        query_string = getenv("QUERY_STRING");
        
        if (query_string == NULL)
        {
                printf("no input data");
        } else if (sscanf(query_string,"id=%d",&id) != 1)
        {
                printf("no input id");
        } else
        {
                printf("user information query
 Student no:  %d
Name:  %s
Age:  %d",id,users[id].username,users[id].age);
        }
        
        return 0;
}


Compile the above CGI program into `gcc user.c -o user.cgi` and put it in a directory at the same level as the web program.
Line 28 of the code reads the environment variables set in the above-mentioned Web service daemon, which is the focus of our demonstration.
FastCGI principles
Compared with CGI/1.1, FastCGI regulates Web servers to fork a sub process locally to execute the CGI program, fills the environment variables predefined by CGI and inserts it into the system environment variables, passes the HTTP body content to the sub-process by the standard input, and returns the processed results by standard output back to the Web server. The core of FastCGI is to replace the traditional fork-and-execute method, reduce the huge cost of each start (followed by an example of PHP), and handle requests in a resident way.
FastCGI workflow is as follows:
1. FastCGI process manager initializes itself, starts multiple CGI interpreter processes, and waits for the connection from Web Server.
2. Web Server and FastCGI process manager conduct Socket communication, and send CGI environment variables and standard input data to the CGI interpreter process through the FastCGI protocol.
3. After the CGI interpreter process completes processing, it returns the standard output and error information along the same connection to the Web Server.
4. The CGI interpreter process then waits for and processes the next connection from the Web Server.


One of the differences between FastCGI and the traditional CGI model is that Web server does not directly execute the CGI program, but interacts with FastCGI responder (FastCGI process manager) through socket and the Web server needs to encapsulate CGI interface data in the FastCGI protocol packet and send it to the FastCGI responder program. Because the FastCGI process manager is based on sockets for communication, it is also distributed, and Web server and CGI responder server should be separated for deployment.
Again, FastCGI is a protocol based on CGI/1.1. It transmits data in CGI/1.1 in the sequence and format defined by FastCGI protocol.
Preparation
Perhaps the above content is still very abstract. This is because starters do not have the general knowledge about FastCGI protocol, and they didn’t learn with the help of real code. So you need to study the content of FastCGI protocol in advance. Do not try to fully understand it yet. You can try to have a broad understanding first, and learn and understand it with the help of this article.
FastCGI protocol analysis
Next we will analyze the PHP code of FastCGI. The following code is derived from the PHP source code unless otherwise specified.
FastCGI message type
FastCGI divides the message to be transferred into many types, and its struct is defined as follows:
typedef enum _fcgi_request_type {
        FCGI_BEGIN_REQUEST                =  1, /* [in]                              */
        FCGI_ABORT_REQUEST                =  2, /* [in]  (not supported)             */
        FCGI_END_REQUEST                =  3, /* [out]                             */
        FCGI_PARAMS                                =  4, /* [in]  environment variables       */
        FCGI_STDIN                                =  5, /* [in]  post data                   */
        FCGI_STDOUT                                =  6, /* [out] response                    */
        FCGI_STDERR                                =  7, /* [out] errors                      */
        FCGI_DATA                                =  8, /* [in]  filter data (not supported) */
        FCGI_GET_VALUES                        =  9, /* [in]                              */
        FCGI_GET_VALUES_RESULT        = 10  /* [out]                             */
} fcgi_request_type;


Message sending sequence
The following figure shows a simple message passing flow.


FCGI_BEGIN_REQUEST is first sent, followed by FCGI_PARAMS and FCGI_STDIN. Because 65535 is the maximum length supported by each message header (details can be found below), the two types of messages may be sent more than once, or for multiple times continuously.
After the FastCGI response body completes processing, it will send FCGI_STDOUT and FCGI_STDERR, maybe for multiple times continuously for the same reason. Finally, FCGI_END_REQUEST indicates the end of the request.
One note, FCGI_BEGIN_REQUEST and FCGI_END_REQUEST respectively mark the request beginning and end, and are closely linked with the entire protocol, so the content of their message bodies is also part of the protocol and will have the corresponding struct (details can be found later). The environment variables, standard input, standard output and error output are business-related, and are not associated with the protocol. So their message body content does not have corresponding structs.
Since the whole message is binary and transmitted continuously, it is necessary to define a unified structured message header to facilitate reading the message body of each message and segmenting the message. This is a very common means in network communications.
FastCGI message header
As mentioned above, FastCGI messages are divided into 10 types. Some are input, and some are output. All the messages start with a message header. Its struct is defined as follows:
typedef struct _fcgi_header {
    unsigned char version;
    unsigned char type;
    unsigned char requestIdB1;
    unsigned char requestIdB0;
    unsigned char contentLengthB1;
    unsigned char contentLengthB0;
    unsigned char paddingLength;
    unsigned char reserved;
} fcgi_header;


Field interpretation:
Version The version of FastCGI protocol.
type The FastCGI record type, that is, the general role of record execution.
requestId The FastCGI request of the record.
contentLength The number of bytes of the record contentData component.
Protocol descriptions about the above xxB1 and xxB0: when two adjacent structural components are of the same name except in the suffix of “B1” and “B0”, it indicates that these two components can be regarded as single digits with an estimated value of B1<<8 + B0. The name of the individual digit is the name without the suffix. This convention summarizes the processing method of digits represented by more than two bytes.
For example, the maximum value of requestId and contentLength in the protocol header is 65535.

#include <stdio.h> #include <stdlib.h> #include <limits.h> int main() { unsigned char requestIdB1 = UCHAR_MAX; unsigned char requestIdB0 = UCHAR_MAX; printf("%d\n", (requestIdB1 << 8) + requestIdB0); // 65535 }

You might wonder what if the length of a message body exceeds 65535. Then the message can be divided into multiple messages of the same type for sending.
Definition of FCGI_BEGIN_REQUEST
typedef struct _fcgi_begin_request {
    unsigned char roleB1;
    unsigned char roleB0;
    unsigned char flags;
    unsigned char reserved[5];
} fcgi_begin_request;


Field interpretation
Role indicates the role of the application that the Web server expects. There are three roles (The ones discussed here are generally responder roles).
typedef enum _fcgi_role {
    FCGI_RESPONDER  = 1,
    FCGI_AUTHORIZER = 2,
    FCGI_FILTER     = 3
} fcgi_role;


The flags component in FCGI_BEGIN_REQUEST contains flags & FCGI_KEEP_CONN that controls line closure: if the value is 0, the application will close the line after the request response. If the value is not 0, the application will not close the line after the request response; and Web server retains the responsiveness on the line.
Definition of FCGI_END_REQUEST
typedef struct _fcgi_end_request {
    unsigned char appStatusB3;
    unsigned char appStatusB2;
    unsigned char appStatusB1;
    unsigned char appStatusB0;
    unsigned char protocolStatus;
    unsigned char reserved[3];
} fcgi_end_request;


Field interpretation
appStatus component is the status code of the application level.
The protocolStatus component is the status code of the protocol level; the value of protocolStatus may be:
FCGI_REQUEST_COMPLETE: the normal end of the request.
FCGI_CANT_MPX_CONN: reject new requests. This occurs when a Web server sends concurrent requests to the application on a line, and the application is designed to handle a request for each line at a time.
FCGI_OVERLOADED: reject new requests. This occurs when the application runs out of some resources, such as a database connection.
FCGI_UNKNOWN_ROLE: reject new requests. This occurs when the Web server specifies a rule that cannot be recognized by the application.
The definition of protocolStatus in PHP is as follows:
typedef enum _fcgi_protocol_status {
    FCGI_REQUEST_COMPLETE   = 0,
    FCGI_CANT_MPX_CONN      = 1,
    FCGI_OVERLOADED         = 2,
    FCGI_UNKNOWN_ROLE       = 3
} dcgi_protocol_status;


Note that the values of each element of dcgi_protocol_status and fcgi_role are defined in the FastCGI protocol, rather than self-defined by PHP.
Message communication sample
To be simple, we only display the type and ID of the message in the message header, and the other fields are not displayed. The following example comes from the official website.
{FCGI_BEGIN_REQUEST,   1, {FCGI_RESPONDER, 0}}
{FCGI_PARAMS,          1, "\013\002SERVER_PORT80\013\016SERVER_ADDR199.170.183.42 ... "}
{FCGI_STDIN,           1, "quantity=100&item=3047936"}
{FCGI_STDOUT,          1, "Content-type: text/html\r\n\r\n<html>\n<head> ... "}
{FCGI_END_REQUEST,     1, {0, FCGI_REQUEST_COMPLETE}}


With the above structs, we can infer the general parsing and response process of the FastCGI responder:
First, read the message header, get the type of FCGI_BEGIN_REQUEST, then parse the message body and get the role required of FCGI_RESPONDER; the flag is 0, indicating the line will be closed after the request end. Parse the second message, get its type of FCGI_PARAMS, and then directly segment the content of the message body with carriage returns and store them in the environment variable. Similarly, after the process is completed, the FCGI_STDOUT message body and the FCGI_END_REQUEST message body are returned to the Web server for parsing.
Implementation of FastCGI in PHP
The following code notes are my personal summary. Please point out any possible errors. It may serve as a guide for those who are not familiar with the code. But if it cannot offer you a clear understanding, please read the code carefully on your own line by line.
Taking php-src/sapi/cgi/cgi_main.c as an example, we suppose the development environment is Unix. We will not discuss about the definition of some variables and SAPI initialization here. We will only illustrate the content on FastCGI.
1. Activate a socket listener service
fcgi_fd = fcgi_listen(bindpath, 128);

Begin to listen from here, and the first three steps of socket, bind and listen are completed in fcgi_listen function.
2. Initialization of request object
Allocate memory for the fcgi_request object, and bind the socket to listen.
fcgi_init_request(&request, fcgi_fd);

The whole request is centered on fcgi_request struct object from the input to the return.
typedef struct _fcgi_request {
    int            listen_socket;
    int            fd;
    int            id;
    int            keep;
    int            closed;
 
    int            in_len;
    int            in_pad;
 
    fcgi_header   *out_hdr;
    unsigned char *out_pos;
    unsigned char  out_buf[1024*8];
    unsigned char  reserved[sizeof(fcgi_end_request_rec)];
 
    HashTable     *env;
} fcgi_request;


3. Create multiple child processes of CGI parser
The number of the child processes is by default 0. The settings are read from the configuration file to the environment variables, and then read in the program. Then a specified number of sub-processes are created to wait for processing the Web server requests.
if (getenv("PHP_FCGI_CHILDREN")) {
    char * children_str = getenv("PHP_FCGI_CHILDREN");
    children = atoi(children_str);
    ...
}
 
do {
    pid = fork();
    switch (pid) {
    case 0:
        parent = 0; // Change the id of the parent process of the child processes to 0 to avoid loop fork
 
        /* don't catch our signals */
        sigaction(SIGTERM, &old_term, 0);
        sigaction(SIGQUIT, &old_quit, 0);
        sigaction(SIGINT,  &old_int,  0);
        break;
    case -1:
        perror("php (pre-forking)");
        exit(1);
        break;
    default:
        /* Fine */
        running++;
        break;
    }
} while (parent && (running < children));


4. Receive requests in the child process
Everything here is socket’s service routine. Accept the request, and then call the fcgi_read_request.
fcgi_accept_request(&request)

int fcgi_accept_request(fcgi_request *req)
{
    int listen_socket = req->listen_socket;
    sa_t sa;
    socklen_t len = sizeof(sa);
    req->fd = accept(listen_socket, (struct sockaddr *)&sa, &len);
 
    ...
 
    if (req->fd >= 0) {
        // Adopt the multiplexing mechanism
        struct pollfd fds;
        int ret;
 
        fds.fd = req->fd;
        fds.events = POLLIN;
        fds.revents = 0;
        do {
            errno = 0;
            ret = poll(&fds, 1, 5000);
        } while (ret < 0 && errno == EINTR);
        if (ret > 0 && (fds.revents & POLLIN)) {
            break;
        }
        // Only turn off the socket connection, instead of emptying req->env
        fcgi_close(req, 1, 0);
    }
 
    ...
 
    if (fcgi_read_request(req)) {
        return req->fd;
    }
}


Put the request into the global variable sapi_globals.server_context. This is very important for facilitating calls from other places to the request.
SG(server_context) = (void *) &request;

5. Read data
The following code deletes some exception processing code and only displays the code of normal sequence of execution.
fcgi_read_request completes message read in the message communication sample message, and many of len = (hdr.contentLengthB1 << 8) | hdr.contentLengthB0;operations have been explained in the FastCGI message header above.
Here is the key to parse the FastCGI protocol.
static inline ssize_t safe_read(fcgi_request *req, const void *buf, size_t count)
{
    int    ret;
    size_t n = 0;
 
    do {
        errno = 0;
        ret = read(req->fd, ((char*)buf)+n, count-n);
        n += ret;
    } while (n != count);
    return n;
}

static int fcgi_read_request(fcgi_request *req)
{
    ...
 
    if (safe_read(req, &hdr, sizeof(fcgi_header)) != sizeof(fcgi_header) || hdr.version < FCGI_VERSION_1) {
        return 0;
    }
 
    len = (hdr.contentLengthB1 << 8) | hdr.contentLengthB0;
    padding = hdr.paddingLength;
 
    req->id = (hdr.requestIdB1 << 8) + hdr.requestIdB0;
 
    if (hdr.type == FCGI_BEGIN_REQUEST && len == sizeof(fcgi_begin_request)) {
        char *val;
 
        if (safe_read(req, buf, len+padding) != len+padding) {
            return 0;
        }
 
        req->keep = (((fcgi_begin_request*)buf)->flags & FCGI_KEEP_CONN);
        
        switch ((((fcgi_begin_request*)buf)->roleB1 << 8) + ((fcgi_begin_request*)buf)->roleB0) {
            case FCGI_RESPONDER:
                val = estrdup("RESPONDER");
                zend_hash_update(req->env, "FCGI_ROLE", sizeof("FCGI_ROLE"), &val, sizeof(char*), NULL);
                break;
            ...
            default:
                return 0;
        }
 
        if (safe_read(req, &hdr, sizeof(fcgi_header)) != sizeof(fcgi_header) || hdr.version < FCGI_VERSION_1) {
            return 0;
        }
 
        len = (hdr.contentLengthB1 << 8) | hdr.contentLengthB0;
        padding = hdr.paddingLength;
 
        while (hdr.type == FCGI_PARAMS && len > 0) {
            if (safe_read(req, &hdr, sizeof(fcgi_header)) != sizeof(fcgi_header) || hdr.version < FCGI_VERSION_1) {
                req->keep = 0;
                return 0;
            }
            len = (hdr.contentLengthB1 << 8) | hdr.contentLengthB0;
            padding = hdr.paddingLength;
        }
        
        ...
    }
}


6. Execution script
Assuming that this request is PHP_MODE_STANDARD, and php_execute_script will be called to execute the PHP file. We won’t go into detail here.
7. End request
fcgi_finish_request(&request, 1);

int fcgi_finish_request(fcgi_request *req, int force_close)
{
    int ret = 1;
 
    if (req->fd >= 0) {
        if (!req->closed) {
            ret = fcgi_flush(req, 1);
            req->closed = 1;
        }
        fcgi_close(req, force_close, 1);
    }
    return ret;
}


fcgi_finish_request calls fcgi_flush which encapsulates a FCGI_END_REQUEST message body, and then the socket-connected client descriptor is written through safe_write.
8. Standard input and standard output processing
The standard input and standard output are not discussed together above. In fact, they are defined in cgi_sapi_module struct, but the coupling of cgi_sapi_module, which is a sapi_module_struct, with other code is too much, and I have no in-depth understanding in it yet. So I will just make a simple comparison and hope other users can provide some explanations and supplementary information.
Cgi_sapi_module defines sapi_cgi_read_post to process POST data reading.
while (read_bytes < count_bytes) {
    fcgi_request *request = (fcgi_request*) SG(server_context);
    tmp_read_bytes = fcgi_read(request, buffer + read_bytes, count_bytes - read_bytes);
    read_bytes += tmp_read_bytes;
}


Fcgi_read reads FCGI_STDIN data.
At the same time, cgi_sapi_module defines sapi_cgibin_ub_write to take over output processing, and calls sapi_cgibin_single_write to implements FCGI_STDOUT FastCGI data package encapsulation.
fcgi_write(request, FCGI_STDOUT, str, str_length);
Guest