Automatisation des réservations ferroviaires en C# via simulation HTTP

Pour interagir programmatiquement avec un site web comme une plateforme de billetterie ferroviaire, il est indispensable de maîtriser les mécanismes HTTP. Les requêtes transitent principalement via deux verbes : GET pour la récupération de données et POST pour la soumission de formulaires. Chaque session repose sur un système de cookies qui maintient l'état d'authentification entre les échanges client-serveur.

Analyse du processus d'authentification

L'endpoint d'authentification se trouve à l'adresse suivante :

https://kyfw.12306.cn/otn/login/loginAysnSuggest

Le formulaire transmis comprend trois champs principaux : l'identifiant utilisateur, le mot de passe en clair, ainsi qu'un code de vérification captcha. Ce dernier est fourni sous forme d'image statique que l'utilisateur doit saisir manuellement.

Classe utilitaire pour les requêtes HTTP

Voici une classe rgeroupant les méthodes essentielles pour effectuer des appels HTTP, récupérer du contenu web et télécharger des images de vérification :

public class HttpClientHelper
{
    private const string DefaultContentType = "application/x-www-form-urlencoded";
    private const string DefaultAccept = "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8";
    private const string DefaultUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
    private const string BaseReferer = "https://kyfw.12306.cn/";

    public static string SendPostRequest(string targetUrl, CookieContainer sessionCookies, string formPayload)
    {
        byte[] payloadBytes = Encoding.UTF8.GetBytes(formPayload);
        var request = (HttpWebRequest)WebRequest.Create(targetUrl);
        request.CookieContainer = sessionCookies;
        request.ContentType = DefaultContentType;
        request.Accept = DefaultAccept;
        request.UserAgent = DefaultUserAgent;
        request.Method = "POST";
        request.ContentLength = payloadBytes.Length;
        request.Referer = BaseReferer;

        using (var writer = request.GetRequestStream())
        {
            writer.Write(payloadBytes, 0, payloadBytes.Length);
        }

        using (var response = (HttpWebResponse)request.GetResponse())
        using (var reader = new StreamReader(response.GetResponseStream(), Encoding.UTF8))
        {
            return reader.ReadToEnd();
        }
    }

    public static string FetchPageContent(string targetUrl, CookieContainer sessionCookies)
    {
        var request = (HttpWebRequest)WebRequest.Create(targetUrl);
        request.CookieContainer = sessionCookies;
        request.Method = "GET";
        request.Accept = DefaultAccept;
        request.UserAgent = DefaultUserAgent;
        request.Referer = BaseReferer;

        using (var response = (HttpWebResponse)request.GetResponse())
        using (var reader = new StreamReader(response.GetResponseStream(), Encoding.UTF8))
        {
            return reader.ReadToEnd();
        }
    }

    public static Stream DownloadImage(string imageUrl, CookieContainer sessionCookies)
    {
        var request = (HttpWebRequest)WebRequest.Create(imageUrl);
        request.CookieContainer = sessionCookies;
        request.UserAgent = DefaultUserAgent;
        request.Accept = "image/webp,*/*;q=0.8";
        request.Referer = BaseReferer;
        request.KeepAlive = true;

       WebResponse response = request.GetResponse();
        return response.GetResponseStream();
    }
}

Connexion au service

Pour s'authentifier, on soumet les identifiants via une requête POST :

string authPayload = $"loginUserDTO.user_name={credentials.Login}&userDTO.password={credentials.Password}&randCode={credentials.Captcha}";
string authResult = HttpClientHelper.SendPostRequest(Endpoints.AuthEndpoint, sessionCookies, authPayload);

La réponse JSON contient un indicateur loginCheck avec la valeur "Y" si l'authentification réussit. En cas d'échec, le message d'erreur est retourné dans un tableau message. Il est impératif de conserver le conteneur de cookies après une connexion réussie, car il porte le jeton de session pour toutes les requêtes ultérieures.

Récupération des données de gares

La liste des gares et leurs codes est disponible dans un fichier JavaScript :

https://kyfw.12306.cn/otn/resources/js/framework/station_name.js

Les données sont séparées par le caractère |. Chaque enregistrement de gare occupe cinq champs successifs. Voici une méthode pour extraire les noms et codes des gares :

Dictionary<string, string> stationLookup = new Dictionary<string, string>();
string rawStationData = FetchPageContent(stationJsUrl, sessionCookies);
string cleanData = rawStationData.Split('=')[1].Replace("'", "").Replace(";", "");
string[] fields = cleanData.Split('|');

