diff options
Diffstat (limited to 'src/user.c')
| -rw-r--r-- | src/user.c | 1514 |
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", ¬after, "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; +} |
