checkrideutil.pas 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  1. unit CheckRideUtil;
  2. {*
  3. This source code is provided under the MIT license:
  4. Copyright (C) 2011 by Reinier Olislagers
  5. Permission is hereby granted, free of charge, to any person obtaining a copy
  6. of this software and associated documentation files (the "Software"), to deal
  7. in the Software without restriction, including without limitation the rights
  8. to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9. copies of the Software, and to permit persons to whom the Software is
  10. furnished to do so, subject to the following conditions:
  11. The above copyright notice and this permission notice shall be included in
  12. all copies or substantial portions of the Software.
  13. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  14. IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  15. FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  16. AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  17. LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  18. OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  19. THE SOFTWARE.
  20. *}
  21. {$mode objfpc}{$H+}
  22. interface
  23. uses
  24. Classes, SysUtils;
  25. const
  26. CheckRideConfigFileName = 'CheckRide.conf'; //Change this when filename changes
  27. CheckRideExe = 'checkride.exe'; //Change this if you change the executable file name
  28. CheckRideHelperExe = 'checkridehelper.exe';
  29. //Change this if you change the executable file name
  30. HelperHostExample = 'changethisline.example.com';
  31. //Edit this when changing the config file
  32. HelperPortExample = '33334';//Edit this when changing the config file
  33. HelperNameExample = 'Wonko the Sane';//Edit this when changing the config file
  34. type
  35. TStunnelMode = (Helped, Helper);
  36. var
  37. FConnectHost: string; //Host to connect to (CheckRide)
  38. FConnectPort: integer; //Port to connect to (CheckRide) or listen on (CheckRideHelper)
  39. FConnectHelper: string; //Show this to CheckRide as a connection identifier
  40. FResourceDir: string = '';
  41. //Directory where configs etc are temporarily placed, no trailing directory separator.
  42. procedure CheckDebug(const Message: string);
  43. //Our own version of debugln that doesn't run if DEBUG not defined
  44. procedure CreateUniqueTempDir;
  45. procedure CleanTempDir; //Cleans up the temp dir, if created.
  46. procedure CleanSystemTray;
  47. function ServiceExists(ServiceName: string): boolean;
  48. function IsServiceRunning(ServiceName: string): boolean;
  49. procedure StartService(ServiceName: string);
  50. procedure StopService(ServiceName: string);
  51. function ProcessExists(ExeFileName: string): boolean;
  52. function WaitForProcessToDie(ProcessName: string): boolean;
  53. function CustomStunnelconfig(TypeOfTunnel: TStunnelMode): string;
  54. { Extract required executables, config files out of resource so we can use them. Returns directory, with trailing \ or /, where extracted.}
  55. function ResourceExtract(ExeFile: string): string;
  56. implementation
  57. uses
  58. FileUtil,
  59. ServiceManager,
  60. JwaTlHelp32 {for running processes},
  61. JwaWinType {for processes declarations},
  62. JwaWinBase {just a guess: for closing process handles},
  63. JwaWinSvc {for services declarations, always required},
  64. jwawinuser {for clearing tray icon/notification area},
  65. Zipper {extraction of resources},
  66. IniFiles, poormansresource,
  67. lclproc {for debugging};
  68. function WaitForProcessToDie(ProcessName: string): boolean;
  69. {description Waits until process is dead, but respects timeout}
  70. const
  71. SleepTimeOut = 4;
  72. var
  73. i: integer;
  74. begin
  75. i := 0;
  76. while ProcessExists(ProcessName) = False do
  77. begin
  78. sleep(500);
  79. //Application.ProcessMessages; //Handle GUI events
  80. i := i + 1;
  81. if i >= SleepTimeOut then
  82. begin
  83. Result := False;
  84. break;
  85. end;
  86. end; //while
  87. Result := True;
  88. end;
  89. procedure CleanSystemTray;
  90. {description Clean dead icons from system tray/notification area}
  91. var
  92. hNotificationArea: HWND;
  93. r: RECT;
  94. x: integer;
  95. y: integer;
  96. begin
  97. hNotificationArea := FindWindowEx(
  98. FindWindowEx(FindWindowEx(FindWindowEx(0, 0, 'Shell_TrayWnd', ''),
  99. 0, 'TrayNotifyWnd', ''), 0, 'SysPager', ''), 0, 'ToolbarWindow32',
  100. 'Notification Area');
  101. GetClientRect(hNotificationArea, r);
  102. //Now we've got the area, force it to update
  103. //by sending mouse messages to it.
  104. x := 0;
  105. y := 0;
  106. while x < r.Right do
  107. begin
  108. while y < r.Bottom do
  109. begin
  110. SendMessage(hNotificationArea, WM_MOUSEMOVE, 0, (y shl 16) + x);
  111. y := y + 5;
  112. end;
  113. x := x + 5;
  114. end;
  115. end;
  116. function ServiceExists(ServiceName: string): boolean;
  117. {description Checks if a Windows service exists}
  118. var
  119. Services: TServiceManager;
  120. ServiceStatus: TServiceStatus;
  121. begin
  122. //Check for existing services
  123. Services := TServiceManager.Create(nil);
  124. try
  125. try
  126. Services.Acces := SC_MANAGER_CONNECT; //Note typo in property.
  127. //We don't need more access permissions than this; by default
  128. //the servicemanager is trying to get all access
  129. Services.Connect;
  130. try
  131. Services.GetServiceStatus(ServiceName, ServiceStatus);
  132. Result := True;
  133. except
  134. Result := False; //assume service does not exist
  135. end;
  136. Services.Disconnect;
  137. except
  138. on E: Exception do
  139. begin
  140. Result := False;
  141. end;
  142. end;
  143. finally
  144. Services.Free;
  145. end;
  146. end;
  147. function IsServiceRunning(ServiceName: string): boolean;
  148. {description Checks if a Windows service is running}
  149. var
  150. Services: TServiceManager;
  151. ServiceStatus: TServiceStatus;
  152. begin
  153. //Check for existing services
  154. //equivalent to sc query <servicename>
  155. Services := TServiceManager.Create(nil);
  156. try
  157. try
  158. Services.Acces := SC_MANAGER_CONNECT; //Note typo in .Access property.
  159. Services.Connect; //Connect with requested access permissions.
  160. Services.GetServiceStatus(ServiceName, ServiceStatus);
  161. if ServiceStatus.dwCurrentState = SERVICE_RUNNING then
  162. begin
  163. Result := True;
  164. end
  165. else
  166. begin
  167. Result := False;
  168. end;
  169. Services.Disconnect;
  170. except
  171. on E: EServiceManager do
  172. begin
  173. // A missing service might throw a missing handle exception? No?
  174. {LogOutput('Error getting service information for ' + ServiceName +
  175. '. Technical details: ' + E.ClassName + '/' + E.Message);}
  176. Result := False;
  177. raise; //rethrow original exception
  178. end;
  179. on E: Exception do
  180. begin
  181. {LogOutput('Error getting service information for ' + ServiceName +
  182. '. Technical details: ' + E.ClassName + '/' + E.Message);
  183. }
  184. Result := False;
  185. raise; //rethrow original exception
  186. end;
  187. end;
  188. finally
  189. Services.Free;
  190. end;
  191. end;
  192. procedure StartService(ServiceName: string);
  193. {description Gives a service the start command}
  194. var
  195. Services: TServiceManager;
  196. ServiceStatus: TServiceStatus;
  197. begin
  198. Services := TServiceManager.Create(nil);
  199. try
  200. try
  201. Services.Acces := SC_MANAGER_CONNECT; //Note typo in .Access property.
  202. Services.Connect; //Connect with requested access permissions.
  203. Services.GetServiceStatus(ServiceName, ServiceStatus);
  204. if ServiceStatus.dwCurrentState <> SERVICE_RUNNING then
  205. begin
  206. Services.StartService(ServiceName, nil);
  207. end;
  208. Services.Disconnect;
  209. except
  210. on E: EServiceManager do
  211. begin
  212. // A missing service might throw a missing handle exception? No?
  213. {LogOutput('Error getting service information for ' + ServiceName +
  214. '. Technical details: ' + E.ClassName + '/' + E.Message);}
  215. raise; //rethrow original exception
  216. end;
  217. on E: Exception do
  218. begin
  219. {LogOutput('Error getting service information for ' + ServiceName +
  220. '. Technical details: ' + E.ClassName + '/' + E.Message);
  221. }
  222. raise; //rethrow original exception
  223. end;
  224. end;
  225. finally
  226. Services.Free;
  227. end;
  228. end;
  229. procedure StopService(ServiceName: string);
  230. {description Gives a service the stop command}
  231. var
  232. Services: TServiceManager;
  233. ServiceStatus: TServiceStatus;
  234. begin
  235. Services := TServiceManager.Create(nil);
  236. try
  237. try
  238. Services.Acces := SC_MANAGER_CONNECT; //Note typo in .Access property.
  239. Services.Connect; //Connect with requested access permissions.
  240. Services.GetServiceStatus(ServiceName, ServiceStatus);
  241. if ServiceStatus.dwCurrentState = SERVICE_RUNNING then
  242. begin
  243. Services.StopService(ServiceName, True);
  244. end;
  245. Services.Disconnect;
  246. except
  247. on E: EServiceManager do
  248. begin
  249. // A missing service might throw a missing handle exception? No?
  250. {LogOutput('Error getting service information for ' + ServiceName +
  251. '. Technical details: ' + E.ClassName + '/' + E.Message);}
  252. raise; //rethrow original exception
  253. end;
  254. on E: Exception do
  255. begin
  256. {LogOutput('Error getting service information for ' + ServiceName +
  257. '. Technical details: ' + E.ClassName + '/' + E.Message);
  258. }
  259. raise; //rethrow original exception
  260. end;
  261. end;
  262. finally
  263. Services.Free;
  264. end;
  265. end;
  266. function ProcessExists(ExeFileName: string): boolean;
  267. {description checks if the process is running. Adapted for freepascal from:
  268. URL: http://www.swissdelphicenter.ch/torry/showcode.php?id=2554}
  269. var
  270. ContinueLoop: BOOL;
  271. FSnapshotHandle: THandle;
  272. FProcessEntry32: TProcessEntry32;
  273. begin
  274. FSnapshotHandle := CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
  275. FProcessEntry32.dwSize := SizeOf(FProcessEntry32);
  276. ContinueLoop := Process32First(FSnapshotHandle, FProcessEntry32);
  277. Result := False;
  278. while integer(ContinueLoop) <> 0 do
  279. begin
  280. if ((UpperCase(ExtractFileName(FProcessEntry32.szExeFile)) =
  281. UpperCase(ExeFileName)) or (UpperCase(FProcessEntry32.szExeFile) =
  282. UpperCase(ExeFileName))) then
  283. begin
  284. Result := True;
  285. end;
  286. ContinueLoop := Process32Next(FSnapshotHandle, FProcessEntry32);
  287. end;
  288. CloseHandle(FSnapshotHandle);
  289. end;
  290. procedure CheckDebug(const Message: string);
  291. begin
  292. {$IFDEF DEBUG}
  293. DebugLn(DateTimeTostr(Now) + ': ' + Message);
  294. sleep(500);//hopefully give time to write
  295. {$ENDIF DEBUG}
  296. end;
  297. procedure CreateUniqueTempDir;
  298. {description Create a uniquely named directory under the user's temporary directory
  299. for this application - one temp dir for all files. Resulting dir in FTempDir
  300. Adapted from http://www.delphifaq.net/how-to-get-a-unique-file-name/}
  301. const
  302. InvalidDir = '++&INVALID_TEMP_DIR!!';
  303. var
  304. chTemp: char;
  305. DirectoryName: string = '';
  306. TempDir: string; //Our user's temporary directory
  307. begin
  308. // Only create dir if it doesn't already exist:
  309. if (FResourceDir = '') or (FResourceDir = InvalidDir) then
  310. begin
  311. TempDir := SysUtils.GetTempDir; //Apparently contains trailing path separator
  312. // Get subdirectory name that doesn't already exist:
  313. repeat
  314. Randomize;
  315. repeat
  316. chTemp := Chr(Random(43) + 47);
  317. if Length(DirectoryName) = 8 then
  318. DirectoryName := DirectoryName + '.'
  319. else if chTemp in ['0'..'9', 'A'..'Z'] then
  320. DirectoryName := DirectoryName + chTemp;
  321. until Length(DirectoryName) = 12;
  322. until not DirectoryExists(TempDir + DirectoryName);
  323. //Once we're sure we have a new directory:
  324. try
  325. mkdir(TempDir + DirectoryName);
  326. FResourceDir := TempDir + DirectoryName; //Store the result
  327. except
  328. //This is bad.
  329. FResourceDir := InvalidDir;
  330. raise Exception.Create('Cannot create temporary directory.');
  331. end;
  332. end;
  333. end;
  334. procedure CleanTempDir;
  335. begin
  336. if FResourceDir <> '' then
  337. begin
  338. DeleteDirectory(FResourceDir, False); //get rid of directory and children
  339. end;
  340. end;
  341. function ResourceExtract(ExeFile: string): string;
  342. {description Extracts required executables and other files from our poor man's resource to
  343. temporary directory - stored in FResourceDir}
  344. {NOTE: if you change files in the resources, please adjust
  345. CheckRideResourceZipper.lpr accordingly and recompile.
  346. CheckRideResourceZipper.exe is called before build of CheckRide and
  347. CheckRideHelper, so it needs to know about changes.}
  348. const
  349. ResourceName = 'ALL';
  350. var
  351. CheckRideConfigFile: string = ''; //Source for config adjustments
  352. CheckRideConfigIni: TMemIniFile;
  353. // Contains CheckRideConfig data for modifying templates
  354. CheckRideConfigStrings: TStringList;// Contains data from ConfigResource
  355. PoorMansResource: TPayload;
  356. UnzipFile: TUnZipper;
  357. ZipToExtract: string;
  358. begin
  359. // Init, just to make sure
  360. Result := '';
  361. if FResourceDir = '' then
  362. begin
  363. CreateUniqueTempDir;
  364. end;
  365. // Point resource stream to the resource
  366. PoorMansResource := TPayload.Create(ExeFile); //point to this executable
  367. {Note: Paramstr(0) not guaranteed portable to Unix etc...}
  368. UnzipFile := TUnZipper.Create();
  369. try
  370. //todo: we might streamline this, reading into memory and unzipping
  371. //relevant files, directly reading others. Not worth the effort for
  372. //minimum speed gains.
  373. ZipToExtract := FResourceDir + DirectorySeparator + ResourceName + '.zip';
  374. CheckDebug('ZipToExtract: ' + ZipToExtract); //is this actually the right file?
  375. PoorMansResource.PayloadIntoFile(ZipToExtract);
  376. UnzipFile.FileName := ZipToExtract;
  377. UnzipFile.OutputPath := FResourceDir;
  378. UnzipFile.UnZipAllFiles; //into temp dir, we hope
  379. finally
  380. UnzipFile.Free;
  381. PoorMansResource.Free;
  382. end;
  383. try
  384. DeleteFile(PChar(ZipToExtract));
  385. except
  386. //ignore errors deleting temp file; let's hope the file system will do this in time.
  387. end;
  388. //Now read in config
  389. CheckRideConfigIni := TMemIniFile.Create(CheckRideConfigFile, False);
  390. CheckRideConfigStrings := TStringList.Create;
  391. try
  392. try
  393. // If ini file exists in exe directory, use that.
  394. CheckRideConfigFile := ExtractFilePath(ParamStr(0)) + CheckRideConfigFileName;
  395. CheckRideConfigStrings.LoadFromFile(CheckRideConfigFile);
  396. except
  397. // Fallback: use resource embedded in program
  398. CheckRideConfigStrings.LoadFromFile(FResourceDir + DirectorySeparator +
  399. CheckRideConfigFileName);
  400. end;
  401. // Load strings into ini object for further processing:
  402. CheckRideConfigIni.SetStrings(CheckRideConfigStrings);
  403. FConnectHost := Trim(CheckRideConfigIni.ReadString('Default',
  404. 'HelperHost', 'thiswillnotwork.example.com'));
  405. FConnectPort := CheckRideConfigIni.ReadInteger('Default', 'HelperPort', 33334);
  406. FConnectHelper := CheckRideConfigIni.ReadString('Default',
  407. 'HelperName', 'FallbackHelperInProgram');
  408. Result := FResourceDir + DirectorySeparator; //Show valid result
  409. finally
  410. CheckRideConfigIni.Free;
  411. CheckRideConfigStrings.Free;
  412. end;
  413. end;
  414. function CustomStunnelconfig(TypeOfTunnel: TStunnelMode): string;
  415. {description Applies CheckRide.conf to relevant template file, writes
  416. result to temporary file and returns the config file name.
  417. If CheckRide.conf doesn't exist in the executable directory,
  418. if necessary extract from resource and use.}
  419. const
  420. HostReplace = '$(HELPERSERVERNAME)';
  421. PortReplace = '$(HELPERSERVERPORT)';
  422. HelpedTemplateName = 'stunnelhelped.conf.template';
  423. HelpedConfigName = 'stunnelhelped.conf';
  424. HelperTemplateName = 'stunnelhelper.conf.template';
  425. HelperConfigName = 'stunnelhelper.conf';
  426. var
  427. HelpedResultFile: string; //Final config file
  428. HelperResultFile: string; //Final config file
  429. ResultingConfig: TStringList; // Template with checkrideconfig replacements run
  430. begin
  431. // Init, just to make sure
  432. if FResourceDir = '' then
  433. begin
  434. CreateUniqueTempDir;
  435. end;
  436. HelpedResultFile := FResourceDir + DirectorySeparator + HelpedConfigName;
  437. HelperResultFile := FResourceDir + DirectorySeparator + HelperConfigName;
  438. ResultingConfig := TStringList.Create;
  439. try
  440. // Replace strings in helped template and save
  441. case TypeOfTunnel of
  442. Helped:
  443. begin
  444. ResultingConfig.LoadFromFile(FResourceDir + DirectorySeparator +
  445. HelpedTemplateName);
  446. end;
  447. Helper:
  448. begin
  449. ResultingConfig.LoadFromFile(FResourceDir + DirectorySeparator +
  450. HelperTemplateName);
  451. end;
  452. else
  453. raise Exception.Create('Invalid TypeOfTunnel selected.');
  454. end;
  455. ResultingConfig.Text := StringReplace(ResultingConfig.Text,
  456. HostReplace, FConnectHost, [rfReplaceAll]);
  457. ResultingConfig.Text := StringReplace(ResultingConfig.Text,
  458. PortReplace, IntToStr(FConnectPort), [rfReplaceAll]);
  459. case TypeOfTunnel of
  460. Helped:
  461. begin
  462. ResultingConfig.SaveToFile(HelpedResultFile);
  463. Result := HelpedResultFile;
  464. end;
  465. Helper:
  466. begin
  467. ResultingConfig.SaveToFile(HelperResultFile);
  468. Result := HelperResultFile;
  469. end;
  470. else
  471. raise Exception.Create('Invalid TypeOfTunnel selected.');
  472. end;
  473. finally
  474. ResultingConfig.Free;
  475. end;
  476. end;
  477. end.