diff --git a/Options.pas b/Options.pas new file mode 100644 index 0000000..420d363 --- /dev/null +++ b/Options.pas @@ -0,0 +1,137 @@ +unit Options; + +interface + +uses + System.SysUtils, + IniFiles, + System.RegularExpressions; + +const + DLL_FILENAME = 'version.dll'; + OPTIONS_FILENAME = 'drover.ini'; + +type + TDroverOptions = record + proxy: string; + useNekoboxProxy: boolean; + nekoboxProxy: string; + end; + + TProxyValue = record + isSpecified: boolean; + prot: string; + login: string; + password: string; + host: string; + port: integer; + isHttp: boolean; + isSocks5: boolean; + isAuth: boolean; + + procedure ParseFromString(url: string); + function FormatToHttpEnv: string; + function FormatToChromeProxy: string; + end; + +function LoadOptions(filename: string): TDroverOptions; +function SaveOptions(filename: string; opt: TDroverOptions): boolean; + +implementation + +procedure TProxyValue.ParseFromString(url: string); +var + match: TMatch; +begin + isSpecified := false; + prot := ''; + login := ''; + password := ''; + host := ''; + port := 0; + isHttp := false; + isSocks5 := false; + isAuth := false; + + match := TRegEx.match(Trim(url), '\A(?:([a-z\d]+)://)?(?:(.+):(.+)@)?(.+):(\d+)\z', [roIgnoreCase]); + if not match.Success then + exit; + + isSpecified := true; + + prot := LowerCase(Trim(match.Groups[1].Value)); + if (prot = '') or (prot = 'https') then + prot := 'http'; + + login := Trim(match.Groups[2].Value); + password := Trim(match.Groups[3].Value); + + host := Trim(match.Groups[4].Value); + port := StrToIntDef(match.Groups[5].Value, 0); + + isHttp := (prot = 'http'); + isSocks5 := (prot = 'socks5'); + isAuth := (login <> '') and (password <> ''); +end; + +function TProxyValue.FormatToHttpEnv: string; +begin + if not isSpecified then + exit(''); + + result := 'http://'; + if isAuth then + result := result + login + ':' + password + '@'; + result := result + host + ':' + IntToStr(port); +end; + +function TProxyValue.FormatToChromeProxy: string; +begin + if isSpecified then + result := Format('%s://%s:%d', [prot, host, port]) + else + result := ''; +end; + +function LoadOptions(filename: string): TDroverOptions; +var + f: TIniFile; +begin + result := Default (TDroverOptions); + + try + f := TIniFile.Create(filename); + try + with f do + begin + result.proxy := ReadString('drover', 'proxy', ''); + result.useNekoboxProxy := ReadBool('drover', 'use-nekobox-proxy', false); + result.nekoboxProxy := ReadString('drover', 'nekobox-proxy', '127.0.0.1:2080'); + end; + finally + f.Free; + end; + except + end; +end; + +function SaveOptions(filename: string; opt: TDroverOptions): boolean; +var + f: TextFile; +begin + try + AssignFile(f, filename); + try + Rewrite(f); + WriteLn(f, '[drover]'); + WriteLn(f, Trim('proxy = ' + opt.proxy)); + finally + CloseFile(f); + end; + result := true; + except + result := false; + end; +end; + +end. diff --git a/README.md b/README.md index b33c5ce..aa11748 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Discord Drover +# Discord Drover (Proxy Settings for Discord) Discord Drover is a program that forces the Discord application for Windows to use a specified proxy server (HTTP or SOCKS5) for TCP connections (chat, updates). This may be necessary because the original Discord application lacks proxy settings, and the global system proxy is also not used. Additionally, the program slightly interferes with Discord's outgoing UDP traffic, which helps bypass some local restrictions on voice chats. @@ -6,7 +6,15 @@ The program works locally at the specific process level (without drivers) and do ## Installation -To use Discord Drover, copy the `version.dll` and `drover.ini` files into the folder containing the `Discord.exe` file (not `Update.exe`). The proxy itself is specified in the `drover.ini` file under the `proxy` parameter. +### Automatic Installation + +For an easier setup, use the included installer `drover.exe`. Run the program and fill in the proxy settings, then click **Install** to automatically place the necessary files in the correct folder. + +To uninstall the program and remove all associated files, run `drover.exe` again and click **Uninstall**. + +### Manual Installation + +If you prefer manual installation, copy the `version.dll` and `drover.ini` files into the folder containing the `Discord.exe` file (not `Update.exe`). The proxy itself is specified in the `drover.ini` file under the `proxy` parameter. ### Example `drover.ini` Configuration: diff --git a/drover.dpr b/drover.dpr index 048729e..ae21f5e 100644 --- a/drover.dpr +++ b/drover.dpr @@ -1,574 +1,474 @@ -library drover; - -uses - System.SysUtils, - Winapi.Windows, - DDetours, - PsAPI, - TlHelp32, - WinSock, - WinSock2, - IniFiles, - System.RegularExpressions, - SocketManager; - -type - TDroverOptions = record - proxy: string; - useNekoboxProxy: boolean; - nekoboxProxy: string; - end; - - TProxyValue = record - isSpecified: boolean; - prot: string; - login: string; - password: string; - host: string; - port: integer; - isHttp: boolean; - isSocks5: boolean; - isAuth: boolean; - - procedure ParseFromString(url: string); - function FormatToHttpEnv: string; - function FormatToChromeProxy: string; - end; - -const - OPTIONS_FILENAME = 'drover.ini'; - DLL_FILENAME = 'version.dll'; - -var - RealGetFileVersionInfoA: function(lptstrFilename: LPSTR; dwHandle, dwLen: DWORD; lpData: Pointer): bool; stdcall; - RealGetFileVersionInfoW: function(lptstrFilename: LPWSTR; dwHandle, dwLen: DWORD; lpData: Pointer): bool; stdcall; - RealGetFileVersionInfoSizeA: function(lptstrFilename: LPSTR; var lpdwHandle: DWORD): DWORD; stdcall; - RealGetFileVersionInfoSizeW: function(lptstrFilename: LPWSTR; var lpdwHandle: DWORD): DWORD; stdcall; - RealVerFindFileA: function(uFlags: DWORD; szFileName, szWinDir, szAppDir, szCurDir: LPSTR; var lpuCurDirLen: UINT; - szDestDir: LPSTR; var lpuDestDirLen: UINT): DWORD; stdcall; - RealVerFindFileW: function(uFlags: DWORD; szFileName, szWinDir, szAppDir, szCurDir: LPWSTR; var lpuCurDirLen: UINT; - szDestDir: LPWSTR; var lpuDestDirLen: UINT): DWORD; stdcall; - RealVerInstallFileA: function(uFlags: DWORD; szSrcFileName, szDestFileName, szSrcDir, szDestDir, szCurDir, - szTmpFile: LPSTR; var lpuTmpFileLen: UINT): DWORD; stdcall; - RealVerInstallFileW: function(uFlags: DWORD; szSrcFileName, szDestFileName, szSrcDir, szDestDir, szCurDir, - szTmpFile: LPWSTR; var lpuTmpFileLen: UINT): DWORD; stdcall; - RealVerLanguageNameA: function(wLang: DWORD; szLang: LPSTR; nSize: DWORD): DWORD; stdcall; - RealVerLanguageNameW: function(wLang: DWORD; szLang: LPWSTR; nSize: DWORD): DWORD; stdcall; - RealVerQueryValueA: function(pBlock: Pointer; lpSubBlock: LPSTR; var lplpBuffer: Pointer; var puLen: UINT) - : bool; stdcall; - RealVerQueryValueW: function(pBlock: Pointer; lpSubBlock: LPWSTR; var lplpBuffer: Pointer; var puLen: UINT) - : bool; stdcall; - - RealGetEnvironmentVariableW: function(lpName: LPCWSTR; lpBuffer: LPWSTR; nSize: DWORD): DWORD; stdcall; - RealCreateProcessW: function(lpApplicationName: LPCWSTR; lpCommandLine: LPWSTR; - lpProcessAttributes, lpThreadAttributes: PSecurityAttributes; bInheritHandles: bool; dwCreationFlags: DWORD; - lpEnvironment: Pointer; lpCurrentDirectory: LPCWSTR; const lpStartupInfo: TStartupInfoW; - var lpProcessInformation: TProcessInformation): bool; stdcall; - RealGetCommandLineW: function: LPWSTR; stdcall; - - RealSocket: function(af, type_, protocol: integer): TSocket; stdcall; - RealWSASocket: function(af, type_, protocol: integer; lpProtocolInfo: LPWSAPROTOCOL_INFO; g: GROUP; dwFlags: DWORD) - : TSocket; stdcall; - RealWSASend: function(s: TSocket; lpBuffers: LPWSABUF; dwBufferCount: DWORD; lpNumberOfBytesSent: PDWORD; - dwFlags: DWORD; lpOverlapped: LPWSAOVERLAPPED; lpCompletionRoutine: LPWSAOVERLAPPED_COMPLETION_ROUTINE) - : integer; stdcall; - RealWSASendTo: function(s: TSocket; lpBuffers: LPWSABUF; dwBufferCount: DWORD; lpNumberOfBytesSent: LPDWORD; - dwFlags: DWORD; const lpTo: TSockAddr; iTolen: integer; lpOverlapped: LPWSAOVERLAPPED; - lpCompletionRoutine: LPWSAOVERLAPPED_COMPLETION_ROUTINE): integer; stdcall; - RealSend: function(s: TSocket; const buf; len, flags: integer): integer; stdcall; - RealRecv: function(s: TSocket; var buf; len, flags: integer): integer; stdcall; - - currentProcessDir: string; - sockManager: TSocketManager; - options: TDroverOptions; - proxyValue: TProxyValue; - -procedure TProxyValue.ParseFromString(url: string); -var - match: TMatch; -begin - isSpecified := false; - prot := ''; - login := ''; - password := ''; - host := ''; - port := 0; - isHttp := false; - isSocks5 := false; - isAuth := false; - - match := TRegEx.match(Trim(url), '\A(?:([a-z\d]+)://)?(?:(.+):(.+)@)?(.+):(\d+)\z', [roIgnoreCase]); - if not match.Success then - exit; - - isSpecified := true; - - prot := LowerCase(Trim(match.Groups[1].Value)); - if (prot = '') or (prot = 'https') then - prot := 'http'; - - login := Trim(match.Groups[2].Value); - password := Trim(match.Groups[3].Value); - - host := Trim(match.Groups[4].Value); - port := StrToIntDef(match.Groups[5].Value, 0); - - isHttp := (prot = 'http'); - isSocks5 := (prot = 'socks5'); - isAuth := (login <> '') and (password <> ''); -end; - -function TProxyValue.FormatToHttpEnv: string; -begin - if not isSpecified then - exit(''); - - result := 'http://'; - if isAuth then - result := result + login + ':' + password + '@'; - result := result + host + ':' + IntToStr(port); -end; - -function TProxyValue.FormatToChromeProxy: string; -begin - if isSpecified then - result := Format('%s://%s:%d', [prot, host, port]) - else - result := ''; -end; - -function MyGetFileVersionInfoA(lptstrFilename: LPSTR; dwHandle, dwLen: DWORD; lpData: Pointer): bool; stdcall; -begin - result := RealGetFileVersionInfoA(lptstrFilename, dwHandle, dwLen, lpData); -end; - -function MyGetFileVersionInfoW(lptstrFilename: LPWSTR; dwHandle, dwLen: DWORD; lpData: Pointer): bool; stdcall; -begin - result := RealGetFileVersionInfoW(lptstrFilename, dwHandle, dwLen, lpData); -end; - -function MyGetFileVersionInfoSizeA(lptstrFilename: LPSTR; var lpdwHandle: DWORD): DWORD; stdcall; -begin - result := RealGetFileVersionInfoSizeA(lptstrFilename, lpdwHandle); -end; - -function MyGetFileVersionInfoSizeW(lptstrFilename: LPWSTR; var lpdwHandle: DWORD): DWORD; stdcall; -begin - result := RealGetFileVersionInfoSizeW(lptstrFilename, lpdwHandle); -end; - -function MyVerFindFileA(uFlags: DWORD; szFileName, szWinDir, szAppDir, szCurDir: LPSTR; var lpuCurDirLen: UINT; - szDestDir: LPSTR; var lpuDestDirLen: UINT): DWORD; stdcall; -begin - result := RealVerFindFileA(uFlags, szFileName, szWinDir, szAppDir, szCurDir, lpuCurDirLen, szDestDir, lpuDestDirLen); -end; - -function MyVerFindFileW(uFlags: DWORD; szFileName, szWinDir, szAppDir, szCurDir: LPWSTR; var lpuCurDirLen: UINT; - szDestDir: LPWSTR; var lpuDestDirLen: UINT): DWORD; stdcall; -begin - result := RealVerFindFileW(uFlags, szFileName, szWinDir, szAppDir, szCurDir, lpuCurDirLen, szDestDir, lpuDestDirLen); -end; - -function MyVerInstallFileA(uFlags: DWORD; szSrcFileName, szDestFileName, szSrcDir, szDestDir, szCurDir, - szTmpFile: LPSTR; var lpuTmpFileLen: UINT): DWORD; stdcall; -begin - result := RealVerInstallFileA(uFlags, szSrcFileName, szDestFileName, szSrcDir, szDestDir, szCurDir, szTmpFile, - lpuTmpFileLen); -end; - -function MyVerInstallFileW(uFlags: DWORD; szSrcFileName, szDestFileName, szSrcDir, szDestDir, szCurDir, - szTmpFile: LPWSTR; var lpuTmpFileLen: UINT): DWORD; stdcall; -begin - result := RealVerInstallFileW(uFlags, szSrcFileName, szDestFileName, szSrcDir, szDestDir, szCurDir, szTmpFile, - lpuTmpFileLen); -end; - -function MyVerLanguageNameA(wLang: DWORD; szLang: LPSTR; nSize: DWORD): DWORD; stdcall; -begin - result := RealVerLanguageNameA(wLang, szLang, nSize); -end; - -function MyVerLanguageNameW(wLang: DWORD; szLang: LPWSTR; nSize: DWORD): DWORD; stdcall; -begin - result := RealVerLanguageNameW(wLang, szLang, nSize); -end; - -function MyVerQueryValueA(pBlock: Pointer; lpSubBlock: LPSTR; var lplpBuffer: Pointer; var puLen: UINT): bool; stdcall; -begin - result := RealVerQueryValueA(pBlock, lpSubBlock, lplpBuffer, puLen); -end; - -function MyVerQueryValueW(pBlock: Pointer; lpSubBlock: LPWSTR; var lplpBuffer: Pointer; var puLen: UINT): bool; stdcall; -begin - result := RealVerQueryValueW(pBlock, lpSubBlock, lplpBuffer, puLen); -end; - -function MyGetEnvironmentVariableW(lpName: LPCWSTR; lpBuffer: LPWSTR; nSize: DWORD): DWORD; stdcall; -var - s: string; - newValue: string; -begin - if proxyValue.isSpecified then - begin - s := lpName; - if (Pos('http_proxy', s) > 0) or (Pos('HTTP_PROXY', s) > 0) or (Pos('https_proxy', s) > 0) or - (Pos('HTTPS_PROXY', s) > 0) then - begin - newValue := proxyValue.FormatToHttpEnv; - StringToWideChar(newValue, lpBuffer, nSize); - result := Length(newValue); - exit; - end; - end; - - result := RealGetEnvironmentVariableW(lpName, lpBuffer, nSize); -end; - -procedure CopyFilesToNewVersionFolderIfNeeded(lpApplicationName: LPCWSTR); -var - launchingDir: string; - srcOptionsPath, srcDllPath, dstOptionsPath, dstDllPath: string; -begin - if lpApplicationName = nil then - exit; - - if not SameText(ExtractFileName(lpApplicationName), 'Discord.exe') then - exit; - - if not SameText(ExtractFileName(ParamStr(0)), 'Discord.exe') then - exit; - - launchingDir := IncludeTrailingPathDelimiter(ExtractFilePath(lpApplicationName)); - - srcOptionsPath := currentProcessDir + OPTIONS_FILENAME; - srcDllPath := currentProcessDir + DLL_FILENAME; - dstOptionsPath := launchingDir + OPTIONS_FILENAME; - dstDllPath := launchingDir + DLL_FILENAME; - - if FileExists(launchingDir + 'Discord.exe') and FileExists(srcOptionsPath) and FileExists(srcDllPath) and - not FileExists(dstOptionsPath) and not FileExists(dstDllPath) then - begin - CopyFile(PChar(srcOptionsPath), PChar(dstOptionsPath), true); - CopyFile(PChar(srcDllPath), PChar(dstDllPath), true); - end; -end; - -function MyCreateProcessW(lpApplicationName: LPCWSTR; lpCommandLine: LPWSTR; - lpProcessAttributes, lpThreadAttributes: PSecurityAttributes; bInheritHandles: bool; dwCreationFlags: DWORD; - lpEnvironment: Pointer; lpCurrentDirectory: LPCWSTR; const lpStartupInfo: TStartupInfoW; - var lpProcessInformation: TProcessInformation): bool; stdcall; -begin - CopyFilesToNewVersionFolderIfNeeded(lpApplicationName); - - result := RealCreateProcessW(lpApplicationName, lpCommandLine, lpProcessAttributes, lpThreadAttributes, - bInheritHandles, dwCreationFlags, lpEnvironment, lpCurrentDirectory, lpStartupInfo, lpProcessInformation); -end; - -function MyGetCommandLineW: LPWSTR; stdcall; -var - s: string; -begin - s := RealGetCommandLineW; - if proxyValue.isSpecified then - begin - if SameText(ExtractFileName(ParamStr(0)), 'Discord.exe') then - s := s + ' --proxy-server=' + proxyValue.FormatToChromeProxy; - end; - result := PChar(s); -end; - -function MySocket(af, type_, protocol: integer): TSocket; stdcall; -begin - result := RealSocket(af, type_, protocol); - sockManager.Add(result, type_, protocol); -end; - -function MyWSASocket(af, type_, protocol: integer; lpProtocolInfo: LPWSAPROTOCOL_INFO; g: GROUP; dwFlags: DWORD) - : TSocket; stdcall; -begin - result := RealWSASocket(af, type_, protocol, lpProtocolInfo, g, dwFlags); - sockManager.Add(result, type_, protocol); -end; - -function AddHttpProxyAuthorizationHeader(socketManagerItem: TSocketManagerItem; lpBuffers: LPWSABUF; - dwBufferCount: DWORD; lpNumberOfBytesSent: PDWORD; dwFlags: DWORD; lpOverlapped: LPWSAOVERLAPPED; - lpCompletionRoutine: LPWSAOVERLAPPED_COMPLETION_ROUTINE): boolean; -begin - result := false; - - if (not proxyValue.isSpecified) or (not proxyValue.isHttp) or (not proxyValue.isAuth) or (not socketManagerItem.isTcp) - then - exit; - - // TODO - - exit(false); -end; - -function MyWSASend(sock: TSocket; lpBuffers: LPWSABUF; dwBufferCount: DWORD; lpNumberOfBytesSent: PDWORD; - dwFlags: DWORD; lpOverlapped: LPWSAOVERLAPPED; lpCompletionRoutine: LPWSAOVERLAPPED_COMPLETION_ROUTINE) - : integer; stdcall; -var - sockManagerItem: TSocketManagerItem; -begin - if sockManager.IsFirstSend(sock, sockManagerItem) then - begin - AddHttpProxyAuthorizationHeader(sockManagerItem, lpBuffers, dwBufferCount, lpNumberOfBytesSent, dwFlags, - lpOverlapped, lpCompletionRoutine); - end; - - result := RealWSASend(sock, lpBuffers, dwBufferCount, lpNumberOfBytesSent, dwFlags, lpOverlapped, - lpCompletionRoutine); -end; - -function MyWSASendTo(sock: TSocket; lpBuffers: LPWSABUF; dwBufferCount: DWORD; lpNumberOfBytesSent: LPDWORD; - dwFlags: DWORD; const lpTo: TSockAddr; iTolen: integer; lpOverlapped: LPWSAOVERLAPPED; - lpCompletionRoutine: LPWSAOVERLAPPED_COMPLETION_ROUTINE): integer; stdcall; -var - zeroByte: Byte; - sockManagerItem: TSocketManagerItem; -begin - if sockManager.IsFirstSend(sock, sockManagerItem) then - begin - if sockManagerItem.isUdp and (lpBuffers.len = 74) then - begin - zeroByte := 0; - sendto(sock, Pointer(@zeroByte)^, 1, 0, @lpTo, iTolen); - end; - end; - - result := RealWSASendTo(sock, lpBuffers, dwBufferCount, lpNumberOfBytesSent, dwFlags, lpTo, iTolen, lpOverlapped, - lpCompletionRoutine); -end; - -function ConvertHttpToSocks5(socketManagerItem: TSocketManagerItem; const buf; len, flags: integer): boolean; -var - s, targetHost: RawByteString; - targetPort: word; - fdSet: TFDSet; - tv: TTimeVal; - i: integer; - match: TMatch; - sock: TSocket; -begin - result := false; - - if (not proxyValue.isSpecified) or (not proxyValue.isSocks5) or (not socketManagerItem.isTcp) then - exit; - - i := 8; - if len < i then - exit; - SetLength(s, i); - Move(buf, s[1], i); - if s <> 'CONNECT ' then - exit; - - SetLength(s, len); - Move(buf, s[1], len); - match := TRegEx.match(string(s), '\ACONNECT ([a-z\d.-]+):(\d+)', [roIgnoreCase]); - if not match.Success then - exit; - targetHost := RawByteString(match.Groups[1].Value); - targetPort := StrToIntDef(match.Groups[2].Value, 0); - - sock := socketManagerItem.sock; - - s := #$05#$01#$00; - i := Length(s); - if RealSend(sock, s[1], i, flags) <> i then - exit; - - FD_ZERO(fdSet); - _FD_SET(sock, fdSet); - tv.tv_sec := 10; - tv.tv_usec := 0; - - if select(0, @fdSet, nil, nil, @tv) < 1 then - exit; - if not FD_ISSET(sock, fdSet) then - exit; - - i := 2; - SetLength(s, i); - if RealRecv(sock, s[1], i, 0) <> i then - exit; - - if s <> #$05#$00 then - exit; - - s := #$05#$01#$00#$03 + RawByteString(AnsiChar(Length(targetHost))) + targetHost + - RawByteString(AnsiChar(Hi(targetPort))) + RawByteString(AnsiChar(Lo(targetPort))); - i := Length(s); - if RealSend(sock, s[1], i, flags) <> i then - exit; - - sockManager.SetFakeHttpProxyFlag(sock); - - result := true; -end; - -function MySend(sock: TSocket; const buf; len, flags: integer): integer; stdcall; -var - sockManagerItem: TSocketManagerItem; -begin - if sockManager.IsFirstSend(sock, sockManagerItem) then - begin - if ConvertHttpToSocks5(sockManagerItem, buf, len, flags) then - exit(len); - end; - - result := RealSend(sock, buf, len, flags); -end; - -function MyRecv(sock: TSocket; var buf; len, flags: integer): integer; stdcall; -var - s: RawByteString; - i: integer; -begin - result := RealRecv(sock, buf, len, flags); - - if (result > 0) and sockManager.ResetFakeHttpProxyFlag(sock) then - begin - if result >= 10 then - begin - // Potential issue: real server data may mix with the SOCKS5 response - SetLength(s, result); - Move(buf, s[1], result); - if Copy(s, 1, 3) = #$05#$00#$00 then - begin - s := 'HTTP/1.1 200 Connection Established' + #13#10 + #13#10; - i := Length(s); - if i <= len then - begin - Move(s[1], buf, i); - exit(i); - end; - end; - end; - end; -end; - -function GetSystemFolder: string; -var - s: string; -begin - SetLength(s, MAX_PATH); - GetSystemDirectory(PChar(s), MAX_PATH); - result := IncludeTrailingPathDelimiter(PChar(s)); -end; - -procedure LoadOriginalVersionDll; -var - hOriginal: THandle; -begin - hOriginal := LoadLibrary(PChar(GetSystemFolder + 'version.dll')); - if hOriginal = 0 then - raise Exception.Create('Error.'); - - @RealGetFileVersionInfoA := GetProcAddress(hOriginal, 'GetFileVersionInfoA'); - @RealGetFileVersionInfoW := GetProcAddress(hOriginal, 'GetFileVersionInfoW'); - @RealGetFileVersionInfoSizeA := GetProcAddress(hOriginal, 'GetFileVersionInfoSizeA'); - @RealGetFileVersionInfoSizeW := GetProcAddress(hOriginal, 'GetFileVersionInfoSizeW'); - @RealVerFindFileA := GetProcAddress(hOriginal, 'VerFindFileA'); - @RealVerFindFileW := GetProcAddress(hOriginal, 'VerFindFileW'); - @RealVerInstallFileA := GetProcAddress(hOriginal, 'VerInstallFileA'); - @RealVerInstallFileW := GetProcAddress(hOriginal, 'VerInstallFileW'); - @RealVerLanguageNameA := GetProcAddress(hOriginal, 'VerLanguageNameA'); - @RealVerLanguageNameW := GetProcAddress(hOriginal, 'VerLanguageNameW'); - @RealVerQueryValueA := GetProcAddress(hOriginal, 'VerQueryValueA'); - @RealVerQueryValueW := GetProcAddress(hOriginal, 'VerQueryValueW'); -end; - -function IsNekoBoxExists: bool; -var - hSnapshot: THandle; - pe32: TProcessEntry32; - processName: string; -begin - result := false; - hSnapshot := CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); - if hSnapshot = INVALID_HANDLE_VALUE then - exit; - try - pe32.dwSize := SizeOf(TProcessEntry32); - - if Process32First(hSnapshot, pe32) then - begin - repeat - processName := LowerCase(StrPas(pe32.szExeFile)); - if (Pos('nekobox', processName) > 0) or (Pos('nekoray', processName) > 0) then - begin - result := true; - exit; - end; - - until not Process32Next(hSnapshot, pe32); - end; - finally - CloseHandle(hSnapshot); - end; -end; - -function LoadOptions: TDroverOptions; -var - f: TIniFile; - filename: string; -begin - try - filename := currentProcessDir + OPTIONS_FILENAME; - - f := TIniFile.Create(filename); - try - with f do - begin - result.proxy := ReadString('drover', 'proxy', ''); - result.useNekoboxProxy := ReadBool('drover', 'use-nekobox-proxy', false); - result.nekoboxProxy := ReadString('drover', 'nekobox-proxy', '127.0.0.1:2080'); - end; - finally - f.Free; - end; - except - end; -end; - -exports - MyGetFileVersionInfoA name 'GetFileVersionInfoA', - MyGetFileVersionInfoW name 'GetFileVersionInfoW', - MyGetFileVersionInfoSizeA name 'GetFileVersionInfoSizeA', - MyGetFileVersionInfoSizeW name 'GetFileVersionInfoSizeW', - MyVerFindFileA name 'VerFindFileA', - MyVerFindFileW name 'VerFindFileW', - MyVerInstallFileA name 'VerInstallFileA', - MyVerInstallFileW name 'VerInstallFileW', - MyVerLanguageNameA name 'VerLanguageNameA', - MyVerLanguageNameW name 'VerLanguageNameW', - MyVerQueryValueA name 'VerQueryValueA', - MyVerQueryValueW name 'VerQueryValueW'; - -begin - currentProcessDir := IncludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0))); - sockManager := TSocketManager.Create; - - options := LoadOptions; - - if options.useNekoboxProxy and IsNekoBoxExists then - proxyValue.ParseFromString(options.nekoboxProxy) - else - proxyValue.ParseFromString(options.proxy); - - LoadOriginalVersionDll; - - RealGetEnvironmentVariableW := InterceptCreate(@GetEnvironmentVariableW, @MyGetEnvironmentVariableW, nil); - RealCreateProcessW := InterceptCreate(@CreateProcessW, @MyCreateProcessW, nil); - RealGetCommandLineW := InterceptCreate(@GetCommandLineW, @MyGetCommandLineW, nil); - - RealSocket := InterceptCreate(@socket, @MySocket, nil); - RealWSASocket := InterceptCreate(@WSASocket, @MyWSASocket, nil); - RealWSASend := InterceptCreate(@WSASend, @MyWSASend, nil); - RealWSASendTo := InterceptCreate(@WSASendTo, @MyWSASendTo, nil); - RealSend := InterceptCreate(@send, @MySend, nil); - RealRecv := InterceptCreate(@recv, @MyRecv, nil); - -end. +library drover; + +uses + System.SysUtils, + Winapi.Windows, + DDetours, + PsAPI, + TlHelp32, + WinSock, + WinSock2, + IniFiles, + System.RegularExpressions, + SocketManager, + Options; + +var + RealGetFileVersionInfoA: function(lptstrFilename: LPSTR; dwHandle, dwLen: DWORD; lpData: Pointer): bool; stdcall; + RealGetFileVersionInfoW: function(lptstrFilename: LPWSTR; dwHandle, dwLen: DWORD; lpData: Pointer): bool; stdcall; + RealGetFileVersionInfoSizeA: function(lptstrFilename: LPSTR; var lpdwHandle: DWORD): DWORD; stdcall; + RealGetFileVersionInfoSizeW: function(lptstrFilename: LPWSTR; var lpdwHandle: DWORD): DWORD; stdcall; + RealVerFindFileA: function(uFlags: DWORD; szFileName, szWinDir, szAppDir, szCurDir: LPSTR; var lpuCurDirLen: UINT; + szDestDir: LPSTR; var lpuDestDirLen: UINT): DWORD; stdcall; + RealVerFindFileW: function(uFlags: DWORD; szFileName, szWinDir, szAppDir, szCurDir: LPWSTR; var lpuCurDirLen: UINT; + szDestDir: LPWSTR; var lpuDestDirLen: UINT): DWORD; stdcall; + RealVerInstallFileA: function(uFlags: DWORD; szSrcFileName, szDestFileName, szSrcDir, szDestDir, szCurDir, + szTmpFile: LPSTR; var lpuTmpFileLen: UINT): DWORD; stdcall; + RealVerInstallFileW: function(uFlags: DWORD; szSrcFileName, szDestFileName, szSrcDir, szDestDir, szCurDir, + szTmpFile: LPWSTR; var lpuTmpFileLen: UINT): DWORD; stdcall; + RealVerLanguageNameA: function(wLang: DWORD; szLang: LPSTR; nSize: DWORD): DWORD; stdcall; + RealVerLanguageNameW: function(wLang: DWORD; szLang: LPWSTR; nSize: DWORD): DWORD; stdcall; + RealVerQueryValueA: function(pBlock: Pointer; lpSubBlock: LPSTR; var lplpBuffer: Pointer; var puLen: UINT) + : bool; stdcall; + RealVerQueryValueW: function(pBlock: Pointer; lpSubBlock: LPWSTR; var lplpBuffer: Pointer; var puLen: UINT) + : bool; stdcall; + + RealGetEnvironmentVariableW: function(lpName: LPCWSTR; lpBuffer: LPWSTR; nSize: DWORD): DWORD; stdcall; + RealCreateProcessW: function(lpApplicationName: LPCWSTR; lpCommandLine: LPWSTR; + lpProcessAttributes, lpThreadAttributes: PSecurityAttributes; bInheritHandles: bool; dwCreationFlags: DWORD; + lpEnvironment: Pointer; lpCurrentDirectory: LPCWSTR; const lpStartupInfo: TStartupInfoW; + var lpProcessInformation: TProcessInformation): bool; stdcall; + RealGetCommandLineW: function: LPWSTR; stdcall; + + RealSocket: function(af, type_, protocol: integer): TSocket; stdcall; + RealWSASocket: function(af, type_, protocol: integer; lpProtocolInfo: LPWSAPROTOCOL_INFO; g: GROUP; dwFlags: DWORD) + : TSocket; stdcall; + RealWSASend: function(s: TSocket; lpBuffers: LPWSABUF; dwBufferCount: DWORD; lpNumberOfBytesSent: PDWORD; + dwFlags: DWORD; lpOverlapped: LPWSAOVERLAPPED; lpCompletionRoutine: LPWSAOVERLAPPED_COMPLETION_ROUTINE) + : integer; stdcall; + RealWSASendTo: function(s: TSocket; lpBuffers: LPWSABUF; dwBufferCount: DWORD; lpNumberOfBytesSent: LPDWORD; + dwFlags: DWORD; const lpTo: TSockAddr; iTolen: integer; lpOverlapped: LPWSAOVERLAPPED; + lpCompletionRoutine: LPWSAOVERLAPPED_COMPLETION_ROUTINE): integer; stdcall; + RealSend: function(s: TSocket; const buf; len, flags: integer): integer; stdcall; + RealRecv: function(s: TSocket; var buf; len, flags: integer): integer; stdcall; + + currentProcessDir: string; + sockManager: TSocketManager; + droverOptions: TDroverOptions; + proxyValue: TProxyValue; + +function MyGetFileVersionInfoA(lptstrFilename: LPSTR; dwHandle, dwLen: DWORD; lpData: Pointer): bool; stdcall; +begin + result := RealGetFileVersionInfoA(lptstrFilename, dwHandle, dwLen, lpData); +end; + +function MyGetFileVersionInfoW(lptstrFilename: LPWSTR; dwHandle, dwLen: DWORD; lpData: Pointer): bool; stdcall; +begin + result := RealGetFileVersionInfoW(lptstrFilename, dwHandle, dwLen, lpData); +end; + +function MyGetFileVersionInfoSizeA(lptstrFilename: LPSTR; var lpdwHandle: DWORD): DWORD; stdcall; +begin + result := RealGetFileVersionInfoSizeA(lptstrFilename, lpdwHandle); +end; + +function MyGetFileVersionInfoSizeW(lptstrFilename: LPWSTR; var lpdwHandle: DWORD): DWORD; stdcall; +begin + result := RealGetFileVersionInfoSizeW(lptstrFilename, lpdwHandle); +end; + +function MyVerFindFileA(uFlags: DWORD; szFileName, szWinDir, szAppDir, szCurDir: LPSTR; var lpuCurDirLen: UINT; + szDestDir: LPSTR; var lpuDestDirLen: UINT): DWORD; stdcall; +begin + result := RealVerFindFileA(uFlags, szFileName, szWinDir, szAppDir, szCurDir, lpuCurDirLen, szDestDir, lpuDestDirLen); +end; + +function MyVerFindFileW(uFlags: DWORD; szFileName, szWinDir, szAppDir, szCurDir: LPWSTR; var lpuCurDirLen: UINT; + szDestDir: LPWSTR; var lpuDestDirLen: UINT): DWORD; stdcall; +begin + result := RealVerFindFileW(uFlags, szFileName, szWinDir, szAppDir, szCurDir, lpuCurDirLen, szDestDir, lpuDestDirLen); +end; + +function MyVerInstallFileA(uFlags: DWORD; szSrcFileName, szDestFileName, szSrcDir, szDestDir, szCurDir, + szTmpFile: LPSTR; var lpuTmpFileLen: UINT): DWORD; stdcall; +begin + result := RealVerInstallFileA(uFlags, szSrcFileName, szDestFileName, szSrcDir, szDestDir, szCurDir, szTmpFile, + lpuTmpFileLen); +end; + +function MyVerInstallFileW(uFlags: DWORD; szSrcFileName, szDestFileName, szSrcDir, szDestDir, szCurDir, + szTmpFile: LPWSTR; var lpuTmpFileLen: UINT): DWORD; stdcall; +begin + result := RealVerInstallFileW(uFlags, szSrcFileName, szDestFileName, szSrcDir, szDestDir, szCurDir, szTmpFile, + lpuTmpFileLen); +end; + +function MyVerLanguageNameA(wLang: DWORD; szLang: LPSTR; nSize: DWORD): DWORD; stdcall; +begin + result := RealVerLanguageNameA(wLang, szLang, nSize); +end; + +function MyVerLanguageNameW(wLang: DWORD; szLang: LPWSTR; nSize: DWORD): DWORD; stdcall; +begin + result := RealVerLanguageNameW(wLang, szLang, nSize); +end; + +function MyVerQueryValueA(pBlock: Pointer; lpSubBlock: LPSTR; var lplpBuffer: Pointer; var puLen: UINT): bool; stdcall; +begin + result := RealVerQueryValueA(pBlock, lpSubBlock, lplpBuffer, puLen); +end; + +function MyVerQueryValueW(pBlock: Pointer; lpSubBlock: LPWSTR; var lplpBuffer: Pointer; var puLen: UINT): bool; stdcall; +begin + result := RealVerQueryValueW(pBlock, lpSubBlock, lplpBuffer, puLen); +end; + +function MyGetEnvironmentVariableW(lpName: LPCWSTR; lpBuffer: LPWSTR; nSize: DWORD): DWORD; stdcall; +var + s: string; + newValue: string; +begin + if proxyValue.isSpecified then + begin + s := lpName; + if (Pos('http_proxy', s) > 0) or (Pos('HTTP_PROXY', s) > 0) or (Pos('https_proxy', s) > 0) or + (Pos('HTTPS_PROXY', s) > 0) then + begin + newValue := proxyValue.FormatToHttpEnv; + StringToWideChar(newValue, lpBuffer, nSize); + result := Length(newValue); + exit; + end; + end; + + result := RealGetEnvironmentVariableW(lpName, lpBuffer, nSize); +end; + +procedure CopyFilesToNewVersionFolderIfNeeded(lpApplicationName: LPCWSTR); +var + launchingDir: string; + srcOptionsPath, srcDllPath, dstOptionsPath, dstDllPath: string; +begin + if lpApplicationName = nil then + exit; + + if not SameText(ExtractFileName(lpApplicationName), 'Discord.exe') then + exit; + + if not SameText(ExtractFileName(ParamStr(0)), 'Discord.exe') then + exit; + + launchingDir := IncludeTrailingPathDelimiter(ExtractFilePath(lpApplicationName)); + + srcOptionsPath := currentProcessDir + OPTIONS_FILENAME; + srcDllPath := currentProcessDir + DLL_FILENAME; + dstOptionsPath := launchingDir + OPTIONS_FILENAME; + dstDllPath := launchingDir + DLL_FILENAME; + + if FileExists(launchingDir + 'Discord.exe') and FileExists(srcOptionsPath) and FileExists(srcDllPath) and + not FileExists(dstOptionsPath) and not FileExists(dstDllPath) then + begin + CopyFile(PChar(srcOptionsPath), PChar(dstOptionsPath), true); + CopyFile(PChar(srcDllPath), PChar(dstDllPath), true); + end; +end; + +function MyCreateProcessW(lpApplicationName: LPCWSTR; lpCommandLine: LPWSTR; + lpProcessAttributes, lpThreadAttributes: PSecurityAttributes; bInheritHandles: bool; dwCreationFlags: DWORD; + lpEnvironment: Pointer; lpCurrentDirectory: LPCWSTR; const lpStartupInfo: TStartupInfoW; + var lpProcessInformation: TProcessInformation): bool; stdcall; +begin + CopyFilesToNewVersionFolderIfNeeded(lpApplicationName); + + result := RealCreateProcessW(lpApplicationName, lpCommandLine, lpProcessAttributes, lpThreadAttributes, + bInheritHandles, dwCreationFlags, lpEnvironment, lpCurrentDirectory, lpStartupInfo, lpProcessInformation); +end; + +function MyGetCommandLineW: LPWSTR; stdcall; +var + s: string; +begin + s := RealGetCommandLineW; + if proxyValue.isSpecified then + begin + if SameText(ExtractFileName(ParamStr(0)), 'Discord.exe') then + s := s + ' --proxy-server=' + proxyValue.FormatToChromeProxy; + end; + result := PChar(s); +end; + +function MySocket(af, type_, protocol: integer): TSocket; stdcall; +begin + result := RealSocket(af, type_, protocol); + sockManager.Add(result, type_, protocol); +end; + +function MyWSASocket(af, type_, protocol: integer; lpProtocolInfo: LPWSAPROTOCOL_INFO; g: GROUP; dwFlags: DWORD) + : TSocket; stdcall; +begin + result := RealWSASocket(af, type_, protocol, lpProtocolInfo, g, dwFlags); + sockManager.Add(result, type_, protocol); +end; + +function AddHttpProxyAuthorizationHeader(socketManagerItem: TSocketManagerItem; lpBuffers: LPWSABUF; + dwBufferCount: DWORD; lpNumberOfBytesSent: PDWORD; dwFlags: DWORD; lpOverlapped: LPWSAOVERLAPPED; + lpCompletionRoutine: LPWSAOVERLAPPED_COMPLETION_ROUTINE): boolean; +begin + result := false; + + if (not proxyValue.isSpecified) or (not proxyValue.isHttp) or (not proxyValue.isAuth) or (not socketManagerItem.isTcp) + then + exit; + + // TODO + + exit(false); +end; + +function MyWSASend(sock: TSocket; lpBuffers: LPWSABUF; dwBufferCount: DWORD; lpNumberOfBytesSent: PDWORD; + dwFlags: DWORD; lpOverlapped: LPWSAOVERLAPPED; lpCompletionRoutine: LPWSAOVERLAPPED_COMPLETION_ROUTINE) + : integer; stdcall; +var + sockManagerItem: TSocketManagerItem; +begin + if sockManager.IsFirstSend(sock, sockManagerItem) then + begin + AddHttpProxyAuthorizationHeader(sockManagerItem, lpBuffers, dwBufferCount, lpNumberOfBytesSent, dwFlags, + lpOverlapped, lpCompletionRoutine); + end; + + result := RealWSASend(sock, lpBuffers, dwBufferCount, lpNumberOfBytesSent, dwFlags, lpOverlapped, + lpCompletionRoutine); +end; + +function MyWSASendTo(sock: TSocket; lpBuffers: LPWSABUF; dwBufferCount: DWORD; lpNumberOfBytesSent: LPDWORD; + dwFlags: DWORD; const lpTo: TSockAddr; iTolen: integer; lpOverlapped: LPWSAOVERLAPPED; + lpCompletionRoutine: LPWSAOVERLAPPED_COMPLETION_ROUTINE): integer; stdcall; +var + payload: byte; + sockManagerItem: TSocketManagerItem; +begin + if sockManager.IsFirstSend(sock, sockManagerItem) then + begin + if sockManagerItem.isUdp and (lpBuffers.len = 74) then + begin + payload := 0; + sendto(sock, Pointer(@payload)^, 1, 0, @lpTo, iTolen); + payload := 1; + sendto(sock, Pointer(@payload)^, 1, 0, @lpTo, iTolen); + Sleep(50); + end; + end; + + result := RealWSASendTo(sock, lpBuffers, dwBufferCount, lpNumberOfBytesSent, dwFlags, lpTo, iTolen, lpOverlapped, + lpCompletionRoutine); +end; + +function ConvertHttpToSocks5(socketManagerItem: TSocketManagerItem; const buf; len, flags: integer): boolean; +var + s, targetHost: RawByteString; + targetPort: word; + fdSet: TFDSet; + tv: TTimeVal; + i: integer; + match: TMatch; + sock: TSocket; +begin + result := false; + + if (not proxyValue.isSpecified) or (not proxyValue.isSocks5) or (not socketManagerItem.isTcp) then + exit; + + i := 8; + if len < i then + exit; + SetLength(s, i); + Move(buf, s[1], i); + if s <> 'CONNECT ' then + exit; + + SetLength(s, len); + Move(buf, s[1], len); + match := TRegEx.match(string(s), '\ACONNECT ([a-z\d.-]+):(\d+)', [roIgnoreCase]); + if not match.Success then + exit; + targetHost := RawByteString(match.Groups[1].Value); + targetPort := StrToIntDef(match.Groups[2].Value, 0); + + sock := socketManagerItem.sock; + + s := #$05#$01#$00; + i := Length(s); + if RealSend(sock, s[1], i, flags) <> i then + exit; + + FD_ZERO(fdSet); + _FD_SET(sock, fdSet); + tv.tv_sec := 10; + tv.tv_usec := 0; + + if select(0, @fdSet, nil, nil, @tv) < 1 then + exit; + if not FD_ISSET(sock, fdSet) then + exit; + + i := 2; + SetLength(s, i); + if RealRecv(sock, s[1], i, 0) <> i then + exit; + + if s <> #$05#$00 then + exit; + + s := #$05#$01#$00#$03 + RawByteString(AnsiChar(Length(targetHost))) + targetHost + + RawByteString(AnsiChar(Hi(targetPort))) + RawByteString(AnsiChar(Lo(targetPort))); + i := Length(s); + if RealSend(sock, s[1], i, flags) <> i then + exit; + + sockManager.SetFakeHttpProxyFlag(sock); + + result := true; +end; + +function MySend(sock: TSocket; const buf; len, flags: integer): integer; stdcall; +var + sockManagerItem: TSocketManagerItem; +begin + if sockManager.IsFirstSend(sock, sockManagerItem) then + begin + if ConvertHttpToSocks5(sockManagerItem, buf, len, flags) then + exit(len); + end; + + result := RealSend(sock, buf, len, flags); +end; + +function MyRecv(sock: TSocket; var buf; len, flags: integer): integer; stdcall; +var + s: RawByteString; + i: integer; +begin + result := RealRecv(sock, buf, len, flags); + + if (result > 0) and sockManager.ResetFakeHttpProxyFlag(sock) then + begin + if result >= 10 then + begin + // Potential issue: real server data may mix with the SOCKS5 response + SetLength(s, result); + Move(buf, s[1], result); + if Copy(s, 1, 3) = #$05#$00#$00 then + begin + s := 'HTTP/1.1 200 Connection Established' + #13#10 + #13#10; + i := Length(s); + if i <= len then + begin + Move(s[1], buf, i); + exit(i); + end; + end; + end; + end; +end; + +function GetSystemFolder: string; +var + s: string; +begin + SetLength(s, MAX_PATH); + GetSystemDirectory(PChar(s), MAX_PATH); + result := IncludeTrailingPathDelimiter(PChar(s)); +end; + +procedure LoadOriginalVersionDll; +var + hOriginal: THandle; +begin + hOriginal := LoadLibrary(PChar(GetSystemFolder + 'version.dll')); + if hOriginal = 0 then + raise Exception.Create('Error.'); + + @RealGetFileVersionInfoA := GetProcAddress(hOriginal, 'GetFileVersionInfoA'); + @RealGetFileVersionInfoW := GetProcAddress(hOriginal, 'GetFileVersionInfoW'); + @RealGetFileVersionInfoSizeA := GetProcAddress(hOriginal, 'GetFileVersionInfoSizeA'); + @RealGetFileVersionInfoSizeW := GetProcAddress(hOriginal, 'GetFileVersionInfoSizeW'); + @RealVerFindFileA := GetProcAddress(hOriginal, 'VerFindFileA'); + @RealVerFindFileW := GetProcAddress(hOriginal, 'VerFindFileW'); + @RealVerInstallFileA := GetProcAddress(hOriginal, 'VerInstallFileA'); + @RealVerInstallFileW := GetProcAddress(hOriginal, 'VerInstallFileW'); + @RealVerLanguageNameA := GetProcAddress(hOriginal, 'VerLanguageNameA'); + @RealVerLanguageNameW := GetProcAddress(hOriginal, 'VerLanguageNameW'); + @RealVerQueryValueA := GetProcAddress(hOriginal, 'VerQueryValueA'); + @RealVerQueryValueW := GetProcAddress(hOriginal, 'VerQueryValueW'); +end; + +function IsNekoBoxExists: bool; +var + hSnapshot: THandle; + pe32: TProcessEntry32; + processName: string; +begin + result := false; + hSnapshot := CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if hSnapshot = INVALID_HANDLE_VALUE then + exit; + try + pe32.dwSize := SizeOf(TProcessEntry32); + + if Process32First(hSnapshot, pe32) then + begin + repeat + processName := LowerCase(StrPas(pe32.szExeFile)); + if (Pos('nekobox', processName) > 0) or (Pos('nekoray', processName) > 0) then + begin + result := true; + exit; + end; + + until not Process32Next(hSnapshot, pe32); + end; + finally + CloseHandle(hSnapshot); + end; +end; + +exports + MyGetFileVersionInfoA name 'GetFileVersionInfoA', + MyGetFileVersionInfoW name 'GetFileVersionInfoW', + MyGetFileVersionInfoSizeA name 'GetFileVersionInfoSizeA', + MyGetFileVersionInfoSizeW name 'GetFileVersionInfoSizeW', + MyVerFindFileA name 'VerFindFileA', + MyVerFindFileW name 'VerFindFileW', + MyVerInstallFileA name 'VerInstallFileA', + MyVerInstallFileW name 'VerInstallFileW', + MyVerLanguageNameA name 'VerLanguageNameA', + MyVerLanguageNameW name 'VerLanguageNameW', + MyVerQueryValueA name 'VerQueryValueA', + MyVerQueryValueW name 'VerQueryValueW'; + +begin + currentProcessDir := IncludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0))); + sockManager := TSocketManager.Create; + + droverOptions := LoadOptions(currentProcessDir + OPTIONS_FILENAME); + + if droverOptions.useNekoboxProxy and IsNekoBoxExists then + proxyValue.ParseFromString(droverOptions.nekoboxProxy) + else + proxyValue.ParseFromString(droverOptions.proxy); + + LoadOriginalVersionDll; + + RealGetEnvironmentVariableW := InterceptCreate(@GetEnvironmentVariableW, @MyGetEnvironmentVariableW, nil); + RealCreateProcessW := InterceptCreate(@CreateProcessW, @MyCreateProcessW, nil); + RealGetCommandLineW := InterceptCreate(@GetCommandLineW, @MyGetCommandLineW, nil); + + RealSocket := InterceptCreate(@socket, @MySocket, nil); + RealWSASocket := InterceptCreate(@WSASocket, @MyWSASocket, nil); + RealWSASend := InterceptCreate(@WSASend, @MyWSASend, nil); + RealWSASendTo := InterceptCreate(@WSASendTo, @MyWSASendTo, nil); + RealSend := InterceptCreate(@send, @MySend, nil); + RealRecv := InterceptCreate(@recv, @MyRecv, nil); + +end. diff --git a/drover.dproj b/drover.dproj index 3278b07..74f726a 100644 --- a/drover.dproj +++ b/drover.dproj @@ -1,7 +1,7 @@  {A3D8BD4E-DD3F-4E7F-88D4-09AC71ED1352} - 20.1 + 20.2 None True Release @@ -57,6 +57,8 @@ true System;Xml;Data;Datasnap;Web;Soap;$(DCC_Namespace) drover + 1049 + CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments= vclwinx;fmx;vclie;DbxCommonDriver;bindengine;IndyIPCommon;VCLRESTComponents;FireDACCommonODBC;FireDACCommonDriver;appanalytics;IndyProtocols;vclx;Skia.Package.RTL;IndyIPClient;dbxcds;vcledge;bindcompvclwinx;bindcompfmx;inetdb;FireDACSqliteDriver;DbxClientDriver;soapmidas;vclactnband;fmxFireDAC;dbexpress;DBXMySQLDriver;VclSmp;inet;vcltouch;fmxase;dbrtl;Skia.Package.FMX;fmxdae;FireDACMSAccDriver;CustomIPTransport;vcldsnap;DBXInterBaseDriver;IndySystem;Skia.Package.VCL;vcldb;vclFireDAC;bindcomp;FireDACCommon;IndyCore;RESTBackendComponents;bindcompdbx;rtl;FireDACMySQLDriver;FireDACADSDriver;RESTComponents;DBXSqliteDriver;vcl;IndyIPServer;dsnapxml;dsnapcon;adortl;vclimg;FireDACPgDriver;FireDAC;inetdbxpress;xmlrtl;tethering;bindcompvcl;dsnap;CloudService;fmxobj;bindcompvclsmp;soaprtl;soapserver;FireDACIBDriver;$(DCC_UsePackage) @@ -70,11 +72,11 @@ vclwinx;fmx;vclie;DbxCommonDriver;bindengine;IndyIPCommon;VCLRESTComponents;FireDACCommonODBC;FireDACCommonDriver;appanalytics;IndyProtocols;vclx;IndyIPClient;dbxcds;vcledge;bindcompvclwinx;bindcompfmx;inetdb;FireDACSqliteDriver;DbxClientDriver;soapmidas;vclactnband;fmxFireDAC;dbexpress;DBXMySQLDriver;VclSmp;inet;vcltouch;fmxase;dbrtl;fmxdae;FireDACMSAccDriver;CustomIPTransport;vcldsnap;DBXInterBaseDriver;IndySystem;Skia.Package.VCL;vcldb;vclFireDAC;bindcomp;FireDACCommon;IndyCore;RESTBackendComponents;bindcompdbx;rtl;FireDACMySQLDriver;FireDACADSDriver;RESTComponents;DBXSqliteDriver;vcl;IndyIPServer;dsnapxml;dsnapcon;adortl;vclimg;FireDACPgDriver;FireDAC;inetdbxpress;xmlrtl;tethering;bindcompvcl;dsnap;CloudService;fmxobj;bindcompvclsmp;soaprtl;soapserver;FireDACIBDriver;$(DCC_UsePackage) Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;$(DCC_Namespace) Debug - CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments= 1033 false (None) none + run.bat DEBUG;$(DCC_Define) @@ -98,6 +100,7 @@ true 1033 + run.bat @@ -128,7 +131,7 @@ Microsoft Office XP Sample Automation Server Wrapper Components - + true @@ -144,12 +147,7 @@ true - - - drover.dll - true - - + 1 @@ -162,16 +160,6 @@ 0 - - - classes - 64 - - - classes - 64 - - res\xml diff --git a/installer/Main.dfm b/installer/Main.dfm new file mode 100644 index 0000000..9339079 --- /dev/null +++ b/installer/Main.dfm @@ -0,0 +1,140 @@ +object frmMain: TfrmMain + Left = 0 + Top = 0 + Margins.Left = 6 + Margins.Top = 6 + Margins.Right = 6 + Margins.Bottom = 6 + BorderIcons = [biSystemMenu, biMinimize] + BorderStyle = bsSingle + Caption = 'Discord Drover' + ClientHeight = 310 + ClientWidth = 520 + Color = clBtnFace + Font.Charset = DEFAULT_CHARSET + Font.Color = clWindowText + Font.Height = -24 + Font.Name = 'Segoe UI' + Font.Style = [] + Menu = MainMenu + Position = poScreenCenter + OnCreate = FormCreate + OnShow = FormShow + PixelsPerInch = 192 + TextHeight = 32 + object lHost: TLabel + Left = 20 + Top = 93 + Width = 121 + Height = 32 + Margins.Left = 6 + Margins.Top = 6 + Margins.Right = 6 + Margins.Bottom = 6 + Caption = 'Host name:' + end + object lPort: TLabel + Left = 20 + Top = 163 + Width = 138 + Height = 32 + Margins.Left = 6 + Margins.Top = 6 + Margins.Right = 6 + Margins.Bottom = 6 + Caption = 'Port number:' + end + object pType: TPanel + Left = 10 + Top = 18 + Width = 500 + Height = 60 + Margins.Left = 6 + Margins.Top = 6 + Margins.Right = 6 + Margins.Bottom = 6 + BevelOuter = bvNone + TabOrder = 0 + object rbHttp: TRadioButton + Left = 10 + Top = 10 + Width = 113 + Height = 40 + Margins.Left = 6 + Margins.Top = 6 + Margins.Right = 6 + Margins.Bottom = 6 + Caption = 'HTTP' + TabOrder = 0 + end + object rbSocks: TRadioButton + Left = 140 + Top = 10 + Width = 149 + Height = 40 + Margins.Left = 6 + Margins.Top = 6 + Margins.Right = 6 + Margins.Bottom = 6 + Caption = 'SOCKS5' + TabOrder = 1 + end + end + object btnInstall: TButton + Left = 20 + Top = 240 + Width = 230 + Height = 50 + Margins.Left = 6 + Margins.Top = 6 + Margins.Right = 6 + Margins.Bottom = 6 + Caption = 'Install' + TabOrder = 3 + OnClick = btnInstallClick + end + object btnUninstall: TButton + Left = 270 + Top = 240 + Width = 230 + Height = 50 + Margins.Left = 6 + Margins.Top = 6 + Margins.Right = 6 + Margins.Bottom = 6 + Caption = 'Uninstall' + TabOrder = 4 + OnClick = btnUninstallClick + end + object eHost: TEdit + Left = 180 + Top = 90 + Width = 320 + Height = 40 + Margins.Left = 6 + Margins.Top = 6 + Margins.Right = 6 + Margins.Bottom = 6 + TabOrder = 1 + end + object ePort: TEdit + Left = 180 + Top = 160 + Width = 140 + Height = 40 + Margins.Left = 6 + Margins.Top = 6 + Margins.Right = 6 + Margins.Bottom = 6 + NumbersOnly = True + TabOrder = 2 + end + object MainMenu: TMainMenu + Left = 416 + Top = 156 + object miAbout: TMenuItem + Caption = 'View on GitHub' + OnClick = miAboutClick + end + end +end diff --git a/installer/Main.pas b/installer/Main.pas new file mode 100644 index 0000000..e270802 --- /dev/null +++ b/installer/Main.pas @@ -0,0 +1,435 @@ +unit Main; + +interface + +uses + Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics, + Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls, System.Win.Registry, System.RegularExpressions, + System.IOUtils, Options, TlHelp32, Vcl.Menus, ShellApi; + +type + TVersion = array [0 .. 3] of integer; + + TfrmMain = class(TForm) + pType: TPanel; + rbHttp: TRadioButton; + rbSocks: TRadioButton; + btnInstall: TButton; + btnUninstall: TButton; + eHost: TEdit; + ePort: TEdit; + lHost: TLabel; + lPort: TLabel; + MainMenu: TMainMenu; + miAbout: TMenuItem; + procedure FormCreate(Sender: TObject); + procedure FormShow(Sender: TObject); + procedure btnInstallClick(Sender: TObject); + procedure btnUninstallClick(Sender: TObject); + procedure miAboutClick(Sender: TObject); + private + currentProcessDir: string; + messageCaption: PChar; + + function FindMostSuitableOptionsPath: string; + procedure FindDiscordBaseDirs(list: TStringList); + procedure FindDiscordDirs(list: TStringList); + function GetNewestDiscordDir(list: TStringList): string; + function IsDiscordRunning: boolean; + function ShowDiscordRunningMessage: boolean; + public + { Public declarations } + end; + +var + frmMain: TfrmMain; + +implementation + +{$R *.dfm} + +procedure TfrmMain.FormCreate(Sender: TObject); +var + optPath: string; + opt: TDroverOptions; + proxyValue: TProxyValue; +begin + messageCaption := PChar(Application.Title); + currentProcessDir := IncludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0))); + + optPath := FindMostSuitableOptionsPath; + + if optPath <> '' then + begin + opt := LoadOptions(optPath); + end + else + begin + opt := Default (TDroverOptions); + opt.proxy := ''; + end; + + proxyValue.ParseFromString(opt.proxy); + + if proxyValue.isSpecified then + begin + eHost.Text := proxyValue.host; + ePort.Text := IntToStr(proxyValue.port); + rbHttp.Checked := proxyValue.isHttp; + rbSocks.Checked := proxyValue.isSocks5; + end; +end; + +procedure TfrmMain.FormShow(Sender: TObject); +begin + self.ActiveControl := btnInstall; +end; + +procedure TfrmMain.miAboutClick(Sender: TObject); +begin + ShellExecute(0, 'open', 'https://github.com/hdrover/discord-drover', nil, nil, SW_SHOWNORMAL); +end; + +procedure TfrmMain.btnInstallClick(Sender: TObject); +var + dirs, errors: TStringList; + dir, dllPath, s: string; + TaskDialog: TTaskDialog; + opt: TDroverOptions; + proto, host: string; + port: integer; +begin + if ShowDiscordRunningMessage then + exit; + + dllPath := currentProcessDir + DLL_FILENAME; + if not FileExists(dllPath) then + begin + Application.MessageBox(PChar(Format('The file ''%s'' is missing.', [DLL_FILENAME])), messageCaption, MB_ICONERROR); + exit; + end; + + proto := ''; + if rbHttp.Checked then + proto := 'http'; + if rbSocks.Checked then + proto := 'socks5'; + + host := Trim(eHost.Text); + port := StrToIntDef(Trim(ePort.Text), 0); + + if proto = '' then + begin + Application.MessageBox('Protocol is not specified.', messageCaption, MB_ICONERROR); + exit; + end; + if host = '' then + begin + Application.MessageBox('Invalid host specified.', messageCaption, MB_ICONERROR); + exit; + end; + if (port < 1) or (port > 65535) then + begin + Application.MessageBox('Invalid port specified.', messageCaption, MB_ICONERROR); + exit; + end; + + opt.proxy := Format('%s://%s:%d', [proto, host, port]); + + dirs := TStringList.Create; + errors := TStringList.Create; + try + FindDiscordDirs(dirs); + if dirs.Count = 0 then + begin + Application.MessageBox('The Discord folder was not found.', messageCaption, MB_ICONERROR); + exit; + end; + + s := currentProcessDir + OPTIONS_FILENAME; + if not SaveOptions(s, opt) then + errors.Add(s); + + for dir in dirs do + begin + s := dir + OPTIONS_FILENAME; + if not SaveOptions(s, opt) then + errors.Add(s); + + s := dir + DLL_FILENAME; + if not CopyFile(PChar(dllPath), PChar(s), false) then + errors.Add(s); + end; + + if errors.Count > 0 then + begin + TaskDialog := TTaskDialog.Create(nil); + try + TaskDialog.Caption := messageCaption; + TaskDialog.Title := 'Installation error'; + TaskDialog.Text := 'Some files could not be written.'; + TaskDialog.ExpandedText := Trim(errors.Text); + TaskDialog.CommonButtons := [tcbClose]; + TaskDialog.Flags := [tfExpandFooterArea]; + TaskDialog.MainIcon := tdiError; + TaskDialog.Execute; + finally + TaskDialog.Free; + end; + end + else + begin + Application.MessageBox('Installation complete!', messageCaption, MB_ICONINFORMATION); + end; + finally + dirs.Free; + errors.Free; + end; +end; + +procedure TfrmMain.btnUninstallClick(Sender: TObject); +const + FILENAMES: array [0 .. 1] of string = (OPTIONS_FILENAME, DLL_FILENAME); +var + dirs, errors: TStringList; + dir, filename, s: string; + TaskDialog: TTaskDialog; +begin + if ShowDiscordRunningMessage then + exit; + + dirs := TStringList.Create; + errors := TStringList.Create; + try + FindDiscordDirs(dirs); + for dir in dirs do + begin + for filename in FILENAMES do + begin + s := dir + filename; + if FileExists(s) and (not DeleteFile(s)) then + errors.Add(s); + end; + end; + + if errors.Count > 0 then + begin + TaskDialog := TTaskDialog.Create(nil); + try + TaskDialog.Caption := messageCaption; + TaskDialog.Title := 'Uninstallation error'; + TaskDialog.Text := 'Some files could not be deleted.'; + TaskDialog.ExpandedText := Trim(errors.Text); + TaskDialog.CommonButtons := [tcbClose]; + TaskDialog.Flags := [tfExpandFooterArea]; + TaskDialog.MainIcon := tdiError; + TaskDialog.Execute; + finally + TaskDialog.Free; + end; + end + else + begin + Application.MessageBox('Uninstallation complete. All files have been successfully removed.', messageCaption, + MB_ICONINFORMATION); + end; + finally + dirs.Free; + errors.Free; + end; +end; + +function TfrmMain.FindMostSuitableOptionsPath: string; +var + dirs: TStringList; + s, dir: string; +begin + dirs := TStringList.Create; + try + FindDiscordDirs(dirs); + dir := GetNewestDiscordDir(dirs); + finally + dirs.Free; + end; + + if dir <> '' then + begin + s := dir + OPTIONS_FILENAME; + if FileExists(s) then + exit(s); + end; + + s := currentProcessDir + OPTIONS_FILENAME; + if FileExists(s) then + exit(s); + + result := ''; +end; + +procedure TfrmMain.FindDiscordBaseDirs(list: TStringList); +var + reg: TRegistry; + match: TMatch; + s, installLoc: string; +begin + reg := TRegistry.Create(KEY_QUERY_VALUE); + try + reg.RootKey := HKEY_CURRENT_USER; + + if reg.OpenKeyReadOnly('Software\Microsoft\Windows\CurrentVersion\Uninstall\Discord') then + begin + if reg.ValueExists('InstallLocation') then + begin + s := reg.ReadString('InstallLocation'); + if s <> '' then + begin + s := IncludeTrailingPathDelimiter(s); + if DirectoryExists(s) then + begin + installLoc := s; + list.Add(s); + end; + end; + end; + reg.CloseKey; + end; + + if reg.OpenKeyReadOnly('Software\Classes\Discord\shell\open\command') then + begin + if reg.ValueExists('') then + begin + s := reg.ReadString(''); + if s <> '' then + begin + match := TRegEx.match(s, '\A"(.+\\)app-'); + if match.Success then + begin + s := match.Groups[1].Value; + if (not SameText(s, installLoc)) and DirectoryExists(s) then + begin + list.Add(s); + end; + end; + end; + end; + reg.CloseKey; + end; + finally + reg.Free; + end; +end; + +procedure TfrmMain.FindDiscordDirs(list: TStringList); +var + baseDirs: TStringList; + subfolders: TArray; + s, subfolder, baseDir: string; +begin + baseDirs := TStringList.Create; + try + FindDiscordBaseDirs(baseDirs); + for baseDir in baseDirs do + begin + if TDirectory.Exists(baseDir) then + begin + subfolders := TDirectory.GetDirectories(baseDir, 'app-*', TSearchOption.soTopDirectoryOnly); + for subfolder in subfolders do + begin + s := IncludeTrailingPathDelimiter(subfolder); + if FileExists(s + 'Discord.exe') then + begin + list.Add(s); + end; + end; + end; + end; + finally + baseDirs.Free; + end; +end; + +function TfrmMain.GetNewestDiscordDir(list: TStringList): string; +var + i, partsLen: integer; + dir: string; + match: TMatch; + maxVer, curVer: TVersion; + parts: TArray; +begin + if list.Count = 0 then + exit(''); + + result := list[0]; + maxVer := Default (TVersion); + + for dir in list do + begin + match := TRegEx.match(dir, 'app-([\d.]+)'); + if not match.Success then + continue; + parts := match.Groups[1].Value.Split(['.']); + partsLen := Length(parts); + for i := 0 to High(curVer) do + begin + if i < partsLen then + curVer[i] := StrToIntDef(parts[i], 0) + else + curVer[i] := 0; + end; + + for i := 0 to High(curVer) do + begin + if curVer[i] <> maxVer[i] then + begin + if curVer[i] > maxVer[i] then + begin + maxVer := curVer; + result := dir; + end; + break; + end; + end; + end; +end; + +function TfrmMain.IsDiscordRunning: boolean; +var + snapshot: THandle; + processEntry: TProcessEntry32; +begin + result := false; + snapshot := CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if snapshot = INVALID_HANDLE_VALUE then + exit; + + try + processEntry.dwSize := SizeOf(processEntry); + if Process32First(snapshot, processEntry) then + begin + repeat + if SameText(ExtractFileName(processEntry.szExeFile), 'Discord.exe') then + begin + result := true; + break; + end; + until not Process32Next(snapshot, processEntry); + end; + finally + CloseHandle(snapshot); + end; +end; + +function TfrmMain.ShowDiscordRunningMessage: boolean; +begin + if IsDiscordRunning then + begin + result := true; + Application.MessageBox('Please exit Discord before proceeding.', messageCaption, MB_ICONERROR); + end + else + begin + result := false; + end; +end; + +end. diff --git a/installer/drover.Artwork/Windows/AppIcon.icns b/installer/drover.Artwork/Windows/AppIcon.icns new file mode 100644 index 0000000..69f3363 Binary files /dev/null and b/installer/drover.Artwork/Windows/AppIcon.icns differ diff --git a/installer/drover.Artwork/Windows/AppIcon.ico b/installer/drover.Artwork/Windows/AppIcon.ico new file mode 100644 index 0000000..76a8e2b Binary files /dev/null and b/installer/drover.Artwork/Windows/AppIcon.ico differ diff --git a/installer/drover.Artwork/Windows/Uwp_150.png b/installer/drover.Artwork/Windows/Uwp_150.png new file mode 100644 index 0000000..e9c5eda Binary files /dev/null and b/installer/drover.Artwork/Windows/Uwp_150.png differ diff --git a/installer/drover.Artwork/Windows/Uwp_44.png b/installer/drover.Artwork/Windows/Uwp_44.png new file mode 100644 index 0000000..09f4f24 Binary files /dev/null and b/installer/drover.Artwork/Windows/Uwp_44.png differ diff --git a/installer/drover.dpr b/installer/drover.dpr new file mode 100644 index 0000000..b3f4ec8 --- /dev/null +++ b/installer/drover.dpr @@ -0,0 +1,16 @@ +program drover; + +uses + Vcl.Forms, + Main in 'Main.pas' {frmMain}, + Options in '..\Options.pas'; + +{$R *.res} + +begin + Application.Initialize; + Application.MainFormOnTaskbar := True; + Application.Title := 'Drover'; + Application.CreateForm(TfrmMain, frmMain); + Application.Run; +end. diff --git a/installer/drover.dproj b/installer/drover.dproj new file mode 100644 index 0000000..a7c72fc --- /dev/null +++ b/installer/drover.dproj @@ -0,0 +1,1135 @@ + + + {F1A676AF-029D-491E-9645-97419EAFA85C} + 20.2 + VCL + True + Release + Win64 + drover + 2 + Application + drover.dpr + + + true + + + true + Base + true + + + true + Base + true + + + true + Base + true + + + true + Cfg_1 + true + true + + + true + Cfg_1 + true + true + + + true + Base + true + + + true + Cfg_2 + true + true + + + true + Cfg_2 + true + true + + + .\$(Platform)\$(Config) + .\$(Platform)\$(Config) + false + false + false + false + false + System;Xml;Data;Datasnap;Web;Soap;Vcl;Vcl.Imaging;Vcl.Touch;Vcl.Samples;Vcl.Shell;$(DCC_Namespace) + $(BDS)\bin\delphi_PROJECTICON.ico + $(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_44.png + $(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_150.png + drover + 1049 + CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments= + + + vclwinx;fmx;vclie;DbxCommonDriver;bindengine;IndyIPCommon;VCLRESTComponents;FireDACCommonODBC;FireDACCommonDriver;appanalytics;IndyProtocols;vclx;Skia.Package.RTL;IndyIPClient;dbxcds;vcledge;bindcompvclwinx;bindcompfmx;inetdb;FireDACSqliteDriver;DbxClientDriver;soapmidas;vclactnband;fmxFireDAC;dbexpress;DBXMySQLDriver;VclSmp;inet;vcltouch;fmxase;dbrtl;Skia.Package.FMX;fmxdae;FireDACMSAccDriver;CustomIPTransport;vcldsnap;DBXInterBaseDriver;IndySystem;Skia.Package.VCL;vcldb;vclFireDAC;bindcomp;FireDACCommon;IndyCore;RESTBackendComponents;bindcompdbx;rtl;FireDACMySQLDriver;FireDACADSDriver;RESTComponents;DBXSqliteDriver;vcl;IndyIPServer;dsnapxml;dsnapcon;adortl;vclimg;FireDACPgDriver;FireDAC;inetdbxpress;xmlrtl;tethering;bindcompvcl;dsnap;CloudService;fmxobj;bindcompvclsmp;soaprtl;soapserver;FireDACIBDriver;$(DCC_UsePackage) + Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;Bde;$(DCC_Namespace) + Debug + true + CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments= + 1033 + $(BDS)\bin\default_app.manifest + $(ProjectName).Artwork\Windows\AppIcon.ico + $(ProjectName).Artwork\Windows\AppIcon.icns + $(ProjectName).Artwork\Windows\Uwp_44.png + $(ProjectName).Artwork\Windows\Uwp_150.png + + + vclwinx;fmx;vclie;DbxCommonDriver;bindengine;IndyIPCommon;VCLRESTComponents;FireDACCommonODBC;FireDACCommonDriver;appanalytics;IndyProtocols;vclx;IndyIPClient;dbxcds;vcledge;bindcompvclwinx;bindcompfmx;inetdb;FireDACSqliteDriver;DbxClientDriver;soapmidas;vclactnband;fmxFireDAC;dbexpress;DBXMySQLDriver;VclSmp;inet;vcltouch;fmxase;dbrtl;fmxdae;FireDACMSAccDriver;CustomIPTransport;vcldsnap;DBXInterBaseDriver;IndySystem;Skia.Package.VCL;vcldb;vclFireDAC;bindcomp;FireDACCommon;IndyCore;RESTBackendComponents;bindcompdbx;rtl;FireDACMySQLDriver;FireDACADSDriver;RESTComponents;DBXSqliteDriver;vcl;IndyIPServer;dsnapxml;dsnapcon;adortl;vclimg;FireDACPgDriver;FireDAC;inetdbxpress;xmlrtl;tethering;bindcompvcl;dsnap;CloudService;fmxobj;bindcompvclsmp;soaprtl;soapserver;FireDACIBDriver;$(DCC_UsePackage) + Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;$(DCC_Namespace) + Debug + 1033 + $(BDS)\bin\default_app.manifest + $(ProjectName).Artwork\Windows\AppIcon.ico + $(ProjectName).Artwork\Windows\AppIcon.icns + $(ProjectName).Artwork\Windows\Uwp_44.png + $(ProjectName).Artwork\Windows\Uwp_150.png + none + + + DEBUG;$(DCC_Define) + true + false + true + true + true + true + true + + + false + PerMonitorV2 + + + PerMonitorV2 + true + 1033 + $(ProjectName).Artwork\Windows\AppIcon.ico + + + false + RELEASE;$(DCC_Define) + 0 + 0 + + + PerMonitorV2 + + + PerMonitorV2 + 1033 + $(ProjectName).Artwork\Windows\AppIcon.ico + + + + MainSource + + +
frmMain
+ dfm +
+ + + Base + + + Cfg_1 + Base + + + Cfg_2 + Base + +
+ + Delphi.Personality.12 + Application + + + + drover.dpr + + + Microsoft Office 2000 Sample Automation Server Wrapper Components + Microsoft Office XP Sample Automation Server Wrapper Components + + + + + + + drover.exe + true + + + + + drover.rsm + true + + + + + 1 + + + Contents\MacOS + 1 + + + 0 + + + + + res\xml + 1 + + + res\xml + 1 + + + + + library\lib\armeabi + 1 + + + library\lib\armeabi + 1 + + + + + library\lib\armeabi-v7a + 1 + + + + + library\lib\mips + 1 + + + library\lib\mips + 1 + + + + + library\lib\armeabi-v7a + 1 + + + library\lib\arm64-v8a + 1 + + + + + library\lib\armeabi-v7a + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\drawable-anydpi-v21 + 1 + + + res\drawable-anydpi-v21 + 1 + + + + + res\values + 1 + + + res\values + 1 + + + + + res\values-v21 + 1 + + + res\values-v21 + 1 + + + + + res\values-v31 + 1 + + + res\values-v31 + 1 + + + + + res\drawable-anydpi-v26 + 1 + + + res\drawable-anydpi-v26 + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\drawable-anydpi-v33 + 1 + + + res\drawable-anydpi-v33 + 1 + + + + + res\values + 1 + + + res\values + 1 + + + + + res\values-night-v21 + 1 + + + res\values-night-v21 + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\drawable-xxhdpi + 1 + + + res\drawable-xxhdpi + 1 + + + + + res\drawable-xxxhdpi + 1 + + + res\drawable-xxxhdpi + 1 + + + + + res\drawable-ldpi + 1 + + + res\drawable-ldpi + 1 + + + + + res\drawable-mdpi + 1 + + + res\drawable-mdpi + 1 + + + + + res\drawable-hdpi + 1 + + + res\drawable-hdpi + 1 + + + + + res\drawable-xhdpi + 1 + + + res\drawable-xhdpi + 1 + + + + + res\drawable-mdpi + 1 + + + res\drawable-mdpi + 1 + + + + + res\drawable-hdpi + 1 + + + res\drawable-hdpi + 1 + + + + + res\drawable-xhdpi + 1 + + + res\drawable-xhdpi + 1 + + + + + res\drawable-xxhdpi + 1 + + + res\drawable-xxhdpi + 1 + + + + + res\drawable-xxxhdpi + 1 + + + res\drawable-xxxhdpi + 1 + + + + + res\drawable-small + 1 + + + res\drawable-small + 1 + + + + + res\drawable-normal + 1 + + + res\drawable-normal + 1 + + + + + res\drawable-large + 1 + + + res\drawable-large + 1 + + + + + res\drawable-xlarge + 1 + + + res\drawable-xlarge + 1 + + + + + res\values + 1 + + + res\values + 1 + + + + + res\drawable-anydpi-v24 + 1 + + + res\drawable-anydpi-v24 + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\drawable-night-anydpi-v21 + 1 + + + res\drawable-night-anydpi-v21 + 1 + + + + + res\drawable-anydpi-v31 + 1 + + + res\drawable-anydpi-v31 + 1 + + + + + res\drawable-night-anydpi-v31 + 1 + + + res\drawable-night-anydpi-v31 + 1 + + + + + 1 + + + Contents\MacOS + 1 + + + 0 + + + + + Contents\MacOS + 1 + .framework + + + Contents\MacOS + 1 + .framework + + + Contents\MacOS + 1 + .framework + + + 0 + + + + + 1 + .dylib + + + 1 + .dylib + + + 1 + .dylib + + + Contents\MacOS + 1 + .dylib + + + Contents\MacOS + 1 + .dylib + + + Contents\MacOS + 1 + .dylib + + + 0 + .dll;.bpl + + + + + 1 + .dylib + + + 1 + .dylib + + + 1 + .dylib + + + Contents\MacOS + 1 + .dylib + + + Contents\MacOS + 1 + .dylib + + + Contents\MacOS + 1 + .dylib + + + 0 + .bpl + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + Contents\Resources\StartUp\ + 0 + + + Contents\Resources\StartUp\ + 0 + + + Contents\Resources\StartUp\ + 0 + + + 0 + + + + + 1 + + + 1 + + + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + + + ..\ + 1 + + + ..\ + 1 + + + ..\ + 1 + + + + + Contents + 1 + + + Contents + 1 + + + Contents + 1 + + + + + Contents\Resources + 1 + + + Contents\Resources + 1 + + + Contents\Resources + 1 + + + + + library\lib\armeabi-v7a + 1 + + + library\lib\arm64-v8a + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + Contents\MacOS + 1 + + + Contents\MacOS + 1 + + + Contents\MacOS + 1 + + + 0 + + + + + library\lib\armeabi-v7a + 1 + + + + + 1 + + + 1 + + + 1 + + + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + + + ..\ + 1 + + + ..\ + 1 + + + ..\ + 1 + + + + + 1 + + + 1 + + + 1 + + + + + ..\$(PROJECTNAME).launchscreen + 64 + + + ..\$(PROJECTNAME).launchscreen + 64 + + + + + 1 + + + 1 + + + 1 + + + + + Assets + 1 + + + Assets + 1 + + + + + Assets + 1 + + + Assets + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + + + + + + + + + + + + + False + True + + + 12 + + + + +
diff --git a/installer/drover.res b/installer/drover.res new file mode 100644 index 0000000..421f46e Binary files /dev/null and b/installer/drover.res differ