Logs
Consultez les logs.
OK
Liste des données
Consultez la liste des données.
OK
Loading...
Formulaire
Saisissez vos données.
Enregistrer
Annuler

Programmation réseau pratique avec C

Vues
633

La programmation réseau est un sujet passionnant, mais aussi très profond ; il se passe beaucoup de choses à de nombreux niveaux. Certains langages de programmation masquent ces abstractions. Avec le langage Python, par exemple, vous pouvez télécharger une page web entière en une seule ligne de code. Ce n'est pas le cas en C ! En C, pour télécharger une page web, il est essentiel de tout connaître. Il faut connaître les sockets, le protocole TCP (Transfer Control Protocol) et HTTP. En programmation réseau en C, rien n'est caché.

Le C est un excellent langage pour apprendre la programmation réseau. Non seulement parce qu'il permet de voir tous les détails, mais aussi parce que les systèmes d'exploitation les plus répandus utilisent tous des noyaux écrits en C. Aucun autre langage ne vous offre un accès aussi performant que le C. En C, tout est sous votre contrôle : vous pouvez organiser vos structures de données exactement comme vous le souhaitez, gérer la mémoire avec précision et même vous tirer une balle dans le pied comme bon vous semble.

En matière de programmation réseau, on ne peut pas se contenter de l'approche « ça marche, c'est suffisant » de la programmation par coïncidence. Il faut utiliser le raisonnement.

Dans ce document, nous nous attachons à aborder la programmation réseau de manière moderne et sécurisée. Les programmes d'exemple sont soigneusement conçus pour fonctionner avec IPv4 et IPv6, et sont tous écrits de manière portable et indépendante du système d'exploitation, dans la mesure du possible.

Dès qu'il existe un risque d'erreur de mémoire, nous nous efforçons d'en tenir compte et de le signaler. La sécurité est trop souvent négligée. Nous sommes convaincus que la sécurité est importante et qu'elle doit être intégrée au système dès le départ. C'est pourquoi, en plus d'enseigner les bases du réseau, ce document aborde en détail les protocoles sécurisés, tels que TLS. Nous espérons que vous prendrez autant de plaisir à lire ce document.  



Contenu de ce document


Nous avons rassemblé dans ce document un ensemble de recettes pratiques pour la mise en oeuvre d'une communication réseau en C/C++ avec les sockets.

Dans la section « Introduction aux réseaux et aux protocoles », nous présenterons les concepts importants liés aux réseaux. Nous inclurons des exemples de programmes pour déterminer votre adresse IP de manière pragmatique.
Dans la section « Se familiariser avec les API Socket », nous présenterons les API de programmation de sockets et nous vous permettrons de créer votre premier programme réseau : un petit serveur web.
Dans la section « Présentation approfondie des connexions TCP », nous nous concentrerons sur la programmation des sockets TCP. Nous présenterons des exemples de programmes développés pour les côtés client et serveur.
Dans la section « Établissement de connexions UDP », nous aborderons la programmation avec les sockets UDP (User Datagram Protocol).
Dans la section « Résolution de noms d'hôtes et DNS », nous expliquerons comment les noms d'hôtes sont traduits en adresses IP. Nous présenterons un exemple de programme permettant d'effectuer des recherches DNS manuelles à l'aide d'UDP.
Dans la section « Création d'un client web simple », nous présenterons HTTP, le protocole qui alimente les sites web. Nous nous lancerons directement dans la création d'un client HTTP en C/C++.
Dans la section « Construire un serveur web simple », nous décrirons comment construire un serveur web entièrement fonctionnel en C/C++. Ce programme sera capable de servir un site web statique à partir de n'importe quel navigateur web moderne.
Dans la section « Mettre votre programme en route pour l'envoi d'e-mails », nous décrirons le protocole SMTP (Simple Mail Transfer Protocol), qui gère les e-mails. Nous développerons un programme permettant d'envoyer des e-mails sur Internet.
Dans la section « Charger des pages web sécurisées avec HTTPS et OpenSSL », nous explorerons TLS, le protocole qui sécurise les pages web. Nous développerons un client HTTPS capable de télécharger des pages web en toute sécurité.
Dans la section « Implémentation d'un serveur web sécurisé », nous poursuivrons le thème de la sécurité et explorerons la construction d'un serveur web HTTPS sécurisé.
Dans la section « Établissement de connexions SSH avec libssh », nous poursuivrons le thème du protocole sécurisé. L'utilisation de Secure Shell (SSH) sera abordée pour nous connecter à un serveur distant, exécuter des commandes et télécharger des fichiers en toute sécurité.
Dans la section « Surveillance et sécurité du réseau », nous présenterons les outils et techniques utilisés pour tester les fonctionnalités du réseau, résoudre les problèmes et intercepter les protocoles de communication non sécurisés.
Dans la section « Conseils et pièges de la programmation par socket », nous détaillerons TCP et aborderons de nombreux cas limites importants liés à la programmation par socket. Les techniques abordées seront précieuses pour créer des programmes réseau robustes.
Dans la section « Programmation Web pour l'Internet des Objets », nous offrirons un aperçu de la conception et de la programmation des applications de l'Internet des Objets (IoT).


Introduction aux réseaux et aux protocoles


Nous aborderons le routage du trafic Internet. Nous apprendrons qu'il existe deux versions du protocole Internet : IPv4 et IPv6. Nous découvrirons qu'IPv4 dispose d'un nombre limité d'adresses, et ces adresses s'épuisent. Nous verrons que l'un des principaux avantages d'IPv6 est qu'il offre suffisamment d'espace d'adressage pour que chaque système possède sa propre adresse unique routable publiquement. Nous découvrirons que le nombre limité d'adresses IPv4 est largement compensé par la traduction d'adresses réseau effectuée par les routeurs. Nous montrerons comment détecter votre adresse IP locale à l'aide des utilitaires et des API fournis par le système d'exploitation. Nous découvrirons que les API permettant de lister les adresses IP locales diffèrent sensiblement entre Windows et les systèmes d'exploitation de type Unix.


