Skip to content

Commit

Permalink
several fixes for POSIX - now with full Unix Domain Sockets client/se…
Browse files Browse the repository at this point in the history
…rver suppport

- UDS seem faster than TCP/HTTP on the loopback, using 'unix:/path/to/myapp.sock' server address on both sides
  • Loading branch information
Arnaud Bouchez committed Feb 26, 2021
1 parent 77acba6 commit 9109db8
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 136 deletions.
27 changes: 27 additions & 0 deletions src/net/mormot.net.client.pas
Expand Up @@ -47,6 +47,33 @@ interface

{ ************** THttpClientSocket Implementing HTTP client over plain sockets }

var
/// THttpRequest timeout default value for DNS resolution
// - only used by TWinHttp class - other clients will ignore it
// - leaving to 0 will let system default value be used
HTTP_DEFAULT_RESOLVETIMEOUT: integer = 0;
/// THttpRequest timeout default value for remote connection
// - default is 30 seconds
// - used e.g. by THttpRequest, TRestHttpClientRequest and TRestHttpClientGeneric
HTTP_DEFAULT_CONNECTTIMEOUT: integer = 30000;
/// THttpRequest timeout default value for data sending
// - default is 30 seconds
// - used e.g. by THttpRequest, TRestHttpClientRequest and TRestHttpClientGeneric
// - you can override this value by setting the corresponding parameter in
// THttpRequest.Create() constructor
HTTP_DEFAULT_SENDTIMEOUT: integer = 30000;
/// THttpRequest timeout default value for data receiving
// - default is 30 seconds
// - used e.g. by THttpRequest, TRestHttpClientRequest and TRestHttpClientGeneric
// - you can override this value by setting the corresponding parameter in
// THttpRequest.Create() constructor
HTTP_DEFAULT_RECEIVETIMEOUT: integer = 30000;

const
/// standard text used to identify the WebSockets protocol
HTTP_WEBSOCKET_PROTOCOL: RawUtf8 = 'SEC-WEBSOCKET-PROTOCOL';


type
/// Socket API based REST and HTTP/1.1 compatible client class
// - this component is HTTP/1.1 compatible, according to RFC 2068 document
Expand Down
110 changes: 45 additions & 65 deletions src/net/mormot.net.http.pas
Expand Up @@ -31,56 +31,7 @@ interface

{ ******************** Shared HTTP Constants and Functions }

var
/// THttpRequest timeout default value for DNS resolution
// - leaving to 0 will let system default value be used
HTTP_DEFAULT_RESOLVETIMEOUT: integer = 0;
/// THttpRequest timeout default value for remote connection
// - default is 60 seconds
// - used e.g. by THttpRequest, TRestHttpClientRequest and TRestHttpClientGeneric
HTTP_DEFAULT_CONNECTTIMEOUT: integer = 60000;
/// THttpRequest timeout default value for data sending
// - default is 30 seconds
// - used e.g. by THttpRequest, TRestHttpClientRequest and TRestHttpClientGeneric
// - you can override this value by setting the corresponding parameter in
// THttpRequest.Create() constructor
HTTP_DEFAULT_SENDTIMEOUT: integer = 30000;
/// THttpRequest timeout default value for data receiving
// - default is 30 seconds
// - used e.g. by THttpRequest, TRestHttpClientRequest and TRestHttpClientGeneric
// - you can override this value by setting the corresponding parameter in
// THttpRequest.Create() constructor
HTTP_DEFAULT_RECEIVETIMEOUT: integer = 30000;

const
/// standard text used to identify the WebSockets protocol
HTTP_WEBSOCKET_PROTOCOL: RawUtf8 = 'SEC-WEBSOCKET-PROTOCOL';


/// compute the 'Authorization: Bearer ####' HTTP header of a given token value
function AuthorizationBearer(const AuthToken: RawUtf8): RawUtf8;

/// will remove most usual HTTP headers which are to be recomputed on sending
function PurgeHeaders(P: PUtf8Char): RawUtf8;


{$ifndef NOXPOWEREDNAME}
const
/// pseudo-header containing the current Synopse mORMot framework version
XPOWEREDNAME = 'X-Powered-By';
/// the full text of the current Synopse mORMot framework version
// - we don't supply full version number with build revision
// (as SYNOPSE_FRAMEWORK_VERSION), to reduce potential attack surface
XPOWEREDVALUE = SYNOPSE_FRAMEWORK_NAME + ' 2 synopse.info';
{$endif NOXPOWEREDNAME}


{ ******************** THttpSocket Implementing HTTP over plain sockets }

type
/// exception class raised during HTTP process
EHttpSocket = class(ENetSock);

