From 306cf74eb0101a12b51549866a4d60296618ee0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=A6=8C=EA=A6=AB=EA=A6=B6=EA=A6=8F=EA=A7=80=EA=A6=A6?= =?UTF-8?q?=EA=A6=BF=EA=A6=A7=EA=A6=AE=EA=A6=91=EA=A6=A9=EA=A6=AD=EA=A7=80?= Date: Wed, 14 Sep 2022 18:19:14 +0800 Subject: OAuth part The minimum to pass all authentications and arrive at the embedded app index. This library is to be used with shopify-app-template-c for now, as it assumes the existence of shopify.app.toml in the parent directory, and index.html in the frontend directory. --- .gitignore | 18 ++++ Makefile.am | 4 + base64.h | 13 +++ config.h | 10 ++ configure.ac | 14 +++ crypt.h | 34 +++++++ regex.h | 17 ++++ request.h | 46 +++++++++ session.h | 6 ++ shopify.c | 300 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ shopify.h | 30 ++++++ token.h | 26 ++++++ 12 files changed, 518 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile.am create mode 100644 base64.h create mode 100644 config.h create mode 100644 configure.ac create mode 100644 crypt.h create mode 100644 regex.h create mode 100644 request.h create mode 100644 session.h create mode 100644 shopify.c create mode 100644 shopify.h create mode 100644 token.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..183ed07 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +*~ +*.swp +Makefile.in +aclocal.m4 +ar-lib +autom4te.cache +autoscan*.log +build +compile +config.guess +config.sub +configure +configure.scan +depcomp +install-sh +libtool +ltmain.sh +missing diff --git a/Makefile.am b/Makefile.am new file mode 100644 index 0000000..7bbe76c --- /dev/null +++ b/Makefile.am @@ -0,0 +1,4 @@ +lib_LTLIBRARIES = libshopify.la +libshopify_la_SOURCES = shopify.c +libshopify_la_CPPFLAGS = $(DEPS_CFLAGS) +include_HEADERS = shopify.h diff --git a/base64.h b/base64.h new file mode 100644 index 0000000..8533af1 --- /dev/null +++ b/base64.h @@ -0,0 +1,13 @@ +#include + +static inline void base64_decode(unsigned char *host, char **decoded_host) +{ + gnutls_datum_t result; + gnutls_base64_decode2(&(gnutls_datum_t){ + host, + strlen((const char *)host) + }, &result); + *decoded_host = malloc(result.size + 1); + strlcpy(*decoded_host, (const char *)result.data, result.size + 1); + gnutls_free(result.data); +} diff --git a/config.h b/config.h new file mode 100644 index 0000000..939d86b --- /dev/null +++ b/config.h @@ -0,0 +1,10 @@ +#include + +static inline void config_getscopes(const char *scope_path, char **scopes) +{ + FILE *fp = fopen(scope_path, "r"); + toml_table_t* toml = toml_parse_file(fp, NULL, 0); + fclose(fp); + *scopes = toml_string_in(toml, "scopes").u.s; + toml_free(toml); +} diff --git a/configure.ac b/configure.ac new file mode 100644 index 0000000..08ad556 --- /dev/null +++ b/configure.ac @@ -0,0 +1,14 @@ +AC_INIT([libshopify], [0.0], [erik@darapsa.co.id]) +AM_INIT_AUTOMAKE([-Wall -Werror foreign]) +AC_PROG_CC +AM_PROG_AR +LT_INIT +PKG_CHECK_MODULES([DEPS], [libmicrohttpd libgcrypt gnutls libpcre2-8 libcurl json-c]) +AC_CHECK_HEADERS([fcntl.h]) +AC_CHECK_HEADER_STDBOOL +AC_C_INLINE +AC_FUNC_MALLOC +AC_FUNC_REALLOC +AC_TYPE_SIZE_T +AC_CONFIG_FILES([Makefile]) +AC_OUTPUT diff --git a/crypt.h b/crypt.h new file mode 100644 index 0000000..f836bcc --- /dev/null +++ b/crypt.h @@ -0,0 +1,34 @@ +#include + +static inline void crypt_init() +{ + gcry_check_version("1.9.4"); +} + +static inline bool crypt_maccmp(const char *key, const char *query, + const char *hmac) +{ + gcry_mac_hd_t hd; + gcry_mac_open(&hd, GCRY_MAC_HMAC_SHA256, GCRY_MAC_FLAG_SECURE, NULL); + gcry_mac_setkey(hd, key, strlen(key)); + gcry_mac_write(hd, query, strlen(query)); + size_t hmac_sha256_len = 32; + unsigned char hmac_sha256[hmac_sha256_len + 1]; + gcry_mac_read(hd, hmac_sha256, &hmac_sha256_len); + gcry_mac_close(hd); + char hmac_sha256_str[65] = { [0] = '\0' }; + for (int i = 0; i < hmac_sha256_len; i++) + sprintf(hmac_sha256_str, "%s%02x", hmac_sha256_str, + hmac_sha256[i]); + return !strcmp(hmac, hmac_sha256_str); +} + +static inline void crypt_getnonce(char *string, const size_t string_len) +{ + string[0] = '\0'; + const size_t nonce_len = string_len / 2; + unsigned char nonce[nonce_len + 1]; + gcry_create_nonce(nonce, nonce_len); + for (int i = 0; i < nonce_len; i++) + sprintf(string, "%s%02x", string, nonce[i]); +} diff --git a/regex.h b/regex.h new file mode 100644 index 0000000..ebd51ac --- /dev/null +++ b/regex.h @@ -0,0 +1,17 @@ +#define PCRE2_CODE_UNIT_WIDTH 8 +#include + +static inline bool regex_match(const char *shop) +{ + pcre2_code *re = pcre2_compile((PCRE2_SPTR) + "^[a-zA-Z0-9][a-zA-Z0-9\\-]*\\.myshopify\\.com", + PCRE2_ZERO_TERMINATED, 0, &(int){ 0 }, + &(PCRE2_SIZE){ 0 }, NULL); + pcre2_match_data *match_data + = pcre2_match_data_create_from_pattern(re, NULL); + int rc = pcre2_match(re, (PCRE2_SPTR)shop, strlen(shop), 0, 0, + match_data, NULL); + pcre2_match_data_free(match_data); + pcre2_code_free(re); + return rc >= 0; +} diff --git a/request.h b/request.h new file mode 100644 index 0000000..7c8fe99 --- /dev/null +++ b/request.h @@ -0,0 +1,46 @@ +#include + +#define TOKEN_URL "https://%s/oauth/access_token" +#define TOKEN_URL_LEN strlen(TOKEN_URL) - strlen("%s") + +#define TOKEN_POST "client_id=%s&client_secret=%s&code=%s" +#define TOKEN_POST_LEN strlen(TOKEN_POST) - strlen("%s") * 3 + +static inline void request_init() +{ + curl_global_init(CURL_GLOBAL_DEFAULT); +} + +static size_t append(char *data, size_t size, size_t nmemb, char **tok) +{ + size_t realsize = size * nmemb; + size_t tok_len = *tok ? strlen(*tok) : 0; + *tok = realloc(*tok, tok_len + realsize + 1); + strlcpy(&(*tok)[tok_len], data, realsize + 1); + return realsize; +} + +static inline void request_token(const char *host, const char *key, + const char *secret_key, const char *code, char **tok) +{ + CURL *curl = curl_easy_init(); + char url[TOKEN_URL_LEN + strlen(host) + 1]; + sprintf(url, TOKEN_URL, host); + curl_easy_setopt(curl, CURLOPT_URL, url); + char post[TOKEN_POST_LEN + strlen(key) + strlen(secret_key) + + strlen(code) + 1]; + sprintf(post, TOKEN_POST, key, secret_key, code); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, tok); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, append); +#ifdef DEBUG + curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); +#endif + curl_easy_perform(curl); + curl_easy_cleanup(curl); +} + +static inline void request_cleanup() +{ + curl_global_cleanup(); +} diff --git a/session.h b/session.h new file mode 100644 index 0000000..9c850bf --- /dev/null +++ b/session.h @@ -0,0 +1,6 @@ +static struct session { + char *shop; + char *nonce; + char *token; + char *scope; +} *sessions; diff --git a/shopify.c b/shopify.c new file mode 100644 index 0000000..fd1c396 --- /dev/null +++ b/shopify.c @@ -0,0 +1,300 @@ +#include +#include +#include "shopify.h" +#include "crypt.h" +#include "base64.h" +#include "regex.h" +#include "config.h" +#include "request.h" +#include "session.h" +#include "token.h" + +#define AUTHORIZE_URL \ + "https://%s/oauth/authorize?client_id=%s&scope=%s&redirect_uri=%s%s"\ + "&state=%s" +#define AUTHORIZE_URL_LEN strlen(AUTHORIZE_URL) - strlen("%s") * 6 + +#define PAGE \ + "\n"\ + "\n"\ + "\t\n"\ + "\t\t\n"\ + "\t\n"\ + "\t\n"\ + "\t\t\n"\ + "\t\t\n"\ + "\t\n"\ + "\n" +#define PAGE_LEN strlen(PAGE) - strlen("%s") * 3 + +#define FRAME "frame-ancestors https://%s https://admin.shopify.com;" +#define FRAME_LEN strlen(FRAME) - strlen("%s") + +#define EMBEDDEDAPP_URL "https://%s/apps/%s/" +#define EMBEDDEDAPP_URL_LEN strlen(EMBEDDEDAPP_URL) - strlen("%s") * 2 + +extern inline void crypt_init(); +extern inline bool crypt_maccmp(const char *, const char *, const char *); +extern inline void crypt_getnonce(char *, const size_t); +extern inline bool regex_match(const char *); +extern inline void base64_decode(unsigned char *, char **); +extern inline void config_getscopes(const char *, char **); +extern inline void request_init(); +extern inline void request_token(const char *, const char *, const char *, + const char *, char **); +extern inline void request_cleanup(); +extern inline void token_parse(char *, struct session *); + +static enum MHD_Result getparam(void *cls, enum MHD_ValueKind kind, + const char *key, const char *val) +{ + if (kind == MHD_GET_ARGUMENT_KIND) { + struct shopify_param **params = cls; + int nparams = 0; + while ((*params)[nparams].key) + nparams++; + *params = realloc(*params, sizeof(struct shopify_param) + * (nparams + 2)); + (*params)[nparams].key = malloc(strlen(key) + 1); + strcpy((*params)[nparams].key, key); + (*params)[nparams].val = malloc(strlen(val) + 1); + strcpy((*params)[nparams].val, val); + (*params)[nparams + 1].key = NULL; + } + return MHD_YES; +} + +void shopify_init() +{ + crypt_init(); + request_init(); + sessions = malloc(sizeof(struct session)); + sessions[0].shop = NULL; +} + +static int paramcmp(const void *param1, const void *param2) +{ + return strcmp(((struct shopify_param *)param1)->key, + ((struct shopify_param *)param2)->key); +} + +static int sessioncmp(const void *session1, const void *session2) +{ + return strcmp(((struct session *)session1)->shop, + ((struct session *)session2)->shop); +} + +static inline void clear(struct shopify_param params[]) +{ + int i = 0; + while (params[i].key) { + free(params[i].val); + free(params[i++].key); + } +} + +bool shopify_valid(struct MHD_Connection *conn, const char *url, + const char *redir_url, const char *secret_key, + struct shopify_param *params[]) +{ + (*params)[0].key = NULL; + MHD_get_connection_values(conn, MHD_GET_ARGUMENT_KIND, getparam, + params); + int nparams = 0; + while ((*params)[nparams].key) + nparams++; + if (!nparams) + return false; + qsort(*params, nparams, sizeof(struct shopify_param), paramcmp); + struct shopify_param *param = bsearch(&(struct shopify_param) + { "shop" }, *params, nparams, + sizeof(struct shopify_param), paramcmp); + char *shop = param->val; + if (!shop || !regex_match(shop)) { + clear(*params); + return false; + } + char *query = NULL; + for (int i = 0; i < nparams; i++) { + const char *key = (*params)[i].key; + const char *val = (*params)[i].val; + if (strcmp(key, "hmac")) { + size_t query_len = query ? strlen(query) : 0; + bool last = i == nparams - 1; + query = realloc(query, query_len + strlen(key) + + strlen(val) + !last + 2); + query[query_len] = '\0'; + sprintf(query, "%s%s=%s%s", query, key, val, + last ? "" : "&"); + } + } + param = bsearch(&(struct shopify_param){ "hmac" }, *params, nparams, + sizeof(struct shopify_param), paramcmp); + char *hmac = param->val; + if (!hmac || !crypt_maccmp(secret_key, query, hmac)) { + clear(*params); + free(query); + return false; + } + free(query); + if (strcmp(url, redir_url)) + return true; + char *state = ((struct shopify_param *)bsearch(&(struct shopify_param) + { "state" }, *params, nparams, + sizeof(struct shopify_param), paramcmp))->val; + int nsessions = 0; + while (sessions[nsessions].shop) + nsessions++; + qsort(sessions, nsessions, sizeof(struct session), sessioncmp); + struct session *session = bsearch(&(struct session){ shop }, sessions, + nsessions, sizeof(struct session), sessioncmp); + if (strcmp(state, session->nonce)) { + clear(*params); + return false; + } + return true; +} + +static inline int redirect(const char *host, const char *id, + struct MHD_Response **resp, struct MHD_Connection *conn) +{ + char url[EMBEDDEDAPP_URL_LEN + strlen(host) + strlen(id) + 1]; + sprintf(url, EMBEDDEDAPP_URL, host, id); + *resp = MHD_create_response_from_buffer(0, "", MHD_RESPMEM_PERSISTENT); + MHD_add_response_header(*resp, "Location", url); + return MHD_queue_response(conn, MHD_HTTP_PERMANENT_REDIRECT, *resp); +} + +enum MHD_Result shopify_respond(struct shopify_param params[], const char *url, + const char *redir_url, const char *app_url, const char *app_id, + const char *key, const char *secret_key, const char *dir, + struct MHD_Connection *conn, struct MHD_Response **resp) +{ + int nparams = 0; + while (params[nparams].key) + nparams++; + char *shop = ((struct shopify_param *)bsearch(&(struct shopify_param) + { "shop" }, params, nparams, + sizeof(struct shopify_param), paramcmp))->val; + const size_t shop_len = strlen(shop); + char *host = ((struct shopify_param *)bsearch(&(struct shopify_param) + { "host" }, params, nparams, + sizeof(struct shopify_param), paramcmp))->val; + struct shopify_param *param = bsearch(&(struct shopify_param) + { "embedded" }, params, nparams, + sizeof(struct shopify_param), paramcmp); + bool embedded = param && !strcmp(param->val, "1"); + char *decoded_host; + base64_decode((unsigned char *)host, &decoded_host); + int nsessions = 0; + while (sessions[nsessions].shop) + nsessions++; + qsort(sessions, nsessions, sizeof(struct session), sessioncmp); + struct session *session = bsearch(&(struct session){ shop }, sessions, + nsessions, sizeof(struct session), sessioncmp); + const size_t dir_len = strlen(dir); + const size_t key_len = strlen(key); + char frame[FRAME_LEN + shop_len + 1]; + sprintf(frame, FRAME, shop); + enum MHD_Result ret; + if (!strcmp(url, redir_url)) { + const char *code = ((struct shopify_param *)bsearch( + &(struct shopify_param){ "code" }, + params, nparams, + sizeof(struct shopify_param), + paramcmp))->val; + char *token = NULL; + request_token(decoded_host, key, secret_key, code, &token); + token_parse(token, session); + free(token); + ret = redirect(decoded_host, app_id, resp, conn); + } else if (session && session->token) { + if (embedded) { + static const char *rel_path + = "/web/frontend/index.html"; + char abs_path[dir_len + strlen(rel_path) + 1]; + sprintf(abs_path, "%s%s", dir, rel_path); + int fd = open(abs_path, O_RDONLY); + struct stat sb; + fstat(fd, &sb); + char index[sb.st_size + 1]; + read(fd, index, sb.st_size); + close(fd); + *resp = MHD_create_response_from_buffer(sb.st_size, + index, MHD_RESPMEM_MUST_COPY); + MHD_add_response_header(*resp, + "Content-Security-Policy", frame); + ret = MHD_queue_response(conn, MHD_HTTP_OK, *resp); + } else + ret = redirect(decoded_host, app_id, resp, conn); + } else { + const size_t decoded_host_len = strlen(decoded_host); + static const char *rel_path = "/shopify.app.toml"; + char abs_path[dir_len + strlen(rel_path) + 1]; + sprintf(abs_path, "%s%s", dir, rel_path); + char *scopes = NULL; + config_getscopes(abs_path, &scopes); + const size_t scopes_len = strlen(scopes); + static const size_t nonce_len = 64; + char nonce[nonce_len + 1]; + crypt_getnonce(nonce, nonce_len); + const size_t authorize_url_len = AUTHORIZE_URL_LEN + + decoded_host_len + key_len + scopes_len + + strlen(app_url) + strlen(redir_url) + nonce_len; + char authorize_url[authorize_url_len + 1]; + sprintf(authorize_url, AUTHORIZE_URL, decoded_host, key, scopes, + app_url, redir_url, nonce); + free(scopes); + sessions = realloc(sessions, sizeof(struct session) + * (nsessions + 2)); + sessions[nsessions].shop = malloc(shop_len + 1); + strcpy(sessions[nsessions].shop, shop); + sessions[nsessions].nonce = malloc(nonce_len + 1); + strcpy(sessions[nsessions].nonce, nonce); + sessions[nsessions + 1].shop = NULL; + if (embedded) { + const size_t page_len = PAGE_LEN + key_len + + strlen(host) + authorize_url_len; + char page[page_len + 1]; + sprintf(page, PAGE, key, host, authorize_url); + *resp = MHD_create_response_from_buffer(page_len, + page, MHD_RESPMEM_MUST_COPY); + MHD_add_response_header(*resp, + "Content-Security-Policy", frame); + ret = MHD_queue_response(conn, MHD_HTTP_OK, *resp); + } else { + *resp = MHD_create_response_from_buffer(0, "", + MHD_RESPMEM_PERSISTENT); + MHD_add_response_header(*resp, "Location", + authorize_url); + ret = MHD_queue_response(conn, + MHD_HTTP_TEMPORARY_REDIRECT, *resp); + } + } + free(decoded_host); + clear(params); + return ret; +} + +void shopify_cleanup() +{ + request_cleanup(); + int i = 0; + while (sessions[i].shop) { + if (sessions[i].scope) + free(sessions[i].scope); + if (sessions[i].token) + free(sessions[i].token); + free(sessions[i].nonce); + free(sessions[i++].shop); + } + free(sessions); +} diff --git a/shopify.h b/shopify.h new file mode 100644 index 0000000..6af62d1 --- /dev/null +++ b/shopify.h @@ -0,0 +1,30 @@ +#ifndef SHOPIFY_H +#define SHOPIFY_H + +#include +#include + +struct shopify_param { + char *key; + char *val; +}; + +#ifdef __cplusplus +extern "C" { +#endif + +void shopify_init(); +bool shopify_valid(struct MHD_Connection *conn, const char *url, + const char *redir_url, const char *key, + struct shopify_param *params[]); +enum MHD_Result shopify_respond(struct shopify_param params[], const char *url, + const char *redir_url, const char *app_url, const char *app_id, + const char *key, const char *secret_key, const char *dir, + struct MHD_Connection *conn, struct MHD_Response **resp); +void shopify_cleanup(); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/token.h b/token.h new file mode 100644 index 0000000..0154ef5 --- /dev/null +++ b/token.h @@ -0,0 +1,26 @@ +#include + +static inline void token_parse(char *tok, struct session *session) +{ + json_tokener *tokener = json_tokener_new(); + json_object *obj = json_tokener_parse_ex(tokener, tok, strlen(tok)); + struct json_object_iterator iter = json_object_iter_begin(obj); + struct json_object_iterator iter_end = json_object_iter_end(obj); + while (!json_object_iter_equal(&iter, &iter_end)) { + if (!strcmp(json_object_iter_peek_name(&iter), + "access_token")) { + const char *val = json_object_get_string( + json_object_iter_peek_value(&iter)); + session->token = malloc(strlen(val) + 1); + strcpy(session->token, val); + } else if (!strcmp(json_object_iter_peek_name(&iter), + "scope")) { + const char *val = json_object_get_string( + json_object_iter_peek_value(&iter)); + session->scope = malloc(strlen(val) + 1); + strcpy(session->scope, val); + } + json_object_iter_next(&iter); + } + json_tokener_free(tokener); +} -- cgit v1.2.3