Affichage du routage du trafic réseau


Nous avons affiché les routeurs entre notre système Windows et un système de destination. Cette opération permet d'identifier les routeurs par lesquels doit transiter une requête en provenance de notre système avant d'atteindre une adresse de destination. Nous avons ouvert un Terminal et avons exécuté la commande d'affichage des routeurs.

// Terminal
...
tracert example.com
...

// Terminal
image.png

Nous avons listé les routeurs entre notre système Unix et un système de destination. Cela permet de connaître les routeurs par lesquels doit transiter une requête en provenance de notre système avant d'atteindre une adresse de destination. Nous avons ouvert un Terminal et avons exécuté la commande d'affichage des routeurs. 

// Terminal
...
traceroute example.com
...

image.png

Affichage de l'adresses IP locales


Nous avons affiché les adresses IP locales disponibles sur notre système Windows. L'adresse IP locale permet à un client de notre réseau local d'accéder à notre serveur local. Nous avons ouvert un Terminal et avons exécuté la commande d'affichage des adresses IP. 

// Terminal
...
ipconfig
...

// Terminal
image.png

Nous avons affiché les adresses IP locales disponibles sur notre système Unix. L'adresse IP locale permet à un client de notre réseau local d'accéder à notre serveur local. Nous avons ouvert un Terminal et avons exécuté la commande d'affichage des adresses IP.

// Terminal
...
ip a s
...

// Terminal
image.png

Affichage de l'adresse IP publique


Nous avons affiché l'adresse IP publique de notre interface réseau. L'adresse IP publique permet à un client extérieur à notre réseau local d'accéder à notre serveur local depuis internet grâce à une redirection de port. Nous nous sommes rendus sur la page web d'un service de gestion d'adresse IP publique (https://api.ipify.org/) et avons pu afficher notre adresse IP publique.

// Navigateur web
...
https://api.ipify.org/
...

// Navigateur web
image.png

Initialisation de l'API des sockets


Nous avons écrit un programme en C/C++ permettant d'initialiser et de nettoyer l'API des sockets, portable sur les systèmes Windows, Linux ou MacOS. Même si cette opération est uniquement obligatoire pour les systèmes Windows, nous devons, néanmoins, assurer la portabilité du code source sur les autres systèmes afin d'offrir une même interface pour tous les systèmes. 


Prérequis sur la portabilité du code source


Nous avons assuré la portabilité du code source dans le fichier (CMakeLists.txt). Sur les systèmes Windows (WIN32), nous avons ajouté le fichier (SocketWin.cpp) aux codes sources. Dans le cas contraire, sur les systèmes Linux ou MacOS, nous avons ajouté le fichier (SocketUnix.cpp) aux codes sources.

// CMakeLists.txt
... 
if(WIN32)
    list(APPEND SRC_FILES
        SocketWin.cpp
    )
else()
    list(APPEND SRC_FILES
        SocketUnix.cpp
    )
endif()  
...

Nous avons assuré la portabilité du code source dans le fichier (Socket.hpp). Sur les systèmes Windows (_WIN32), nous avons défini la version minimale du système d'exploitation Windows compatible avec le code source (_WIN32_WINNT), nous avons inclus le fichier d'entête de l'API des sockets Windows (Winsock2.h) et nous avons édité les liens du code source avec la librairie de l'API des sockets Windows (ws2_32.lib). Sur les systèmes Linux ou MacOS, nous n'avons pas eu besoin d'initialiser ou de nettoyer l'API des sockets.

// Socket.hpp
... 
#if defined(_WIN32)
#ifndef _WIN32_WINNT
#define _WIN32_WINNT _WIN32_WINNT_WIN6
#endif
#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib")
#endif  
...

Prérequis sur l'initialisation de l'API des sockets


Nous avons initialisé l'API des sockets sur les systèmes Windows avec la fonction (WSAStartup). Sur les systèmes Linux ou MacOS, nous n'avons pas eu besoin d'initialiser l'API des sockets.

// SocketWin.cpp
... 
bool Socket::initSocket() const
{
    WSADATA d;

    if (WSAStartup(MAKEWORD(2, 2), &d))
    {
        printf("initSocket() failed.\n");
        return false;
    }
    return true;
}  
...

Prérequis sur le nettoyage de l'API des sockets


Nous avons nettoyé l'API des sockets sur les systèmes Windows avec la fonction (WSACleanup). Sur les systèmes Linux ou MacOS, nous n'avons pas besoin d'initialiser l'API des sockets.

// SocketWin.cpp
... 
void Socket::cleanSocket() const
{
    WSACleanup();
}  
...

Prérequis sur l'automatisation du nettoyage de l'API des sockets


Nous avons automatisé le nettoyage de l'API des sockets. Nous avons créé une classe (SocketClean) et avons appelé la méthode (cleanSocket) dans le destructeur de la classe (~SocketClean). Ainsi, après l'initialisation de l'API des sockets, nous pouvons créer une instance de la classe (SocketClean) pour garantir le nettoyage automatique de l'API des sockets.

// Socket.cpp
... 
SocketClean::~SocketClean()
{
    Socket oSocket;
    oSocket.cleanSocket();
}  
...

Développement du programme principal


