diff --git a/userspace/libraries/CMakeLists.txt b/userspace/libraries/CMakeLists.txt index b616c94d..8b03acf4 100644 --- a/userspace/libraries/CMakeLists.txt +++ b/userspace/libraries/CMakeLists.txt @@ -1,6 +1,7 @@ set(USERSPACE_LIBRARIES LibAudio LibC + LibClipboard LibDEFLATE LibDL LibELF diff --git a/userspace/libraries/LibClipboard/CMakeLists.txt b/userspace/libraries/LibClipboard/CMakeLists.txt new file mode 100644 index 00000000..090bc06e --- /dev/null +++ b/userspace/libraries/LibClipboard/CMakeLists.txt @@ -0,0 +1,10 @@ +set(LIBCLIPBOARD_SOURCES + Clipboard.cpp +) + +add_library(libclipboard ${LIBCLIPBOARD_SOURCES}) +banan_link_library(libclipboard ban) +banan_link_library(libclipboard libc) + +banan_install_headers(libclipboard) +install(TARGETS libclipboard OPTIONAL) diff --git a/userspace/libraries/LibClipboard/Clipboard.cpp b/userspace/libraries/LibClipboard/Clipboard.cpp new file mode 100644 index 00000000..b65377af --- /dev/null +++ b/userspace/libraries/LibClipboard/Clipboard.cpp @@ -0,0 +1,201 @@ +#include + +#include +#include +#include +#include + +namespace LibClipboard +{ + + static int s_server_fd = -1; + + static BAN::ErrorOr send_credentials(int fd) + { + char dummy = '\0'; + iovec iovec { + .iov_base = &dummy, + .iov_len = 1, + }; + + constexpr size_t control_size = CMSG_LEN(sizeof(ucred)); + uint8_t control_buffer[control_size]; + + cmsghdr* control = reinterpret_cast(control_buffer); + + *control = { + .cmsg_len = control_size, + .cmsg_level = SOL_SOCKET, + .cmsg_type = SCM_CREDENTIALS, + }; + + *reinterpret_cast(CMSG_DATA(control)) = { + .pid = getpid(), + .uid = getuid(), + .gid = getgid(), + }; + + const msghdr message { + .msg_name = nullptr, + .msg_namelen = 0, + .msg_iov = &iovec, + .msg_iovlen = 1, + .msg_control = control, + .msg_controllen = control_size, + .msg_flags = 0, + }; + + if (sendmsg(fd, &message, 0) < 0) + return BAN::Error::from_errno(errno); + + return {}; + } + + static BAN::ErrorOr ensure_connected() + { + if (s_server_fd != -1) + return {}; + + const int sock = socket(AF_UNIX, SOCK_STREAM, 0); + if (sock == -1) + return BAN::Error::from_errno(errno); + + sockaddr_un server_addr; + server_addr.sun_family = AF_UNIX; + strcpy(server_addr.sun_path, s_clipboard_server_socket.data()); + + if (connect(sock, reinterpret_cast(&server_addr), sizeof(server_addr)) == -1) + { + close(sock); + return BAN::Error::from_errno(errno); + } + + if (auto ret = send_credentials(sock); ret.is_error()) + { + close(sock); + return ret; + } + + s_server_fd = sock; + return {}; + } + + static BAN::ErrorOr recv_sized(void* data, size_t size) + { + ASSERT(s_server_fd != -1); + + uint8_t* u8_data = static_cast(data); + + size_t total_recv = 0; + while (total_recv < size) + { + const ssize_t nrecv = recv(s_server_fd, u8_data + total_recv, size - total_recv, 0); + if (nrecv <= 0) + { + const int error = nrecv ? errno : ECONNRESET; + close(s_server_fd); + s_server_fd = -1; + return BAN::Error::from_errno(error); + } + total_recv += nrecv; + } + + return {}; + } + + static BAN::ErrorOr send_sized(const void* data, size_t size) + { + ASSERT(s_server_fd != -1); + + const uint8_t* u8_data = static_cast(data); + + size_t total_sent = 0; + while (total_sent < size) + { + const ssize_t nsend = send(s_server_fd, u8_data + total_sent, size - total_sent, 0); + if (nsend <= 0) + { + const int error = nsend ? errno : ECONNRESET; + close(s_server_fd); + s_server_fd = -1; + return BAN::Error::from_errno(error); + } + total_sent += nsend; + } + + return {}; + } + + BAN::ErrorOr Clipboard::get_clipboard() + { + TRY(ensure_connected()); + + { + DataType type = DataType::__get; + TRY(send_sized(&type, sizeof(type))); + } + + Info info; + TRY(recv_sized(&info.type, sizeof(info.type))); + + switch (info.type) + { + case DataType::__get: + ASSERT_NOT_REACHED(); + case DataType::None: + break; + case DataType::Text: + size_t data_size; + TRY(recv_sized(&data_size, sizeof(data_size))); + + TRY(info.data.resize(data_size)); + TRY(recv_sized(info.data.data(), data_size)); + break; + } + + return info; + } + + BAN::ErrorOr Clipboard::set_clipboard(DataType type, BAN::Span data) + { + ASSERT(type != DataType::__get); + + TRY(ensure_connected()); + + TRY(send_sized(&type, sizeof(type))); + + switch (type) + { + case DataType::__get: + ASSERT_NOT_REACHED(); + case DataType::None: + break; + case DataType::Text: + const size_t size = data.size(); + TRY(send_sized(&size, sizeof(size))); + TRY(send_sized(data.data(), size)); + break; + } + + return {}; + } + + BAN::ErrorOr Clipboard::get_clipboard_text() + { + auto info = TRY(get_clipboard()); + if (info.type != DataType::Text) + return BAN::String {}; + + BAN::String string; + TRY(string.resize(info.data.size())); + memcpy(string.data(), info.data.data(), info.data.size()); + + return string; + } + + BAN::ErrorOr Clipboard::set_clipboard_text(BAN::StringView string) + { + return set_clipboard(DataType::Text, { reinterpret_cast(string.data()), string.size() }); + } + +} diff --git a/userspace/libraries/LibClipboard/include/LibClipboard/Clipboard.h b/userspace/libraries/LibClipboard/include/LibClipboard/Clipboard.h new file mode 100644 index 00000000..30c655e7 --- /dev/null +++ b/userspace/libraries/LibClipboard/include/LibClipboard/Clipboard.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include +#include + +namespace LibClipboard +{ + + static constexpr BAN::StringView s_clipboard_server_socket = "/tmp/clipboard-server.socket"_sv; + + class Clipboard + { + public: + enum class DataType : uint32_t + { + None, + Text, + __get = UINT32_MAX, + }; + + struct Info + { + DataType type = DataType::None; + BAN::Vector data; + }; + + public: + static BAN::ErrorOr get_clipboard(); + static BAN::ErrorOr set_clipboard(DataType type, BAN::Span data); + + static BAN::ErrorOr get_clipboard_text(); + static BAN::ErrorOr set_clipboard_text(BAN::StringView string); + }; + +} diff --git a/userspace/programs/CMakeLists.txt b/userspace/programs/CMakeLists.txt index 4843f5df..d08d7e0e 100644 --- a/userspace/programs/CMakeLists.txt +++ b/userspace/programs/CMakeLists.txt @@ -7,6 +7,7 @@ set(USERSPACE_PROGRAMS cat-mmap chmod chown + ClipboardServer cp dd dhcp-client diff --git a/userspace/programs/ClipboardServer/CMakeLists.txt b/userspace/programs/ClipboardServer/CMakeLists.txt new file mode 100644 index 00000000..84865664 --- /dev/null +++ b/userspace/programs/ClipboardServer/CMakeLists.txt @@ -0,0 +1,10 @@ +set(SOURCES + main.cpp +) + +add_executable(ClipboardServer ${SOURCES}) +banan_include_headers(ClipboardServer libclipboard) +banan_link_library(ClipboardServer ban) +banan_link_library(ClipboardServer libc) + +install(TARGETS ClipboardServer OPTIONAL) diff --git a/userspace/programs/ClipboardServer/main.cpp b/userspace/programs/ClipboardServer/main.cpp new file mode 100644 index 00000000..52e50850 --- /dev/null +++ b/userspace/programs/ClipboardServer/main.cpp @@ -0,0 +1,267 @@ +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +static uid_t receive_credentials(int fd) +{ + char dummy = '\0'; + iovec iovec { + .iov_base = &dummy, + .iov_len = 1, + }; + + constexpr size_t control_size = CMSG_LEN(sizeof(ucred)); + uint8_t control_buffer[control_size]; + + msghdr message { + .msg_name = nullptr, + .msg_namelen = 0, + .msg_iov = &iovec, + .msg_iovlen = 1, + .msg_control = control_buffer, + .msg_controllen = control_size, + .msg_flags = 0, + }; + + const ssize_t nrecv = recvmsg(fd, &message, 0); + if (nrecv <= 0) + { + if (nrecv < 0) + dwarnln("recvmsg: {}", strerror(errno)); + return -1; + } + + for (auto* cheader = CMSG_FIRSTHDR(&message); cheader; cheader = CMSG_NXTHDR(&message, cheader)) + { + if (cheader->cmsg_level != SOL_SOCKET) + continue; + if (cheader->cmsg_type != SCM_CREDENTIALS) + continue; + + auto* ucred = reinterpret_cast(CMSG_DATA(cheader)); + if (ucred->uid < 0) + { + dwarnln("got uid {} from SCM_CREDENTIALS"); + return -1; + } + + return ucred->uid; + } + + dwarnln("no credentials in client's first message"); + return -1; +} + +static bool recv_sized(int fd, void* data, size_t size) +{ + uint8_t* u8_data = static_cast(data); + + size_t total_recv = 0; + while (total_recv < size) + { + const ssize_t nrecv = recv(fd, u8_data + total_recv, size - total_recv, 0); + if (nrecv <= 0) + { + const int error = nrecv ? errno : ECONNRESET; + dwarnln("recv: {}", strerror(error)); + return false; + } + total_recv += nrecv; + } + + return true; +} + +static bool send_sized(int fd, const void* data, size_t size) +{ + const uint8_t* u8_data = static_cast(data); + + size_t total_sent = 0; + while (total_sent < size) + { + const ssize_t nsend = send(fd, u8_data + total_sent, size - total_sent, 0); + if (nsend <= 0) + { + const int error = nsend ? errno : ECONNRESET; + dwarnln("send: {}", strerror(error)); + return false; + } + total_sent += nsend; + } + + return true; +} + +int main() +{ + using namespace LibClipboard; + + int server_sock = socket(AF_UNIX, SOCK_STREAM, 0); + if (server_sock == -1) + { + dwarnln("socket: {}", strerror(errno)); + return 1; + } + + sockaddr_un server_addr; + server_addr.sun_family = AF_UNIX; + strcpy(server_addr.sun_path, LibClipboard::s_clipboard_server_socket.data()); + if (bind(server_sock, reinterpret_cast(&server_addr), sizeof(server_addr)) == -1) + { + dwarnln("bind: {}", strerror(errno)); + return 1; + } + + if (chmod(LibClipboard::s_clipboard_server_socket.data(), 0777) == -1) + dwarnln("chmod: {}", strerror(errno)); + + if (listen(server_sock, SOMAXCONN) == -1) + { + dwarnln("listen: {}", strerror(errno)); + return 1; + } + + struct Client + { + int fd; + uid_t uid = -1; + }; + + BAN::Vector clients; + BAN::HashMap clipboards; + + for (;;) + { + fd_set fds; + FD_ZERO(&fds); + + int max_fd = server_sock; + FD_SET(server_sock, &fds); + + for (const auto& client : clients) + { + FD_SET(client.fd, &fds); + max_fd = BAN::Math::max(client.fd, max_fd); + } + + if (select(max_fd + 1, &fds, nullptr, nullptr, nullptr) == -1) + continue; + + if (FD_ISSET(server_sock, &fds)) + { + const int client = accept(server_sock, nullptr, nullptr); + if (client == -1) + dwarnln("accept: {}", strerror(errno)); + else if (clients.emplace_back(client).is_error()) + { + dwarnln("failed to allocate space for new client"); + close(client); + } + } + + for (size_t i = 0; i < clients.size(); i++) + { + auto& client = clients[i]; + if (!FD_ISSET(client.fd, &fds)) + continue; + + bool closed = false; + + if (client.uid == -1) + { + client.uid = receive_credentials(client.fd); + if (client.uid == -1) + closed = true; + else if (!clipboards.contains(client.uid) && clipboards.emplace(client.uid).is_error()) + { + dwarnln("failed to allocate clipboard for {}", client.fd); + closed = true; + } + } + else + { + Clipboard::DataType data_type; + + auto& clipboard = clipboards[client.uid]; + + if (!recv_sized(client.fd, &data_type, sizeof(data_type))) + closed = true; + else switch (data_type) + { + case Clipboard::DataType::__get: + { + closed = true; + + const auto data_type = clipboard.type; + if (!send_sized(client.fd, &data_type, sizeof(data_type))) + break; + + const auto data_size = clipboard.data.size(); + if (!send_sized(client.fd, &data_size, sizeof(data_size))) + break; + + if (!send_sized(client.fd, clipboard.data.data(), data_size)) + break; + + closed = false; + break; + } + case Clipboard::DataType::None: + clipboard = { + .type = data_type, + .data = {}, + }; + break; + case Clipboard::DataType::Text: + { + closed = true; + + // FIXME: client can hang the server if it doesn't + // send the actual clipboard data... + + size_t data_size; + if (!recv_sized(client.fd, &data_size, sizeof(data_size))) + break; + + BAN::Vector new_clipboard; + if (new_clipboard.resize(data_size).is_error()) + { + dwarnln("failed to allocate {} bytes for clipboard", data_size); + break; + } + + if (!recv_sized(client.fd, new_clipboard.data(), data_size)) + break; + + clipboard = { + .type = data_type, + .data = BAN::move(new_clipboard), + }; + + closed = false; + break; + } + default: + dwarnln("unexpected data type {}", static_cast(data_type)); + closed = true; + break; + } + } + + if (closed) + { + close(client.fd); + clients.remove(i--); + } + } + } +}