-- 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
-- Copyright (C) 2022, Stephane Carrez
pragma Ada_2012;
with Ada.Characters.Latin_1; use Ada.Characters.Latin_1;
with Ada.Strings.Bounded;
with Ada.Strings.Fixed;
with Ada.Strings.Maps.Constants;
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Unchecked_Deallocation;
with GNAT.Regpat;
with Interfaces.C;
with EWS.Dynamic;
with EWS.Static;
package body EWS.HTTP is
package Str is new Ada.Strings.Bounded.Generic_Bounded_Length (1024);
use GNAT.Sockets;
package SS renames Smart_Strings;
-- RFC 3986 Uniform Resource Identifier (URI): Generic Syntax,
-- Appendix B. Parsing a URI Reference with a Regular Expression
-- defines the following regular expression:
-- ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
URL_Request : constant String :=
"(\r\n)?"
& "(GET|POST|HEAD|PUT|DELETE|OPTIONS|PATCH)" -- request, 2
& "\s"
& "(/|((/[a-z0-9._-]+)+)/?)" -- the URL, 3
& "(\?([^#]*))?" -- query, 6
& "\s"
& "HTTP/(\d\.\d)" -- version, 8
& "\r\n";
Method_Match : constant := 2;
URL_Match : constant := 3;
Query_Match : constant := 6;
Version_Match : constant := 8;
URL_Matcher : constant GNAT.Regpat.Pattern_Matcher :=
GNAT.Regpat.Compile (URL_Request,
Flags => GNAT.Regpat.Case_Insensitive);
URL_Max_Parens : constant GNAT.Regpat.Match_Count :=
GNAT.Regpat.Paren_Count (URL_Matcher);
CRLF : constant String := CR & LF;
---------------------
-- Utility specs --
---------------------
procedure Determine_Line_Style (Used_In : in out Cursor);
procedure Free_Stream
is new Ada.Unchecked_Deallocation (Ada.Streams.Root_Stream_Type'Class,
Stream_Access);
function Get_Content_Length (From : String) return Natural;
function Index
(Source : String;
Pattern : String;
From : Positive;
Going : Ada.Strings.Direction := Ada.Strings.Forward;
Mapping : Ada.Strings.Maps.Character_Mapping
:= Ada.Strings.Maps.Identity)
return Natural;
-- A replacement for Ada05 Ada.Strings.Fixed.Index.
function Plus_To_Space (S : String) return String;
function Read_Request (From : Socket_Type) return String;
function To_String
(In_String : String;
From : GNAT.Regpat.Match_Location) return String
with Inline;
function Unescape (S : String) return String;
-------------------------
-- Public operations --
-------------------------
procedure Initialize (R : out Request;
From : GNAT.Sockets.Socket_Type;
Terminated : out Boolean)
is
S : Stream_Access := Stream (From);
begin
R.Head := SS.Create (new String'(Read_Request (From)));
declare
Content_Length : constant Natural
:= Get_Content_Length (SS.Value (R.Head).all);
begin
if Content_Length > 0 then
R.Content := SS.Create (new String (1 .. Content_Length));
String'Read (S, SS.Value (R.Content).all);
end if;
end;
Free_Stream (S);
Terminated := SS.Value (R.Head)'Length = 0;
exception
when GNAT.Sockets.Socket_Error =>
-- This is what happens on VxWorks when the peer closes the
-- socket; other OSs happily read an empty header.
Terminated := True;
end Initialize;
function Get_Method (From : Request) return Method
is
Matches : GNAT.Regpat.Match_Array (0 .. URL_Max_Parens);
Input : String renames SS.Value (From.Head).all;
use type GNAT.Regpat.Match_Location;
begin
GNAT.Regpat.Match (URL_Matcher, Input, Matches);
if Matches (0) = GNAT.Regpat.No_Match then
return "";
else
return To_String (Input, Matches (Method_Match));
end if;
end Get_Method;
function Get_Version (From : Request) return Version
is
Matches : GNAT.Regpat.Match_Array (0 .. URL_Max_Parens);
Input : String renames SS.Value (From.Head).all;
use type GNAT.Regpat.Match_Location;
begin
GNAT.Regpat.Match (URL_Matcher, Input, Matches);
if Matches (0) = GNAT.Regpat.No_Match then
return "";
else
return To_String (Input, Matches (Version_Match));
end if;
end Get_Version;
function Get_URL (From : Request) return URL
is
Matches : GNAT.Regpat.Match_Array (0 .. URL_Max_Parens);
Input : String renames SS.Value (From.Head).all;
use type GNAT.Regpat.Match_Location;
begin
GNAT.Regpat.Match (URL_Matcher, Input, Matches);
if Matches (0) = GNAT.Regpat.No_Match then
return "";
else
return Unescape (To_String (Input, Matches (URL_Match)));
end if;
end Get_URL;
function Get_Property (Named : String;
From : Request) return Property
is
Query_Matches : GNAT.Regpat.Match_Array (0 .. URL_Max_Parens);
Query_Input : String renames SS.Value (From.Head).all;
Property_Matcher : constant GNAT.Regpat.Pattern_Matcher :=
GNAT.Regpat.Compile ("(^|&|\?)" & Named & "=([^&]*)",
Flags => GNAT.Regpat.Case_Insensitive);
Property_Matches : GNAT.Regpat.Match_Array (0 .. 2);
use type GNAT.Regpat.Match_Location;
begin
GNAT.Regpat.Match (URL_Matcher, Query_Input, Query_Matches);
-- which has to succeed, we wouldn't get here with an illegal head
pragma Assert (Query_Matches (0) /= GNAT.Regpat.No_Match);
if Ada.Strings.Fixed.Translate
(To_String (Query_Input, Query_Matches (Method_Match)),
Ada.Strings.Maps.Constants.Upper_Case_Map)
in "GET" | "OPTIONS" | "HEAD"
and then Query_Matches (Query_Match) /= GNAT.Regpat.No_Match
then
declare
Property_Input : constant String :=
To_String (Query_Input, Query_Matches (Query_Match));
begin
GNAT.Regpat.Match
(Property_Matcher, Property_Input, Property_Matches);
if Property_Matches (0) = GNAT.Regpat.No_Match then
return "";
else
return Plus_To_Space
(Unescape (To_String (Property_Input, Property_Matches (2))));
end if;
end;
elsif Ada.Strings.Fixed.Translate
(To_String (Query_Input, Query_Matches (Method_Match)),
Ada.Strings.Maps.Constants.Upper_Case_Map)
in "POST" | "PUT" | "DELETE" | "PATCH"
and then SS.Value (From.Content) /= null
then
declare
Property_Input : String renames SS.Value (From.Content).all;
begin
GNAT.Regpat.Match
(Property_Matcher, Property_Input, Property_Matches);
if Property_Matches (0) = GNAT.Regpat.No_Match then
return "";
else
return Plus_To_Space
(Unescape (To_String (Property_Input, Property_Matches (2))));
end if;
end;
else
return "";
end if;
end Get_Property;
function Get_Field (Named : String; From : Request) return Property
is
Field_Request : constant String
:= Named & ":\s*([[:^cntrl:]]+(\r\n\s[[:^cntrl:]]+)*)";
Field_Matcher : constant GNAT.Regpat.Pattern_Matcher :=
GNAT.Regpat.Compile (Field_Request,
Flags => GNAT.Regpat.Case_Insensitive);
Field_Max_Parens : constant GNAT.Regpat.Match_Count :=
GNAT.Regpat.Paren_Count (Field_Matcher);
Matches : GNAT.Regpat.Match_Array (0 .. Field_Max_Parens);
use type GNAT.Regpat.Match_Location;
begin
GNAT.Regpat.Match (Field_Matcher, SS.Value (From.Head).all, Matches);
if Matches (0) = GNAT.Regpat.No_Match then
return "";
else
return To_String (SS.Value (From.Head).all, Matches (1));
end if;
end Get_Field;
function Keep_Alive_After_Responding (The_Request : Request) return Boolean
is
begin
if Get_Version (The_Request) = "1.0" then
return Get_Field ("Connection", From => The_Request) = "Keep-Alive";
else
return Get_Field ("Connection", From => The_Request) /= "close";
end if;
end Keep_Alive_After_Responding;
-- Debug support
function Get_Head (From : Request) return String
is
begin
return SS.Value (From.Head).all;
end Get_Head;
function Get_Body (From : Request) return String
is
begin
return SS.Value (From.Content).all;
end Get_Body;
-- Content/attachment management --
function Get_Attachments (From : Request) return Attachments
is
begin
return Attachments (From);
end Get_Attachments;
procedure Clear (The_Attachments : in out Attachments)
is
begin
The_Attachments := (Head => SS.Null_Pointer, Content => SS.Null_Pointer);
end Clear;
function Get_Field (Named : String;
From : Attachments;
Index : Positive := 1) return Property
is
begin
if SS.Value (From.Content) = null then
return "";
else
declare
Part_Start : Positive;
Part_Finish : Natural;
begin
Locate_Whole_Body_Part (From, Index, Part_Start, Part_Finish);
declare
Whole_Part : String
renames SS.Value (From.Content) (Part_Start .. Part_Finish);
Finish : constant Natural :=
HTTP.Index (Whole_Part, CRLF & CRLF);
Headers : String
renames Whole_Part (Whole_Part'First .. Finish);
Field_Request : constant String
:= Named & ":\s*([[:^cntrl:]]+(\r\n\s[[:^cntrl:]]+)*)";
Field_Matcher : constant GNAT.Regpat.Pattern_Matcher :=
GNAT.Regpat.Compile (Field_Request,
Flags => GNAT.Regpat.Case_Insensitive);
Field_Max_Parens : constant GNAT.Regpat.Match_Count :=
GNAT.Regpat.Paren_Count (Field_Matcher);
Matches : GNAT.Regpat.Match_Array (0 .. Field_Max_Parens);
use type GNAT.Regpat.Match_Location;
begin
GNAT.Regpat.Match (Field_Matcher, Headers, Matches);
if Matches (0) = GNAT.Regpat.No_Match then
return "";
else
return To_String (Headers, Matches (1));
end if;
end;
end;
end if;
end Get_Field;
-- String content
Empty_String : aliased constant String := "";
function Get_Content (From : Attachments;
Index : Positive := 1) return Contents
is
begin
if SS.Value (From.Content) = null then
return Empty_String'Access;
else
declare
Part_Start : Positive;
Part_Finish : Natural;
begin
Locate_Whole_Body_Part (From, Index, Part_Start, Part_Finish);
declare
Whole_Part : String
renames SS.Value (From.Content) (Part_Start .. Part_Finish);
Start : constant Natural :=
HTTP.Index (Whole_Part, CRLF & CRLF);
begin
-- Strip the header fields (if any) & the CRLF delimiter
-- pair.
return new String'(Whole_Part (Start + 4 .. Whole_Part'Last));
end;
end;
end if;
end Get_Content;
function Get_Content_Kind (From : Contents) return Content_Kind
is
(if (for some C of From.all => Character'Pos (C) > 127) then
Binary
else
Text);
-- Text content
procedure Open (C : in out Cursor;
From : Attachments;
Index : Positive := 1)
is
begin
if C.Open then
raise Status_Error;
end if;
C.Open := True;
C.Line_Ending := Unknown;
C.Data := Get_Content (From, Index);
C.Start := C.Data'First;
C.Last := C.Data'Last;
C.Next := C.Start;
Determine_Line_Style (C);
end Open;
procedure Open (C : in out Cursor;
From : Contents)
is
begin
if C.Open then
raise Status_Error;
end if;
C.Open := True;
C.Line_Ending := Unknown;
C.Data := From;
C.Start := C.Data'First;
C.Last := C.Data'Last;
C.Next := C.Start;
Determine_Line_Style (C);
end Open;
procedure Close (C : in out Cursor)
is
begin
if not C.Open then
raise Status_Error;
end if;
C.Open := False;
-- XXXXXXXXXXXXXXXXXXXXXXXX Clear (C.Data);
end Close;
function End_Of_File (C : Cursor) return Boolean
is
begin
if not C.Open then
raise Status_Error;
end if;
return C.Next > C.Last;
end End_Of_File;
procedure Get_Line (C : in out Cursor;
Line : out String;
Last : out Natural)
is
begin
if not C.Open then
raise Status_Error;
end if;
if C.Next > C.Last then
raise End_Error;
end if;
declare
Text : String renames C.Data.all;
CR : constant String := (1 => ASCII.CR);
LF : constant String := (1 => ASCII.LF);
Terminator : Natural;
begin
case C.Line_Ending is
when Unknown =>
raise Program_Error;
when Unterminated =>
Terminator := 0;
when Unix =>
Terminator := Index (Text, LF, C.Next);
when Windows =>
Terminator := Index (Text, CR, C.Next);
end case;
if Terminator = 0 then
-- NB, this covers the case when there's no line ending
-- left. The whole of the rest of the string is
-- available
Terminator := C.Last + 1;
end if;
declare
Source_Length : constant Natural := Terminator - C.Next;
Target_Length : constant Natural := Line'Length;
begin
if Source_Length = 0 then
Last := Line'First - 1;
elsif Source_Length <= Target_Length then
Last := Line'First + Source_Length - 1;
Line (Line'First .. Line'First + Source_Length - 1)
:= Text (C.Next .. C.Next + Source_Length - 1);
C.Next := C.Next + Source_Length;
else
Last := Line'Last;
Line := Text (C.Next .. C.Next + Target_Length - 1);
C.Next := C.Next + Target_Length;
end if;
end;
-- skip pending LF or CRLF
if C.Next <= C.Last then
if C.Line_Ending = Unix and then Text (C.Next) = LF (1) then
C.Next := C.Next + 1;
elsif C.Line_Ending = Windows and then Text (C.Next) = CR (1) then
C.Next := C.Next + 2;
end if;
end if;
end;
end Get_Line;
---------------------------
-- Response management --
---------------------------
function Find (For_Request : not null access Request) return Response'Class
is
begin
declare
R : constant Response'Class
:= Dynamic.Find (For_Request);
begin
if R in Dynamic.Dynamic_Response'Class then
return R;
end if;
end;
return Static.Find (For_Request);
exception
when E : others =>
Put_Line ("failed in read/respond, "
& Ada.Exceptions.Exception_Information (E));
return Exception_Response (E, For_Request);
end Find;
-------------------------------
-- Default implementations --
-------------------------------
function Response_Kind (This : Response) return String
is
pragma Unreferenced (This);
begin
return "200 OK";
end Response_Kind;
function Cacheable (This : Response) return Boolean
is
pragma Unreferenced (This);
begin
return True;
end Cacheable;
function Content_Type (This : Response) return String
is
pragma Unreferenced (This);
begin
return "text/plain";
end Content_Type;
function Content_Length (This : Response) return Integer
is
begin
return Content (Response'Class (This))'Length;
-- NB the dispatching call.
end Content_Length;
function Content (This : Response) return String
is
pragma Unreferenced (This);
begin
return "";
end Content;
function Headers (This : Response) return String
is
begin
return "Content-Type: " & Content_Type (Response'Class (This)) & CRLF &
"Content-Length: " & Content_Length (Response'Class (This))'Img & CRLF;
-- NB the dispatching call.
end Headers;
procedure Write_Content
(This : Response;
To : not null access Ada.Streams.Root_Stream_Type'Class)
is
begin
String'Write (To, Content (Response'Class (This)));
-- NB the dispatching call.
end Write_Content;
procedure Respond (This : Response'Class;
To : GNAT.Sockets.Socket_Type)
is
U : aliased Unbounded_Memory_Stream;
begin
if Get_Version (This.To.all) = "1.0" then
String'Write
(U'Access,
"HTTP/1.0 " & Response_Kind (This) & CRLF &
"Server: EWS" & CRLF &
Headers (This));
if Keep_Alive_After_Responding (This.To.all) then
String'Write (U'Access, "Connection: Keep-Alive" & CRLF);
end if;
else
String'Write
(U'Access,
"HTTP/1.1 " & Response_Kind (This) & CRLF &
"Server: EWS" & CRLF &
Headers (This));
if not Keep_Alive_After_Responding (This.To.all) then
String'Write (U'Access, "Connection: close" & CRLF);
end if;
end if;
if not Cacheable (This) then
String'Write (U'Access, "Cache-Control: no-cache" & CRLF);
end if;
String'Write (U'Access, CRLF);
Write_Content (This, U'Access);
Copy (U, To);
end Respond;
------------------------------------------------------
-- Simple (error) responses and factory functions --
------------------------------------------------------
type Not_Found_Response (To : Request_P)
is new Response (To) with null record;
overriding
function Response_Kind (This : Not_Found_Response) return String;
overriding
function Content (This : Not_Found_Response) return String;
function Not_Found
(R : not null access Request) return Response'Class is
begin
return Not_Found_Response'(To => Request_P (R));
end Not_Found;
type Not_Implemented_Response (To : Request_P)
is new Response (To) with null record;
overriding
function Response_Kind (This : Not_Implemented_Response) return String;
overriding
function Content (This : Not_Implemented_Response) return String;
function Not_Implemented
(R : not null access Request) return Response'Class is
begin
return Not_Implemented_Response'(To => Request_P (R));
end Not_Implemented;
type Exception_Response_T (To : Request_P)
is new Response (To) with record
Info : Str.Bounded_String;
end record;
overriding
function Response_Kind (This : Exception_Response_T) return String;
overriding
function Content (This : Exception_Response_T) return String;
function Exception_Response
(E : Ada.Exceptions.Exception_Occurrence;
R : access Request) return Response'Class
is
Info : constant String := Ada.Exceptions.Exception_Information (E);
begin
if Info'Length > Str.Max_Length then
return Exception_Response_T'
(To => Request_P (R),
Info =>
Str.To_Bounded_String
(Info (Info'First .. Info'First + Str.Max_Length - 1)));
else
return Exception_Response_T'
(To => Request_P (R),
Info => Str.To_Bounded_String (Info));
end if;
end Exception_Response;
----------------------
-- Utility bodies --
----------------------
procedure Determine_Line_Style (Used_In : in out Cursor)
is
Text : String renames Used_In.Data.all;
CR : constant String := (1 => ASCII.CR);
LF : constant String := (1 => ASCII.LF);
begin
if Used_In.Line_Ending = Unknown then
declare
CR_Index : constant Natural := Index (Text, CR, Used_In.Next);
LF_Index : constant Natural := Index (Text, LF, Used_In.Next);
begin
if CR_Index = 0 and LF_Index = 0 then
Used_In.Line_Ending := Unterminated;
elsif CR_Index = 0 then
Used_In.Line_Ending := Unix;
elsif LF_Index = 0 then
Used_In.Line_Ending := Windows;
elsif CR_Index < LF_Index then
Used_In.Line_Ending := Windows;
else
Used_In.Line_Ending := Unix;
end if;
end;
end if;
end Determine_Line_Style;
function Get_Content_Length (From : String) return Natural
is
Content_Length_Request : constant String :=
"Content-Length:\s([0-9]+)\r\n";
Content_Length_Matcher : constant GNAT.Regpat.Pattern_Matcher :=
GNAT.Regpat.Compile (Content_Length_Request,
Flags => GNAT.Regpat.Case_Insensitive);
Content_Length_Max_Parens : constant GNAT.Regpat.Match_Count :=
GNAT.Regpat.Paren_Count (Content_Length_Matcher);
Matches : GNAT.Regpat.Match_Array (0 .. Content_Length_Max_Parens);
use type GNAT.Regpat.Match_Location;
begin
GNAT.Regpat.Match (Content_Length_Matcher, From, Matches);
if Matches (0) = GNAT.Regpat.No_Match then
return 0;
else
return Natural'Value (To_String (From, Matches (1)));
end if;
end Get_Content_Length;
-- This is a reworking of the GNAT-GPL-2006
-- Ada.Strings.Search.Index which doesn't copy the whole Source
-- onto the stack.
function Index
(Source : String;
Pattern : String;
Going : Ada.Strings.Direction := Ada.Strings.Forward;
Mapping : Ada.Strings.Maps.Character_Mapping
:= Ada.Strings.Maps.Identity)
return Natural
is
Cur_Index : Natural;
Potential_Match : Boolean;
use Ada.Strings;
use Ada.Strings.Maps;
begin
if Pattern = "" then
raise Pattern_Error;
end if;
-- Forwards case
if Going = Forward then
for J in 1 .. Source'Length - Pattern'Length + 1 loop
Cur_Index := Source'First + J - 1;
Potential_Match := True;
for K in Pattern'Range loop
if Pattern (K) /=
Value (Mapping, Source (Cur_Index + K - 1))
then
Potential_Match := False;
exit;
end if;
end loop;
if Potential_Match then
return Cur_Index;
end if;
end loop;
-- Backwards case
else
for J in reverse 1 .. Source'Length - Pattern'Length + 1 loop
Cur_Index := Source'First + J - 1;
Potential_Match := True;
for K in Pattern'Range loop
if Pattern (K) /=
Value (Mapping, Source (Cur_Index + K - 1))
then
Potential_Match := False;
exit;
end if;
end loop;
if Potential_Match then
return Cur_Index;
end if;
end loop;
end if;
-- Fall through if no match found. Note that the loops are skipped
-- completely in the case of the pattern being longer than the source.
return 0;
end Index;
function Index
(Source : String;
Pattern : String;
From : Positive;
Going : Ada.Strings.Direction := Ada.Strings.Forward;
Mapping : Ada.Strings.Maps.Character_Mapping
:= Ada.Strings.Maps.Identity)
return Natural
is
Candidate : String renames Source (From .. Source'Last);
begin
return Index (Source => Candidate,
Pattern => Pattern,
Going => Going,
Mapping => Mapping);
end Index;
procedure Locate_Whole_Body_Part (Within : Attachments;
Index : Positive := 1;
Start : out Positive;
Finish : out Natural)
is
Content_Type : constant String := Get_Field ("Content-Type",
From => Request (Within));
Text : String_P renames SS.Value (Within.Content);
begin
if Text = null then
Start := 1;
Finish := 0;
return;
elsif Content_Type'Length = 0
or else HTTP.Index (Content_Type, "multipart") = 0
then
Start := Text'First;
Finish := Text'Last;
return;
end if;
declare
Marker : constant String := "boundary=";
Boundary : constant String :=
"--" &
Content_Type (HTTP.Index (Content_Type, Marker)
+ Marker'Length .. Content_Type'Last);
Part : Natural := 0;
begin
Start := Text'First + Boundary'Length + 1; -- past the CRLF
loop
if Start > Text'Last - Boundary'Length then
-- Problem with Index on GNAT-GPL-2006; would have
-- expected 0 even on the trailing boundary (should
-- have trailing --) but got Constraint_Error.
raise Name_Error;
end if;
Finish := HTTP.Index (Text.all, Boundary, Start);
if Finish < Start then
raise Name_Error;
end if;
Part := Part + 1;
if Part = Index then
-- Omit the trailing CRLF.
Finish := Finish - 3;
exit;
end if;
-- Not done yet, on to the next part.
Start := Finish + Boundary'Length + 1;
end loop;
end;
end Locate_Whole_Body_Part;
function Plus_To_Space (S : String) return String
is
Mapping : constant Ada.Strings.Maps.Character_Mapping
:= Ada.Strings.Maps.To_Mapping (From => "+",
To => " ");
begin
return Ada.Strings.Fixed.Translate (S, Mapping);
end Plus_To_Space;
function Read_Request (From : Socket_Type) return String
is
use type Ada.Streams.Stream_Element_Array;
use type Ada.Streams.Stream_Element_Offset;
Tmp : Ada.Streams.Stream_Element_Array (1 .. 2048);
Last : Ada.Streams.Stream_Element_Offset := Tmp'First - 1;
Next : Ada.Streams.Stream_Element_Offset;
Termination : constant Ada.Streams.Stream_Element_Array :=
(Character'Pos (CR),
Character'Pos (LF),
Character'Pos (CR),
Character'Pos (LF));
S : Stream_Access := Stream (From);
begin
-- We need to read the whole request from the client. Of course
-- we don't know how long it is. We can't just issue an
-- Ada.Streams.Read for a large buffer, because the client may
-- not have sent that much and if she hasn't we'll block until
-- she gives up and closes the socket. So we read a character
-- at a time until we've got the CR/LF/CR/LF which terminates
-- the line.
loop
Ada.Streams.Read (Stream => S.all,
Item => Tmp (Last + 1 .. Last + 1),
Last => Next);
exit when Next = Last;
Last := Last + 1;
exit when Last >= Termination'Length
and then Tmp (Last - 3 .. Last) = Termination;
exit when Last = Tmp'Last;
end loop;
Free_Stream (S);
declare
Result : String (1 .. Natural (Last)) with Import, Convention => Ada;
for Result'Address use Tmp'Address;
begin
return Result;
end;
end Read_Request;
function To_String
(In_String : String;
From : GNAT.Regpat.Match_Location) return String
is
Last : Natural := From.Last;
begin
-- Konqueror has been known to append a \0
while Last >= From.First and then In_String (Last) = ASCII.NUL loop
Last := Last - 1;
end loop;
return In_String (From.First .. Last);
end To_String;
function Unescape (S : String) return String
is
function Hex (H : String) return Natural;
function Hex (H : String) return Natural
is
Result : Natural := 0;
begin
for I in H'Range loop
declare
C : constant Character := H (I);
D : Natural;
begin
case C is
when '0' .. '9' =>
D := Character'Pos (C) - Character'Pos ('0');
when 'a' .. 'f' =>
D := Character'Pos (C) - Character'Pos ('a') + 16#A#;
when 'A' .. 'F' =>
D := Character'Pos (C) - Character'Pos ('A') + 16#A#;
when others =>
raise Constraint_Error;
end case;
Result := Result * 16#10# + D;
end;
end loop;
return Result;
end Hex;
Result : String (S'Range);
Next_In : Positive := S'First;
Next_Out : Positive := Next_In;
begin
if S'Length = 0 then
return S;
else
loop
if S (Next_In) /= '%' then
Result (Next_Out) := S (Next_In);
Next_In := Next_In + 1;
Next_Out := Next_Out + 1;
else
Result (Next_Out) :=
Character'Val (Hex (S (Next_In + 1 .. Next_In + 2)));
Next_In := Next_In + 3;
Next_Out := Next_Out + 1;
end if;
exit when Next_In > S'Last;
end loop;
return Result (Result'First .. Next_Out - 1);
end if;
end Unescape;
-----------------------------
-- Error response bodies --
-----------------------------
function Response_Kind (This : Not_Found_Response) return String
is
pragma Unreferenced (This);
begin
return "404 Not Found";
end Response_Kind;
function Content (This : Not_Found_Response) return String
is
pragma Unreferenced (This);
begin
return "Not found.";
end Content;
function Response_Kind (This : Not_Implemented_Response) return String
is
pragma Unreferenced (This);
begin
return "501 Not implemented";
end Response_Kind;
function Content (This : Not_Implemented_Response) return String
is
pragma Unreferenced (This);
begin
return "Not implemented.";
end Content;
function Response_Kind (This : Exception_Response_T) return String
is
pragma Unreferenced (This);
begin
return "500 Internal server error";
end Response_Kind;
function Content (This : Exception_Response_T) return String is
begin
return "Exception: " & Str.To_String (This.Info);
end Content;
---------------------------------------
-- Unbounded Memory Streams bodies --
---------------------------------------
procedure Free
is new Ada.Unchecked_Deallocation (Stream_Chunk, Stream_Chunk_P);
procedure Finalize (UMSF : in out Unbounded_Memory_Stream_Finalizer)
is
Current : Stream_Chunk_P := UMSF.UMS.Head;
begin
while Current /= null loop
declare
Next : constant Stream_Chunk_P := Current.Next;
begin
Free (Current);
Current := Next;
end;
end loop;
end Finalize;
procedure Copy (Stream : Unbounded_Memory_Stream;
To : GNAT.Sockets.Socket_Type)
is
Chunks : Natural := 0;
begin
declare
Chunk : Stream_Chunk_P := Stream.Head;
begin
while Chunk /= null loop
Chunks := Chunks + 1;
Chunk := Chunk.Next;
end loop;
end;
declare
use Ada.Streams;
Vector : GNAT.Sockets.Vector_Type (1 .. Chunks);
Chunk : Stream_Chunk_P := Stream.Head;
Index : Positive := Vector'First;
Bytes_To_Send : Stream_Element_Count := Stream.Length;
Bytes_Sent : Stream_Element_Count;
begin
while Chunk /= null loop
Vector (Index) :=
(Base => Chunk.Elements (Chunk.Elements'First)'Access,
Length => Interfaces.C.size_t
(Stream_Element_Offset'Min
(Bytes_To_Send, Stream_Chunk_Elements'Length)));
Bytes_To_Send :=
Bytes_To_Send - Stream_Element_Offset (Vector (Index).Length);
Chunk := Chunk.Next;
Index := Index + 1;
end loop;
GNAT.Sockets.Send_Vector (Socket => To,
Vector => Vector,
Count => Bytes_Sent);
pragma Assert (Bytes_Sent = Stream.Length,
"byte count mismatch in Copy");
end;
end Copy;
-- Read isn't meant to be called; output contents via Copy.
procedure Read (Stream : in out Unbounded_Memory_Stream;
Item : out Ada.Streams.Stream_Element_Array;
Last : out Ada.Streams.Stream_Element_Offset)
is
begin
raise Program_Error;
end Read;
procedure Write (Stream : in out Unbounded_Memory_Stream;
Item : Ada.Streams.Stream_Element_Array)
is
use Ada.Streams;
First_Byte_In_Item : Stream_Element_Offset := Item'First;
Bytes_To_Write : Stream_Element_Offset := Item'Length;
begin
loop
exit when Bytes_To_Write = 0;
declare
Bytes_Remaining_In_Chunk : constant Stream_Element_Count :=
Stream_Chunk_Elements'Length
- Stream.Length mod Stream_Chunk_Elements'Length;
-- NB! If Stream.Length (the number of bytes written so
-- far) is a multiple of Stream_Chunk_Elements'Length,
-- there will be no space left in the Tail chunk (if
-- there is one, there won't be at the first Write) BUT
-- the Bytes_Remaining_In_Chunk will be
-- Stream_Chunk_Elements'Length. In any other case, the
-- count will be correct.
begin
if Bytes_Remaining_In_Chunk = Stream_Chunk_Elements'Length then
-- The Tail chunk, if any, is full; allocate another.
declare
New_Chunk : constant Stream_Chunk_P := new Stream_Chunk;
begin
if Stream.Head = null then
-- If this is the first Write, both Head and
-- Tail will be null;
Stream.Head := New_Chunk;
Stream.Tail := New_Chunk;
else
-- otherwise, tack the new chunk on at Tail.
Stream.Tail.Next := New_Chunk;
Stream.Tail := New_Chunk;
end if;
end;
end if;
declare
First_Byte_In_Chunk : constant Stream_Element_Count
:= Stream_Chunk_Elements'Last - Bytes_Remaining_In_Chunk + 1;
Bytes_To_Write_Now : constant Stream_Element_Count
:= Stream_Element_Count'Min (Bytes_To_Write,
Bytes_Remaining_In_Chunk);
begin
Stream.Tail.Elements
(First_Byte_In_Chunk ..
First_Byte_In_Chunk + Bytes_To_Write_Now - 1)
:= Item
(First_Byte_In_Item ..
First_Byte_In_Item + Bytes_To_Write_Now - 1);
Bytes_To_Write := Bytes_To_Write - Bytes_To_Write_Now;
Stream.Length := Stream.Length + Bytes_To_Write_Now;
First_Byte_In_Item := First_Byte_In_Item + Bytes_To_Write_Now;
end;
end;
end loop;
end Write;
end EWS.HTTP;