Nous avons initialisé l'API des sockets (initSocket) et avons créé une instance de la classe (SocketClean) pour nettoyer automatiquement l'API des sockets sur les systèmes Windows, Linux ou MacOS.

// main.cpp
#include "Socket.hpp"

int main(int _argc, char** _argv)
{
    Socket oSocket;
    if (!oSocket.initSocket())
        return 0;

    SocketClean oSocketClean;

    printf("Socket() Ok.\n");
    return 0;
}

Développement du manager de l'API des sockets


// Socket.hpp
#pragma once

// Windows
#if defined(_WIN32)
#ifndef _WIN32_WINNT
#define _WIN32_WINNT _WIN32_WINNT_WIN6
#endif
#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib")
#endif

#include <iostream>

// Socket
class Socket
{
public:
    explicit Socket();
    ~Socket();
    bool initSocket() const;
    void cleanSocket() const;
};

// SocketClean
class SocketClean
{
public:
    explicit SocketClean();
    ~SocketClean();
};

// Socket.cpp
#include "Socket.hpp"

// Socket
Socket::Socket()
{
}

Socket::~Socket()
{
}

// SocketClean
SocketClean::SocketClean()
{
}

SocketClean::~SocketClean()
{
    Socket oSocket;
    oSocket.cleanSocket();
}

// SocketWin.cpp
#include "Socket.hpp"

bool Socket::initSocket() const
{
    WSADATA d;

    if (WSAStartup(MAKEWORD(2, 2), &d))
    {
        printf("initSocket() failed.\n");
        return false;
    }
    return true;
}

void Socket::cleanSocket() const
{
    WSACleanup();
}

// SocketUnix.cpp
#include "Socket.hpp"

bool Socket::initSocket() const
{
    return true;
}

void Socket::cleanSocket() const
{
}

Développement de la configuration CMake


// CMakeLists.txt
cmake_minimum_required(VERSION 3.10.0)
project(c01-socket-init VERSION 0.1.0 LANGUAGES C CXX)

set(SRC_FILES
    main.cpp
    Socket.hpp
    Socket.cpp
)

if(WIN32)
    list(APPEND SRC_FILES
        SocketWin.cpp
    )
else()
    list(APPEND SRC_FILES
        SocketUnix.cpp
    )
endif()

add_executable(${PROJECT_NAME}
    ${SRC_FILES}
)

Test sur l'initialisation de l'API des sockets


Nous avons affiché un message « socket() Ok. » pour indiquer que l'initialisation de l'API des sockets s'est bien déroulé.

// Terminal
image.png

Affichage des cartes réseau


Nous avons écrit un programme en C/C++ permettant d'afficher la liste des cartes réseau sur une machine. Nous avons assuré la portabilité du code source sur les systèmes Windows, Linux ou MacOS en offrant une même interface pour tous les systèmes.


Prérequis sur la portabilité du code source


Nous avons assuré la portabilité du code source sur les systèmes Windows, Linux ou MacOS à partir du fichier de configuration (CMakeLists.txt). Sur les systèmes Windows (WIN32), nous avons ajouté le fichier (AdapterWin.cpp) aux codes sources. Dans le cas contraire, sur les systèmes Linux ou MacOS, nous avons ajouté le fichier (AdapterUnix.cpp) aux codes sources.

// CMakeLists.txt
... 
if(WIN32)
    list(APPEND SRC_FILES
        AdapterWin.cpp
    )
else()
    list(APPEND SRC_FILES
        AdapterUnix.cpp
    )
endif()  
...

Nous avons assuré la portabilité du code source dans le fichier (Adapter.hpp). Sur les systèmes Windows (_WIN32), nous avons défini la version minimale du système d'exploitation Windows compatible avec le code source (_WIN32_WINNT), nous avons inclus les fichiers d'entête de l'API des sockets Windows (winsock2.h, iphlpapi.h, ws2tcpip.h) et nous avons édité les liens du code source avec les librairies de l'API des sockets Windows (ws2_32.lib, iphlpapi.lib). Sur les systèmes Linux ou MacOS, nous avons inclus les fichiers d'entête de l'API des sockets Unix (socket.h, netdb.h, ifaddrs.h).

// Adapter.hpp
... 
// Windows
#if defined (_WIN32)
#ifndef _WIN32_WINNT
#define _WIN32_WINNT _WIN32_WINNT_WIN6
#endif

#include <winsock2.h>
#include <iphlpapi.h>
#include <ws2tcpip.h>

#pragma comment(lib, "ws2_32.lib")
#pragma comment(lib, "iphlpapi.lib")
#else
// Unix
#include <sys/socket.h>
#include <netdb.h>
#include <ifaddrs.h>
#endif  
...

Nous avons assuré la portabilité du code source au niveau de la structure de données des paramètres de cartes réseau (AdapterParams) dans le fichier (Adapter.hpp). Sur les systèmes Windows (_WIN32), nous avons déclaré le pointeur de cartes réseau (adapters) de type (PIP_ADAPTER_ADDRESSES) et la taille de sa mémoire tampon (size) de type (DWORD). Sur les systèmes Linux ou MacOS, nous avons déclaré le pointeur d'interfaces réseau (addresses) de type (ifaddrs).

// Adapter.hpp
... 
struct AdapterParams
{
#if defined(_WIN32)
    DWORD size;
    PIP_ADAPTER_ADDRESSES adapters = nullptr;
#else
    struct ifaddrs* addresses = nullptr;
#endif
};  
...

Prérequis sur le chargement des cartes réseau


Nous avons chargé les cartes réseau sur les systèmes Windows. Nous avons récupéré la liste des cartes réseau avec la fonction (GetAdaptersAddresses). Nous avons récupéré la famille de l'adresse IP de l'interface réseau à partir de la propriété (sa_family). Nous avons récupéré l'adresse IP de l'interface réseau avec la fonction (getnameinfo).