/// event used to compress or uncompress some data during HTTP protocol
// - should always return the protocol name for ACCEPT-ENCODING: header
// e.g. 'gzip' or 'deflate' for standard HTTP format, but you can add
Expand Down Expand Up @@ -109,6 +60,48 @@ THttpSocketCompressRec = record
// - filled from ACCEPT-ENCODING: header value
THttpSocketCompressSet = set of 0..31;


/// adjust HTTP body compression according to the supplied 'CONTENT-TYPE'
// - will detect most used compressible content (like 'text/*' or
// 'application/json') from OutContentType
function CompressDataAndGetHeaders(Accepted: THttpSocketCompressSet;
const Handled: THttpSocketCompressRecDynArray; const OutContentType: RawUtf8;
var OutContent: RawByteString): RawUtf8;

/// enable a give compression function for a HTTP link
function RegisterCompressFunc(var Compress: THttpSocketCompressRecDynArray;
aFunction: THttpSocketCompress; var aAcceptEncoding: RawUtf8;
aCompressMinSize: integer): RawUtf8;

/// decode 'CONTENT-ENCODING: ' parameter from registered compression list
function ComputeContentEncoding(const Compress: THttpSocketCompressRecDynArray;
P: PUtf8Char): THttpSocketCompressSet;


/// compute the 'Authorization: Bearer ####' HTTP header of a given token value
function AuthorizationBearer(const AuthToken: RawUtf8): RawUtf8;

/// will remove most usual HTTP headers which are to be recomputed on sending
function PurgeHeaders(P: PUtf8Char): RawUtf8;


{$ifndef NOXPOWEREDNAME}
const
/// pseudo-header containing the current Synopse mORMot framework version
XPOWEREDNAME = 'X-Powered-By';
/// the full text of the current Synopse mORMot framework version
// - we don't supply full version number with build revision
// (as SYNOPSE_FRAMEWORK_VERSION), to reduce potential attacker knowledge
XPOWEREDVALUE = SYNOPSE_FRAMEWORK_NAME + ' 2 synopse.info';
{$endif NOXPOWEREDNAME}


{ ******************** THttpSocket Implementing HTTP over plain sockets }

type
/// exception class raised during HTTP process
EHttpSocket = class(ENetSock);

