diff --git a/debian/control b/debian/control index 788d3ebb2..341b4b835 100644 --- a/debian/control +++ b/debian/control @@ -31,6 +31,7 @@ Build-Depends: libiptc-dev, libjson-glib-dev, libjson-perl, + libjwt-dev, libmnl-dev, libmosquitto-dev, libncurses-dev, diff --git a/lib/.ycm_extra_conf.py b/lib/.ycm_extra_conf.py index 88ed2a23e..a142e0a8f 100644 --- a/lib/.ycm_extra_conf.py +++ b/lib/.ycm_extra_conf.py @@ -30,6 +30,7 @@ flags = [ '-I/usr/include/glib-2.0', '-I/usr/include/opus', '-I/usr/lib/x86_64-linux-gnu/glib-2.0/include', + '-I/usr/include/json-glib-1.0', '-pthread', '-D_GNU_SOURCE', '-D__DEBUG=1', diff --git a/lib/oauth.c b/lib/oauth.c new file mode 100644 index 000000000..a71ebc4a1 --- /dev/null +++ b/lib/oauth.c @@ -0,0 +1,293 @@ +#include "oauth.h" +#include +#include +#include +#include +#include "auxlib.h" +#include "http.h" + + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(jwt_t, jwt_free) + + +// called during init +// returns g_strdup'd error +static char *oauth_service_account(oauth_context_t *ctx) { + g_autoptr(JsonParser) parser = json_parser_new(); + g_autoptr(GError) error = NULL; + json_parser_load_from_file(parser, ctx->service_account_file, &error); + if (error) + return g_strdup(error->message); + + const char *err; + + err = "Failed to get JSON root node"; + JsonNode *root = json_parser_get_root(parser); + if (!root) + goto err; + err = "JSON root node is not an object"; + JsonObject *o = json_node_get_object(root); + if (!o) + goto err; + + + err = "No 'client_email' in service account file"; + JsonNode *c = json_object_get_member(o, "client_email"); + if (!c) + goto err; + + err = "JSON 'client_email' is not a string"; + const char *s = json_node_get_string(c); + if (!s) + goto err; + ctx->_iss = g_strdup(s); + + + err = "No 'token_uri' in service account file"; + c = json_object_get_member(o, "token_uri"); + if (!c) + goto err; + + err = "JSON 'token_uri' is not a string"; + s = json_node_get_string(c); + if (!s) + goto err; + ctx->_aud = g_strdup(s); + + + err = "No 'private_key' in service account file"; + c = json_object_get_member(o, "private_key"); + if (!c) + goto err; + + err = "JSON 'private_key' is not a string"; + s = json_node_get_string(c); + if (!s) + goto err; + ctx->_private_key = g_strdup(s); + + + err = "algorithm not supported by JWT library"; + ctx->_alg = jwt_str_alg(ctx->algorithm); + if (ctx->_alg == JWT_ALG_INVAL) + goto err; + + + return NULL; + +err: + return g_strdup(err); +} + + +// lock is held +// returns g_strdup'd token +static char *oauth_set_token(oauth_context_t *ctx, const char *token, int64_t expires_in, time_t now) { + g_clear_pointer(&ctx->_token, g_free); + ctx->_token = g_strdup(token); + ctx->_expires = now + expires_in; + + return g_strdup(token); +} + + +// lock is held +// returns g_strdup'd token +static char *oauth_decode_token(oauth_context_t *ctx, GString *json, time_t now, char **errp) { + g_autoptr(JsonParser) parser = json_parser_new(); + g_autoptr(GError) error = NULL; + json_parser_load_from_data(parser, json->str, json->len, &error); + if (error) { + *errp = g_strdup(error->message); + return NULL; + } + + const char *err; + + err = "Failed to get JSON root node"; + JsonNode *root = json_parser_get_root(parser); + if (!root) + goto err; + err = "JSON root node is not an object"; + JsonObject *o = json_node_get_object(root); + if (!o) + goto err; + + + err = "No 'access_token' in OAuth response"; + JsonNode *c = json_object_get_member(o, "access_token"); + if (!c) + goto err; + + err = "JSON 'access_token' is not a string"; + const char *s = json_node_get_string(c); + if (!s) + goto err; + + + err = "No 'expires_in' in OAuth response"; + c = json_object_get_member(o, "expires_in"); + if (!c) + goto err; + + err = "JSON 'expires_in' is not valid"; + int64_t i = json_node_get_int(c); + if (!i) + goto err; + + + return oauth_set_token(ctx, s, i, now); + +err: + *errp = g_strdup(err); + return false;} + + +// lock is held +// returns g_strdup'd token +static char *oauth_request(oauth_context_t *ctx, const char *jwt, time_t now, char **errp) { + g_autoptr(char) req = g_strdup_printf("{'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer', " + "'assertion': '%s'}", jwt); + size_t req_len = strlen(req); + + struct curl_slist *headers = NULL; + + http_add_header(&headers, "Content-type: application/json"); + http_add_header(&headers, "Content-length: %zu", req_len); + + const char *err; + CURLcode curl_code = CURLE_OK; + g_autoptr(GString) resp = g_string_new(""); + err = "internal cURL error"; + CURL *c = http_create_req(ctx->_aud, + http_download_write, resp, + http_upload_read, + &(http_upload) {.s = { .s = req, .len = req_len } }, + headers, true, &curl_code, &err); + + if (!c) + goto err; + + // POST + err = "setting CURLOPT_POST"; + if ((curl_code = curl_easy_setopt(c, CURLOPT_POST, 1L)) != CURLE_OK) + goto err; + + err = "performing request"; + if ((curl_code = curl_easy_perform(c)) != CURLE_OK) + goto err; + + long code; + err = "getting CURLINFO_RESPONSE_CODE"; + if ((curl_code = curl_easy_getinfo(c, CURLINFO_RESPONSE_CODE, &code)) != CURLE_OK) + goto err; + + err = "checking response code (not 2xx)"; + if (code < 200 || code >= 300) { + *errp = g_strdup_printf("Request OAuth/JWT token failed with code %ld: %s%.*s%s", + code, FMT_M((int) resp->len, resp->str)); + return NULL; + } + + return oauth_decode_token(ctx, resp, now, errp); + +err: + *errp = g_strdup_printf("Failed to request OAuth token: %s / %s", err, curl_easy_strerror(curl_code)); + return NULL; +} + + +// lock is held +// returns g_strdup'd token +static char *oauth_request_token(oauth_context_t *ctx, time_t now, char **errp) { + const char *err; + + err = "failed to create libjwt object"; + g_autoptr(jwt_t) jwt = NULL; + int ret = jwt_new(&jwt); + if (ret) + goto err; + + err = "failed to add 'iss' grant"; + ret = jwt_add_grant(jwt, "iss", ctx->_iss); + if (ret) + goto err; + + err = "failed to add 'scope' grant"; + ret = jwt_add_grant(jwt, "scope", ctx->scope); + if (ret) + goto err; + + err = "failed to add 'aud' grant"; + ret = jwt_add_grant(jwt, "aud", ctx->_aud); + if (ret) + goto err; + + err = "failed to add 'iat' grant"; + ret = jwt_add_grant_int(jwt, "iat", now); + if (ret) + goto err; + + time_t expiry = now + 3600; + err = "failed to add 'exp' grant"; + ret = jwt_add_grant_int(jwt, "exp", expiry); + if (ret) + goto err; + + err = "failed to set algorithm or key"; + ret = jwt_set_alg(jwt, ctx->_alg, (unsigned char *) ctx->_private_key, strlen(ctx->_private_key)); + if (ret) + goto err; + + err = "failed encode JWT"; + char *j = jwt_encode_str(jwt); + if (!j) + goto err; + + char *token = oauth_request(ctx, j, now, errp); + free(j); + + return token; + +err: + *errp = g_strdup(err); + return NULL; +} + + +// returns g_strdup'd token +static char *oauth_get_token(oauth_context_t *ctx, char **errp) { + time_t now = time(NULL); + time_t cutoff = now - 10; + + LOCK(&ctx->_lock); + + if (ctx->_token && ctx->_expires > cutoff) + return g_strdup(ctx->_token); + + return oauth_request_token(ctx, now, errp); +} + + +void oauth_add_auth(struct curl_slist **headers, oauth_context_t *ctx, char **errp) { + g_autoptr(char) jwt = oauth_get_token(ctx, errp); + if (!jwt) + return; + + http_add_header(headers, "Authorization: Bearer %s", jwt); +} + + +char *oauth_init(oauth_context_t *ctx) { + mutex_init(&ctx->_lock); + return oauth_service_account(ctx); +} + + +void oauth_cleanup(oauth_context_t *ctx) { + g_clear_pointer(&ctx->_aud, g_free); + g_clear_pointer(&ctx->_iss, g_free); + g_clear_pointer(&ctx->_private_key, g_free); + g_clear_pointer(&ctx->_token, g_free); + mutex_destroy(&ctx->_lock); +} diff --git a/lib/oauth.h b/lib/oauth.h new file mode 100644 index 000000000..e163732e9 --- /dev/null +++ b/lib/oauth.h @@ -0,0 +1,35 @@ +#ifndef _OAUTH_H_ +#define _OAUTH_H_ + +#include +#include +#include +#include "auxlib.h" + + +typedef struct { + const char *service_account_file; + const char *scope; // https://www.googleapis.com/auth/cloud-platform + const char *algorithm; // RS256 + + // fields below here get filled in and are private, + // but must be initialised to zero + + jwt_alg_t _alg; + char *_iss; // client_email + char *_aud; // token_uri + char *_private_key; // PEM + + mutex_t _lock; + char *_token; + time_t _expires; +} oauth_context_t; + + +char *oauth_init(oauth_context_t *); +void oauth_cleanup(oauth_context_t *); + +void oauth_add_auth(struct curl_slist **headers, oauth_context_t *ctx, char **errp); + + +#endif diff --git a/recording-daemon/.ycm_extra_conf.py b/recording-daemon/.ycm_extra_conf.py index 67c5b6b0c..c26d9c392 100644 --- a/recording-daemon/.ycm_extra_conf.py +++ b/recording-daemon/.ycm_extra_conf.py @@ -20,6 +20,7 @@ flags = [ '-fno-strict-aliasing', '-I/usr/include/glib-2.0', '-I/usr/lib/x86_64-linux-gnu/glib-2.0/include', + '-I/usr/include/json-glib-1.0', '-I/usr/include/mysql', '-I.', '-I../lib/',