Оболочка Google API

Играем с Google API.

Я написал демонстрационные приложения для использования API Google Gmail. Но на самом деле я ищу обзор оболочек C ++ для аутентификации (OAuth2) и начальной оболочки GMail. Хочу внести свой вклад, прежде чем я начну двигаться дальше.

Пожалуйста, не торопитесь, чтобы указать на все даже малейшие проблемы с кодом (у меня толстая кожа). НО также интересно, может ли кто-нибудь придумать лучшие способы разработки интерфейсов. Как вы ожидаете использовать объекты из Google (нам не нужно сопоставлять REST API с методом класса, я бы хотел спроектировать систему, взаимодействующую с объектами на уровне кода, это интуитивно понятно).

CurlHelper.h

#ifndef THROSANVIL_CURL_CURLHELPER_H
#define THROSANVIL_CURL_CURLHELPER_H

#include <curl/curl.h>
#include <memory>
#include <functional>
#include <string>

namespace ThorsAnvil::Curl
{
    using CurlDeleter = std::function<void(CURL*)>;
    using CurlHDeleter= std::function<void(curl_slist*)>;

    class CurlUniquePtr: public std::unique_ptr<CURL, CurlDeleter>
    {
        public:
            CurlUniquePtr(std::string const& url)
                : std::unique_ptr<CURL, CurlDeleter>{curl_easy_init(), [](CURL* c){curl_easy_cleanup(c);}}
            {
                if (get() == nullptr) {
                    throw std::runtime_error("Failed to Create Curl Handle");
                }
                if (curl_easy_setopt(get(), CURLOPT_URL, url.c_str()) != CURLE_OK) {
                    throw std::runtime_error("Could not set a valid URL");
                }
            }
            operator CURL*() {
                return get();
            }
    };
    class CurlHeaderUniquePtr: public std::unique_ptr<curl_slist, CurlHDeleter>
    {
        public:
            CurlHeaderUniquePtr(std::initializer_list<std::string> const& list = std::initializer_list<std::string>{})
                : std::unique_ptr<curl_slist, CurlHDeleter>{nullptr, [](curl_slist* s){curl_slist_free_all(s);}}
            {
                for (auto const& head: list) {
                    append(head);
                }
            }

            void append(std::string const& header)
            {
                curl_slist* tmp = curl_slist_append(get(), header.c_str());
                if (tmp) {
                    release();
                    reset(tmp);
                }
            }
    };
}

#endif

Credentials.h

#ifndef THORSANVIL_GOOGLE_CREDENTIALS_H
#define THORSANVIL_GOOGLE_CREDENTIALS_H

#include "CurlHelper.h"

#include "ThorSerialize/Traits.h"
#include "ThorSerialize/SerUtil.h"
#include "ThorSerialize/JsonThor.h"

#include <string>
#include <vector>
#include <fstream>
#include <sstream>

extern "C" size_t outputToStream(char* ptr, size_t size, size_t nmemb, void* userdata)
{
    std::iostream& stream = *(reinterpret_cast<std::iostream*>(userdata));
    stream.write(ptr, size * nmemb);
    return size * nmemb;
}
extern "C" size_t dropHeaders(char* ptr, size_t size, size_t nmemb, void* userdata)
{
    // Drop and ignore headers.
    // Otherwise they clutter the standard output.
    return size * nmemb;
}


namespace ThorsAnvil::Google
{

struct ServiceAccount
{
    std::string     client_id;
    std::string     project_id;
    std::string     auth_uri;
    std::string     token_uri;
    std::string     auth_provider_x509_cert_url;
    std::string     client_secret;
    std::vector<std::string>    redirect_uris;
};

struct ApplicaionInfo
{
    ServiceAccount  installed;
    ApplicaionInfo(std::istream& stream)
    {
        namespace TAS=ThorsAnvil::Serialize;
        stream >> TAS::jsonImporter(*this, TAS::ParserInterface::ParserConfig{TAS::JsonParser::ParseType::Strict, false});
    }