/// map the presence of some HTTP headers for THttpSocket.HeaderFlags
THttpSocketHeaderFlags = set of (
hfTransferChuked,
Expand Down Expand Up @@ -209,20 +202,6 @@ THttpSocket = class(TCrtSocket)
end;


/// adjust HTTP body compression according to the supplied 'CONTENT-TYPE'
function CompressDataAndGetHeaders(Accepted: THttpSocketCompressSet;
const Handled: THttpSocketCompressRecDynArray; const OutContentType: RawUtf8;
var OutContent: RawByteString): RawUtf8;

/// enable a give compression function for a HTTP link
function RegisterCompressFunc(var Compress: THttpSocketCompressRecDynArray;
aFunction: THttpSocketCompress; var aAcceptEncoding: RawUtf8;
aCompressMinSize: integer): RawUtf8;

/// decode 'CONTENT-ENCODING: ' parameter from registered compression list
function ComputeContentEncoding(const Compress: THttpSocketCompressRecDynArray;
P: PUtf8Char): THttpSocketCompressSet;


{ ******************** Abstract Server-Side Types used e.g. for Client-Server Protocol }

Expand Down Expand Up @@ -804,7 +783,7 @@ procedure THttpSocket.GetBody;
SetLength(Content, ContentLength); // not chuncked: direct read
SockInRead(pointer(Content), ContentLength); // works with SockIn=nil or not
end
else if (ContentLength < 0) and
else if (ContentLength < 0) and // -1 means no Content-Length header
IdemPChar(pointer(Command), 'HTTP/1.0 200') then
begin
// body = either Content-Length or Transfer-Encoding (HTTP/1.1 RFC2616 4.3)
Expand All @@ -824,7 +803,8 @@ procedure THttpSocket.GetBody;
if cardinal(fContentCompress) < cardinal(length(fCompress)) then
if fCompress[fContentCompress].Func(Content, false) = '' then
// invalid content
raise EHttpSocket.CreateFmt('%s uncompress', [fCompress[fContentCompress].Name]);
raise EHttpSocket.CreateFmt(
'%s uncompress', [fCompress[fContentCompress].Name]);
ContentLength := length(Content); // update Content-Length
{$ifdef SYNCRTDEBUGLOW}
TSynLog.Add.Log(sllCustom2, 'GetBody sock=% pending=% sockin=% len=% %',
Expand Down
27 changes: 16 additions & 11 deletions src/net/mormot.net.server.pas
Expand Up @@ -524,8 +524,8 @@ THttpServer = class(THttpServerGeneric)
// - this constructor will raise a EHttpServer exception if binding failed
// - expects the port to be specified as string, e.g. '1234'; you can
// optionally specify a server address to bind to, e.g. '1.2.3.4:1234'
// - can listed on UDS in case port is specified with 'unix:' prefix, e.g.
// 'unix:/run/myapp.sock'
// - can listed to local Unix Domain Sockets file in case port is prefixed
// with 'unix:', e.g. 'unix:/run/myapp.sock' - faster and safer than TCP
// - on Linux in case aPort is empty string will check if external fd
// is passed by systemd and use it (so called systemd socked activation)
// - you can specify a number of threads to be initialized to handle
Expand Down Expand Up @@ -1449,9 +1449,14 @@ destructor THttpServer.Destroy;
if (fExecuteState = esRunning) and
(Sock <> nil) then
begin
Sock.Close; // shutdown the socket to unlock Accept() in Execute
if NewSocket('127.0.0.1', Sock.Port, nlTCP, false, 1, 1, 1, 0, callback) = nrOK then
if Sock.SocketLayer <> nlUNIX then
Sock.Close; // shutdown TCP/UDP socket to unlock Accept() in Execute
if NewSocket(Sock.Server, Sock.Port, Sock.SocketLayer,
{dobind=}false, 10, 10, 10, 0, callback) = nrOK then
// Windows TCP/UDP socket may not release Accept() until connected
callback.ShutdownAndClose({rdwr=}false);
if Sock.SockIsDefined then
Sock.Close; // nlUNIX expects shutdown after accept() returned
end;
endtix := mormot.core.os.GetTickCount64 + 20000;
EnterCriticalSection(fProcessCS);
Expand All @@ -1469,7 +1474,7 @@ destructor THttpServer.Destroy;
(fExecuteState <> esRunning) then
break;
LeaveCriticalSection(fProcessCS);
SleepHiRes(100);
SleepHiRes(10);
EnterCriticalSection(fProcessCS);
until mormot.core.os.GetTickCount64 > endtix;
FreeAndNil(fInternalHttpServerRespList);
Expand Down Expand Up @@ -1610,9 +1615,9 @@ procedure THttpServer.Execute;
try
fSock := TCrtSocket.Bind(fSockPort); // BIND + LISTEN
{$ifdef OSLINUX}
// in case we started by systemd, listening socket is created by another process
// and do not interrupt while process got a signal. So we need to set a timeout to
// unblock accept() periodically and check we need terminations
// in case was started by systemd, listening socket is created by another
// process and do not interrupt while process got a signal. So we need to
// set a timeout to unlock accept() periodically and check for termination
if fSockPort = '' then // external socket
fSock.ReceiveTimeout := 1000; // unblock accept every second
{$endif OSLINUX}
Expand Down Expand Up @@ -2331,9 +2336,9 @@ procedure TSynThreadPoolTHttpServer.Task(aCaller: TSynThread; aContext: Pointer)
grOwned:
// e.g. for asynchrounous WebSockets
srvsock := nil; // to ignore FreeAndNil(srvsock) below
else if Assigned(fServer.Sock.OnLog) then
fServer.Sock.OnLog(sllTrace, 'Task: close after GetRequest=% from %',
[ToText(res)^, srvsock.RemoteIP], self);
else if Assigned(fServer.Sock.OnLog) then
fServer.Sock.OnLog(sllTrace, 'Task: close after GetRequest=% from %',
[ToText(res)^, srvsock.RemoteIP], self);
end;
finally
srvsock.Free;
Expand Down
95 changes: 60 additions & 35 deletions src/net/mormot.net.sock.pas
Expand Up @@ -529,9 +529,11 @@ TCrtSocket = class
aTimeOut: cardinal = 10000; aTLS: boolean = false; aTLSContext: PNetTLSContext = nil);
/// bind to an address
// - aAddr='1234' - bind to a port on all interfaces, the same as '0.0.0.0:1234'
// - aAddr='IP:port' - bind to specified interface only, e.g. '1.2.3.4:1234'
// - aAddr='unix:/path/to/file' - bind to unix domain socket, e.g. 'unix:/run/mormot.sock'
// - aAddr='' - bind to systemd descriptor on linux. See
// - aAddr='IP:port' - bind to specified interface only, e.g.
// '1.2.3.4:1234'
// - aAddr='unix:/path/to/file' - bind to unix domain socket, e.g.
// 'unix:/run/mormot.sock'
// - aAddr='' - bind to systemd descriptor on linux - see
// http://0pointer.de/blog/projects/socket-activation.html
constructor Bind(const aAddress: RawUtf8; aLayer: TNetLayer = nlTCP;
aTimeOut: integer = 10000);
Expand Down Expand Up @@ -1078,7 +1080,11 @@ function NewSocket(const address, port: RawUtf8; layer: TNetLayer;
(not dobind) and
Assigned(NewSocketAddressCache) and
ToCardinal(port, p, 1) then
if NewSocketAddressCache.Search(address, addr) then
if (address = '') or
(address = cLocalhost) or
(address = cAnyHost) then // for client: '0.0.0.0'->'127.0.0.1'
result := addr.SetFrom(cLocalhost, port, layer)
else if NewSocketAddressCache.Search(address, addr) then
begin
fromcache := true;
result := addr.SetPort(p);
Expand Down Expand Up @@ -1215,7 +1221,11 @@ function TNetSocketWrap.Accept(out clientsocket: TNetSocket;
len := SizeOf(addr);
sock := mormot.net.sock.accept(TSocket(@self), @addr, len);
if sock = -1 then
result := NetLastError
begin
result := NetLastError;
if result = nrOk then
result := nrNotImplemented;
end
else
begin
clientsocket := TNetSocket(sock);
Expand Down Expand Up @@ -1582,6 +1592,32 @@ procedure TPollSockets.Terminate;
const
UNIX_LOW = ord('u') + ord('n') shl 8 + ord('i') shl 16 + ord('x') shl 24;

function StartWith(p, up: PUtf8Char): boolean;
// to avoid linking mormot.core.text for IdemPChar()
var
c, u: AnsiChar;
begin
result := false;
if (p = nil) or
(up = nil) then
exit;
repeat
u := up^;
if u = #0 then
break;
inc(up);
c := p^;
inc(p);
if (c >= 'a') and
(c <= 'z') then
dec(c, 32);
if c <> u then
exit;
until false;
result := true;
end;


{ TCrtSocket }

function TCrtSocket.GetRawSocket: PtrInt;
Expand Down Expand Up @@ -1625,8 +1661,17 @@ constructor TCrtSocket.Open(const aServer, aPort: RawUtf8;
Create(aTimeOut); // default read timeout is 10 seconds
if aTLSContext <> nil then
TLS := aTLSContext^; // copy the input parameters before OpenBind()
// raise exception on error
OpenBind(aServer, aPort, {dobind=}false, aTLS, aLayer);
// OpenBind() raise an exception on error
{$ifdef OSPOSIX}
if StartWith(pointer(aServer), 'UNIX:') then
begin
// aServer='unix:/path/to/myapp.socket'
OpenBind(copy(aServer, 6, 200), '', {dobind=}false, aTLS, nlUNIX);
fServer := aServer; // keep the full server name if reused
end
else
{$endif OSPOSIX}
OpenBind(aServer, aPort, {dobind=}false, aTLS, aLayer);
end;

function SplitFromRight(const Text: RawUtf8; Sep: AnsiChar;
Expand Down Expand Up @@ -1683,9 +1728,10 @@ constructor TCrtSocket.Bind(const aAddress: RawUtf8; aLayer: TNetLayer;
{$ifdef OSPOSIX}
if s = 'unix' then
begin
aLayer := nlUNIX;
s := p;
p := '';
// aAddress='unix:/path/to/myapp.socket'
FpUnlink(pointer(p)); // previous bind may have left the .socket file
OpenBind(p, '', {dobind=}true, {tls=}false, nlUnix, {%H-}TNetSocket(aSock));
exit;
end;
{$endif OSPOSIX}
end;
Expand Down Expand Up @@ -1982,6 +2028,10 @@ procedure TCrtSocket.Close;
fSock := TNetSocket(-1);
// don't reset fServer/fPort/fTls/fWasBind: caller may use them to reconnect
// (see e.g. THttpClientSocket.Request)
{$ifdef OSPOSIX}
if fSocketLayer = nlUnix then
FpUnlink(pointer(fServer)); // 'unix:/path/to/myapp.socket' -> delete file
{$endif OSPOSIX}
end;

destructor TCrtSocket.Destroy;
Expand Down Expand Up @@ -2507,31 +2557,6 @@ function TCrtSocket.PeerPort: integer;

{ TUri }

function StartWith(p, up: PUtf8Char): boolean;
// to avoid linking mormot.core.text for IdemPChar()
var
c, u: AnsiChar;
begin
result := false;
if (p = nil) or
(up = nil) then
exit;
repeat
u := up^;
if u = #0 then
break;
inc(up);
c := p^;
inc(p);
if (c >= 'a') and
(c <= 'z') then
dec(c, 32);
if c <> u then
exit;
until false;
result := true;
end;

procedure TUri.Clear;
begin
Https := false;
Expand Down

0 comments on commit 9109db8

Please sign in to comment.