Search code examples
delphicgiintraweb

Running a Delphi app with CGI and sent result to front end


I have a winCGI executable script returning a image. I am using Intraweb as server.

I have a custom web server made in delphi and I create a function to run the cgi and return the image.

The cgi is from a third party and I can't change your code. I have the next code returning from CGI when I query a image:

'Content-type: image/gif'#$A'Access-Control-Allow-Origin: *'#$A#$A'GIF89a@'#1'@'#1#0#0#0'!ÿ'#$B'NETSCAPE2.0'#3#1'ÿÿ'#0'!ù'#4#0'!'#0#0#0','#0#0#0#0'@'#1'@'#1'‡'#0#0#0#1#1#1#2#2#2#3#3#3#4#4#4#5#5#5#6#6#6#7#7#7#8#8#8#9#9#9#$A#$A#$A#$B#$B#$B#$C#$C#$C#$D#$D#$D#$E#$E#$E#$F#$F#$F#$10#$10#$10#$11#$11#$11#$12#$12#$12#$13#$13#$13#$14#$14#$14#$15#$15#$15#$16#$16#$16#$17#$17#$17#$18#$18#$18#$19#........

I need to send the image to a front end app in the browser.

<div>
 <img src="getImage(1)">
</div>

Here, getImage function takes the image from server, but not is showing, because I think the format I am returning the image from server to front end has something wrong. How could I fix the content text of the image on server to be a valid image in the front end?

function RunCGIOutput(CommandLine: string; Stream:TStream;Folder: string = ''): string;
const
  CReadBuffer = 2400;
var
  saSecurity: TSecurityAttributes;
  hRead: THandle;
  hWrite: THandle;
  suiStartup: TStartupInfo;
  piProcess: TProcessInformation;
  dRead: DWORD;
  Handle,WasOK:Boolean;
  Buffer: array[0..CReadBuffer] of AnsiChar;
  BytesRead: Cardinal;
  WorkingDirP,EnvBlock:PChar;

