{lsocket.pas} {socket code by plugwash} { Copyright (C) 2005 Bas Steendijk and Peter Green For conditions of distribution and use, see copyright notice in zlib_license.txt which is included in the package ----------------------------------------------------------------------------- } { changes by plugwash (20030728) * created handlefdtrigger virtual method in tlasio (overridden in tlsocket) and moved a lot of code from messageloop into it * changed tlasio to tlasio * split fdhandle into fdhandlein and fdhandleout * i now use fdsrmaster and fdswmaster instead of rebuilding the lists every loop * split lsocket.pas into lsocket.pas and lcore.pas changes by beware (20030903) * added getxaddr, getxport (local addr, port, as string) * added getpeername, remote addr+port as binary * added htons and htonl functions (endian swap, same interface as windows API) beware (20030905) * if connect failed (conn refused) set state to connected and call internalclose, to get closed handler (instead of fdclose) * (lcore) if closing the fd's in internalcose, set fd's to -1 because closing an fd makes it invalid beware (20030927) * fixed: on connect failed, tried to close fdhandle's which were already set to -1, added check beware (20031017) * added getpeeraddr, getpeerport, remote addr+port as string } unit lsocket; {$ifdef fpc} {$mode delphi} {$endif} {$include lcoreconfig.inc} interface uses sysutils, {$ifdef win32} windows,winsock, {$else} {$ifdef VER1_0} linux, {$else} baseunix,unix,unixutil, {$endif} sockets, {$endif} classes,{pgdebugout,}pgtypes,lcore,fd_utils,binipstuff,dnssync; {$ifdef ipv6} const v4listendefault:boolean=false; {$endif} type sunB = packed record s_b1, s_b2, s_b3, s_b4: byte; end; SunW = packed record s_w1, s_w2: word; end; TInAddr = packed record case integer of 0: (S_un_b: SunB); 1: (S_un_w: SunW); 2: (S_addr: cardinal); end; type TLsocket = class(tlasio) public //a: string; inAddr : TInetSockAddrV; biniplist:tbiniplist; trymoreips:boolean; currentip:integer; connecttimeout:tltimer; { inAddrSize:integer;} //host : THostentry ; //mainthread : boolean ; //for debuggin only addr:string; port:string; localaddr:string; localport:string; proto:string; udp,dgram:boolean; listenqueue:integer; {$ifdef secondlistener} secondlistener:tlsocket; lastsessionfromsecond:boolean; procedure secondaccepthandler(sender:tobject;error:word); procedure internalclose(error:word);override; {$endif} function getaddrsize:integer; procedure connect; virtual; procedure realconnect; procedure bindsocket; procedure listen; function accept : longint; function sendto(dest:TInetSockAddrV;destlen:integer;data:pointer;len:integer):integer; virtual; function receivefrom(data:pointer;len:integer;var src:TInetSockAddrV;var srclen:integer):integer; virtual; procedure handlefdtrigger(readtrigger,writetrigger:boolean); override; function send(data:pointer;len:integer):integer;override; procedure sendstr(const str : string);override; function Receive(Buf:Pointer;BufSize:integer):integer; override; function getpeername(var addr:tsockaddrin;addrlen:integer):integer; virtual; procedure getXaddrbin(var binip:tbinip); virtual; procedure getpeeraddrbin(var binip:tbinip); virtual; function getXaddr:string; virtual; function getpeeraddr:string; virtual; function getXport:string; virtual; function getpeerport:string; virtual; constructor Create(AOwner: TComponent); override; //this one has to be kept public for now because lcorewsaasyncselect calls it procedure connectionfailedhandler(error:word); private procedure taskcallconnectionfailedhandler(wparam,lparam : longint); procedure connecttimeouthandler(sender:tobject); procedure connectsuccesshandler; {$ifdef win32} procedure myfdclose(fd : integer); override; function myfdwrite(fd: LongInt;const buf;size: LongInt):LongInt; override; function myfdread(fd: LongInt;var buf;size: LongInt):LongInt; override; {$endif} end; tsocket=longint; // for compatibility with twsocket twsocket=tlsocket; {easy} const TCP_NODELAY=1; IPPROTO_TCP=6; implementation {$include unixstuff.inc} function tlsocket.getaddrsize:integer; begin result := inaddrsize(inaddr); end; procedure tlsocket.realconnect; var a,b:integer; begin //writeln('trying to connect to ',ipbintostr(biniplist_get(biniplist,currentip)),'#',port); makeinaddrv(biniplist_get(biniplist,currentip),port,inaddr); inc(currentip); if (currentip >= biniplist_getcount(biniplist)) then trymoreips := false; udp := false; if (uppercase(proto) = 'UDP') then begin b := IPPROTO_UDP; a := SOCK_DGRAM; udp := true; dgram := true; end else if (uppercase(proto) = 'TCP') or (uppercase(proto) = '') then begin b := IPPROTO_TCP; a := SOCK_STREAM; dgram := false; end else if (uppercase(proto) = 'ICMP') or (strtointdef(proto,256) < 256) then begin b := strtointdef(proto,IPPROTO_ICMP); a := SOCK_RAW; dgram := true; end else begin raise ESocketException.create('unrecognised protocol'); end; a := Socket(inaddr.inaddr.family,a,b); //writeln(ord(inaddr.inaddr.family)); if a = -1 then begin //unable to create socket, fire an error event (better to use an error event //to avoid poor interaction with multilistener stuff. //a socket value of -2 is a special value to say there is no socket but //we want internalclose to act as if there was fdhandlein := -2; fdhandleout := -2; tltask.create(taskcallconnectionfailedhandler,self,{$ifdef win32}wsagetlasterror{$else}socketerror{$endif},0); exit; end; try dup(a); bindsocket; if dgram then begin {$ifndef win32} SetSocketOptions(fdhandleout, SOL_SOCKET, SO_BROADCAST, 'TRUE', Length('TRUE')); {$else} SetSockOpt(fdhandleout, SOL_SOCKET, SO_BROADCAST, 'TRUE', Length('TRUE')); {$endif} state := wsconnected; if assigned(onsessionconnected) then onsessionconnected(self,0); eventcore.rmasterset(fdhandlein,false); eventcore.wmasterclr(fdhandleout); end else begin state :=wsconnecting; {$ifdef win32} //writeln(inaddr.inaddr.port); winsock.Connect(fdhandlein,winsock.psockaddr(@inADDR)^,getaddrsize); {$else} sockets.Connect(fdhandlein,inADDR,getaddrsize); {$endif} eventcore.rmasterset(fdhandlein,false); eventcore.wmasterset(fdhandleout); if trymoreips then connecttimeout.enabled := true; end; //sendq := ''; except on e: exception do begin fdcleanup; raise; //reraise the exception end; end; end; procedure tlsocket.connecttimeouthandler(sender:tobject); begin connecttimeout.enabled := false; destroying := true; //hack to not cause handler to trigger internalclose(0); destroying := false; realconnect; end; procedure tlsocket.connect; var a:integer; ip:tbinip; begin if state <> wsclosed then close; //prevtime := 0; if isbiniplist(addr) then biniplist := addr else biniplist := forwardlookuplist(addr,0); if biniplist_getcount(biniplist) = 0 then raise exception.create('unable to resolve '+addr); //makeinaddrv(addr,port,inaddr); currentip := 0; if not assigned(connecttimeout) then begin connecttimeout := tltimer.create(self); connecttimeout.Tag := integer(self); connecttimeout.ontimer := connecttimeouthandler; connecttimeout.interval := 2500; connecttimeout.enabled := false; end; realconnect; end; procedure tlsocket.sendstr(const str : string); begin if dgram then begin send(@str[1],length(str)) end else begin inherited sendstr(str); end; end; function tlsocket.send(data:pointer;len:integer):integer; begin if dgram then begin // writeln('sending to '+ipbintostr(inaddrvtobinip(inaddr)),' ',htons(inaddr.inaddr.port),' ',len,' bytes'); result := sendto(inaddr,getaddrsize,data,len); // writeln('send result ',result); // writeln('errno',errno); end else begin result := inherited send(data,len); end; end; function tlsocket.receive(Buf:Pointer;BufSize:integer):integer; begin if dgram then begin {$ifdef secondlistener} if lastsessionfromsecond then begin result := secondlistener.receive(buf,bufsize); lastsessionfromsecond := false; end else {$endif} result := myfdread(self.fdhandlein,buf^,bufsize); end else begin result := inherited receive(buf,bufsize); end; end; procedure tlsocket.bindsocket; var a:integer; inAddrtemp:TInetSockAddrV; inAddrtempx:{$ifdef win32}winsock.TSockaddr{$else}TInetSockAddrV{$endif} absolute inaddrtemp; inaddrtempsize:integer; begin try if (localaddr <> '') or (localport <> '') then begin if localaddr = '' then begin {$ifdef ipv6} if inaddr.inaddr.family = AF_INET6 then localaddr := '::' else {$endif} localaddr := '0.0.0.0'; end; //gethostbyname(localaddr,host); inaddrtempsize := makeinaddrv(forwardlookup(localaddr,0),localport,inaddrtemp); If Bind(fdhandlein,inaddrtempx,inaddrtempsize)<> {$ifdef win32}0{$else}true{$endif} Then begin state := wsclosed; lasterror := {$ifdef win32}getlasterror{$else}socketerror{$endif}; raise ESocketException.create('unable to bind on address '+localaddr+'#'+localport+', error '+inttostr(lasterror)); end; state := wsbound; end; except on e: exception do begin fdcleanup; raise; //reraise the exception end; end; end; procedure tlsocket.listen; var yes:longint; socktype:integer; biniptemp:tbinip; origaddr:string; begin if state <> wsclosed then close; udp := uppercase(proto) = 'UDP'; if udp then begin socktype := SOCK_DGRAM; dgram := true; end else socktype := SOCK_STREAM; origaddr := addr; if addr = '' then begin {$ifdef ipv6} if not v4listendefault then begin addr := '::'; end else {$endif} addr := '0.0.0.0'; end; if isbiniplist(addr) then biniptemp := biniplist_get(addr,0) else biniptemp := forwardlookup(addr,10); addr := ipbintostr(biniptemp); fdhandlein := socket(biniptemp.family,socktype,0); {$ifdef ipv6} if (addr = '::') and (origaddr = '') and (fdhandlein < 0) then begin addr := '0.0.0.0'; fdhandlein := socket(AF_INET,socktype,0); end; {$endif} if fdhandlein = -1 then raise ESocketException.create('unable to create socket'{$ifdef win32}+' error='+inttostr(wsagetlasterror){$endif}); dupnowatch(fdhandlein); // sets up maxs and copies handle to fdhandleout among other things //eventcore.setfdreverse(fdhandlein,self); //already taken care of by dup state := wsclosed; // then set this back as it was an undesired side effect of dup try yes := $01010101; {Copied this from existing code. Value is empiric, but works. (yes=true<>0) } {$ifndef win32} if SetSocketOptions(fdhandlein, SOL_SOCKET, SO_REUSEADDR,yes,sizeof(yes))=-1 then begin raise ESocketException.create('unable to set socket options'); end; {$endif} localaddr := addr; localport := port; bindsocket; if not udp then begin {!!! allow custom queue length? default 5} if listenqueue = 0 then listenqueue := 5; If {$ifdef win32}winsock{$else}sockets{$endif}.Listen(fdhandlein,listenqueue)<>{$ifdef win32}0{$else}true{$endif} Then raise esocketexception.create('unable to listen'); state := wsListening; end else begin {$ifndef win32} SetSocketOptions(fdhandleout, SOL_SOCKET, SO_BROADCAST, 'TRUE', Length('TRUE')); {$else} SetSockOpt(fdhandleout, SOL_SOCKET, SO_BROADCAST, 'TRUE', Length('TRUE')); {$endif} state := wsconnected; end; {$ifdef secondlistener} //listening on ::. try to listen on 0.0.0.0 as well for platforms which don't already do that if addr = '::' then begin secondlistener := tlsocket.create(nil); secondlistener.proto := proto; secondlistener.addr := '0.0.0.0'; secondlistener.port := port; if udp then begin secondlistener.ondataavailable := secondaccepthandler; end else begin secondlistener.onsessionAvailable := secondaccepthandler; end; try secondlistener.listen; except secondlistener.destroy; secondlistener := nil; end; end; {$endif} finally if state = wsclosed then begin if fdhandlein >= 0 then begin {one *can* get here without fd -beware} eventcore.rmasterclr(fdhandlein); myfdclose(fdhandlein); // we musnt leak file discriptors eventcore.setfdreverse(fdhandlein,nil); fdhandlein := -1; end; end else begin eventcore.rmasterset(fdhandlein,not udp); end; if fdhandleout >= 0 then eventcore.wmasterclr(fdhandleout); end; //writeln('listened on addr '+addr+':'+port+' '+proto+' using socket number ',fdhandlein); end; {$ifdef secondlistener} procedure tlsocket.internalclose(error:word); begin if assigned(secondlistener) then begin secondlistener.destroy; secondlistener := nil; end; inherited internalclose(error); end; procedure tlsocket.secondaccepthandler; begin lastsessionfromsecond := true; if udp then begin ondataavailable(self,error); end else begin if assigned(onsessionavailable) then onsessionavailable(self,error); end; end; {$endif} function tlsocket.accept : longint; var FromAddrSize : LongInt; // i don't realy know what to do with these at this FromAddr : TInetSockAddrV; // at this point time will tell :) a:integer; begin {$ifdef secondlistener} if (lastsessionfromsecond) then begin lastsessionfromsecond := false; result := secondlistener.accept; exit; end; {$endif} FromAddrSize := Sizeof(FromAddr); {$ifdef win32} result := winsock.accept(fdhandlein,@fromaddr,@fromaddrsize); {$else} result := sockets.accept(fdhandlein,fromaddr,fromaddrsize); {$endif} //now we have accepted one request start monitoring for more again eventcore.rmasterset(fdhandlein,true); if result = -1 then begin raise esocketexception.create('error '+inttostr({$ifdef win32}getlasterror{$else}socketerror{$endif})+' while accepting'); end; if result > absoloutemaxs then begin myfdclose(result); a := result; result := -1; raise esocketexception.create('file discriptor out of range: '+inttostr(a)); end; end; function tlsocket.sendto(dest:TInetSockAddrV;destlen:integer;data:pointer;len:integer):integer; var destx : {$ifdef win32}winsock.TSockAddr{$else}TInetSockAddrV{$endif} absolute dest; begin {$ifdef secondlistener} if assigned(secondlistener) then if (dest.inaddr.family = AF_INET) then begin result := secondlistener.sendto(dest,destlen,data,len); exit; end; {$endif} result := {$ifdef win32}winsock{$else}sockets{$endif}.sendto(self.fdhandleout,data^,len,0,destx,destlen); end; function tlsocket.receivefrom(data:pointer;len:integer;var src:TInetSockAddrV;var srclen:integer):integer; var tempsrc:TInetSockAddrV; tempsrclen:integer; srcx : {$ifdef win32}winsock.TSockAddr{$else}TInetSockAddrV{$endif} absolute tempsrc; biniptemp:tbinip; begin {$ifdef secondlistener} if assigned(secondlistener) then if lastsessionfromsecond then begin lastsessionfromsecond := false; result := secondlistener.receivefrom(data,len,src,srclen); exit; end; {$endif} tempsrclen := sizeof(tempsrc); result := {$ifdef win32}winsock{$else}sockets{$endif}.recvfrom(self.fdhandlein,data^,len,0,srcx,tempsrclen); {$ifdef ipv6} biniptemp := inaddrvtobinip(tempsrc); if needconverttov4(biniptemp) then begin converttov4(biniptemp); tempsrclen := makeinaddrv(biniptemp,inttostr(ntohs(tempsrc.InAddr.port)),tempsrc); end; {$endif} move(tempsrc,src,srclen); srclen := tempsrclen; end; procedure tlsocket.taskcallconnectionfailedhandler(wparam,lparam : longint); begin connectionfailedhandler(wparam); end; procedure tlsocket.connectionfailedhandler(error:word); begin if trymoreips then begin // writeln('failed with error ',error); connecttimeout.enabled := false; destroying := true; state := wsconnected; self.internalclose(0); destroying := false; realconnect; end else begin state := wsconnected; if assigned(onsessionconnected) then onsessionconnected(self,error); self.internalclose(0); recvq.del(maxlongint); end; end; procedure tlsocket.connectsuccesshandler; begin trymoreips := false; connecttimeout.enabled := false; if assigned(onsessionconnected) then onsessionconnected(self,0); end; procedure tlsocket.handlefdtrigger(readtrigger,writetrigger:boolean); var tempbuf:array[0..receivebufsize-1] of byte; begin // writeln('got a fd trigger, readtrigger=',readtrigger,' writetrigger=',writetrigger,' state=',integer(state)); if (state =wslistening) and readtrigger then begin { debugout('listening socket triggered on read');} eventcore.rmasterclr(fdhandlein); if assigned(onsessionAvailable) then onsessionAvailable(self,0); end; if dgram and readtrigger then begin if assigned(ondataAvailable) then ondataAvailable(self,0); {!!!test} exit; end; if (state =wsconnecting) and writetrigger then begin // code for dealing with the reults of a non-blocking connect is // rather complex // if just write is triggered it means connect suceeded // if both read and write are triggered it can mean 2 things // 1: connect ok and data availible // 2: connect fail // to find out which you must read from the socket and look for errors // there if we read successfully we drop through into the code for fireing // the read event if not readtrigger then begin state := wsconnected; connectsuccesshandler; end else begin numread := myfdread(fdhandlein,tempbuf,sizeof(tempbuf)); if numread <> -1 then begin state := wsconnected; connectsuccesshandler; //connectread := true; recvq.add(@tempbuf,numread); end else begin connectionfailedhandler({$ifdef win32}wsagetlasterror{$else}linuxerror{$endif}); exit; end; // if things went well here we are now in the state wsconnected with data sitting in our receive buffer // so we drop down into the processing for data availible end; if fdhandlein >= 0 then begin if state = wsconnected then begin eventcore.rmasterset(fdhandlein,false); end else begin eventcore.rmasterclr(fdhandlein); end; end; if fdhandleout >= 0 then begin if sendq.size = 0 then begin //don't clear the bit in fdswmaster if data is in the sendq eventcore.wmasterclr(fdhandleout); end; end; end; inherited handlefdtrigger(readtrigger,writetrigger); end; constructor tlsocket.Create(AOwner: TComponent); begin inherited create(aowner); closehandles := true; trymoreips := true; end; function tlsocket.getpeername(var addr:tsockaddrin;addrlen:integer):integer; var addrx : {$ifdef win32}winsock.tsockaddr{$else}tsockaddrin{$endif} absolute addr; begin result := {$ifdef win32}winsock{$else}sockets{$endif}.getpeername(self.fdhandlein,addrx,addrlen); end; procedure tlsocket.getxaddrbin(var binip:tbinip); var addr:tinetsockaddrv; i:integer; begin i := sizeof(addr); fillchar(addr,sizeof(addr),0); {$ifdef win32} winsock.getsockname(self.fdhandlein,psockaddr(@addr)^,i); {$else} sockets.getsocketname(self.fdhandlein,addr,i); {$endif} binip := inaddrvtobinip(addr); converttov4(binip); end; procedure tlsocket.getpeeraddrbin(var binip:tbinip); var addr:tinetsockaddrv; i:integer; begin i := sizeof(addr); fillchar(addr,sizeof(addr),0); {$ifdef win32} winsock.getpeername(self.fdhandlein,psockaddr(@addr)^,i); {$else} sockets.getpeername(self.fdhandlein,addr,i); {$endif} binip := inaddrvtobinip(addr); converttov4(binip); end; function tlsocket.getXaddr:string; var biniptemp:tbinip; begin getxaddrbin(biniptemp); result := ipbintostr(biniptemp); if result = '' then result := 'error'; end; function tlsocket.getpeeraddr:string; var biniptemp:tbinip; begin getpeeraddrbin(biniptemp); result := ipbintostr(biniptemp); if result = '' then result := 'error'; end; function tlsocket.getXport:string; var addr:tinetsockaddrv; i:integer; begin i := sizeof(addr); {$ifdef win32} winsock.getsockname(self.fdhandlein,psockaddrin(@addr)^,i); {$else} sockets.getsocketname(self.fdhandlein,addr,i); {$endif} result := inttostr(htons(addr.InAddr.port)); end; function tlsocket.getpeerport:string; var addr:tinetsockaddrv; i:integer; begin i := sizeof(addr); {$ifdef win32} winsock.getpeername(self.fdhandlein,psockaddrin(@addr)^,i); {$else} sockets.getpeername(self.fdhandlein,addr,i); {$endif} result := inttostr(htons(addr.InAddr.port)); end; {$ifdef win32} procedure tlsocket.myfdclose(fd : integer); begin closesocket(fd); end; function tlsocket.myfdwrite(fd: LongInt;const buf;size: LongInt):LongInt; begin result := winsock.send(fd,(@buf)^,size,0); end; function tlsocket.myfdread(fd: LongInt;var buf;size: LongInt):LongInt; begin result := winsock.recv(fd,buf,size,0); end; {$endif} end.