// AdapterWin.cpp
... 
bool Adapter::loadAdapters(AdapterParams& _params) const
{
    _params.adapters = 
    (PIP_ADAPTER_ADDRESSES)malloc(config::adapter::memorySize);
    if (!_params.adapters)
    {
        fprintf(stderr, "loadAdapters(1) failed.|size=%ld\n", _params.size);
        return false;

    }

    int adpaterResult = GetAdaptersAddresses(AF_UNSPEC, 
    GAA_FLAG_INCLUDE_PREFIX, 0, _params.adapters, &_params.size);
    if (adpaterResult == ERROR_BUFFER_OVERFLOW)
    {
        fprintf(stderr, "loadAdapters(2) failed.|size=%ld\n", _params.size);
        return false;
    }

    PIP_ADAPTER_ADDRESSES adapter = _params.adapters;
    while (adapter)
    {
        AdapterNameParams* adapterName = _params.addAdapterName();
        adapterName->name = oTools.toString(adapter->FriendlyName);

        PIP_ADAPTER_UNICAST_ADDRESS address = adapter->FirstUnicastAddress;
        while (address)
        {
            int family = address->Address.lpSockaddr->sa_family;

            if (family == AF_INET || family == AF_INET6)
            {
                AdapterAddressParams* addressName = 
               _params.addApaterAddress(adapterName);

                addressName->family = (family == AF_INET) ? "IPv4" : "IPv6";

                char addressIP[100];

                getnameinfo(address->Address.lpSockaddr,
                    address->Address.iSockaddrLength,
                    addressIP, sizeof(addressIP), 0, 0, NI_NUMERICHOST);

                addressName->address = addressIP;
            }

            address = address->Next;
        }

        adapter = adapter->Next;
    }

    return true;
}  
...

Nous avons chargé les cartes réseau sur les systèmes Linux ou MacOS. Nous avons récupéré la liste des cartes réseau avec la fonction (getifaddrs). Nous avons récupéré la famille de l'adresse IP de l'interface réseau à partir de la propriété (family). Nous avons récupéré l'adresse IP de l'interface  réseau avec la fonction (getnameinfo).

// AdapterUnix.cpp
... 
bool Adapter::loadAdapters(AdapterParams& _params) const
{
    if (getifaddrs(&_params.addresses) == -1)
    {
        fprintf(stderr, "loadAdapters() failed.\n");
        return false;
    }

    struct ifaddrs* address = _params.addresses;
    while (address)
    {
        if (address->ifa_addr == nullptr)
        {
            address = address->ifa_next;
            continue;
        }

        int family = address->ifa_addr->sa_family;

        if (family == AF_INET || family == AF_INET6)
        {
            AdapterNameParams* adapterName;

            if (!_params.getAdapterName(&adapterName, address->ifa_name))
            {
                adapterName = _params.addAdapterName();
                adapterName->name = address->ifa_name;
            }

            AdapterAddressParams* addressName = 
            _params.addApaterAddress(adapterName);
            addressName->family = (family == AF_INET) ? "IPv4" : "IPv6";

            char ap[100];
            const int family_size = (family == AF_INET) ?
                sizeof(struct sockaddr_in) : sizeof(struct sockaddr_in6);
            getnameinfo(
                address->ifa_addr, family_size, ap, sizeof(ap), 0, 0, 
                NI_NUMERICHOST);
            addressName->address = ap;
        }

        address = address->ifa_next;
    }

    return true;
}  
...

Prérequis sur l'affichage des cartes réseau


Nous avons affiché les cartes réseau sur les systèmes Windows, Linux ou MacOS.

// Adapter.cpp
... 
void Adapter::printAdapters(const AdapterParams& _params) const
{
    const int margin = 13;
    printf("---\n");
    printf("AdapterNameParams:\n");
    for (auto* adapterName : _params.adapterNameList)
    {
        printf("---\n");
        printf("Adapter name: %s\n", adapterName->name.c_str());

        for (auto* addressName : _params.getAddressList(adapterName))
        {
            printf("\t%s\t%s\n", addressName->family.c_str(), 
            addressName->address.c_str());
        }
    }

}  
...

Développement du programme principal


Nous avons initialisé l'API des sockets avec la fonction (initSocket) et avons créé une instance de la classe (AdapterClean) pour nettoyer automatiquement l'API des sockets. Nous avons chargé les cartes réseau disponibles sur notre système Windows, Linux ou MacOS avec la fonction (loadAdapters) et avons affiché la liste des cartes réseau avec la fonction (printAdapters).
 
// main.cpp
#include "Adapter.hpp"

int main(int _argc, char** _argv)
{
    Adapter oAdapter;
    AdapterParams adapterParams;

    if (!oAdapter.initSocket())
        return 0;

    AdapterClean oAdapterClean;

    if (!oAdapter.loadAdapters(adapterParams))
        return 0;

    oAdapter.print(adapterParams);
    oAdapter.printAdapters(adapterParams);

    return 0;
}

Développement du manager des cartes réseau


// Adapter.hpp
#pragma once

// Windows
#if defined (_WIN32)
#ifndef _WIN32_WINNT
#define _WIN32_WINNT 0x0600
#endif

#include <winsock2.h>
#include <iphlpapi.h>
#include <ws2tcpip.h>

#pragma comment(lib, "ws2_32.lib")
#pragma comment(lib, "iphlpapi.lib")
#else
// Unix
#include <sys/socket.h>
#include <netdb.h>
#include <ifaddrs.h>
#endif

// Common
#include <iostream>
#include <string>
#include <vector>

