aboutsummaryrefslogtreecommitdiffstats
path: root/src/user.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/user.c')
-rw-r--r--src/user.c1514
1 files changed, 1514 insertions, 0 deletions
diff --git a/src/user.c b/src/user.c
new file mode 100644
index 0000000..fd1acd5
--- /dev/null
+++ b/src/user.c
@@ -0,0 +1,1514 @@
+#include "user.h"
+#include "l2su.h"
+#include "command.h"
+#include "macros.h"
+#include "uuid/uuid.h"
+
+#include <curl/curl.h>
+#include <curl/easy.h>
+#include <fcntl.h>
+#include <jansson.h>
+#include <string.h>
+#include <errno.h>
+#include <stdbool.h>
+#include <unistd.h>
+
+/* refresh mojang token if it expires in this amount of time or less (12 hours) */
+#define L2_USER__MOJ_TOKEN_EXPIRE_EARLY ((time_t)8640)
+
+struct l2_user_session_tag {
+ char *access_token;
+ time_t access_token_exp;
+
+ char *xbl_token;
+ time_t xbl_token_exp;
+
+ char *mc_xsts_token;
+ char *user_hash;
+ time_t mc_xsts_token_exp;
+
+ char *moj_token;
+ time_t moj_token_exp;
+
+ char *refresh_token;
+
+ uuid_t xuid;
+ bool xuid_present;
+};
+
+void l2_user_session_cleanup(struct l2_user_session_tag *session)
+{
+ free(session->xbl_token);
+ free(session->mc_xsts_token);
+ free(session->moj_token);
+ free(session->refresh_token);
+}
+
+int l2_user__load_session(json_t *jsession, l2_user_session_t *osession)
+{
+ const char *cxbl_token = NULL;
+ json_int_t jxbl_token_exp = -1;
+
+ const char *cmc_xsts_token = NULL;
+ const char *cuser_hash = NULL;
+ json_int_t jmc_xsts_token_exp = -1;
+
+ const char *cmoj_token = NULL;
+ json_int_t jmoj_token_exp = -1;
+
+ const char *crefresh_token = NULL;
+
+ const char *cxuidstr = NULL;
+
+ l2_user_session_t session;
+ memset(&session, 0, sizeof(session));
+
+ if (json_unpack(jsession, "{s?:s, s?:I, s?:s, s?:s, s?:I, s?:s, s?:I, s?:s, s?:s}",
+ "xbl_token", &cxbl_token, "xbl_token_exp", &jxbl_token_exp,
+ "mc_xsts_token", &cmc_xsts_token, "user_hash", &cuser_hash,
+ "mc_xsts_token_exp", &jmc_xsts_token_exp, "moj_token", &cmoj_token, "moj_token_exp", &jmoj_token_exp,
+ "refresh_token", &crefresh_token, "xuid", &cxuidstr) < 0) {
+ CMD_WARN0("Malformed session");
+ return -1;
+ }
+
+ if (cxuidstr && !l2_uuid_from_string(&session.xuid, cxuidstr)) {
+ CMD_WARN0("Malformed session: invalid XUID string");
+ return -1;
+ }
+
+ session.xuid_present = !!cxuidstr;
+ session.xuid.uuid_ms = 0;
+
+#define HANDLE_TOKEN2(_field, _field_exp, _cstr, _jexp) \
+ do { \
+ if (_cstr && _jexp >= 0) { \
+ session._field = strdup(_cstr); \
+ session._field_exp = (time_t)(_jexp); \
+ if (!session._field) goto cleanup; \
+ } \
+ } while (0)
+
+#define HANDLE_TOKEN(_fname) HANDLE_TOKEN2(_fname, _fname ## _exp, c ## _fname, j ## _fname ## _exp)
+
+ HANDLE_TOKEN(xbl_token);
+ HANDLE_TOKEN(mc_xsts_token);
+ HANDLE_TOKEN(moj_token);
+ /* token doesn't look like a word anymore */
+
+#undef HANDLE_TOKEN
+#undef HANDLE_TOKEN2
+
+ if (cuser_hash) {
+ session.user_hash = strdup(cuser_hash);
+ if (!session.user_hash) goto cleanup;
+ }
+
+ if (crefresh_token) {
+ session.refresh_token = strdup(crefresh_token);
+ if (!session.refresh_token) goto cleanup;
+ }
+
+ memcpy(osession, &session, sizeof(l2_user_session_t));
+
+ return 0;
+
+cleanup:
+ l2_user_session_cleanup(&session);
+ return -1;
+}
+
+void l2_user_profile_free_properties(struct l2_user_profile *profile) {
+ for (struct l2_user_profile_property *cur = profile->properties; profile->nproperties; --profile->nproperties) {
+ free(cur->name);
+ free(cur->value);
+ free(cur->signature);
+ }
+
+ free(profile->properties);
+ profile->properties = NULL;
+}
+
+int l2_user__load_user_profile(json_t *juprofile, struct l2_user_profile *oprofile)
+{
+ const char *uuidstr;
+ const char *namestr;
+ size_t namelen;
+ json_t *jproperties = NULL;
+
+ struct l2_user_profile profile;
+ memset(&profile, 0, sizeof(profile));
+
+ if (json_unpack(juprofile, "{s:s, s:s%, s?:o}", "id", &uuidstr, "name", &namestr, &namelen, "properties", &jproperties) < 0) {
+ CMD_WARN0("Malformed user profile");
+ return -1;
+ }
+
+ if (!l2_uuid_from_string_short(&profile.uuid, uuidstr)) {
+ CMD_WARN0("Malformed UUID in profile");
+ return -1;
+ }
+
+ if (namelen > 16) {
+ CMD_WARN("Invalid profile: name is too long (%zu > 16)", namelen);
+ return -1;
+ }
+
+ if (!namelen) {
+ CMD_WARN0("Invalid profile: empty name");
+ return -1;
+ }
+
+ memcpy(profile.name, namestr, namelen);
+
+ if (json_is_array(jproperties)) {
+ size_t propidx;
+ json_t *jprop;
+ profile.properties = calloc(json_array_size(jproperties), sizeof(struct l2_user_profile_property));
+ if (!profile.properties) {
+ goto cleanup;
+ }
+
+ profile.nproperties = json_array_size(jproperties);
+
+ json_array_foreach(jproperties, propidx, jprop) {
+ const char *name;
+ const char *value;
+ const char *signature = NULL;
+ if (json_unpack(jprop, "{s:s, s:s, s?:s}", "name", &name, "value", &value, "signature", &signature) < 0) {
+ CMD_WARN("Invalid profile %s: property could not be unpacked", profile.name);
+ goto cleanup;
+ }
+
+ profile.properties[propidx].name = strdup(name);
+ if (!profile.properties[propidx].name) goto cleanup;
+
+ profile.properties[propidx].value = strdup(value);
+ if (!profile.properties[propidx].value) goto cleanup;
+
+ if (signature) {
+ profile.properties[propidx].signature = strdup(signature);
+ if (!profile.properties[propidx].signature) goto cleanup;
+ }
+ }
+ } else if (jproperties) {
+ CMD_WARN0("Invalid profile: properties value is not an array");
+ return -1;
+ }
+
+ memcpy(oprofile, &profile, sizeof(struct l2_user_profile));
+ return 0;
+
+cleanup:
+ l2_user_profile_free_properties(&profile);
+
+ return -1;
+}
+
+int l2_user__load_one_user(json_t *juser, struct l2_user *ouser)
+{
+ const char *cnickname = NULL;
+ json_t *session = NULL;
+ json_t *profile = NULL;
+
+ if (json_unpack(juser, "{s?:s, s?:o, s:o}", "nickname", &cnickname, "session", &session, "profile", &profile) < 0) {
+ CMD_WARN0("Malformed user: probably missing profile");
+ return -1;
+ }
+
+ if (cnickname) {
+ ouser->nickname = strdup(cnickname);
+ if (!ouser->nickname) return -1;
+ } else {
+ ouser->nickname = NULL;
+ }
+
+ int res;
+ if ((res = l2_user__load_user_profile(profile, &ouser->profile)) < 0) {
+ return res;
+ }
+
+ if (session) {
+ ouser->session = calloc(1, sizeof(l2_user_session_t));
+ if (!ouser->session) return -1;
+
+ if ((res = l2_user__load_session(session, ouser->session)) < 0) {
+ return res;
+ }
+ } else {
+ ouser->session = NULL;
+ }
+
+ return 0;
+}
+
+int l2_user__load_from_json(json_t *jusers)
+{
+ json_t *juserarr = NULL;
+ if (json_unpack(jusers, "{s:o}", "users", &juserarr) < 0 || !json_is_array(juserarr)) {
+ CMD_WARN0("Malformed users database (missing key \"users\" or it is not an array)");
+ return -1;
+ }
+
+ struct l2_user *newuser = NULL;
+
+ json_t *juser;
+ size_t useridx;
+
+ json_array_foreach(juserarr, useridx, juser) {
+ newuser = calloc(1, sizeof(struct l2_user));
+ if (!newuser) goto cleanup;
+
+ if (l2_user__load_one_user(juser, newuser) < 0) {
+ goto cleanup;
+ }
+
+ l2_user_add(newuser);
+
+ newuser = NULL;
+ }
+
+ return 0;
+
+cleanup:
+ if (newuser) {
+ l2_user_free(newuser);
+ }
+
+ return -1;
+}
+
+int l2_user_load(void)
+{
+ char *userspath;
+ size_t userspathlen;
+
+ L2_ASPRINTF(userspath, userspathlen, "%s/users.json", l2_state.paths.config);
+
+ FILE *usersfp = fopen(userspath, "r");
+ if (!usersfp) {
+ if (errno == ENOENT) {
+ return 0;
+ }
+
+ CMD_WARN("Failed to open users database %s: %s", userspath, strerror(errno));
+ return -1;
+ }
+
+ json_error_t err;
+ json_t *jusers = json_loadf(usersfp, JSON_REJECT_DUPLICATES, &err);
+ if (!jusers) {
+ CMD_WARN("Failed to load users from %s: %s", userspath, err.text);
+ goto cleanup;
+ }
+
+ fclose(usersfp);
+ usersfp = NULL;
+
+ int res = l2_user__load_from_json(jusers);
+ json_decref(jusers);
+ return res;
+
+cleanup:
+ if (usersfp) fclose(usersfp);
+ return -1;
+}
+
+json_t *l2_user__save_user(struct l2_user *user)
+{
+ json_t *userobj = json_object();
+
+ if (user->nickname) {
+ if (json_object_set_new(userobj, "nickname", json_string(user->nickname)) < 0) goto cleanup;
+ }
+
+ if (user->session) {
+ json_t *jsession = json_object();
+ if (!jsession) goto cleanup;
+
+ json_object_set_new(userobj, "session", jsession);
+
+ if (user->session->xbl_token && user->session->xbl_token_exp >= 0) {
+ if (json_object_set_new(jsession, "xbl_token", json_string(user->session->xbl_token)) < 0) goto cleanup;
+ if (json_object_set_new(jsession, "xbl_token_exp", json_integer(user->session->xbl_token_exp)) < 0) goto cleanup;
+ }
+
+ if (user->session->mc_xsts_token && user->session->mc_xsts_token_exp >= 0) {
+ if (json_object_set_new(jsession, "mc_xsts_token", json_string(user->session->mc_xsts_token)) < 0) goto cleanup;
+ if (json_object_set_new(jsession, "user_hash", json_string(user->session->user_hash)) < 0) goto cleanup;
+ if (json_object_set_new(jsession, "mc_xsts_token_exp", json_integer(user->session->mc_xsts_token_exp)) < 0) goto cleanup;
+ }
+
+ if (user->session->moj_token && user->session->moj_token_exp >= 0) {
+ if (json_object_set_new(jsession, "moj_token", json_string(user->session->moj_token)) < 0) goto cleanup;
+ if (json_object_set_new(jsession, "moj_token_exp", json_integer(user->session->moj_token_exp)) < 0) goto cleanup;
+ }
+
+ if (user->session->refresh_token) {
+ if (json_object_set_new(jsession, "refresh_token", json_string(user->session->refresh_token)) < 0) goto cleanup;
+ }
+
+ if (user->session->xuid_present) {
+ char xuid_str[UUID_STRLEN + 1];
+ l2_uuid_to_string(&user->session->xuid, xuid_str);
+
+ if (json_object_set_new(jsession, "xuid", json_string(xuid_str)) < 0) goto cleanup;
+ }
+ }
+
+ json_t *jprofile = json_object();
+ if (!jprofile) goto cleanup;
+
+ json_object_set_new(userobj, "profile", jprofile);
+
+ if (json_object_set_new(jprofile, "name", json_string(user->profile.name)) < 0) goto cleanup;
+
+ char prof_uuid_str[UUID_STRLEN_SHORT + 1];
+ l2_uuid_to_string_short(&user->profile.uuid, prof_uuid_str);
+ if (json_object_set_new(jprofile, "id", json_string(prof_uuid_str)) < 0) goto cleanup;
+
+
+ if (user->profile.nproperties > 0) {
+ json_t *jprofprops = json_array();
+ if (!jprofprops) goto cleanup;
+
+ if (json_object_set_new(jprofile, "properties", jprofprops) < 0) goto cleanup;
+
+ for (size_t propidx = 0; propidx < user->profile.nproperties; ++propidx) {
+ json_t *jproperty = json_object();
+ if (!jproperty) goto cleanup;
+
+ if (json_array_append_new(jprofprops, jproperty) < 0) goto cleanup;
+
+ if (json_object_set_new(jproperty, "name", json_string(user->profile.properties[propidx].name)) < 0) goto cleanup;
+ if (json_object_set_new(jproperty, "value", json_string(user->profile.properties[propidx].value)) < 0) goto cleanup;
+ if (user->profile.properties[propidx].signature
+ && json_object_set_new(jproperty, "signature", json_string(user->profile.properties[propidx].signature)) < 0) goto cleanup;
+ }
+ }
+
+ return userobj;
+
+cleanup:
+ json_decref(userobj);
+ return NULL;
+}
+
+json_t *l2_user__save_users(void)
+{
+ json_t *usersobj = json_object();
+ if (!usersobj) return NULL;
+
+ json_t *juserarr;
+ if (json_object_set_new(usersobj, "users", juserarr = json_array()) < 0 || !juserarr) goto cleanup;
+
+ for (struct l2_user *cur = l2_state.users_head; cur; cur = cur->next) {
+ json_t *juser = l2_user__save_user(cur);
+ if (!juser) goto cleanup;
+
+ json_array_append_new(juserarr, juser);
+ }
+
+ return usersobj;
+
+cleanup:
+ json_decref(usersobj);
+ return NULL;
+}
+
+int l2_user_save(void)
+{
+ char *userspath;
+ size_t userspathlen;
+ FILE *usersfp = NULL;
+
+ L2_ASPRINTF(userspath, userspathlen, "%s/users.json", l2_state.paths.config);
+
+ errno = 0;
+ if (l2_launcher_mkdir_parents_ex(userspath, 1) < 0) {
+ CMD_WARN("Failed to create directory for users.json: %s", strerror(errno));
+ return -1;
+ }
+
+ json_t *jusers = l2_user__save_users();
+ if (!jusers) {
+ CMD_WARN0("Failed to create JSON representation of users. Probably due to resource exhaustion.");
+ return -1;
+ }
+
+ int fd = creat(userspath, S_IRUSR | S_IWUSR);
+ if (fd < 0) {
+ CMD_WARN("Failed to open %s for writing: %s", userspath, strerror(errno));
+ goto cleanup;
+ }
+
+ usersfp = fdopen(fd, "w");
+ if (!usersfp) {
+ CMD_WARN("Failed to open fd %d (%s) as FILE: %s", fd, userspath, strerror(errno));
+ goto cleanup;
+ }
+
+ fd = -1;
+
+ if (json_dumpf(jusers, usersfp, JSON_INDENT(4)) < 0) {
+ CMD_WARN0("JSON error dumping users to file");
+ goto cleanup;
+ }
+
+ fputc('\n', usersfp);
+
+ fclose(usersfp);
+ usersfp = NULL;
+
+ json_decref(jusers);
+ jusers = NULL;
+
+ return 0;
+
+cleanup:
+ if (fd > 0) close(fd);
+ if (usersfp) fclose(usersfp);
+ if (jusers) json_decref(jusers);
+ return -1;
+}
+
+void l2_user_free(struct l2_user *user)
+{
+ l2_user_profile_free_properties(&user->profile);
+
+ if (user->session) {
+ l2_user_session_cleanup(user->session);
+ free(user->session);
+ }
+
+ free(user->nickname);
+ free(user);
+}
+
+int l2_user__clone_to(struct l2_user *L2_RESTRICT dest, const struct l2_user *L2_RESTRICT user)
+{
+ memcpy(&dest->profile, &user->profile, sizeof(struct l2_user_profile));
+ if (user->profile.nproperties > 0) {
+ dest->profile.properties = calloc(dest->profile.nproperties, sizeof(struct l2_user_profile_property));
+ if (!dest->profile.properties) goto cleanup;
+
+ /* no risk of accidentally freeing user's properties because we just overwrote retuser's pointer to them */
+
+ for (size_t propidx = 0; propidx < user->profile.nproperties; ++propidx) {
+ dest->profile.properties[propidx].name = strdup(user->profile.properties[propidx].name);
+ if (!dest->profile.properties[propidx].name) goto cleanup;
+
+ dest->profile.properties[propidx].value = strdup(user->profile.properties[propidx].value);
+ if (!dest->profile.properties[propidx].value) goto cleanup;
+
+ if (user->profile.properties[propidx].signature) {
+ dest->profile.properties[propidx].signature = strdup(user->profile.properties[propidx].signature);
+ if (!dest->profile.properties[propidx].signature) goto cleanup;
+ }
+ }
+ } else {
+ dest->profile.properties = NULL;
+ }
+
+ if (user->session) {
+ dest->session = calloc(1, sizeof(l2_user_session_t));
+ if (!dest->session) goto cleanup;
+
+ dest->session->xbl_token_exp = user->session->xbl_token_exp;
+ dest->session->mc_xsts_token_exp = user->session->mc_xsts_token_exp;
+ dest->session->moj_token_exp = user->session->moj_token_exp;
+ dest->session->xuid_present = user->session->xuid_present;
+
+ l2_uuid_copy(&dest->session->xuid, &user->session->xuid);
+
+ if (user->session->xbl_token) {
+ dest->session->xbl_token = strdup(user->session->xbl_token);
+ if (!dest->session->xbl_token) goto cleanup;
+ }
+
+ if (user->session->mc_xsts_token) {
+ dest->session->mc_xsts_token = strdup(user->session->mc_xsts_token);
+ dest->session->user_hash = strdup(user->session->user_hash);
+ if (!dest->session->mc_xsts_token || !user->session->user_hash) goto cleanup;
+ }
+
+ if (user->session->moj_token) {
+ dest->session->moj_token = strdup(user->session->moj_token);
+ if (!dest->session->moj_token) goto cleanup;
+ }
+
+ if (user->session->refresh_token) {
+ dest->session->refresh_token = strdup(user->session->refresh_token);
+ if (!dest->session->refresh_token) goto cleanup;
+ }
+ }
+
+ dest->nickname = strdup(user->nickname);
+ if (!dest->nickname) goto cleanup;
+
+ return 0;
+
+cleanup:
+ l2_user_free(dest);
+ return -1;
+}
+
+struct l2_user *l2_user_clone(const struct l2_user *user)
+{
+ struct l2_user *retuser = calloc(1, sizeof(struct l2_user));
+ if (!retuser) return NULL;
+
+ if (l2_user__clone_to(retuser, user) < 0) {
+ return NULL;
+ }
+
+ return retuser;
+}
+
+void l2_user_add(struct l2_user *user)
+{
+ if (l2_state.users_tail) {
+ l2_state.users_tail->next = user;
+ user->prev = l2_state.users_tail;
+ l2_state.users_tail = user;
+ } else {
+ l2_state.users_head = l2_state.users_tail = user;
+ }
+}
+
+void l2_user_delete(struct l2_user *user)
+{
+ if (user->prev) {
+ user->prev->next = user->next;
+ } else {
+ l2_state.users_head = user->next;
+ }
+
+ if (user->next) {
+ user->next->prev = user->prev;
+ } else {
+ l2_state.users_tail = user->prev;
+ }
+}
+
+void l2_user_init(struct l2_user *user)
+{
+ /* using calloc works as well, so that's what we do in this file (elsewhere should use this function though) */
+ memset(user, 0, sizeof(struct l2_user));
+}
+
+l2_user_session_t *l2_user_session_new(void)
+{
+ l2_user_session_t *session = calloc(1, sizeof(l2_user_session_t));
+ if (!session) return NULL;
+
+ session->access_token = NULL;
+ session->access_token_exp = -1;
+
+ session->xbl_token_exp = -1;
+ session->mc_xsts_token_exp = -1;
+ session->moj_token_exp = -1;
+ session->xuid_present = false;
+
+ return session;
+}
+
+json_t *l2_user__microsoft_request(CURL *curl, const char *url)
+{
+ CURLcode code;
+ void *rdata = NULL;
+ json_t *rjson = NULL;
+ size_t rsize;
+
+ if ((code = l2_launcher_download(curl, url, &rdata, &rsize)) != CURLE_OK) {
+ CMD_WARN("Authentication request failed: %s", curl_easy_strerror(code));
+ goto cleanup;
+ }
+
+ json_error_t err;
+ rjson = json_loadb(rdata, rsize, JSON_REJECT_DUPLICATES, &err);
+ if (!rjson) {
+ CMD_WARN("Authentication request failed: JSON error: %s", err.text);
+ goto cleanup;
+ }
+
+#ifndef NDEBUG
+ CMD_DEBUG0("auth response:");
+ json_dumpf(rjson, stderr, JSON_INDENT(4));
+ fputc('\n', stderr);
+#endif
+
+ const char *errcode = NULL, *errdesc = NULL;
+ long httpres;
+ curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpres);
+
+ json_unpack(rjson, "{s?:s, s?:s}", "error", &errcode, "error_description", &errdesc);
+
+ if (httpres / 100 != 2 || errcode) {
+ CMD_WARN("Authentication request failed: (HTTP %ld) %s - %s", httpres, errcode ? errcode : "(unknown)", errdesc ? errdesc : "(unknown)");
+ goto cleanup;
+ }
+
+ free(rdata);
+ return rjson;
+
+cleanup:
+ free(rdata);
+ json_decref(rjson);
+
+ return NULL;
+}
+
+int l2_user__refresh_access_token(time_t nowish, l2_user_session_t *session)
+{
+ CMD_DEBUG0("Refreshing XBL token using refresh token if possible");
+ if (!session->refresh_token) {
+ CMD_DEBUG0("No offline access (refresh token missing). Must log in interactively again.");
+ return 0; /* must log in interactively again */
+ }
+
+ int ret;
+
+ CURL *curl = curl_easy_init();
+ if (!curl) return -1;
+
+ char *esc_clientid = NULL;
+ char *esc_refresh_token = NULL;
+ char *esc_scopes = NULL;
+ char *post_body = NULL;
+ struct curl_slist *headers = NULL;
+
+ json_t *rjson = NULL;
+
+ char *access_token_dup = NULL;
+ char *refresh_token_dup = NULL;
+
+ esc_scopes = curl_easy_escape(curl, L2_MSA_SCOPES, 0);
+ esc_clientid = curl_easy_escape(curl, L2_MSA_CLIENT_ID, 0);
+ esc_refresh_token = curl_easy_escape(curl, session->refresh_token, 0);
+
+ if (!(esc_clientid && esc_refresh_token)) {
+ ret = -1;
+ goto cleanup;
+ }
+
+ post_body = l2_launcher_sprintf_alloc("scope=%s&client_id=%s&refresh_token=%s&grant_type=%s", esc_scopes, esc_clientid, esc_refresh_token, "refresh_token");
+ if (!post_body) {
+ ret = -1;
+ goto cleanup;
+ }
+
+ headers = curl_slist_append(NULL, "Accept: application/json");
+ if (!headers) {
+ ret = -1;
+ goto cleanup;
+ }
+
+ curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post_body);
+ curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
+
+ rjson = l2_user__microsoft_request(curl, L2_MSA_URL_XBOX_AUTH);
+ if (!rjson) {
+ ret = -1;
+ goto cleanup;
+ }
+
+ const char *recv_access_token;
+ const char *recv_refresh_token;
+ json_int_t expires_in;
+
+ if (json_unpack(rjson, "{s:s, s:s, s:I}", "access_token", &recv_access_token, "refresh_token", &recv_refresh_token, "expires_in", &expires_in) < 0) {
+ CMD_WARN0("Failed to authenticate with xbox live (refresh token): could not parse JSON response");
+ ret = -1;
+ goto cleanup;
+ }
+
+ access_token_dup = strdup(recv_access_token);
+ if (!access_token_dup) {
+ ret = -1;
+ goto cleanup;
+ }
+
+ refresh_token_dup = strdup(recv_refresh_token);
+ if (!refresh_token_dup) {
+ ret = -1;
+ goto cleanup;
+ }
+
+ session->access_token = access_token_dup;
+ session->access_token_exp = nowish + expires_in;
+ access_token_dup = NULL;
+
+ session->refresh_token = refresh_token_dup;
+ refresh_token_dup = NULL;
+
+ ret = 1;
+
+cleanup:
+ if (curl) curl_easy_cleanup(curl);
+
+ curl_slist_free_all(headers);
+
+ curl_free(esc_scopes);
+ curl_free(esc_clientid);
+ curl_free(esc_refresh_token);
+
+ free(post_body);
+
+ if (rjson) json_decref(rjson);
+
+ free(access_token_dup);
+ free(refresh_token_dup);
+
+ return ret;
+}
+
+int l2_user__login_xbox_live(time_t nowish, l2_user_session_t *session)
+{
+ int ret = -1;
+ CMD_DEBUG0("Logging into xbox live using access token");
+
+ if (!session->access_token || session->access_token_exp - nowish < 300) {
+ CMD_DEBUG0("Cannot log in to xbox live without an access code!");
+ int refreshres = l2_user__refresh_access_token(nowish, session);
+ if (refreshres <= 0) return refreshres;
+ }
+
+ struct curl_slist *headers = NULL, *temp;
+ char *rqjson_str = NULL;
+ json_t *rjson = NULL;
+ char *xbl_tok_dup = NULL;
+
+ CURL *curl = curl_easy_init();
+ if (!curl) return -1;
+
+ json_t *rqjson = json_pack("{s:{s:s, s:s, s:s+}, s:s, s:s}", "Properties", "AuthMethod", "RPS", "SiteName", "user.auth.xboxlive.com", "RpsTicket", "d=", session->access_token, "RelyingParty", "http://auth.xboxlive.com", "TokenType", "JWT");
+
+ rqjson_str = json_dumps(rqjson, 0);
+ if (!rqjson_str) goto cleanup;
+
+ headers = curl_slist_append(NULL, "Accept: application/json");
+ if (!headers) goto cleanup;
+
+ temp = curl_slist_append(headers, "Content-type: application/json");
+ if (!temp) goto cleanup;
+ headers = temp;
+
+ curl_easy_setopt(curl, CURLOPT_POSTFIELDS, rqjson_str);
+ curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
+
+ rjson = l2_user__microsoft_request(curl, L2_MSA_URL_XBOX_LOGIN);
+ if (!rjson) {
+ goto cleanup;
+ }
+
+ const char *notafter;
+ const char *xbl_tok;
+
+ if (json_unpack(rjson, "{s:s, s:s}", "NotAfter", &notafter, "Token", &xbl_tok) < 0) {
+ CMD_WARN0("Failed to unpack XBL login response");
+ goto cleanup;
+ }
+
+ time_t natime;
+ if (l2_parse_time(notafter, &natime) < 0) {
+ CMD_WARN("Could not read XBL login response: failed to parse \"NotAfter\" time: %s", notafter);
+ goto cleanup;
+ }
+
+ xbl_tok_dup = strdup(xbl_tok);
+ if (!xbl_tok_dup) goto cleanup;
+
+ session->xbl_token = xbl_tok_dup;
+ xbl_tok_dup = NULL;
+
+ session->xbl_token_exp = natime;
+
+ ret = 1;
+
+cleanup:
+ free(xbl_tok_dup);
+
+ curl_easy_cleanup(curl);
+
+ if (rqjson) json_decref(rqjson);
+ if (rjson) json_decref(rjson);
+ free(rqjson_str);
+
+ curl_slist_free_all(headers);
+
+ return ret;
+}
+
+json_t *l2_user__get_xsts(const char *xbl_token, const char *relying_party)
+{
+ struct curl_slist *headers = NULL, *temp;
+ char *rqjson_str = NULL;
+ json_t *rjson = NULL;
+
+ CURL *curl = curl_easy_init();
+ if (!curl) return NULL;
+
+ json_t *rqjson = json_pack("{s:{s:s, s:[s]}, s:s, s:s}", "Properties", "SandboxId", "RETAIL", "UserTokens", xbl_token, "RelyingParty", relying_party, "TokenType", "JWT");
+ if (!rqjson) goto cleanup;
+
+ rqjson_str = json_dumps(rqjson, 0);
+ if (!rqjson_str) goto cleanup;
+
+ headers = curl_slist_append(NULL, "Accept: application/json");
+ if (!headers) goto cleanup;
+
+ temp = curl_slist_append(headers, "Content-type: application/json");
+ if (!temp) goto cleanup;
+ headers = temp;
+
+ curl_easy_setopt(curl, CURLOPT_POSTFIELDS, rqjson_str);
+ curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
+
+ rjson = l2_user__microsoft_request(curl, L2_MSA_URL_XBOX_XSTS);
+ goto cleanup_done;
+
+cleanup:
+ if (rjson) {
+ json_decref(rjson);
+ rjson = NULL;
+ }
+
+cleanup_done:
+ if (curl) curl_easy_cleanup(curl);
+ if (rqjson) json_decref(rqjson);
+
+ curl_slist_free_all(headers);
+ return rjson;
+}
+
+int l2_user__refresh_xbl_xsts(time_t nowish, l2_user_session_t *session)
+{
+ int refreshres;
+ if (!session->xbl_token || session->xbl_token_exp - nowish < 300) {
+ CMD_DEBUG0("Cannot refresh XBL XSTS token (XBL token is missing or expired)");
+ refreshres = l2_user__login_xbox_live(nowish, session);
+ if (refreshres <= 0) return refreshres;
+ }
+
+ int ret = -1;
+ json_t *xstsinfo = l2_user__get_xsts(session->xbl_token, "http://xboxlive.com");
+ if (!xstsinfo) return -1;
+
+ const char *xuidstr;
+ char *xuidstr_end;
+ uint64_t xuidval;
+
+ if (json_unpack(xstsinfo, "{s:{s:[{s:s}]}}", "DisplayClaims", "xui", "xid", &xuidstr) < 0) {
+ CMD_WARN0("Failed to unpack XBL XSTS response (for XUID)");
+ goto cleanup;
+ }
+
+ xuidval = (uint64_t)strtoull(xuidstr, &xuidstr_end, 10);
+ if (*xuidstr_end) {
+ CMD_WARN("Failed to read XBL XSTS response: XUID contained invalid characters: %s", xuidstr);
+ goto cleanup;
+ }
+
+ session->xuid.uuid_ms = 0;
+ session->xuid.uuid_ls = xuidval;
+ session->xuid_present = true;
+
+ ret = 1;
+
+cleanup:
+ if (xstsinfo) json_decref(xstsinfo);
+ return ret;
+}
+
+int l2_user__refresh_mc_xsts(time_t nowish, l2_user_session_t *session)
+{
+ int refreshres;
+ if (!session->xbl_token || session->xbl_token_exp - nowish < 300) {
+ CMD_DEBUG0("Cannot refresh Minecraft XSTS token (XBL token is missing or expired)");
+ refreshres = l2_user__login_xbox_live(nowish, session);
+ if (refreshres <= 0) return refreshres;
+ }
+
+ int res = -1;
+ json_t *xstsinfo = l2_user__get_xsts(session->xbl_token, "rp://api.minecraftservices.com/");
+
+ const char *nastr;
+ const char *uhstr;
+ const char *mc_xsts_tok;
+
+ char *uhstr_dup = NULL;
+ char *mc_xsts_tok_dup = NULL;
+
+ if (json_unpack(xstsinfo, "{s:s, s:s, s:{s:[{s:s}]}}", "NotAfter", &nastr, "Token", &mc_xsts_tok, "DisplayClaims", "xui", "uhs", &uhstr) < 0) {
+ CMD_WARN0("Failed to unpack MC XSTS response (for UHS and token)");
+ goto cleanup;
+ }
+
+ time_t natime;
+ if (l2_parse_time(nastr, &natime) < 0) {
+ CMD_WARN("Could not read \"NotAfter\" time for MC XSTS token: bad format (%s)", nastr);
+ goto cleanup;
+ }
+
+ uhstr_dup = strdup(uhstr);
+ mc_xsts_tok_dup = strdup(mc_xsts_tok);
+ if (!uhstr_dup || !mc_xsts_tok_dup) goto cleanup;
+
+ session->mc_xsts_token = mc_xsts_tok_dup;
+ session->user_hash = uhstr_dup;
+ session->mc_xsts_token_exp = natime;
+
+ mc_xsts_tok_dup = NULL;
+ uhstr_dup = NULL;
+
+ res = 1;
+
+cleanup:
+ if (xstsinfo) json_decref(xstsinfo);
+ free(uhstr_dup);
+ free(mc_xsts_tok_dup);
+ return res;
+}
+
+int l2_user__minecraft_login(time_t nowish, l2_user_session_t *session)
+{
+ int refreshres;
+ CMD_DEBUG0("Logging into Minecraft");
+ if (!session->mc_xsts_token || !session->user_hash || session->mc_xsts_token_exp - nowish < 300) {
+ CMD_DEBUG0("Cannot log in (minecraft XSTS token or user_hash missing).");
+ refreshres = l2_user__refresh_mc_xsts(nowish, session);
+ if (refreshres <= 0) return refreshres;
+ }
+
+ if (!session->xuid_present) {
+ CMD_DEBUG0("Cannot log in (XUID missing).");
+ refreshres = l2_user__refresh_xbl_xsts(nowish, session);
+ if (refreshres <= 0) return refreshres;
+ }
+
+ int ret = -1;
+
+ json_t *jpostbody = NULL;
+ char *postbody = NULL;
+ struct curl_slist *headers = NULL, *temp;
+ char *tokendup = NULL;
+ json_t *rjson = NULL;
+
+ CURL *curl = curl_easy_init();
+ if (!curl) return -1;
+
+ jpostbody = json_pack("{s:s+++}", "identityToken", "XBL3.0 x=", session->user_hash, ";", session->mc_xsts_token);
+ if (!jpostbody) goto cleanup;
+
+ postbody = json_dumps(jpostbody, 0);
+ if (!postbody) goto cleanup;
+
+ headers = curl_slist_append(NULL, "Accept: application/json");
+ if (!headers) goto cleanup;
+
+ temp = curl_slist_append(headers, "Content-type: application/json");
+ if (!temp) goto cleanup;
+ headers = temp;
+
+ curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postbody);
+ curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
+
+ rjson = l2_user__microsoft_request(curl, L2_MSA_URL_MINECRAFT_LOGIN);
+ if (!rjson) goto cleanup;
+
+ const char *token;
+ json_int_t expires_in;
+ if (json_unpack(rjson, "{s:s, s:I}", "access_token", &token, "expires_in", &expires_in) < 0) {
+ CMD_WARN0("Could unpack Minecraft login response");
+ goto cleanup;
+ }
+
+ tokendup = strdup(token);
+ if (!tokendup) goto cleanup;
+
+ session->moj_token = tokendup;
+ session->moj_token_exp = time(NULL) + expires_in;
+
+ tokendup = NULL;
+ ret = 1;
+
+cleanup:
+ json_decref(jpostbody);
+ free(postbody);
+ curl_slist_free_all(headers);
+ curl_easy_cleanup(curl);
+
+ free(tokendup);
+ json_decref(rjson);
+
+ return ret;
+}
+
+/* https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow */
+
+int l2_user_session_refresh(l2_user_session_t *session)
+{
+ time_t now = time(NULL);
+
+ if (session->xuid_present && session->moj_token_exp >= 0 && session->moj_token_exp - now > L2_USER__MOJ_TOKEN_EXPIRE_EARLY) {
+ CMD_DEBUG("Mojang token good (expires in %jd seconds), and XUID exists! Not refreshing.", (intmax_t)(now - session->moj_token_exp));
+ return 1;
+ }
+
+ return l2_user__minecraft_login(now, session);
+}
+
+json_t *l2_user__request_device_code(void)
+{
+ json_t *ret = NULL;
+
+ CURL *curl = curl_easy_init();
+ if (!curl) return NULL;
+
+ char *esc_clientid = NULL;
+ char *esc_scopes = NULL;
+ char *post_body = NULL;
+ struct curl_slist *headers = NULL;
+
+ esc_clientid = curl_easy_escape(curl, L2_MSA_CLIENT_ID, 0);
+ esc_scopes = curl_easy_escape(curl, L2_MSA_SCOPES, 0);
+
+ if (!esc_scopes || !esc_clientid) {
+ goto cleanup;
+ }
+
+ post_body = l2_launcher_sprintf_alloc("client_id=%s&scope=%s", esc_clientid, esc_scopes);
+ if (!post_body) goto cleanup;
+
+ headers = curl_slist_append(NULL, "Accept: application/json");
+ if (!headers) goto cleanup;
+
+ curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post_body);
+ curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
+
+ ret = l2_user__microsoft_request(curl, "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode");
+
+cleanup:
+ if (curl) curl_easy_cleanup(curl);
+
+ curl_slist_free_all(headers);
+ curl_free(esc_clientid);
+ curl_free(esc_scopes);
+ free(post_body);
+
+ return ret;
+}
+
+enum {
+ AUTH_SUCCESS,
+ AUTH_ERROR_PENDING,
+ AUTH_ERROR_DECLINED,
+ AUTH_ERROR_EXPIRED_TOKEN,
+ AUTH_ERROR_OTHER
+};
+
+int l2_user__try_device_auth(l2_user_session_t *session, const char *device_code, json_int_t *wait_interval)
+{
+ int ret = AUTH_ERROR_OTHER;
+
+ CURL *curl = curl_easy_init();
+ if (!curl) return AUTH_ERROR_OTHER;
+
+ char *esc_clientid = NULL;
+ char *esc_dev_code = NULL;
+ char *post_body = NULL;
+ struct curl_slist *headers = NULL;
+
+ void *rdata = NULL;
+ size_t rsize;
+ json_t *rjson = NULL;
+
+ char *access_token_dup = NULL;
+ char *refresh_token_dup = NULL;
+
+ esc_clientid = curl_easy_escape(curl, L2_MSA_CLIENT_ID, 0);
+ esc_dev_code = curl_easy_escape(curl, device_code, 0);
+
+ if (!esc_clientid || !esc_dev_code) goto cleanup;
+
+ post_body = l2_launcher_sprintf_alloc("grant_type=%s&client_id=%s&device_code=%s", "urn:ietf:params:oauth:grant-type:device_code", esc_clientid, esc_dev_code);
+ if (!post_body) goto cleanup;
+
+ headers = curl_slist_append(NULL, "Accept: application/json");
+ if (!headers) goto cleanup;
+
+ curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post_body);
+ curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
+
+ if (l2_launcher_download(curl, "https://login.microsoftonline.com/consumers/oauth2/v2.0/token", &rdata, &rsize) < 0) {
+ goto cleanup;
+ }
+
+ long httpres = 0;
+ curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpres);
+
+ json_error_t err;
+ rjson = json_loadb(rdata, rsize, JSON_REJECT_DUPLICATES, &err);
+ if (!rjson) {
+ CMD_WARN("Failed to parse token response: JSON parse error: %s", err.text);
+ goto cleanup;
+ }
+
+#ifndef NDEBUG
+ CMD_DEBUG("result of code thing: %ld", httpres);
+ json_dumpf(rjson, stderr, JSON_INDENT(4));
+ fputc('\n', stderr);
+#endif
+
+ const char *errcode = NULL, *errdesc = NULL;
+ json_unpack(rjson, "{s?:s, s?:s}", "error", &errcode, "error_description", &errdesc);
+ if (errcode || httpres / 100 != 2) {
+ if (!strcmp(errcode, "authorization_pending")) {
+ json_unpack(rjson, "{s:I}", "interval", wait_interval);
+
+ ret = AUTH_ERROR_PENDING;
+ } else if (!strcmp(errcode, "authorization_declined")) {
+ ret = AUTH_ERROR_DECLINED;
+ } else if (!strcmp(errcode, "expired_token")) {
+ ret = AUTH_ERROR_EXPIRED_TOKEN;
+ } else {
+ CMD_WARN("Error receiving token (device code flow): (HTTP %ld) %s: %s", httpres, errcode, errdesc);
+ ret = AUTH_ERROR_OTHER;
+ }
+ goto cleanup;
+ }
+
+ const char *access_token;
+ const char *refresh_token;
+ json_int_t access_token_exp_in;
+
+ if (json_unpack(rjson, "{s:s, s:s, s:I}", "access_token", &access_token, "refresh_token", &refresh_token, "expires_in", &access_token_exp_in) < 0) {
+ CMD_WARN0("Failed to unpack device code response");
+ goto cleanup;
+ }
+
+ access_token_dup = strdup(access_token);
+ refresh_token_dup = strdup(refresh_token);
+
+ if (!access_token_dup || !refresh_token_dup) goto cleanup;
+
+ session->access_token = access_token_dup;
+ session->refresh_token = refresh_token_dup;
+ session->access_token_exp = access_token_exp_in + time(NULL);
+
+ access_token_dup = NULL;
+ refresh_token_dup = NULL;
+ ret = AUTH_SUCCESS;
+
+cleanup:
+ if (curl) curl_easy_cleanup(curl);
+
+ curl_free(esc_clientid);
+ curl_free(esc_dev_code);
+ free(post_body);
+
+ curl_slist_free_all(headers);
+
+ free(rdata);
+ json_decref(rjson);
+
+ free(access_token_dup);
+ free(refresh_token_dup);
+
+ return ret;
+}
+
+int l2_user_session_login(l2_user_session_t *session)
+{
+ /* TODO call first function then call second function in a loop */
+ int ret = -1;
+ json_t *code = l2_user__request_device_code();
+ if (!code) {
+ CMD_WARN0("Failed to request device code.");
+ return -1;
+ }
+
+ const char *user_message;
+ const char *device_code;
+ json_int_t interval;
+ if (json_unpack(code, "{s:s, s:s, s:I}", "message", &user_message, "device_code", &device_code, "interval", &interval) < 0) {
+ CMD_WARN0("Failed to unpack device code response");
+ goto cleanup;
+ }
+
+ CMD_INFO("%s", user_message);
+
+ int authres;
+ do {
+ sleep(interval);
+ authres = l2_user__try_device_auth(session, device_code, &interval);
+ } while (authres == AUTH_ERROR_PENDING);
+
+ switch (authres) {
+ case AUTH_ERROR_DECLINED:
+ case AUTH_ERROR_EXPIRED_TOKEN:
+ ret = 0;
+ CMD_WARN0("Authentication declined or expired.");
+ goto cleanup;
+ case AUTH_ERROR_OTHER:
+ goto cleanup;
+ case AUTH_SUCCESS:
+ ret = 1;
+ }
+
+ CMD_DEBUG0("Successfully logged in, now get mojang token");
+ ret = l2_user_session_refresh(session);
+ if (ret <= 0) {
+ CMD_DEBUG0("Failed to refresh session :(");
+ ret = -1;
+ }
+
+cleanup:
+ json_decref(code);
+
+ return ret;
+}
+
+json_t *l2_user__load_my_profile(l2_user_session_t *session)
+{
+ if (!session->moj_token) {
+ CMD_DEBUG0("Can't load profile if there is no mojang token!");
+ return NULL;
+ }
+
+ CURL *curl = NULL;
+ void *rdata = NULL;
+ size_t rsize;
+ json_t *rjson = NULL;
+ char *tokenheader = NULL;
+
+ struct curl_slist *headers = NULL, *temp = NULL;
+
+ curl = curl_easy_init();
+ if (!curl) goto cleanup;
+
+ headers = curl_slist_append(NULL, "Accept: application/json");
+ if (!headers) goto cleanup;
+
+ tokenheader = l2_launcher_sprintf_alloc("Authorization: Bearer %s", session->moj_token);
+ if (!tokenheader) goto cleanup;
+
+ temp = curl_slist_append(headers, tokenheader);
+ if (!temp) goto cleanup;
+ headers = temp;
+
+ char errbuf[CURL_ERROR_SIZE];
+ memset(errbuf, 0, sizeof(errbuf));
+
+ curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
+ curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, errbuf);
+
+ CURLcode code = l2_launcher_download(curl, L2_MC_API_PROFILE, &rdata, &rsize);
+ if (code != CURLE_OK) {
+ CMD_WARN("Failed to download my own profile: %s: %s", curl_easy_strerror(code), errbuf);
+ goto cleanup;
+ }
+
+ json_error_t err;
+ rjson = json_loadb(rdata, rsize, JSON_REJECT_DUPLICATES, &err);
+ if (!rjson) {
+ CMD_WARN("Failed to parse JSON profile response: %s", err.text);
+ goto cleanup;
+ }
+
+#ifndef NDEBUG
+ CMD_DEBUG0("My profile response:");
+ json_dumpf(rjson, stderr, JSON_INDENT(4));
+ fputc('\n', stderr);
+#endif
+
+ long httpres = 0;
+ curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpres);
+ if (httpres == 404) {
+ goto cleanup;
+ } else if (httpres / 100 != 2) {
+ const char *error = "(no info)", *error_message = "(unknown)";
+ json_unpack(rjson, "{s?:s, s?:s}", "error", &error, "errorMessage", &error_message);
+ CMD_WARN("Error downloading profile: (HTTP %ld) %s: %s", httpres, error, error_message);
+
+ if (httpres == 404) {
+ CMD_WARN0("HTTP 404 can indicate that you haven't created a profile yet.");
+ }
+
+ goto cleanup;
+ }
+
+ curl_easy_cleanup(curl);
+ curl_slist_free_all(headers);
+ free(rdata);
+ return rjson;
+
+cleanup:
+ if (curl) curl_easy_cleanup(curl);
+
+ curl_slist_free_all(headers);
+ free(rdata);
+ free(tokenheader);
+ json_decref(rjson);
+
+ return NULL;
+}
+
+int l2_user__fill_profile(const uuid_t *uuid, struct l2_user_profile *profile)
+{
+ int ret = -1;
+
+ CURL *curl = NULL;
+ struct curl_slist *headers = NULL;
+ void *rdata = NULL;
+ size_t rsize;
+ json_t *rjson = NULL;
+
+ char *enc_uuid = NULL;
+
+ curl = curl_easy_init();
+
+ headers = curl_slist_append(NULL, "Accept: application/json");
+ if (!headers) goto cleanup;
+
+ curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
+
+ char uuidstr[UUID_STRLEN_SHORT + 1];
+ l2_uuid_to_string_short(uuid, uuidstr);
+ enc_uuid = curl_easy_escape(curl, uuidstr, 0);
+
+ char *url;
+ size_t urllen;
+ L2_ASPRINTF(url, urllen, L2_MC_API_OPROFILE_FORMAT, enc_uuid);
+
+ CURLcode code = l2_launcher_download(curl, url, &rdata, &rsize);
+ if (code != CURLE_OK) {
+ CMD_WARN("Failed to download profile of %s: %s", uuidstr, curl_easy_strerror(code));
+ goto cleanup;
+ }
+
+ json_error_t err;
+ rjson = json_loadb(rdata, rsize, JSON_REJECT_DUPLICATES, &err);
+ if (!rjson) {
+ CMD_WARN("Failed to parse JSON of profile: %s", err.text);
+ goto cleanup;
+ }
+
+#ifndef NDEBUG
+ CMD_DEBUG("%s profile response:", uuidstr);
+ json_dumpf(rjson, stderr, JSON_INDENT(4));
+ fputc('\n', stderr);
+#endif
+
+ long httpres;
+ curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpres);
+
+ const char *error_msg = "(unknown)";
+ json_unpack(rjson, "{s?:s}", "errorMessage", &error_msg);
+ if (httpres / 100 != 2) {
+ CMD_WARN("Failed to download profile: API error: (HTTP %ld) %s", httpres, error_msg);
+ goto cleanup;
+ }
+
+ ret = l2_user__load_user_profile(rjson, profile);
+
+cleanup:
+ if (curl) curl_easy_cleanup(curl);
+ curl_slist_free_all(headers);
+
+ free(rdata);
+ json_decref(rjson);
+ free(enc_uuid);
+
+ return ret;
+}
+
+int l2_user_update_profile(struct l2_user *user)
+{
+ int ret = -1;
+ json_t *jprofileinfo = l2_user__load_my_profile(user->session);
+ if (!jprofileinfo) return -1;
+
+ const char *uuidstr;
+ if (json_unpack(jprofileinfo, "{s:s}", "id", &uuidstr) < 0) {
+ CMD_WARN0("Invalid profile response (couldn't get UUID string)");
+ goto cleanup;
+ }
+
+ uuid_t id;
+ if (!l2_uuid_from_string_short(&id, uuidstr)) {
+ CMD_WARN("Invalid short UUID string %s", uuidstr);
+ goto cleanup;
+ }
+
+ ret = l2_user__fill_profile(&id, &user->profile);
+ CMD_DEBUG("fill_profile: %d", ret);
+
+cleanup:
+ json_decref(jprofileinfo);
+ return ret;
+}
+
+int l2_user_session_fill_subst(l2_user_session_t *session, void *subst_)
+{
+ l2_subst_t *subst = subst_;
+
+ if (session && session->moj_token && session->xuid_present) {
+ char xuidstr[UUID_STRLEN + 1];
+ l2_uuid_to_string(&session->xuid, xuidstr);
+
+ if (l2_subst_add(subst, "auth_access_token", session->moj_token) < 0) goto cleanup;
+ if (l2_subst_add(subst, "auth_xuid", xuidstr) < 0) goto cleanup;
+ if (l2_subst_add(subst, "clientid", L2_MSA_CLIENT_ID) < 0) goto cleanup;
+ if (l2_subst_add(subst, "auth_session", session->moj_token) < 0) goto cleanup;
+ if (l2_subst_add(subst, "user_type", "msa") < 0) goto cleanup;
+ } else {
+ if (l2_subst_add(subst, "auth_access_token", "-") < 0) goto cleanup;
+ if (l2_subst_add(subst, "auth_xuid", "null") < 0) goto cleanup;
+ if (l2_subst_add(subst, "clientid", "null") < 0) goto cleanup;
+ if (l2_subst_add(subst, "auth_session", "-") < 0) goto cleanup;
+ if (l2_subst_add(subst, "user_type", "msa") < 0) goto cleanup;
+ }
+
+ return 0;
+
+cleanup:
+ return -1;
+}
+
+char *l2_user_properties_serialize(const struct l2_user_profile *profile, bool legacy)
+{
+ json_t *jret;
+
+ if (legacy) {
+ if (!(jret = json_object())) goto cleanup;
+
+ for (size_t propidx = 0; propidx < profile->nproperties; ++propidx) {
+ json_t *val = json_object_get(jret, profile->properties[propidx].name);
+ if (!val) {
+ if (json_object_set_new(jret, profile->properties[propidx].name, val = json_array()) < 0) goto cleanup;
+ }
+
+ if (json_array_append_new(val, json_string(profile->properties[propidx].value)) < 0) goto cleanup;
+ }
+ } else {
+ if (!(jret = json_array())) goto cleanup;
+
+ for (size_t propidx = 0; propidx < profile->nproperties; ++propidx) {
+ if (json_array_append_new(jret, json_pack("{s:s, s:s, s:s*}",
+ "name", profile->properties[propidx].name,
+ "value", profile->properties[propidx].value,
+ "signature", profile->properties[propidx].signature)) < 0) goto cleanup;
+ }
+ }
+
+ char *retstr = json_dumps(jret, 0);
+ if (!retstr) goto cleanup;
+ json_decref(jret);
+ return retstr;
+
+cleanup:
+ json_decref(jret);
+ return NULL;
+}