begin
  saSecurity.nLength := SizeOf(TSecurityAttributes);
  saSecurity.bInheritHandle := true;
  saSecurity.lpSecurityDescriptor := nil;
  EnvBlock := BuildEnvBlock(True);
  if Folder <> '' then WorkingDirP := PChar(Folder)
  else WorkingDirP := nil;
  if CreatePipe(hRead, hWrite, @saSecurity, 0) then
    try
      FillChar(suiStartup, SizeOf(TStartupInfo), #0);
      suiStartup.cb := SizeOf(TStartupInfo);
      suiStartup.hStdInput := hRead;
      suiStartup.hStdOutput := hWrite;
      suiStartup.hStdError := hWrite;
      suiStartup.dwFlags := STARTF_USESTDHANDLES or STARTF_USESHOWWINDOW;
      suiStartup.wShowWindow := SW_HIDE;
      Handle:=CreateProcess(nil, PChar(CommandLine), @saSecurity, @saSecurity, true, CREATE_UNICODE_ENVIRONMENT, EnvBlock, WorkingDirP, suiStartup,
        piProcess);

    CloseHandle(hWrite);
    if Handle then
      try
        repeat
              WasOK := ReadFile(hRead, Buffer, CReadBuffer, BytesRead, nil) and (BytesRead>0);
              if WasOK then
                Stream.WriteBuffer(Buffer, BytesRead);
        until not WasOK;
        WaitForSingleObject(piProcess.hProcess, INFINITE);
      finally
        CloseHandle(piProcess.hThread);
        CloseHandle(piProcess.hProcess);
      end;
  finally
    CloseHandle(hRead);
    StrDispose(EnvBlock);
  end;
end;


//Run CGI, take image and send to browser
function TContentImaqge.Execute(aRequest: THttpRequest; aReply: THttpReply;
  const aPathname: string; aSession: TIWApplication;
  aParams: TStrings): boolean;
var
  s,wresult,saida,LocalDoc:string;
  i:integer;
  Stream:TMemoryStream;
begin
    Result:=True;
    LocalDoc:=TIWAppInfo.GetAppPath + 'wwwroot\cgi-bin\newweb\dgate.exe';
    Stream:=TMemoryStream.Create;
    saida:=RunCGIOutput(LocalDoc,Stream,TIWAppInfo.GetAppPath + 'wwwroot\cgi-bin\newweb\');
    Stream.Position:=0;
    saida:=ReadStringFromStream(Stream, -1, IndyTextEncoding_OSDefault);
    with aReply do
     begin
       ResetReplyType;
       Code := 200;
       ContentType := MIME_GIF; // MIME_HTML;
       SendStream(Stream);
    end;
end;

Solution

  • Your CGI module generates raw HTTP response that should be transmitted as-is by your IW content handler. The response message consists of:

    1. Status line
    2. HTTP header lines
    3. An empty line separating headers and body
    4. Body (actual image data in your case)

    IntraWeb's THTTPReply doesn't seem to give you control over raw HTTP response. It provides dedicated interface to separately manipulate with status, headers and body. That's why you need to pre-process the stream returned from CGI and split headers apart from body by the empty line. Then you can transmit them via THTTPReply. Maybe you'd also like to whitelist only some headers and ignore the rest.

    In your code snippet you use ReadStringFromStream which is pointless, because you discard the returned value anyway. It moves the position of Stream to the end of the stream, but that doesn't matter, because aReply.SendStream(Stream) sends the whole stream content from the beginning.

    Another point is that you use IndyTextEncoding_OSDefault as the last parameter to ReadStringFromStream, which is probably wrong, because HTTP header is encoded in ASCII and therefore you should use IndyTextEncoding_ASCII.

    Try this code instead:

    function TContentImaqge.Execute(aRequest: THttpRequest; aReply: THttpReply;
      const aPathname: string; aSession: TIWApplication;
      aParams: TStrings): Boolean;
    var
      CGIOutput, ContentStream: TMemoryStream;
      LocalDoc, Line: string;
      CharPos: Integer;
    begin
      Result := True;
      CGIOutput := TMemoryStream.Create;
      try
        LocalDoc := TIWAppInfo.GetAppPath + 'wwwroot\cgi-bin\newweb\dgate.exe';
        RunCGIOutput(LocalDoc, CGIOutput, TIWAppInfo.GetAppPath + 'wwwroot\cgi-bin\newweb\');
    
        if ReadLnFromStream(CGIOutput, Line, -1, IndyTextEncoding_ASCII) then
        begin
          { process status line }
          CharPos := Pos(' ', Line);
          if CharPos > 0 then
          begin
            aReply.Code := StrToInt(Copy(Line, CharPos + 1, 3));
            CharPos := Pos(' ', Line, CharPos);
            if CharPos > 0 then
              aReply.CodeText := Copy(Line, CharPos + 1);
          end;
    
          { process headers (copy headers as they are) }
          while ReadLnFromStream(CGIOutput, Line, -1, IndyTextEncoding_ASCII) and (Line <> '') do
            aReply.Headers.Add(Line);
    
          { at this point CGIOutput.Position is at the beginning of body, so let's just copy
            the content to a separate memory stream }
          ContentStream := TMemoryStream.Create;
          try
            ContentStream.CopyFrom(CGIOutput, CGIOutput.Size - CGIOutput.Position);
          except
            ContentStream.Free;
            raise;
          end;
          aReply.SendStream(ContentStream);
        end
        else
        begin
          aReply.Code := 500;
          aReply.CodeText := RSHTTPInternalServerError;
          aReply.WriteString('CGI module returned malformed response.');
        end;
      finally
        CGIOutput.Free;
      end;
    end;
    

    Also please note that the CGI output you enclosed doesn't contain status line and starts straight with headers (Content-type: ...). If that's what you get from CGI then you should update the code to handle such a case.