    std::string getManualAuthURL(std::string const& scope)
    {
        // Find URL
        std::size_t uriIndix = 0;
        for (;uriIndix < installed.redirect_uris.size(); ++uriIndix) {
            if (installed.redirect_uris[uriIndix].substr(0,4) == "urn:") {
                break;
            }
        }
        if (uriIndix >= installed.redirect_uris.size()) {
            throw std::runtime_error("Failed to find URN");
        }

        std::stringstream oauth2URLStream;
        oauth2URLStream << "https://accounts.google.com/o/oauth2/v2/auth"
                        << "?client_id=" <<  installed.client_id
                        << "&redirect_uri=" << installed.redirect_uris[uriIndix]
                        << "&response_type=code"
                        << "&scope=" << scope;

        return oauth2URLStream.str();
    }

    void createCredentialFile(std::string const& credFileName, std::string const& token)
    {
        namespace TAC=ThorsAnvil::Curl;
        namespace TAS=ThorsAnvil::Serialize;

        // Find URL
        std::size_t uriIndix = 0;
        for (;uriIndix < installed.redirect_uris.size(); ++uriIndix) {
            if (installed.redirect_uris[uriIndix].substr(0,4) == "urn:") {
                break;
            }
        }
        if (uriIndix >= installed.redirect_uris.size()) {
            throw std::runtime_error("Failed to find URN");
        }

        // Convert token into OAuth2 credentials
        // Part 1: Build Request
        std::stringstream requestBodyStream;
        requestBodyStream << "client_id=" + installed.client_id
                          << "&client_secret=" + installed.client_secret
                          << "&grant_type=authorization_code"
                          << "&redirect_uri=" + installed.redirect_uris[uriIndix]
                          << "&code=" + token;

        // Open a scope so we can create an output file stream to credStore.
        // Closed at the end of this scope.
        std::string     requestBody = requestBodyStream.str();
        std::fstream    credStore(credFileName, std::ios_base::out);

        // Part 2: Send request to OAuth server
        TAC::CurlUniquePtr          curl("https://oauth2.googleapis.com/token");
        TAC::CurlHeaderUniquePtr    headerList{{"Content-Type: application/x-www-form-urlencoded"}};

        curl_easy_setopt(curl, CURLOPT_POST, 1L);
        curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headerList.get());
        curl_easy_setopt(curl, CURLOPT_POSTFIELDS, requestBody.c_str());
        curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, requestBody.size());
        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, outputToStream);
        curl_easy_setopt(curl, CURLOPT_WRITEDATA, &static_cast<std::iostream&>(credStore));
        curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, dropHeaders);

        CURLcode code  = curl_easy_perform(curl);
        if (code != CURLE_OK) {
            throw std::runtime_error("Failed to get OAuth2 credentials");
        }
    }
};

struct OAuth2Creds
{
    std::string     access_token;
    std::size_t     expires_in;
    std::string     refresh_token;
    std::string     scope;
    std::string     token_type;

    OAuth2Creds(std::istream& stream)
    {
        namespace TAS=ThorsAnvil::Serialize;
        stream >> TAS::jsonImporter(*this, TAS::ParserInterface::ParserConfig{TAS::JsonParser::ParseType::Strict, false});
    }

    void refresh(ApplicaionInfo& application)
    {
        namespace TAC=ThorsAnvil::Curl;
        namespace TAS=ThorsAnvil::Serialize;

        TAC::CurlUniquePtr   refreshRequest("https://oauth2.googleapis.com//token");
        curl_easy_setopt(refreshRequest, CURLOPT_POST, 1L);

        std::stringstream bodyStream;
        bodyStream  << "client_id=" << application.installed.client_id
                    << "&client_secret=" << application.installed.client_secret
                    << "&refresh_token=" << refresh_token
                    << "&grant_type=refresh_token";
        std::string body(std::move(bodyStream.str()));
        curl_easy_setopt(refreshRequest, CURLOPT_POSTFIELDS, body.c_str());
        curl_easy_setopt(refreshRequest, CURLOPT_POSTFIELDSIZE, body.size());

        std::stringstream stream;
        curl_easy_setopt(refreshRequest, CURLOPT_WRITEFUNCTION, outputToStream);
        curl_easy_setopt(refreshRequest, CURLOPT_WRITEDATA, &static_cast<std::iostream&>(stream));


        CURLcode code  = curl_easy_perform(refreshRequest);
        if (code != CURLE_OK) {
            throw std::runtime_error("Failed to refresh OAuth2 credentials");
        }
};

}

