Search code examples
httpdelphicookiesindy

IdCookieManager cut part of server cookie which contains quotechar


There's a problem with IdCookieManager. When server returns cookie where value contains ", it recognize first occurrence of " as end of value. You can reproduce it easily with next code:

procedure TSomeObject.Test;
var
  HTTP: TIdHTTP;
  Cookie: TIdCookieManager;
  i: Integer;
begin
  HTTP := TIdHTTP.Create(nil);
  Cookie := TIdCookieManager.Create(HTTP);
  HTTP.CookieManager := Cookie;
  HTTP.HandleRedirects := True;

  HTTP.Get('http://httpbin.org/cookies/set?test_cookie1=' +
    TIdURI.ParamsEncode('{"key": 123}') + '&test_cookie2=&test_cookie3=value');

  for i := 0 to Cookie.CookieCollection.Count - 1 do
    Form1.Memo1.Lines.Add(Cookie.CookieCollection[i].CookieName + ' = ' +
      Cookie.CookieCollection[i].Value);

  HTTP.Free;
end;

Digging into Indy sources I found that problem is in TIdCookie.ParseServerCookie(). It uses IdGlobal.Fetch() to extract value between quotes and ... it does what it does.

Would you recommend me how to let it parse whole value?


Solution

  • There's no neat solution (at least I haven't found one) ,so I've added OnHeadersAvailable event listener to TIdHTTP which rewrites cookie value after TIdCookie.ParseServerCookie() executed and replace previously parsed cookies by adding new cookie with same name to TIdCookieManager.

    Code (DO NOT USE THIS HANDLER, THERE'S ONE MORE BELOW):

    procedure TSomeObject.FixCookies(Sender: TObject; AHeaders: TIdHeaderList;
    var VContinue: Boolean);
    const
      CookieDelimiter = ';';
      QuoteChar = '"';
      SpaceChar = ' ';
    var
      RawHeader, RawCookie, RawCookieValue: string;
      i, CookieDelimiterPos, CookieNamePos, CookieValuePos: Integer;
      Cookie: TIdCookie;
    begin
      for i := 0 to AHeaders.Count - 1 do
      begin
        RawHeader := AHeaders[i];
        if Pos('Set-Cookie', RawHeader) = 1 then  // starts with "Set-Cookie"
        begin
          for CookieNamePos := Length('Set-Cookie') + 2 to Length(RawHeader) do
            if RawHeader[CookieNamePos] <> SpaceChar then
              Break;
    
          RawCookie := Copy(RawHeader, CookieNamePos,
            Length(RawHeader) - CookieNamePos + 1);
          CookieDelimiterPos := Pos(CookieDelimiter, RawCookie);
          CookieValuePos := Pos('=', RawCookie);
    
          if (CookieDelimiterPos > 0) and (CookieValuePos > 0) then
          begin
            RawCookieValue := Copy(RawCookie, CookieValuePos + 1,
              CookieDelimiterPos - CookieValuePos - 1);
            if (Length(RawCookieValue) > 0) and (RawCookieValue[1] = QuoteChar) and
              (RawCookieValue[Length(RawCookieValue)] = QuoteChar) then
              RawCookieValue := Copy(RawCookieValue, 2, Length(RawCookieValue) - 2);
    
            with (Sender as TIdHTTP) do
            begin
              Cookie := TIdCookie.Create(nil);
              if Cookie.ParseServerCookie(RawCookie, URL) then
              begin
                Cookie.Value := RawCookieValue;
                CookieManager.CookieCollection.AddCookie(Cookie, URL);
              end
              else
                Cookie.Free;
            end;
          end;
        end;
      end;
    
      VContinue := True;
    end;
    

    Usage:

    HTTP.OnHeadersAvailable := FixCookies; // Set it after object initialization
    

    P.S. I haven't used delphi for long time, so I'm sure that code isn't perfect and lot of improvements could be done (welcome to comments), but it works.


    I have no clue how it happened but using this event handler somehow brakes TIdCookieManager and Cookie.CookieCollection.Count returns 0 while there're cookies in collection which successfully added in further requests..

    I have no mood to spend more time digging into Indy sources so I changed handler to modify existing cookies instead of replacing them. It doesn't brake anything (probably):

    procedure TSomeObject.FixCookies(Sender: TObject; AHeaders: TIdHeaderList;
      var VContinue: Boolean);
    const
      CookieDelimiter = ';';
      QuoteChar = '"';
      SpaceChar = ' ';
    var
      RawHeader, RawCookie, RawCookieValue, RawCookieName: string;
      i, CookieDelimiterPos, CookieNamePos, CookieValuePos, CookieIndex: Integer;
    begin
      for i := 0 to AHeaders.Count - 1 do
      begin
        RawHeader := AHeaders[i];
        if Pos('Set-Cookie', RawHeader) = 1 then  // starts with "Set-Cookie"
        begin
          for CookieNamePos := Length('Set-Cookie') + 2 to Length(RawHeader) do
            if RawHeader[CookieNamePos] <> SpaceChar then
              Break;
    
          RawCookie := Copy(RawHeader, CookieNamePos,
            Length(RawHeader) - CookieNamePos + 1);
          CookieDelimiterPos := Pos(CookieDelimiter, RawCookie);
          CookieValuePos := Pos('=', RawCookie);
    
          if (CookieDelimiterPos > 0) and (CookieValuePos > 0) then
          begin
            RawCookieName := Copy(RawCookie, 1, CookieValuePos - 1);
            RawCookieValue := Copy(RawCookie, CookieValuePos + 1,
              CookieDelimiterPos - CookieValuePos - 1);
            if (Length(RawCookieValue) > 0) and (RawCookieValue[1] = QuoteChar) and
              (RawCookieValue[Length(RawCookieValue)] = QuoteChar) then
              RawCookieValue := Copy(RawCookieValue, 2, Length(RawCookieValue) - 2);
    
            with (Sender as TIdHTTP) do
            begin
              CookieIndex := CookieManager.CookieCollection.GetCookieIndex
                (RawCookieName);
              if CookieIndex >= 0 then
                CookieManager.CookieCollection[CookieIndex].Value := RawCookieValue
            end;
          end;
        end;
      end;
    
      VContinue := True;
    end;
    

    P.S. Are there any Indy developers? I just a bit confused how does first handler let CookieCollection to return zero.