{ $Project$ $Workfile$ $Revision$ $DateUTC$ $Id$ This file is part of the Indy (Internet Direct) project, and is offered under the dual-licensing agreement described on the Indy website. (http://www.indyproject.org/) Copyright: (c) 1993-2005, Chad Z. Hower and the Indy Pit Crew. All rights reserved. } { $Log$ } { Rev 1.5 11/27/2004 8:27:14 PM JPMugaas Fix for compiler errors. Rev 1.4 11/27/04 2:56:40 AM RLebeau Added support for overloaded version of LoginSASL(). Added GetDisplayName() method to TIdSASLListEntry, and FindSASL() method to TIdSASLEntries. Rev 1.3 10/26/2004 10:55:32 PM JPMugaas Updated refs. Rev 1.2 6/11/2004 9:38:38 AM DSiders Added "Do not Localize" comments. Rev 1.1 2004.02.03 5:45:50 PM czhower Name changes Rev 1.0 1/25/2004 3:09:54 PM JPMugaas New collection class for SASL mechanism processing. } unit IdSASLCollection; interface {$i IdCompilerDefines.inc} uses Classes, IdBaseComponent, IdCoder, IdException, IdSASL, IdTCPConnection; type TIdSASLEntries = class; TIdSASLListEntry = class(TCollectionItem) protected {$IFDEF USE_OBJECT_ARC}[Weak]{$ENDIF} FSASL : TIdSASL; function GetDisplayName: String; override; function GetOwnerComponent: TComponent; function GetSASLEntries: TIdSASLEntries; procedure SetSASL(AValue : TIdSASL); public procedure Assign(Source: TPersistent); override; property OwnerComponent: TComponent read GetOwnerComponent; property SASLEntries: TIdSASLEntries read GetSASLEntries; published property SASL : TIdSASL read FSASL write SetSASL; end; TIdSASLEntries = class ( TOwnedCollection ) protected procedure CheckIfEmpty; function GetItem(Index: Integer) : TIdSASLListEntry; function GetOwnerComponent: TComponent; procedure SetItem(Index: Integer; const Value: TIdSASLListEntry); public constructor Create ( AOwner : TPersistent ); reintroduce; function Add: TIdSASLListEntry; procedure LoginSASL(const ACmd, AHost, AProtocolName: String; const AOkReplies, AContinueReplies: array of string; AClient : TIdTCPConnection; ACapaReply : TStrings; const AAuthString : String = 'AUTH'; {Do not Localize} ACanAttemptIR: Boolean = True); overload; procedure LoginSASL(const ACmd, AHost, AProtocolName, AServiceName: String; const AOkReplies, AContinueReplies: array of string; AClient : TIdTCPConnection; ACapaReply : TStrings; const AAuthString : String = 'AUTH'; {Do not Localize} ACanAttemptIR: Boolean = True); overload; function ParseCapaReply(ACapaReply: TStrings; const AAuthString: String = 'AUTH') : TStrings; {$IFDEF HAS_DEPRECATED}deprecated{$IFDEF HAS_DEPRECATED_MSG} 'Use ParseCapaReplyToList()'{$ENDIF};{$ENDIF} {do not localize} procedure ParseCapaReplyToList(ACapaReply, ADestList: TStrings; const AAuthString: String = 'AUTH'); {do not localize} function FindSASL(const AServiceName: String): TIdSASL; function Insert(Index: Integer): TIdSASLListEntry; procedure RemoveByComp(AComponent : TComponent); function IndexOfComp(AItem : TIdSASL): Integer; property Items[Index: Integer] : TIdSASLListEntry read GetItem write SetItem; default; property OwnerComponent: TComponent read GetOwnerComponent; end; EIdSASLException = class(EIdException); EIdSASLNotSupported = class(EIdSASLException); EIdSASLNotReady = class(EIdSASLException); EIdSASLMechNeeded = class(EIdSASLException); implementation uses {$IFDEF HAS_UNIT_Generics_Collections} System.Generics.Collections, {$ENDIF} IdCoderMIME, IdGlobal, IdGlobalProtocols, IdReply, IdResourceStringsProtocols, SysUtils; { TIdSASLListEntry } procedure TIdSASLListEntry.Assign(Source: TPersistent); begin if Source is TIdSASLListEntry then begin SASL := TIdSASLListEntry(Source).SASL; end else begin inherited Assign(Source); end; end; function TIdSASLListEntry.GetDisplayName: String; begin if FSASL <> nil then begin Result := String(FSASL.ServiceName); end else begin Result := inherited GetDisplayName; end; end; function TIdSASLListEntry.GetOwnerComponent: TComponent; var LEntries: TIdSASLEntries; begin LEntries := SASLEntries; if Assigned(LEntries) then begin Result := LEntries.OwnerComponent; end else begin Result := nil; end; end; function TIdSASLListEntry.GetSASLEntries: TIdSASLEntries; begin if Collection is TIdSASLEntries then begin Result := TIdSASLEntries(Collection); end else begin Result := nil; end; end; procedure TIdSASLListEntry.SetSASL(AValue : TIdSASL); var LOwnerComp: TComponent; begin if FSASL <> AValue then begin LOwnerComp := OwnerComponent; if (FSASL <> nil) and (LOwnerComp <> nil) then begin FSASL.RemoveFreeNotification(LOwnerComp); end; FSASL := AValue; if (FSASL <> nil) and (LOwnerComp <> nil) then begin FSASL.FreeNotification(LOwnerComp); end; end; end; { TIdSASLEntries } // RLebeau 2/8/2013: WARNING!!! To work around a design limitation in the way // TIdIMAP4 implements SendCmd(), it cannot use TIdSASLEntries.LoginSASL() for // SASL authentication because the SASL commands sent in this unit will not end // up being IMAP-compatible! Until that can be addressed, any changes made to // PerformSASLLogin() or LoginSASL() in this unit need to be duplicated in the // IdIMAP4.pas unit for the TIdIMAP4.Login() method as well... function CheckStrFail(const AStr : String; const AOk, ACont: array of string) : Boolean; begin Result := (PosInStrArray(AStr, AOk) = -1) and (PosInStrArray(AStr, ACont) = -1); end; function PerformSASLLogin(const ACmd, AHost, AProtocolName: String; ASASL: TIdSASL; AEncoder: TIdEncoder; ADecoder: TIdDecoder; const AOkReplies, AContinueReplies: array of string; AClient : TIdTCPConnection; ACanAttemptIR: Boolean): Boolean; var S: String; AuthStarted: Boolean; begin Result := False; AuthStarted := False; // TODO: handle ACanAttemptIR based on AProtocolName. // // SASL in SMTP and DICT supported Initial-Response from the beginning, // as should any new SASL-enabled protocol moving forward. // // SASL in IMAP did not originally support Initial-Response, but it was // added in RFC 4959 along with an explicit capability ('SASL-IR') to // indicate when Initial-Response is supported. SASL in IMAP is currently // handled by TIdIMAP4 directly, but should it be updated to use // TIdSASLEntries.LoginSASL() in the future then it will set the // ACanAttemptIR parameter accordingly. // // SASL in POP3 did not originally support Initial-Response. It was added // in RFC 2449 along with the CAPA command. If a server supports the CAPA // command then it *should* also support Initial-Response as well, however // many POP3 servers support CAPA but do not support Initial-Response // (which was formalized in RFC 5034). So, to handle that descrepency, // TIdPOP3 currently sets ACanAttemptIR to false. In the future, we could // let it set ACanAttemptIR to True instead, and then if Initial-Response // fails here for POP3 then re-attempt without Initial-Response before // exiting with a failure. if ACanAttemptIR then begin if ASASL.TryStartAuthenticate(AHost, AProtocolName, S) then begin AClient.SendCmd(ACmd + ' ' + String(ASASL.ServiceName) + ' ' + AEncoder.Encode(S), []);//[334, 504]); if CheckStrFail(AClient.LastCmdResult.Code, AOkReplies, AContinueReplies) then begin ASASL.FinishAuthenticate; Exit; // this mechanism is not supported end; AuthStarted := True; end; end; if not AuthStarted then begin AClient.SendCmd(ACmd + ' ' + String(ASASL.ServiceName), []);//[334, 504]); if CheckStrFail(AClient.LastCmdResult.Code, AOkReplies, AContinueReplies) then begin Exit; // this mechanism is not supported end; end; if (PosInStrArray(AClient.LastCmdResult.Code, AOkReplies) > -1) then begin if AuthStarted then begin ASASL.FinishAuthenticate; end; Result := True; Exit; // we've authenticated successfully :) end; // must be a continue reply... if not AuthStarted then begin S := ADecoder.DecodeString(TrimRight(AClient.LastCmdResult.Text.Text)); S := ASASL.StartAuthenticate(S, AHost, AProtocolName); AClient.SendCmd(AEncoder.Encode(S)); if CheckStrFail(AClient.LastCmdResult.Code, AOkReplies, AContinueReplies) then begin ASASL.FinishAuthenticate; Exit; end; end; while PosInStrArray(AClient.LastCmdResult.Code, AContinueReplies) > -1 do begin S := ADecoder.DecodeString(TrimRight(AClient.LastCmdResult.Text.Text)); S := ASASL.ContinueAuthenticate(S, AHost, AProtocolName); AClient.SendCmd(AEncoder.Encode(S)); if CheckStrFail(AClient.LastCmdResult.Code, AOkReplies, AContinueReplies) then begin ASASL.FinishAuthenticate; Exit; end; end; Result := (PosInStrArray(AClient.LastCmdResult.Code, AOkReplies) > -1); ASASL.FinishAuthenticate; end; function TIdSASLEntries.Add: TIdSASLListEntry; begin Result := TIdSASLListEntry(inherited Add); end; constructor TIdSASLEntries.Create(AOwner: TPersistent); begin inherited Create(AOwner, TIdSASLListEntry); end; procedure TIdSASLEntries.CheckIfEmpty; var I: Integer; begin for I := 0 to Count-1 do begin if Items[I].SASL <> nil then begin Exit; end; end; raise EIdSASLMechNeeded.Create(RSSASLRequired); end; function TIdSASLEntries.GetItem(Index: Integer): TIdSASLListEntry; begin Result := TIdSASLListEntry(inherited Items[Index]); end; function TIdSASLEntries.GetOwnerComponent: TComponent; var LOwner: TPersistent; begin LOwner := inherited GetOwner; if LOwner is TComponent then begin Result := TComponent(LOwner); end else begin Result := nil; end; end; function TIdSASLEntries.IndexOfComp(AItem: TIdSASL): Integer; begin for Result := 0 to Count -1 do begin if Items[Result].SASL = AItem then begin Exit; end; end; Result := -1; end; function TIdSASLEntries.Insert(Index: Integer): TIdSASLListEntry; begin Result := TIdSASLListEntry( inherited Insert(Index) ); end; type {$IFDEF HAS_GENERICS_TList} TIdSASLList = TList; {$ELSE} // TODO: flesh out to match TList for non-Generics compilers TIdSASLList = TList; {$ENDIF} procedure TIdSASLEntries.LoginSASL(const ACmd, AHost, AProtocolName: String; const AOkReplies, AContinueReplies: array of string; AClient: TIdTCPConnection; ACapaReply: TStrings; const AAuthString: String; ACanAttemptIR: Boolean); var i : Integer; LE : TIdEncoderMIME; LD : TIdDecoderMIME; LSupportedSASL : TStrings; LSASLList: TIdSASLList; LSASL : TIdSASL; LError : TIdReply; function SetupErrorReply: TIdReply; begin Result := TIdReplyClass(AClient.LastCmdResult.ClassType).Create(nil); Result.Assign(AClient.LastCmdResult); end; begin // make sure the collection is not empty CheckIfEmpty; //create a list of mechanisms that both parties support LSASLList := TIdSASLList.Create; try LSupportedSASL := TStringList.Create; try ParseCapaReplyToList(ACapaReply, LSupportedSASL, AAuthString); for i := Count-1 downto 0 do begin LSASL := Items[i].SASL; if LSASL <> nil then begin if not LSASL.IsAuthProtocolAvailable(LSupportedSASL) then begin Continue; end; if LSASLList.IndexOf(LSASL) = -1 then begin LSASLList.Add(LSASL); end; end; end; finally FreeAndNil(LSupportedSASL); end; if LSASLList.Count = 0 then begin raise EIdSASLNotSupported.Create(RSSASLNotSupported); end; //now do it LE := nil; try LD := nil; try LError := nil; try for i := 0 to LSASLList.Count-1 do begin LSASL := {$IFDEF HAS_GENERICS_TList}LSASLList.Items[i]{$ELSE}TIdSASL(LSASLList.Items[i]){$ENDIF}; if not LSASL.IsReadyToStart then begin Continue; end; if not Assigned(LE) then begin LE := TIdEncoderMIME.Create(nil); end; if not Assigned(LD) then begin LD := TIdDecoderMIME.Create(nil); end; if PerformSASLLogin(ACmd, AHost, AProtocolName, LSASL, LE, LD, AOkReplies, AContinueReplies, AClient, ACanAttemptIR) then begin Exit; end; if not Assigned(LError) then begin LError := SetupErrorReply; end; end; if Assigned(LError) then begin LError.RaiseReplyError; end else begin raise EIdSASLNotReady.Create(RSSASLNotReady); end; finally FreeAndNil(LError); end; finally FreeAndNil(LD); end; finally FreeAndNil(LE); end; finally FreeAndNil(LSASLList); end; end; procedure TIdSASLEntries.LoginSASL(const ACmd, AHost, AProtocolName, AServiceName: String; const AOkReplies, AContinueReplies: array of string; AClient: TIdTCPConnection; ACapaReply: TStrings; const AAuthString: String; ACanAttemptIR: Boolean); var LE : TIdEncoderMIME; LD : TIdDecoderMIME; LSupportedSASL : TStrings; LSASL : TIdSASL; begin LSASL := nil; // make sure the collection is not empty CheckIfEmpty; //determine if both parties support the same mechanism LSupportedSASL := TStringList.Create; try ParseCapaReplyToList(ACapaReply, LSupportedSASL, AAuthString); if LSupportedSASL.IndexOf(AServiceName) <> -1 then begin LSASL := FindSASL(AServiceName); end; finally FreeAndNil(LSupportedSASL); end; if LSASL = nil then begin raise EIdSASLNotSupported.Create(RSSASLNotSupported); end; if not LSASL.IsReadyToStart then begin raise EIdSASLNotReady.Create(RSSASLNotReady); end; //now do it LE := TIdEncoderMIME.Create(nil); try LD := TIdDecoderMIME.Create(nil); try if not PerformSASLLogin(ACmd, AHost, AProtocolName, LSASL, LE, LD, AOkReplies, AContinueReplies, AClient, ACanAttemptIR) then begin AClient.RaiseExceptionForLastCmdResult; end; finally FreeAndNil(LD); end; finally FreeAndNil(LE); end; end; {$I IdDeprecatedImplBugOff.inc} function TIdSASLEntries.ParseCapaReply(ACapaReply: TStrings; const AAuthString: String): TStrings; {$I IdDeprecatedImplBugOn.inc} begin Result := TStringList.Create; try ParseCapaReplyToList(ACapaReply, Result, AAuthString); except FreeAndNil(Result); raise; end; end; procedure TIdSASLEntries.ParseCapaReplyToList(ACapaReply, ADestList: TStrings; const AAuthString: String = 'AUTH'); {do not localize} const VALIDDELIMS: String = ' ='; {Do not Localize} var i: Integer; s: string; LEntry : String; begin if ACapaReply = nil then begin Exit; end; ADestList.BeginUpdate; try for i := 0 to ACapaReply.Count - 1 do begin s := ACapaReply[i]; if TextStartsWith(s, AAuthString) and CharIsInSet(s, Length(AAuthString)+1, VALIDDELIMS) then begin s := UpperCase(Copy(s, Length(AAuthString)+1, MaxInt)); s := ReplaceAll(s, '=', ' '); {Do not Localize} while Length(s) > 0 do begin LEntry := Fetch(s, ' '); {Do not Localize} if LEntry <> '' then begin if ADestList.IndexOf(LEntry) = -1 then begin ADestList.Add(LEntry); end; end; end; end; end; finally ADestList.EndUpdate; end; end; function TIdSASLEntries.FindSASL(const AServiceName: String): TIdSASL; var i: Integer; LEntry: TIdSASLListEntry; begin Result := nil; For i := 0 to Count-1 do begin LEntry := Items[i]; if LEntry.SASL <> nil then begin if TextIsSame(String(LEntry.SASL.ServiceName), AServiceName) then begin Result := LEntry.SASL; Exit; end; end; end; end; procedure TIdSASLEntries.RemoveByComp(AComponent: TComponent); var i : Integer; begin for i := Count-1 downto 0 do begin if Items[i].SASL = AComponent then begin Delete(i); end; end; end; procedure TIdSASLEntries.SetItem(Index: Integer; const Value: TIdSASLListEntry); begin inherited SetItem(Index, Value); end; end.