1// Copyright (c) 2012 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#include "cloud_print/virtual_driver/win/port_monitor/port_monitor.h" 6 7#include <lmcons.h> 8#include <shellapi.h> 9#include <shlobj.h> 10#include <strsafe.h> 11#include <userenv.h> 12#include <windows.h> 13#include <winspool.h> 14 15#include "base/at_exit.h" 16#include "base/command_line.h" 17#include "base/file_util.h" 18#include "base/files/file_enumerator.h" 19#include "base/logging.h" 20#include "base/path_service.h" 21#include "base/process/process.h" 22#include "base/process/launch.h" 23#include "base/strings/string16.h" 24#include "base/win/registry.h" 25#include "base/win/scoped_handle.h" 26#include "base/win/windows_version.h" 27#include "chrome/common/chrome_switches.h" 28#include "chrome/installer/launcher_support/chrome_launcher_support.h" 29#include "cloud_print/common/win/cloud_print_utils.h" 30#include "cloud_print/virtual_driver/win/port_monitor/spooler_win.h" 31#include "cloud_print/virtual_driver/win/virtual_driver_consts.h" 32#include "cloud_print/virtual_driver/win/virtual_driver_helpers.h" 33 34namespace cloud_print { 35 36namespace { 37 38const wchar_t kIePath[] = L"Internet Explorer\\iexplore.exe"; 39 40const char kChromeInstallUrl[] = 41 "http://google.com/cloudprint/learn/chrome.html"; 42 43const wchar_t kCloudPrintRegKey[] = L"Software\\Google\\CloudPrint"; 44 45const wchar_t kXpsMimeType[] = L"application/vnd.ms-xpsdocument"; 46 47const wchar_t kAppDataDir[] = L"Google\\Cloud Printer"; 48 49struct MonitorData { 50 scoped_ptr<base::AtExitManager> at_exit_manager; 51}; 52 53struct PortData { 54 PortData() : job_id(0), printer_handle(NULL), file(0) { 55 } 56 ~PortData() { 57 Close(); 58 } 59 void Close() { 60 if (printer_handle) { 61 ClosePrinter(printer_handle); 62 printer_handle = NULL; 63 } 64 if (file) { 65 file_util::CloseFile(file); 66 file = NULL; 67 } 68 } 69 DWORD job_id; 70 HANDLE printer_handle; 71 FILE* file; 72 base::FilePath file_path; 73}; 74 75typedef struct { 76 ACCESS_MASK granted_access; 77} XcvUiData; 78 79 80MONITORUI g_monitor_ui = { 81 sizeof(MONITORUI), 82 MonitorUiAddPortUi, 83 MonitorUiConfigureOrDeletePortUI, 84 MonitorUiConfigureOrDeletePortUI 85}; 86 87MONITOR2 g_monitor_2 = { 88 sizeof(MONITOR2), 89 Monitor2EnumPorts, 90 Monitor2OpenPort, 91 NULL, // OpenPortEx is not supported. 92 Monitor2StartDocPort, 93 Monitor2WritePort, 94 Monitor2ReadPort, 95 Monitor2EndDocPort, 96 Monitor2ClosePort, 97 NULL, // AddPort is not supported. 98 NULL, // AddPortEx is not supported. 99 NULL, // ConfigurePort is not supported. 100 NULL, // DeletePort is not supported. 101 NULL, 102 NULL, // SetPortTimeOuts is not supported. 103 Monitor2XcvOpenPort, 104 Monitor2XcvDataPort, 105 Monitor2XcvClosePort, 106 Monitor2Shutdown 107}; 108 109base::FilePath GetAppDataDir() { 110 base::FilePath file_path; 111 base::win::Version version = base::win::GetVersion(); 112 int path_id = (version >= base::win::VERSION_VISTA) ? 113 base::DIR_LOCAL_APP_DATA_LOW : base::DIR_LOCAL_APP_DATA; 114 if (!PathService::Get(path_id, &file_path)) { 115 LOG(ERROR) << "Can't get DIR_LOCAL_APP_DATA"; 116 return base::FilePath(); 117 } 118 return file_path.Append(kAppDataDir); 119} 120 121// Delete files which where not deleted by chrome. 122void DeleteLeakedFiles(const base::FilePath& dir) { 123 base::Time delete_before = base::Time::Now() - base::TimeDelta::FromDays(1); 124 base::FileEnumerator enumerator(dir, false, base::FileEnumerator::FILES); 125 for (base::FilePath file_path = enumerator.Next(); !file_path.empty(); 126 file_path = enumerator.Next()) { 127 if (enumerator.GetInfo().GetLastModifiedTime() < delete_before) 128 base::DeleteFile(file_path, false); 129 } 130} 131 132// Attempts to retrieve the title of the specified print job. 133// On success returns TRUE and the first title_chars characters of the job title 134// are copied into title. 135// On failure returns FALSE and title is unmodified. 136bool GetJobTitle(HANDLE printer_handle, 137 DWORD job_id, 138 string16 *title) { 139 DCHECK(printer_handle != NULL); 140 DCHECK(title != NULL); 141 DWORD bytes_needed = 0; 142 GetJob(printer_handle, job_id, 1, NULL, 0, &bytes_needed); 143 if (bytes_needed == 0) { 144 LOG(ERROR) << "Unable to get bytes needed for job info."; 145 return false; 146 } 147 scoped_ptr<BYTE[]> buffer(new BYTE[bytes_needed]); 148 if (!GetJob(printer_handle, 149 job_id, 150 1, 151 buffer.get(), 152 bytes_needed, 153 &bytes_needed)) { 154 LOG(ERROR) << "Unable to get job info."; 155 return false; 156 } 157 JOB_INFO_1* job_info = reinterpret_cast<JOB_INFO_1*>(buffer.get()); 158 *title = job_info->pDocument; 159 return true; 160} 161 162// Handler for the UI functions exported by the port monitor. 163// Verifies that a valid parent Window exists and then just displays an 164// error message to let the user know that there is no interactive 165// configuration. 166void HandlePortUi(HWND hwnd, const string16& caption) { 167 if (hwnd != NULL && IsWindow(hwnd)) { 168 DisplayWindowsMessage(hwnd, CO_E_NOT_SUPPORTED, cloud_print::kPortName); 169 } 170} 171 172// Gets the primary token for the user that submitted the print job. 173bool GetUserToken(HANDLE* primary_token) { 174 HANDLE token = NULL; 175 if (!OpenThreadToken(GetCurrentThread(), 176 TOKEN_QUERY|TOKEN_DUPLICATE|TOKEN_ASSIGN_PRIMARY, 177 FALSE, 178 &token)) { 179 LOG(ERROR) << "Unable to get thread token."; 180 return false; 181 } 182 base::win::ScopedHandle token_scoped(token); 183 if (!DuplicateTokenEx(token, 184 TOKEN_QUERY|TOKEN_DUPLICATE|TOKEN_ASSIGN_PRIMARY, 185 NULL, 186 SecurityImpersonation, 187 TokenPrimary, 188 primary_token)) { 189 LOG(ERROR) << "Unable to get primary thread token."; 190 return false; 191 } 192 return true; 193} 194 195// Launches the Cloud Print dialog in Chrome. 196// xps_path references a file to print. 197// job_title is the title to be used for the resulting print job. 198bool LaunchPrintDialog(const base::FilePath& xps_path, 199 const string16& job_title) { 200 HANDLE token = NULL; 201 if (!GetUserToken(&token)) { 202 LOG(ERROR) << "Unable to get user token."; 203 return false; 204 } 205 base::win::ScopedHandle primary_token_scoped(token); 206 207 base::FilePath chrome_path = GetChromeExePath(); 208 if (chrome_path.empty()) { 209 LOG(ERROR) << "Unable to get chrome exe path."; 210 return false; 211 } 212 213 CommandLine command_line(chrome_path); 214 215 base::FilePath chrome_profile = GetChromeProfilePath(); 216 if (!chrome_profile.empty()) { 217 command_line.AppendSwitchPath(switches::kUserDataDir, chrome_profile); 218 } 219 220 command_line.AppendSwitchPath(switches::kCloudPrintFile, 221 xps_path); 222 command_line.AppendSwitchNative(switches::kCloudPrintFileType, 223 kXpsMimeType); 224 command_line.AppendSwitchNative(switches::kCloudPrintJobTitle, 225 job_title); 226 command_line.AppendSwitch(switches::kCloudPrintDeleteFile); 227 base::LaunchOptions options; 228 options.as_user = primary_token_scoped; 229 base::LaunchProcess(command_line, options, NULL); 230 return true; 231} 232 233// Launches a page to allow the user to download chrome. 234// TODO(abodenha@chromium.org) Point to a custom page explaining what's wrong 235// rather than the generic chrome download page. See 236// http://code.google.com/p/chromium/issues/detail?id=112019 237void LaunchChromeDownloadPage() { 238 if (kIsUnittest) 239 return; 240 HANDLE token = NULL; 241 if (!GetUserToken(&token)) { 242 LOG(ERROR) << "Unable to get user token."; 243 return; 244 } 245 base::win::ScopedHandle token_scoped(token); 246 247 base::FilePath ie_path; 248 PathService::Get(base::DIR_PROGRAM_FILESX86, &ie_path); 249 ie_path = ie_path.Append(kIePath); 250 CommandLine command_line(ie_path); 251 command_line.AppendArg(kChromeInstallUrl); 252 253 base::LaunchOptions options; 254 options.as_user = token_scoped; 255 base::LaunchProcess(command_line, options, NULL); 256} 257 258// Returns false if the print job is being run in a context 259// that shouldn't be launching Chrome. 260bool ValidateCurrentUser() { 261 HANDLE token = NULL; 262 if (!GetUserToken(&token)) { 263 // If we can't get the token we're probably not impersonating 264 // the user, so validation should fail. 265 return false; 266 } 267 base::win::ScopedHandle token_scoped(token); 268 269 if (base::win::GetVersion() >= base::win::VERSION_VISTA) { 270 DWORD session_id = 0; 271 DWORD dummy; 272 if (!GetTokenInformation(token_scoped, 273 TokenSessionId, 274 reinterpret_cast<void *>(&session_id), 275 sizeof(DWORD), 276 &dummy)) { 277 return false; 278 } 279 if (session_id == 0) { 280 return false; 281 } 282 } 283 return true; 284} 285} // namespace 286 287base::FilePath ReadPathFromRegistry(HKEY root, const wchar_t* path_name) { 288 base::win::RegKey gcp_key(HKEY_CURRENT_USER, kCloudPrintRegKey, KEY_READ); 289 string16 data; 290 if (SUCCEEDED(gcp_key.ReadValue(path_name, &data)) && 291 base::PathExists(base::FilePath(data))) { 292 return base::FilePath(data); 293 } 294 return base::FilePath(); 295} 296 297base::FilePath ReadPathFromAnyRegistry(const wchar_t* path_name) { 298 base::FilePath result = ReadPathFromRegistry(HKEY_CURRENT_USER, path_name); 299 if (!result.empty()) 300 return result; 301 return ReadPathFromRegistry(HKEY_LOCAL_MACHINE, path_name); 302} 303 304base::FilePath GetChromeExePath() { 305 base::FilePath path = ReadPathFromAnyRegistry(kChromeExePathRegValue); 306 if (!path.empty()) 307 return path; 308 return chrome_launcher_support::GetAnyChromePath(); 309} 310 311base::FilePath GetChromeProfilePath() { 312 base::FilePath path = ReadPathFromAnyRegistry(kChromeProfilePathRegValue); 313 if (!path.empty() && base::DirectoryExists(path)) 314 return path; 315 return base::FilePath(); 316} 317 318BOOL WINAPI Monitor2EnumPorts(HANDLE, 319 wchar_t*, 320 DWORD level, 321 BYTE* ports, 322 DWORD ports_size, 323 DWORD* needed_bytes, 324 DWORD* returned) { 325 if (needed_bytes == NULL) { 326 LOG(ERROR) << "needed_bytes should not be NULL."; 327 SetLastError(ERROR_INVALID_PARAMETER); 328 return FALSE; 329 } 330 if (level == 1) { 331 *needed_bytes = sizeof(PORT_INFO_1); 332 } else if (level == 2) { 333 *needed_bytes = sizeof(PORT_INFO_2); 334 } else { 335 LOG(ERROR) << "Level " << level << "is not supported."; 336 SetLastError(ERROR_INVALID_LEVEL); 337 return FALSE; 338 } 339 *needed_bytes += static_cast<DWORD>(cloud_print::kPortNameSize); 340 if (ports_size < *needed_bytes) { 341 LOG(WARNING) << *needed_bytes << " bytes are required. Only " 342 << ports_size << " were allocated."; 343 SetLastError(ERROR_INSUFFICIENT_BUFFER); 344 return FALSE; 345 } 346 if (ports == NULL) { 347 LOG(ERROR) << "ports should not be NULL."; 348 SetLastError(ERROR_INVALID_PARAMETER); 349 return FALSE; 350 } 351 if (returned == NULL) { 352 LOG(ERROR) << "returned should not be NULL."; 353 SetLastError(ERROR_INVALID_PARAMETER); 354 return FALSE; 355 } 356 357 // Windows expects any strings refernced by PORT_INFO_X structures to 358 // appear at the END of the buffer referenced by ports. Placing 359 // strings immediately after the PORT_INFO_X structure will cause 360 // EnumPorts to fail until the spooler is restarted. 361 // This is NOT mentioned in the documentation. 362 wchar_t* string_target = 363 reinterpret_cast<wchar_t*>(ports + ports_size - 364 cloud_print::kPortNameSize); 365 if (level == 1) { 366 PORT_INFO_1* port_info = reinterpret_cast<PORT_INFO_1*>(ports); 367 port_info->pName = string_target; 368 StringCbCopy(port_info->pName, 369 cloud_print::kPortNameSize, 370 cloud_print::kPortName); 371 } else { 372 PORT_INFO_2* port_info = reinterpret_cast<PORT_INFO_2*>(ports); 373 port_info->pPortName = string_target; 374 StringCbCopy(port_info->pPortName, 375 cloud_print::kPortNameSize, 376 cloud_print::kPortName); 377 port_info->pMonitorName = NULL; 378 port_info->pDescription = NULL; 379 port_info->fPortType = PORT_TYPE_WRITE; 380 port_info->Reserved = 0; 381 } 382 *returned = 1; 383 return TRUE; 384} 385 386BOOL WINAPI Monitor2OpenPort(HANDLE, wchar_t*, HANDLE* handle) { 387 PortData* port_data = new PortData(); 388 if (port_data == NULL) { 389 LOG(ERROR) << "Unable to allocate memory for internal structures."; 390 SetLastError(E_OUTOFMEMORY); 391 return FALSE; 392 } 393 if (handle == NULL) { 394 LOG(ERROR) << "handle should not be NULL."; 395 SetLastError(ERROR_INVALID_PARAMETER); 396 return FALSE; 397 } 398 *handle = (HANDLE)port_data; 399 return TRUE; 400} 401 402BOOL WINAPI Monitor2StartDocPort(HANDLE port_handle, 403 wchar_t* printer_name, 404 DWORD job_id, 405 DWORD, 406 BYTE*) { 407 SetGoogleUpdateUsage(kGoogleUpdateProductId); 408 if (port_handle == NULL) { 409 LOG(ERROR) << "port_handle should not be NULL."; 410 SetLastError(ERROR_INVALID_PARAMETER); 411 return FALSE; 412 } 413 if (printer_name == NULL) { 414 LOG(ERROR) << "printer_name should not be NULL."; 415 SetLastError(ERROR_INVALID_PARAMETER); 416 return FALSE; 417 } 418 if (!ValidateCurrentUser()) { 419 // TODO(abodenha@chromium.org) Abort the print job. 420 return FALSE; 421 } 422 PortData* port_data = reinterpret_cast<PortData*>(port_handle); 423 port_data->job_id = job_id; 424 if (!OpenPrinter(printer_name, &(port_data->printer_handle), NULL)) { 425 LOG(WARNING) << "Unable to open printer " << printer_name << "."; 426 // We can continue without a handle to the printer. 427 // It just means we can't get the job title or tell the spooler that 428 // the print job is complete. 429 // This is the normal flow during a unit test. 430 port_data->printer_handle = NULL; 431 } 432 base::FilePath& file_path = port_data->file_path; 433 base::FilePath app_data_dir = GetAppDataDir(); 434 if (app_data_dir.empty()) 435 return FALSE; 436 DeleteLeakedFiles(app_data_dir); 437 if (!file_util::CreateDirectory(app_data_dir) || 438 !file_util::CreateTemporaryFileInDir(app_data_dir, &file_path)) { 439 LOG(ERROR) << "Can't create temporary file in " << app_data_dir.value(); 440 return FALSE; 441 } 442 port_data->file = file_util::OpenFile(file_path, "wb+"); 443 if (port_data->file == NULL) { 444 LOG(ERROR) << "Error opening file " << file_path.value() << "."; 445 return FALSE; 446 } 447 return TRUE; 448} 449 450BOOL WINAPI Monitor2WritePort(HANDLE port_handle, 451 BYTE* buffer, 452 DWORD buffer_size, 453 DWORD* bytes_written) { 454 PortData* port_data = reinterpret_cast<PortData*>(port_handle); 455 if (!ValidateCurrentUser()) { 456 // TODO(abodenha@chromium.org) Abort the print job. 457 return FALSE; 458 } 459 *bytes_written = 460 static_cast<DWORD>(fwrite(buffer, 1, buffer_size, port_data->file)); 461 if (*bytes_written > 0) { 462 return TRUE; 463 } else { 464 return FALSE; 465 } 466} 467 468BOOL WINAPI Monitor2ReadPort(HANDLE, BYTE*, DWORD, DWORD* read_bytes) { 469 LOG(ERROR) << "Read is not supported."; 470 *read_bytes = 0; 471 SetLastError(ERROR_NOT_SUPPORTED); 472 return FALSE; 473} 474 475BOOL WINAPI Monitor2EndDocPort(HANDLE port_handle) { 476 if (!ValidateCurrentUser()) { 477 // TODO(abodenha@chromium.org) Abort the print job. 478 return FALSE; 479 } 480 PortData* port_data = reinterpret_cast<PortData*>(port_handle); 481 if (port_data == NULL) { 482 SetLastError(ERROR_INVALID_PARAMETER); 483 return FALSE; 484 } 485 486 if (port_data->file != NULL) { 487 file_util::CloseFile(port_data->file); 488 port_data->file = NULL; 489 bool delete_file = true; 490 int64 file_size = 0; 491 file_util::GetFileSize(port_data->file_path, &file_size); 492 if (file_size > 0) { 493 string16 job_title; 494 if (port_data->printer_handle != NULL) { 495 GetJobTitle(port_data->printer_handle, 496 port_data->job_id, 497 &job_title); 498 } 499 if (!LaunchPrintDialog(port_data->file_path, job_title)) { 500 LaunchChromeDownloadPage(); 501 } else { 502 delete_file = false; 503 } 504 } 505 if (delete_file) 506 base::DeleteFile(port_data->file_path, false); 507 } 508 if (port_data->printer_handle != NULL) { 509 // Tell the spooler that the job is complete. 510 SetJob(port_data->printer_handle, 511 port_data->job_id, 512 0, 513 NULL, 514 JOB_CONTROL_SENT_TO_PRINTER); 515 } 516 port_data->Close(); 517 // Return success even if we can't display the dialog. 518 // TODO(abodenha@chromium.org) Come up with a better way of handling 519 // this situation. 520 return TRUE; 521} 522 523BOOL WINAPI Monitor2ClosePort(HANDLE port_handle) { 524 if (port_handle == NULL) { 525 LOG(ERROR) << "port_handle should not be NULL."; 526 SetLastError(ERROR_INVALID_PARAMETER); 527 return FALSE; 528 } 529 PortData* port_data = reinterpret_cast<PortData*>(port_handle); 530 delete port_data; 531 return TRUE; 532} 533 534VOID WINAPI Monitor2Shutdown(HANDLE monitor_handle) { 535 if (monitor_handle != NULL) { 536 MonitorData* monitor_data = 537 reinterpret_cast<MonitorData*>(monitor_handle); 538 delete monitor_handle; 539 } 540} 541 542BOOL WINAPI Monitor2XcvOpenPort(HANDLE, 543 const wchar_t*, 544 ACCESS_MASK granted_access, 545 HANDLE* handle) { 546 if (handle == NULL) { 547 LOG(ERROR) << "handle should not be NULL."; 548 SetLastError(ERROR_INVALID_PARAMETER); 549 return FALSE; 550 } 551 XcvUiData* xcv_data = new XcvUiData(); 552 if (xcv_data == NULL) { 553 LOG(ERROR) << "Unable to allocate memory for internal structures."; 554 SetLastError(E_OUTOFMEMORY); 555 return FALSE; 556 } 557 xcv_data->granted_access = granted_access; 558 *handle = (HANDLE)xcv_data; 559 return TRUE; 560} 561 562DWORD WINAPI Monitor2XcvDataPort(HANDLE xcv_handle, 563 const wchar_t* data_name, 564 BYTE*, 565 DWORD, 566 BYTE* output_data, 567 DWORD output_data_bytes, 568 DWORD* output_data_bytes_needed) { 569 XcvUiData* xcv_data = reinterpret_cast<XcvUiData*>(xcv_handle); 570 DWORD ret_val = ERROR_SUCCESS; 571 if ((xcv_data->granted_access & SERVER_ACCESS_ADMINISTER) == 0) { 572 return ERROR_ACCESS_DENIED; 573 } 574 if (output_data == NULL || output_data_bytes == 0) { 575 return ERROR_INVALID_PARAMETER; 576 } 577 // We don't handle AddPort or DeletePort since we don't support 578 // dynamic creation of ports. 579 if (lstrcmp(L"MonitorUI", data_name) == 0) { 580 DWORD dll_path_len = 0; 581 base::FilePath dll_path(GetPortMonitorDllName()); 582 dll_path_len = static_cast<DWORD>(dll_path.value().length()); 583 if (output_data_bytes_needed != NULL) { 584 *output_data_bytes_needed = dll_path_len; 585 } 586 if (output_data_bytes < dll_path_len) { 587 return ERROR_INSUFFICIENT_BUFFER; 588 } else { 589 ret_val = StringCbCopy(reinterpret_cast<wchar_t*>(output_data), 590 output_data_bytes, 591 dll_path.value().c_str()); 592 } 593 } else { 594 return ERROR_INVALID_PARAMETER; 595 } 596 return ret_val; 597} 598 599BOOL WINAPI Monitor2XcvClosePort(HANDLE handle) { 600 XcvUiData* xcv_data = reinterpret_cast<XcvUiData*>(handle); 601 delete xcv_data; 602 return TRUE; 603} 604 605BOOL WINAPI MonitorUiAddPortUi(const wchar_t*, 606 HWND hwnd, 607 const wchar_t* monitor_name, 608 wchar_t**) { 609 HandlePortUi(hwnd, monitor_name); 610 return TRUE; 611} 612 613BOOL WINAPI MonitorUiConfigureOrDeletePortUI(const wchar_t*, 614 HWND hwnd, 615 const wchar_t* port_name) { 616 HandlePortUi(hwnd, port_name); 617 return TRUE; 618} 619 620} // namespace cloud_print 621 622MONITOR2* WINAPI InitializePrintMonitor2(MONITORINIT*, 623 HANDLE* handle) { 624 cloud_print::MonitorData* monitor_data = new cloud_print::MonitorData; 625 if (monitor_data == NULL) { 626 return NULL; 627 } 628 if (handle != NULL) { 629 *handle = (HANDLE)monitor_data; 630 if (!cloud_print::kIsUnittest) { 631 // Unit tests set up their own AtExitManager 632 monitor_data->at_exit_manager.reset(new base::AtExitManager()); 633 // Single spooler.exe handles verbose users. 634 PathService::DisableCache(); 635 } 636 } else { 637 SetLastError(ERROR_INVALID_PARAMETER); 638 return NULL; 639 } 640 return &cloud_print::g_monitor_2; 641} 642 643MONITORUI* WINAPI InitializePrintMonitorUI(void) { 644 return &cloud_print::g_monitor_ui; 645} 646 647