// Params
struct AdapterParams;
struct AdapterNameParams;
struct AdapterAddressParams;

using AdapterNameList = std::vector<AdapterNameParams*>;
using AdapterAddressList = std::vector<AdapterAddressParams*>;

// AdapterParams
struct AdapterParams
{
#if defined(_WIN32)
    DWORD size;
    PIP_ADAPTER_ADDRESSES adapters = nullptr;
#else
    struct ifaddrs* addresses = nullptr;
#endif
    AdapterNameList adapterNameList;
    AdapterAddressList addressNameList;

    explicit AdapterParams();
    ~AdapterParams();
    AdapterNameParams* addAdapterName();
    AdapterAddressParams* addApaterAddress(AdapterNameParams* _adapterName);
    bool getAdapterName(AdapterNameParams** _adapterName, const std::string& _name) const;
    AdapterAddressList getAddressList(AdapterNameParams* _adapterName) const;
};

// AdapterNameParams
struct AdapterNameParams
{
    std::string name;
};

// AdapterAddressParams
struct AdapterAddressParams
{
    AdapterNameParams* adapterName;
    std::string address;
    std::string family;

    explicit AdapterAddressParams(AdapterNameParams* _adapterName);
    ~AdapterAddressParams();
};

// Adapter
class Adapter
{
public:
    explicit Adapter();
    ~Adapter();
    bool initSocket() const;
    void cleanSocket() const;
    bool loadAdapters(AdapterParams& _params) const;
    void print(const AdapterParams& _params) const;
    void printAdapters(const AdapterParams& _params) const;
};

// AdapterClean
class AdapterClean
{
public:
    explicit AdapterClean();
    ~AdapterClean();
};

// AdapterException
class AdapterException : public std::exception
{
public:
    explicit AdapterException(const std::string& _msg);
    ~AdapterException();
    const char* what() const throw();

private:
    std::string m_msg;
};

// Adapter.cpp
#include "Adapter.hpp"
#include "Tools.hpp"

// AdapterParams
AdapterNameParams* AdapterParams::addAdapterName()
{
    AdapterNameParams* adapterName = new AdapterNameParams;
    if (!adapterName)
    {
        throw AdapterException("addAdapterName() failed.");
    }
    adapterNameList.push_back(adapterName);
    return adapterName;
}

AdapterAddressParams* AdapterParams::addApaterAddress(AdapterNameParams* _adapterName)
{
    AdapterAddressParams* addressName = new AdapterAddressParams(_adapterName);
    if (!addressName)
    {
        throw AdapterException("addApaterAddress() failed.");
    }
    addressNameList.push_back(addressName);
    return addressName;
}

bool AdapterParams::getAdapterName(AdapterNameParams** _adapterName, const std::string& _name) const
{
    for (auto* adapterName : adapterNameList)
    {
        if (adapterName->name == _name)
        {
            (*_adapterName) = adapterName;
            return true;
        }
    }
    return false;
}


AdapterAddressList AdapterParams::getAddressList(AdapterNameParams* _adapterName) const
{
    AdapterAddressList addressNameNewList;
    for (auto* addressName : addressNameList)
    {
        if (addressName->adapterName == _adapterName)
        {
            addressNameNewList.push_back(addressName);
        }
    }
    return addressNameNewList;
}

// AdapterAddressParams
AdapterAddressParams::AdapterAddressParams(AdapterNameParams* _adapterName)
    : adapterName(_adapterName)
{
}

AdapterAddressParams::~AdapterAddressParams()
{
}

// Adapter
Adapter::Adapter()
{
}

Adapter::~Adapter()
{
}

void Adapter::print(const AdapterParams& _params) const
{
    const int margin = 13;
    printf("---\n");
    printf("AdapterNameParams:\n");
    for (auto* adapterName : _params.adapterNameList)
    {
        printf("%*s:\n", margin, "---");
        printf("%*s: %s\n", margin, "name", adapterName->name.c_str());

        for (auto* addressName : _params.getAddressList(adapterName))
        {
            printf("%*s:\n", 2 * margin, "---");
            printf("%*s: %s\n", 2 * margin, "family", addressName->family.c_str());
            printf("%*s: %s\n", 2 * margin, "address", addressName->address.c_str());
        }
    }
}

void Adapter::printAdapters(const AdapterParams& _params) const
{
    const int margin = 13;
    printf("---\n");
    printf("AdapterNameParams:\n");
    for (auto* adapterName : _params.adapterNameList)
    {
        printf("---\n");
        printf("Adapter name: %s\n", adapterName->name.c_str());

        for (auto* addressName : _params.getAddressList(adapterName))
        {
            printf("\t%s\t%s\n", addressName->family.c_str(), addressName->address.c_str());
        }
    }

}

//AdapterClean
AdapterClean::AdapterClean()
{
}

AdapterClean::~AdapterClean()
{
}

// AdapterException
AdapterException::AdapterException(const std::string& _msg)
    : m_msg(_msg)
{
}

AdapterException::~AdapterException()
{
}

const char* AdapterException::what() const throw()
{
    return m_msg.c_str();
}

// AdapterWin.cpp
#include "Adapter.hpp"
#include "Tools.hpp"

// Config
namespace config::adapter
{
    static const int memorySize = 20 * 1025; // 20ko
}

// AdapterParams
AdapterParams::AdapterParams()
    :size(config::adapter::memorySize)
{
}

AdapterParams::~AdapterParams()
{
    if (adapters)
    {
        free(adapters);
    }

    for (auto* adapterName : adapterNameList)
    {
        delete adapterName;
    }

    for (auto* addressName : addressNameList)
    {
        delete addressName;
    }
}

