------------------------------------------------------------------------------ -- Ada Web Server -- -- -- -- Copyright (C) 2005-2020, AdaCore -- -- -- -- This library 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. This library 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 -- -- . -- -- -- -- As a special exception, if other files instantiate generics from this -- -- unit, or you link this unit with other files to produce an executable, -- -- this unit does not by itself cause the resulting executable to be -- -- covered by the GNU General Public License. This exception does not -- -- however invalidate any other reasons why the executable file might be -- -- covered by the GNU Public License. -- ------------------------------------------------------------------------------ pragma Ada_2012; with Ada.Characters.Handling; with Ada.Directories; with Ada.Streams; with Ada.Streams.Stream_IO; with Ada.Strings.Fixed; with Ada.Strings.Maps; with Ada.Strings.Unbounded; with Ada.Text_IO; with GNAT.MD5; with GNAT.OS_Lib; with GNAT.Regexp; with AWS.Attachments; with AWS.Digest; with AWS.Dispatchers; with AWS.Headers.Values; with AWS.Hotplug; with AWS.Log; with AWS.Messages; with AWS.MIME; with AWS.Net; with AWS.Net.Buffered; with AWS.Net.WebSocket.Handshake_Error; with AWS.Net.WebSocket.Protocol.Draft76; with AWS.Net.WebSocket.Protocol.RFC6455; with AWS.Net.WebSocket.Registry.Utils; with AWS.Parameters; with AWS.Response.Set; with AWS.Server.Get_Status; with AWS.Session; with AWS.Status.Set; with AWS.Templates; with AWS.Translator; with AWS.URL; with AWS.Utils; package body AWS.Server.HTTP_Utils is use Ada.Streams; use Ada.Strings; use Ada.Strings.Unbounded; protected File_Upload_UID is procedure Get (ID : out Natural); -- returns a UID for file upload. This is to ensure that files -- coming from clients will always have different name. private UID : Natural := 0; end File_Upload_UID; ---------------------- -- Answer_To_Client -- ---------------------- procedure Answer_To_Client (HTTP_Server : in out AWS.Server.HTTP; Line_Index : Positive; C_Stat : in out AWS.Status.Data; Socket_Taken : in out Boolean; Will_Close : in out Boolean) is use type Messages.Status_Code; Admin_URI : constant String := CNF.Admin_URI (HTTP_Server.Properties); Answer : Response.Data; procedure Build_Answer; -- Build the Answer that should be sent to the client's browser procedure Create_Session; -- Create a session if needed function Status_Page (URI : String) return Response.Data; -- Handle status page function Is_Ignored (Answer : Response.Data) return Boolean; -- Returns True if the Answer is to be ignored based on If-Match or -- If-Not-Match and ETag if any. ------------------ -- Build_Answer -- ------------------ procedure Build_Answer is URL : constant AWS.URL.Object := AWS.Status.URI (C_Stat); URI : constant String := AWS.URL.Abs_Path (URL); begin -- Check if the status page, status page logo or status page images -- are requested. These are AWS internal data that should not be -- handled by AWS users. -- AWS Internal status page handling if Admin_URI'Length > 0 and then URI'Length >= Admin_URI'Length and then URI (URI'First .. URI'First + Admin_URI'Length - 1) = Admin_URI then Answer := Status_Page (URI); -- Check if the URL is trying to reference resource above Web root -- directory. elsif CNF.Check_URL_Validity (HTTP_Server.Properties) and then not AWS.URL.Is_Valid (URL) then -- 403 status code "Forbidden" Answer := Response.Build (Status_Code => Messages.S403, Content_Type => "text/plain", Message_Body => "Request " & URI & ASCII.LF & " trying to reach resource above the Web root directory."); -- Check if we have a websockets request elsif Headers.Values.Unnamed_Value_Exists (Status.Connection (C_Stat), "upgrade", Case_Sensitive => False) and then Headers.Values.Unnamed_Value_Exists (Status.Upgrade (C_Stat), "websocket", Case_Sensitive => False) then Answer := Response.WebSocket; else -- Otherwise, check if a session needs to be created Create_Session; -- and get answer from client callback declare use type Dispatchers.Handler_Class_Access; Found : Boolean; begin -- Check the hotplug filters Hotplug.Apply (HTTP_Server.Filters, C_Stat, Found, Answer); -- If no one applied, run the user callback if not Found then if HTTP_Server.New_Dispatcher /= null then HTTP_Server.Dispatcher_Sem.Write; Dispatchers.Free (HTTP_Server.Dispatcher); HTTP_Server.Dispatcher := HTTP_Server.New_Dispatcher; HTTP_Server.New_Dispatcher := null; HTTP_Server.Dispatcher_Sem.Release_Write; end if; HTTP_Server.Dispatcher_Sem.Read; -- Be sure to always release the read semaphore begin Answer := Dispatchers.Dispatch (HTTP_Server.Dispatcher.all, C_Stat); HTTP_Server.Dispatcher_Sem.Release_Read; exception when others => HTTP_Server.Dispatcher_Sem.Release_Read; raise; end; end if; -- Then check if the answer is to be ignored as per -- If-Match/If-None-Match and ETag values. if Is_Ignored (Answer) then Answer := Response.Acknowledge (Messages.S304); end if; end; AWS.Status.Set.Delete_Idle_Session (C_Stat); end if; end Build_Answer; -------------------- -- Create_Session -- -------------------- procedure Create_Session is begin if CNF.Session (HTTP_Server.Properties) and then (not Status.Has_Session (C_Stat) or else not Session.Exist (Status.Session (C_Stat))) then -- Generate the session ID Status.Set.Session (C_Stat); end if; end Create_Session; ---------------- -- Is_Ignored -- ---------------- function Is_Ignored (Answer : Response.Data) return Boolean is begin if Response.Has_Header (Answer, Messages.ETag_Token) then declare ETag : constant String := Response.Header (Answer, Messages.ETag_Token); H : constant Headers.List := Status.Header (C_Stat); begin -- The request must be ignored if the header If_Match is -- found and the ETag does not correspond or if the header -- If-None-Match is found and the ETag correspond. return (H.Exist (Messages.If_Match_Token) and then Strings.Fixed.Index (H.Get_Values (Messages.If_Match_Token), ETag) = 0) or else (H.Exist (Messages.If_None_Match_Token) and then Strings.Fixed.Index (H.Get_Values (Messages.If_None_Match_Token), ETag) /= 0); end; else return False; end if; end Is_Ignored; ----------------- -- Status_Page -- ----------------- function Status_Page (URI : String) return Response.Data is use type AWS.Status.Authorization_Type; Answer : Response.Data; Username : constant String := AWS.Status.Authorization_Name (C_Stat); Password : constant String := AWS.Status.Authorization_Password (C_Stat); Method : constant AWS.Status.Authorization_Type := AWS.Status.Authorization_Mode (C_Stat); procedure Answer_File (File_Name : String); -- Assign File to Answer response data ----------------- -- Answer_File -- ----------------- procedure Answer_File (File_Name : String) is begin Answer := Response.File (Content_Type => MIME.Content_Type (File_Name), Filename => File_Name); end Answer_File; begin -- First check for authentification if Method = AWS.Status.Digest then if AWS.Status.Authorization_Response (C_Stat) = GNAT.MD5.Digest (CNF.Admin_Password (HTTP_Server.Properties) & AWS.Status.Authorization_Tail (C_Stat)) then if not AWS.Digest.Check_Nonce (Status.Authorization_Nonce (C_Stat)) then return AWS.Response.Authenticate (CNF.Admin_Realm (HTTP_Server.Properties), AWS.Response.Digest, Stale => True); end if; else return AWS.Response.Authenticate (CNF.Admin_Realm (HTTP_Server.Properties), AWS.Response.Digest); end if; elsif (Method = AWS.Status.Basic and then CNF.Admin_Password (HTTP_Server.Properties) /= GNAT.MD5.Digest (Username & ':' & CNF.Admin_Realm (HTTP_Server.Properties) & ':' & Password)) or else Method = AWS.Status.None or else Password = "" then return Response.Authenticate (CNF.Admin_Realm (HTTP_Server.Properties), Response.Any); end if; if URI = Admin_URI then -- Status page begin Answer := Response.Build (Content_Type => MIME.Text_HTML, Message_Body => Get_Status (HTTP_Server)); exception when Templates.Template_Error => Answer := Response.Build (Content_Type => MIME.Text_HTML, Message_Body => "Status template error. Please check " & "that '" & CNF.Status_Page (HTTP_Server.Properties) & "' file is valid."); end; elsif URI = Admin_URI & "-logo" then -- Status page logo Answer_File (CNF.Logo_Image (HTTP_Server.Properties)); elsif URI = Admin_URI & "-uparr" then -- Status page hotplug up-arrow Answer_File (CNF.Up_Image (HTTP_Server.Properties)); elsif URI = Admin_URI & "-downarr" then -- Status page hotplug down-arrow Answer_File (CNF.Down_Image (HTTP_Server.Properties)); elsif URI = Admin_URI & "-HPup" then -- Status page hotplug up message Hotplug.Move_Up (HTTP_Server.Filters, Positive'Value (Status.Parameter (C_Stat, "N"))); Answer := Response.URL (Admin_URI); elsif URI = Admin_URI & "-HPdown" then -- Status page hotplug down message Hotplug.Move_Down (HTTP_Server.Filters, Positive'Value (Status.Parameter (C_Stat, "N"))); Answer := Response.URL (Admin_URI); else Answer := Response.Build (Content_Type => MIME.Text_HTML, Message_Body => "Invalid use of reserved status URI prefix: " & Admin_URI); end if; return Answer; end Status_Page; Need_Purge : Boolean := False; begin Build_Answer; if HTTP_Server.Slots.Phase (Line_Index) = Client_Data then -- User callback did not read clients message body. If client do not -- support 100 (Continue) response, we have to close -- socket to discard pending client data. Need_Purge := Status.Expect (C_Stat) /= Messages.S100_Continue; if not Will_Close then Will_Close := Need_Purge; end if; if Response.Status_Code (Answer) < Messages.S300 then Log.Write (HTTP_Server.Error_Log, C_Stat, "User does not upload server data but return status " & Messages.Image (Response.Status_Code (Answer))); end if; end if; Send (Answer, HTTP_Server, Line_Index, C_Stat, Socket_Taken, Will_Close); if Need_Purge then -- User callback did not read client data and client does not support -- 100 (Continue) response. We need clear socket input buffers to be -- able to close socket gracefully. declare use Ada.Real_Time; Socket : constant Net.Socket_Type'Class := Status.Socket (C_Stat); Buffer : Stream_Element_Array (1 .. 4096); Last : Stream_Element_Offset; Length : Stream_Element_Count := Status.Content_Length (C_Stat); Stamp : constant Time := Clock; Span : constant Time_Span := To_Time_Span (CNF.Receive_Timeout (HTTP_Server.Properties)); -- To do not spend too much time on wrong working clients Agent : constant String := Status.User_Agent (C_Stat); Fully : constant Boolean := Fixed.Index (Agent, "Firefox/") > 0 or else Fixed.Index (Agent, "konqueror/") > 0; -- JavaScript engine of some browsers does not read the server -- responce until successfully send the whole message body. -- So we have to read the whole body to let them chance to read -- the server answer. -- Tested for Firefox/43.0 and konqueror/4.14.9. -- Does not need this trick: -- OPR/32.0.1948.69 - Opera -- Midori/0.5 -- Chrome/47.0.2526.106 begin while (Fully and then Length > 0 and then Stamp - Clock <= Span) or else Socket.Pending > 0 loop Socket.Receive (Buffer, Last); Length := Length - Stream_Element_Count (Last); end loop; end; end if; end Answer_To_Client; --------------------- -- File_Upload_UID -- --------------------- protected body File_Upload_UID is --------- -- Get -- --------- procedure Get (ID : out Natural) is begin ID := UID; UID := UID + 1; end Get; end File_Upload_UID; ---------------------- -- Get_Message_Data -- ---------------------- procedure Get_Message_Data (HTTP_Server : AWS.Server.HTTP; Line_Index : Positive; C_Stat : in out AWS.Status.Data; Expect_100 : Boolean) is use type Status.Request_Method; type Message_Mode is (Root_Attachment, -- Read the root attachment Attachment, -- Read an attachment File_Upload); -- Read a file upload procedure Get_File_Data (Server_Filename : String; Filename : String; Start_Boundary : String; Mode : Message_Mode; Headers : AWS.Headers.List; End_Found : out Boolean); -- Read file data from the stream, set End_Found if the end-boundary -- signature has been read. Server_Filename is the filename to be used -- for on-disk content (Attachment and File_Upload mode). procedure File_Upload (Start_Boundary, End_Boundary : String; Parse_Boundary : Boolean); -- Handle file upload data coming from the client browser procedure Store_Attachments (Start_Boundary, End_Boundary : String; Parse_Boundary : Boolean; Root_Part_CID : String); -- Store attachments coming from the client browser function Get_File_Upload_UID return String; -- Returns a unique id for each file upload Status_Multipart_Boundary : Unbounded_String; Status_Root_Part_CID : Unbounded_String; Status_Content_Type : Unbounded_String; Sock : constant Net.Socket_Type'Class := Status.Socket (C_Stat); Attachments : AWS.Attachments.List; ----------------- -- File_Upload -- ----------------- procedure File_Upload (Start_Boundary, End_Boundary : String; Parse_Boundary : Boolean) is procedure Target_Filename (Filename : String; Server_Filename, Decoded_Server_Filename : out Unbounded_String); -- Returns the full path names (std and decoded) for the -- file as stored on the server side. --------------------- -- Target_Filename -- --------------------- procedure Target_Filename (Filename : String; Server_Filename, Decoded_Server_Filename : out Unbounded_String) is Upload_Path : constant String := CNF.Upload_Directory (HTTP_Server.Properties); File_Upload_UID : constant String := Get_File_Upload_UID; begin Server_Filename := To_Unbounded_String (Upload_Path & File_Upload_UID & '.' & Filename); Decoded_Server_Filename := To_Unbounded_String (Upload_Path & File_Upload_UID & '.' & URL.Decode (Filename)); end Target_Filename; Name : Unbounded_String; Filename : Unbounded_String; Server_Filename : Unbounded_String; Decoded_Server_Filename : Unbounded_String; Is_File_Upload : Boolean; Headers : AWS.Headers.List; End_Found : Boolean := False; -- Set to true when the end-boundary has been found begin -- File_Upload -- Reach the boundary if Parse_Boundary then loop declare Data : constant String := Net.Buffered.Get_Line (Sock); begin exit when Data = Start_Boundary; if Data = End_Boundary then -- This is the end of the multipart data return; end if; end; end loop; end if; -- Read header Headers.Read (Sock); if AWS.Headers.Get_Values (Headers, Messages.Content_Type_Token) = MIME.Application_Form_Data then -- This chunk is the form parameter Status.Set.Read_Body (Sock, C_Stat, Boundary => Start_Boundary); -- Skip CRLF after boundary declare Data : constant String := Net.Buffered.Get_Line (Sock) with Unreferenced; begin null; end; Status.Set.Parameters_From_Body (C_Stat); File_Upload (Start_Boundary, End_Boundary, False); else -- Read file upload parameters declare Data : constant String := AWS.Headers.Get_Values (Headers, Messages.Content_Disposition_Token); L_Name : constant String := AWS.Headers.Values.Search (Data, "name"); L_Filename : constant String := URL.Decode (AWS.Headers.Values.Search (Data, "filename")); -- Get the simple name as we do not want to expose the client -- full pathname to the user's callback. Microsoft Internet -- Explorer sends the full pathname, Firefox only send the -- simple name. begin Is_File_Upload := (L_Filename /= ""); Name := To_Unbounded_String (L_Name); if Is_File_Upload then Filename := To_Unbounded_String (URL.Encode (Directories.Simple_Name (L_Filename))); end if; end; -- Read file/field data if Is_File_Upload then -- This part of the multipart message contains file data if CNF.Upload_Directory (HTTP_Server.Properties) = "" then raise Constraint_Error with "File upload not supported by server " & CNF.Server_Name (HTTP_Server.Properties); end if; -- Set Server_Filename, the name of the file in the local file -- sytstem. Target_Filename (To_String (Filename), Server_Filename, Decoded_Server_Filename); if To_String (Filename) /= "" then -- First value is the unique name on the server side Status.Set.Add_Parameter (C_Stat, To_String (Name), To_String (Server_Filename)); -- Status.Set.Add_Parameter does not decode values -- Second value is the original name as found on the client -- side. Status.Set.Add_Parameter (C_Stat, To_String (Name), To_String (Filename)); -- Status.Set.Add_Parameter does not decode values -- Read file data, set End_Found if the end-boundary -- signature has been read. Get_File_Data (To_String (Decoded_Server_Filename), To_String (Filename), Start_Boundary, File_Upload, Headers, End_Found); -- Create an attachment entry, this will ensure that the -- physical file will be removed. It will also be possible -- to work with the attachment instead of the parameters set -- above. AWS.Attachments.Add (Attachments, Filename => To_String (Decoded_Server_Filename), Name => To_String (Filename), Content_Id => To_String (Name), Headers => Headers); Status.Set.Attachments (C_Stat, Attachments); if not End_Found then File_Upload (Start_Boundary, End_Boundary, False); end if; else -- There is no file for this multipart, user did not enter -- something in the field. File_Upload (Start_Boundary, End_Boundary, True); end if; else -- This part of the multipart message contains field values declare Value : Unbounded_String; begin loop declare L : constant String := Net.Buffered.Get_Line (Sock); begin End_Found := (L = End_Boundary); exit when End_Found or else L = Start_Boundary; -- Append this line to the value Utils.Append_With_Sep (Value, L, Sep => ASCII.CR & ASCII.LF); end; end loop; Status.Set.Add_Parameter (C_Stat, Name, Value, Decode => False); -- Do not decode values for multipart/form-data end; if not End_Found then File_Upload (Start_Boundary, End_Boundary, False); end if; end if; end if; end File_Upload; ------------------- -- Get_File_Data -- ------------------- procedure Get_File_Data (Server_Filename : String; Filename : String; Start_Boundary : String; Mode : Message_Mode; Headers : AWS.Headers.List; End_Found : out Boolean) is type Error_State is (No_Error, Name_Error, Device_Error); -- This state is to monitor the file upload process. If we receice -- Name_Error or Device_Error while writing data on disk we need to -- continue reading all data from the socket if we want to be able -- to send back an error message. function Check_EOF return Boolean; -- Returns True if we have reach the end of file data procedure Write (Buffer : Streams.Stream_Element_Array; Trim : Boolean) with Inline; -- Write buffer to the file, handle the Device_Error exception File : Streams.Stream_IO.File_Type; Buffer : Streams.Stream_Element_Array (1 .. 4 * 1_024); Index : Streams.Stream_Element_Offset := Buffer'First; Data : Streams.Stream_Element_Array (1 .. 1); Data2 : Streams.Stream_Element_Array (1 .. 2); Error : Error_State := No_Error; --------------- -- Check_EOF -- --------------- function Check_EOF return Boolean is Signature : constant Streams.Stream_Element_Array := (1 => 13, 2 => 10) & Translator.To_Stream_Element_Array (Start_Boundary); Buffer : Streams.Stream_Element_Array (1 .. Signature'Length); Index : Streams.Stream_Element_Offset := Buffer'First; procedure Write_Data; -- Put buffer data into the main buffer (Get_Data.Buffer). If -- the main buffer is not big enough, it will write the buffer -- into the file before. ---------------- -- Write_Data -- ---------------- procedure Write_Data is begin if Error /= No_Error then return; end if; if Get_File_Data.Buffer'Last < Get_File_Data.Index + Index - 1 then Write (Get_File_Data.Buffer (Get_File_Data.Buffer'First .. Get_File_Data.Index - 1), False); Get_File_Data.Index := Get_File_Data.Buffer'First; end if; Get_File_Data.Buffer (Get_File_Data.Index .. Get_File_Data.Index + Index - 2) := Buffer (Buffer'First .. Index - 1); Get_File_Data.Index := Get_File_Data.Index + Index - 1; end Write_Data; begin -- Check_EOF Buffer (Index) := 13; Index := Index + 1; loop Net.Buffered.Read (Sock, Data); if Data (1) = 13 then Write_Data; return False; end if; Buffer (Index) := Data (1); if Index = Buffer'Last then if Buffer = Signature then return True; else Write_Data; return False; end if; end if; Index := Index + 1; end loop; end Check_EOF; ----------- -- Write -- ----------- procedure Write (Buffer : Streams.Stream_Element_Array; Trim : Boolean) is begin if Error = No_Error then if Mode in Attachment .. File_Upload then Streams.Stream_IO.Write (File, Buffer); else -- This is the root part of an MIME attachment, append the -- data to the status record. Status.Set.Append_Body (C_Stat, Buffer, Trim); end if; end if; exception when Text_IO.Device_Error => Error := Device_Error; end Write; begin begin if Mode in Attachment .. File_Upload then Streams.Stream_IO.Create (File, Streams.Stream_IO.Out_File, Server_Filename); end if; exception when Text_IO.Name_Error => Error := Name_Error; end; Read_File : loop Net.Buffered.Read (Sock, Data); while Data (1) = 13 loop exit Read_File when Check_EOF; end loop; Buffer (Index) := Data (1); Index := Index + 1; if Index > Buffer'Last then Write (Buffer, False); Index := Buffer'First; HTTP_Server.Slots.Check_Data_Timeout (Line_Index); end if; end loop Read_File; if Index /= Buffer'First then Write (Buffer (Buffer'First .. Index - 1), True); end if; if Error = No_Error then case Mode is when Root_Attachment => null; when Attachment => Streams.Stream_IO.Close (File); AWS.Attachments.Add (Attachments, Server_Filename, Headers, Filename); when File_Upload => Streams.Stream_IO.Close (File); end case; end if; -- Check for end-boundary, at this point we have at least two -- chars. Either the terminating "--" or CR+LF. Net.Buffered.Read (Sock, Data2); if Data2 (2) = 10 then -- We have CR+LF, it is a start-boundary End_Found := False; else -- We have read the "--", read line terminator. This is the -- end-boundary. End_Found := True; Net.Buffered.Read (Sock, Data2); end if; if Error = Name_Error then -- We can't create the file, add a clear exception message raise HTTP_Utils.Name_Error with "Cannot create file " & Server_Filename; elsif Error = Device_Error then -- We can't write to the file, there is probably no space left -- on devide. raise HTTP_Utils.Device_Error with "No space left on device while writing " & Server_Filename; end if; end Get_File_Data; ------------------------- -- Get_File_Upload_UID -- ------------------------- function Get_File_Upload_UID return String is use GNAT; Pid : constant Natural := Integer'Max (0, OS_Lib.Pid_To_Integer (OS_Lib.Current_Process_Id)); -- On OS where Current_Process_Id is not support -1 is returned. We -- ensure that in this case the Pid is set to 0 in this case. UID : Natural; begin File_Upload_UID.Get (UID); return Utils.Image (Pid) & "-" & Utils.Image (UID); end Get_File_Upload_UID; ----------------------- -- Store_Attachments -- ----------------------- procedure Store_Attachments (Start_Boundary, End_Boundary : String; Parse_Boundary : Boolean; Root_Part_CID : String) is function Attachment_Filename (Extension : String) return String; -- Returns the full path name for the file as stored on the -- server side. ------------------------- -- Attachment_Filename -- ------------------------- function Attachment_Filename (Extension : String) return String is Upload_Path : constant String := CNF.Upload_Directory (HTTP_Server.Properties); begin if Extension = "" then return Upload_Path & Get_File_Upload_UID; else return Upload_Path & Get_File_Upload_UID & '.' & Extension; end if; end Attachment_Filename; Server_Filename : Unbounded_String; Content_Id : Unbounded_String; Headers : AWS.Headers.List; End_Found : Boolean := False; -- Set to true when the end-boundary has been found begin -- Store_Attachments -- Reach the boundary if Parse_Boundary then loop declare Data : constant String := Net.Buffered.Get_Line (Sock); begin exit when Data = Start_Boundary; if Data = End_Boundary then -- This is the end of the multipart data return; end if; end; end loop; end if; -- Read header Headers.Read (Sock); if AWS.Headers.Get_Values (Headers, Messages.Content_Type_Token) = MIME.Application_Form_Data then -- This chunk is the form parameter Status.Set.Read_Body (Sock, C_Stat, Boundary => "--" & To_String (Status_Multipart_Boundary)); -- Skip CRLF after boundary declare Data : constant String := Net.Buffered.Get_Line (Sock) with Unreferenced; begin null; end; Status.Set.Parameters_From_Body (C_Stat); Store_Attachments (Start_Boundary, End_Boundary, False, Root_Part_CID); else Content_Id := To_Unbounded_String (AWS.Headers.Get (Headers, Messages.Content_Id_Token)); -- Read file/field data if Content_Id = Status_Root_Part_CID then Get_File_Data ("", "", Start_Boundary, Root_Attachment, Headers, End_Found); else Server_Filename := To_Unbounded_String (Attachment_Filename (AWS.MIME.Extension (AWS.Headers.Values.Get_Unnamed_Value (AWS.Headers.Get (Headers, Messages.Content_Type_Token))))); Get_File_Data (To_String (Server_Filename), To_String (Server_Filename), Start_Boundary, Attachment, Headers, End_Found); end if; -- More attachments ? if End_Found then AWS.Status.Set.Attachments (C_Stat, Attachments); else Store_Attachments (Start_Boundary, End_Boundary, False, Root_Part_CID); end if; end if; end Store_Attachments; begin -- Get_Message_Data if Expect_100 then Net.Buffered.Put_Line (Sock, Messages.Status_Line (Messages.S100)); Net.Buffered.New_Line (Sock); Net.Buffered.Flush (Sock); end if; -- Get necessary data from header for reading HTTP body declare procedure Named_Value (Name, Value : String; Quit : in out Boolean); -- Looking for the Boundary value in the Content-Type header line procedure Value (Item : String; Quit : in out Boolean); -- Reading the first unnamed value into the Status_Content_Type -- variable from the Content-Type header line. ----------------- -- Named_Value -- ----------------- procedure Named_Value (Name, Value : String; Quit : in out Boolean) is pragma Unreferenced (Quit); L_Name : constant String := Ada.Characters.Handling.To_Lower (Name); begin if L_Name = "boundary" then Status_Multipart_Boundary := To_Unbounded_String (Value); elsif L_Name = "start" then Status_Root_Part_CID := To_Unbounded_String (Value); end if; end Named_Value; ----------- -- Value -- ----------- procedure Value (Item : String; Quit : in out Boolean) is begin if Status_Content_Type /= Null_Unbounded_String then -- Only first unnamed value is the Content_Type Quit := True; elsif Item'Length > 0 then Status_Content_Type := To_Unbounded_String (Item); end if; end Value; procedure Parse is new Headers.Values.Parse (Value, Named_Value); begin -- Clear Content-Type status as this could have already been set -- in previous request. Status_Content_Type := Null_Unbounded_String; Parse (Status.Content_Type (C_Stat)); end; if Status.Method (C_Stat) = Status.POST and then Status_Content_Type = MIME.Application_Form_Data then -- Read data from the stream and convert it to a string as -- these are a POST form parameters. -- The body has the format: name1=value1&name2=value2... Status.Set.Read_Body (Sock, C_Stat); Status.Set.Parameters_From_Body (C_Stat); elsif Status.Method (C_Stat) = Status.POST and then Status_Content_Type = MIME.Multipart_Form_Data then -- This is a file upload File_Upload ("--" & To_String (Status_Multipart_Boundary), "--" & To_String (Status_Multipart_Boundary) & "--", True); elsif Status.Method (C_Stat) = Status.POST and then Status_Content_Type = MIME.Multipart_Related then -- Attachments are to be written to separate files Store_Attachments ("--" & To_String (Status_Multipart_Boundary), "--" & To_String (Status_Multipart_Boundary) & "--", True, To_String (Status_Root_Part_CID)); else -- Let's suppose for now that all others content type data are -- binary data. Status.Set.Read_Body (Sock, C_Stat); end if; Status.Reset_Body_Index (C_Stat); HTTP_Server.Slots.Mark_Phase (Line_Index, Server_Processing); Status.Set.Uploaded (C_Stat); end Get_Message_Data; ---------------------- -- Get_Request_Line -- ---------------------- procedure Get_Request_Line (C_Stat : in out AWS.Status.Data) is Sock : constant Net.Socket_Type'Class := Status.Socket (C_Stat); begin -- Get and parse request line loop declare Data : constant String := Net.Buffered.Get_Line (Sock); begin -- RFC 2616 -- 4.1 Message Types -- .................... -- In the interest of robustness, servers SHOULD ignore any empty -- line(s) received where a Request-Line is expected. if Data /= "" then Parse_Request_Line (Data, C_Stat); exit; end if; end; end loop; end Get_Request_Line; ------------------------ -- Parse_Request_Line -- ------------------------ procedure Parse_Request_Line (Command : String; C_Stat : in out AWS.Status.Data) is I1, I2 : Natural; -- Index of first space and second space I3 : Natural; -- Index of ? if present in the URI (means that there is some -- parameters) procedure Cut_Command; -- Parse Command and set I1, I2 and I3 function Method return String with Inline; -- Returns the method function Resource return String with Inline; -- Returns first parameter. parameters are separated by spaces function Parameters return String; -- Returns parameters if some where specified in the URI function HTTP_Version return String with Inline; -- Returns second parameter. parameters are separated by spaces ----------------- -- Cut_Command -- ----------------- procedure Cut_Command is begin I1 := Fixed.Index (Command, " "); I2 := Fixed.Index (Command (I1 + 1 .. Command'Last), " ", Backward); I3 := Fixed.Index (Command (I1 + 1 .. I2 - 1), "?"); if I1 = 0 or else I2 = 0 or else I1 = I2 then raise Wrong_Request_Line with "Wrong request line '" & Command & '''; elsif I3 = 0 then -- Could be encoded ? I3 := Fixed.Index (Command (I1 + 1 .. I2 - 1), "%3f"); if I3 = 0 then I3 := Fixed.Index (Command (I1 + 1 .. I2 - 1), "%3F"); end if; end if; end Cut_Command; ------------------ -- HTTP_Version -- ------------------ function HTTP_Version return String is begin return Command (I2 + 1 .. Command'Last); end HTTP_Version; ------------ -- Method -- ------------ function Method return String is begin return Command (Command'First .. I1 - 1); end Method; ---------------- -- Parameters -- ---------------- function Parameters return String is begin if I3 = 0 then return ""; else if Command (I3) = '%' then return Command (I3 + 3 .. I2 - 1); else return Command (I3 + 1 .. I2 - 1); end if; end if; end Parameters; -------------- -- Resource -- -------------- function Resource return String is begin return Command (I1 + 1 .. (if I3 = 0 then I2 else I3) - 1); end Resource; begin Cut_Command; -- GET and HEAD can have a set of parameters (query) attached. This is -- not really standard see [RFC 2616 - 13.9] but is widely used now. -- -- POST parameters are passed into the message body, but we allow -- parameters also in this case. It is not clear if it is permitted or -- prohibited by reading RFC 2616. Other technologies do offer this -- feature so AWS do this as well. Status.Set.Request (C_Stat, Method, Resource, HTTP_Version); Status.Set.Query (C_Stat, Parameters); end Parse_Request_Line; ---------- -- Send -- ---------- procedure Send (Answer : in out Response.Data; HTTP_Server : in out AWS.Server.HTTP; Line_Index : Positive; C_Stat : AWS.Status.Data; Socket_Taken : in out Boolean; Will_Close : in out Boolean) is LA : constant Line_Attribute.Attribute_Handle := Line_Attribute.Reference; Status_Code : Messages.Status_Code := Response.Status_Code (Answer); Length : Resources.Content_Length_Type := 0; procedure Send_General_Header (Sock : Net.Socket_Type'Class); -- Send the "Date:", "Server:", "Set-Cookie:" and "Connection:" header procedure Send_Header_Only; -- Send HTTP message header only. This is used to implement the HEAD -- request. procedure Send_Data; -- Send a text/binary data to the client procedure Send_WebSocket_Handshake; -- Send reply, accept the switching protocol procedure Send_WebSocket_Handshake_Error (Status_Code : Messages.Status_Code; Reason_Phrase : String := ""); -- Deny the WebSocket handshake --------------- -- Send_Data -- --------------- procedure Send_Data is use type AWS.Status.Request_Method; type File_Status is (Changed, Up_To_Date, Not_Found); Sock : constant Net.Socket_Type'Class := Status.Socket (C_Stat); Method : constant AWS.Status.Request_Method := Status.Method (C_Stat); Filename : constant String := Response.Filename (Answer); File_Mode : constant Boolean := Response.Mode (Answer) in Response.File .. Response.Stream; With_Body : constant Boolean := Messages.With_Body (Status_Code); F_Status : File_Status := Changed; File : Resources.File_Type; File_Time : Ada.Calendar.Time := Utils.AWS_Epoch; begin if File_Mode and then Filename /= "" then if Resources.Is_Regular_File (Filename) then File_Time := Resources.File_Timestamp (Filename); if Utils.Is_Valid_HTTP_Date (Status.If_Modified_Since (C_Stat)) and then Messages.To_HTTP_Date (File_Time) = Status.If_Modified_Since (C_Stat) -- Equal used here see [RFC 2616 - 14.25] then F_Status := Up_To_Date; else F_Status := Changed; end if; else F_Status := Not_Found; end if; end if; if F_Status in Up_To_Date .. Not_Found then if F_Status = Up_To_Date then -- [RFC 2616 - 10.3.5] Status_Code := Messages.S304; else -- File is not found on disk, returns now with 404 Status_Code := Messages.S404; end if; Net.Buffered.Put_Line (Sock, Messages.Status_Line (Status_Code)); Send_General_Header (Sock); Net.Buffered.New_Line (Sock); Net.Buffered.Flush (Sock); return; elsif Headers.Get_Values (Status.Header (C_Stat), Messages.Range_Token) /= "" and then With_Body then -- Partial range request, answer accordingly Status_Code := Messages.S206; end if; Net.Buffered.Put_Line (Sock, Messages.Status_Line (Status_Code)); -- Note. We have to call Create_Resource before send header fields -- defined in the Answer to the client, because this call could -- setup Content-Encoding header field to Answer. Answer header -- lines would be send below in the Send_General_Header. Response.Create_Resource (Answer, File, AWS.Status.Is_Supported (C_Stat, Messages.GZip)); -- Length is the real resource/file size Length := Resources.Size (File); -- Checking if we have to close connection because of undefined -- message length coming from a user's stream. Or because of user -- do not want to keep connection alive. if (Length = Resources.Undefined_Length and then Status.HTTP_Version (C_Stat) = HTTP_10 -- We cannot use transfer-encoding chunked in HTTP_10 and then Method /= Status.HEAD) -- We have to send message_body or else not Response.Keep_Alive (Answer) then -- In this case we need to close the connection explicitly at the -- end of the transfer. Will_Close := True; end if; Send_General_Header (Sock); -- Send Cache-Control, Location, WWW-Authenticate and others -- user defined header lines. Response.Send_Header (Socket => Sock, D => Answer); -- Send file last-modified timestamp info in case of a file if File_Mode and then not Response.Has_Header (Answer, Messages.Last_Modified_Token) then Net.Buffered.Put_Line (Sock, Messages.Last_Modified (File_Time)); end if; -- Note that we cannot send the Content_Length header at this -- point. A server should not send Content_Length if the -- transfer-coding used is not identity. This is allowed by the -- RFC but it seems that some implementation does not handle this -- right. The file can be sent using either identity or chunked -- transfer-coding. The proper header will be sent in Send_Resource -- see [RFC 2616 - 4.4]. -- Send message body if With_Body then Send_Resource (Answer, Method, File, Length, HTTP_Server, Line_Index, C_Stat); else -- RFC-2616 4.4 -- ... -- Any response message which "MUST NOT" include a message-body -- (such as the 1xx, 204, and 304 responses and any response to a -- HEAD request) is always terminated by the first empty line -- after the header fields, regardless of the entity-header fields -- present in the message. Net.Buffered.New_Line (Sock); if Length > 0 then Log.Write (HTTP_Server.Error_Log, C_Stat, "Message body was not sent. Response with status '" & Messages.Image (Status_Code) & "' can't have it."); end if; end if; Net.Buffered.Flush (Sock); end Send_Data; ------------------------- -- Send_General_Header -- ------------------------- procedure Send_General_Header (Sock : Net.Socket_Type'Class) is begin -- Session if CNF.Session (HTTP_Server.Properties) and then AWS.Status.Session_Created (C_Stat) then -- This is an HTTP connection with session but there is no session -- ID set yet. So, send cookie to client browser. Response.Set.Add_Header (D => Answer, Name => Messages.Set_Cookie_Token, Value => CNF.Session_Name (HTTP_Server.Properties) & '=' & Session.Image (AWS.Status.Session (C_Stat)) & "; path=/; Version=1"); -- And the internal private session Response.Set.Add_Header (D => Answer, Name => Messages.Set_Cookie_Token, Value => CNF.Session_Private_Name (HTTP_Server.Properties) & '=' & AWS.Status.Session_Private (C_Stat) & "; path=/; Version=1"); end if; -- Date Net.Buffered.Put_Line (Sock, "Date: " & Messages.To_HTTP_Date (Utils.GMT_Clock)); -- Server declare Server : constant String := CNF.Server_Header (HTTP_Server.Properties); begin if Server /= "" then Net.Buffered.Put_Line (Sock, "Server: " & Server); end if; end; if Will_Close then -- We have decided to close connection after answering the client Response.Set.Update_Header (Answer, Messages.Connection_Token, Value => "close"); else Response.Set.Update_Header (Answer, Messages.Connection_Token, Value => "keep-alive"); end if; end Send_General_Header; ---------------------- -- Send_Header_Only -- ---------------------- procedure Send_Header_Only is Sock : constant Net.Socket_Type'Class := Status.Socket (C_Stat); begin -- First let's output the status line Net.Buffered.Put_Line (Sock, Messages.Status_Line (Status_Code)); Send_General_Header (Sock); Net.Buffered.Put_Line (Sock, Messages.Content_Type (Response.Content_Type (Answer))); -- Send Cache-Control, Location, WWW-Authenticate and others -- user defined header lines. Response.Send_Header (Socket => Sock, D => Answer); -- There is no content Net.Buffered.Put_Line (Sock, Messages.Content_Length (0)); -- End of header Net.Buffered.New_Line (Sock); Net.Buffered.Flush (Sock); end Send_Header_Only; ------------------------------ -- Send_WebSocket_Handshake -- ------------------------------ procedure Send_WebSocket_Handshake is Sock : constant Net.Socket_Type'Class := Status.Socket (C_Stat); Headers : constant AWS.Headers.List := Status.Header (C_Stat); begin -- First let's output the status line Net.Buffered.Put_Line (Sock, Messages.Status_Line (Status_Code)); -- Send Cache-Control, Location, WWW-Authenticate and others -- user defined header lines. Response.Send_Header (Socket => Sock, D => Answer); if Headers.Exist (Messages.Sec_WebSocket_Key1_Token) and then Headers.Exist (Messages.Sec_WebSocket_Key2_Token) then Net.WebSocket.Protocol.Draft76.Send_Header (Sock, C_Stat); else -- Send WebSocket-Accept handshake Net.WebSocket.Protocol.RFC6455.Send_Header (Sock, C_Stat); -- End of header Net.Buffered.New_Line (Sock); Net.Buffered.Flush (Sock); end if; end Send_WebSocket_Handshake; ------------------------------------ -- Send_WebSocket_Handshake_Error -- ------------------------------------ procedure Send_WebSocket_Handshake_Error (Status_Code : Messages.Status_Code; Reason_Phrase : String := "") is Sock : constant Net.Socket_Type'Class := Status.Socket (C_Stat); begin -- First let's output the status line Net.Buffered.Put_Line (Sock, Messages.Status_Line (Status_Code, Reason_Phrase)); Net.Buffered.Put_Line (Sock, Messages.Content_Length (0)); -- End of header Net.Buffered.New_Line (Sock); Net.Buffered.Flush (Sock); end Send_WebSocket_Handshake_Error; begin case Response.Mode (Answer) is when Response.File | Response.File_Once | Response.Stream | Response.Message => HTTP_Server.Slots.Mark_Phase (Line_Index, Server_Response); Send_Data; when Response.Header => HTTP_Server.Slots.Mark_Phase (Line_Index, Server_Response); Send_Header_Only; when Response.Socket_Taken => HTTP_Server.Slots.Socket_Taken (Line_Index); Socket_Taken := True; when Response.WebSocket => Socket_Taken := False; Will_Close := True; if not CNF.Is_WebSocket_Origin_Set or else GNAT.Regexp.Match (Status.Origin (C_Stat), CNF.WebSocket_Origin) then -- Get the WebSocket begin declare -- The call to the constructor will raise an exception -- if the WebSocket is not to be accepted. In this case -- a forbidden message is sent back. WS : constant Net.WebSocket.Object'Class := Net.WebSocket.Registry.Constructor (Status.URI (C_Stat)) (Socket => Status.Socket (C_Stat), Request => C_Stat); begin -- Register this new WebSocket if WS in Net.WebSocket.Handshake_Error.Object'Class then declare E : constant Net.WebSocket.Handshake_Error.Object := Net.WebSocket.Handshake_Error.Object (WS); begin Send_WebSocket_Handshake_Error (E.Status_Code, E.Reason_Phrase); end; else -- First try to register the WebSocket object declare use type Net.WebSocket.Object_Class; W : Net.WebSocket.Object_Class; begin W := Net.WebSocket.Registry.Utils.Register (WS); if W = null then Send_WebSocket_Handshake_Error (Messages.S412, "too many WebSocket registered"); else Send_WebSocket_Handshake; HTTP_Server.Slots.Socket_Taken (Line_Index); Socket_Taken := True; Will_Close := False; Net.WebSocket.Registry.Utils.Watch (W); end if; end; end if; exception when E : others => Send_WebSocket_Handshake_Error (Messages.S403, Exception_Message (E)); WS.Shutdown; end; exception when E : others => Send_WebSocket_Handshake_Error (Messages.S403, Exception_Message (E)); raise; end; else Send_WebSocket_Handshake_Error (Messages.S403); end if; when Response.No_Data => raise Constraint_Error with "Answer not properly initialized (No_Data)"; end case; if LA.Skip_Log then LA.Skip_Log := False; elsif CNF.Log_Extended_Fields_Length (HTTP_Server.Properties) > 0 then declare use Real_Time; use type Strings.Maps.Character_Set; Start : constant Time := Status.Request_Time (C_Stat); begin if Start /= Time_First then Log.Set_Field (LA.Server.Log, LA.Log_Data, "time-taken", Utils.Significant_Image (To_Duration (Clock - Start), 3)); end if; Log.Set_Header_Fields (LA.Server.Log, LA.Log_Data, "cs", Status.Header (C_Stat)); Log.Set_Header_Fields (LA.Server.Log, LA.Log_Data, "sc", Response.Header (Answer)); Log.Set_Field (LA.Server.Log, LA.Log_Data, "cs-method", Status.Method (C_Stat)); Log.Set_Field (LA.Server.Log, LA.Log_Data, "cs-username", Status.Authorization_Name (C_Stat)); Log.Set_Field (LA.Server.Log, LA.Log_Data, "cs-version", Status.HTTP_Version (C_Stat)); declare use AWS.URL; Encoding : constant Strings.Maps.Character_Set := Strings.Maps.To_Set (Span => (Low => Character'Val (128), High => Character'Last)) or Strings.Maps.To_Set ("+"" "); URI : constant String := Encode (Status.URI (C_Stat), Encoding); Query : constant String := Parameters.URI_Format (Status.Parameters (C_Stat)); begin Log.Set_Field (LA.Server.Log, LA.Log_Data, "cs-uri-stem", URI); Log.Set_Field (LA.Server.Log, LA.Log_Data, "cs-uri-query", Query); Log.Set_Field (LA.Server.Log, LA.Log_Data, "cs-uri", URI & Query); end; Log.Set_Field (LA.Server.Log, LA.Log_Data, "sc-status", Messages.Image (Status_Code)); Log.Set_Field (LA.Server.Log, LA.Log_Data, "sc-bytes", Utils.Image (Integer (Length))); Log.Write (LA.Server.Log, LA.Log_Data); end; else Log.Write (HTTP_Server.Log, C_Stat, Status_Code, Length); end if; end Send; ------------------- -- Send_Resource -- ------------------- procedure Send_Resource (Answer : in out Response.Data; Method : Status.Request_Method; File : in out Resources.File_Type; Length : in out Resources.Content_Length_Type; HTTP_Server : AWS.Server.HTTP; Line_Index : Positive; C_Stat : AWS.Status.Data) is use type Status.Request_Method; Sock : constant Net.Socket_Type'Class := Status.Socket (C_Stat); Buffer_Size : constant := 4 * 1_024; -- Size of the buffer used to send the file Chunk_Size : constant := 1_024; -- Size of the buffer used to send the file with the chunked encoding. -- This is the maximum size of each chunk. Ranges : constant String := Headers.Get_Values (Status.Header (C_Stat), Messages.Range_Token); -- The ranges for partial sending if defined Close : constant Boolean := Response.Close_Resource (Answer); procedure Send_File; -- Send file in one part procedure Send_Ranges; -- Send a set of ranges of file content procedure Send_File_Chunked; -- Send file in chunks, used in HTTP/1.1 and when the message length -- is not known) Last : Streams.Stream_Element_Offset; --------------- -- Send_File -- --------------- procedure Send_File is Buffer : Streams.Stream_Element_Array (1 .. Buffer_Size); begin loop Resources.Read (File, Buffer, Last); exit when Last < Buffer'First; Net.Buffered.Write (Sock, Buffer (1 .. Last)); Length := Length + Last; HTTP_Server.Slots.Check_Data_Timeout (Line_Index); end loop; end Send_File; --------------------- -- Send_File_Chunk -- --------------------- procedure Send_File_Chunked is -- Note that we do not use a buffered socket here. Opera on SSL -- sockets does not like chunk that are not sent in a whole. Buffer : Streams.Stream_Element_Array (1 .. Chunk_Size); -- Each chunk will have a maximum length of Buffer'Length CRLF : constant Streams.Stream_Element_Array := (1 => Character'Pos (ASCII.CR), 2 => Character'Pos (ASCII.LF)); Last_Chunk : constant Streams.Stream_Element_Array := Character'Pos ('0') & CRLF & CRLF; -- Last chunk for a chunked encoding stream. See [RFC 2616 - 3.6.1] begin Send_Chunks : loop Resources.Read (File, Buffer, Last); if Last = 0 then -- There is not more data to read, the previous chunk was the -- last one, just terminate the chunk message here. Net.Send (Sock, Last_Chunk); exit Send_Chunks; end if; Length := Length + Last; HTTP_Server.Slots.Check_Data_Timeout (Line_Index); declare H_Last : constant String := Utils.Hex (Positive (Last)); Chunk : constant Streams.Stream_Element_Array := Translator.To_Stream_Element_Array (H_Last) & CRLF & Buffer (1 .. Last) & CRLF; -- A chunk is composed of: -- the Size of the chunk in hexadecimal -- a line feed -- the chunk -- a line feed begin -- Check if the last data portion if Last < Buffer'Last then -- No more data, add the terminating chunk Net.Send (Sock, Chunk & Last_Chunk); exit Send_Chunks; else Net.Send (Sock, Chunk); end if; end; end loop Send_Chunks; end Send_File_Chunked; ----------------- -- Send_Ranges -- ----------------- procedure Send_Ranges is Boundary : constant String := "aws_range_separator"; N_Range : constant Positive := 1 + Fixed.Count (Ranges, ","); N_Minus : constant Natural := Fixed.Count (Ranges, "-"); -- Number of ranges defined Equal : constant Natural := Fixed.Index (Ranges, "="); First, Last : Positive; procedure Send_Range (R : String); -- Send a single range as defined by R ---------------- -- Send_Range -- ---------------- procedure Send_Range (R : String) is I_Minus : constant Positive := Fixed.Index (R, "-"); First : Stream_Element_Offset; Last : Stream_Element_Offset; R_Length : Stream_Element_Offset; begin if N_Range /= 1 then -- Send the multipart/byteranges Net.Buffered.Put_Line (Sock, "--" & Boundary); end if; -- Computer First / Last and the range length if I_Minus = R'Last then Last := Length - 1; else Last := Stream_Element_Offset'Value (R (I_Minus + 1 .. R'Last)); if Last >= Length then Last := Length - 1; end if; end if; if R'First = I_Minus then -- In this case we want to get the last N bytes from the file First := Length - Last; Last := Length - 1; else First := Stream_Element_Offset'Value (R (R'First .. I_Minus - 1)); end if; R_Length := Last - First + 1; -- Content-Range: bytes -/ Net.Buffered.Put_Line (Sock, Messages.Content_Range_Token & ": bytes " & Utils.Image (Natural (First)) & "-" & Utils.Image (Natural (Last)) & "/" & Utils.Image (Natural (Length))); Net.Buffered.Put_Line (Sock, Messages.Content_Length (R_Length)); Net.Buffered.New_Line (Sock); Resources.Set_Index (File, First + 1); declare Buffer : Streams.Stream_Element_Array (1 .. Buffer_Size); Sent : Stream_Element_Offset := 0; Size : Stream_Element_Offset := 0; Last : Stream_Element_Offset; begin loop Size := Stream_Element_Offset'Min (R_Length - Sent, Buffer_Size); exit when Size = 0; Resources.Read (File, Buffer (1 .. Size), Last); exit when Last < Buffer'First; Net.Buffered.Write (Sock, Buffer (1 .. Last)); Sent := Sent + Last; HTTP_Server.Slots.Check_Data_Timeout (Line_Index); end loop; end; end Send_Range; begin -- Check range definition if N_Range /= N_Minus or else Equal = 0 or else Ranges (Ranges'First .. Equal - 1) /= "bytes" then -- Range is wrong, let's send the whole file then Send_File; end if; if N_Range = 1 then Net.Buffered.Put_Line (Sock, Messages.Content_Type (Response.Content_Type (Answer))); else -- Then we will send a multipart/byteranges Net.Buffered.Put_Line (Sock, Messages.Content_Type (MIME.Multipart_Byteranges & "; boundary=" & Boundary)); end if; First := Equal + 1; for K in 1 .. N_Range loop if K = N_Range then Last := Ranges'Last; else Last := Fixed.Index (Ranges (First .. Ranges'Last), ",") - 1; end if; Send_Range (Ranges (First .. Last)); First := Last + 2; end loop; -- End the multipart/byteranges message if N_Range /= 1 then -- Send the multipart/byteranges Net.Buffered.Put_Line (Sock, "--" & Boundary & "--"); end if; end Send_Ranges; begin if Ranges /= "" and then Length /= Resources.Undefined_Length then -- Range: header present, we need to send only the specified bytes Net.Buffered.Put_Line (Sock, Messages.Accept_Ranges_Token & ": bytes"); -- Only bytes supported Send_Ranges; elsif Status.HTTP_Version (C_Stat) = HTTP_10 or else Length /= Resources.Undefined_Length then Net.Buffered.Put_Line (Sock, Messages.Content_Type (Response.Content_Type (Answer))); -- If content length is undefined and we handle an HTTP/1.0 protocol -- then the end of the stream will be determined by closing the -- connection. [RFC 1945 - 7.2.2] See the Will_Close local variable. if Length /= Resources.Undefined_Length then Net.Buffered.Put_Line (Sock, Messages.Content_Length (Length)); end if; -- Terminate header Net.Buffered.New_Line (Sock); if Method /= Status.HEAD and then Length /= 0 then Length := 0; Send_File; end if; else Net.Buffered.Put_Line (Sock, Messages.Content_Type (Response.Content_Type (Answer))); -- HTTP/1.1 case and we do not know the message length -- -- Terminate header, do not send the Content_Length see -- [RFC 2616 - 4.4]. It could be possible to send the Content_Length -- as this is cleary a permission but it does not work in some -- obsucre cases. Net.Buffered.Put_Line (Sock, Messages.Transfer_Encoding ("chunked")); Net.Buffered.New_Line (Sock); Net.Buffered.Flush (Sock); -- Past this point we will not use the buffered mode. Opera on SSL -- sockets does not like chunk that are not sent in a whole. if Method /= Status.HEAD then Length := 0; Send_File_Chunked; end if; end if; if Close then Resources.Close (File); end if; exception when others => if Close then Resources.Close (File); end if; raise; end Send_Resource; ---------------------- -- Set_Close_Status -- ---------------------- procedure Set_Close_Status (C_Stat : AWS.Status.Data; Keep_Alive : Boolean; Will_Close : in out Boolean) is Connection : constant String := Status.Connection (C_Stat); begin -- Connection, check connection string with Match to skip connection -- options [RFC 2616 - 14.10]. Will_Close := Utils.Match (Connection, "close") or else not Keep_Alive or else (Status.HTTP_Version (C_Stat) = HTTP_10 and then not Utils.Match (Connection, "keep-alive")); end Set_Close_Status; end AWS.Server.HTTP_Utils;