web-dev-qa-db-fra.com

Comment faire une requête get HTTP en C sans libcurl?

Je veux écrire un programme C pour générer une requête Get sans utiliser de bibliothèques externes. Est-ce possible en utilisant uniquement des bibliothèques C, en utilisant des sockets? Je pense à créer un paquet http (en utilisant un formatage approprié) et à l'envoyer au serveur. Est-ce le seul moyen possible ou existe-t-il un meilleur moyen?

30
asudhak

En utilisant des sockets BSD ou, si vous êtes quelque peu limité, dites que vous avez du RTOS, du simple TCP, comme lwIP, vous pouvez former la requête GET/POST.

Il existe un certain nombre d'implémentations open source. Voir le "happyhttp" comme exemple ( http://scumways.com/happyhttp/happyhttp.html ). Je sais, c'est du C++, pas du C, mais la seule chose qui "dépend du C++" est la gestion des chaînes/tableaux, donc elle est facilement portée en C. pur.

Attention, il n'y a pas de "paquets", car HTTP est généralement transféré via la connexion TCP, donc techniquement il n'y a qu'un flux de symboles au format RFC. Comme les requêtes http sont généralement effectuées dans une connexion -envoyer-déconnecter, on pourrait en fait appeler cela un "paquet".

Fondamentalement, une fois que vous avez un socket ouvert (sockfd) "tout" vous devez faire est quelque chose comme

char sendline[MAXLINE + 1], recvline[MAXLINE + 1];
char* ptr;

size_t n;

/// Form request
snprintf(sendline, MAXSUB, 
     "GET %s HTTP/1.0\r\n"  // POST or GET, both tested and works. Both HTTP 1.0 HTTP 1.1 works, but sometimes 
     "Host: %s\r\n"     // but sometimes HTTP 1.0 works better in localhost type
     "Content-type: application/x-www-form-urlencoded\r\n"
     "Content-length: %d\r\n\r\n"
     "%s\r\n", page, Host, (unsigned int)strlen(poststr), poststr);

/// Write the request
if (write(sockfd, sendline, strlen(sendline))>= 0) 
{
    /// Read the response
    while ((n = read(sockfd, recvline, MAXLINE)) > 0) 
    {
        recvline[n] = '\0';

        if(fputs(recvline,stdout) == EOF) { cout << ("fputs erros"); }
        /// Remove the trailing chars
        ptr = strstr(recvline, "\r\n\r\n");

        // check len for OutResponse here ?
        snprintf(OutResponse, MAXRESPONSE,"%s", ptr);
    }          
}
27
Viktor Latypov

Exemple exécutable minimal POSIX 7

Allons chercher http://example.com .

wget.c

#define _XOPEN_SOURCE 700
#include <arpa/inet.h>
#include <assert.h>
#include <netdb.h> /* getprotobyname */
#include <netinet/in.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>

