Изменение заголовков HTTP в потоке с каналами

Примечание. Это первый раз, когда я написал что-либо в прямолинейном C. Другими словами: Я понятия не имею, что я делаю.

Недавно у меня была задача, которая включала временную передачу ответов с одного сервера через сервер, обращенный к веб-интерфейсу, а затем на клиента. Вдобавок сервер ретрансляции должен динамически добавлять собственный HTTP-заголовок к ретранслированному ответу. Пришел с быстрым и грязным сценарием, но он работал на время, когда он должен был.

Тем не менее, я подумал, что это может быть хорошим упражнением для знакомства с C, просто для удовольствия, поскольку что-то вроде этого выиграет от написания ближе к металу.

Итак, ниже - это программа (я называю ее headshape из-за отсутствия лучшего имени), где вы подключаетесь (скажем, от curl command) HTTP-ответ, включая заголовки, и программа добавит /удалит /изменит один из этих заголовков и передаст остальное на as-is. Без аргументов, stdin просто переходит к stdout.

например.

$ curl -i example.com | headshape              # pass through
$ curl -i example.com | headshape Server       # remove the Server header
$ curl -i example.com | headshape Via 'foobar' # add/modify the Via header

Поэтому он немного анализирует HTTP, чтобы найти правильный заголовок - или добавить его, если его нет - и выяснить, что такое код ответа. Только ответы с состоянием 2xx будут изменены, 1xx ответы будут проигнорированы, а 3xx и выше заставят его просто переслать все оставшиеся данные без изменений (это отчасти произвольный выбор, это всего лишь упражнение с кодом). Он также по умолчанию пересылает все как есть, если видит что-то, о чем он не может понять.

Ниже приведен код. К моему большому удивлению, похоже, что он работает точно так, как предполагалось, но, опять же, я никогда не писал C-код , поэтому он, без сомнения, ужасен. Любые советы приветствуются.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define CRLF "\r\n"            // HTTP line ending
#define BLOCK_SIZE 1024        // Any reason to make this larger/smaller?
#define HEADER_DELIM ": "      // Used to spot header lines
#define MAX_HEADER_LENGTH 100  // max length of a header *name*; not the whole header line

static int   parseStatus(char* line);
static char* getHeaderName(char* line);
static int   isDelimited(char* line, int continuation);
static int   isBoundary(char* line);
void         writeLine(char* line);
void         passthru();

int main(int argc, char const *argv[]) {
    // states
    int should_parse = 0;
    int effective_status = 0;

    // options/
    char* target_header = NULL;
    char* payload_line  = NULL;

    // loop variables
    int   line_continued = 0;
    int   current_status = 0;
    char* current_header = NULL;
    char* line_out       = NULL;
    char  line[BLOCK_SIZE];

    // exit with >0 if nothing's being piped
    if(isatty(fileno(stdin))) {
        return 1; // TODO: is there a better code for this?
    }

    // Get the target header argument, if any
    if(argc > 1) {
        should_parse = 1; // could just check target_header, but this seems more descriptive
        target_header = (char*) argv[1];
    }

    // Get the target value argument, if any
    if(argc > 2) {
        // build the payload header line (i.e. "Header: value")
        size_t payload_length = 1;
        payload_length += strlen(target_header);
        payload_length += strlen(HEADER_DELIM);
        payload_length += strlen(argv[2]);
        payload_length += strlen(CRLF);

        payload_line = (char *) malloc(payload_length);

        strcpy(payload_line, target_header);
        strcat(payload_line, HEADER_DELIM);
        strcat(payload_line, argv[2]);
        strcat(payload_line, CRLF);
    }

    // heading parsing loop
    while(should_parse && fgets(line, BLOCK_SIZE, stdin)) {

        // if previous fgets didn't get a complete, CRLF-delimited line
        // (i.e. a line exceeded BLOCK_SIZE), assume the current chunk
        // is the continuation, and output it without parsing
        if(line_continued) {
            writeLine(line_out);
            line_continued = !isDelimited(line, line_continued);
            continue;
        }

        // check if this is a "complete" CRLF-delimited line
        line_continued = !isDelimited(line, line_continued);

        // reference our input for later
        line_out = line;

        if((current_status = parseStatus(line))) {

            // set the status code
            effective_status = current_status;

            // we're not interested in rewriting 3xx and higher responses
            if(effective_status >= 300) {
                // pass the remaining data directly to stdout
                should_parse = 0;
            }

        } else if((current_header = getHeaderName(line))) {

            // check if this is the header we're looking for
            if(strcmp(current_header, target_header) == 0) {

                // replace or remove the header
                line_out = payload_line;

                // pass the remaining data directly to stdout
                should_parse = 0; 
            }

        } else if(isBoundary(line)) {

            // We've reached the end of the HTTP header section.
            // If effective_status is 1xx, parsing continues but
            // 2xx will stop the parsing and insert the header
            // if need be
            if(effective_status >= 200) {

                // add the header, if this is a 2xx response
                if(payload_line && effective_status < 300) {
                    writeLine(payload_line);
                }

                // pass the remaining data directly to stdout
                should_parse = 0;
            }

        } else {

            // stop parsing if the line isn't recognized and
            // pass the remaining data directly to stdout
            should_parse = 0;
        }

        // output the line (unless it's NULL)
        if(line_out) writeLine(line_out);
    }

    // pass the remaining data directly to stdout
    passthru();

    // free the payload line, if necessary
    if(payload_line) free(payload_line);

    return 0;
}