ThorsAnvil_MakeTrait(ThorsAnvil::Google::OAuth2Creds, access_token, expires_in, refresh_token, scope, token_type);
ThorsAnvil_MakeTrait(ThorsAnvil::Google::ServiceAccount, client_id, project_id, auth_uri, token_uri, auth_provider_x509_cert_url, client_secret, redirect_uris);
ThorsAnvil_MakeTrait(ThorsAnvil::Google::ApplicaionInfo, installed);

#endif

GoogleMail.h

#ifndef THORSANVIL_GOOGLE_MAIL_H
#define THORSANVIL_GOOGLE_MAIL_H

#include "ThorSerialize/Traits.h"
#include "ThorSerialize/SerUtil.h"
#include "ThorSerialize/JsonThor.h"

#include <string>
#include <vector>

namespace ThorsAnvil::Google
{

struct Header
{
    std::string name;
    std::string value;
};

struct MessagePartBody
{
    std::string     attachmentId;
    std::size_t     size;
    std::string     data;
};

struct MessagePart
{
    using Headers = std::vector<Header>;
    using MessageParts = std::vector<MessagePart>;

    std::string     partId;
    std::string     mimeType;
    std::string     filename;
    Headers         headers;
    MessagePartBody body;
    MessageParts    parts;
};

struct Message
{
    using Labels = std::vector<std::string>;

    std::string     id;
    std::string     threadId;
    Labels          labelIds;
    std::string     snippet;
    MessagePart     payload;
    std::size_t     sizeEstimate;
    std::string     raw;
};

struct Draft
{
    std::string     id;
    Message         message;
};

}

ThorsAnvil_MakeTrait(ThorsAnvil::Google::Header, name, value);
ThorsAnvil_MakeTrait(ThorsAnvil::Google::MessagePartBody, attachmentId, size, data);
ThorsAnvil_MakeTrait(ThorsAnvil::Google::MessagePart, partId, mimeType, filename, headers, body, parts);
ThorsAnvil_MakeTrait(ThorsAnvil::Google::Message, id, threadId, labelIds, snippet, payload, sizeEstimate, raw);
ThorsAnvil_MakeTrait(ThorsAnvil::Google::Draft, id, message);

#endif

auth.cpp

#include "Credentials.h"
#include "CurlHelper.h"

#include <iostream>
#include <fstream>
#include <iterator>

namespace TAG=ThorsAnvil::Google;
namespace TAC=ThorsAnvil::Curl;

void printIntructions()
{
        std::cout   << R"Instructions(
First Run: You need to manually authorize with Google to get an OAuth2 token.

If you already have an Account/Application/OAuth2 tokens set up you can reuse them
These instructions assume you have not set up anything yet and are starting fresh
feel free to skip instructions that you have already completed before

Step 0: Set up a google developer account
        A: https://console.cloud.google.com/
        B: Sign in with your normal google credentials
Step 1: Create an Application in Google console
        A: Open: https://console.cloud.google.com/home/dashboard
        B: Click on the Burger top left
        C: From the DropDown Menu Select: 'IAM & Admin/Create a Project'
            C1: Name the App Click Create.
Step 2: Create OAuth2 Credential
        A: Click on the Burger top left
        B: From the DropDown Menu Select: 'APIs & Services/Credentials'
        C: Click the button '+ Create Credentials'
        D: From the DropDown Menu Select: 'OAuth client ID'
            D1: Make the 'Application Type' a 'Desktop app'
            D2: Enter the Application Name
            D2: Click 'Create'
            D3: This brings you back to the 'Credentials Screen'
            D4: Clock the 'Download Arrow (on the right) next to your OAuth2 Client
Step 3: Locate the Key File.
        A: Step 3 should have downloaded a file your computer.
        B: Locate this file and put it somewhere safe and note the absolute address of this file
        C: Enter the Full path of this file below when asked
Step 4: Enable API
        A: Click on the Burger top left
        B: From the DropDown Menu Select: 'APIs & Services/Library'
        C: Search for the API you want (gmail)
        D: Select the API. Then Click 'Enable'
Step 5: Decide the scope of you application
        A: Goto https://developers.google.com/identity/protocols/oauth2/scopes
        B: Locate the scope you want to give this application: (https://www.googleapis.com/auth/gmail.compose)
        C: Enter this value blow when asked
)Instructions";
}