for (int idx = 0; idx < fields.Length; idx += 5)
{
    if (idx + 4 < fields.Length)
    {
        string stationName = fields[idx + 1];
        string stationCode = fields[idx + 2];
        stationLookup[stationName] = stationCode;
    }
}

Chargement des passagers enregistrés

Les informations des voyageurs sont récupérées depuis l'endpoint suivant :

https://kyfw.12306.cn/otn/confirmPassenger/getPassengerDTOs

La réponse JSON détaille chaque passager avec son numéro d'identité, numéro de téléphone, catégorie (adulte, étudiant), et d'autres métadonnées nécessaires à la réservation.

Consultation des disponibilités

Pour vérifier les places restantes, on interroge l'API avec les paramètres de trajet :

string availabilityUrl = string.Format(Endpoints.TicketAvailability, departureDate, originCode, destinationCode);
string availabilityJson = HttpClientHelper.FetchPageContent(availabilityUrl, sessionCookies);

La réponse contient pour chaque train des informations comme le secretStr (identifiant chiffré du trajet), les horaires, et le nombre de places par classe (couchettes molles, sièges durs, etc.).

Soumission de la commande

Une fois le trajet sélectionné, on initie la commande :

string orderPayload = $"secretStr={HttpUtility.UrlEncode(train.SecretKey)}" +
    $"&train_date={train.DepartureDate}" +
    $"&back_train_date={train.ReturnDate}" +
    $"&tour_flag=dc&purpose_codes=ADULT" +
    $"&query_from_station_name={train.OriginStation}" +
    $"&query_to_station_name={train.DestinationStation}";

HttpClientHelper.SendPostRequest(Endpoints.OrderInitiation, sessionCookies, orderPayload);

Récupération du jeton de soumission

Le token de sécurité, nécessaire pour finaliser la transaction, est extrait de la page d'initialisation :

string initPageResponse = HttpClientHelper.SendPostRequest(Endpoints.OrderConfirmInit, sessionCookies, "_json_att=");
// Extraire REPEAT_SUBMIT_TOKEN, key_check_isChange, leftTicketStr, train_location depuis la réponse HTML

Construction des paramètres passagers

Les chaînes passengerTicketStr et oldPassengerStr sont construites selon un format spécifique. Chaque voyageur est séparé par un underscore. Les champs incluent le code de classe de voyage, le nom du passager, son numéro d'identité et son téléphone, avec encodage URL pour les caractères spéciaux.

Vérification avant paiement

Un captcha supplémentaire est requis avant la validation finale :

Stream captchaStream = HttpClientHelper.DownloadImage(Endpoints.OrderCaptchaUrl, sessionCookies);
picCaptchaBox.Image = Image.FromStream(captchaStream);

Ensuite, on soumet l'ordre pour vérification :

string checkPayload = "cancel_flag=2&bed_level_order_num=000000000000000000000000000000" +
    $"&{passengerData}&{returnPassengerData}" +
    $"&tour_flag=dc&randCode={userCaptcha}" +
    $"&_json_att=&REPEAT_SUBMIT_TOKEN={sessionToken}";

string checkResult = HttpClientHelper.SendPostRequest(Endpoints.OrderVerification, sessionCookies, checkPayload);

Confirmation finale de la commande

L'étape décisive consiste à confirmer la réservation :

string confirmPayload = $"{passengerData}&{returnPassengerData}" +
    $"&randCode={userCaptcha}&purpose_codes=00" +
    $"&key_check_isChange={securityKey}" +
    $"&leftTicketStr={ticketSignature}" +
    $"&train_location={trainLocation}" +
    $"&_json_att=&REPEAT_SUBMIT_TOKEN={sessionToken}";

string confirmResult = HttpClientHelper.SendPostRequest(Endpoints.ConfirmOrder, sessionCookies, confirmPayload);

Suivi de l'état de la commande

Enfin, on interroge périodiquement l'endpoint de statut pour obtenir le numéro de commande :

string statusUrl = string.Format(Endpoints.OrderStatus, pollCounter, sessionToken);
string statusResponse = HttpClientHelper.FetchPageContent(statusUrl, sessionCookies);
// Analyser la réponse JSON pour la présence d'un orderId non nul

Lorsque le champ orderId dans la réponse JSON est supérieur à zéro, la réservation est effectuée avec succès. Il ne reste plus qu'à procéder au paiement dans les délais impartis.

Étiquettes: C# HttpWebRequest CookieContainer JSON web scraping

Publié le 24 juillet à 04h29