// Gets the header name in the line, if any.
static char* getHeaderName(char* line) {
    static char header[MAX_HEADER_LENGTH];
    char* delim = strstr(line, HEADER_DELIM);

    if(delim) {
        size_t count = delim - line;
        count = count >= MAX_HEADER_LENGTH ? MAX_HEADER_LENGTH - 1 : count;
        strncpy(header, line, count);
        header[count] = '\0';
        return header;
    }

    return NULL;
}

// Match a line like "HTTP/1.x xxx..." and extract the status code
// Not exactly regex-like precision here, though. Maybe use strtok()?
static int parseStatus(char* line) {
    size_t code_length = 4;
    char* http  = strstr(line, "HTTP/1.");
    char* delim = strchr(line, ' ');

    size_t remaining = strlen(line) - (delim - line) + 1;

    if(http && delim && remaining > code_length) {
        char code[remaining];
        strncpy(code, delim, remaining);
        return atoi(code);
    }

    return 0;
}

// Check whether a string ends with CRLF
// Note: This function keeps the last char of the line from its previous
// invocation in memory in order to spot the 2-character CRLF even if it's
// been split into separate fgets lines.
static int isDelimited(char* line, int continuation) {
    static char prev = 0;
    char tail[3] = {'\0', '\0', '\0'};

    int offset = strlen(line) - 2;
    if(offset >= 0) {
        // grab last 2 chars
        tail[0] = line[offset];
        tail[1] = line[offset + 1];

    } else if(continuation && offset == -1) {
        // grab char carried over from earlier, and the line's single char
        tail[0] = prev;
        tail[1] = line[0];
    }

    prev = tail[1];
    return !strcmp(tail, CRLF);
}

// is this line a "blank" CRLF line?
static int isBoundary(char* line) {
    return strlen(line) == strlen(CRLF) && strcmp(line, CRLF) == 0;
}

// write to stdout & flush
void writeLine(char* line) {
    fwrite(line, sizeof(char), strlen(line), stdout);
    fflush(stdout);
}

// pass stdin through to stdout
void passthru() {
    char buffer[BLOCK_SIZE];
    while(1) {
        size_t bytes = fread(buffer, sizeof(char), BLOCK_SIZE, stdin);
        fwrite(buffer, sizeof(char), bytes, stdout);
        fflush(stdout);
        if(bytes < BLOCK_SIZE && feof(stdin)) break;
    }
}
11 голосов | спросил Flambino 4 Maypm14 2014, 23:21:39

1 ответ


10

Несколько элементов:

Используйте STDIN_FILENO