// Adapter
bool Adapter::initSocket() const
{
    WSADATA d;

    if (WSAStartup(MAKEWORD(2, 2), &d))
    {
        printf("initSocket() failed.\n");
        return false;
    }
    return true;
}

void Adapter::cleanSocket() const
{
    WSACleanup();
}

bool Adapter::loadAdapters(AdapterParams& _params) const
{
    _params.adapters = (PIP_ADAPTER_ADDRESSES)malloc(config::adapter::memorySize);
    if (!_params.adapters)
    {
        fprintf(stderr, "loadAdapters(1) failed.|size=%ld\n", _params.size);
        return false;

    }

    int adpaterResult = GetAdaptersAddresses(AF_UNSPEC, GAA_FLAG_INCLUDE_PREFIX, 0, _params.adapters, &_params.size);
    if (adpaterResult == ERROR_BUFFER_OVERFLOW)
    {
        fprintf(stderr, "loadAdapters(2) failed.|size=%ld\n", _params.size);
        return false;
    }

    PIP_ADAPTER_ADDRESSES adapter = _params.adapters;
    while (adapter)
    {
        AdapterNameParams* adapterName = _params.addAdapterName();
        adapterName->name = oTools.toString(adapter->FriendlyName);

        PIP_ADAPTER_UNICAST_ADDRESS address = adapter->FirstUnicastAddress;
        while (address)
        {
            int family = address->Address.lpSockaddr->sa_family;

            if (family == AF_INET || family == AF_INET6)
            {
                AdapterAddressParams* addressName = _params.addApaterAddress(adapterName);

                addressName->family = (family == AF_INET) ? "IPv4" : "IPv6";

                char addressIP[100];

                getnameinfo(address->Address.lpSockaddr,
                    address->Address.iSockaddrLength,
                    addressIP, sizeof(addressIP), 0, 0, NI_NUMERICHOST);

                addressName->address = addressIP;
            }

            address = address->Next;
        }

        adapter = adapter->Next;
    }

    return true;
}

// AdapterUnix.cpp
#include "Adapter.hpp"

// AdapterParams
AdapterParams::AdapterParams()
{
}

AdapterParams::~AdapterParams()
{
    if (addresses)
    {
        freeifaddrs(addresses);
    }

    for (auto* adapterName : adapterNameList)
    {
        delete adapterName;
    }

    for (auto* addressName : addressNameList)
    {
        delete addressName;
    }
}

// Adapter
bool Adapter::initSocket() const
{
    return true;
}

void Adapter::cleanSocket() const
{
}

bool Adapter::loadAdapters(AdapterParams& _params) const
{
    if (getifaddrs(&_params.addresses) == -1)
    {
        fprintf(stderr, "loadAdapters() failed.\n");
        return false;
    }

    struct ifaddrs* address = _params.addresses;
    while (address)
    {
        if (address->ifa_addr == nullptr)
        {
            address = address->ifa_next;
            continue;
        }

        int family = address->ifa_addr->sa_family;

        if (family == AF_INET || family == AF_INET6)
        {
            AdapterNameParams* adapterName;

            if (!_params.getAdapterName(&adapterName, address->ifa_name))
            {
                adapterName = _params.addAdapterName();
                adapterName->name = address->ifa_name;
            }

            AdapterAddressParams* addressName = _params.addApaterAddress(adapterName);
            addressName->family = (family == AF_INET) ? "IPv4" : "IPv6";

            char ap[100];
            const int family_size = (family == AF_INET) ?
                sizeof(struct sockaddr_in) : sizeof(struct sockaddr_in6);
            getnameinfo(
                address->ifa_addr, family_size, ap, sizeof(ap), 0, 0, NI_NUMERICHOST);
            addressName->address = ap;
        }

        address = address->ifa_next;
    }

    return true;
}

Développement du manager des outils


Tools.hpp
#pragma once

// Windows
#if defined (_WIN32)
#ifndef _WIN32_WINNT
#define _WIN32_WINNT 0x0600
#endif
#include <windows.h>
#endif

// Common
#include <string>

#define oTools Tools::Instance()

class Tools
{
private:
    explicit Tools();

public:
    ~Tools();
    static Tools& Instance();
    std::string toString(const wchar_t* _data) const;
};

Tools.cpp
#include "Tools.hpp"

Tools::Tools()
{
}

Tools::~Tools()
{
}

Tools& Tools::Instance()
{
    static Tools instance;
    return instance;
}

// string
std::string Tools::toString(const wchar_t* _data) const
{
    std::wstring ws(_data);
    return std::string(ws.begin(), ws.end());
}

Développement de la configuration CMake


// CMakeLists.txt
cmake_minimum_required(VERSION 3.10.0)
project(c01-adapter-list VERSION 0.1.0 LANGUAGES C CXX)

set(SRC_FILES
    main.cpp
    Adapter.hpp
    Adapter.cpp
    Tools.hpp
    Tools.cpp
)

if(WIN32)
    list(APPEND SRC_FILES
        AdapterWin.cpp
    )
else()
    list(APPEND SRC_FILES
        AdapterUnix.cpp
    )
endif()

add_executable(${PROJECT_NAME}
    ${SRC_FILES}
)

Test sur l'affichage des cartes réseau


Nous avons pu afficher la liste des cartes réseau sur les systèmes Windows.

// Terminal
image.png
image.png

Nous avons pu afficher la liste des cartes réseau sur les systèmes Linux ou MacOS.

// Terminal
image.png


Se familiariser avec les API Socket


