diff options
author | ꦌꦫꦶꦏ꧀ꦦꦿꦧꦮꦑꦩꦭ꧀ <erik@darapsa.co.id> | 2022-09-14 18:19:14 +0800 |
---|---|---|
committer | ꦌꦫꦶꦏ꧀ꦦꦿꦧꦮꦑꦩꦭ꧀ <erik@darapsa.co.id> | 2022-09-14 18:19:14 +0800 |
commit | 306cf74eb0101a12b51549866a4d60296618ee0b (patch) | |
tree | 89fe13b234f40f97de84cdb6610bcd11e1d47a88 |
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.
-rw-r--r-- | .gitignore | 18 | ||||
-rw-r--r-- | Makefile.am | 4 | ||||
-rw-r--r-- | base64.h | 13 | ||||
-rw-r--r-- | config.h | 10 | ||||
-rw-r--r-- | configure.ac | 14 | ||||
-rw-r--r-- | crypt.h | 34 | ||||
-rw-r--r-- | regex.h | 17 | ||||
-rw-r--r-- | request.h | 46 | ||||
-rw-r--r-- | session.h | 6 | ||||
-rw-r--r-- | shopify.c | 300 | ||||
-rw-r--r-- | shopify.h | 30 | ||||
-rw-r--r-- | token.h | 26 |
12 files changed, 518 insertions, 0 deletions
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 <gnutls/gnutls.h> + +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 <toml.h> + +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 @@ -0,0 +1,34 @@ +#include <gcrypt.h> + +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]); +} @@ -0,0 +1,17 @@ +#define PCRE2_CODE_UNIT_WIDTH 8 +#include <pcre2.h> + +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 <curl/curl.h> + +#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 <fcntl.h> +#include <sys/stat.h> +#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 \ + "<!DOCTYPE html>\n"\ + "<html lang=\"en\">\n"\ + "\t<head>\n"\ + "\t\t<meta charset=\"utf-8\"/>\n"\ + "\t</head>\n"\ + "\t<body>\n"\ + "\t\t<script src=\"https://unpkg.com/@shopify/app-bridge@3\"/>\n"\ + "\t\t</script>\n"\ + "\t\t<script>\n"\ + "\t\t\tvar appBridge = window['app-bridge'];\n"\ + "\t\t\tvar redirect = appBridge.actions.Redirect;\n"\ + "\t\t\tredirect.create(appBridge.createApp({\n"\ + "\t\t\t\tapiKey: '%s',\n"\ + "\t\t\t\thost: '%s'\n"\ + "\t\t\t})).dispatch(redirect.Action.REMOTE, '%s');\n"\ + "\t\t</script>\n"\ + "\t</body>\n"\ + "</html>\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 <stdbool.h> +#include <microhttpd.h> + +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 @@ -0,0 +1,26 @@ +#include <json.h> + +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); +} |