/******************************************************************************/
/* */
/* HTTP.Request.cls - The HTTP request class */
/* ========================================= */
/* */
/* This file is part of the RexxHTTP package */
/* [See https://rexx.epbcn.com/rexxhttp/] */
/* */
/* Copyright (c) 2006-2026 Josep Maria Blasco <josep.maria.blasco@epbcn.com> */
/* */
/* License: Apache License 2.0 (https://www.apache.org/licenses/LICENSE-2.0) */
/* */
/* Version history: */
/* */
/* Date Version Details */
/* -------- ------- --------------------------------------------------------- */
/* 20061102 0.1 First version */
/* 20170301 0.2 Drop support for mod_rexx, OS/2, IIS, rsp compilers */
/* Change the way request~message works -- see 'unknown'. */
/* By default, it looks in the environment variable pool. */
/* 20260611 1.0 Global refactor, switch to Apache license */
/* 20260615 Add decoding methods */
/* */
/******************************************************************************/
.environment~HTTP.Request = .HTTP.Request
::Class HTTP.Request Public
--------------------------------------------------------------------------------
-- INIT --
--------------------------------------------------------------------------------
::Method init
Expose env envvars args_processed cookies_processed,
rexxletFilename filename script_filename,
content_length content_type transfer_encoding,
path_info path_translated,
post_string query_string,
request_method request_uri request_url uri arg_violation,
action_request standard_request,
action_document,
urlbase base
-- A third redirect level = three chained Actions: unsupported topology.
If Env("REDIRECT_REDIRECT_REDIRECT_REXXHTTP_BASE") \== "" Then
Raise syntax 98.900 array -
("rawResourcePath: unsupported Action chain (three redirect levels)")
-- Two redirect levels? -- A rexxlet used as an Action
-- One redirect level? -- A standard rexxlet
action_request = Env("REDIRECT_REDIRECT_REXXHTTP_BASE") \== ""
standard_request = \ action_request
Select
When standard_request Then Do
urlbase = Env("REDIRECT_REXXHTTP_URLBASE")
base = Env("REDIRECT_REXXHTTP_BASE")
End
When action_request Then Do
urlbase = Env("REDIRECT_REDIRECT_REXXHTTP_URLBASE")
base = Env("REDIRECT_REDIRECT_REXXHTTP_BASE")
End
End
-- Separator: "/" for unix, "\" for windows
sep = .File~separator
-- The environment pool cache: one directory for every variable name,
-- HTTP_* included (see ~unknown for why they need no pool of their own).
envvars = .directory~new
-- Retrieve environment variables
content_length = Env(content_length)
content_type = Env(content_type )
request_method = Env(request_method)
-- Transfer-Encoding arrives as an HTTP_ header, so the bare-name trick
-- (uninitialised symbol -> its own uppercase name) would read the wrong
-- variable; pass the HTTP_-prefixed literal explicitly. Needed to tell a
-- chunked POST (no CONTENT_LENGTH, body read to EOF) apart from a POST
-- with no body. See design-post-body-reading.md.
transfer_encoding = Env("HTTP_TRANSFER_ENCODING")
-- Variables PATH_INFO, PATH_TRANSLATED, POST_STRING, QUERY_STRING,
-- SCRIPT_NAME have to be calculated manually
path = Env(path_translated)
query_string = Env(query_string )
request_uri = Env(request_uri )
If .File~separator == "\" Then
path = path~translate("\","/")
args_processed = .false
cookies_processed = .false
arg_violation = ""
-- Determine rexxlet file from PATH_TRANSLATED, adjust FILENAME, and
-- set current directory to point to the rexxlet file.
rexxletFilename = ""
If SysIsFile(path) Then Do
rexxletFilename = path
path_info = ""
End
Else Do
pos = path~pos(sep)
Do Forever
pos = path~pos(sep,pos+1)
If pos = 0 Then Leave
If SysIsFile(path~left(pos-1)) Then Do
rexxletFilename = path~left(pos-1)
path_info = path~substr(pos)~translate("/",sep)
Leave
End
End
End
-- SCRIPT_FILENAME is the disk path of the PROCESSOR: the rexxlet Apache
-- resolved. RexxHTTP.rex reads it as the callee it Calls, and ~script_name
-- maps it back to URL space -- it is the execution/processor axis and must
-- stay the rexxlet.
script_filename = rexxletFilename
-- FILENAME / PATH_TRANSLATED are the disk path of the DOCUMENT the client
-- asked for -- the disk twin of REQUEST_URL. Nothing executes them (the
-- callee is script_filename), so they name the document. One hop: requested
-- == processor, so the rexxlet (final value here). Two hops: reassigned to the
-- document after the Action cut below.
filename = rexxletFilename
path_translated = rexxletFilename
directory = rexxletFilename~left(rexxletFilename~lastpos(sep))
Call directory directory
-- Build REQUEST_URL (the self-referential URL: the client's own URL with
-- PATH_INFO and query string peeled off). This is NOT the CGI SCRIPT_NAME:
-- that one is served by the ~script_name method, reconstructed from
-- path_translated. This value was historically named script_name, hence the
-- "SCRIPT_NAME" wording that survives in the length-clamp note below.
-- Decode as a URI path, NOT as form data: in a URL path "+" is a literal
-- plus (RFC 3986), not a space. decodeForm applies the form-urlencoded
-- "+"->space rule, which mangles a document at e.g. /notas/c++.md into
-- "/notas/c .md". decodeURIComponent does the %xx-decoding without the "+"
-- rule -- the correct decoder for a path -- and already lives in this class.
-- ~uri and request_url are both built from requri, so both were affected.
-- See ref-decisiones.md "v99 -- decodeForm en el path".
requri = self~decodeURIComponent(request_uri)
-- REQUEST_URL (the self-referential URL: the client's own URL, PATH_INFO and
-- query string peeled off) is built BELOW, after the Action document/tail cut
-- has resolved path_info to its final document-axis, tail-only value. It must
-- read that doctored path_info, not the raw handler-axis walk above: the raw
-- value fuses processor+document under an Action (PATH_INFO longer than
-- REQUEST_URI, subtraction negative, request_url empties out), whereas the
-- doctored path_info is always a genuine suffix of REQUEST_URI, in one hop and
-- in two. See the Action cut and the request_url build that follows it.
If requri~pos("?") == 0 Then uri = requri
Else uri = requri~left(requri~pos("?")-1)
-- Action document/tail cut. For a rexxlet used as an Action (two hops) the
-- handler-axis walk near the top left path_info holding the whole client
-- request (document + tail fused) -- Apache's PATH_INFO smuggling. Re-run the
-- same SysIsFile walk on the document axis (rawResourcePath) to split the
-- resolved document from its trailing path, and OVERWRITE path_info with the
-- tail only -- "what you put after the resource you named". This happens
-- AFTER request_url read the raw path_info above, so request_url keeps its
-- established value while the path_info attribute ends up tail-only. Computed
-- here, once, like everything else: CGI has no persistent state to make a
-- lazy cache meaningful. action_document is kept for reuse (path_translated
-- as document, a later round). Standard rexlets skip this: their handler axis
-- and document axis coincide, so the raw path_info is already tail-only.
action_document = ""
If action_request Then Do
reqfile = self~rawResourcePath
action_document = reqfile
path_info = ""
If \SysIsFile(reqfile) Then Do
pos = reqfile~pos(sep)
Do Forever
pos = reqfile~pos(sep, pos + 1)
If pos = 0 Then Leave
If SysIsFile(reqfile~left(pos - 1)) Then Do
action_document = reqfile~left(pos - 1)
path_info = reqfile~substr(pos)~translate("/", sep)
Leave
End
End
End
End
-- FILENAME / PATH_TRANSLATED for an Action (two hops): the document on disk.
-- The callee is script_filename (the rexxlet), so both may name the document.
-- action_document holds the requested resource, resolved via rawResourcePath
-- (REQUEST_URI + BASE/URLBASE) -- the honest, Alias-aware disk path. When it
-- is a regular file it IS the document (target.mtx, any /x/y tail already
-- split off). When it is a DIRECTORY it can only be a DirectoryIndex request
-- (we never serve directories), so Apache has already resolved and appended
-- the index basename to the tail of the raw PATH_TRANSLATED; graft that
-- basename onto the directory to name the index file actually served
-- (.../sub/ + index.mtx). One hop leaves action_document empty and both keep
-- their rexxlet value.
If action_document \== "" Then Do
If SysIsFile(action_document) Then
filename = action_document
Else
filename = action_document || path~substr(path~lastpos(sep) + 1)
path_translated = filename
End
-- REQUEST_URL: the self-referential URL -- the client's own URL with its
-- extra PATH_INFO and query string peeled off, in ONE hop and in TWO. Built
-- here, after the Action cut, because path_info is now the document-axis,
-- tail-only value: a genuine suffix of REQUEST_URI in every cell of the
-- matrix (docroot/Alias x rexxlet/DirectoryIndex x one/two hops x with/without
-- extra path). Reading the RAW handler-axis path_info here instead would fuse
-- processor+document under an Action, make PATH_INFO longer than REQUEST_URI,
-- drive the length negative and empty request_url out (the two-hop bug). With
-- the doctored suffix the subtraction is always non-negative; the Max(0,...)
-- guard stays only as a belt-and-braces against a decode length mismatch.
-- These two feed only the LENGTH arithmetic below, and the two decoders are
-- length-identical (they differ only in "+"->space vs literal "+", a
-- one-for-one substitution), so the clamp is decode-invariant either way.
-- path_info is a PATH -> decodeURIComponent (matching requri). query_string
-- genuinely IS form data -> decodeForm is the honest decoder there; it is
-- used only for its length, so the "+" difference never reaches a value.
pathinfolen = self~decodeURIComponent(path_info)~length
querylen = self~decodeForm(query_string)~length
If querylen > 0 Then querylen = querylen + 1
request_url = requri~left(Max(0, requri~length - pathinfolen - querylen))
-- Delay processing until Arg() has been called
post_string = ""
Return
--------------------------------------------------------------------------------
-- VALIDATE --
-- Force eager parsing of the request arguments and apply the configured --
-- argument policy. Called by the processor BEFORE the rexxlet runs, so a --
-- policy violation can be reported (400/500) without ceding control. --
-- Returns "" if the request is acceptable, or a "code:detail" string: --
-- novalue:<token> a parameter without "=" --
-- noname:<token> a parameter with an empty name ("=value") --
-- duplicate:<name> a repeated name (caseless) --
-- digitname:<name> a name beginning with a digit --
-- badpolicy:<value> REXXHTTP_ARGPOLICY set to an unknown value --
-- The processor renders 400 for the first four, 500 for badpolicy. --
--------------------------------------------------------------------------------
::Method validate public
Expose arg_violation
-- Argument policy. Only "strict" is recognised in 1.0; the env var is
-- already read (caseless, with REDIRECT_ fallback via the message
-- mechanism) so that .htaccess can configure it and 1.1 only needs to
-- add values to the enum. Absent => strict. Unknown => config error.
policy = self~REXXHTTP_ARGPOLICY
If policy == "" Then policy = "strict"
If policy~upper \== "STRICT" Then Return "badpolicy:"policy
self~process_args -- eager parse + strict validation
Return arg_violation
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- P R I V A T E M E T H O D S --
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
::Method process_args private
Expose args nargs args_processed request_method post_string arg_violation
If args_processed Then Return
-- Read the request body eagerly: the old lazy parsing existed only because
-- mod_rexx's wwwGetArgs committed the response. With mod_rexx gone, reading
-- stdin does not commit stdout, so we read up front, before the rexxlet runs.
--
-- POST, PUT and DELETE can all carry a body, and Apache frames all three
-- identically (MEASURED): CONTENT_LENGTH for a fixed-length body, chunked with
-- CONTENT_LENGTH empty, or no body at all. We must DRAIN stdin for every one of
-- them: an unread body leaves Apache blocked waiting for the CGI to consume it
-- (the same stall the POST drain fixed in v5). So the body read below is keyed
-- on "carries a body", not on POST alone.
--
-- Reading strategy, per the v96 measurements (design-post-body-reading.md):
-- CONTENT_LENGTH present -> one blocking read (fast path).
-- CONTENT_LENGTH absent + Transfer-Encoding: chunked -> read to EOF (Apache
-- unwraps the chunks but cannot fill CONTENT_LENGTH mid-receive).
-- neither -> empty body.
-- The chunked branch folded into "no body" was rejected in v96: it turns a
-- legitimate chunked request into silent data loss.
--
-- Where the ARGUMENTS come from is a SEPARATE decision (see below), and it is
-- NOT symmetric with the body read: only POST parses the body as arguments.
If self~carriesBody Then Do
If self~content_length \== "" Then
post_string = CharIn(,,self~content_length)
Else If self~transfer_encoding~caselessEquals("chunked") Then
post_string = self~readBodyToEOF
Else
post_string = ""
End
args = .stem~new
-- Argument source (v99 decision, PUT/DELETE): GET takes args from the query
-- string and POST from the body, as before. PUT and DELETE take args from the
-- QUERY STRING too, and leave their body untouched as raw bytes in
-- ~post_string for the rexxlet to interpret (it may be JSON, a binary
-- resource, anything -- there is no universal convention that a PUT/DELETE
-- body is form-urlencoded, so we do not presume to parse it). Only POST reads
-- its arguments from the body. See ref-decisiones.md "v99 -- PUT/DELETE".
If self~method == "POST" Then query = post_string
Else query = self~query_string
args[] = ""
nargs = 0
arg_violation = ""
-- Split on "&" FIRST, then analyse each token in isolation, so an
-- "&" can never be swallowed into a name (the old single-Parse
-- pattern mis-parsed "debug&x=1" as name="debug&x"). Strict policy:
-- every token must be name=value with a non-empty, unique (caseless)
-- name that does not begin with a digit (the Rexx symbol convention;
-- a numeric name would be unreachable through the arg method anyway).
-- Empty tokens (&&, trailing &) are skipped, not rejected.
rest = query
i = 0
Do While rest \== ""
Parse var rest token"&"rest
If token == "" Then Iterate -- && or trailing &: ignore
If token~pos("=") == 0 Then Do
arg_violation = "novalue:"token -- parameter without "="
Leave
End
Parse var token name"="value
-- Decode the NAME with the same decodeForm as the value (v11): a query
-- string is application/x-www-form-urlencoded, where BOTH sides of
-- each pair are percent-encoded and "+" means space. Decoding only
-- the value was a parity bug: arg("user name") could not find a
-- field sent as user+name. Validate the DECODED name, since that is
-- what the rexxlet sees and keys on.
name = self~decodeForm(name)
If name == "" Then Do
arg_violation = "noname:"token -- "=value", empty name
Leave
End
If name~left(1)~datatype("W") Then Do
arg_violation = "digitname:"name -- name beginning with a digit
Leave
End
key = name~upper -- caseless identity (Rexx-style)
If args~hasindex(key) Then Do
arg_violation = "duplicate:"name -- repeated name (caseless)
Leave
End
i = i + 1
args[i,"NAME"] = name -- decoded name (v11), as the rexxlet sees it
args[i,"VALUE"] = self~decodeForm(value)
args[key] = i -- search key is uppercased
nargs = i
End
args_processed = .true
Return
--------------------------------------------------------------------------------
-- READBODYTOEOF --
-- Read stdin to EOF in 8 K blocks. Used only for a chunked POST, where --
-- Apache hands us the reassembled body but no CONTENT_LENGTH. --
-- --
-- Two ooRexx facts govern this (both MEASURED, see --
-- design-post-body-reading.md): --
-- - ~chars returns 0 on a pipe even with data pending, so it is useless --
-- as a loop guard here. --
-- - CharIn(,,n) blocks until n bytes OR EOF; it never returns short on a --
-- slow sender. So a short read IS a reliable EOF signal. --
-- Blocks of 8 K match the single-read speed (0.06 s for 10 MB); byte by --
-- byte would be ~240x slower. --
--------------------------------------------------------------------------------
::Method readBodyToEOF private
bufsize = 8192
mb = .MutableBuffer~new
Do Forever
blk = CharIn(,,bufsize)
If blk == "" Then Leave
mb~append(blk)
If blk~length < bufsize Then Leave -- short read = EOF (see above)
End
Return mb~string
--------------------------------------------------------------------------------
-- CARRIESBODY (private): whether this method can carry a request body that we --
-- must drain from stdin. POST, PUT and DELETE do; GET (and everything else) --
-- does not. Draining is mandatory for all three even when we do not parse --
-- the body as arguments -- an unread body stalls Apache (v5 stdin drain). --
-- "|" does not short-circuit in ooRexx, but there is no guard dependency --
-- here (three independent equality tests), so it is correct and clear. --
--------------------------------------------------------------------------------
::Method carriesBody private
m = self~method
Return m == "POST" | m == "PUT" | m == "DELETE"
--------------------------------------------------------------------------------
::Method process_cookies private
Expose cookies ncookies cookies_processed
If cookies_processed Then Return
cookies = .stem~new
cookieline = self~http_cookie
cookies[] = ""
ncookies = 0
If cookieline == .nil | cookieline == "" Then Do
cookies_processed = .true
Return
End
-- RFC 6265: the Cookie header is "name=value; name=value": split FIRST,
-- decode each piece AFTER, so that an encoded "%3B" in a value can never
-- act as a separator. The space that follows each ";" is stripped from
-- the name. Decoding is percent-only: the "+" convention belongs to
-- form-urlencoded data (see decodeForm), not to cookies.
i = 0
Do While cookieline \== ""
Parse var cookieline pair";"cookieline
If pair~strip == "" Then Iterate
Parse var pair name"="value
name = self~decodeURIComponent(name~strip)
value = self~decodeURIComponent(value)
i = i + 1
cookies[i,"NAME"] = name
cookies[i,"VALUE"] = value
-- First-wins by name (v11): if two cookies share a name, the
-- browser sends the most specific path first, and established
-- server practice keeps the first. Positional cookie(n) still
-- sees them all; only the by-name lookup is pinned to the first.
-- (Lookup stays case-sensitive: no upper of the key, as in v10.)
If \cookies~hasindex(name) Then cookies[name] = i
ncookies = i
End
cookies_processed = .true
Return
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- P U B L I C M E T H O D S --
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- "[]" --
--------------------------------------------------------------------------------
::Method "[]"
Use Strict Arg entryname
name = .Validate~requestClassType(1, entryname, .String)
name = Upper(name)~translate("_","-")
-- Bracket access always goes to the environment pool, never to a
-- like-named method: [] is the raw-variable door, ~name is the
-- library face. (A name reaching a method is the job of ~name, which
-- the language already resolves method-first.)
Forward Array (name, .array~new) Message (unknown)
--------------------------------------------------------------------------------
-- ARG --
-- --
-- Syntax: (n is an integer, s a string --
-- arg() --> Number of arguments --
-- arg(n[,Value]) --> value for nth argument --
-- arg(s[,Value]) --> value for argname=s, or "" --
-- arg(s,Exists) --> If there is an arg named s then 1 else 0 --
-- Similarly with Omitted --
-- arg(n,Name) --> name of nth arg --
-- arg(n,Value) --> value of nth arg --
--------------------------------------------------------------------------------
::Method arg
Expose args nargs args_processed
If \args_processed Then self~process_args
If Arg() = 0 Then Return nargs
If Arg(1,'o') Then Raise syntax 93.903 array (1)
If Arg() > 2 Then Raise syntax 93.902 array (2)
n = .Validate~requestClassType(1, Arg(1), .String)
If Arg(2,'o') Then option = "V"
Else Do
option = .Validate~requestClassType(2, Arg(2), .String)
option = option~translate~strip~left(1)
If option = "" Then option = "V"
If "NVEO"~pos(option) == 0 Then
Raise syntax 93.914 array (2,"NVEO",option)
End
If n~datatype("w"), n > 0 Then Do
If n > nargs Then Do
If option == "E" Then Return .false
Else If option == "O" Then Return .true
Else Return ""
End
If option == "E" Then Return .true
If option == "O" Then Return .false
If option == "V" Then Return args[n,"VALUE"]
If option == "N" Then Return args[n,"NAME"]
End
-- Named lookup is caseless (Rexx-style): names are indexed uppercased
-- in process_args, so the search key must be uppercased too.
key = n~upper
If option == "E" Then Return args[key] \== ""
If option == "O" Then Return args[key] == ""
If option == "V" Then
If args[key] = "" Then Return ""
Else Return args[args[key],"VALUE"]
If option == "N" Then
If args[key] = "" Then Return ""
Else Return args[args[key],"NAME"]
--------------------------------------------------------------------------------
-- CONTENT_LENGTH --
--------------------------------------------------------------------------------
::Attribute content_length Get
--------------------------------------------------------------------------------
-- CONTENT_TYPE --
--------------------------------------------------------------------------------
::Attribute content_type Get
--------------------------------------------------------------------------------
-- TRANSFER_ENCODING --
-- The HTTP_TRANSFER_ENCODING header, read at init. "chunked" here (with an --
-- empty CONTENT_LENGTH) is the discriminator that tells process_args to --
-- read the POST body to EOF. See design-post-body-reading.md. --
--------------------------------------------------------------------------------
::Attribute transfer_encoding Get
--------------------------------------------------------------------------------
-- COOKIE --
-- --
-- Syntax: (n is an integer, s a string --
-- cookie() --> Number of cookies --
-- cookie(n[,Value]) --> value of nth cookie --
-- cookie(s[,Value]) --> value for cookiename=s, or "" --
-- cookie(s,Exists) --> If there is a cookie named s then 1 else 0 --
-- Similarly with Omitted --
-- cookie(n,Name) --> name of nth cookie --
-- cookie(n,Value) --> value of nth arg --
--------------------------------------------------------------------------------
::Method cookie
Expose cookies ncookies cookies_processed
If \cookies_processed Then self~process_cookies
If Arg() = 0 Then Return ncookies
If Arg(1,'o') Then Raise syntax 93.903 array (1)
If Arg() > 2 Then Raise syntax 93.902 array (2)
n = .Validate~requestClassType(1, Arg(1), .String)
If Arg(2,'o') Then option = "V"
Else Do
option = .Validate~requestClassType(2, Arg(2), .String)
option = option~translate~strip~left(1)
If option = "" Then option = "V"
If "NVEO"~pos(option) == 0 Then
Raise syntax 93.914 array (2,"NVEO",option)
End
If n~datatype("w"), n > 0 Then Do
If n > ncookies Then Do
If option == "E" Then Return .false
Else If option == "O" Then Return .true
Else Return ""
End
If option == "E" Then Return .true
If option == "O" Then Return .false
If option == "V" Then Return cookies[n,"VALUE"]
If option == "N" Then Return cookies[n,"NAME"]
End
If option == "E" Then Return cookies[n] \== ""
If option == "O" Then Return cookies[n] == ""
If option == "V" Then
If cookies[n] = "" Then Return ""
Else Return cookies[cookies[n],"VALUE"]
If option == "N" Then
If cookies[n] = "" Then Return ""
Else Return cookies[cookies[n],"NAME"]
--------------------------------------------------------------------------------
-- FILENAME --
-- The disk path of the DOCUMENT the client requested -- the file on disk --
-- of what was asked for. One hop: the rexxlet (requested == processor). Two --
-- hops: the document served through the processor (target.mtx), or the --
-- resolved DirectoryIndex file for a requested directory. Always a genuine --
-- file; any trailing PATH_INFO is excluded. Alias of PATH_TRANSLATED. The --
-- processor that runs it is SCRIPT_FILENAME. Resolved at init. --
--------------------------------------------------------------------------------
::Attribute filename Get
--------------------------------------------------------------------------------
-- HANDLER --
-- When the rexxlet is being used as a handler, the name of that handler as --
-- specified in the Apache configuration (e.g., "markdown"); otherwise, --
-- the method returns .Nil. --
--------------------------------------------------------------------------------
-- When a rexxlet is being used as a handler, there are _two_ levels of
-- redirect. We are interested in the last one.
::Method handler
handler = Env("REDIRECT_REDIRECT_HANDLER")
If handler = "" Then Return .Nil
Return handler
--------------------------------------------------------------------------------
-- IS_ACTION --
-- Returns .True when this rexxlet has been called as a handler using an --
-- Action Apache directive, and .False otherwise --
--------------------------------------------------------------------------------
::Method is_action
Return self~handler \== .Nil
--------------------------------------------------------------------------------
-- METHOD --
--------------------------------------------------------------------------------
::Method method
Forward message (request_method)
--------------------------------------------------------------------------------
-- PATH_INFO --
-- The extra path following the resource the CLIENT named -- "what you put --
-- after something". Set by init: for a standard rexxlet it is the raw --
-- handler-axis tail; for an Action it is the document-axis tail, split --
-- from the resolved document (see the Action cut in init). A plain --
-- attribute getter -- the value is fully resolved at construction. --
--------------------------------------------------------------------------------
::Attribute path_info Get
--------------------------------------------------------------------------------
-- PATH_TRANSLATED --
-- The disk path of the DOCUMENT requested -- alias of FILENAME (both --
-- assigned from the same resolution in init). One hop == SCRIPT_FILENAME; --
-- two hops it is the document, not the processor. For the processor's disk --
-- path (the callee) use SCRIPT_FILENAME. --
--------------------------------------------------------------------------------
::Attribute path_translated Get
--------------------------------------------------------------------------------
-- POST_STRING --
--------------------------------------------------------------------------------
::Attribute post_string Get
--------------------------------------------------------------------------------
-- QUERY_STRING --
--------------------------------------------------------------------------------
::Attribute query_string Get
--------------------------------------------------------------------------------
-- REQUEST_METHOD --
-- Alias: METHOD --
--------------------------------------------------------------------------------
::Method request_method
Expose request_method
Use Strict Arg
Return request_method
--------------------------------------------------------------------------------
-- REQUEST_URI --
-- Alias: UNPARSEDURI --
-- This is calculated at INIT time --
--------------------------------------------------------------------------------
::Attribute request_uri Get
--------------------------------------------------------------------------------
-- rawResourcePath (PRIVATE) --
-- The RAW, pre-resolution on-disk path of the resource the client asked --
-- for -- an internal step in computing FILENAME, not a public accessor. --
-- It reconstructs the path from the URL but does NOT resolve it against --
-- disk: in the Action case it still carries any trailing PATH_INFO, and --
-- for a directory request it names the directory, not the served index. --
-- The init runs a SysIsFile walk over this value to split the resolved --
-- document from its tail; the RESULT of that walk is FILENAME, which is --
-- the openable, document-on-disk path callers actually want. So this is --
-- deliberately not exposed: it can return a value readLines cannot open --
-- (".../doc.mtx/x/y", ".../sub/"). Public code wanting the document reads --
-- ~filename, which since the v79 disk-axis reorder always names the --
-- document (script_filename names the PROCESSOR). Kept private, and named --
-- for what it is (a raw resource path), to make that contract clear. --
-- --
-- For a plain rexxlet (one redirect hop) the requested resource IS the --
-- rexxlet, so this returns script_filename unchanged. For a rexxlet used --
-- as an Action (two hops, e.g. a .md served through the Markdown --
-- renderer) the document lives elsewhere -- possibly under an Alias --
-- outside the docroot -- and its path is reconstructed, deterministically --
-- and without guessing against disk, from a pair the installer/operator --
-- freezes in each <Directory>: --
-- REXXHTTP_BASE the zone's root ON DISK --
-- REXXHTTP_URLBASE the zone's URL prefix; "*" means "no prefix" --
-- (docroot or a plain subdir), "/ext" means aliased. --
-- Apache stacks a REDIRECT_ prefix per internal redirect, so the pair for --
-- the RESOURCE sits at the DEEPEST populated level (see the design note --
-- design-rexxlet-as-action.md). The rule, once the deepest pair is read: --
-- rel = REQUEST_URI (query string stripped) with urlbase peeled off --
-- file = base || rel --
-- --
-- One condition is a configuration error, reported as 500 via Raise: --
-- - REXXHTTP_BASE present but REXXHTTP_URLBASE empty at the resource --
-- level: the operator wrote the base but forgot the paired urlbase --
-- ("*" is the docroot value; empty is never legitimate here). Because --
-- Value() cannot tell an unset variable from one set to "", the "*" --
-- sentinel is what makes this distinguishable at all. --
--------------------------------------------------------------------------------
::Method rawResourcePath Private
Expose script_filename request_uri action_request urlbase base
Use Strict Arg
-- No anchor at any level: nothing to reconstruct from. Fall back to the
-- processor's own file (the plain, pre-REXXHTTP_BASE behaviour).
If base == "" Then Return script_filename
-- A plain rexxlet: the requested resource IS the rexxlet.
If \action_request Then Return script_filename
-- Two hops (rexxlet-as-Action). We need the full pair. An empty urlbase
-- here is the operator's forgotten SetEnv (docroot legitimately carries
-- the "*" sentinel, never "").
If urlbase == "" Then
Raise syntax 98.900 array -
("rawResourcePath: REXXHTTP_BASE set without a paired REXXHTTP_URLBASE")
-- Clean original URL: REQUEST_URI without any query string.
requri = request_uri
If requri~pos("?") > 0 Then requri = requri~left(requri~pos("?") - 1)
-- Peel the URL prefix off the front, unless it is the "*" sentinel
-- (no prefix: docroot or plain subdir). Nested If, not a comma guard,
-- to stay clear of the non-short-circuit "&" and the trailing-comma trap.
rel = requri
If urlbase \== "*" Then
If requri~left(urlbase~length) == urlbase Then
rel = requri~substr(urlbase~length + 1)
-- Decode the relative path before joining it to the on-disk base: REQUEST_URI
-- is %-encoded, but the disk path is not, so a document at "docs/mi
-- documento.md" arrives as "docs/mi%20documento.md" and the SysIsFile walk in
-- init would never find it (-> a garbled ~filename and a 500). Decode as a
-- PATH (decodeURIComponent: "+" is a literal plus, %xx decoded), matching the
-- requri decode in init. The urlbase peel above runs on the RAW prefix, which
-- an operator's SetEnv never percent-encodes, so peeling before decoding is
-- safe and keeps the prefix comparison exact. See ref-decisiones.md
-- "v99 -- rawResourcePath".
rel = self~decodeURIComponent(rel)
Return base || rel
--------------------------------------------------------------------------------
-- REQUEST_URL --
-- The self-referential URL: the client's own URL with any PATH_INFO and --
-- query string peeled off. Was named ~script_name until the CGI SCRIPT_NAME --
-- (RFC 3875, resolved resource) took over that name; this is the value the --
-- two production clients (calendario, SQLForm) actually consumed -- a URL --
-- to hang return links off, NOT the CGI processor path. Computed in init. --
--------------------------------------------------------------------------------
::Attribute request_url Get
--------------------------------------------------------------------------------
-- SCRIPT_FILENAME --
-- The disk path of the PROCESSOR -- the rexxlet Apache resolved and ran, --
-- the disk twin of SCRIPT_NAME. This is the callee RexxHTTP.rex Calls. One --
-- hop: == FILENAME. Two hops: the rexxlet, distinct from FILENAME/ --
-- PATH_TRANSLATED (the document). Resolved at init. --
--------------------------------------------------------------------------------
::Attribute script_filename Get
--------------------------------------------------------------------------------
-- SCRIPT_NAME --
-- The RFC 3875 SCRIPT_NAME: the URL-space path of the resource Apache --
-- actually resolved and ran, NOT the URL the client typed. They differ --
-- whenever Apache rewrites internally before reaching us: --
-- - DirectoryIndex: client asks "/test/", Apache resolves it to --
-- "/test/index.rxl"; the RFC SCRIPT_NAME is "/test/index.rxl", but --
-- ~request_url returns "/test/" (the self-referential URL, useful in --
-- its own right, just not what CGI names SCRIPT_NAME). --
-- - plain rexxlet (client names it): resolved == requested, so this --
-- returns the same value ~request_url does, by an honest path. --
-- --
-- This mirrors the reconstruction machinery of rawResourcePath. That --
-- method goes URL->disk (peel urlbase off REQUEST_URI, prepend base); --
-- here we take the resolved processor on disk (script_filename) and map --
-- it back into URL space: strip the on-disk base prefix, prepend the URL --
-- prefix. The REXXHTTP_BASE / REXXHTTP_URLBASE pair, and the --
-- deepest-redirect-level rule for reading it, are exactly the same. --
-- --
-- Where the pair is not configured (pre-REXXHTTP_BASE deployments) it --
-- falls back to the stored request_url value, so nothing regresses. --
--------------------------------------------------------------------------------
::Method script_name
Expose request_url script_filename base urlbase
Use Strict Arg
sep = .File~separator
-- No anchor configured: fall back to the stored request_url. This keeps
-- pre-REXXHTTP_BASE deployments behaving exactly as before.
If base == "" Then Return request_url
-- The resolved PROCESSOR on disk (script_filename). On Windows it may carry
-- native separators; normalise to "/" so the base-strip and the emitted
-- URL are both in URL space. normBase is a LOCAL copy: translating the
-- Exposed `base` in place would mutate the instance variable, so a later
-- ~RexxHTTP_DIR (and rawResourcePath, which reads `base`) would see "/"
-- where the native separator belongs. Windows only. See §3.5.
file = script_filename
normBase = base
If sep == "\" Then Do
file = file~translate("/", sep)
normBase = base~translate("/", sep)
End
-- Strip the on-disk base prefix to get the resource path relative to the
-- zone root, then map it into URL space by prepending the URL prefix
-- ("*" is the no-prefix sentinel: docroot or a plain subdir).
rel = file
If file~left(normBase~length) == normBase Then
rel = file~substr(normBase~length + 1)
-- Ensure a single leading "/" between prefix and relative path.
If rel~left(1) \== "/" Then rel = "/"rel
If urlbase == "*" Then Return rel
Return urlbase || rel
--------------------------------------------------------------------------------
-- SYSTEM_VERSION --
-- Returns the current system version string --
--------------------------------------------------------------------------------
::Method system_version
Use Strict Arg
Return "REXXHTTP/1.0 20260611"
--------------------------------------------------------------------------------
-- UNKNOWN --
--------------------------------------------------------------------------------
::Method unknown
Expose envvars
Use arg message, args
If args~dimension(1) > 0 Then Raise syntax 93.902 array (0)
-- ONE branch for the whole pool: read the variable directly and, if it
-- comes back empty, retry it REDIRECT_-prefixed. Client headers (HTTP_*)
-- used to have a branch of their own, without the fallback. They no longer
-- need one: measured against Apache 2.4.58 at one and at two redirect
-- levels, HTTP_* always arrive DIRECT and no REDIRECT_HTTP_* variable
-- exists in any topology. The reason is structural -- Apache prefixes what
-- was in the PREVIOUS request's subprocess_env, and HTTP_* are not
-- materialised until the CGI handler runs, i.e. only in the final request.
-- So for an HTTP_* name the fallback looks up a name Apache never creates,
-- and one branch behaves exactly as two did. See ref-decisiones "v113".
If \envvars~hasindex(message) Then Do
val = Env(message)
If val = "" Then val = Env("REDIRECT_"message)
envvars[message] = val
End
Return envvars[message]
--------------------------------------------------------------------------------
-- UNPARSEDURI --
-- Alias of : REQUEST_URI --
--------------------------------------------------------------------------------
::Method unparseduri
Forward message (request_uri)
--------------------------------------------------------------------------------
-- URI --
-- This is calculated at INIT time --
--------------------------------------------------------------------------------
::Attribute uri Get
--------------------------------------------------------------------------------
-- RexxHTTP_URL --
-- URL of the RexxHTTP package, or "" if RexxHTTP is not reachable --
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- RexxHTTP_DIR --
-- Filesystem directory of the RexxHTTP package -- no final "/" --
--------------------------------------------------------------------------------
::Method RexxHTTP_DIR
Expose base
Return base
--------------------------------------------------------------------------------
-- RexxHTTP_URL --
-- URL of the RexxHTTP package, or "" if RexxHTTP is not reachable --
-- When it is not "", it always ends with "/" (mod_dir convention) --
--------------------------------------------------------------------------------
::Method RexxHTTP_URL
Expose urlbase
If urlbase == "" Then Return "" -- unreachable: no pair configured
If urlbase == "*" Then Return "/" -- docroot: present, no prefix
Return urlbase"/" -- subdir: "/rexxhttp" -> "/rexxhttp/"
--------------------------------------------------------------------------------
-- DOCUMENT_URI --
-- The URL-space path of the DOCUMENT Apache actually resolved and served, --
-- carrying its URL prefix (e.g. "/mtx/target.mtx", "/ali/sub/index.mtx"). --
-- It is the disk->URL twin of ~filename, exactly as ~script_name is the --
-- twin of ~script_filename: filename names the served document on disk, --
-- document_uri names that same document in URL space. --
-- --
-- It therefore RESOLVES what ~uri leaves raw. ~uri is the client's typed --
-- URL minus the query string, so it keeps any extra PATH_INFO tail and, --
-- for a DirectoryIndex request, names the directory ("/mtx/sub/"). This --
-- value instead follows filename: the extra tail is gone (the document is --
-- named, not what came after it) and a DirectoryIndex is resolved to the --
-- index actually served ("/mtx/sub/index.mtx"). Under an Action it is the --
-- original document, not the processor -- script_name covers that axis. --
-- --
-- Reconstructed with the same REXXHTTP_BASE / REXXHTTP_URLBASE pair, read --
-- at the deepest redirect level, that script_name and rawResourcePath use: --
-- strip the on-disk base off filename to get the path relative to the zone --
-- root, then prepend the URL prefix ("*" is the no-prefix sentinel). Where --
-- the pair is not configured (pre-REXXHTTP_BASE deployments) it falls back --
-- to the stored ~uri, so nothing regresses. --
--------------------------------------------------------------------------------
::Method document_uri
Expose uri filename base urlbase
Use Strict Arg
sep = .File~separator
-- No anchor configured: fall back to the raw ~uri (client URL, query
-- stripped). Keeps pre-REXXHTTP_BASE deployments behaving as before.
If base == "" Then Return uri
-- The served DOCUMENT on disk (filename). On Windows it may carry native
-- separators; normalise to "/" so the base-strip and the emitted URL are
-- both in URL space. normBase is a LOCAL copy: translating the Exposed
-- `base` in place would mutate the instance variable, so a later
-- ~RexxHTTP_DIR (and rawResourcePath, which reads `base`) would see "/"
-- where the native separator belongs. Windows only. See §3.5.
file = filename
normBase = base
If sep == "\" Then Do
file = file~translate("/", sep)
normBase = base~translate("/", sep)
End
-- Strip the on-disk base prefix to get the document path relative to the
-- zone root, then map it into URL space by prepending the URL prefix
-- ("*" is the no-prefix sentinel: docroot or a plain subdir).
rel = file
If file~left(normBase~length) == normBase Then
rel = file~substr(normBase~length + 1)
-- Ensure a single leading "/" between prefix and relative path.
If rel~left(1) \== "/" Then rel = "/"rel
If urlbase == "*" Then Return rel
Return urlbase || rel
--------------------------------------------------------------------------------
-- DECODEURICOMPONENT -- Percent-decode a URL component (no "+" rule) --
--------------------------------------------------------------------------------
--
-- %xx-decoding alone: "+" stays literal. This is the correct decode for a
-- URL component and for cookie values, where "+" is a plus, not a space.
-- Inverse of encodeURIComponent (HTTP.Response). Available as a class method
-- (.HTTP.Request~decodeURIComponent(s)) and an instance method; the body
-- lives once, on the class. process_cookies uses it internally.
--
::Method decodeURIComponent Class
Use Strict Arg from
to = .mutablebuffer~new
lfrom = from~length
Do i = 1 To lfrom
c = from~substr(i,1)
If c == "%" , i+2 <= lfrom Then Do
hex = from~substr(i+1,2)
If hex~datatype("X") Then Do
i = i + 2
c = hex~x2c
End
End
to~append(c)
End
Return to~string
::Method decodeURIComponent
Use Strict Arg from
Return .HTTP.Request~decodeURIComponent(from)
--------------------------------------------------------------------------------
-- DECODEFORM -- Decode application/x-www-form-urlencoded --
--------------------------------------------------------------------------------
--
-- %xx-decoding PLUS the "+"->space rule. This is the decode for form data:
-- a conforming form serialiser emits space as "+", and this accepts both
-- "+" and %20 as space. Inverse of encodeForm (HTTP.Response). Available as
-- a class method and an instance method; the body lives once, on the class.
-- init and process_args use it internally.
--
::Method decodeForm Class
Use Strict Arg from
to = .mutablebuffer~new
lfrom = from~length
Do i = 1 To lfrom
c = from~substr(i,1)
If c == "+" Then c = " "
If c == "%" , i+2 <= lfrom Then Do
hex = from~substr(i+1,2)
If hex~datatype("X") Then Do
i = i + 2
c = hex~x2c
End
End
to~append(c)
End
Return to~string
::Method decodeForm
Use Strict Arg from
Return .HTTP.Request~decodeForm(from)
--------------------------------------------------------------------------------
-- Env: Return the value of an environment variable --
--------------------------------------------------------------------------------
::Routine Env
Return Value(Arg(1),,"ENVIRONMENT")