Вместо fileno(stdin) вы можете использовать символ препроцессора STDIN_FILENO, определенный в <unistd.h>, который вы уже включили.

should_parse должен быть объявлен bool

Предполагая, что вы используете компилятор, который не был десятилетним (bool был добавлен в стандарт c99), вы можете включить <stdbool.h> и используйте bool для типа should_parse. Он также позволяет использовать значения true и false в коде, чтобы сделать этот флаг более явным.

То же самое верно для line_continued, возвращаемое значение isBoundary и isDelimited, а второй аргумент isDelimited.

target_header должен быть const и argv не должен

Ваша переменная target_header должна быть объявлена ​​const, потому что его содержимое не изменяется программой. Если вы сделаете это изменение, вы также можете удалить бросок из строки

target_header = (const char *)argv[1];

Однако на самом деле вам не понадобится это приложение, если вы объявили main как:

int main(int argc, char *argv[])

Кажется, что кажется должен быть const *argv, но это противоречит тому, что говорит стандарт C. В разделе 5.1.2.2.1 говорится, что:

  

строки, на которые указывает массив argv, могут быть изменены программой

, что означает, что они не являются const.

Не злоупотреблять fflush

Вероятно, вам не нужно вызывать fflush после каждого fwrite. Поток автоматически очистится, когда файл будет закрыт, что также произойдет автоматически, когда заканчивается main.

Сделать вывод цикла явным

В passthru() вместо while(1), а затем с помощью break было бы более понятно написать его следующим образом:

size_t bytes;
do {
    bytes = fread(buffer, sizeof(char), BLOCK_SIZE, stdin);
    fwrite(buffer, sizeof(char), bytes, stdout);
} while (bytes == BLOCK_SIZE && !feof(stdin));

Упростить isBoundary

Процедура isBoundary может быть упрощена следующим образом:

// is this line a "blank" CRLF line?
static bool isBoundary(char* line) {
    return strcmp(line, CRLF) == 0;
}

Нет необходимости также сравнивать их длины, поскольку это уже подразумевается в strcmp.

Вычисление payload_line является сложным

Вычисляется размер переменной payload_line, а затем malloc 'd, а затем скопирован, но он используется только в нескольких других местах. То, что у вас есть, не ошибочно, но, возможно, стоит выделить фиксированный размер, а затем использовать snprintf, чтобы заполнить строку. Это разрушило бы дюжину строк, используемых для этого вычисления, в гораздо более простой отдельной строке:

snprintf(payload_line, BLOCK_SIZE, "%s" HEADER_DELIM "%s" CRLF, target_header, argv[2]);

Я уверен, что есть больше, но на данный момент мне не хватает времени.

Update

Я нашел некоторое время и несколько других вещей в коде.

Рефактор parseStatus

Текущая процедура parseStatus делает больше работы, чем требуется для извлечения кода состояния. Вам не нужно копировать строку, чтобы передать ее в atoi, чтобы ее можно было значительно упростить следующим образом:

static int parseStatus(char* line) {
    char* http  = strstr(line, "HTTP/1.");
    if (http) {
        http = strchr(http, ' ');
        if (http) {
            return atoi(http);
        }
    }
    return 0;
}

Рефактор getHeaderName

В настоящее время функция getHeaderName обозначается примерно так:

static char* getHeaderName(char* line) {
    static char header[MAX_HEADER_LENGTH];
    /* ... */
    return header;
}

Это работает (и даже некоторые древние библиотечные функциибыли сделаны таким образом), но он не является потокобезопасным, и дизайн можно легко улучшить. Вместо этого лучше, чтобы вызывающая функция выделяла буфер и передавала его вместе с длиной, а не указывала на внутренний статический буфер.

Рефактор isDelimited

В аналогичном ключе ваша функция isDelimited действительно проверяет только два отдельных байта, поэтому было бы более ясно и немного быстрее для проверки их непосредственно, а не для создания копии в памяти, а затем с помощью strcmp.

ответил Edward 5 Mayam14 2014, 03:32:24

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132