#include "user.h" #include "l2su.h" #include "command.h" #include "macros.h" #include "uuid/uuid.h" #include #include #include #include #include #include #include #include /* 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; } } struct l2_user *l2_user_search(const char *searchstr) { uuid_t uuid; bool have_uuid = false; if (l2_uuid_from_string(&uuid, searchstr) == 0) { have_uuid = true; } else { if (l2_uuid_from_string_short(&uuid, searchstr) == 0) { have_uuid = true; } } for (struct l2_user *cur = l2_state.users_head; cur; cur = cur->next) { if (have_uuid && !l2_uuid_compare(&cur->profile.uuid, &uuid)) { return cur; } else if (cur->nickname && !strcmp(searchstr, cur->nickname)) { return cur; } else if (!strcmp(searchstr, cur->profile.name)) { return cur; } } return NULL; } 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; }