aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorLibravatar bigfoot547 <[email protected]>2024-01-11 00:39:56 -0600
committerLibravatar bigfoot547 <[email protected]>2024-01-11 00:39:56 -0600
commitb837ef02aff4c0974161ff2e077551a9710fdac5 (patch)
tree3ae0af26264f2ec62af5ebf4391ef6399f67354f /src
parentactually use runtime (diff)
add auth
Diffstat (limited to 'src')
-rw-r--r--src/cmd-version.c71
-rw-r--r--src/l2su.h8
-rw-r--r--src/launch.c60
-rw-r--r--src/launch.h3
-rw-r--r--src/launcherutil.c135
-rw-r--r--src/macros.h23
-rw-r--r--src/meson.build2
-rw-r--r--src/user.c1514
-rw-r--r--src/user.h63
-rw-r--r--src/uuid/uuid.c2
-rw-r--r--src/version.c9
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");
}
diff --git a/src/l2su.h b/src/l2su.h
index 4353266..a455b58 100644
--- a/src/l2su.h
+++ b/src/l2su.h
@@ -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", &notafter, "Token", &xbl_tok) < 0) {
+ CMD_WARN0("Failed to unpack XBL login response");
+ goto cleanup;
+ }
+
+ time_t natime;
+ if (l2_parse_time(notafter, &natime) < 0) {
+ CMD_WARN("Could not read XBL login response: failed to parse \"NotAfter\" time: %s", notafter);
+ goto cleanup;
+ }
+
+ xbl_tok_dup = strdup(xbl_tok);
+ if (!xbl_tok_dup) goto cleanup;
+
+ session->xbl_token = xbl_tok_dup;
+ xbl_tok_dup = NULL;
+
+ session->xbl_token_exp = natime;
+
+ ret = 1;
+
+cleanup:
+ free(xbl_tok_dup);
+
+ curl_easy_cleanup(curl);
+
+ if (rqjson) json_decref(rqjson);
+ if (rjson) json_decref(rjson);
+ free(rqjson_str);
+
+ curl_slist_free_all(headers);
+
+ return ret;
+}
+
+json_t *l2_user__get_xsts(const char *xbl_token, const char *relying_party)
+{
+ struct curl_slist *headers = NULL, *temp;
+ char *rqjson_str = NULL;
+ json_t *rjson = NULL;
+
+ CURL *curl = curl_easy_init();
+ if (!curl) return NULL;
+
+ json_t *rqjson = json_pack("{s:{s:s, s:[s]}, s:s, s:s}", "Properties", "SandboxId", "RETAIL", "UserTokens", xbl_token, "RelyingParty", relying_party, "TokenType", "JWT");
+ if (!rqjson) goto cleanup;
+
+ rqjson_str = json_dumps(rqjson, 0);
+ if (!rqjson_str) goto cleanup;
+
+ headers = curl_slist_append(NULL, "Accept: application/json");
+ if (!headers) goto cleanup;
+
+ temp = curl_slist_append(headers, "Content-type: application/json");
+ if (!temp) goto cleanup;
+ headers = temp;
+
+ curl_easy_setopt(curl, CURLOPT_POSTFIELDS, rqjson_str);
+ curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
+
+ rjson = l2_user__microsoft_request(curl, L2_MSA_URL_XBOX_XSTS);
+ goto cleanup_done;
+
+cleanup:
+ if (rjson) {
+ json_decref(rjson);
+ rjson = NULL;
+ }
+
+cleanup_done:
+ if (curl) curl_easy_cleanup(curl);
+ if (rqjson) json_decref(rqjson);
+
+ curl_slist_free_all(headers);
+ return rjson;
+}
+
+int l2_user__refresh_xbl_xsts(time_t nowish, l2_user_session_t *session)
+{
+ int refreshres;
+ if (!session->xbl_token || session->xbl_token_exp - nowish < 300) {
+ CMD_DEBUG0("Cannot refresh XBL XSTS token (XBL token is missing or expired)");
+ refreshres = l2_user__login_xbox_live(nowish, session);
+ if (refreshres <= 0) return refreshres;
+ }
+
+ int ret = -1;
+ json_t *xstsinfo = l2_user__get_xsts(session->xbl_token, "http://xboxlive.com");
+ if (!xstsinfo) return -1;
+
+ const char *xuidstr;
+ char *xuidstr_end;
+ uint64_t xuidval;
+
+ if (json_unpack(xstsinfo, "{s:{s:[{s:s}]}}", "DisplayClaims", "xui", "xid", &xuidstr) < 0) {
+ CMD_WARN0("Failed to unpack XBL XSTS response (for XUID)");
+ goto cleanup;
+ }
+
+ xuidval = (uint64_t)strtoull(xuidstr, &xuidstr_end, 10);
+ if (*xuidstr_end) {
+ CMD_WARN("Failed to read XBL XSTS response: XUID contained invalid characters: %s", xuidstr);
+ goto cleanup;
+ }
+
+ session->xuid.uuid_ms = 0;
+ session->xuid.uuid_ls = xuidval;
+ session->xuid_present = true;
+
+ ret = 1;
+
+cleanup:
+ if (xstsinfo) json_decref(xstsinfo);
+ return ret;
+}
+
+int l2_user__refresh_mc_xsts(time_t nowish, l2_user_session_t *session)
+{
+ int refreshres;
+ if (!session->xbl_token || session->xbl_token_exp - nowish < 300) {
+ CMD_DEBUG0("Cannot refresh Minecraft XSTS token (XBL token is missing or expired)");
+ refreshres = l2_user__login_xbox_live(nowish, session);
+ if (refreshres <= 0) return refreshres;
+ }
+
+ int res = -1;
+ json_t *xstsinfo = l2_user__get_xsts(session->xbl_token, "rp://api.minecraftservices.com/");
+
+ const char *nastr;
+ const char *uhstr;
+ const char *mc_xsts_tok;
+
+ char *uhstr_dup = NULL;
+ char *mc_xsts_tok_dup = NULL;
+
+ if (json_unpack(xstsinfo, "{s:s, s:s, s:{s:[{s:s}]}}", "NotAfter", &nastr, "Token", &mc_xsts_tok, "DisplayClaims", "xui", "uhs", &uhstr) < 0) {
+ CMD_WARN0("Failed to unpack MC XSTS response (for UHS and token)");
+ goto cleanup;
+ }
+
+ time_t natime;
+ if (l2_parse_time(nastr, &natime) < 0) {
+ CMD_WARN("Could not read \"NotAfter\" time for MC XSTS token: bad format (%s)", nastr);
+ goto cleanup;
+ }
+
+ uhstr_dup = strdup(uhstr);
+ mc_xsts_tok_dup = strdup(mc_xsts_tok);
+ if (!uhstr_dup || !mc_xsts_tok_dup) goto cleanup;
+
+ session->mc_xsts_token = mc_xsts_tok_dup;
+ session->user_hash = uhstr_dup;
+ session->mc_xsts_token_exp = natime;
+
+ mc_xsts_tok_dup = NULL;
+ uhstr_dup = NULL;
+
+ res = 1;
+
+cleanup:
+ if (xstsinfo) json_decref(xstsinfo);
+ free(uhstr_dup);
+ free(mc_xsts_tok_dup);
+ return res;
+}
+
+int l2_user__minecraft_login(time_t nowish, l2_user_session_t *session)
+{
+ int refreshres;
+ CMD_DEBUG0("Logging into Minecraft");
+ if (!session->mc_xsts_token || !session->user_hash || session->mc_xsts_token_exp - nowish < 300) {
+ CMD_DEBUG0("Cannot log in (minecraft XSTS token or user_hash missing).");
+ refreshres = l2_user__refresh_mc_xsts(nowish, session);
+ if (refreshres <= 0) return refreshres;
+ }
+
+ if (!session->xuid_present) {
+ CMD_DEBUG0("Cannot log in (XUID missing).");
+ refreshres = l2_user__refresh_xbl_xsts(nowish, session);
+ if (refreshres <= 0) return refreshres;
+ }
+
+ int ret = -1;
+
+ json_t *jpostbody = NULL;
+ char *postbody = NULL;
+ struct curl_slist *headers = NULL, *temp;
+ char *tokendup = NULL;
+ json_t *rjson = NULL;
+
+ CURL *curl = curl_easy_init();
+ if (!curl) return -1;
+
+ jpostbody = json_pack("{s:s+++}", "identityToken", "XBL3.0 x=", session->user_hash, ";", session->mc_xsts_token);
+ if (!jpostbody) goto cleanup;
+
+ postbody = json_dumps(jpostbody, 0);
+ if (!postbody) goto cleanup;
+
+ headers = curl_slist_append(NULL, "Accept: application/json");
+ if (!headers) goto cleanup;
+
+ temp = curl_slist_append(headers, "Content-type: application/json");
+ if (!temp) goto cleanup;
+ headers = temp;
+
+ curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postbody);
+ curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
+
+ rjson = l2_user__microsoft_request(curl, L2_MSA_URL_MINECRAFT_LOGIN);
+ if (!rjson) goto cleanup;
+
+ const char *token;
+ json_int_t expires_in;
+ if (json_unpack(rjson, "{s:s, s:I}", "access_token", &token, "expires_in", &expires_in) < 0) {
+ CMD_WARN0("Could unpack Minecraft login response");
+ goto cleanup;
+ }
+
+ tokendup = strdup(token);
+ if (!tokendup) goto cleanup;
+
+ session->moj_token = tokendup;
+ session->moj_token_exp = time(NULL) + expires_in;
+
+ tokendup = NULL;
+ ret = 1;
+
+cleanup:
+ json_decref(jpostbody);
+ free(postbody);
+ curl_slist_free_all(headers);
+ curl_easy_cleanup(curl);
+
+ free(tokendup);
+ json_decref(rjson);
+
+ return ret;
+}
+
+/* https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow */
+
+int l2_user_session_refresh(l2_user_session_t *session)
+{
+ time_t now = time(NULL);
+
+ if (session->xuid_present && session->moj_token_exp >= 0 && session->moj_token_exp - now > L2_USER__MOJ_TOKEN_EXPIRE_EARLY) {
+ CMD_DEBUG("Mojang token good (expires in %jd seconds), and XUID exists! Not refreshing.", (intmax_t)(now - session->moj_token_exp));
+ return 1;
+ }
+
+ return l2_user__minecraft_login(now, session);
+}
+
+json_t *l2_user__request_device_code(void)
+{
+ json_t *ret = NULL;
+
+ CURL *curl = curl_easy_init();
+ if (!curl) return NULL;
+
+ char *esc_clientid = NULL;
+ char *esc_scopes = NULL;
+ char *post_body = NULL;
+ struct curl_slist *headers = NULL;
+
+ esc_clientid = curl_easy_escape(curl, L2_MSA_CLIENT_ID, 0);
+ esc_scopes = curl_easy_escape(curl, L2_MSA_SCOPES, 0);
+
+ if (!esc_scopes || !esc_clientid) {
+ goto cleanup;
+ }
+
+ post_body = l2_launcher_sprintf_alloc("client_id=%s&scope=%s", esc_clientid, esc_scopes);
+ if (!post_body) goto cleanup;
+
+ headers = curl_slist_append(NULL, "Accept: application/json");
+ if (!headers) goto cleanup;
+
+ curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post_body);
+ curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
+
+ ret = l2_user__microsoft_request(curl, "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode");
+
+cleanup:
+ if (curl) curl_easy_cleanup(curl);
+
+ curl_slist_free_all(headers);
+ curl_free(esc_clientid);
+ curl_free(esc_scopes);
+ free(post_body);
+
+ return ret;
+}
+
+enum {
+ AUTH_SUCCESS,
+ AUTH_ERROR_PENDING,
+ AUTH_ERROR_DECLINED,
+ AUTH_ERROR_EXPIRED_TOKEN,
+ AUTH_ERROR_OTHER
+};
+
+int l2_user__try_device_auth(l2_user_session_t *session, const char *device_code, json_int_t *wait_interval)
+{
+ int ret = AUTH_ERROR_OTHER;
+
+ CURL *curl = curl_easy_init();
+ if (!curl) return AUTH_ERROR_OTHER;
+
+ char *esc_clientid = NULL;
+ char *esc_dev_code = NULL;
+ char *post_body = NULL;
+ struct curl_slist *headers = NULL;
+
+ void *rdata = NULL;
+ size_t rsize;
+ json_t *rjson = NULL;
+
+ char *access_token_dup = NULL;
+ char *refresh_token_dup = NULL;
+
+ esc_clientid = curl_easy_escape(curl, L2_MSA_CLIENT_ID, 0);
+ esc_dev_code = curl_easy_escape(curl, device_code, 0);
+
+ if (!esc_clientid || !esc_dev_code) goto cleanup;
+
+ post_body = l2_launcher_sprintf_alloc("grant_type=%s&client_id=%s&device_code=%s", "urn:ietf:params:oauth:grant-type:device_code", esc_clientid, esc_dev_code);
+ if (!post_body) goto cleanup;
+
+ headers = curl_slist_append(NULL, "Accept: application/json");
+ if (!headers) goto cleanup;
+
+ curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post_body);
+ curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
+
+ if (l2_launcher_download(curl, "https://login.microsoftonline.com/consumers/oauth2/v2.0/token", &rdata, &rsize) < 0) {
+ goto cleanup;
+ }
+
+ long httpres = 0;
+ curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpres);
+
+ json_error_t err;
+ rjson = json_loadb(rdata, rsize, JSON_REJECT_DUPLICATES, &err);
+ if (!rjson) {
+ CMD_WARN("Failed to parse token response: JSON parse error: %s", err.text);
+ goto cleanup;
+ }
+
+#ifndef NDEBUG
+ CMD_DEBUG("result of code thing: %ld", httpres);
+ json_dumpf(rjson, stderr, JSON_INDENT(4));
+ fputc('\n', stderr);
+#endif
+
+ const char *errcode = NULL, *errdesc = NULL;
+ json_unpack(rjson, "{s?:s, s?:s}", "error", &errcode, "error_description", &errdesc);
+ if (errcode || httpres / 100 != 2) {
+ if (!strcmp(errcode, "authorization_pending")) {
+ json_unpack(rjson, "{s:I}", "interval", wait_interval);
+
+ ret = AUTH_ERROR_PENDING;
+ } else if (!strcmp(errcode, "authorization_declined")) {
+ ret = AUTH_ERROR_DECLINED;
+ } else if (!strcmp(errcode, "expired_token")) {
+ ret = AUTH_ERROR_EXPIRED_TOKEN;
+ } else {
+ CMD_WARN("Error receiving token (device code flow): (HTTP %ld) %s: %s", httpres, errcode, errdesc);
+ ret = AUTH_ERROR_OTHER;
+ }
+ goto cleanup;
+ }
+
+ const char *access_token;
+ const char *refresh_token;
+ json_int_t access_token_exp_in;
+
+ if (json_unpack(rjson, "{s:s, s:s, s:I}", "access_token", &access_token, "refresh_token", &refresh_token, "expires_in", &access_token_exp_in) < 0) {
+ CMD_WARN0("Failed to unpack device code response");
+ goto cleanup;
+ }
+
+ access_token_dup = strdup(access_token);
+ refresh_token_dup = strdup(refresh_token);
+
+ if (!access_token_dup || !refresh_token_dup) goto cleanup;
+
+ session->access_token = access_token_dup;
+ session->refresh_token = refresh_token_dup;
+ session->access_token_exp = access_token_exp_in + time(NULL);
+
+ access_token_dup = NULL;
+ refresh_token_dup = NULL;
+ ret = AUTH_SUCCESS;
+
+cleanup:
+ if (curl) curl_easy_cleanup(curl);
+
+ curl_free(esc_clientid);
+ curl_free(esc_dev_code);
+ free(post_body);
+
+ curl_slist_free_all(headers);
+
+ free(rdata);
+ json_decref(rjson);
+
+ free(access_token_dup);
+ free(refresh_token_dup);
+
+ return ret;
+}
+
+int l2_user_session_login(l2_user_session_t *session)
+{
+ /* TODO call first function then call second function in a loop */
+ int ret = -1;
+ json_t *code = l2_user__request_device_code();
+ if (!code) {
+ CMD_WARN0("Failed to request device code.");
+ return -1;
+ }
+
+ const char *user_message;
+ const char *device_code;
+ json_int_t interval;
+ if (json_unpack(code, "{s:s, s:s, s:I}", "message", &user_message, "device_code", &device_code, "interval", &interval) < 0) {
+ CMD_WARN0("Failed to unpack device code response");
+ goto cleanup;
+ }
+
+ CMD_INFO("%s", user_message);
+
+ int authres;
+ do {
+ sleep(interval);
+ authres = l2_user__try_device_auth(session, device_code, &interval);
+ } while (authres == AUTH_ERROR_PENDING);
+
+ switch (authres) {
+ case AUTH_ERROR_DECLINED:
+ case AUTH_ERROR_EXPIRED_TOKEN:
+ ret = 0;
+ CMD_WARN0("Authentication declined or expired.");
+ goto cleanup;
+ case AUTH_ERROR_OTHER:
+ goto cleanup;
+ case AUTH_SUCCESS:
+ ret = 1;
+ }
+
+ CMD_DEBUG0("Successfully logged in, now get mojang token");
+ ret = l2_user_session_refresh(session);
+ if (ret <= 0) {
+ CMD_DEBUG0("Failed to refresh session :(");
+ ret = -1;
+ }
+
+cleanup:
+ json_decref(code);
+
+ return ret;
+}
+
+json_t *l2_user__load_my_profile(l2_user_session_t *session)
+{
+ if (!session->moj_token) {
+ CMD_DEBUG0("Can't load profile if there is no mojang token!");
+ return NULL;
+ }
+
+ CURL *curl = NULL;
+ void *rdata = NULL;
+ size_t rsize;
+ json_t *rjson = NULL;
+ char *tokenheader = NULL;
+
+ struct curl_slist *headers = NULL, *temp = NULL;
+
+ curl = curl_easy_init();
+ if (!curl) goto cleanup;
+
+ headers = curl_slist_append(NULL, "Accept: application/json");
+ if (!headers) goto cleanup;
+
+ tokenheader = l2_launcher_sprintf_alloc("Authorization: Bearer %s", session->moj_token);
+ if (!tokenheader) goto cleanup;
+
+ temp = curl_slist_append(headers, tokenheader);
+ if (!temp) goto cleanup;
+ headers = temp;
+
+ char errbuf[CURL_ERROR_SIZE];
+ memset(errbuf, 0, sizeof(errbuf));
+
+ curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
+ curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, errbuf);
+
+ CURLcode code = l2_launcher_download(curl, L2_MC_API_PROFILE, &rdata, &rsize);
+ if (code != CURLE_OK) {
+ CMD_WARN("Failed to download my own profile: %s: %s", curl_easy_strerror(code), errbuf);
+ goto cleanup;
+ }
+
+ json_error_t err;
+ rjson = json_loadb(rdata, rsize, JSON_REJECT_DUPLICATES, &err);
+ if (!rjson) {
+ CMD_WARN("Failed to parse JSON profile response: %s", err.text);
+ goto cleanup;
+ }
+
+#ifndef NDEBUG
+ CMD_DEBUG0("My profile response:");
+ json_dumpf(rjson, stderr, JSON_INDENT(4));
+ fputc('\n', stderr);
+#endif
+
+ long httpres = 0;
+ curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpres);
+ if (httpres == 404) {
+ goto cleanup;
+ } else if (httpres / 100 != 2) {
+ const char *error = "(no info)", *error_message = "(unknown)";
+ json_unpack(rjson, "{s?:s, s?:s}", "error", &error, "errorMessage", &error_message);
+ CMD_WARN("Error downloading profile: (HTTP %ld) %s: %s", httpres, error, error_message);
+
+ if (httpres == 404) {
+ CMD_WARN0("HTTP 404 can indicate that you haven't created a profile yet.");
+ }
+
+ goto cleanup;
+ }
+
+ curl_easy_cleanup(curl);
+ curl_slist_free_all(headers);
+ free(rdata);
+ return rjson;
+
+cleanup:
+ if (curl) curl_easy_cleanup(curl);
+
+ curl_slist_free_all(headers);
+ free(rdata);
+ free(tokenheader);
+ json_decref(rjson);
+
+ return NULL;
+}
+
+int l2_user__fill_profile(const uuid_t *uuid, struct l2_user_profile *profile)
+{
+ int ret = -1;
+
+ CURL *curl = NULL;
+ struct curl_slist *headers = NULL;
+ void *rdata = NULL;
+ size_t rsize;
+ json_t *rjson = NULL;
+
+ char *enc_uuid = NULL;
+
+ curl = curl_easy_init();
+
+ headers = curl_slist_append(NULL, "Accept: application/json");
+ if (!headers) goto cleanup;
+
+ curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
+
+ char uuidstr[UUID_STRLEN_SHORT + 1];
+ l2_uuid_to_string_short(uuid, uuidstr);
+ enc_uuid = curl_easy_escape(curl, uuidstr, 0);
+
+ char *url;
+ size_t urllen;
+ L2_ASPRINTF(url, urllen, L2_MC_API_OPROFILE_FORMAT, enc_uuid);
+
+ CURLcode code = l2_launcher_download(curl, url, &rdata, &rsize);
+ if (code != CURLE_OK) {
+ CMD_WARN("Failed to download profile of %s: %s", uuidstr, curl_easy_strerror(code));
+ goto cleanup;
+ }
+
+ json_error_t err;
+ rjson = json_loadb(rdata, rsize, JSON_REJECT_DUPLICATES, &err);
+ if (!rjson) {
+ CMD_WARN("Failed to parse JSON of profile: %s", err.text);
+ goto cleanup;
+ }
+
+#ifndef NDEBUG
+ CMD_DEBUG("%s profile response:", uuidstr);
+ json_dumpf(rjson, stderr, JSON_INDENT(4));
+ fputc('\n', stderr);
+#endif
+
+ long httpres;
+ curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpres);
+
+ const char *error_msg = "(unknown)";
+ json_unpack(rjson, "{s?:s}", "errorMessage", &error_msg);
+ if (httpres / 100 != 2) {
+ CMD_WARN("Failed to download profile: API error: (HTTP %ld) %s", httpres, error_msg);
+ goto cleanup;
+ }
+
+ ret = l2_user__load_user_profile(rjson, profile);
+
+cleanup:
+ if (curl) curl_easy_cleanup(curl);
+ curl_slist_free_all(headers);
+
+ free(rdata);
+ json_decref(rjson);
+ free(enc_uuid);
+
+ return ret;
+}
+
+int l2_user_update_profile(struct l2_user *user)
+{
+ int ret = -1;
+ json_t *jprofileinfo = l2_user__load_my_profile(user->session);
+ if (!jprofileinfo) return -1;
+
+ const char *uuidstr;
+ if (json_unpack(jprofileinfo, "{s:s}", "id", &uuidstr) < 0) {
+ CMD_WARN0("Invalid profile response (couldn't get UUID string)");
+ goto cleanup;
+ }
+
+ uuid_t id;
+ if (!l2_uuid_from_string_short(&id, uuidstr)) {
+ CMD_WARN("Invalid short UUID string %s", uuidstr);
+ goto cleanup;
+ }
+
+ ret = l2_user__fill_profile(&id, &user->profile);
+ CMD_DEBUG("fill_profile: %d", ret);
+
+cleanup:
+ json_decref(jprofileinfo);
+ return ret;
+}
+
+int l2_user_session_fill_subst(l2_user_session_t *session, void *subst_)
+{
+ l2_subst_t *subst = subst_;
+
+ if (session && session->moj_token && session->xuid_present) {
+ char xuidstr[UUID_STRLEN + 1];
+ l2_uuid_to_string(&session->xuid, xuidstr);
+
+ if (l2_subst_add(subst, "auth_access_token", session->moj_token) < 0) goto cleanup;
+ if (l2_subst_add(subst, "auth_xuid", xuidstr) < 0) goto cleanup;
+ if (l2_subst_add(subst, "clientid", L2_MSA_CLIENT_ID) < 0) goto cleanup;
+ if (l2_subst_add(subst, "auth_session", session->moj_token) < 0) goto cleanup;
+ if (l2_subst_add(subst, "user_type", "msa") < 0) goto cleanup;
+ } else {
+ if (l2_subst_add(subst, "auth_access_token", "-") < 0) goto cleanup;
+ if (l2_subst_add(subst, "auth_xuid", "null") < 0) goto cleanup;
+ if (l2_subst_add(subst, "clientid", "null") < 0) goto cleanup;
+ if (l2_subst_add(subst, "auth_session", "-") < 0) goto cleanup;
+ if (l2_subst_add(subst, "user_type", "msa") < 0) goto cleanup;
+ }
+
+ return 0;
+
+cleanup:
+ return -1;
+}
+
+char *l2_user_properties_serialize(const struct l2_user_profile *profile, bool legacy)
+{
+ json_t *jret;
+
+ if (legacy) {
+ if (!(jret = json_object())) goto cleanup;
+
+ for (size_t propidx = 0; propidx < profile->nproperties; ++propidx) {
+ json_t *val = json_object_get(jret, profile->properties[propidx].name);
+ if (!val) {
+ if (json_object_set_new(jret, profile->properties[propidx].name, val = json_array()) < 0) goto cleanup;
+ }
+
+ if (json_array_append_new(val, json_string(profile->properties[propidx].value)) < 0) goto cleanup;
+ }
+ } else {
+ if (!(jret = json_array())) goto cleanup;
+
+ for (size_t propidx = 0; propidx < profile->nproperties; ++propidx) {
+ if (json_array_append_new(jret, json_pack("{s:s, s:s, s:s*}",
+ "name", profile->properties[propidx].name,
+ "value", profile->properties[propidx].value,
+ "signature", profile->properties[propidx].signature)) < 0) goto cleanup;
+ }
+ }
+
+ char *retstr = json_dumps(jret, 0);
+ if (!retstr) goto cleanup;
+ json_decref(jret);
+ return retstr;
+
+cleanup:
+ json_decref(jret);
+ return NULL;
+}
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) {