Nous aborderons les bases de l'utilisation des sockets pour la programmation réseau. Nous découvrirons qu'il existe de nombreuses différences entre les sockets Berkeley (utilisées sur les systèmes d'exploitation de type Unix) et les sockets Winsock (utilisées sous Windows). Nous atténuerons ces différences grâce à des instructions de préprocesseur. Nous écrirons un programme C/C++ qui compile sans problème sous Windows, Linux et macOS. Nous comprendrons en quoi le protocole UDP est un protocole sans connexion et expliquerons ce que cela implique. Nous apprendrons que le protocole TCP est un protocole orienté connexion qui offre certaines garanties de fiabilité, comme la détection et le renvoi automatiques des paquets perdus. Nous constaterons que le protocole UDP est souvent utilisé pour les protocoles simples (par exemple, le DNS) et pour les applications de diffusion en continu en temps réel. Nous verrons que le protocole TCP est utilisé pour la plupart des autres protocoles. Nous travaillerons sur un exemple concret en convertissant une application console en serveur web. Nous apprendrons à écrire le programme C/C++ en utilisant la fonction (getaddrinfo), et comprendrons pourquoi cela est important pour rendre le programme compatible IPv4/IPv6. Nous utiliserons les fonctions (bind, listen, et accept) sur le serveur pour attendre une connexion entrante du navigateur web. Les données seront ensuite lues depuis le client à l'aide de la fonction (recv), et une réponse sera envoyée à l'aide de la fonction (send). Enfin, nous fermerons la connexion avec la fonction (close) sous Unix, et la fonction (closesocket) sous Windows. Nous construirons un serveur web pour afficher la date et l'heure courantes à partir d'un navigateur web.


Présentation approfondie des connexions TCP


Nous aborderons le protocole TCP qui constitue véritablement l'épine dorsale de l'expérience Internet moderne. Nous comprendrons que le protocole TCP est utilisé par HTTP (HyperText Transfer Protocol), le protocole qui alimente les sites web, et par SMTP (Simple Mail Transfer Protocol), le protocole qui alimente la messagerie électronique. Nous découvrirons que la création d'un client TCP est relativement simple et montrerons que la seule difficulté réside dans la surveillance simultanée des entrées du terminal local et des données transitant par le socket. Nous verrons comment surmonter cette difficulté en utilisant la fonction (select) sur les systèmes Windows et Unix. Nous constaterons que de nombreuses applications concrètes n'ont pas besoin de surveiller les entrées du terminal et que cette étape n'est donc pas toujours nécessaire. Nous montrerons que la création d'un serveur TCP adapté à de nombreuses connexions parallèles n'est pas beaucoup plus difficile. Nous verrons comment utiliser la fonction (select) qui s'avère extrêmement utile pour gérer les connexions multiples en permettant de surveiller facilement le socket d'écoute pour les nouvelles connexions, tout en surveillant les connexions existantes pour les nouvelles données. Nous aborderons certains problèmes courants, par exemple, le protocole TCP ne fournit pas de méthode native pour partitionner les données. Nous verrons que pour surmonter cette problématique, pour les protocoles plus complexes où cela est nécessaire, il faudra mettre en mémoire tampon les données reçues par la fonction (recv) jusqu'à ce qu'une quantité suffisante soit disponible pour l'interprétation. Nous constaterons que pour les pairs TCP qui traitent de grandes quantités de données, la mise en mémoire tampon pour la fonction (send) est également nécessaire.


Établissement de connexions UDP


Nous verrons que la programmation avec les sockets UDP est plus simple qu'avec les sockets TCP. Nous apprendrons que les sockets UDP n'ont pas besoin des appels de fonction (listen), (accept) ou (connect). Ceci est principalement dû au fait que les fonction (sendto) et (recvfrom) gèrent directement les adresses. Pour les programmes plus complexes, nous pourrons toujours utiliser la fonction (select) pour voir quels sockets sont prêts pour les E/S. Nous découvrirons que les sockets UDP sont sans connexion. Ceci contraste avec les sockets TCP orientés connexion. Avec TCP, nous devions établir une connexion avant d'envoyer des données, tandis qu'avec UDP, nous envoyons simplement des paquets individuels directement à une adresse de destination. Cela simplifie la programmation des sockets UDP, mais peut complexifier la conception des protocoles d'application, et UDP ne gère pas automatiquement les échecs de communication ni ne garantit l'arrivée des paquets dans l'ordre.





Résolution de noms d'hôtes et DNS


Nous aborderons les noms d'hôtes et les requêtes DNS. Nous découvrirons le fonctionnement du DNS et apprendrons que la résolution d'un nom d'hôte peut impliquer l'envoi de nombreux paquets UDP sur le réseau. Nous examinerons plus en détail la fonction (getaddrinfo) et montrerons pourquoi elle est généralement la méthode privilégiée pour effectuer une recherche de nom d'hôte. Nous étudierons également sa fonction soeur, (getnameinfo), capable de convertir une adresse en texte ou même d'effectuer une requête DNS inverse. Enfin, nous implémenterons un programme envoyant des requêtes DNS. Ce programme constituera une excellente expérience d'apprentissage pour mieux comprendre le protocole DNS et nous permettra d'acquérir de l'expérience dans l'implémentation d'un protocole binaire. Lors de l'implémentation d'un protocole binaire, nous devrons porter une attention particulière à l'ordre des octets. Pour le format simple des messages DNS, cela sera réalisé en interprétant soigneusement les octets un par un.



Création d'un client web simple


Nous examinerons le format des messages HTTP. Nous implémenterons ensuite un programme en C capable de demander et de recevoir des pages Web. Nous découvrirons les types de requêtes HTTP. Nous explorerons les en-têtes HTTP courants. Nous analyserons le code de réponse HTTP. Nous verrons l'encodage des données de formulaire (POST). Nous aborderons le téléchargement de fichiers HTTP.



Construire un serveur web simple


Nous aborderons le protocole HTTP du point de vue du serveur. Nous y construirons un serveur web simple. Ce serveur fonctionnera avec le protocole HTTP et nous pourrons nous y connecter avec n'importe quel navigateur web standard. Bien qu'il ne soit pas complet, il conviendra parfaitement pour servir quelques fichiers statiques en local. Il pourra gérer plusieurs connexions simultanées de différents clients. Nous découvrirons l'acceptation et la mise en mémoire tampon de plusieurs connexions. Nous analyserons une ligne de requête HTTP. Nous explorerons le formatage d'une réponse HTTP. Nous montrerons comment servir un fichier. Nous prendrons en considération les notions de sécurité.



Mettre votre programme en route pour l'envoi d'e-mails


Nous aborderons le protocole assurant la distribution des courriels sur Internet. Ce protocole est appelé le protocole de transfert de courrier simple (SMTP). Nous expliquerons le fonctionnement interne du transfert de courriels. Nous construirons un client SMTP simple capable d'envoyer des courriels courts. Nous identifierons le serveur de messagerie responsable à un domaine donné. Nous découvrirons la protection contre le spam et pièges liés à l'envoi de courriels.





Charger des pages web sécurisées avec HTTPS et OpenSSL


Nous apprendrons à établir des connexions sécurisées aux serveurs web à l'aide du protocole HTTPS (HyperText Transfer Protocol Secure). HTTPS offre plusieurs avantages par rapport à HTTP. Il fournit une méthode d'authentification permettant d'identifier les serveurs et de détecter les usurpateurs d'identité. Il protège également la confidentialité de toutes les données transmises et empêche les intercepteurs de les altérer ou de les falsifier. En HTTPS, la communication est sécurisée par le protocole TLS (Transport Layer Security). Nous apprendrons à utiliser la bibliothèque OpenSSL pour implémenter les fonctionnalités TLS. Nous aborderons les informations générales sur HTTPS. Nous découvrirons les types de chiffrements et l'authentification des serveurs. Nous aborderons une utilisation de base d'OpenSSL et créerons un client HTTPS simple.


Implémentation d'un serveur web sécurisé


Nous créerons un serveur HTTPS simple. Il servira de composante au client HTTPS présenté dans le section précédente. Le protocole HTTPS repose sur le protocole TLS (Transport Layer Security). Contrairement aux clients HTTPS, les serveurs HTTPS doivent s'authentifier à l'aide de certificats. Nous verrons comment écouter les connexions HTTPS, fournir des certificats et envoyer une réponse HTTP via TLS. Nous aborderons la présentation du protocole HTTPS. Nous découvrirons les certificats HTTPS et la configuration d'un serveur HTTP avec OpenSSL. Nous apprendrons l'acceptation des connexions HTTPS.





Établissement de connexions SSH avec libssh


Nous aborderons la programmation avec le protocole Secure Shell (SSH). SSH est un protocole réseau sécurisé utilisé pour s'authentifier auprès de serveurs distants, accorder un accès en ligne de commande et transférer des fichiers en toute sécurité. SSH est largement utilisé pour la configuration et la gestion de serveurs distants. Souvent, les serveurs web ne sont pas connectés à des écrans ou à des claviers. Pour nombre de ces serveurs, SSH constitue la seule méthode d'accès en ligne de commande et d'administration. Nous présenterons le protocole SSH et la bibliothèque libssh. Nous découvrirons l'établissement d'une connexion et les méthodes d'authentification SSH. Nous analyserons l'exécution d'une commande distante et le transferts de fichiers via SSH.


Surveillance et sécurité du réseau


Nous examinerons les outils et techniques courants de surveillance réseau. Ces techniques pourront s'avérer utiles pour nous alerter de l'apparition de problèmes et pour résoudre les problèmes existants. La surveillance réseau est importante du point de vue de la sécurité, car elle permet de détecter, d'enregistrer, voire de prévenir les intrusions. Nous aborderons la vérification de l'accessibilité des hôtes et l'affichage d'un itinéraire de connexion. Nous découvrirons l'affichage des ports ouverts et la liste des connexions ouvertes. Nous analyserons le trafic réseau, le pare-feu et le filtrage des paquets.





Conseils et pièges de la programmation par socket


Nous reviendrons sur toutes les connaissances acquises dans ce document. La programmation de sockets peut s'avérer complexe. De nombreux pièges sont à éviter et des techniques de programmation subtiles doivent être mises en oeuvre. Nous aborderons certains détails nuancés de la programmation réseau, essentiels à l'écriture de programmes robustes. Nous découvrirons la gestion et la description des erreurs. Nous analyserons les échanges TCP et la libération ordonnée. Nous explorerons le délai d'attente de la fonction (connect) et la prévention des blocages TCP. Nous verrons le contrôle de flux TCP et la prévention des erreurs d'adresse déjà utilisée. Nous apprendrons la prévention des plantages SIGPIPE et les limitations du multiplexage de la fonction (select).


Programmation Web pour l'Internet des Objets IoT


Nous nous intéresserons à l'Internet des objets (IoT). L'IoT est une nouvelle tendance passionnante qui consiste à ajouter une connectivité Internet aux objets physiques du quotidien. Associé à des composants électroniques et des capteurs embarqués, l'accès à Internet permet aux objets physiques d'interagir entre eux, d'être contrôlés et surveillés depuis n'importe où dans le monde. Nous aborderons la définition de l'IoT et les types de connectivité. Nous découvrirons les considérations relatives à la bande passante et les types de contrôleurs. Nous analyserons l'éthique de l'IoT et la sécurité de l'IoT.