summaryrefslogtreecommitdiff
path: root/Userland/Services/LaunchServer
diff options
context:
space:
mode:
Diffstat (limited to 'Userland/Services/LaunchServer')
-rw-r--r--Userland/Services/LaunchServer/CMakeLists.txt13
-rw-r--r--Userland/Services/LaunchServer/ClientConnection.cpp159
-rw-r--r--Userland/Services/LaunchServer/ClientConnection.h64
-rw-r--r--Userland/Services/LaunchServer/LaunchClient.ipc4
-rw-r--r--Userland/Services/LaunchServer/LaunchServer.ipc12
-rw-r--r--Userland/Services/LaunchServer/Launcher.cpp309
-rw-r--r--Userland/Services/LaunchServer/Launcher.h78
-rw-r--r--Userland/Services/LaunchServer/main.cpp65
8 files changed, 704 insertions, 0 deletions
diff --git a/Userland/Services/LaunchServer/CMakeLists.txt b/Userland/Services/LaunchServer/CMakeLists.txt
new file mode 100644
index 0000000000..29308ce7f1
--- /dev/null
+++ b/Userland/Services/LaunchServer/CMakeLists.txt
@@ -0,0 +1,13 @@
+compile_ipc(LaunchServer.ipc LaunchServerEndpoint.h)
+compile_ipc(LaunchClient.ipc LaunchClientEndpoint.h)
+
+set(SOURCES
+ ClientConnection.cpp
+ Launcher.cpp
+ main.cpp
+ LaunchClientEndpoint.h
+ LaunchServerEndpoint.h
+)
+
+serenity_bin(LaunchServer)
+target_link_libraries(LaunchServer LibCore LibIPC LibDesktop)
diff --git a/Userland/Services/LaunchServer/ClientConnection.cpp b/Userland/Services/LaunchServer/ClientConnection.cpp
new file mode 100644
index 0000000000..7ac167c1ae
--- /dev/null
+++ b/Userland/Services/LaunchServer/ClientConnection.cpp
@@ -0,0 +1,159 @@
+/*
+ * Copyright (c) 2020, Nicholas Hollett <niax@niax.co.uk>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "ClientConnection.h"
+#include "Launcher.h"
+#include <AK/HashMap.h>
+#include <AK/URL.h>
+#include <LaunchServer/LaunchClientEndpoint.h>
+
+namespace LaunchServer {
+
+static HashMap<int, RefPtr<ClientConnection>> s_connections;
+ClientConnection::ClientConnection(NonnullRefPtr<Core::LocalSocket> client_socket, int client_id)
+ : IPC::ClientConnection<LaunchClientEndpoint, LaunchServerEndpoint>(*this, move(client_socket), client_id)
+{
+ s_connections.set(client_id, *this);
+}
+
+ClientConnection::~ClientConnection()
+{
+}
+
+void ClientConnection::die()
+{
+ s_connections.remove(client_id());
+}
+
+OwnPtr<Messages::LaunchServer::GreetResponse> ClientConnection::handle(const Messages::LaunchServer::Greet&)
+{
+ return make<Messages::LaunchServer::GreetResponse>(client_id());
+}
+
+OwnPtr<Messages::LaunchServer::OpenURLResponse> ClientConnection::handle(const Messages::LaunchServer::OpenURL& request)
+{
+ if (!m_allowlist.is_empty()) {
+ bool allowed = false;
+ for (auto& allowed_handler : m_allowlist) {
+ if (allowed_handler.handler_name == request.handler_name()
+ && (allowed_handler.any_url || allowed_handler.urls.contains_slow(request.url()))) {
+ allowed = true;
+ break;
+ }
+ }
+ if (!allowed) {
+ // You are not on the list, go home!
+ did_misbehave(String::formatted("Client requested a combination of handler/URL that was not on the list: '{}' with '{}'", request.handler_name(), request.url()).characters());
+ return {};
+ }
+ }
+
+ URL url(request.url());
+ auto result = Launcher::the().open_url(url, request.handler_name());
+ return make<Messages::LaunchServer::OpenURLResponse>(result);
+}
+
+OwnPtr<Messages::LaunchServer::GetHandlersForURLResponse> ClientConnection::handle(const Messages::LaunchServer::GetHandlersForURL& request)
+{
+ URL url(request.url());
+ auto result = Launcher::the().handlers_for_url(url);
+ return make<Messages::LaunchServer::GetHandlersForURLResponse>(result);
+}
+
+OwnPtr<Messages::LaunchServer::GetHandlersWithDetailsForURLResponse> ClientConnection::handle(const Messages::LaunchServer::GetHandlersWithDetailsForURL& request)
+{
+ URL url(request.url());
+ auto result = Launcher::the().handlers_with_details_for_url(url);
+ return make<Messages::LaunchServer::GetHandlersWithDetailsForURLResponse>(result);
+}
+
+OwnPtr<Messages::LaunchServer::AddAllowedURLResponse> ClientConnection::handle(const Messages::LaunchServer::AddAllowedURL& request)
+{
+ if (m_allowlist_is_sealed) {
+ did_misbehave("Got request to add more allowed handlers after list was sealed");
+ return {};
+ }
+
+ if (!request.url().is_valid()) {
+ did_misbehave("Got request to allow invalid URL");
+ return {};
+ }
+
+ m_allowlist.empend(String(), false, Vector<URL> { request.url() });
+
+ return make<Messages::LaunchServer::AddAllowedURLResponse>();
+}
+
+OwnPtr<Messages::LaunchServer::AddAllowedHandlerWithAnyURLResponse> ClientConnection::handle(const Messages::LaunchServer::AddAllowedHandlerWithAnyURL& request)
+{
+ if (m_allowlist_is_sealed) {
+ did_misbehave("Got request to add more allowed handlers after list was sealed");
+ return {};
+ }
+
+ if (request.handler_name().is_empty()) {
+ did_misbehave("Got request to allow empty handler name");
+ return {};
+ }
+
+ m_allowlist.empend(request.handler_name(), true, Vector<URL>());
+
+ return make<Messages::LaunchServer::AddAllowedHandlerWithAnyURLResponse>();
+}
+
+OwnPtr<Messages::LaunchServer::AddAllowedHandlerWithOnlySpecificURLsResponse> ClientConnection::handle(const Messages::LaunchServer::AddAllowedHandlerWithOnlySpecificURLs& request)
+{
+ if (m_allowlist_is_sealed) {
+ did_misbehave("Got request to add more allowed handlers after list was sealed");
+ return {};
+ }
+
+ if (request.handler_name().is_empty()) {
+ did_misbehave("Got request to allow empty handler name");
+ return {};
+ }
+
+ if (request.urls().is_empty()) {
+ did_misbehave("Got request to allow empty URL list");
+ return {};
+ }
+
+ m_allowlist.empend(request.handler_name(), false, request.urls());
+
+ return make<Messages::LaunchServer::AddAllowedHandlerWithOnlySpecificURLsResponse>();
+}
+
+OwnPtr<Messages::LaunchServer::SealAllowlistResponse> ClientConnection::handle(const Messages::LaunchServer::SealAllowlist&)
+{
+ if (m_allowlist_is_sealed) {
+ did_misbehave("Got more than one request to seal the allowed handlers list");
+ return {};
+ }
+
+ return make<Messages::LaunchServer::SealAllowlistResponse>();
+}
+
+}
diff --git a/Userland/Services/LaunchServer/ClientConnection.h b/Userland/Services/LaunchServer/ClientConnection.h
new file mode 100644
index 0000000000..77eef53374
--- /dev/null
+++ b/Userland/Services/LaunchServer/ClientConnection.h
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2020, Nicholas Hollett <niax@niax.co.uk>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LaunchServer/LaunchClientEndpoint.h>
+#include <LaunchServer/LaunchServerEndpoint.h>
+#include <LibIPC/ClientConnection.h>
+
+namespace LaunchServer {
+
+class ClientConnection final : public IPC::ClientConnection<LaunchClientEndpoint, LaunchServerEndpoint>
+ , public LaunchServerEndpoint {
+ C_OBJECT(ClientConnection)
+public:
+ ~ClientConnection() override;
+
+ virtual void die() override;
+
+private:
+ explicit ClientConnection(NonnullRefPtr<Core::LocalSocket>, int client_id);
+
+ virtual OwnPtr<Messages::LaunchServer::GreetResponse> handle(const Messages::LaunchServer::Greet&) override;
+ virtual OwnPtr<Messages::LaunchServer::OpenURLResponse> handle(const Messages::LaunchServer::OpenURL&) override;
+ virtual OwnPtr<Messages::LaunchServer::GetHandlersForURLResponse> handle(const Messages::LaunchServer::GetHandlersForURL&) override;
+ virtual OwnPtr<Messages::LaunchServer::GetHandlersWithDetailsForURLResponse> handle(const Messages::LaunchServer::GetHandlersWithDetailsForURL&) override;
+ virtual OwnPtr<Messages::LaunchServer::AddAllowedURLResponse> handle(const Messages::LaunchServer::AddAllowedURL&) override;
+ virtual OwnPtr<Messages::LaunchServer::AddAllowedHandlerWithAnyURLResponse> handle(const Messages::LaunchServer::AddAllowedHandlerWithAnyURL&) override;
+ virtual OwnPtr<Messages::LaunchServer::AddAllowedHandlerWithOnlySpecificURLsResponse> handle(const Messages::LaunchServer::AddAllowedHandlerWithOnlySpecificURLs&) override;
+ virtual OwnPtr<Messages::LaunchServer::SealAllowlistResponse> handle(const Messages::LaunchServer::SealAllowlist&) override;
+
+ struct AllowlistEntry {
+ String handler_name;
+ bool any_url { false };
+ Vector<URL> urls;
+ };
+
+ Vector<AllowlistEntry> m_allowlist;
+ bool m_allowlist_is_sealed { false };
+};
+}
diff --git a/Userland/Services/LaunchServer/LaunchClient.ipc b/Userland/Services/LaunchServer/LaunchClient.ipc
new file mode 100644
index 0000000000..abd4e9ba3d
--- /dev/null
+++ b/Userland/Services/LaunchServer/LaunchClient.ipc
@@ -0,0 +1,4 @@
+endpoint LaunchClient = 102
+{
+ Dummy() =|
+}
diff --git a/Userland/Services/LaunchServer/LaunchServer.ipc b/Userland/Services/LaunchServer/LaunchServer.ipc
new file mode 100644
index 0000000000..4f50221f4b
--- /dev/null
+++ b/Userland/Services/LaunchServer/LaunchServer.ipc
@@ -0,0 +1,12 @@
+endpoint LaunchServer = 101
+{
+ Greet() => (i32 client_id)
+ OpenURL(URL url, String handler_name) => (bool response)
+ GetHandlersForURL(URL url) => (Vector<String> handlers)
+ GetHandlersWithDetailsForURL(URL url) => (Vector<String> handlers_details)
+
+ AddAllowedURL(URL url) => ()
+ AddAllowedHandlerWithAnyURL(String handler_name) => ()
+ AddAllowedHandlerWithOnlySpecificURLs(String handler_name, Vector<URL> urls) => ()
+ SealAllowlist() => ()
+}
diff --git a/Userland/Services/LaunchServer/Launcher.cpp b/Userland/Services/LaunchServer/Launcher.cpp
new file mode 100644
index 0000000000..ad6d025035
--- /dev/null
+++ b/Userland/Services/LaunchServer/Launcher.cpp
@@ -0,0 +1,309 @@
+/*
+ * Copyright (c) 2020, Nicholas Hollett <niax@niax.co.uk>, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "Launcher.h"
+#include <AK/Function.h>
+#include <AK/JsonObject.h>
+#include <AK/JsonObjectSerializer.h>
+#include <AK/JsonValue.h>
+#include <AK/LexicalPath.h>
+#include <AK/StringBuilder.h>
+#include <LibCore/ConfigFile.h>
+#include <LibDesktop/AppFile.h>
+#include <serenity.h>
+#include <spawn.h>
+#include <stdio.h>
+#include <sys/stat.h>
+
+namespace LaunchServer {
+
+static Launcher* s_the;
+static bool spawn(String executable, String argument);
+
+String Handler::name_from_executable(const StringView& executable)
+{
+ auto separator = executable.find_last_of('/');
+ if (separator.has_value()) {
+ auto start = separator.value() + 1;
+ return executable.substring_view(start, executable.length() - start);
+ }
+ return executable;
+}
+
+void Handler::from_executable(Type handler_type, const String& executable)
+{
+ this->handler_type = handler_type;
+ this->name = name_from_executable(executable);
+ this->executable = executable;
+}
+
+String Handler::to_details_str() const
+{
+ StringBuilder builder;
+ JsonObjectSerializer obj { builder };
+ obj.add("executable", executable);
+ obj.add("name", name);
+ switch (handler_type) {
+ case Type::Application:
+ obj.add("type", "app");
+ break;
+ case Type::UserDefault:
+ obj.add("type", "userdefault");
+ break;
+ case Type::UserPreferred:
+ obj.add("type", "userpreferred");
+ break;
+ default:
+ break;
+ }
+ obj.finish();
+ return builder.build();
+}
+
+Launcher::Launcher()
+{
+ ASSERT(s_the == nullptr);
+ s_the = this;
+}
+
+Launcher& Launcher::the()
+{
+ ASSERT(s_the);
+ return *s_the;
+}
+
+void Launcher::load_handlers(const String& af_dir)
+{
+ Desktop::AppFile::for_each([&](auto af) {
+ auto app_name = af->name();
+ auto app_executable = af->executable();
+ HashTable<String> file_types;
+ for (auto& file_type : af->launcher_file_types())
+ file_types.set(file_type);
+ HashTable<String> protocols;
+ for (auto& protocol : af->launcher_protocols())
+ protocols.set(protocol);
+ m_handlers.set(app_executable, { Handler::Type::Default, app_name, app_executable, file_types, protocols });
+ },
+ af_dir);
+}
+
+void Launcher::load_config(const Core::ConfigFile& cfg)
+{
+ for (auto key : cfg.keys("FileType")) {
+ auto handler = cfg.read_entry("FileType", key).trim_whitespace();
+ if (handler.is_empty())
+ continue;
+ m_file_handlers.set(key.to_lowercase(), handler);
+ }
+
+ for (auto key : cfg.keys("Protocol")) {
+ auto handler = cfg.read_entry("Protocol", key).trim_whitespace();
+ if (handler.is_empty())
+ continue;
+ m_protocol_handlers.set(key.to_lowercase(), handler);
+ }
+}
+
+Vector<String> Launcher::handlers_for_url(const URL& url)
+{
+ Vector<String> handlers;
+ if (url.protocol() == "file") {
+ for_each_handler_for_path(url.path(), [&](auto& handler) -> bool {
+ handlers.append(handler.executable);
+ return true;
+ });
+ } else {
+ for_each_handler(url.protocol(), m_protocol_handlers, [&](const auto& handler) -> bool {
+ if (handler.handler_type != Handler::Type::Default || handler.protocols.contains(url.protocol())) {
+ handlers.append(handler.executable);
+ return true;
+ }
+ return false;
+ });
+ }
+ return handlers;
+}
+
+Vector<String> Launcher::handlers_with_details_for_url(const URL& url)
+{
+ Vector<String> handlers;
+ if (url.protocol() == "file") {
+ for_each_handler_for_path(url.path(), [&](auto& handler) -> bool {
+ handlers.append(handler.to_details_str());
+ return true;
+ });
+ } else {
+ for_each_handler(url.protocol(), m_protocol_handlers, [&](const auto& handler) -> bool {
+ if (handler.handler_type != Handler::Type::Default || handler.protocols.contains(url.protocol())) {
+ handlers.append(handler.to_details_str());
+ return true;
+ }
+ return false;
+ });
+ }
+ return handlers;
+}
+
+bool Launcher::open_url(const URL& url, const String& handler_name)
+{
+ if (!handler_name.is_null())
+ return open_with_handler_name(url, handler_name);
+
+ if (url.protocol() == "file")
+ return open_file_url(url);
+
+ return open_with_user_preferences(m_protocol_handlers, url.protocol(), url.to_string());
+}
+
+bool Launcher::open_with_handler_name(const URL& url, const String& handler_name)
+{
+ auto handler_optional = m_handlers.get(handler_name);
+ if (!handler_optional.has_value())
+ return false;
+
+ auto& handler = handler_optional.value();
+ String argument;
+ if (url.protocol() == "file")
+ argument = url.path();
+ else
+ argument = url.to_string();
+ return spawn(handler.executable, argument);
+}
+
+bool spawn(String executable, String argument)
+{
+ pid_t child_pid;
+ const char* argv[] = { executable.characters(), argument.characters(), nullptr };
+ if ((errno = posix_spawn(&child_pid, executable.characters(), nullptr, nullptr, const_cast<char**>(argv), environ))) {
+ perror("posix_spawn");
+ return false;
+ } else {
+ if (disown(child_pid) < 0)
+ perror("disown");
+ }
+ return true;
+}
+
+Handler Launcher::get_handler_for_executable(Handler::Type handler_type, const String& executable) const
+{
+ Handler handler;
+ auto existing_handler = m_handlers.get(executable);
+ if (existing_handler.has_value()) {
+ handler = existing_handler.value();
+ handler.handler_type = handler_type;
+ } else {
+ handler.from_executable(handler_type, executable);
+ }
+ return handler;
+}
+
+bool Launcher::open_with_user_preferences(const HashMap<String, String>& user_preferences, const String key, const String argument, const String default_program)
+{
+ auto program_path = user_preferences.get(key);
+ if (program_path.has_value())
+ return spawn(program_path.value(), argument);
+
+ // There wasn't a handler for this, so try the fallback instead
+ program_path = user_preferences.get("*");
+ if (program_path.has_value())
+ return spawn(program_path.value(), argument);
+
+ // Absolute worst case, try the provided default program, if any
+ if (!default_program.is_empty())
+ return spawn(default_program, argument);
+
+ return false;
+}
+
+void Launcher::for_each_handler(const String& key, HashMap<String, String>& user_preference, Function<bool(const Handler&)> f)
+{
+ auto user_preferred = user_preference.get(key);
+ if (user_preferred.has_value())
+ f(get_handler_for_executable(Handler::Type::UserPreferred, user_preferred.value()));
+
+ size_t counted = 0;
+ for (auto& handler : m_handlers) {
+ // Skip over the existing item in the list
+ if (user_preferred.has_value() && user_preferred.value() == handler.value.executable)
+ continue;
+ if (f(handler.value))
+ counted++;
+ }
+
+ auto user_default = user_preference.get("*");
+ if (counted == 0 && user_default.has_value())
+ f(get_handler_for_executable(Handler::Type::UserDefault, user_default.value()));
+}
+
+void Launcher::for_each_handler_for_path(const String& path, Function<bool(const Handler&)> f)
+{
+ struct stat st;
+ if (stat(path.characters(), &st) < 0) {
+ perror("stat");
+ return;
+ }
+
+ // TODO: Make directory opening configurable
+ if (S_ISDIR(st.st_mode)) {
+ f(get_handler_for_executable(Handler::Type::Default, "/bin/FileManager"));
+ return;
+ }
+
+ if ((st.st_mode & S_IFMT) == S_IFREG && (st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH)))
+ f(get_handler_for_executable(Handler::Type::Application, path));
+
+ auto extension = LexicalPath(path).extension().to_lowercase();
+
+ for_each_handler(extension, m_file_handlers, [&](const auto& handler) -> bool {
+ if (handler.handler_type != Handler::Type::Default || handler.file_types.contains(extension))
+ return f(handler);
+ return false;
+ });
+}
+
+bool Launcher::open_file_url(const URL& url)
+{
+ struct stat st;
+ if (stat(url.path().characters(), &st) < 0) {
+ perror("stat");
+ return false;
+ }
+
+ // TODO: Make directory opening configurable
+ if (S_ISDIR(st.st_mode))
+ return spawn("/bin/FileManager", url.path());
+
+ if ((st.st_mode & S_IFMT) == S_IFREG && st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH))
+ return spawn(url.path(), {});
+
+ auto extension_parts = url.path().to_lowercase().split('.');
+ String extension = {};
+ if (extension_parts.size() > 1)
+ extension = extension_parts.last();
+ return open_with_user_preferences(m_file_handlers, extension, url.path(), "/bin/TextEditor");
+}
+}
diff --git a/Userland/Services/LaunchServer/Launcher.h b/Userland/Services/LaunchServer/Launcher.h
new file mode 100644
index 0000000000..8708cdc258
--- /dev/null
+++ b/Userland/Services/LaunchServer/Launcher.h
@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) 2020, Nicholas Hollett <niax@niax.co.uk>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/HashMap.h>
+#include <AK/HashTable.h>
+#include <AK/URL.h>
+#include <LibCore/ConfigFile.h>
+#include <LibDesktop/AppFile.h>
+
+namespace LaunchServer {
+
+struct Handler {
+ enum class Type {
+ Default = 0,
+ Application,
+ UserPreferred,
+ UserDefault
+ };
+ Type handler_type;
+ String name;
+ String executable;
+ HashTable<String> file_types {};
+ HashTable<String> protocols {};
+
+ static String name_from_executable(const StringView&);
+ void from_executable(Type, const String&);
+ String to_details_str() const;
+};
+
+class Launcher {
+public:
+ Launcher();
+ static Launcher& the();
+
+ void load_handlers(const String& af_dir = Desktop::AppFile::APP_FILES_DIRECTORY);
+ void load_config(const Core::ConfigFile&);
+ bool open_url(const URL&, const String& handler_name);
+ Vector<String> handlers_for_url(const URL&);
+ Vector<String> handlers_with_details_for_url(const URL&);
+
+private:
+ HashMap<String, Handler> m_handlers;
+ HashMap<String, String> m_protocol_handlers;
+ HashMap<String, String> m_file_handlers;
+
+ Handler get_handler_for_executable(Handler::Type, const String&) const;
+ void for_each_handler(const String& key, HashMap<String, String>& user_preferences, Function<bool(const Handler&)> f);
+ void for_each_handler_for_path(const String&, Function<bool(const Handler&)> f);
+ bool open_file_url(const URL&);
+ bool open_with_user_preferences(const HashMap<String, String>& user_preferences, const String key, const String argument, const String default_program = {});
+ bool open_with_handler_name(const URL&, const String& handler_name);
+};
+}
diff --git a/Userland/Services/LaunchServer/main.cpp b/Userland/Services/LaunchServer/main.cpp
new file mode 100644
index 0000000000..2535d1e8bb
--- /dev/null
+++ b/Userland/Services/LaunchServer/main.cpp
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2020, Nicholas Hollett <niax@niax.co.uk>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "ClientConnection.h"
+#include "Launcher.h"
+#include <LibCore/ConfigFile.h>
+#include <LibCore/EventLoop.h>
+#include <LibCore/LocalServer.h>
+#include <stdio.h>
+#include <unistd.h>
+
+int main([[maybe_unused]] int argc, [[maybe_unused]] char** argv)
+{
+ Core::EventLoop event_loop;
+ auto server = Core::LocalServer::construct();
+
+ auto launcher = LaunchServer::Launcher();
+
+ launcher.load_handlers();
+ launcher.load_config(Core::ConfigFile::get_for_app("LaunchServer"));
+
+ if (pledge("stdio accept rpath proc exec", nullptr) < 0) {
+ perror("pledge");
+ return 1;
+ }
+
+ bool ok = server->take_over_from_system_server();
+ ASSERT(ok);
+ server->on_ready_to_accept = [&] {
+ auto client_socket = server->accept();
+ if (!client_socket) {
+ dbgln("LaunchServer: accept failed.");
+ return;
+ }
+ static int s_next_client_id = 0;
+ int client_id = ++s_next_client_id;
+ dbgln("Received connection");
+ IPC::new_client_connection<LaunchServer::ClientConnection>(client_socket.release_nonnull(), client_id);
+ };
+
+ return event_loop.exec();
+}