1 /*
2 * This file is part of mpv.
3 *
4 * mpv is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2.1 of the License, or (at your option) any later version.
8 *
9 * mpv is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public
15 * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
16 */
17
18 #include <windows.h>
19 #include <sddl.h>
20
21 #include "config.h"
22
23 #include "osdep/io.h"
24 #include "osdep/threads.h"
25 #include "osdep/windows_utils.h"
26
27 #include "common/common.h"
28 #include "common/global.h"
29 #include "common/msg.h"
30 #include "input/input.h"
31 #include "libmpv/client.h"
32 #include "options/m_config.h"
33 #include "options/options.h"
34 #include "player/client.h"
35
36 struct mp_ipc_ctx {
37 struct mp_log *log;
38 struct mp_client_api *client_api;
39 const wchar_t *path;
40
41 pthread_t thread;
42 HANDLE death_event;
43 };
44
45 struct client_arg {
46 struct mp_log *log;
47 struct mpv_handle *client;
48
49 char *client_name;
50 HANDLE client_h;
51 bool writable;
52 OVERLAPPED write_ol;
53 };
54
55 // Get a string SID representing the current user. Must be freed by LocalFree.
get_user_sid(void)56 static char *get_user_sid(void)
57 {
58 char *ssid = NULL;
59 TOKEN_USER *info = NULL;
60 HANDLE t;
61 if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &t))
62 goto done;
63
64 DWORD info_len;
65 if (!GetTokenInformation(t, TokenUser, NULL, 0, &info_len) &&
66 GetLastError() != ERROR_INSUFFICIENT_BUFFER)
67 goto done;
68
69 info = talloc_size(NULL, info_len);
70 if (!GetTokenInformation(t, TokenUser, info, info_len, &info_len))
71 goto done;
72 if (!info->User.Sid)
73 goto done;
74
75 ConvertSidToStringSidA(info->User.Sid, &ssid);
76 done:
77 if (t)
78 CloseHandle(t);
79 talloc_free(info);
80 return ssid;
81 }
82
83 // Get a string SID for the process integrity level. Must be freed by LocalFree.
get_integrity_sid(void)84 static char *get_integrity_sid(void)
85 {
86 char *ssid = NULL;
87 TOKEN_MANDATORY_LABEL *info = NULL;
88 HANDLE t;
89 if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &t))
90 goto done;
91
92 DWORD info_len;
93 if (!GetTokenInformation(t, TokenIntegrityLevel, NULL, 0, &info_len) &&
94 GetLastError() != ERROR_INSUFFICIENT_BUFFER)
95 goto done;
96
97 info = talloc_size(NULL, info_len);
98 if (!GetTokenInformation(t, TokenIntegrityLevel, info, info_len, &info_len))
99 goto done;
100 if (!info->Label.Sid)
101 goto done;
102
103 ConvertSidToStringSidA(info->Label.Sid, &ssid);
104 done:
105 if (t)
106 CloseHandle(t);
107 talloc_free(info);
108 return ssid;
109 }
110
111 // Create a security descriptor that only grants access to processes running
112 // under the current user at the current integrity level or higher
create_restricted_sd(void)113 static PSECURITY_DESCRIPTOR create_restricted_sd(void)
114 {
115 char *user_sid = get_user_sid();
116 char *integrity_sid = get_integrity_sid();
117 if (!user_sid || !integrity_sid)
118 return NULL;
119
120 char *sddl = talloc_asprintf(NULL,
121 "O:%s" // Set the owner to user_sid
122 "D:(A;;GRGW;;;%s)" // Grant GENERIC_{READ,WRITE} access to user_sid
123 "S:(ML;;NRNWNX;;;%s)", // Disallow read, write and execute permissions
124 // to integrity levels below integrity_sid
125 user_sid, user_sid, integrity_sid);
126 LocalFree(user_sid);
127 LocalFree(integrity_sid);
128
129 PSECURITY_DESCRIPTOR sd = NULL;
130 ConvertStringSecurityDescriptorToSecurityDescriptorA(sddl, SDDL_REVISION_1,
131 &sd, NULL);
132 talloc_free(sddl);
133
134 return sd;
135 }
136
wakeup_cb(void * d)137 static void wakeup_cb(void *d)
138 {
139 HANDLE event = d;
140 SetEvent(event);
141 }
142
143 // Wrapper for ReadFile that treats ERROR_IO_PENDING as success
async_read(HANDLE file,void * buf,unsigned size,OVERLAPPED * ol)144 static DWORD async_read(HANDLE file, void *buf, unsigned size, OVERLAPPED* ol)
145 {
146 DWORD err = ReadFile(file, buf, size, NULL, ol) ? 0 : GetLastError();
147 return err == ERROR_IO_PENDING ? 0 : err;
148 }
149
150 // Wrapper for WriteFile that treats ERROR_IO_PENDING as success
async_write(HANDLE file,const void * buf,unsigned size,OVERLAPPED * ol)151 static DWORD async_write(HANDLE file, const void *buf, unsigned size, OVERLAPPED* ol)
152 {
153 DWORD err = WriteFile(file, buf, size, NULL, ol) ? 0 : GetLastError();
154 return err == ERROR_IO_PENDING ? 0 : err;
155 }
156
pipe_error_is_fatal(DWORD error)157 static bool pipe_error_is_fatal(DWORD error)
158 {
159 switch (error) {
160 case 0:
161 case ERROR_HANDLE_EOF:
162 case ERROR_BROKEN_PIPE:
163 case ERROR_PIPE_NOT_CONNECTED:
164 case ERROR_NO_DATA:
165 return false;
166 }
167 return true;
168 }
169
ipc_write_str(struct client_arg * arg,const char * buf)170 static DWORD ipc_write_str(struct client_arg *arg, const char *buf)
171 {
172 DWORD error = 0;
173
174 if ((error = async_write(arg->client_h, buf, strlen(buf), &arg->write_ol)))
175 goto done;
176 if (!GetOverlappedResult(arg->client_h, &arg->write_ol, &(DWORD){0}, TRUE)) {
177 error = GetLastError();
178 goto done;
179 }
180
181 done:
182 if (pipe_error_is_fatal(error)) {
183 MP_VERBOSE(arg, "Error writing to pipe: %s\n",
184 mp_HRESULT_to_str(HRESULT_FROM_WIN32(error)));
185 }
186
187 if (error)
188 arg->writable = false;
189 return error;
190 }
191
report_read_error(struct client_arg * arg,DWORD error)192 static void report_read_error(struct client_arg *arg, DWORD error)
193 {
194 // Only report the error if it's not just due to the pipe closing
195 if (pipe_error_is_fatal(error)) {
196 MP_ERR(arg, "Error reading from pipe: %s\n",
197 mp_HRESULT_to_str(HRESULT_FROM_WIN32(error)));
198 } else {
199 MP_VERBOSE(arg, "Client disconnected\n");
200 }
201 }
202
client_thread(void * p)203 static void *client_thread(void *p)
204 {
205 pthread_detach(pthread_self());
206
207 struct client_arg *arg = p;
208 char buf[4096];
209 HANDLE wakeup_event = CreateEventW(NULL, TRUE, FALSE, NULL);
210 OVERLAPPED ol = { .hEvent = CreateEventW(NULL, TRUE, TRUE, NULL) };
211 bstr client_msg = { talloc_strdup(NULL, ""), 0 };
212 DWORD ioerr = 0;
213 DWORD r;
214
215 mpthread_set_name(arg->client_name);
216
217 arg->write_ol.hEvent = CreateEventW(NULL, TRUE, TRUE, NULL);
218 if (!wakeup_event || !ol.hEvent || !arg->write_ol.hEvent) {
219 MP_ERR(arg, "Couldn't create events\n");
220 goto done;
221 }
222
223 MP_VERBOSE(arg, "Client connected\n");
224
225 mpv_set_wakeup_callback(arg->client, wakeup_cb, wakeup_event);
226
227 // Do the first read operation on the pipe
228 if ((ioerr = async_read(arg->client_h, buf, 4096, &ol))) {
229 report_read_error(arg, ioerr);
230 goto done;
231 }
232
233 while (1) {
234 HANDLE handles[] = { wakeup_event, ol.hEvent };
235 int n = WaitForMultipleObjects(2, handles, FALSE, 0);
236 if (n == WAIT_TIMEOUT)
237 n = WaitForMultipleObjects(2, handles, FALSE, INFINITE);
238
239 switch (n) {
240 case WAIT_OBJECT_0: // wakeup_event
241 ResetEvent(wakeup_event);
242
243 while (1) {
244 mpv_event *event = mpv_wait_event(arg->client, 0);
245
246 if (event->event_id == MPV_EVENT_NONE)
247 break;
248
249 if (event->event_id == MPV_EVENT_SHUTDOWN)
250 goto done;
251
252 if (!arg->writable)
253 continue;
254
255 char *event_msg = mp_json_encode_event(event);
256 if (!event_msg) {
257 MP_ERR(arg, "Encoding error\n");
258 goto done;
259 }
260
261 ipc_write_str(arg, event_msg);
262 talloc_free(event_msg);
263 }
264
265 break;
266 case WAIT_OBJECT_0 + 1: // ol.hEvent
267 // Complete the read operation on the pipe
268 if (!GetOverlappedResult(arg->client_h, &ol, &r, TRUE)) {
269 report_read_error(arg, GetLastError());
270 goto done;
271 }
272
273 bstr_xappend(NULL, &client_msg, (bstr){buf, r});
274 while (bstrchr(client_msg, '\n') != -1) {
275 char *reply_msg = mp_ipc_consume_next_command(arg->client,
276 NULL, &client_msg);
277 if (reply_msg && arg->writable)
278 ipc_write_str(arg, reply_msg);
279 talloc_free(reply_msg);
280 }
281
282 // Begin the next read operation on the pipe
283 if ((ioerr = async_read(arg->client_h, buf, 4096, &ol))) {
284 report_read_error(arg, ioerr);
285 goto done;
286 }
287 break;
288 default:
289 MP_ERR(arg, "WaitForMultipleObjects failed\n");
290 goto done;
291 }
292 }
293
294 done:
295 if (client_msg.len > 0)
296 MP_WARN(arg, "Ignoring unterminated command on disconnect.\n");
297
298 if (CancelIoEx(arg->client_h, &ol) || GetLastError() != ERROR_NOT_FOUND)
299 GetOverlappedResult(arg->client_h, &ol, &(DWORD){0}, TRUE);
300 if (wakeup_event)
301 CloseHandle(wakeup_event);
302 if (ol.hEvent)
303 CloseHandle(ol.hEvent);
304 if (arg->write_ol.hEvent)
305 CloseHandle(arg->write_ol.hEvent);
306
307 CloseHandle(arg->client_h);
308 mpv_destroy(arg->client);
309 talloc_free(arg);
310 return NULL;
311 }
312
ipc_start_client(struct mp_ipc_ctx * ctx,struct client_arg * client)313 static void ipc_start_client(struct mp_ipc_ctx *ctx, struct client_arg *client)
314 {
315 client->client = mp_new_client(ctx->client_api, client->client_name),
316 client->log = mp_client_get_log(client->client);
317
318 pthread_t client_thr;
319 if (pthread_create(&client_thr, NULL, client_thread, client)) {
320 mpv_destroy(client->client);
321 CloseHandle(client->client_h);
322 talloc_free(client);
323 }
324 }
325
ipc_start_client_json(struct mp_ipc_ctx * ctx,int id,HANDLE h)326 static void ipc_start_client_json(struct mp_ipc_ctx *ctx, int id, HANDLE h)
327 {
328 struct client_arg *client = talloc_ptrtype(NULL, client);
329 *client = (struct client_arg){
330 .client_name = talloc_asprintf(client, "ipc-%d", id),
331 .client_h = h,
332 .writable = true,
333 };
334
335 ipc_start_client(ctx, client);
336 }
337
mp_ipc_start_anon_client(struct mp_ipc_ctx * ctx,struct mpv_handle * h,int out_fd[2])338 bool mp_ipc_start_anon_client(struct mp_ipc_ctx *ctx, struct mpv_handle *h,
339 int out_fd[2])
340 {
341 return false;
342 }
343
ipc_thread(void * p)344 static void *ipc_thread(void *p)
345 {
346 // Use PIPE_TYPE_MESSAGE | PIPE_READMODE_BYTE so message framing is
347 // maintained for message-mode clients, but byte-mode clients can still
348 // connect, send and receive data. This is the most compatible mode.
349 static const DWORD state =
350 PIPE_TYPE_MESSAGE | PIPE_READMODE_BYTE | PIPE_WAIT |
351 PIPE_REJECT_REMOTE_CLIENTS;
352 static const DWORD mode =
353 PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED;
354 static const DWORD bufsiz = 4096;
355
356 struct mp_ipc_ctx *arg = p;
357 HANDLE server = INVALID_HANDLE_VALUE;
358 HANDLE client = INVALID_HANDLE_VALUE;
359 int client_num = 0;
360
361 mpthread_set_name("ipc named pipe listener");
362 MP_VERBOSE(arg, "Starting IPC master\n");
363
364 SECURITY_ATTRIBUTES sa = {
365 .nLength = sizeof sa,
366 .lpSecurityDescriptor = create_restricted_sd(),
367 };
368 if (!sa.lpSecurityDescriptor) {
369 MP_ERR(arg, "Couldn't create security descriptor");
370 goto done;
371 }
372
373 OVERLAPPED ol = { .hEvent = CreateEventW(NULL, TRUE, TRUE, NULL) };
374 if (!ol.hEvent) {
375 MP_ERR(arg, "Couldn't create event");
376 goto done;
377 }
378
379 server = CreateNamedPipeW(arg->path, mode | FILE_FLAG_FIRST_PIPE_INSTANCE,
380 state, PIPE_UNLIMITED_INSTANCES, bufsiz, bufsiz, 0, &sa);
381 if (server == INVALID_HANDLE_VALUE) {
382 MP_ERR(arg, "Couldn't create first pipe instance: %s\n",
383 mp_LastError_to_str());
384 goto done;
385 }
386
387 MP_VERBOSE(arg, "Listening to IPC pipe.\n");
388
389 while (1) {
390 DWORD err = ConnectNamedPipe(server, &ol) ? 0 : GetLastError();
391
392 if (err == ERROR_IO_PENDING) {
393 int n = WaitForMultipleObjects(2, (HANDLE[]) {
394 arg->death_event,
395 ol.hEvent,
396 }, FALSE, INFINITE) - WAIT_OBJECT_0;
397
398 switch (n) {
399 case 0:
400 // Stop waiting for new clients
401 CancelIo(server);
402 GetOverlappedResult(server, &ol, &(DWORD){0}, TRUE);
403 goto done;
404 case 1:
405 // Complete the ConnectNamedPipe request
406 err = GetOverlappedResult(server, &ol, &(DWORD){0}, TRUE)
407 ? 0 : GetLastError();
408 break;
409 default:
410 MP_ERR(arg, "WaitForMultipleObjects failed\n");
411 goto done;
412 }
413 }
414
415 // ERROR_PIPE_CONNECTED is returned if a client connects before
416 // ConnectNamedPipe is called. ERROR_NO_DATA is returned if a client
417 // connects, (possibly) writes data and exits before ConnectNamedPipe
418 // is called. Both cases should be handled as normal connections.
419 if (err == ERROR_PIPE_CONNECTED || err == ERROR_NO_DATA)
420 err = 0;
421
422 if (err) {
423 MP_ERR(arg, "ConnectNamedPipe failed: %s\n",
424 mp_HRESULT_to_str(HRESULT_FROM_WIN32(err)));
425 goto done;
426 }
427
428 // Create the next pipe instance before the client thread to avoid the
429 // theoretical race condition where the client thread immediately
430 // closes the handle and there are no active instances of the pipe
431 client = server;
432 server = CreateNamedPipeW(arg->path, mode, state,
433 PIPE_UNLIMITED_INSTANCES, bufsiz, bufsiz, 0, &sa);
434 if (server == INVALID_HANDLE_VALUE) {
435 MP_ERR(arg, "Couldn't create additional pipe instance: %s\n",
436 mp_LastError_to_str());
437 goto done;
438 }
439
440 ipc_start_client_json(arg, client_num++, client);
441 client = NULL;
442 }
443
444 done:
445 if (sa.lpSecurityDescriptor)
446 LocalFree(sa.lpSecurityDescriptor);
447 if (client != INVALID_HANDLE_VALUE)
448 CloseHandle(client);
449 if (server != INVALID_HANDLE_VALUE)
450 CloseHandle(server);
451 if (ol.hEvent)
452 CloseHandle(ol.hEvent);
453 return NULL;
454 }
455
mp_init_ipc(struct mp_client_api * client_api,struct mpv_global * global)456 struct mp_ipc_ctx *mp_init_ipc(struct mp_client_api *client_api,
457 struct mpv_global *global)
458 {
459 struct MPOpts *opts = mp_get_config_group(NULL, global, &mp_opt_root);
460
461 struct mp_ipc_ctx *arg = talloc_ptrtype(NULL, arg);
462 *arg = (struct mp_ipc_ctx){
463 .log = mp_log_new(arg, global->log, "ipc"),
464 .client_api = client_api,
465 };
466
467 if (!opts->ipc_path || !*opts->ipc_path)
468 goto out;
469
470 // Ensure the path is a legal Win32 pipe name by prepending \\.\pipe\ if
471 // it's not already present. Qt's QLocalSocket uses the same logic, so
472 // cross-platform programs that use paths like /tmp/mpv-socket should just
473 // work. (Win32 converts this path to \Device\NamedPipe\tmp\mpv-socket)
474 if (!strncmp(opts->ipc_path, "\\\\.\\pipe\\", 9)) {
475 arg->path = mp_from_utf8(arg, opts->ipc_path);
476 } else {
477 char *path = talloc_asprintf(NULL, "\\\\.\\pipe\\%s", opts->ipc_path);
478 arg->path = mp_from_utf8(arg, path);
479 talloc_free(path);
480 }
481
482 if (!(arg->death_event = CreateEventW(NULL, TRUE, FALSE, NULL)))
483 goto out;
484
485 if (pthread_create(&arg->thread, NULL, ipc_thread, arg))
486 goto out;
487
488 talloc_free(opts);
489 return arg;
490
491 out:
492 if (arg->death_event)
493 CloseHandle(arg->death_event);
494 talloc_free(arg);
495 talloc_free(opts);
496 return NULL;
497 }
498
mp_uninit_ipc(struct mp_ipc_ctx * arg)499 void mp_uninit_ipc(struct mp_ipc_ctx *arg)
500 {
501 if (!arg)
502 return;
503
504 SetEvent(arg->death_event);
505 pthread_join(arg->thread, NULL);
506
507 CloseHandle(arg->death_event);
508 talloc_free(arg);
509 }
510