--
-- Author: Brent Seidel
-- Date: 6-Aug-2024
--
-- This file is part of Simple Ada Web Server.
-- Simple Ada Web Server is free software: you can redistribute it and/or
-- modify it under the terms of the GNU General Public License as published
-- by the Free Software Foundation, either version 3 of the License, or (at
-- your option) any later version.
--
-- Simple Ada Web Server 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. See the GNU General Public License for
-- more details.
--
-- You should have received a copy of the GNU General Public License along
-- with Simple Ada Web Server. If not, see .--
--
with Ada.Characters.Latin_1;
with Ada.Text_IO;
with Ada.Streams;
use type Ada.Streams.Stream_Element_Offset;
package body BBS.web.http is
package ASU renames Ada.Strings.Unbounded; -- not the school
--
-- Return code 200 OK for normal cases
--
procedure ok(s : GNAT.Sockets.Stream_Access; txt: String) is
begin
String'Write(s, "HTTP/1.0 200 OK" & CRLF);
String'Write(s, "Content-Type: " & txt & CRLF);
String'Write(s, server_header);
String'Write(s, "Connection: Close" & CRLF);
String'Write(s, CRLF);
end ok;
--
-- Return code 200 OK for OPTIONS reqest cases
--
procedure options_ok(s : GNAT.Sockets.Stream_Access; item : String;
dir : dictionary.Map) is
begin
String'Write(s, "HTTP/1.0 200 OK" & CRLF);
if item = "*" then
String'Write(s, "Allow: OPTIONS, GET, POST" & CRLF);
elsif dir.Contains(item) then
declare
el : constant element := dir.Element(item);
mime : constant String := ASU.To_String(el.mime);
begin
if mime = "internal" then
String'Write(s, "Allow: OPTIONS, GET, POST" & CRLF);
else
String'Write(s, "Allow: OPTIONS, GET" & CRLF);
end if;
end;
else
String'Write(s, "Allow: OPTIONS" & CRLF);
end if;
String'Write(s, server_header);
String'Write(s, "Content-Length: 0" & CRLF);
String'Write(s, CRLF);
end options_ok;
--
-- Return code 404 NOT FOUND for when the requested item is not in the
-- directory.
--
procedure not_found(s : GNAT.Sockets.Stream_Access; item: String) is
begin
String'Write(s, "HTTP/1.0 404 NOT FOUND" & CRLF);
String'Write(s, "Content-Type: text/html" & CRLF);
String'Write(s, server_header);
String'Write(s, "Connection: Close" & CRLF);
String'Write(s, CRLF);
String'Write(s, "
" & item & " not found");
String'Write(s, "Item " & item & " cannot be found on the server.");
String'Write(s, "");
end not_found;
--
-- Return code 500 INTERNAL SERVER ERROR generally when unable to open the
-- file for the specified item.
--
procedure internal_error(s : GNAT.Sockets.Stream_Access; file: String) is
begin
String'Write(s, "HTTP/1.0 500 INTERNAL ERROR" & CRLF);
String'Write(s, "Content-Type: text/html" & CRLF);
String'Write(s, server_header);
String'Write(s, "Connection: Close" & CRLF);
String'Write(s, CRLF);
String'Write(s, "Internal Error");
String'Write(s, "Internal error trying to serve " & file);
String'Write(s, "");
end internal_error;
--
-- Return code 501 NOT IMPLEMENTED for any request other than GET or POST.
--
procedure not_implemented_req(s : GNAT.Sockets.Stream_Access; req: String) is
begin
String'Write(s, "HTTP/1.0 501 NOT IMPLEMENTED" & CRLF);
String'Write(s, "Content-Type: text/html" & CRLF);
String'Write(s, server_header);
String'Write(s, "Connection: Close" & CRLF);
String'Write(s, CRLF);
String'Write(s, "Request type not implemented");
String'Write(s, "Request type " & req & " is not implemented");
String'Write(s, "");
end not_implemented_req;
--
-- Return code 501 NOT IMPLEMENTED for a request for an internally generated
-- item that is not yet implemented..
--
procedure not_implemented_int(s : GNAT.Sockets.Stream_Access; item: String) is
begin
String'Write(s, "HTTP/1.0 501 NOT IMPLEMENTED" & CRLF);
String'Write(s, "Content-Type: text/html" & CRLF);
String'Write(s, server_header);
String'Write(s, "Connection: Close" & CRLF);
String'Write(s, CRLF);
String'Write(s, "" & item & " not implemented");
String'Write(s, "Item " & item & " is not yet implemented on the server.");
String'Write(s, "");
end not_implemented_int;
--
-- Read a line from the input stream. The line is terminated with a CR-LF.
-- The CR-LF is stripped from the return string as it can just be assumed to
-- be there.
--
-- *** There is a potential problem using Character'Input(). No
-- *** notification is given should the stream be closed while waiting for
-- *** input. If this happens, the task will hang. This is an easy
-- *** opportunity for a denial of service attack. This has been addressed
-- *** by using GNAT.Sockets.Receive_Socket instead which can indicate a
-- *** socket closure by the value of last. Should this be detected, an
-- *** Ada.Text_IO.End_Error (perhaps not the best choice) is raised and
-- *** further input is abandoned.
--
function get_line_from_stream(s : GNAT.Sockets.Socket_Type)
return Ada.Strings.Unbounded.Unbounded_String is
c : Character;
last : Ada.Streams.Stream_Element_Offset;
elem : Ada.Streams.Stream_Element_Array(1 .. 1);
str : Ada.Strings.Unbounded.Unbounded_String;
begin
loop
loop
GNAT.Sockets.Receive_Socket(s, elem, last);
if last = 0 then
raise closed_by_peer;
end if;
c := Character'Val(elem(1));
str := str & c;
exit when c = Ada.Characters.Latin_1.CR;
end loop;
GNAT.Sockets.Receive_Socket(s, elem, last);
if last = 0 then
raise closed_by_peer;
end if;
c := Character'Val(elem(1));
str := str & c;
exit when c = Ada.Characters.Latin_1.LF;
end loop;
return Ada.Strings.Unbounded.Head(str, ASU.Length(str) - 2);
end get_line_from_stream;
--
-- Read a specified number of characters from an input stream. This is
-- needed because a POST request is not terminated by CRLF. Instead the
-- content-length header needs to be parsed and that number of characters
-- read at the end.
--
function get_data_from_stream(s : GNAT.Sockets.Socket_Type; len : Natural)
return Ada.Strings.Unbounded.Unbounded_String is
c : Character;
last : Ada.Streams.Stream_Element_Offset;
elem : Ada.Streams.Stream_Element_Array(1 .. 1);
str : Ada.Strings.Unbounded.Unbounded_String;
begin
for i in Natural range 1 .. len loop
GNAT.Sockets.Receive_Socket(s, elem, last);
if last = 0 then
raise closed_by_peer;
end if;
c := Character'Val(elem(1));
str := str & c;
end loop;
return str;
end get_data_from_stream;
--
-- Read the headers from the request..
--
procedure read_headers(s : GNAT.Sockets.Stream_Access;
sock : GNAT.Sockets.Socket_Type;
method : out request_type;
item : out ASU.Unbounded_String;
headers : in out params.Map;
args : in out params.Map;
dir : dictionary.Map) is
param_string : ASU.Unbounded_String := ASU.Null_Unbounded_String;
length : Natural;
line : ASU.Unbounded_String;
req : ASU.Unbounded_String;
temp1 : ASU.Unbounded_String;
temp2 : ASU.Unbounded_String;
index : Natural;
begin
args.Clear;
headers.Clear;
--
-- The first line contains the request. Parse it out.
--
line := get_line_from_stream(sock);
if debug_req.get then
Ada.Text_IO.Put_Line(ASU.To_String(line));
end if;
index := ASU.Index(line, " ");
req := ASU.Head(line, index - 1);
line := ASU.Tail(line, ASU.Length(line) - index);
if req = "CONNECT" then
method := CONNECT;
elsif req = "DELETE" then
method := DELETE;
elsif req = "GET" then
method := GET;
elsif req = "HEAD" then
method := HEAD;
elsif req = "OPTIONS" then
method := OPTIONS;
elsif req = "PATCH" then
method := PATCH;
elsif req = "POST" then
method := POST;
elsif req = "PUT" then
method := PUT;
elsif req = "TRACE" then
method := TRACE;
else
method := Other;
end if;
--
-- Parse out the requested item. Don't care about the HTTP version
--
index := ASU.Index(line, " ");
if index > 0 then
item := ASU.Head(line, index - 1);
line := ASU.Tail(line, ASU.Length(line) - index);
else
item := line;
line := ASU.Null_Unbounded_String;
end if;
--
-- Scan through the rest of the headers. If needed, the content-length
-- header is parsed.
--
loop
line := get_line_from_stream(sock);
exit when ASU.Length(line) = 0;
index := ASU.Index(line, " ");
temp1 := ASU.Head(line, index - 1);
temp2 := ASU.Tail(line, ASU.Length(line) - index);
headers.Insert(Key => ASU.To_String(temp1),
New_Item => ASU.To_String(temp2));
--
-- If this is a POST request, we need to find the Content-Length:
-- header and determine the length. To be strictly correct, the
-- Content-Type: header should also be examined.
--
if method = POST then
if (temp1 = "Content-Length:") then
length := Natural'Value(ASU.To_String(temp2));
end if;
end if;
if debug_head.get then
Ada.Text_IO.Put_Line(ASU.To_String(line));
end if;
end loop;
--
-- Check the request method and send proper response if not implemented.
--
case method is
when GET =>
--
-- If a GET request, check to see if parameters are attached
--
line := item;
index := ASU.Index(line, "?");
if index > 0 then
item := ASU.Head(line, index - 1);
param_string := ASU.Tail(line, ASU.Length(line) - index);
end if;
when POST =>
--
-- If the method is post, the parameters will be read here.
--
param_string := get_data_from_stream(sock, length);
when OPTIONS =>
options_ok(s, ASU.To_String(item), dir);
when others =>
not_implemented_req(s, ASU.To_String(req));
end case;
--
-- If there are parameters, process them and store them in a parameter
-- dictionary.
--
if (param_string /= ASU.Null_Unbounded_String) then
while (ASU.Length(param_string) > 0) loop
--
-- First split off a key value pair. They are separated by '&'.
--
index := ASU.Index(param_string, "&");
if index > 0 then
temp1 := ASU.Head(param_string, index - 1);
param_string := ASU.Tail(param_string, ASU.Length(param_string) - index);
else
temp1 := param_string;
param_string := ASU.Null_Unbounded_String;
end if;
--
-- Split the key-value pair into separate key and values and store
-- them in the params map. At this point, the value could be URL
-- decoded.
--
index := ASU.Index(temp1, "=");
declare
key : constant String := ASU.To_String(ASU.Head(temp1, index - 1));
value : constant String := url_decode(ASU.To_String(
ASU.Tail(temp1,
ASU.Length(temp1) - index)));
begin
args.Insert(Key => key,
New_Item => value);
end;
end loop;
end if;
end read_headers;
end BBS.web.http;