diff options
| author | 2024-01-11 00:39:56 -0600 | |
|---|---|---|
| committer | 2024-01-11 00:39:56 -0600 | |
| commit | b837ef02aff4c0974161ff2e077551a9710fdac5 (patch) | |
| tree | 3ae0af26264f2ec62af5ebf4391ef6399f67354f | |
| parent | actually use runtime (diff) | |
add auth
| -rw-r--r-- | src/cmd-version.c | 71 | ||||
| -rw-r--r-- | src/l2su.h | 8 | ||||
| -rw-r--r-- | src/launch.c | 60 | ||||
| -rw-r--r-- | src/launch.h | 3 | ||||
| -rw-r--r-- | src/launcherutil.c | 135 | ||||
| -rw-r--r-- | src/macros.h | 23 | ||||
| -rw-r--r-- | src/meson.build | 2 | ||||
| -rw-r--r-- | src/user.c | 1514 | ||||
| -rw-r--r-- | src/user.h | 63 | ||||
| -rw-r--r-- | src/uuid/uuid.c | 2 | ||||
| -rw-r--r-- | src/version.c | 9 |
11 files changed, 1845 insertions, 45 deletions
diff --git a/src/cmd-version.c b/src/cmd-version.c index 5796a72..b39daec 100644 --- a/src/cmd-version.c +++ b/src/cmd-version.c @@ -9,6 +9,7 @@ #include "assets.h" #include "launch.h" #include "instance.h" +#include "user.h" #include <jansson.h> #include <stdio.h> @@ -37,24 +38,56 @@ unsigned cmd_version_list_remote(struct l2_context_node *ctx, char **args) unsigned cmd_version_list_local(struct l2_context_node *ctx, char **args) { - json_t *manifest; - if (l2_runtime_load_manifest(&manifest) < 0) { - CMD_FATAL0("Failed to load manifest"); + int res; + if ((res = l2_user_load()) < 0) { + CMD_FATAL0("Failed to load users."); } - json_dumpf(manifest, stdout, JSON_INDENT(4)); - putchar('\n'); + struct l2_user *user = NULL; + for (struct l2_user *cur = l2_state.users_head; cur; cur = cur->next) { + if (!strcmp(cur->profile.name, *args)) { + user = cur; + break; + } + } + + struct l2_user luser; + memset(&luser, 0, sizeof(luser)); + + if (!user) { + CMD_DEBUG("User %s not found", *args); + user = &luser; + } - char *jrepath = NULL; + l2_user_session_t *session = user->session; + if (!session) { + session = l2_user_session_new(); - if (l2_runtime_install_component(manifest, "jre-legacy", &jrepath) < 0) { - CMD_FATAL0("Failed to install component"); + if (!session) { + CMD_FATAL0("Failed to create session"); + } + user->session = session; } - CMD_DEBUG("JRE path: %s", jrepath); + if ((res = l2_user_session_refresh(session)) < 0) { + CMD_FATAL0("Failed to refresh session"); + } else if (res == 0) { + if ((res = l2_user_session_login(session)) <= 0) { + CMD_FATAL0("Failed to login"); + } + } + + if (l2_user_update_profile(user) < 0) { + CMD_FATAL0("Failed to uhh stuff"); + } - free(jrepath); - json_decref(manifest); + if (user == &luser) { + l2_user_add(user); + } + + if (l2_user_save() < 0) { + CMD_FATAL0("Failed to save user"); + } return CMD_RESULT_SUCCESS; } @@ -69,6 +102,7 @@ bool feat_match_cb(const char *name, json_t *js, void *unused) { unsigned cmd_version_install(struct l2_context_node *ctx, char **args) { unsigned res = l2_version_load_remote(); + int ures; if (res != VERSION_SUCCESS) { CMD_FATAL("Failed to load versions: %s", l2_version_strerror(res)); } @@ -77,11 +111,26 @@ unsigned cmd_version_install(struct l2_context_node *ctx, char **args) CMD_FATAL("Failed to load instances: %s", l2_instance_errormsg[res]); } + if ((ures = l2_user_load()) < 0) { + CMD_FATAL0("Failed to load users"); + } + struct l2_launch launch = { 0 }; if (l2_launch_init(&launch, *args, l2_state.instance_head) < 0) { CMD_FATAL0("Failed to launch the game"); } + struct l2_user *user = NULL; + for (struct l2_user *cur = l2_state.users_head; cur; cur = cur->next) { + if (!strcmp(cur->profile.name, "figboot")) { + user = cur; + break; + } + } + + if (!user) CMD_FATAL0("figboot not found"); + launch.user = user; + if (l2_launch_init_substitutor(&launch) < 0) { CMD_FATAL0("Failed to initialize argument substitutor"); } @@ -6,6 +6,7 @@ #include "instance.h" #include "version.h" #include "macros.h" +#include "user.h" #include <fcntl.h> #include <time.h> @@ -28,6 +29,8 @@ struct tag_l2_state_t { struct l2_version_remote *ver_remote_head; struct l2_version_remote *ver_remote_tail; + + struct l2_user *users_head, *users_tail; }; extern struct tag_l2_state_t l2_state; @@ -68,8 +71,6 @@ typedef int (l2_ftw_proc_t)(const char * /*fname*/, const struct stat * /*sb*/, int l2_launcher_ftw(const char *path, int depth, l2_ftw_proc_t *proc, void *user); -char *l2_launcher_parse_iso_time(const char *str, struct tm *ts); - struct l2_dlbuf { void *data; size_t size; @@ -98,4 +99,7 @@ int l2_subst_add(l2_subst_t *sp, const char *name, const char *value); int l2_subst_apply(l2_subst_t *sp, const char *in, char **out); void l2_subst_free(l2_subst_t *sp); +/* parses an RFC3339 time string */ +int l2_parse_time(const char *timestr, time_t *ocaltime); + #endif /* include guard */ diff --git a/src/launch.c b/src/launch.c index a8ea689..942cbbb 100644 --- a/src/launch.c +++ b/src/launch.c @@ -3,6 +3,8 @@ #include "l2su.h" #include "launch.h" #include "macros.h" +#include "user.h" +#include "uuid/uuid.h" #include "version.h" #include "assets.h" #include "args.h" @@ -261,9 +263,12 @@ int l2_launch_init_substitutor(struct l2_launch *launch) #define L2_LAUNCH_ADD_SUB(_st, _name, _val) \ if (l2_subst_add(_st, _name, _val) < 0) goto cleanup + int ret = -1; char *keyname = NULL, *apath = NULL; size_t keycap = 0, acap = 0; + char *props_arr = NULL, *props_map = NULL; + char *classpath = launch->classpath; const char *ver_name; @@ -276,16 +281,38 @@ int l2_launch_init_substitutor(struct l2_launch *launch) if (l2_subst_init(&launch->arg_subst) < 0) goto cleanup; l2_subst_t *st = launch->arg_subst; - L2_LAUNCH_ADD_SUB(st, "auth_access_token", "-"); - L2_LAUNCH_ADD_SUB(st, "user_properties", "{}"); - L2_LAUNCH_ADD_SUB(st, "user_property_map", "{}"); - L2_LAUNCH_ADD_SUB(st, "auth_xuid", "null"); - L2_LAUNCH_ADD_SUB(st, "clientid", "null"); - L2_LAUNCH_ADD_SUB(st, "auth_session", "-"); + l2_user_session_t *session = NULL; + if (launch->user) session = launch->user->session; + + if (l2_user_session_fill_subst(session, st) < 0) goto cleanup; + + if (launch->user && *launch->user->profile.name) { + char uuidstr[UUID_STRLEN_SHORT + 1]; + + l2_uuid_to_string_short(&launch->user->profile.uuid, uuidstr); + + L2_LAUNCH_ADD_SUB(st, "auth_player_name", launch->user->profile.name); + L2_LAUNCH_ADD_SUB(st, "auth_uuid", uuidstr); + + props_arr = l2_user_properties_serialize(&launch->user->profile, true); + props_map = l2_user_properties_serialize(&launch->user->profile, false); - L2_LAUNCH_ADD_SUB(st, "auth_player_name", "figboot"); - L2_LAUNCH_ADD_SUB(st, "auth_uuid", "afc3f2d153844959bd05b2a5dc519c06"); - L2_LAUNCH_ADD_SUB(st, "user_type", "msa"); + if (!props_arr || !props_map) goto cleanup; + + L2_LAUNCH_ADD_SUB(st, "user_properties", props_arr); + L2_LAUNCH_ADD_SUB(st, "user_property_map", props_map); + } else { + uuid_t rnduuid; + char rnduuidstr[UUID_STRLEN_SHORT + 1]; + + l2_uuid_random(&rnduuid); + l2_uuid_to_string_short(&rnduuid, rnduuidstr); + + L2_LAUNCH_ADD_SUB(st, "auth_player_name", "Player$"); + L2_LAUNCH_ADD_SUB(st, "auth_uuid", rnduuidstr); + L2_LAUNCH_ADD_SUB(st, "user_properties", "[]"); + L2_LAUNCH_ADD_SUB(st, "user_property_map", "{}"); + } if (launch->instance) { L2_LAUNCH_ADD_SUB(st, "profile_name", launch->instance->name); @@ -339,23 +366,20 @@ int l2_launch_init_substitutor(struct l2_launch *launch) } } - free(keyname); - free(apath); - - keyname = NULL; - apath = NULL; - launch->classpath = classpath; - - return 0; + classpath = NULL; + ret = 0; cleanup: free(keyname); free(apath); + free(props_arr); + free(props_map); + if (classpath != launch->classpath) free(classpath); - return -1; + return ret; #undef L2_LAUNCH_ADD_SUB } diff --git a/src/launch.h b/src/launch.h index 598a152..3378e41 100644 --- a/src/launch.h +++ b/src/launch.h @@ -4,6 +4,7 @@ #include "l2su.h" #include "instance.h" #include "version.h" +#include "user.h" #include <jansson.h> @@ -34,6 +35,8 @@ struct l2_launch { size_t ngame_args; char **jvm_args; size_t njvm_args; + + struct l2_user *user; }; int l2_launch_init(struct l2_launch *launch, const char *vername, struct l2_instance *inst); diff --git a/src/launcherutil.c b/src/launcherutil.c index 4a866f5..11d5e00 100644 --- a/src/launcherutil.c +++ b/src/launcherutil.c @@ -252,11 +252,6 @@ int l2_launcher_mkdir_parents_ex(const char *path, unsigned ignore) return 0; } -char *l2_launcher_parse_iso_time(const char *str, struct tm *ts) -{ - return strptime(str, "%FT%T%z", ts); /* TODO: replace with something portable */ -} - void l2_launcher_download_init(struct l2_dlbuf *buf) { buf->data = NULL; @@ -655,3 +650,133 @@ int l2_launcher_ftw(const char *path, int depth, l2_ftw_proc_t *proc, void *user return res; } + +int l2_launcher__parse_u64(const char *num, size_t len, uint64_t *onum) +{ + uint64_t accum = 0; + for (; len; --len, ++num) { + accum *= 10; + if (*num >= '0' && *num <= '9') { + accum += *num - '0'; + } else { + return -1; + } + } + + *onum = accum; + return 0; +} + +int l2_parse_time(const char *timestr, time_t *ocaltime) +{ +#define L2_TM_PARSE_FIELD(_tmstr, _len, _tmp, _type, _field) do { \ + if (l2_launcher__parse_u64(_tmstr, _len, &(_tmp)) < 0) return -1; \ + _field = (_type)(_tmp); \ + _tmstr += _len; \ + } while (0) + +#define L2_TM_ENSURE_CHAR(_tmstr, _ch) do { \ + if (*(_tmstr) != _ch) return -1; \ + _tmstr += 1; \ + } while (0) + +#define L2_TM_ASSERT_RANGE(_field, _min, _max) do { \ + L2_DIAG_PUSH L2_DIAG_IGNORED(-Wtype-limits) \ + if (_field < _min || _field > _max) return -1; \ + L2_DIAG_POP \ + } while (0) + + uint64_t temp; + struct tm otime; + time_t timeres; + memset(&otime, 0, sizeof(otime)); + otime.tm_isdst = -1; + +#ifndef NDEBUG + const char *origstr = timestr; +#endif + + /* full-date */ + /* date-fullyear */ + L2_TM_PARSE_FIELD(timestr, 4, temp, int, otime.tm_year); + L2_TM_ENSURE_CHAR(timestr, '-'); + otime.tm_year -= 1900; + + /* date-month */ + L2_TM_PARSE_FIELD(timestr, 2, temp, int, otime.tm_mon); + L2_TM_ASSERT_RANGE(temp, 1, 12); + --otime.tm_mon; + + L2_TM_ENSURE_CHAR(timestr, '-'); + + /* date-mday */ + L2_TM_PARSE_FIELD(timestr, 2, temp, int, otime.tm_mday); + L2_TM_ASSERT_RANGE(temp, 1, 31); + + if (*timestr != 't' && *timestr != 'T') return -1; + ++timestr; + + /* full-time */ + + /* partial-time */ + /* time-hour */ + L2_TM_PARSE_FIELD(timestr, 2, temp, int, otime.tm_hour); + L2_TM_ASSERT_RANGE(temp, 0, 23); + L2_TM_ENSURE_CHAR(timestr, ':'); + + /* time-minute */ + L2_TM_PARSE_FIELD(timestr, 2, temp, int, otime.tm_min); + L2_TM_ASSERT_RANGE(temp, 0, 59); + L2_TM_ENSURE_CHAR(timestr, ':'); + + /* time-second */ + L2_TM_PARSE_FIELD(timestr, 2, temp, int, otime.tm_sec); + L2_TM_ASSERT_RANGE(temp, 0, 60); /* for leap seconds */ + + /* ignoring time-secfrac (idc about it lol) */ + if (*timestr == '.') { + do { + ++timestr; + } while (*timestr >= '0' && *timestr <= '9'); + } + + /* time-offset */ + if (*timestr == 'Z' || *timestr == 'z') { + goto done; + } + + int tzoff_mult; + switch (*timestr) { + case '+': + tzoff_mult = 1; + break; + case '-': + tzoff_mult = -1; + break; + default: return -1; + } + + ++timestr; + + CMD_DEBUG("trying to parse timestamp %s, which has zoneoffset. " + "This is not fully supported (contains bugs 100%% guaranteed or your money back)", origstr); + + int houroff; + L2_TM_PARSE_FIELD(timestr, 2, temp, int, houroff); + L2_TM_ASSERT_RANGE(houroff, 0, 23); + L2_TM_ENSURE_CHAR(timestr, ':'); + + int minoff; + L2_TM_PARSE_FIELD(timestr, 2, temp, int, minoff); + L2_TM_ASSERT_RANGE(minoff, 0, 59); + + otime.tm_hour -= houroff * tzoff_mult; + otime.tm_min -= minoff * tzoff_mult; + +done: + timeres = mktime(&otime); + if (timeres == (time_t)-1) return -1; + *ocaltime = timeres; + + return 0; +} diff --git a/src/macros.h b/src/macros.h index 684323e..8749abb 100644 --- a/src/macros.h +++ b/src/macros.h @@ -46,13 +46,36 @@ #define L2_URL_META_RUNTIME_MANIFEST L2_URL_META_BASE "/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json" #define L2_URL_RESOURCES_BASE "https://resources.download.minecraft.net" +#define L2_MSA_CLIENT_ID "60b6cc54-fc07-4bab-bca9-cbe9aa713c80" +#define L2_MSA_SCOPES "Xboxlive.signin offline_access" + +#define L2_MSA_URL_XBOX_AUTH "https://login.microsoftonline.com/consumers/oauth2/v2.0/token" +#define L2_MSA_URL_XBOX_LOGIN "https://user.auth.xboxlive.com/user/authenticate" +#define L2_MSA_URL_XBOX_XSTS "https://xsts.auth.xboxlive.com/xsts/authorize" +#define L2_MSA_URL_MINECRAFT_LOGIN "https://api.minecraftservices.com/authentication/login_with_xbox" + +#define L2_MC_API_PROFILE "https://api.minecraftservices.com/minecraft/profile" +#define L2_MC_API_OPROFILE_FORMAT "https://sessionserver.mojang.com/session/minecraft/profile/%s?unsigned=false" + #ifdef __GNUC__ #define L2_GNU_ATTRIBUTE(_x) __attribute__(_x) #define L2_FORMAT(_flavor, _stridx, _argidx) L2_GNU_ATTRIBUTE((format (_flavor, _stridx, _argidx))) #define L2_SENTINEL L2_GNU_ATTRIBUTE((sentinel)) + +#define L2_PRAGMA(_x) _Pragma(#_x) +#define L2_DIAG_PUSH L2_PRAGMA(GCC diagnostic push) +#define L2_DIAG_POP L2_PRAGMA(GCC diagnostic pop) +#define L2_DIAG_IGNORED(_w) L2_PRAGMA(GCC diagnostic ignored #_w) + #else #define L2_FORMAT(_unused1, _unused2, _unused3) +#define L2_SENTINEL + +#define L2_PRAGMA(_x) +#define L2_DIAG_PUSH +#define L2_DIAG_POP +#define L2_DIAG_IGNORED(_w) #endif #ifdef __cplusplus diff --git a/src/meson.build b/src/meson.build index c3b11f5..9da4e88 100644 --- a/src/meson.build +++ b/src/meson.build @@ -1,7 +1,7 @@ launcher_srcs = files('l2su.c', 'command.c', 'cmd-instance.c', 'uuid/uuid.c', 'launcherutil.c', 'instance.c', 'cmd-version.c', 'digest/sha1.c', 'version.c', 'subst.c', 'downloadpool.c', 'assets.c', 'args.c', 'launch.c', 'jniwrap.c', - 'runtime.c') + 'runtime.c', 'user.c') configure_file(input : 'config.h.in', output : 'config.h', configuration : config_data) config_include_dir = include_directories('.') 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; +} diff --git a/src/user.h b/src/user.h new file mode 100644 index 0000000..f84e3e1 --- /dev/null +++ b/src/user.h @@ -0,0 +1,63 @@ +#ifndef L2SU_USER_H_INCLUDED +#define L2SU_USER_H_INCLUDED + +#include "uuid/uuid.h" +#include "macros.h" + +#include <time.h> + +#define L2_PROFILE_NAME_STRLEN (16) + +typedef void *l2_user_profile_properties_iter_t; + +struct l2_user_profile_property { + char *name; + char *value; + char *signature; +}; + +struct l2_user_profile { + uuid_t uuid; + char name[L2_PROFILE_NAME_STRLEN + 1]; + + size_t nproperties; + struct l2_user_profile_property *properties; +}; + +typedef struct l2_user_session_tag l2_user_session_t; + +struct l2_user { + struct l2_user_profile profile; + l2_user_session_t *session; /* NULL for offline users */ + + char *nickname; + + struct l2_user *next, *prev; +}; + +int l2_user_load(void); +int l2_user_save(void); + +void l2_user_init(struct l2_user *user); +void l2_user_free(struct l2_user *user); +struct l2_user *l2_user_clone(const struct l2_user *user); + +void l2_user_add(struct l2_user *user); + +/* does NOT free the user */ +void l2_user_delete(struct l2_user *user); + +l2_user_session_t *l2_user_session_new(void); + +/* returns 1 if refresh succeeded, 0 if the session needs login, or -1 if there was an error */ +int l2_user_session_refresh(l2_user_session_t *session); + +/* interactively login (probably not a good general interface but whatever) */ +int l2_user_session_login(l2_user_session_t *session); + +int l2_user_update_profile(struct l2_user *user); + +int l2_user_session_fill_subst(l2_user_session_t *session, void *subst); +char *l2_user_properties_serialize(const struct l2_user_profile *profile, bool legacy); + +#endif /* include guard */ diff --git a/src/uuid/uuid.c b/src/uuid/uuid.c index e1498fc..07fc009 100644 --- a/src/uuid/uuid.c +++ b/src/uuid/uuid.c @@ -44,7 +44,7 @@ void l2_uuid_to_string(const uuid_t *id, char *out) void l2_uuid_to_string_short(const uuid_t *id, char *out) { - snprintf(out, UUID_STRLEN_SHORT + 1, "%08" PRIx64 "%08" PRIx64, id->halves[1], id->halves[0]); + snprintf(out, UUID_STRLEN_SHORT + 1, "%016" PRIx64 "%016" PRIx64, id->halves[1], id->halves[0]); } bool l2_uuid_from_string(uuid_t *id, const char *str) diff --git a/src/version.c b/src/version.c index a80988c..6aa32db 100644 --- a/src/version.c +++ b/src/version.c @@ -496,7 +496,6 @@ unsigned l2_version__load_all_from_json(json_t *json) unsigned l2_version__add_remote(json_t *js) { struct l2_version_remote *ver = NULL; - struct tm ts; json_t *val; unsigned res = VERSION_SUCCESS; @@ -533,15 +532,11 @@ unsigned l2_version__add_remote(json_t *js) val = json_object_get(js, "time"); if (!json_is_string(val)) goto cleanup; - memset(&ts, 0, sizeof(struct tm)); - if (!l2_launcher_parse_iso_time(json_string_value(val), &ts)) goto cleanup; - ver->update_time = mktime(&ts); + if (l2_parse_time(json_string_value(val), &ver->update_time) < 0) goto cleanup; val = json_object_get(js, "releaseTime"); if (!json_is_string(val)) goto cleanup; - memset(&ts, 0, sizeof(struct tm)); - if (!l2_launcher_parse_iso_time(json_string_value(val), &ts)) goto cleanup; - ver->release_time = mktime(&ts); + if (l2_parse_time(json_string_value(val), &ver->release_time) < 0) goto cleanup; /* add the thing to the global list */ if (l2_state.ver_remote_tail) { |
