-- This package is free software; you can redistribute it and/or -- modify it under terms of the GNU General Public License as -- published by the Free Software Foundation; either version 3, or -- (at your option) any later version. It is distributed in the -- hope that it will be useful, but WITHOUT ANY WARRANTY; without -- even the implied warranty of MERCHANTABILITY or FITNESS FOR A -- PARTICULAR PURPOSE. -- -- As a special exception under Section 7 of GPL version 3, you are -- granted additional permissions described in the GCC Runtime -- Library Exception, version 3.1, as published by the Free Software -- Foundation. -- -- You should have received a copy of the GNU General Public License -- and a copy of the GCC Runtime Library Exception along with this -- program; see the files COPYING3 and COPYING.RUNTIME respectively. -- If not, see . -- -- Copyright (C) 2003-2022, Simon Wright with Ada.Dynamic_Priorities; with Ada.Exceptions; with Ada.Text_IO; with EWS.HTTP; with Interfaces.C.Strings; package body EWS.Server is task type Server (With_Stack : Positive) is pragma Storage_Size (With_Stack); entry Start (Using_Port : GNAT.Sockets.Port_Type; At_Priority : System.Priority; Logging_Via : Logger; Tracing : Boolean); end Server; type Server_P is access Server; procedure Respond (To : GNAT.Sockets.Socket_Type; In_Sockets : in out GNAT.Sockets.Socket_Set_Type; Logging_Via : Logger; Tracing : Boolean); -- Logging/tracing procedure Default_Logger (Message : String; Level : Error_Level); procedure Log (Logging_Via : Logger; Message : String; With_Exception : Ada.Exceptions.Exception_Occurrence); function Resolve_Exception (Occurrence : Ada.Exceptions.Exception_Occurrence) return String; procedure Trace (Logging_Via : Logger; Message : String; Skt : GNAT.Sockets.Socket_Type; Tracing : Boolean); procedure Trace (Logging_Via : Logger; Message : String; Tracing : Boolean); task body Server is Port : GNAT.Sockets.Port_Type; Priority : System.Priority; Logging_Via : Logger; Tracing : Boolean; Address : GNAT.Sockets.Sock_Addr_Type; Server_Socket : GNAT.Sockets.Socket_Type; Sockets : GNAT.Sockets.Socket_Set_Type; Write_Sockets : GNAT.Sockets.Socket_Set_Type; -- never used Selector : GNAT.Sockets.Selector_Type; begin pragma Warnings (Off, "call to obsolescent procedure *"); GNAT.Sockets.Initialize; pragma Warnings (On, "call to obsolescent procedure *"); accept Start (Using_Port : GNAT.Sockets.Port_Type; At_Priority : System.Priority; Logging_Via : Logger; Tracing : Boolean) do Port := Using_Port; Priority := At_Priority; if Logging_Via = null then Server.Logging_Via := Default_Logger'Access; else Server.Logging_Via := Logging_Via; end if; Server.Tracing := Tracing; end Start; Ada.Dynamic_Priorities.Set_Priority (Priority); Address.Addr := GNAT.Sockets.Any_Inet_Addr; Address.Port := Port; GNAT.Sockets.Create_Socket (Server_Socket); GNAT.Sockets.Set_Socket_Option (Server_Socket, GNAT.Sockets.Socket_Level, (GNAT.Sockets.Reuse_Address, True)); GNAT.Sockets.Bind_Socket (Server_Socket, Address); GNAT.Sockets.Listen_Socket (Server_Socket); GNAT.Sockets.Set (Sockets, Server_Socket); GNAT.Sockets.Create_Selector (Selector); loop declare Read_Sockets : GNAT.Sockets.Socket_Set_Type; Status : GNAT.Sockets.Selector_Status; use type GNAT.Sockets.Selector_Status; begin -- Initialize Read_Sockets with the sockets in use -- (Write_Sockets remains empty, we don't care if a -- socket becomes writable; we'll just block in that -- case). GNAT.Sockets.Copy (Sockets, Read_Sockets); -- Wait until something happens on one of the sockets. GNAT.Sockets.Check_Selector (Selector, Read_Sockets, Write_Sockets, Status); if Status = GNAT.Sockets.Completed then -- This was a successful completion. Find out which -- socket woke us up and deal with it. If there was -- more than one, we'll find out next time round the -- loop. declare Socket : GNAT.Sockets.Socket_Type; use type GNAT.Sockets.Socket_Type; begin -- Which socket? GNAT.Sockets.Get (Read_Sockets, Socket); if Socket = Server_Socket then -- It was the server; a new client has called -- connect(). Accept the connection ... GNAT.Sockets.Accept_Socket (Server_Socket, Socket, Address); Trace (Logging_Via, "connection", Socket, Tracing); -- ... and add the new connected socket to the -- set of sockets in use. GNAT.Sockets.Set (Sockets, Socket); elsif Socket = GNAT.Sockets.No_Socket then -- None of our sockets has data/connection -- available; don't care why. Logging_Via ("server got No_Socket", Error); else -- There's an HTTP request to be read on one of -- our connected clients' sockets; deal with it. Trace (Logging_Via, "request", Socket, Tracing); Respond (Socket, Sockets, Logging_Via, Tracing); end if; end; else -- Unexpected, non-fatal error. Logging_Via ("server: Check_Selector returned " & Status'Img, Error); end if; -- Clean up. GNAT.Sockets.Empty (Read_Sockets); exception when E : others => Log (Logging_Via, "server failed in inner loop", With_Exception => E); end; end loop; exception when E : others => Log (Logging_Via, "server task failed", With_Exception => E); GNAT.Sockets.Close_Socket (Server_Socket); end Server; procedure Serve (Using_Port : GNAT.Sockets.Port_Type; At_Priority : System.Priority := System.Default_Priority; With_Stack : Positive := 20_000; Logging_Via : Logger := null; Tracing : Boolean := False) is EWS_Server : constant Server_P := new Server (With_Stack); begin EWS_Server.Start (Using_Port, At_Priority, Logging_Via, Tracing); end Serve; procedure Respond (To : GNAT.Sockets.Socket_Type; In_Sockets : in out GNAT.Sockets.Socket_Set_Type; Logging_Via : Logger; Tracing : Boolean) is procedure Close; procedure Close is begin GNAT.Sockets.Clear (In_Sockets, To); GNAT.Sockets.Close_Socket (To); end Close; Request : aliased HTTP.Request; Terminated : Boolean; begin begin HTTP.Initialize (Request, From => To, Terminated => Terminated); exception when E : others => Log (Logging_Via, "failed reading request", With_Exception => E); return; end; if Terminated then Trace (Logging_Via, "connection terminated", Tracing); Close; return; end if; Trace (Logging_Via, "method " & HTTP.Get_Method (Request) & ", version " & HTTP.Get_Version (Request) & ", url " & HTTP.Get_URL (Request), Tracing); begin HTTP.Respond (HTTP.Find (Request'Unchecked_Access), To => To); exception when GNAT.Sockets.Socket_Error => -- Going to assume that a socket error occurs because of -- some browser behaviour (they've closed the socket -- without waiting for the response). Close; return; when E : others => Log (Logging_Via, "failed in respond", With_Exception => E); begin HTTP.Respond (HTTP.Exception_Response (E, Request'Unchecked_Access), To => To); exception when others => null; end; Close; return; end; if not HTTP.Keep_Alive_After_Responding (Request) then Close; end if; end Respond; procedure Default_Logger (Message : String; Level : Error_Level) is begin Ada.Text_IO.Put_Line (Ada.Text_IO.Standard_Error, "EWS: " & Level'Img & ": " & Message); end Default_Logger; procedure Log (Logging_Via : Logger; Message : String; With_Exception : Ada.Exceptions.Exception_Occurrence) is use Ada.Exceptions; begin if Exception_Identity (With_Exception) = GNAT.Sockets.Socket_Error'Identity then begin Logging_Via (Message & ", " & Resolve_Exception (With_Exception) & ", " & Exception_Information (With_Exception), Error); exception -- If the special Socket_Error handling fails (XXX why -- would it?), revert to the standard case. when others => Logging_Via (Message & ", " & Exception_Information (With_Exception), Error); end; else Logging_Via (Message & ", " & Exception_Information (With_Exception), Error); end if; end Log; function Resolve_Exception (Occurrence : Ada.Exceptions.Exception_Occurrence) return String is -- Fragmentarily copied from GNAT.Sockets function of the same -- name. Error : constant GNAT.Sockets.Error_Type := GNAT.Sockets.Resolve_Exception (Occurrence); begin case Error is when GNAT.Sockets.Cannot_Resolve_Error => -- The errno is at the start of the exception message -- (inside []). declare function C_Strerror (Errnum : Interfaces.C.int) return Interfaces.C.Strings.chars_ptr; pragma Import (C, C_Strerror, "strerror"); Msg : String renames Ada.Exceptions.Exception_Message (Occurrence); First : Natural; Last : Natural; Errno : Integer; C_Msg : Interfaces.C.Strings.chars_ptr; use type Interfaces.C.Strings.chars_ptr; begin First := Msg'First; while First <= Msg'Last and then Msg (First) not in '0' .. '9' loop First := First + 1; end loop; if First > Msg'Last then return "Cannot_Resolve_Error"; end if; Last := First; while Last < Msg'Last and then Msg (Last + 1) in '0' .. '9' loop Last := Last + 1; end loop; Errno := Integer'Value (Msg (First .. Last)); C_Msg := C_Strerror (Interfaces.C.int (Errno)); if C_Msg = Interfaces.C.Strings.Null_Ptr then return "unknown error " & Errno'Img; else return Interfaces.C.Strings.Value (C_Msg); end if; end; when others => return Error'Img; end case; end Resolve_Exception; procedure Trace (Logging_Via : Logger; Message : String; Skt : GNAT.Sockets.Socket_Type; Tracing : Boolean) is begin if Tracing then Logging_Via (Message & ", socket" & GNAT.Sockets.Image (Skt) & " from " & GNAT.Sockets.Image (GNAT.Sockets.Get_Peer_Name (Skt)), Trace); end if; exception -- Only seen on Mandrake 10 on browser close. Leave it to -- Respond to clear up when it sees the send failure. when E : GNAT.Sockets.Socket_Error => Log (Logging_Via, "failed in trace (" & Message & ")", With_Exception => E); end Trace; procedure Trace (Logging_Via : Logger; Message : String; Tracing : Boolean) is begin if Tracing then Logging_Via (Message, Trace); end if; end Trace; end EWS.Server;