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 /shopify.c | |
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.
Diffstat (limited to 'shopify.c')
| -rw-r--r-- | shopify.c | 300 |
1 files changed, 300 insertions, 0 deletions
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); +} |