Играем с 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 ответ
Написание:
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>;
. Он скрывает тип контейнера.
Написание. У такого языка, как английский, есть нерв отклоняться от Стандартной формы Мартина. Спасибо, я их пропустил.
— Мартин Йорк
Ага, надо было использовать композицию на этих обертках.
— Мартин Йорк
Тихая неудача, моя любимая. Исправление.
— Мартин Йорк
Удалите код вырезанной пасты. Абсолютно. Исправление.
— Мартин Йорк
Проверьте коды ошибок C (человек, я ненавижу C за это, он делает код таким уродливым).
— Мартин Йорк
Показывать 5 больше комментариев