void oauth2()
{
    std::ifstream   credStore("creds.json");
    if (!credStore) {
        printIntructions();

        // Get the Key File
        std::cout << "nnPlease enter path of key filen";
        std::string keyFilePath;
        std::getline(std::cin, keyFilePath);

        std::ifstream keyFileStream(keyFilePath);
        TAG::ApplicaionInfo  keyFile(keyFileStream);

        // Get the Scope Info
        std::cout << "nnPlease enter the scope for the OAuth2 tokenn";
        std::string scope;
        std::getline(std::cin, scope);

        // Request the user authorize the App.
        std::string oauth2URL = keyFile.getManualAuthURL(scope);
        std::cout   << "nnPlease authorize this applicationn"
                    << "Open the following URL in a browser and follow the instructions.n"
                    << "Once complete copy and paste the token into the string below.n"
                    << "n"
                    << "Open: " << oauth2URL << "n";
        // Get Temp Token:
        std::cout << "nnPlease input token generated by OAuth2n";
        std::string token;
        std::getline(std::cin, token);

        keyFile.createCredentialFile("creds.json", token);

        // Now that we have downloaded the creds.
        // Try to open the file again. So we can attempt to load it.
        credStore.open("creds.json");
        if (!credStore) {
            throw std::runtime_error("Failed to open creds.json");
        }
    }

    // If this loads without an exception we have some credentials.
    TAG::OAuth2Creds       oathCreds(credStore);
}

int main()
{
    try {
        oauth2();
    }
    catch(std::exception const& e) {
        std::cout << "Error: " << e.what() << "n";
        throw;
    }
}

mail.cpp

#include "GoogleMail.h"
#include "Credentials.h"
#include "CurlHelper.h"


#include <iostream>
#include <fstream>
#include <iterator>

namespace TAC=ThorsAnvil::Curl;
namespace TAG=ThorsAnvil::Google;
namespace TAS=ThorsAnvil::Serialize;

extern "C" size_t headerCallback(char *ptr, size_t size, size_t nmemb, void *userdata)
{
    return size * nmemb;
}
extern "C" size_t writeCallback(char *ptr, size_t size, size_t nmemb, void *userdata)
{
    return size * nmemb;
}

void mail()
{
    using namespace std::string_literals;

    std::ifstream       keyFileStream("KeyFile.json"); // Same as the one used in auth
    TAG::ApplicaionInfo keyFile(keyFileStream);

    std::ifstream       credStream("creds.json");
    TAG::OAuth2Creds    oathCreds(credStream);

    oathCreds.refresh(keyFile);

    const std::string createDraft="https://gmail.googleapis.com/gmail/v1/users/me/drafts";

    TAG::Draft   draft;
    draft.message.snippet           = "Test";
    draft.message.payload.body.data = "VGhpcyBpcyBhIHRlc3QgZS1tYWls"; // base64 of "This is a test e-mail";

    std::stringstream bodyStream;
    bodyStream << TAS::jsonExporter(draft, TAS::PrinterInterface::OutputType::Stream);
    std::string body(std::move(bodyStream.str()));

    TAC::CurlUniquePtr       curl(createDraft);
    TAC::CurlHeaderUniquePtr headerList({"Content-Type: application/json; charset=UTF-8",
                                         "Authorization: "s + oathCreds.token_type + " " + oathCreds.access_token
                                        });

    curl_easy_setopt(curl, CURLOPT_POST, 1);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str());
    curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, body.size());
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headerList.get());
    curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, headerCallback);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback);

    CURLcode code  = curl_easy_perform(curl);
    std::cerr << "Code: " << code << "n";
}
int main()
{
    try {
        mail();
    }
    catch(char const* msg) {
        std::cout << "Error: " << msg << "n";
    }
}

Makefile

CXXFLAGS   += -std=c++17 -I/usr/local/include
CXXFLAGS   += -L/usr/local/lib -lcurl -lThorSerialize17 -lThorsLogging17

Строительство

Для этого кода необходимы ThorsSerializer и curl. Их обоих можно восстановить с помощью варева.

> brew install thors_serializer
> brew install curl

Brew доступен для макинтош Linux и даже Windows!

Затем вы можете построить с помощью:

> make auth
> make mail

Использовать. Runt ./auth для создания файла учетных данных (нужно сделать это только один раз). Тогда ты можешь бежать ./mail для создания черновика электронного письма. Отправку почты добавлю позже.

1 ответ
1

Написание:

  • ApplicaionInfo -> ApplicationInfo
  • uriIndix -> uriIndex

