summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorAylur <[email protected]>2024-11-05 21:23:38 +0100
committerGitHub <[email protected]>2024-11-05 21:23:38 +0100
commitca6a37c98a784bcb652ebe3737c906274c0ff1ab (patch)
tree62d4ab53ce3f47a6927d47b5b1faeae1ef6baed7 /lib
parent8c6d2185a68e9f09d6780284d1b61fcfaefa655b (diff)
parent524030ae79f5cffafef3ef09036064070e814879 (diff)
Merge pull request #66 from Aylur/feat/greetd
feat: greetd ipc client
Diffstat (limited to 'lib')
-rw-r--r--lib/greet/cli.vala72
-rw-r--r--lib/greet/client.vala267
-rw-r--r--lib/greet/config.vala.in6
l---------lib/greet/gir.py1
-rw-r--r--lib/greet/meson.build109
-rw-r--r--lib/greet/meson_options.txt11
-rw-r--r--lib/greet/version1
7 files changed, 467 insertions, 0 deletions
diff --git a/lib/greet/cli.vala b/lib/greet/cli.vala
new file mode 100644
index 0000000..946ec72
--- /dev/null
+++ b/lib/greet/cli.vala
@@ -0,0 +1,72 @@
+static bool help;
+static bool version;
+static string username;
+static string password;
+static string cmd;
+[CCode (array_length = false, array_null_terminated = true)]
+static string[] env;
+
+const OptionEntry[] options = {
+ { "version", 'v', OptionFlags.NONE, OptionArg.NONE, ref version, null, null },
+ { "help", 'h', OptionFlags.NONE, OptionArg.NONE, ref help, null, null },
+ { "username", 'u', OptionFlags.NONE, OptionArg.STRING, ref username, null, null },
+ { "password", 'p', OptionFlags.NONE, OptionArg.STRING, ref password, null, null },
+ { "cmd", 'c', OptionFlags.NONE, OptionArg.STRING, ref cmd, null, null },
+ { "env", 'e', OptionFlags.NONE, OptionArg.STRING_ARRAY, ref env, null, null },
+ { null },
+};
+
+async int main(string[] argv) {
+ try {
+ var opts = new OptionContext();
+ opts.add_main_entries(options, null);
+ opts.set_help_enabled(false);
+ opts.set_ignore_unknown_options(false);
+ opts.parse(ref argv);
+ } catch (OptionError err) {
+ printerr (err.message);
+ return 1;
+ }
+
+ if (help) {
+ print("Usage:\n");
+ print(" %s [flags]\n\n", argv[0]);
+ print("Flags:\n");
+ print(" -h, --help Print this help and exit\n");
+ print(" -v, --version Print version number and exit\n");
+ print(" -u, --username User to login to\n");
+ print(" -p, --password Password of the user\n");
+ print(" -c, --cmd Command to start the session with\n");
+ print(" -e, --env Additional env vars to set for the session\n");
+ return 0;
+ }
+
+ if (version) {
+ printerr(AstalGreet.VERSION);
+ return 0;
+ }
+
+ if (username == null) {
+ printerr("missing username\n");
+ return 1;
+ }
+
+ if (password == null) {
+ printerr("missing password\n");
+ return 1;
+ }
+
+ if (cmd == null) {
+ printerr("missing cmd\n");
+ return 1;
+ }
+
+ try {
+ yield AstalGreet.login_with_env(username, password, cmd, env);
+ } catch (Error err) {
+ printerr(err.message);
+ return 1;
+ }
+
+ return 0;
+}
diff --git a/lib/greet/client.vala b/lib/greet/client.vala
new file mode 100644
index 0000000..ee220b5
--- /dev/null
+++ b/lib/greet/client.vala
@@ -0,0 +1,267 @@
+namespace AstalGreet {
+/**
+ * Shorthand for creating a session, posting the password,
+ * and starting the session with the given `cmd`
+ * which is parsed with [[email protected]_parse_argv].
+ *
+ * @param username User to login to
+ * @param password Password of the user
+ * @param cmd Command to start the session with
+ */
+public async void login(
+ string username,
+ string password,
+ string cmd
+) throws GLib.Error {
+ yield login_with_env(username, password, cmd, {});
+}
+
+/**
+ * Same as [[email protected]] but allow for setting additonal env
+ * in the form of `name=value` pairs.
+ *
+ * @param username User to login to
+ * @param password Password of the user
+ * @param cmd Command to start the session with
+ * @param env Additonal env vars to set for the session
+ */
+public async void login_with_env(
+ string username,
+ string password,
+ string cmd,
+ string[] env
+) throws GLib.Error {
+ string[] argv;
+ Shell.parse_argv(cmd, out argv);
+ try {
+ yield new CreateSession(username).send();
+ yield new PostAuthMesssage(password).send();
+ yield new StartSession(argv, env).send();
+ } catch (GLib.Error err) {
+ yield new CancelSession().send();
+ throw err;
+ }
+}
+
+/**
+ * Base Request type.
+ */
+public abstract class Request : Object {
+ protected abstract string type_name { get; }
+
+ private string serialize() {
+ var node = Json.gobject_serialize(this);
+ var obj = node.get_object();
+ obj.set_string_member("type", obj.get_string_member("type-name"));
+ obj.remove_member("type-name");
+
+ return Json.to_string(node, false);
+ }
+
+ private int bytes_to_int(Bytes bytes) {
+ uint8[] data = (uint8[]) bytes.get_data();
+ int value = 0;
+
+ for (int i = 0; i < data.length; i++) {
+ value = (value << 8) | data[i];
+ }
+
+ return value;
+ }
+
+ /**
+ * Send this request to greetd.
+ */
+ public async Response send() throws GLib.Error {
+ var sock = Environment.get_variable("GREETD_SOCK");
+ if (sock == null) {
+ throw new IOError.NOT_FOUND("greetd socket not found");
+ }
+
+ var addr = new UnixSocketAddress(sock);
+ var socket = new SocketClient();
+ var conn = socket.connect(addr);
+ var payload = serialize();
+ var ostream = new DataOutputStream(conn.get_output_stream()) {
+ byte_order = DataStreamByteOrder.HOST_ENDIAN,
+ };
+
+ ostream.put_int32(payload.length, null);
+ ostream.put_string(payload, null);
+ ostream.close(null);
+
+ var istream = conn.get_input_stream();
+
+ var response_head = yield istream.read_bytes_async(4, Priority.DEFAULT, null);
+ var response_length = bytes_to_int(response_head);
+ var response_body = yield istream.read_bytes_async(response_length, Priority.DEFAULT, null);
+
+ var response = (string)response_body.get_data();
+ conn.close(null);
+
+ var parser = new Json.Parser();
+ parser.load_from_data(response);
+ var obj = parser.get_root().get_object();
+ var type = obj.get_string_member("type");
+
+ print(@"$type: $response\n");
+
+ switch (type) {
+ case Success.TYPE: return new Success(obj);
+ case Error.TYPE: return new Error(obj);
+ case AuthMessage.TYPE: return new AuthMessage(obj);
+ default: throw new IOError.NOT_FOUND("unknown response type");
+ }
+ }
+}
+
+/**
+ * Creates a session and initiates a login attempted for the given user.
+ * The session is ready to be started if a success is returned.
+ */
+public class CreateSession : Request {
+ protected override string type_name { get { return "create_session"; } }
+ public string username { get; set; }
+
+ public CreateSession(string username) {
+ Object(username: username);
+ }
+}
+
+/**
+ * Answers an authentication message.
+ * If the message was informative (info, error),
+ * then a response does not need to be set in this message.
+ * The session is ready to be started if a success is returned.
+ */
+public class PostAuthMesssage : Request {
+ protected override string type_name { get { return "post_auth_message_response"; } }
+ public string response { get; set; }
+
+ public PostAuthMesssage(string response) {
+ Object(response: response);
+ }
+}
+
+/**
+ * Requests for the session to be started using the provided command line,
+ * adding the supplied environment to that created by PAM.
+ * The session will start after the greeter process terminates
+ */
+public class StartSession : Request {
+ protected override string type_name { get { return "start_session"; } }
+ public string[] cmd { get; set; }
+ public string[] env { get; set; }
+
+ public StartSession(string[] cmd, string[] env = {}) {
+ Object(cmd: cmd, env: env);
+ }
+}
+
+/**
+ * Cancels the session that is currently under configuration.
+ */
+public class CancelSession : Request {
+ internal override string type_name { get { return "cancel_session"; } }
+}
+
+/**
+ * Base Response type.
+ */
+public abstract class Response : Object {
+ // nothing to do
+}
+
+/**
+ * Indicates that the request succeeded.
+ */
+public class Success : Response {
+ internal const string TYPE = "success";
+
+ internal Success(Json.Object obj) {
+ // nothing to do
+ }
+}
+
+/**
+ * Indicates that the request succeeded.
+ */
+public class Error : Response {
+ internal const string TYPE = "error";
+
+ public enum Type {
+ /**
+ * Indicates that authentication failed.
+ * This is not a fatal error, and is likely caused by incorrect credentials.
+ */
+ AUTH_ERROR,
+ /**
+ * A general error.
+ * See the error description for more information.
+ */
+ ERROR;
+
+ internal static Type from_string(string str) throws IOError {
+ switch (str) {
+ case "auth_error": return Type.AUTH_ERROR;
+ case "error": return Type.ERROR;
+ default: throw new IOError.FAILED(@"unknown error_type: $str");
+ }
+ }
+ }
+
+ public Type error_type { get; private set; }
+ public string description { get; private set; }
+
+ internal Error(Json.Object obj) throws IOError {
+ error_type = Type.from_string(obj.get_string_member("error_type"));
+ description = obj.get_string_member("description");
+ }
+}
+
+/**
+ * Indicates that the request succeeded.
+ */
+public class AuthMessage : Response {
+ internal const string TYPE = "auth_message";
+
+ public enum Type {
+ /**
+ * Indicates that input from the user should be
+ * visible when they answer this question.
+ */
+ VISIBLE,
+ /**
+ * Indicates that input from the user should be
+ * considered secret when they answer this question.
+ */
+ SECRET,
+ /**
+ * Indicates that this message is informative, not a question.
+ */
+ INFO,
+ /**
+ * Indicates that this message is an error, not a question.
+ */
+ ERROR;
+
+ internal static Type from_string(string str) throws IOError {
+ switch (str) {
+ case "visible": return VISIBLE;
+ case "secret": return Type.SECRET;
+ case "info": return Type.INFO;
+ case "error": return Type.ERROR;
+ default: throw new IOError.FAILED(@"unknown message_type: $str");
+ }
+ }
+ }
+
+ public Type message_type { get; private set; }
+ public string message { get; private set; }
+
+ internal AuthMessage(Json.Object obj) throws IOError {
+ message_type = Type.from_string(obj.get_string_member("auth_message_type"));
+ message = obj.get_string_member("auth_message");
+ }
+}
+}
diff --git a/lib/greet/config.vala.in b/lib/greet/config.vala.in
new file mode 100644
index 0000000..333d735
--- /dev/null
+++ b/lib/greet/config.vala.in
@@ -0,0 +1,6 @@
+namespace AstalGreet {
+ public const int MAJOR_VERSION = @MAJOR_VERSION@;
+ public const int MINOR_VERSION = @MINOR_VERSION@;
+ public const int MICRO_VERSION = @MICRO_VERSION@;
+ public const string VERSION = "@VERSION@";
+}
diff --git a/lib/greet/gir.py b/lib/greet/gir.py
new file mode 120000
index 0000000..b5b4f1d
--- /dev/null
+++ b/lib/greet/gir.py
@@ -0,0 +1 @@
+../gir.py \ No newline at end of file
diff --git a/lib/greet/meson.build b/lib/greet/meson.build
new file mode 100644
index 0000000..11321b0
--- /dev/null
+++ b/lib/greet/meson.build
@@ -0,0 +1,109 @@
+project(
+ 'astal-greet',
+ 'vala',
+ 'c',
+ version: run_command('cat', join_paths(meson.project_source_root(), 'version')).stdout().strip(),
+ meson_version: '>= 0.62.0',
+ default_options: [
+ 'warning_level=2',
+ 'werror=false',
+ 'c_std=gnu11',
+ ],
+)
+
+assert(
+ get_option('lib') or get_option('cli'),
+ 'Either lib or cli option must be set to true.',
+)
+
+version_split = meson.project_version().split('.')
+api_version = version_split[0] + '.' + version_split[1]
+gir = 'AstalGreet-' + api_version + '.gir'
+typelib = 'AstalGreet-' + api_version + '.typelib'
+
+config = configure_file(
+ input: 'config.vala.in',
+ output: 'config.vala',
+ configuration: {
+ 'VERSION': meson.project_version(),
+ 'MAJOR_VERSION': version_split[0],
+ 'MINOR_VERSION': version_split[1],
+ 'MICRO_VERSION': version_split[2],
+ },
+)
+
+deps = [
+ dependency('glib-2.0'),
+ dependency('gobject-2.0'),
+ dependency('gio-unix-2.0'),
+ dependency('json-glib-1.0'),
+]
+
+sources = [config] + files(
+ 'client.vala',
+)
+
+if get_option('lib')
+ lib = library(
+ meson.project_name(),
+ sources,
+ dependencies: deps,
+ vala_args: ['--vapi-comments'],
+ vala_header: meson.project_name() + '.h',
+ vala_vapi: meson.project_name() + '-' + api_version + '.vapi',
+ version: meson.project_version(),
+ install: true,
+ install_dir: [true, true, true],
+ )
+
+ pkgs = []
+ foreach dep : deps
+ pkgs += ['--pkg=' + dep.name()]
+ endforeach
+
+ gir_tgt = custom_target(
+ gir,
+ command: [find_program('python3'), files('gir.py'), meson.project_name(), gir]
+ + pkgs
+ + sources,
+ input: sources,
+ depends: lib,
+ output: gir,
+ install: true,
+ install_dir: get_option('datadir') / 'gir-1.0',
+ )
+
+ custom_target(
+ typelib,
+ command: [
+ find_program('g-ir-compiler'),
+ '--output', '@OUTPUT@',
+ '--shared-library', get_option('prefix') / get_option('libdir') / '@PLAINNAME@',
+ meson.current_build_dir() / gir,
+ ],
+ input: lib,
+ output: typelib,
+ depends: [lib, gir_tgt],
+ install: true,
+ install_dir: get_option('libdir') / 'girepository-1.0',
+ )
+
+ import('pkgconfig').generate(
+ lib,
+ name: meson.project_name(),
+ filebase: meson.project_name() + '-' + api_version,
+ version: meson.project_version(),
+ subdirs: meson.project_name(),
+ requires: deps,
+ install_dir: get_option('libdir') / 'pkgconfig',
+ )
+endif
+
+if get_option('cli')
+ executable(
+ meson.project_name(),
+ ['cli.vala', sources],
+ dependencies: deps,
+ install: true,
+ )
+endif
diff --git a/lib/greet/meson_options.txt b/lib/greet/meson_options.txt
new file mode 100644
index 0000000..f110242
--- /dev/null
+++ b/lib/greet/meson_options.txt
@@ -0,0 +1,11 @@
+option(
+ 'lib',
+ type: 'boolean',
+ value: true,
+)
+
+option(
+ 'cli',
+ type: 'boolean',
+ value: true,
+)
diff --git a/lib/greet/version b/lib/greet/version
new file mode 100644
index 0000000..6e8bf73
--- /dev/null
+++ b/lib/greet/version
@@ -0,0 +1 @@
+0.1.0