; Установщик «System Helper» для Windows x64. ; Сборка: ISCC.exe /DAppVersion=0.1.0 installer\setup.iss ; Результат кладётся в dist\syshelper-<версия>-setup.exe #ifndef AppVersion #define AppVersion "0.0.0-dev" #endif #ifndef ExeSource #define ExeSource "..\target\release\syshelper.exe" #endif #define AppName "System Helper" #define AppExeName "syshelper.exe" #define AppUrl "http://127.0.0.1:8787" ; Имя службы — должно совпадать с SERVICE_NAME в src/service.rs. #define ServiceName "SysHelper" ; Каталог данных — должен совпадать с data_dir() в src/main.rs. #define DataDir "{commonappdata}\SysHelper" [Setup] ; AppId определяет программу при обновлении и удалении — не менять между версиями. AppId={{B3F1C0A2-7E5D-4A19-9C3B-2D8E6F4A1B57} AppName={#AppName} AppVersion={#AppVersion} AppVerName={#AppName} {#AppVersion} DefaultDirName={autopf}\SysHelper DefaultGroupName={#AppName} DisableProgramGroupPage=yes UninstallDisplayName={#AppName} UninstallDisplayIcon={app}\{#AppExeName} OutputDir=..\dist OutputBaseFilename=syshelper-{#AppVersion}-setup Compression=lzma2/max SolidCompression=yes WizardStyle=modern ; Ставим в 64-битный Program Files; программа собрана только под x64. ArchitecturesAllowed=x64compatible ArchitecturesInstallIn64BitMode=x64compatible ; Регистрация службы и запись в Program Files требуют администратора. PrivilegesRequired=admin ; Страховка: службу мы останавливаем сами, но если файл всё же занят — ; пусть Inno закроет процесс, а не просит перезагрузку. CloseApplications=yes RestartApplications=no [Languages] Name: "russian"; MessagesFile: "compiler:Languages\Russian.isl" Name: "english"; MessagesFile: "compiler:Default.isl" [Tasks] ; Автозапуск больше не задача: служба и так стартует вместе с системой. Name: "desktopicon"; \ Description: "{cm:CreateDesktopIcon}"; \ GroupDescription: "Дополнительно:"; \ Flags: unchecked [Dirs] ; Каталог данных службы: правила, журнал, хеш пароля, скачанные обновления. ; Права закрываем через icacls ниже — унаследованные от ProgramData позволяют ; обычному пользователю создавать здесь файлы. Он не должен ни читать хеш ; пароля, ни править правила, ни подменять установщик обновления. Name: "{#DataDir}" [Files] Source: "{#ExeSource}"; DestDir: "{app}"; Flags: ignoreversion [Icons] ; Ярлыки ведут на панель: своего окна у программы нет, работает она службой. Name: "{group}\{#AppName}"; Filename: "{#AppUrl}" Name: "{group}\{cm:UninstallProgram,{#AppName}}"; Filename: "{uninstallexe}" Name: "{autodesktop}\{#AppName}"; Filename: "{#AppUrl}"; Tasks: desktopicon [Run] ; Права на каталог данных выставляются в [Code] до записи пароля и старта ; службы — здесь было бы уже поздно: [Run] выполняется последним. Filename: "{#AppUrl}"; \ Description: "Открыть панель"; \ Flags: shellexec nowait postinstall skipifsilent [UninstallDelete] ; Правила, журнал и скачанный установщик сами не удалятся. Type: filesandordirs; Name: "{#DataDir}" [Code] function ServiceExists(): Boolean; var Code: Integer; begin // sc query для несуществующей службы возвращает 1060 Result := Exec(ExpandConstant('{sys}\sc.exe'), 'query {#ServiceName}', '', SW_HIDE, ewWaitUntilTerminated, Code) and (Code = 0); end; function RunSc(const Params: String): Boolean; var Code: Integer; begin Result := Exec(ExpandConstant('{sys}\sc.exe'), Params, '', SW_HIDE, ewWaitUntilTerminated, Code) and (Code = 0); end; // Останавливаем через net stop, а не sc stop: net ждёт фактической остановки, // тогда как sc возвращает управление сразу, оставив службу в STOP_PENDING. // Если не дождаться, exe останется занят и обновление не встанет. procedure StopService(); var Code: Integer; begin if not ServiceExists() then Exit; Exec(ExpandConstant('{sys}\net.exe'), 'stop {#ServiceName}', '', SW_HIDE, ewWaitUntilTerminated, Code); // даём файловой системе отпустить exe Sleep(1000); end; // Страница пароля. Показываем только при первой установке: при обновлении // пароль уже задан, а тихая установка (автообновление) проходит без мастера. var PasswordPage: TInputQueryWizardPage; function AuthConfigured(): Boolean; begin Result := FileExists(ExpandConstant('{#DataDir}\auth.hash')); end; procedure InitializeWizard(); begin PasswordPage := CreateInputQueryPage(wpSelectTasks, 'Пароль панели', 'Защитите панель управления от того, кого она ограничивает.', 'Панель открывается в браузере по адресу ' + '{#AppUrl}' + '. Без пароля ' + 'её сможет открыть любой пользователь этого компьютера и отключить правила.'); PasswordPage.Add('Пароль:', True); end; function ShouldSkipPage(PageID: Integer): Boolean; begin Result := (PasswordPage <> nil) and (PageID = PasswordPage.ID) and AuthConfigured(); end; function NextButtonClick(CurPageID: Integer): Boolean; begin Result := True; if (PasswordPage <> nil) and (CurPageID = PasswordPage.ID) and (Trim(PasswordPage.Values[0]) = '') then begin MsgBox('Задайте пароль — иначе панель останется открытой для всех.', mbError, MB_OK); Result := False; end; end; function PrepareToInstall(var NeedsRestart: Boolean): String; begin StopService(); Result := ''; end; // Пароль передаём файлом, а не аргументом командной строки: аргумент был бы // виден в списке процессов. Файл пишем в UTF-8 явно — Rust читает его так же, // иначе кириллический пароль исказился бы и вход перестал работать. procedure SetPassword(); var PwFile: String; Code: Integer; begin // При тихой установке (автообновление) мастера нет и страницы тоже — // обращение к ней уронило бы установщик посреди обновления. if (PasswordPage = nil) or AuthConfigured() or (Trim(PasswordPage.Values[0]) = '') then Exit; PwFile := ExpandConstant('{tmp}\pw.txt'); if not SaveStringsToUTF8File(PwFile, [PasswordPage.Values[0]], False) then Exit; // exe сам удалит файл после чтения Exec(ExpandConstant('{app}\{#AppExeName}'), '--set-password-file "' + PwFile + '"', '', SW_HIDE, ewWaitUntilTerminated, Code); end; // Запускаем через net start, а не sc start, по той же причине, что и остановку: // net дожидается фактического запуска и возвращает ошибку, если он не удался. // sc отдаёт управление сразу, и сломанная установка выглядела бы успешной. function StartService(): Boolean; var Code: Integer; begin Result := Exec(ExpandConstant('{sys}\net.exe'), 'start {#ServiceName}', '', SW_HIDE, ewWaitUntilTerminated, Code) and (Code = 0); end; function InstallService(): Boolean; var Bin: String; begin // Путь к exe содержит пробелы (Program Files), поэтому его кавычки должны // попасть внутрь значения binPath — sc.exe ждёт их экранированными, иначе // служба зарегистрируется с обрезанным путём и не запустится. Bin := 'binPath= "\"' + ExpandConstant('{app}\{#AppExeName}') + '\" --service"'; if ServiceExists() then // пути могли измениться при переустановке в другой каталог RunSc('config {#ServiceName} ' + Bin + ' start= delayed-auto') else RunSc('create {#ServiceName} ' + Bin + ' start= delayed-auto ' + 'DisplayName= "{#AppName}"'); RunSc('description {#ServiceName} "Родительский контроль: следит за запуском программ."'); // Родительский контроль не должен исчезать из-за одной аварии. RunSc('failure {#ServiceName} reset= 86400 actions= restart/5000/restart/5000/restart/60000'); Result := StartService(); end; // Каталог данных закрываем от обычных пользователей: унаследованные от // ProgramData права позволяют им создавать здесь файлы. Ребёнок не должен ни // читать хеш пароля, ни править правила, ни подменять установщик обновления. // SID вместо имён групп — имена локализованы, «Administrators» на русской // системе не существует. procedure LockDownDataDir(); var Code: Integer; begin Exec(ExpandConstant('{sys}\icacls.exe'), '"' + ExpandConstant('{#DataDir}') + '" /inheritance:r /grant:r ' + '*S-1-5-18:(OI)(CI)F *S-1-5-32-544:(OI)(CI)F', '', SW_HIDE, ewWaitUntilTerminated, Code); end; // Без правила брандмауэра панель доступна только с самого компьютера: Windows // режет входящие подключения по умолчанию. Ограничиваемся частными и доменными // сетями — в публичной (кафе, вокзал) панель наружу смотреть не должна. procedure RemoveFirewallRule(); var Code: Integer; begin Exec(ExpandConstant('{sys}\netsh.exe'), 'advfirewall firewall delete rule name="{#AppName}"', '', SW_HIDE, ewWaitUntilTerminated, Code); end; procedure OpenFirewallPort(); var Code: Integer; begin // сначала снимаем старое правило, иначе при переустановке они накопятся RemoveFirewallRule(); Exec(ExpandConstant('{sys}\netsh.exe'), 'advfirewall firewall add rule name="{#AppName}" dir=in action=allow' + ' protocol=TCP localport=8787 profile=private,domain' + ' program="' + ExpandConstant('{app}\{#AppExeName}') + '"', '', SW_HIDE, ewWaitUntilTerminated, Code); end; procedure CurStepChanged(CurStep: TSetupStep); var Started: Boolean; begin if CurStep <> ssPostInstall then Exit; // Порядок важен: сначала закрываем каталог, потом кладём туда хеш пароля и // только затем поднимаем службу. LockDownDataDir(); SetPassword(); OpenFirewallPort(); Started := InstallService(); // Обе проверки — про молчаливый отказ: без них установка отрапортует об // успехе, а защиты не будет. При тихой установке (автообновление) человека // за экраном нет, и разбираться придётся по журналу Inno, /LOG= его пишет. if WizardSilent() then Exit; if not AuthConfigured() then MsgBox('Не удалось сохранить пароль панели.'#13#10 + 'Панель откроется без пароля, и задать его сможет любой, ' + 'кто зайдёт первым.', mbError, MB_OK); if not Started then MsgBox('Служба не запустилась — контроль сейчас не работает.'#13#10 + 'Откройте «Службы» и посмотрите System Helper.', mbError, MB_OK); end; procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); begin if CurUninstallStep <> usUninstall then Exit; StopService(); RunSc('delete {#ServiceName}'); RemoveFirewallRule(); end;