summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorꦌꦫꦶꦏ꧀ꦦꦿꦧꦮꦑꦩꦭ꧀ <erik@darapsa.co.id>2022-09-14 18:19:14 +0800
committerꦌꦫꦶꦏ꧀ꦦꦿꦧꦮꦑꦩꦭ꧀ <erik@darapsa.co.id>2022-09-14 18:19:14 +0800
commit306cf74eb0101a12b51549866a4d60296618ee0b (patch)
tree89fe13b234f40f97de84cdb6610bcd11e1d47a88
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--.gitignore18
-rw-r--r--Makefile.am4
-rw-r--r--base64.h13
-rw-r--r--config.h10
-rw-r--r--configure.ac14
-rw-r--r--crypt.h34
-rw-r--r--regex.h17
-rw-r--r--request.h46
-rw-r--r--session.h6
-rw-r--r--shopify.c300
-rw-r--r--shopify.h30
-rw-r--r--token.h26
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
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 <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]);
+}
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 <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
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 <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);
+}