2017-01-06 2 views
0

Я пытаюсь написать прокси-сервер в Indy для приема вызовов HTTPS от внешних клиентов и пересылки их в HTTP на другое серверное приложение на том же компьютере. Причина в том, что другое приложение не поддерживает SSL, поэтому я хочу, чтобы его трафик был на уровне SSL для внешней безопасности.Indy Proxy Server HTTPS to HTTP

Мой текущий подход заключается в использовании TIdHTTP-сервера с SSL IOHandler, а в обработчике OnCommandGet я создаю клиент TIdHTTP «на лету», который извлекает TFileStream ContentStream из внутреннего приложения и возвращает этот поток в качестве Response.ContentStream для внешний вызывающий.

Проблема с этим подходом заключается в задержке, вызванной необходимостью дожидаться, когда внутренний поток контента будет полностью принят до того, как внешний поток начнет посылаться. И, например, он не может работать для потокового мультимедиа.

Мой вопрос: есть ли лучший способ прокси HTTPS для HTTP, который будет работать для потоков? I.e без использования промежуточного файлового потока.

ответ

1

Если запрашивающий клиент поддерживает HTTP 1.1 chunking (см. RFC 2616 Section 3.6.1), это позволит вам считывать данные с целевого сервера и немедленно отправлять его клиенту в режиме реального времени.

Если вы используете достаточно свежую версию Indy, TIdHTTP имеет OnChunkReceived событие, и hoNoReadChunked флаг в HTTPOptions собственности:

New TIdHTTP flags and OnChunkReceived event

В обработчике событий TIdHTTPServer.OnCommand..., вы можете заполнить AResponseInfo по мере необходимости. Убедитесь в том, чтобы:

  • отпуск AResponseInfo.ContentText и AResponseInfo.ContentStream неназначенной

  • набор AResponseInfo.ContentLength до 0

  • установить AResponseInfo.TransferEncoding в 'chunked'

Затем вызовите метод AResponseInfo.WriteHeader() непосредственно, например, в TIdHTTP.OnHeadersRecceived событие, чтобы отправить заголовки ответов клиенту.

Затем вы можете прочитать тело ответа целевого сервера с помощью OnChunkedReceived или hoNoReadChunked и записать каждый полученный фрагмент клиенту, используя непосредственно AContext.Connection.IOHandler.

Однако, есть некоторые оговорки к этому:

  • если вы используете TIdHTTP.OnChunkReceived событие, вы все равно должны обеспечить выход TStream к TIdHTTP или иначе событие не срабатывает (это ограничение может быть удален в будущей версии). Однако вы можете использовать TIdEventStream без назначения обработчику событий OnWrite. Или напишите пользовательский класс TStream, который переопределяет виртуальный метод Write(), чтобы ничего не делать. Или просто используйте любой нужный TStream, и обработчик события OnChunkReceived очистит принятый Chunk, поэтому ничего не существует, чтобы написать TStream.

  • если вы используете hoNoReadChunked флаг, это позволяет вручную читать HTTP ломти из TIdHTTP.IOHandler непосредственно после TIdHTTP выходов. Просто убедитесь, что вы активируете HTTP-авиалинии, иначе TIdHTTP закроет соединение с сервером, прежде чем у вас появится возможность прочитать тело ответа сервера.

Если вы используете более старую версию Indy или если целевой сервер не поддерживает блокирование, все не потеряно. Вы должны иметь возможность написать собственный класс TStream, который перезаписывает виртуальный метод Write() для записи предоставленного блока данных клиенту в виде блока HTTP. И тогда вы можете использовать этот класс как выход TStream для TIdHTTP.

Если клиент не поддерживает HTTP-коммутацию, или если эти подходы не сработают для вас, то вам, скорее всего, придется прибегать к использованию TIdTCPServer непосредственно вместо TIdHTTPServer и реализовать весь HTTP-протокол самостоятельно с нуля, тогда вы можете обрабатывайте свои собственные потоки по мере необходимости. Посмотрите на исходный код для TIdHTTPProxyServer для некоторых идей (TIdHTTPProxyServer сам по себе не подходит для вашей конкретной ситуации, но он покажет вам, как передавать HTTP-запросы/ответы между соединениями почти в режиме реального времени в целом).

0

Благодарим вас за исчерпывающий ответ. То, как я, наконец, решил это, - создать потомка TStream для использования в качестве ContentStream для ответа сервера. TStream представляет собой обертку вокруг TIdTcpClient, которая содержит рудиментарную реализацию HTTP и функция TStream.Read извлекает содержимое HTTP для соединения Tcp.

