Files
syshelper/installer/setup.iss
T
robonen 0223dd817b
CI / Windows x64 (push) Successful in 9m31s
feat: add custom dns resolver
2026-08-09 15:47:38 +07:00

316 lines
14 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
; Установщик «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;
// Пока служба работала, адаптеры смотрели на её резолвер (127.0.0.1). Если
// удалить программу и оставить эту настройку, машина останется без разрешения
// имён вовсе — то есть без интернета. Возврат делает сама программа: ключ
// --restore-dns снимает и настройку адаптеров, и правила брандмауэра.
procedure RestoreDns();
var
Code: Integer;
begin
Exec(ExpandConstant('{app}\{#AppExeName}'), '--restore-dns',
'', SW_HIDE, ewWaitUntilTerminated, Code);
end;
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
begin
if CurUninstallStep <> usUninstall then
Exit;
// Порядок обратный установке: сначала гасим службу, потом возвращаем сеть —
// работающая служба переставила бы настройку обратно на следующем такте.
StopService();
RestoreDns();
RunSc('delete {#ServiceName}');
RemoveFirewallRule();
end;