class CurlUniquePtr: public std::unique_ptr<CURL, CurlDeleter>

class CurlHeaderUniquePtr: public std::unique_ptr<curl_slist, CurlHDeleter>

Немного хитроумно. На самом деле мы не должны наследовать std::unique_ptr и нам не нужен его полный интерфейс. Так что мы могли бы просто std::unique_ptr переменная-член внутри класса.

В качестве альтернативы мы могли бы ввести тип указателя и записать 3 функции, которые нам нужны, как бесплатные функции, например:

using CurlUniquePtr = std::unique_ptr<CURL, std::function<void(CURL*)>>;

CurlUniquePtr initCurl()
{
    auto curl = curl_easy_init();

    if (!curl)
        throw std::runtime_error("curl_easy_init() failed");
    
    return { curlUniquePtr, [] (CURL* c) { return curl_easy_cleanup(c); } };
}

using CurlSlistPtr = std::unique_ptr<curl_slist, std::function<void(curl_slist*)>>;

CurlSlistPtr curlSlistInit(std::initializer_list<std::string> list)
{
    auto slist = CurlSlistPtr();

    for (auto const& s : list)
        curlSlistAppend(slist, s);

    return slist;
}

void curlSlistAppend(CurlSlistPtr& slist, std::string const& s)
{
    auto added = curl_slist_append(slist.get(), s.c_str());

    if (!added)
        throw std::runtime_error("curl_slist_append() failed");
    
    slist.release();
    slist.reset(added);

    return slist;
}

Примечание. Вероятно, нам следует проверить возвращаемое значение curl_slist_append() вместо того, чтобы молча потерпеть неудачу.


    // Find URL
    std::size_t uriIndix = 0;
    for (;uriIndix < installed.redirect_uris.size(); ++uriIndix) {
        if (installed.redirect_uris[uriIndix].substr(0,4) == "urn:") {
            break;
        }
    }
    if (uriIndix >= installed.redirect_uris.size()) {
        throw std::runtime_error("Failed to find URN");
    }

Поиск URN повторяется и может быть преобразован в функцию. Рассмотрите возможность использования std::find_if вместо простого цикла for, например:

std::vector<std::string>::const_iterator findUrn(std::vector<std::string> const& uris)
{
    return std::find_if(uris.begin(), uris.end(),
        [] (std::string const& uri) { return uri.starts_with("urn:"); });
}

...

    auto uri = findUrn(installed.redirect_uris);

    if (uri == uris.end())
        throw std::runtime_error("Failed to find URI");

curl_easy_setopt(...)

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


extern "C" size_t outputToStream(char* ptr, size_t size, size_t nmemb, void* userdata)
{
    std::iostream& stream = *(reinterpret_cast<std::iostream*>(userdata));
    stream.write(ptr, size * nmemb);
    return size * nmemb;
}

static_cast здесь должно работать нормально, не нужно reinterpret_cast.

    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &static_cast<std::iostream&>(credStore));

Не очень люблю ручное приведение к базовому указателю. Но я думаю, упаковка curl_easy_setopt в типобезопасном слое мы не хотим этого делать. (Опять же, мы не используем так много библиотеки, так что это того стоит).

Может быть предпочтительнее использовать лямбда для функции записи вместо свободной функции … но, похоже, есть свои сложности.


Некоторые комментарии по поводу. Credentials.h а также GoogleMail.h.

  • Было бы более гибко отделить получение учетных данных с сервера от записи файла «creds.json» (даже если мы можем сделать и то, и другое одновременно с помощью curl).

    (В качестве побочного примечания, нам, вероятно, следует иметь здесь больше обработки ошибок — проверка того, что файл открыт, а запись выполнена успешно и т. Д.)

  • Не люблю typedefs вроде using Headers = std::vector<Header>;. Он скрывает тип контейнера.

  • 1

    Написание. У такого языка, как английский, есть нерв отклоняться от Стандартной формы Мартина. Спасибо, я их пропустил.

    — Мартин Йорк

  • Ага, надо было использовать композицию на этих обертках.

    — Мартин Йорк

  • Тихая неудача, моя любимая. Исправление.

    — Мартин Йорк

  • Удалите код вырезанной пасты. Абсолютно. Исправление.

    — Мартин Йорк

  • Проверьте коды ошибок C (человек, я ненавижу C за это, он делает код таким уродливым).

    — Мартин Йорк

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *