/* Simple web server */ /* Takes parameters port and docroot */ /* e.g. rexx simplewebserver.rexx 80 c:\htdocs */ /* Default port is 80 */ /* Will server pages from docroot */ /* Demonstration code, not for production use */ /* This code is intended to demonstrate unsafe programming */ /* Do not use as a public server */ /* Copyright Mike Protts 2009 */ /* Please contact mike at protts.uk.eu.org for details */ /* The following can turn on trace, e.g. trace 'i' */ trace 'o' Parse Arg port docroot /* Check rxsock functions are available */ If RxFuncQuery('SockDropFuncs') then do rc = RxFuncAdd('SockLoadFuncs','rxsock','SockLoadFuncs') if (rc <> 0) then do say 'RxFuncAdd rxsock returned' rc exit end call SockLoadFuncs end /* Start server thread */ thisserver = .server~new() /* Allow the port and docroot to be specified (should have address as well) */ if (port <> '') then do thisserver~port=(port) end if (docroot <> '') then do thisserver~docroot=(docroot) end rc = thisserver~run() if (rc <= 0) then do say 'aborting' rc exit rc end /* We can now work as a console thread */ say 'Running, press enter to stop' pull resp /* Signal a clean shutdown */ thisserver~stop=(1) Say 'Stopping' exit 0 /* Server class */ ::class server ::attribute stop unguarded /* set to 1 to shutdown cleanly */ ::attribute address ::attribute port ::attribute docroot ::method init self~stop=(0) self~address=('0.0.0.0') self~port=(80) self~docroot=('./') /* The real work starts here */ ::method run listen_address.family = 'AF_INET' say 'Starting web server with port', self~address()':'self~port() 'and docroot' self~docroot() listen_address.port = self~port() listen_address.addr = self~address() /* Open a socket */ listen_socket = SockSocket('AF_INET', 'SOCK_STREAM', 'IPPROTO_TCP') if (listen_socket <= 0) then do say 'SockSocket returned' listen_socket errno SockSock_Errno() SockPSock_Errno() return end /* Set socket options so we can repeat program */ call SockSetSockOpt listen_socket, 'SOL_SOCKET', 'SO_REUSEADDR', 1 if (result <> 0) then do say 'SockSetSockOpt returned' result errno SockSock_Errno() SockPSock_Errno() return end /* Bind to the port */ Call SockBind listen_socket, listen_address. if (result <> 0) then do say 'SockBind returned' result errno SockSock_Errno() SockPSock_Errno() return end /* Listen for a connection */ Call SockListen listen_socket, 1 if (result <> 0) then do say 'SockListen returned' result errno SockSock_Errno() SockPSock_Errno() return end /* We are about to start work, so release the caller */ reply listen_socket do forever say 'Waiting to accept' /* Let's make sure we don't block on the accept */ receive_sock.0 = 1 receive_sock.1 = listen_socket send_sock.0 = 0 err_sock.0 = 0 rc = SockSelect('RECEIVE_SOCK.', 'SEND_SOCK.', 'ERR_SOCK.', 3) /* Se can perform a clean shutdown */ if (self~stop() = 1) then do call SockClose listen_socket return end Select When (rc = 0) then do Say 'Accept Timeout' end When (rc < 0) then do say 'SockSelect' rc errno SockPSock_Errno() SockSock_Errno() end otherwise do do i = 1 to receive_sock.0 /* We have some work, so allocate a */ /* client handler thread */ if receive_sock.i = listen_socket then do thisclient = .client_handler~new(self~docroot()) thisclient~accept_conn(listen_socket) end end end end end /* Class to handle client connection */ ::class client_handler ::attribute mydocroot ::attribute header ::attribute content ::attribute file ::attribute data ::attribute content_length ::attribute conn_socket ::attribute read ::attribute req ::attribute client ::method init use arg self~mydocroot /* default filename */ self~file=('/index.html') /* Let the mime type code be handled as a small method. */ /* Later could use /etc/mime.types */ ::method getmimetype file = self~file() parse var file fname'.'ftype . select when ftype = 'html' then mtype = 'text/html; charset=iso-8859-1' when ftype = 'xml' then mtype = 'text/xml; charset=iso-8859-1' when ftype = 'css' then mtype = 'text/css; charset=iso-8859-1' when ftype = 'png' then mtype = 'image/png' when ftype = 'ico' then mtype = 'image/x-icon' when ftype = 'jpg' then mtype = 'image/jpg' otherwise mtype = 'application/octet-stream' end return mtype /* Reads a chunk from the file */ ::method getdata buffsize = self~content_length()-self~read() /* Limit size per packet */ if buffsize > 32767 then buffsize=32767 self~content=(charin(self~mydocroot() || self~file(), self~read()+1, buffsize)) /* track byte count */ self~read=(self~read()+buffsize) /* Return 1 when finished */ return self~read()=self~content_length() /* Accept connection and handle request */ ::method accept_conn use arg listen_sock crlf = '0d0a'x /* Accept a connection */ self~conn_socket=(SockAccept(listen_sock)) /* *** add error check */ /* Release caller, let them know socket in case needed */ reply self~conn_socket() /* Receive some data */ client=(.client~new()) client~socket=(self~conn_socket()) recrc = client~saferecv() do i = 1 by 1 while recrc = 0 say 'Request' i client~data() client~process_request() /* *** note uri should map to a content delivery object */ /* *** a file is a special case */ if ((client~request~uri() <> '') & (client~request~uri() <> '/')) then do self~file=(client~request~uri()) end say date('S') time('L') self~conn_socket() self~mydocroot() || self~file() 'started' time1 = time('E') /* Send a response */ self~read=(0) self~content_length=(stream(self~mydocroot() || self~file(),'c','query size')) client~response=(.response~new()) client~response~build_header(self~getmimetype(), self~content_length(),, client~request~header~version()) say client~response~getheader() rc = client~safesend(client~response~getheader()) do j = 1 by 1 while rc = 0 rc = self~getdata() rc = rc + client~safesend(self~content()) if (j//100 = 0) then do say date('S') time('L') self~conn_socket() 'sent', self~read()%1024 'of' self~content_length()%1024 || 'KB' end end /* Close the file */ call stream self~mydocroot() || self~file(), 'C', 'CLOSE' say date('S') time('L') self~conn_socket() self~mydocroot()|| self~file() 'completed' say date('S') time('L') self~conn_socket() self~content_length()%1024 || 'KB in', time('E') - time1 's ('self~content_length()%1024/(time('E') - time1)'KB/s)' if client~request~header~version() = 'HTTP/1.1' then do /* Check for another request */ recrc = client~saferecv() end else do recrc = 1 end end /* Close connection */ call SockClose self~conn_socket return /* client class */ ::class client ::attribute socket ::attribute ipaddress ::attribute data ::attribute request ::attribute response ::attribute version ::method process_request self~request=(.request~new()) self~request~process_header(self~data()) ::method safesend use arg data receive_sock.0 = 0 send_sock.0 = 1 send_sock.1 = self~socket() err_sock.0 = 1 err_sock.1 = self~socket() rc = SockSelect('RECEIVE_SOCK.', 'SEND_SOCK.', 'ERR_SOCK.', 3) Select When (rc = 0) then do Say 'Send Timeout' end When (rc < 0) then do say 'SockSelect' rc errno SockPSock_Errno() SockSock_Errno() end otherwise do if (err_sock.0 = 1) then do say 'Error on' err_sock.1 return 99 end rc = SockSend(send_sock.1, data) if (rc > 0) then do rc = 0 end else do say 'send error' rc errno SockPSock_Errno() SockSock_Errno() end end end return rc ::method saferecv receive_sock.0 = 1 receive_sock.1 = self~socket() send_sock.0 = 0 err_sock.0 = 0 rc = SockSelect('RECEIVE_SOCK.', 'SEND_SOCK.', 'ERR_SOCK.', 3) Select When (rc = 0) then do Say 'Receive Timeout' rc = 1 end When (rc < 0) then do say 'SockSelect' rc errno SockPSock_Errno() SockSock_Errno() end otherwise do Call SockRecv receive_sock.1, 'DATA', 32767 if (result > 0) then do self~data=(data) rc = 0 end else do say 'receive error' rc errno SockPSock_Errno() SockSock_Errno() end end end return rc ::class request subclass message ::attribute uri get return self~header~uri() ::method process_header use arg data crlf = '0d0a'x self~header=(.header~new()) self~body=(.body~new()) parse var data self~header~data (crlf||crlf) self~body~data /* *** Note rfc acceptable methods */ parse value self~header~data() with, self~header~method self~header~uri self~header~version (crlf) /* *** Need to access all header pairs */ ::class response subclass message /* Build http header */ ::method build_header use arg mimetype, content_length, version self~header=(.header~new()) self~header~content_length=(content_length) crlf = '0d0a'x resp = version '200 OK' || crlf resp = resp || 'Content-Length:' || self~header~content_length() || crlf resp = resp || 'Content-Type:' mimetype || crlf if version = 'HTTP/1.1' then do resp = resp || 'Connection: keep-alive' || crlf end resp = resp || crlf self~header~data=(resp) return 0 ::class message ::attribute header ::attribute body ::method getheader return self~header~data() ::class header ::attribute data ::attribute method ::attribute uri ::attribute version ::attribute accept ::attribute cookies ::attribute content_length ::class body ::attribute data