IdSASLCollection.pas 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567
  1. {
  2. $Project$
  3. $Workfile$
  4. $Revision$
  5. $DateUTC$
  6. $Id$
  7. This file is part of the Indy (Internet Direct) project, and is offered
  8. under the dual-licensing agreement described on the Indy website.
  9. (http://www.indyproject.org/)
  10. Copyright:
  11. (c) 1993-2005, Chad Z. Hower and the Indy Pit Crew. All rights reserved.
  12. }
  13. {
  14. $Log$
  15. }
  16. {
  17. Rev 1.5 11/27/2004 8:27:14 PM JPMugaas
  18. Fix for compiler errors.
  19. Rev 1.4 11/27/04 2:56:40 AM RLebeau
  20. Added support for overloaded version of LoginSASL().
  21. Added GetDisplayName() method to TIdSASLListEntry, and FindSASL() method to
  22. TIdSASLEntries.
  23. Rev 1.3 10/26/2004 10:55:32 PM JPMugaas
  24. Updated refs.
  25. Rev 1.2 6/11/2004 9:38:38 AM DSiders
  26. Added "Do not Localize" comments.
  27. Rev 1.1 2004.02.03 5:45:50 PM czhower
  28. Name changes
  29. Rev 1.0 1/25/2004 3:09:54 PM JPMugaas
  30. New collection class for SASL mechanism processing.
  31. }
  32. unit IdSASLCollection;
  33. interface
  34. {$i IdCompilerDefines.inc}
  35. uses
  36. Classes,
  37. IdBaseComponent,
  38. IdCoder,
  39. IdException,
  40. IdSASL,
  41. IdTCPConnection;
  42. type
  43. TIdSASLEntries = class;
  44. TIdSASLListEntry = class(TCollectionItem)
  45. protected
  46. {$IF DEFINED(HAS_UNSAFE_OBJECT_REF)}[Unsafe]
  47. {$ELSEIF DEFINED(HAS_WEAK_OBJECT_REF)}[Weak]
  48. {$IFEND} FSASL : TIdSASL;
  49. //
  50. function GetDisplayName: String; override;
  51. function GetOwnerComponent: TComponent;
  52. function GetSASLEntries: TIdSASLEntries;
  53. procedure SetSASL(AValue : TIdSASL);
  54. public
  55. procedure Assign(Source: TPersistent); override;
  56. property OwnerComponent: TComponent read GetOwnerComponent;
  57. property SASLEntries: TIdSASLEntries read GetSASLEntries;
  58. published
  59. property SASL : TIdSASL read FSASL write SetSASL;
  60. end;
  61. TIdSASLEntries = class ( TOwnedCollection )
  62. protected
  63. procedure CheckIfEmpty;
  64. function GetItem(Index: Integer) : TIdSASLListEntry;
  65. function GetOwnerComponent: TComponent;
  66. procedure SetItem(Index: Integer; const Value: TIdSASLListEntry);
  67. public
  68. constructor Create ( AOwner : TPersistent ); reintroduce;
  69. function Add: TIdSASLListEntry;
  70. procedure LoginSASL(const ACmd, AHost: string; const APort: TIdPort; const AProtocolName: String;
  71. const AOkReplies, AContinueReplies: array of string; AClient : TIdTCPConnection;
  72. ACapaReply : TStrings; const AAuthString : String = 'AUTH'; {Do not Localize}
  73. ACanAttemptIR: Boolean = True); overload;
  74. procedure LoginSASL(const ACmd, AHost: string; const APort: TIdPort; const AProtocolName, AServiceName: String;
  75. const AOkReplies, AContinueReplies: array of string; AClient : TIdTCPConnection;
  76. ACapaReply : TStrings; const AAuthString : String = 'AUTH'; {Do not Localize}
  77. ACanAttemptIR: Boolean = True); overload;
  78. procedure ParseCapaReplyToList(ACapaReply, ADestList: TStrings; const AAuthString: String = 'AUTH'); {do not localize}
  79. function FindSASL(const AServiceName: String): TIdSASL;
  80. function Insert(Index: Integer): TIdSASLListEntry;
  81. procedure RemoveByComp(AComponent : TComponent);
  82. function IndexOfComp(AItem : TIdSASL): Integer;
  83. property Items[Index: Integer] : TIdSASLListEntry read GetItem write SetItem; default;
  84. property OwnerComponent: TComponent read GetOwnerComponent;
  85. end;
  86. EIdSASLException = class(EIdException);
  87. EIdSASLNotSupported = class(EIdSASLException);
  88. EIdSASLNotReady = class(EIdSASLException);
  89. EIdSASLMechNeeded = class(EIdSASLException);
  90. implementation
  91. uses
  92. {$IFDEF HAS_UNIT_Generics_Collections}
  93. System.Generics.Collections,
  94. {$ENDIF}
  95. IdAssignedNumbers,
  96. IdCoderMIME,
  97. IdGlobal,
  98. IdGlobalProtocols,
  99. IdReply,
  100. IdResourceStringsProtocols,
  101. SysUtils;
  102. { TIdSASLListEntry }
  103. procedure TIdSASLListEntry.Assign(Source: TPersistent);
  104. begin
  105. if Source is TIdSASLListEntry then begin
  106. SASL := TIdSASLListEntry(Source).SASL;
  107. end else begin
  108. inherited Assign(Source);
  109. end;
  110. end;
  111. function TIdSASLListEntry.GetDisplayName: String;
  112. var
  113. // under ARC, convert a weak reference to a strong reference before working with it
  114. LSASL: TIdSASL;
  115. begin
  116. LSASL := FSASL;
  117. if Assigned(LSASL) then begin
  118. Result := String(LSASL.ServiceName);
  119. end else begin
  120. Result := inherited GetDisplayName;
  121. end;
  122. end;
  123. function TIdSASLListEntry.GetOwnerComponent: TComponent;
  124. var
  125. LEntries: TIdSASLEntries;
  126. begin
  127. LEntries := SASLEntries;
  128. if Assigned(LEntries) then begin
  129. Result := LEntries.OwnerComponent;
  130. end else begin
  131. Result := nil;
  132. end;
  133. end;
  134. function TIdSASLListEntry.GetSASLEntries: TIdSASLEntries;
  135. begin
  136. if Collection is TIdSASLEntries then begin
  137. Result := TIdSASLEntries(Collection);
  138. end else begin
  139. Result := nil;
  140. end;
  141. end;
  142. procedure TIdSASLListEntry.SetSASL(AValue : TIdSASL);
  143. var
  144. LOwnerComp: TComponent;
  145. begin
  146. if FSASL <> AValue then begin
  147. // the component that owns the TIdSASLEntries collection must override
  148. // TComponent.Notification() to call TIdSASLEntries.RemoveByComp()...
  149. //
  150. // TODO: figure out a way to detect the free automatically. Maybe have
  151. // TIdSASL expose an event/callback that TIdSASLListEntry can subscribe to
  152. // to remove itself from the list...
  153. //
  154. LOwnerComp := OwnerComponent;
  155. if (FSASL <> nil) and (LOwnerComp <> nil) then begin
  156. FSASL.RemoveFreeNotification(LOwnerComp);
  157. end;
  158. FSASL := AValue;
  159. if (FSASL <> nil) and (LOwnerComp <> nil) then begin
  160. FSASL.FreeNotification(LOwnerComp);
  161. end;
  162. end;
  163. end;
  164. { TIdSASLEntries }
  165. // RLebeau 2/8/2013: WARNING!!! To work around a design limitation in the way
  166. // TIdIMAP4 implements SendCmd(), it cannot use TIdSASLEntries.LoginSASL() for
  167. // SASL authentication because the SASL commands sent in this unit will not end
  168. // up being IMAP-compatible! Until that can be addressed, any changes made to
  169. // PerformSASLLogin() or LoginSASL() in this unit need to be duplicated in the
  170. // IdIMAP4.pas unit for the TIdIMAP4.Login() method as well...
  171. function CheckStrFail(const AStr : String; const AOk, ACont: array of string) : Boolean;
  172. begin
  173. Result := (PosInStrArray(AStr, AOk) = -1) and
  174. (PosInStrArray(AStr, ACont) = -1);
  175. end;
  176. function PerformSASLLogin(const ACmd, AHost: string; const APort: TIdPort; const AProtocolName: String;
  177. ASASL: TIdSASL; AEncoder: TIdEncoder; ADecoder: TIdDecoder; const AOkReplies, AContinueReplies: array of string;
  178. AClient : TIdTCPConnection; ACanAttemptIR: Boolean): Boolean;
  179. var
  180. S: String;
  181. AuthStarted: Boolean;
  182. begin
  183. Result := False;
  184. AuthStarted := False;
  185. // TODO: handle ACanAttemptIR based on AProtocolName.
  186. //
  187. // SASL in SMTP and DICT supported Initial-Response from the beginning,
  188. // as should any new SASL-enabled protocol moving forward.
  189. //
  190. // SASL in IMAP did not originally support Initial-Response, but it was
  191. // added in RFC 4959 along with an explicit capability ('SASL-IR') to
  192. // indicate when Initial-Response is supported. SASL in IMAP is currently
  193. // handled by TIdIMAP4 directly, but should it be updated to use
  194. // TIdSASLEntries.LoginSASL() in the future then it will set the
  195. // ACanAttemptIR parameter accordingly.
  196. //
  197. // SASL in POP3 did not originally support Initial-Response. It was added
  198. // in RFC 2449 along with the CAPA command. If a server supports the CAPA
  199. // command then it *should* also support Initial-Response as well, however
  200. // many POP3 servers support CAPA but do not support Initial-Response
  201. // (which was formalized in RFC 5034). So, to handle that descrepency,
  202. // TIdPOP3 currently sets ACanAttemptIR to false. In the future, we could
  203. // let it set ACanAttemptIR to True instead, and then if Initial-Response
  204. // fails here for POP3 then re-attempt without Initial-Response before
  205. // exiting with a failure.
  206. if ACanAttemptIR then begin
  207. if ASASL.TryStartAuthenticate(AHost, APort, AProtocolName, S) then begin
  208. AClient.SendCmd(ACmd + ' ' + String(ASASL.ServiceName) + ' ' + AEncoder.Encode(S), []);//[334, 504]);
  209. if CheckStrFail(AClient.LastCmdResult.Code, AOkReplies, AContinueReplies) then begin
  210. // TODO: re-attempt without IR unconditionally? Or add a callback
  211. // to let the caller decide whether to re-attempt or not...
  212. if not TextIsSame(AProtocolName, IdGSKSSN_pop) then begin
  213. ASASL.FinishAuthenticate;
  214. Exit; // this mechanism is not supported
  215. end;
  216. end else begin
  217. AuthStarted := True;
  218. end;
  219. end;
  220. end;
  221. if not AuthStarted then begin
  222. AClient.SendCmd(ACmd + ' ' + String(ASASL.ServiceName), []);//[334, 504]);
  223. if CheckStrFail(AClient.LastCmdResult.Code, AOkReplies, AContinueReplies) then begin
  224. Exit; // this mechanism is not supported
  225. end;
  226. end;
  227. if (PosInStrArray(AClient.LastCmdResult.Code, AOkReplies) > -1) then begin
  228. if AuthStarted then begin
  229. ASASL.FinishAuthenticate;
  230. end;
  231. Result := True;
  232. Exit; // we've authenticated successfully :)
  233. end;
  234. // must be a continue reply...
  235. if not AuthStarted then begin
  236. S := ADecoder.DecodeString(TrimRight(AClient.LastCmdResult.Text.Text));
  237. S := ASASL.StartAuthenticate(S, AHost, APort, AProtocolName);
  238. AClient.SendCmd(AEncoder.Encode(S));
  239. if CheckStrFail(AClient.LastCmdResult.Code, AOkReplies, AContinueReplies) then
  240. begin
  241. ASASL.FinishAuthenticate;
  242. Exit;
  243. end;
  244. end;
  245. while PosInStrArray(AClient.LastCmdResult.Code, AContinueReplies) > -1 do begin
  246. S := ADecoder.DecodeString(TrimRight(AClient.LastCmdResult.Text.Text));
  247. S := ASASL.ContinueAuthenticate(S, AHost, APort, AProtocolName);
  248. AClient.SendCmd(AEncoder.Encode(S));
  249. if CheckStrFail(AClient.LastCmdResult.Code, AOkReplies, AContinueReplies) then
  250. begin
  251. ASASL.FinishAuthenticate;
  252. Exit;
  253. end;
  254. end;
  255. Result := (PosInStrArray(AClient.LastCmdResult.Code, AOkReplies) > -1);
  256. ASASL.FinishAuthenticate;
  257. end;
  258. function TIdSASLEntries.Add: TIdSASLListEntry;
  259. begin
  260. Result := TIdSASLListEntry(inherited Add);
  261. end;
  262. constructor TIdSASLEntries.Create(AOwner: TPersistent);
  263. begin
  264. inherited Create(AOwner, TIdSASLListEntry);
  265. end;
  266. procedure TIdSASLEntries.CheckIfEmpty;
  267. var
  268. I: Integer;
  269. begin
  270. for I := 0 to Count-1 do begin
  271. if Items[I].SASL <> nil then begin
  272. Exit;
  273. end;
  274. end;
  275. raise EIdSASLMechNeeded.Create(RSSASLRequired);
  276. end;
  277. function TIdSASLEntries.GetItem(Index: Integer): TIdSASLListEntry;
  278. begin
  279. Result := TIdSASLListEntry(inherited Items[Index]);
  280. end;
  281. function TIdSASLEntries.GetOwnerComponent: TComponent;
  282. var
  283. LOwner: TPersistent;
  284. begin
  285. LOwner := inherited GetOwner;
  286. if LOwner is TComponent then begin
  287. Result := TComponent(LOwner);
  288. end else begin
  289. Result := nil;
  290. end;
  291. end;
  292. function TIdSASLEntries.IndexOfComp(AItem: TIdSASL): Integer;
  293. begin
  294. for Result := 0 to Count -1 do
  295. begin
  296. if Items[Result].SASL = AItem then
  297. begin
  298. Exit;
  299. end;
  300. end;
  301. Result := -1;
  302. end;
  303. function TIdSASLEntries.Insert(Index: Integer): TIdSASLListEntry;
  304. begin
  305. Result := TIdSASLListEntry( inherited Insert(Index) );
  306. end;
  307. type
  308. {$IFDEF HAS_GENERICS_TList}
  309. TIdSASLList = TList<TIdSASL>;
  310. {$ELSE}
  311. // TODO: flesh out to match TList<TIdSASL> for non-Generics compilers
  312. TIdSASLList = TList;
  313. {$ENDIF}
  314. procedure TIdSASLEntries.LoginSASL(const ACmd, AHost: string; const APort: TIdPort;
  315. const AProtocolName: String; const AOkReplies, AContinueReplies: array of string;
  316. AClient: TIdTCPConnection; ACapaReply: TStrings; const AAuthString: String;
  317. ACanAttemptIR: Boolean);
  318. var
  319. i : Integer;
  320. LE : TIdEncoderMIME;
  321. LD : TIdDecoderMIME;
  322. LSupportedSASL : TStrings;
  323. LSASLList: TIdSASLList;
  324. LSASL : TIdSASL;
  325. LError : TIdReply;
  326. function SetupErrorReply: TIdReply;
  327. begin
  328. Result := TIdReplyClass(AClient.LastCmdResult.ClassType).Create(nil);
  329. Result.Assign(AClient.LastCmdResult);
  330. end;
  331. begin
  332. // make sure the collection is not empty
  333. CheckIfEmpty;
  334. //create a list of mechanisms that both parties support
  335. LSASLList := TIdSASLList.Create;
  336. try
  337. LSupportedSASL := TStringList.Create;
  338. try
  339. ParseCapaReplyToList(ACapaReply, LSupportedSASL, AAuthString);
  340. for i := Count-1 downto 0 do begin
  341. LSASL := Items[i].SASL;
  342. if LSASL <> nil then begin
  343. if not LSASL.IsAuthProtocolAvailable(LSupportedSASL) then begin
  344. Continue;
  345. end;
  346. if LSASLList.IndexOf(LSASL) = -1 then begin
  347. LSASLList.Add(LSASL);
  348. end;
  349. end;
  350. end;
  351. finally
  352. LSupportedSASL.Free;
  353. end;
  354. if LSASLList.Count = 0 then begin
  355. raise EIdSASLNotSupported.Create(RSSASLNotSupported);
  356. end;
  357. //now do it
  358. LE := nil;
  359. try
  360. LD := nil;
  361. try
  362. LError := nil;
  363. try
  364. for i := 0 to LSASLList.Count-1 do begin
  365. LSASL := {$IFDEF HAS_GENERICS_TList}LSASLList.Items[i]{$ELSE}TIdSASL(LSASLList.Items[i]){$ENDIF};
  366. if not LSASL.IsReadyToStart then begin
  367. Continue;
  368. end;
  369. if not Assigned(LE) then begin
  370. LE := TIdEncoderMIME.Create(nil);
  371. end;
  372. if not Assigned(LD) then begin
  373. LD := TIdDecoderMIME.Create(nil);
  374. end;
  375. if PerformSASLLogin(ACmd, AHost, APort, AProtocolName, LSASL, LE, LD, AOkReplies, AContinueReplies, AClient, ACanAttemptIR) then begin
  376. Exit;
  377. end;
  378. if not Assigned(LError) then begin
  379. LError := SetupErrorReply;
  380. end;
  381. end;
  382. if Assigned(LError) then begin
  383. LError.RaiseReplyError;
  384. end else begin
  385. raise EIdSASLNotReady.Create(RSSASLNotReady);
  386. end;
  387. finally
  388. LError.Free;
  389. end;
  390. finally
  391. LD.Free;
  392. end;
  393. finally
  394. LE.Free;
  395. end;
  396. finally
  397. LSASLList.Free;
  398. end;
  399. end;
  400. procedure TIdSASLEntries.LoginSASL(const ACmd, AHost: string; APort: TIdPort;
  401. const AProtocolName, AServiceName: String; const AOkReplies, AContinueReplies: array of string;
  402. AClient: TIdTCPConnection; ACapaReply: TStrings; const AAuthString: String;
  403. ACanAttemptIR: Boolean);
  404. var
  405. LE : TIdEncoderMIME;
  406. LD : TIdDecoderMIME;
  407. LSupportedSASL : TStrings;
  408. LSASL : TIdSASL;
  409. begin
  410. LSASL := nil;
  411. // make sure the collection is not empty
  412. CheckIfEmpty;
  413. //determine if both parties support the same mechanism
  414. LSupportedSASL := TStringList.Create;
  415. try
  416. ParseCapaReplyToList(ACapaReply, LSupportedSASL, AAuthString);
  417. if LSupportedSASL.IndexOf(AServiceName) <> -1 then begin
  418. LSASL := FindSASL(AServiceName);
  419. end;
  420. finally
  421. LSupportedSASL.Free;
  422. end;
  423. if LSASL = nil then begin
  424. raise EIdSASLNotSupported.Create(RSSASLNotSupported);
  425. end;
  426. if not LSASL.IsReadyToStart then begin
  427. raise EIdSASLNotReady.Create(RSSASLNotReady);
  428. end;
  429. //now do it
  430. LE := TIdEncoderMIME.Create(nil);
  431. try
  432. LD := TIdDecoderMIME.Create(nil);
  433. try
  434. if not PerformSASLLogin(ACmd, AHost, APort, AProtocolName, LSASL, LE, LD, AOkReplies, AContinueReplies, AClient, ACanAttemptIR) then begin
  435. AClient.RaiseExceptionForLastCmdResult;
  436. end;
  437. finally
  438. LD.Free;
  439. end;
  440. finally
  441. LE.Free;
  442. end;
  443. end;
  444. procedure TIdSASLEntries.ParseCapaReplyToList(ACapaReply, ADestList: TStrings;
  445. const AAuthString: String = 'AUTH'); {do not localize}
  446. const
  447. VALIDDELIMS: String = ' ='; {Do not Localize}
  448. var
  449. i: Integer;
  450. s: string;
  451. LEntry : String;
  452. begin
  453. if ACapaReply = nil then begin
  454. Exit;
  455. end;
  456. ADestList.BeginUpdate;
  457. try
  458. for i := 0 to ACapaReply.Count - 1 do
  459. begin
  460. s := ACapaReply[i];
  461. if TextStartsWith(s, AAuthString) and CharIsInSet(s, Length(AAuthString)+1, VALIDDELIMS) then
  462. begin
  463. s := UpperCase(Copy(s, Length(AAuthString)+1, MaxInt));
  464. s := ReplaceAll(s, '=', ' '); {Do not Localize}
  465. while s <> '' do
  466. begin
  467. LEntry := Fetch(s, ' '); {Do not Localize}
  468. if LEntry <> '' then
  469. begin
  470. if ADestList.IndexOf(LEntry) = -1 then begin
  471. ADestList.Add(LEntry);
  472. end;
  473. end;
  474. end;
  475. end;
  476. end;
  477. finally
  478. ADestList.EndUpdate;
  479. end;
  480. end;
  481. function TIdSASLEntries.FindSASL(const AServiceName: String): TIdSASL;
  482. var
  483. i: Integer;
  484. LEntry: TIdSASLListEntry;
  485. begin
  486. Result := nil;
  487. For i := 0 to Count-1 do begin
  488. LEntry := Items[i];
  489. if LEntry.SASL <> nil then begin
  490. if TextIsSame(String(LEntry.SASL.ServiceName), AServiceName) then begin
  491. Result := LEntry.SASL;
  492. Exit;
  493. end;
  494. end;
  495. end;
  496. end;
  497. procedure TIdSASLEntries.RemoveByComp(AComponent: TComponent);
  498. var
  499. i : Integer;
  500. begin
  501. for i := Count-1 downto 0 do
  502. begin
  503. if Items[i].SASL = AComponent then begin
  504. Delete(i);
  505. end;
  506. end;
  507. end;
  508. procedure TIdSASLEntries.SetItem(Index: Integer; const Value: TIdSASLListEntry);
  509. begin
  510. inherited SetItem(Index, Value);
  511. end;
  512. end.