int main(int argc, char** argv) {
    char buffer[BUFSIZ];
    enum CONSTEXPR { MAX_REQUEST_LEN = 1024};
    char request[MAX_REQUEST_LEN];
    char request_template[] = "GET / HTTP/1.1\r\nHost: %s\r\n\r\n";
    struct protoent *protoent;
    char *hostname = "example.com";
    in_addr_t in_addr;
    int request_len;
    int socket_file_descriptor;
    ssize_t nbytes_total, nbytes_last;
    struct hostent *hostent;
    struct sockaddr_in sockaddr_in;
    unsigned short server_port = 80;

    if (argc > 1)
        hostname = argv[1];
    if (argc > 2)
        server_port = strtoul(argv[2], NULL, 10);

    request_len = snprintf(request, MAX_REQUEST_LEN, request_template, hostname);
    if (request_len >= MAX_REQUEST_LEN) {
        fprintf(stderr, "request length large: %d\n", request_len);
        exit(EXIT_FAILURE);
    }

    /* Build the socket. */
    protoent = getprotobyname("tcp");
    if (protoent == NULL) {
        perror("getprotobyname");
        exit(EXIT_FAILURE);
    }
    socket_file_descriptor = socket(AF_INET, SOCK_STREAM, protoent->p_proto);
    if (socket_file_descriptor == -1) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    /* Build the address. */
    hostent = gethostbyname(hostname);
    if (hostent == NULL) {
        fprintf(stderr, "error: gethostbyname(\"%s\")\n", hostname);
        exit(EXIT_FAILURE);
    }
    in_addr = inet_addr(inet_ntoa(*(struct in_addr*)*(hostent->h_addr_list)));
    if (in_addr == (in_addr_t)-1) {
        fprintf(stderr, "error: inet_addr(\"%s\")\n", *(hostent->h_addr_list));
        exit(EXIT_FAILURE);
    }
    sockaddr_in.sin_addr.s_addr = in_addr;
    sockaddr_in.sin_family = AF_INET;
    sockaddr_in.sin_port = htons(server_port);

    /* Actually connect. */
    if (connect(socket_file_descriptor, (struct sockaddr*)&sockaddr_in, sizeof(sockaddr_in)) == -1) {
        perror("connect");
        exit(EXIT_FAILURE);
    }

    /* Send HTTP request. */
    nbytes_total = 0;
    while (nbytes_total < request_len) {
        nbytes_last = write(socket_file_descriptor, request + nbytes_total, request_len - nbytes_total);
        if (nbytes_last == -1) {
            perror("write");
            exit(EXIT_FAILURE);
        }
        nbytes_total += nbytes_last;
    }

    /* Read the response. */
    fprintf(stderr, "debug: before first read\n");
    while ((nbytes_total = read(socket_file_descriptor, buffer, BUFSIZ)) > 0) {
        fprintf(stderr, "debug: after a read\n");
        write(STDOUT_FILENO, buffer, nbytes_total);
    }
    fprintf(stderr, "debug: after last read\n");
    if (nbytes_total == -1) {
        perror("read");
        exit(EXIT_FAILURE);
    }

    close(socket_file_descriptor);
    exit(EXIT_SUCCESS);
}

GitHub en amont .

Compiler:

gcc -ggdb3 -std=c99 -Wall -Wextra -o wget wget.c

Obtenez http://example.com et affichez-le sur stdout:

./wget example.com

Cette commande se bloque pour la plupart des serveurs jusqu'à l'expiration du délai, ce qui est attendu:

  • le serveur ou le client doit fermer la connexion
  • nous (client) ne le faisons pas
  • la plupart des serveurs HTTP laissent la connexion ouverte jusqu'à un délai d'attente en attendant d'autres demandes, par exemple JavaScript, CSS et images suivant une page HTML
  • nous pourrions analyser la réponse et fermer lorsque les octets Content-Length sont lus, mais nous ne l'avons pas fait pour des raisons de simplicité. Quels en-têtes de réponse HTTP sont requis indique que si Content-Length n'est pas envoyé, le serveur peut simplement fermer pour déterminer la longueur.

La partie connexion fonctionne également avec l'IP:

Host example.com

donne:

example.com has address 93.184.216.34
example.com has IPv6 address 2606:2800:220:1:248:1893:25c8:1946

et nous faisons donc:

./wget 93.184.216.34

cependant, la réponse est une erreur, car nous ne définissons pas le Host: correctement dans notre programme, et c'est requis dans HTTP 1.1 .

Testé sur Ubuntu 18.04.

Exemples de serveurs

"Sans aucune bibliothèque externe" à proprement parler exclurait également la libc, vous devez donc écrire vous-même tous les appels système. Je doute que vous le pensiez aussi strict, cependant. Si vous ne souhaitez pas créer de lien vers une autre bibliothèque et ne souhaitez pas copier le code source d'une autre bibliothèque dans votre application, vous devez alors traiter directement le flux TCP à l'aide de l'API socket). meilleure approche.

La création de la requête HTTP et son envoi via connexion socket TCP est facile, tout comme la lecture de la réponse. C'est l'analyse de la réponse qui va être très délicate, en particulier si vous visez à prendre en charge une partie assez importante de la norme. Des choses comme les pages d'erreur, les redirections, la négociation de contenu, etc. peuvent rendre notre vie assez difficile si vous parlez à des serveurs Web arbitraires. Si, d'autre part, le serveur est connu pour se comporter correctement et qu'un message d'erreur simple convient parfaitement à toute réponse inattendue du serveur, cela est également assez simple.

5
MvG