type 
    TTcpSocketStream = class(TStream) 
    private 
    FAuthorization: string; 
    FBuffer: TBytes; 
    FBytesRead: Int64; 
    FCommand: string; 
    FContentLength: Int64; 
    FContentType: string; 
    FDocument: string; 
    FHeaders: TIdHeaderList; 
    FHost: string; 
    FIntercept: TServerLogEvent; 
    FPort: Integer; 
    FResponseCode: Integer; 
    FQueryParams: string; 
    FTcpClient: TIdTCPClient; 
    FWwwAuthenticate: string; 
    public 
    constructor Create; 
    destructor Destroy; override; 
    procedure Initialize; 
    function Read(var Buffer; Count: Longint): Longint; override; 
    function Seek(const Offset: Int64; Origin: TSeekOrigin): Int64; override; 
    property Authorization: string read FAuthorization write FAuthorization; 
    property Command: string read FCommand write FCommand; 
    property ContentType: string read FContentType; 
    property ContentLength: Int64 read FContentLength; 
    property Document: string read FDocument write FDocument; 
    property Host: string read fHost write FHost; 
    property Intercept: TServerLogEvent read FIntercept write FIntercept; 
    property Port: Integer read FPort write FPort; 
    property QueryParams: string read FQueryParams write FQueryParams; 
    property ResponseCode: Integer read FResponseCode; 
    property WWWAuthenticate: string read FWwwAuthenticate 
     write FWwwAuthenticate; 
    end; 

const 
    crlf = #13#10; 
    cContentSeparator = crlf+crlf; 

implementation 

{ TTcpSocketStream } 

constructor TTcpSocketStream.Create; 
begin 
    inherited; 

    FHeaders := TIdHeaderList.Create(QuoteHTTP); 
    FTcpClient := TIdTcpClient.Create(nil); 
    FTcpClient.ConnectTimeout := 5000; 
    FTcpClient.ReadTimeout := 5000; 

    FCommand := 'GET'; 
    FPort := 443; 
    FResponseCode := 404; 
end; 

destructor TTcpSocketStream.Destroy; 
begin 
    if FTcpClient.Connected then 
    FTcpClient.Disconnect; 

    if FTcpClient.Intercept <> nil then 
    begin 
    FTcpClient.Intercept.Free; 
    FTcpClient.Intercept := nil; 
    end; 

    FTcpClient.Free; 
    FHeaders.Free; 
    SetLength(FBuffer, 0); 

    inherited; 
end; 

procedure TTcpSocketStream.Initialize; 
var 
    s: string; 
    LLog: TClientLogEvent; 
    LRespText: string; 
begin 
    try 
    if FQueryParams <> '' then 
     FQueryParams := '?' + FQueryParams; 

    FTcpClient.Port := FPort; 
    FTcpClient.Host := FHost; 

    if FIntercept <> nil then 
    begin 
     LLog := TClientLogEvent.Create; 
     LLog.OnLog := FIntercept.OnLog; 
     FTcpClient.Intercept := LLog; 
    end; 

    FTcpClient.Connect; 
    if FTcpClient.Connected then 
    begin 

     FTcpClient.IOHandler.Writeln(Format('%s %s%s HTTP/1.1', 
     [FCommand, FDocument, FQueryParams])); 
     FTcpClient.IOHandler.Writeln('Accept: */*'); 
     if FAuthorization <> '' then 
     FTcpClient.IOHandler.Writeln(Format('Authorization: %s', 
      [FAuthorization])); 
     FTcpClient.IOHandler.Writeln('Connection: Close'); 
     FTcpClient.IOHandler.Writeln(Format('Host: %s:%d', [FHost, FPort])); 
     FTcpClient.IOHandler.Writeln('User-Agent: Whitebear SSL Proxy'); 
     FTcpClient.IOHandler.Writeln(''); 

     LRespText := FTcpClient.IOHandler.ReadLn; 
     s := LRespText; 
     Fetch(s); 
     s := Trim(s); 
     FResponseCode := StrToIntDef(Fetch(s, ' ', False), -1); 

     repeat 
     try 
      s := FTcpClient.IOHandler.ReadLn; 
     except 
      on Exception do 
      break; 
     end; 
     if s <> '' then 
      FHeaders.Add(s); 
     until s = ''; 

     FContentLength := StrToInt64Def(FHeaders.Values['Content-Length'], -1); 
     FContentType := FHeaders.Values['Content-Type']; 
     FWwwAuthenticate := FHeaders.Values['WWW-Authenticate']; 
    end; 

    except 
    on E:Exception do ; 
    end; 
end; 

function TTcpSocketStream.Read(var Buffer; Count: Integer): Longint; 
begin 
    Result := 0; 
    try 
    if FTcpClient.Connected then 
    begin 
     if Length(FBuffer) < Count then 
     SetLength(FBuffer, Count); 
     FTcpClient.IOHandler.ReadBytes(FBuffer, Count, False); 
     Move(FBuffer[0], PChar(Buffer), Count); 
     Inc(FBytesRead, Count); 
     Result := Count; 
    end; 
    except 
    on Exception do ; 
    end; 
end; 

function TTcpSocketStream.Seek(const Offset: Int64; Origin: TSeekOrigin): Int64; 
begin 
    Result := 0; 
    case Origin of 
    soBeginning: Result := Offset; 
    soCurrent: Result := FBytesRead + Offset; 
    soEnd: Result := FContentLength + Offset; 
    end; 
end;