/* * This file is part of mpv. * * mpv is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * mpv is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with mpv. If not, see . */ #include #include #include "osdep/io.h" #include "osdep/threads.h" #include "osdep/windows_utils.h" #include "common/common.h" #include "common/global.h" #include "common/msg.h" #include "input/input.h" #include "libmpv/client.h" #include "options/m_config.h" #include "options/options.h" #include "player/client.h" struct mp_ipc_ctx { struct mp_log *log; struct mp_client_api *client_api; const wchar_t *path; mp_thread thread; HANDLE death_event; }; struct client_arg { struct mp_log *log; struct mpv_handle *client; char *client_name; HANDLE client_h; bool writable; OVERLAPPED write_ol; }; // Get a string SID representing the current user. Must be freed by LocalFree. static char *get_user_sid(void) { char *ssid = NULL; TOKEN_USER *info = NULL; HANDLE t; if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &t)) goto done; DWORD info_len; if (!GetTokenInformation(t, TokenUser, NULL, 0, &info_len) && GetLastError() != ERROR_INSUFFICIENT_BUFFER) goto done; info = talloc_size(NULL, info_len); if (!GetTokenInformation(t, TokenUser, info, info_len, &info_len)) goto done; if (!info->User.Sid) goto done; ConvertSidToStringSidA(info->User.Sid, &ssid); done: if (t) CloseHandle(t); talloc_free(info); return ssid; } // Get a string SID for the process integrity level. Must be freed by LocalFree. static char *get_integrity_sid(void) { char *ssid = NULL; TOKEN_MANDATORY_LABEL *info = NULL; HANDLE t; if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &t)) goto done; DWORD info_len; if (!GetTokenInformation(t, TokenIntegrityLevel, NULL, 0, &info_len) && GetLastError() != ERROR_INSUFFICIENT_BUFFER) goto done; info = talloc_size(NULL, info_len); if (!GetTokenInformation(t, TokenIntegrityLevel, info, info_len, &info_len)) goto done; if (!info->Label.Sid) goto done; ConvertSidToStringSidA(info->Label.Sid, &ssid); done: if (t) CloseHandle(t); talloc_free(info); return ssid; } // Create a security descriptor that only grants access to processes running // under the current user at the current integrity level or higher static PSECURITY_DESCRIPTOR create_restricted_sd(void) { char *user_sid = get_user_sid(); char *integrity_sid = get_integrity_sid(); if (!user_sid || !integrity_sid) return NULL; char *sddl = talloc_asprintf(NULL, "O:%s" // Set the owner to user_sid "D:(A;;GRGW;;;%s)" // Grant GENERIC_{READ,WRITE} access to user_sid "S:(ML;;NRNWNX;;;%s)", // Disallow read, write and execute permissions // to integrity levels below integrity_sid user_sid, user_sid, integrity_sid); LocalFree(user_sid); LocalFree(integrity_sid); PSECURITY_DESCRIPTOR sd = NULL; ConvertStringSecurityDescriptorToSecurityDescriptorA(sddl, SDDL_REVISION_1, &sd, NULL); talloc_free(sddl); return sd; } static void wakeup_cb(void *d) { HANDLE event = d; SetEvent(event); } // Wrapper for ReadFile that treats ERROR_IO_PENDING as success static DWORD async_read(HANDLE file, void *buf, unsigned size, OVERLAPPED* ol) { DWORD err = ReadFile(file, buf, size, NULL, ol) ? 0 : GetLastError(); return err == ERROR_IO_PENDING ? 0 : err; } // Wrapper for WriteFile that treats ERROR_IO_PENDING as success static DWORD async_write(HANDLE file, const void *buf, unsigned size, OVERLAPPED* ol) { DWORD err = WriteFile(file, buf, size, NULL, ol) ? 0 : GetLastError(); return err == ERROR_IO_PENDING ? 0 : err; } static bool pipe_error_is_fatal(DWORD error) { switch (error) { case 0: case ERROR_HANDLE_EOF: case ERROR_BROKEN_PIPE: case ERROR_PIPE_NOT_CONNECTED: case ERROR_NO_DATA: return false; } return true; } static DWORD ipc_write_str(struct client_arg *arg, const char *buf) { DWORD error = 0; if ((error = async_write(arg->client_h, buf, strlen(buf), &arg->write_ol))) goto done; if (!GetOverlappedResult(arg->client_h, &arg->write_ol, &(DWORD){0}, TRUE)) { error = GetLastError(); goto done; } done: if (pipe_error_is_fatal(error)) { MP_VERBOSE(arg, "Error writing to pipe: %s\n", mp_HRESULT_to_str(HRESULT_FROM_WIN32(error))); } if (error) arg->writable = false; return error; } static void report_read_error(struct client_arg *arg, DWORD error) { // Only report the error if it's not just due to the pipe closing if (pipe_error_is_fatal(error)) { MP_ERR(arg, "Error reading from pipe: %s\n", mp_HRESULT_to_str(HRESULT_FROM_WIN32(error))); } else { MP_VERBOSE(arg, "Client disconnected\n"); } } static MP_THREAD_VOID client_thread(void *p) { struct client_arg *arg = p; char buf[4096]; HANDLE wakeup_event = CreateEventW(NULL, TRUE, FALSE, NULL); OVERLAPPED ol = { .hEvent = CreateEventW(NULL, TRUE, TRUE, NULL) }; bstr client_msg = { talloc_strdup(NULL, ""), 0 }; DWORD ioerr = 0; DWORD r; char *tname = talloc_asprintf(NULL, "ipc/%s", arg->client_name); mp_thread_set_name(tname); talloc_free(tname); arg->write_ol.hEvent = CreateEventW(NULL, TRUE, TRUE, NULL); if (!wakeup_event || !ol.hEvent || !arg->write_ol.hEvent) { MP_ERR(arg, "Couldn't create events\n"); goto done; } MP_VERBOSE(arg, "Client connected\n"); mpv_set_wakeup_callback(arg->client, wakeup_cb, wakeup_event); // Do the first read operation on the pipe if ((ioerr = async_read(arg->client_h, buf, 4096, &ol))) { report_read_error(arg, ioerr); goto done; } while (1) { HANDLE handles[] = { wakeup_event, ol.hEvent }; int n = WaitForMultipleObjects(2, handles, FALSE, 0); if (n == WAIT_TIMEOUT) n = WaitForMultipleObjects(2, handles, FALSE, INFINITE); switch (n) { case WAIT_OBJECT_0: // wakeup_event ResetEvent(wakeup_event); while (1) { mpv_event *event = mpv_wait_event(arg->client, 0); if (event->event_id == MPV_EVENT_NONE) break; if (event->event_id == MPV_EVENT_SHUTDOWN) goto done; if (!arg->writable) continue; char *event_msg = mp_json_encode_event(event); if (!event_msg) { MP_ERR(arg, "Encoding error\n"); goto done; } ipc_write_str(arg, event_msg); talloc_free(event_msg); } break; case WAIT_OBJECT_0 + 1: // ol.hEvent // Complete the read operation on the pipe if (!GetOverlappedResult(arg->client_h, &ol, &r, TRUE)) { report_read_error(arg, GetLastError()); goto done; } bstr_xappend(NULL, &client_msg, (bstr){buf, r}); while (bstrchr(client_msg, '\n') != -1) { char *reply_msg = mp_ipc_consume_next_command(arg->client, NULL, &client_msg); if (reply_msg && arg->writable) ipc_write_str(arg, reply_msg); talloc_free(reply_msg); } // Begin the next read operation on the pipe if ((ioerr = async_read(arg->client_h, buf, 4096, &ol))) { report_read_error(arg, ioerr); goto done; } break; default: MP_ERR(arg, "WaitForMultipleObjects failed\n"); goto done; } } done: if (client_msg.len > 0) MP_WARN(arg, "Ignoring unterminated command on disconnect.\n"); if (CancelIoEx(arg->client_h, &ol) || GetLastError() != ERROR_NOT_FOUND) GetOverlappedResult(arg->client_h, &ol, &(DWORD){0}, TRUE); if (wakeup_event) CloseHandle(wakeup_event); if (ol.hEvent) CloseHandle(ol.hEvent); if (arg->write_ol.hEvent) CloseHandle(arg->write_ol.hEvent); CloseHandle(arg->client_h); mpv_destroy(arg->client); talloc_free(arg); MP_THREAD_RETURN(); } static void ipc_start_client(struct mp_ipc_ctx *ctx, struct client_arg *client) { client->client = mp_new_client(ctx->client_api, client->client_name), client->log = mp_client_get_log(client->client); mp_thread client_thr; if (mp_thread_create(&client_thr, client_thread, client)) { mpv_destroy(client->client); CloseHandle(client->client_h); talloc_free(client); } mp_thread_detach(client_thr); } static void ipc_start_client_json(struct mp_ipc_ctx *ctx, int id, HANDLE h) { struct client_arg *client = talloc_ptrtype(NULL, client); *client = (struct client_arg){ .client_name = talloc_asprintf(client, "ipc-%d", id), .client_h = h, .writable = true, }; ipc_start_client(ctx, client); } bool mp_ipc_start_anon_client(struct mp_ipc_ctx *ctx, struct mpv_handle *h, int out_fd[2]) { return false; } static MP_THREAD_VOID ipc_thread(void *p) { // Use PIPE_TYPE_MESSAGE | PIPE_READMODE_BYTE so message framing is // maintained for message-mode clients, but byte-mode clients can still // connect, send and receive data. This is the most compatible mode. static const DWORD state = PIPE_TYPE_MESSAGE | PIPE_READMODE_BYTE | PIPE_WAIT | PIPE_REJECT_REMOTE_CLIENTS; static const DWORD mode = PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED; static const DWORD bufsiz = 4096; struct mp_ipc_ctx *arg = p; HANDLE server = INVALID_HANDLE_VALUE; HANDLE client = INVALID_HANDLE_VALUE; int client_num = 0; mp_thread_set_name("ipc/named-pipe"); MP_VERBOSE(arg, "Starting IPC master\n"); OVERLAPPED ol = {0}; SECURITY_ATTRIBUTES sa = { .nLength = sizeof sa, .lpSecurityDescriptor = create_restricted_sd(), }; if (!sa.lpSecurityDescriptor) { MP_ERR(arg, "Couldn't create security descriptor"); goto done; } ol = (OVERLAPPED){ .hEvent = CreateEventW(NULL, TRUE, TRUE, NULL) }; if (!ol.hEvent) { MP_ERR(arg, "Couldn't create event"); goto done; } server = CreateNamedPipeW(arg->path, mode | FILE_FLAG_FIRST_PIPE_INSTANCE, state, PIPE_UNLIMITED_INSTANCES, bufsiz, bufsiz, 0, &sa); if (server == INVALID_HANDLE_VALUE) { MP_ERR(arg, "Couldn't create first pipe instance: %s\n", mp_LastError_to_str()); goto done; } MP_VERBOSE(arg, "Listening to IPC pipe.\n"); while (1) { DWORD err = ConnectNamedPipe(server, &ol) ? 0 : GetLastError(); if (err == ERROR_IO_PENDING) { int n = WaitForMultipleObjects(2, (HANDLE[]) { arg->death_event, ol.hEvent, }, FALSE, INFINITE) - WAIT_OBJECT_0; switch (n) { case 0: // Stop waiting for new clients CancelIo(server); GetOverlappedResult(server, &ol, &(DWORD){0}, TRUE); goto done; case 1: // Complete the ConnectNamedPipe request err = GetOverlappedResult(server, &ol, &(DWORD){0}, TRUE) ? 0 : GetLastError(); break; default: MP_ERR(arg, "WaitForMultipleObjects failed\n"); goto done; } } // ERROR_PIPE_CONNECTED is returned if a client connects before // ConnectNamedPipe is called. ERROR_NO_DATA is returned if a client // connects, (possibly) writes data and exits before ConnectNamedPipe // is called. Both cases should be handled as normal connections. if (err == ERROR_PIPE_CONNECTED || err == ERROR_NO_DATA) err = 0; if (err) { MP_ERR(arg, "ConnectNamedPipe failed: %s\n", mp_HRESULT_to_str(HRESULT_FROM_WIN32(err))); goto done; } // Create the next pipe instance before the client thread to avoid the // theoretical race condition where the client thread immediately // closes the handle and there are no active instances of the pipe client = server; server = CreateNamedPipeW(arg->path, mode, state, PIPE_UNLIMITED_INSTANCES, bufsiz, bufsiz, 0, &sa); if (server == INVALID_HANDLE_VALUE) { MP_ERR(arg, "Couldn't create additional pipe instance: %s\n", mp_LastError_to_str()); goto done; } ipc_start_client_json(arg, client_num++, client); client = NULL; } done: if (sa.lpSecurityDescriptor) LocalFree(sa.lpSecurityDescriptor); if (client != INVALID_HANDLE_VALUE) CloseHandle(client); if (server != INVALID_HANDLE_VALUE) CloseHandle(server); if (ol.hEvent) CloseHandle(ol.hEvent); MP_THREAD_RETURN(); } struct mp_ipc_ctx *mp_init_ipc(struct mp_client_api *client_api, struct mpv_global *global) { struct MPOpts *opts = mp_get_config_group(NULL, global, &mp_opt_root); struct mp_ipc_ctx *arg = talloc_ptrtype(NULL, arg); *arg = (struct mp_ipc_ctx){ .log = mp_log_new(arg, global->log, "ipc"), .client_api = client_api, }; if (opts->ipc_client && opts->ipc_client[0]) { int fd = -1; if (strncmp(opts->ipc_client, "fd://", 5) == 0) { char *end; unsigned long l = strtoul(opts->ipc_client + 5, &end, 0); if (!end[0] && l <= INT_MAX) fd = l; } if (fd < 0) { MP_ERR(arg, "Invalid IPC client argument: '%s'\n", opts->ipc_client); } else { HANDLE h = (HANDLE)_get_osfhandle(fd); if (h && h != INVALID_HANDLE_VALUE && (intptr_t)h != -2) ipc_start_client_json(arg, -1, h); else MP_ERR(arg, "Invalid IPC client fd: '%d'\n", fd); } } if (!opts->ipc_path || !*opts->ipc_path) goto out; // Ensure the path is a legal Win32 pipe name by prepending \\.\pipe\ if // it's not already present. Qt's QLocalSocket uses the same logic, so // cross-platform programs that use paths like /tmp/mpv-socket should just // work. (Win32 converts this path to \Device\NamedPipe\tmp\mpv-socket) if (!strncmp(opts->ipc_path, "\\\\.\\pipe\\", 9)) { arg->path = mp_from_utf8(arg, opts->ipc_path); } else { char *path = talloc_asprintf(NULL, "\\\\.\\pipe\\%s", opts->ipc_path); arg->path = mp_from_utf8(arg, path); talloc_free(path); } if (!(arg->death_event = CreateEventW(NULL, TRUE, FALSE, NULL))) goto out; if (mp_thread_create(&arg->thread, ipc_thread, arg)) goto out; talloc_free(opts); return arg; out: if (arg->death_event) CloseHandle(arg->death_event); talloc_free(arg); talloc_free(opts); return NULL; } void mp_uninit_ipc(struct mp_ipc_ctx *arg) { if (!arg) return; SetEvent(arg->death_event); mp_thread_join(arg->thread); CloseHandle(arg->death_event); talloc_free(arg); }