Added WSF_REQUEST.percent_encoded_path_info: READABLE_STRING_8
to keep url encoded path info, as it is useful for specific component
The router is now using WSF_REQUEST.percent_encoded_path_info
since URI_TEMPLATE are handling URI (and not IRI)
this fixes an issue with unicode path parameters.
This should not break existing code, and this fixes various unicode related issues related
to PATH parameter and path info
but also any component using file names.
(required EiffelStudio >= 7.2)
545 lines
13 KiB
Plaintext
545 lines
13 KiB
Plaintext
note
|
|
description: "Summary description for {WSF_FILE_SYSTEM_HANDLER}."
|
|
author: ""
|
|
date: "$Date$"
|
|
revision: "$Revision$"
|
|
|
|
class
|
|
WSF_FILE_SYSTEM_HANDLER
|
|
|
|
inherit
|
|
WSF_STARTS_WITH_HANDLER
|
|
rename
|
|
execute as execute_starts_with
|
|
end
|
|
|
|
WSF_SELF_DOCUMENTED_HANDLER
|
|
|
|
SHARED_HTML_ENCODER
|
|
|
|
SHARED_WSF_PERCENT_ENCODER
|
|
rename
|
|
percent_encoder as url_encoder
|
|
export
|
|
{NONE} all
|
|
end
|
|
|
|
SHARED_EXECUTION_ENVIRONMENT
|
|
export
|
|
{NONE} all
|
|
end
|
|
|
|
create
|
|
make_with_path,
|
|
make_hidden_with_path,
|
|
make,
|
|
make_hidden
|
|
|
|
feature {NONE} -- Initialization
|
|
|
|
make_with_path (d: like document_root)
|
|
do
|
|
if d.is_empty then
|
|
document_root := execution_environment.current_working_path
|
|
else
|
|
document_root := d
|
|
end
|
|
ensure
|
|
not document_root.is_empty
|
|
end
|
|
|
|
make_hidden_with_path (d: like document_root)
|
|
do
|
|
make_with_path (d)
|
|
is_hidden := True
|
|
ensure
|
|
hidden: is_hidden
|
|
end
|
|
|
|
make (d: READABLE_STRING_GENERAL)
|
|
do
|
|
make_with_path (create {PATH}.make_from_string (d))
|
|
end
|
|
|
|
make_hidden (d: READABLE_STRING_GENERAL)
|
|
do
|
|
make (d)
|
|
is_hidden := True
|
|
ensure
|
|
hidden: is_hidden
|
|
end
|
|
|
|
is_hidden: BOOLEAN
|
|
-- Current mapped handler should be hidden from self documentation
|
|
|
|
feature -- Documentation
|
|
|
|
mapping_documentation (m: WSF_ROUTER_MAPPING; a_request_methods: detachable WSF_REQUEST_METHODS): WSF_ROUTER_MAPPING_DOCUMENTATION
|
|
-- <Precursor>
|
|
do
|
|
create Result.make (m)
|
|
Result.set_is_hidden (is_hidden)
|
|
Result.add_description ("File service")
|
|
end
|
|
|
|
feature -- Access
|
|
|
|
document_root: PATH
|
|
|
|
max_age: INTEGER
|
|
|
|
index_disabled: BOOLEAN
|
|
-- Index disabled?
|
|
|
|
directory_index: detachable ARRAY [READABLE_STRING_8]
|
|
-- File serve if a directory index is requested
|
|
|
|
not_found_handler: detachable PROCEDURE [ANY, TUPLE [uri: READABLE_STRING_8; req: WSF_REQUEST; res: WSF_RESPONSE]]
|
|
|
|
access_denied_handler: detachable PROCEDURE [ANY, TUPLE [uri: READABLE_STRING_8; req: WSF_REQUEST; res: WSF_RESPONSE]]
|
|
|
|
feature -- Element change
|
|
|
|
enable_index
|
|
do
|
|
index_disabled := False
|
|
end
|
|
|
|
disable_index
|
|
do
|
|
index_disabled := True
|
|
end
|
|
|
|
set_max_age (a_seconds: like max_age)
|
|
do
|
|
max_age := a_seconds
|
|
end
|
|
|
|
set_directory_index (idx: like directory_index)
|
|
-- Set `directory_index' as `idx'
|
|
do
|
|
if idx = Void or else idx.is_empty then
|
|
directory_index := Void
|
|
else
|
|
directory_index := idx
|
|
end
|
|
end
|
|
|
|
set_not_found_handler (h: like not_found_handler)
|
|
-- Set `not_found_handler' to `h'
|
|
do
|
|
not_found_handler := h
|
|
end
|
|
|
|
set_access_denied_handler (h: like access_denied_handler)
|
|
-- Set `access_denied_handler' to `h'
|
|
do
|
|
access_denied_handler := h
|
|
end
|
|
|
|
feature -- Execution
|
|
|
|
execute (a_start_path: READABLE_STRING_8; req: WSF_REQUEST; res: WSF_RESPONSE)
|
|
local
|
|
p: STRING_32
|
|
do
|
|
create p.make_from_string (req.path_info)
|
|
if p.starts_with_general (a_start_path) then
|
|
p.remove_head (a_start_path.count)
|
|
else
|
|
check starts_with_base: False end
|
|
end
|
|
process_uri (p, req, res)
|
|
end
|
|
|
|
execute_starts_with (a_start_path: READABLE_STRING_8; req: WSF_REQUEST; res: WSF_RESPONSE)
|
|
do
|
|
execute (a_start_path, req, res)
|
|
end
|
|
|
|
process_uri (uri: READABLE_STRING_32; req: WSF_REQUEST; res: WSF_RESPONSE)
|
|
local
|
|
f: RAW_FILE
|
|
fn: like resource_filename
|
|
do
|
|
fn := resource_filename (uri)
|
|
create f.make_with_path (fn)
|
|
if f.exists then
|
|
if f.is_readable then
|
|
if f.is_directory then
|
|
if index_disabled then
|
|
process_directory_index_disabled (uri, req, res)
|
|
else
|
|
process_index (req.request_uri, fn, req, res)
|
|
end
|
|
else
|
|
process_file (f, req, res)
|
|
end
|
|
else
|
|
process_access_denied (uri, req, res)
|
|
end
|
|
else
|
|
process_not_found (uri, req, res)
|
|
end
|
|
end
|
|
|
|
process_index (a_uri: READABLE_STRING_8; dn: PATH; req: WSF_REQUEST; res: WSF_RESPONSE)
|
|
local
|
|
h: HTTP_HEADER
|
|
uri, s: STRING_8
|
|
d: DIRECTORY
|
|
l_files: LIST [PATH]
|
|
do
|
|
create d.make_with_path (dn)
|
|
d.open_read
|
|
if attached directory_index_file (d) as f then
|
|
process_file (f, req, res)
|
|
else
|
|
uri := a_uri
|
|
if not uri.is_empty and then uri [uri.count] /= '/' then
|
|
uri.append_character ('/')
|
|
end
|
|
s := "[
|
|
<html>
|
|
<head>
|
|
<title>Index for folder: $URI</title>
|
|
</head>
|
|
<body>
|
|
<h1>Index for $URI</h1>
|
|
<ul>
|
|
]"
|
|
s.replace_substring_all ("$URI", uri)
|
|
|
|
from
|
|
l_files := d.entries
|
|
l_files.start
|
|
until
|
|
l_files.after
|
|
loop
|
|
s.append ("<li><a href=%"" + uri)
|
|
url_encoder.append_percent_encoded_string_to (l_files.item.name, s)
|
|
s.append ("%">")
|
|
s.append (html_encoder.encoded_string (l_files.item.name))
|
|
s.append ("</a></li>%N")
|
|
l_files.forth
|
|
end
|
|
s.append ("[
|
|
</ul>
|
|
</body>
|
|
</html>
|
|
]"
|
|
)
|
|
|
|
create h.make
|
|
h.put_content_type_text_html
|
|
res.set_status_code ({HTTP_STATUS_CODE}.ok)
|
|
h.put_content_length (s.count)
|
|
res.put_header_text (h.string)
|
|
if not req.request_method.same_string ({HTTP_REQUEST_METHODS}.method_head) then
|
|
res.put_string (s)
|
|
end
|
|
res.flush
|
|
end
|
|
d.close
|
|
end
|
|
|
|
process_file (f: FILE; req: WSF_REQUEST; res: WSF_RESPONSE)
|
|
local
|
|
ext: READABLE_STRING_32
|
|
ct: detachable READABLE_STRING_8
|
|
fres: WSF_FILE_RESPONSE
|
|
dt: DATE_TIME
|
|
do
|
|
ext := extension (f.path.name)
|
|
ct := extension_mime_mapping.mime_type (ext)
|
|
if ct = Void then
|
|
ct := {HTTP_MIME_TYPES}.application_force_download
|
|
end
|
|
if
|
|
attached req.meta_string_variable ("HTTP_IF_MODIFIED_SINCE") as s_if_modified_since and then
|
|
attached http_date_format_to_date (s_if_modified_since) as l_if_modified_since_date and then
|
|
attached file_date (f) as f_date and then
|
|
f_date <= l_if_modified_since_date
|
|
then
|
|
process_not_modified (f_date, req, res)
|
|
else
|
|
create fres.make_with_content_type (ct, f.path.name)
|
|
fres.set_status_code ({HTTP_STATUS_CODE}.ok)
|
|
|
|
-- cache control
|
|
create dt.make_now_utc
|
|
fres.header.put_cache_control ("private, max-age=" + max_age.out)
|
|
fres.header.put_utc_date (dt)
|
|
if max_age > 0 then
|
|
dt := dt.twin
|
|
dt.second_add (max_age)
|
|
end
|
|
fres.header.put_expires_date (dt)
|
|
|
|
fres.set_answer_head_request_method (req.request_method.same_string ({HTTP_REQUEST_METHODS}.method_head))
|
|
res.send (fres)
|
|
end
|
|
end
|
|
|
|
process_not_modified (a_utc_date: detachable DATE_TIME; req: WSF_REQUEST; res: WSF_RESPONSE)
|
|
local
|
|
h: HTTP_HEADER
|
|
dt: DATE_TIME
|
|
do
|
|
create dt.make_now_utc
|
|
create h.make
|
|
h.put_cache_control ("private, max-age=" + max_age.out)
|
|
h.put_utc_date (dt)
|
|
if max_age > 0 then
|
|
dt := dt.twin
|
|
dt.second_add (max_age)
|
|
end
|
|
h.put_expires_date (dt)
|
|
|
|
if a_utc_date /= Void then
|
|
h.put_last_modified (a_utc_date)
|
|
end
|
|
res.set_status_code ({HTTP_STATUS_CODE}.not_modified)
|
|
res.put_header_text (h.string)
|
|
res.flush
|
|
end
|
|
|
|
process_not_found (uri: READABLE_STRING_8; req: WSF_REQUEST; res: WSF_RESPONSE)
|
|
local
|
|
h: HTTP_HEADER
|
|
s: STRING_8
|
|
do
|
|
if attached not_found_handler as hdl then
|
|
hdl.call ([uri, req, res])
|
|
else
|
|
create h.make
|
|
h.put_content_type_text_plain
|
|
create s.make_empty
|
|
s.append ("Resource %"" + uri + "%" not found%N")
|
|
res.set_status_code ({HTTP_STATUS_CODE}.not_found)
|
|
h.put_content_length (s.count)
|
|
res.put_header_text (h.string)
|
|
res.put_string (s)
|
|
res.flush
|
|
end
|
|
end
|
|
|
|
process_access_denied (uri: READABLE_STRING_8; req: WSF_REQUEST; res: WSF_RESPONSE)
|
|
local
|
|
h: HTTP_HEADER
|
|
s: STRING_8
|
|
do
|
|
if attached access_denied_handler as hdl then
|
|
hdl.call ([uri, req, res])
|
|
else
|
|
create h.make
|
|
h.put_content_type_text_plain
|
|
create s.make_empty
|
|
s.append ("Resource %"" + uri + "%": Access denied%N")
|
|
res.set_status_code ({HTTP_STATUS_CODE}.forbidden)
|
|
h.put_content_length (s.count)
|
|
res.put_header_text (h.string)
|
|
res.put_string (s)
|
|
res.flush
|
|
end
|
|
end
|
|
|
|
process_directory_index_disabled (uri: READABLE_STRING_8; req: WSF_REQUEST; res: WSF_RESPONSE)
|
|
local
|
|
h: HTTP_HEADER
|
|
s: STRING_8
|
|
do
|
|
if attached access_denied_handler as hdl then
|
|
hdl.call ([uri, req, res])
|
|
else
|
|
create h.make
|
|
h.put_content_type_text_plain
|
|
create s.make_empty
|
|
s.append ("Directory index: Access denied%N")
|
|
res.set_status_code ({HTTP_STATUS_CODE}.forbidden)
|
|
h.put_content_length (s.count)
|
|
res.put_header_text (h.string)
|
|
res.put_string (s)
|
|
res.flush
|
|
end
|
|
end
|
|
|
|
feature {NONE} -- Implementation
|
|
|
|
directory_index_file (d: DIRECTORY): detachable FILE
|
|
local
|
|
f: detachable RAW_FILE
|
|
fn: PATH
|
|
do
|
|
if attached directory_index as default_index then
|
|
across
|
|
default_index as c
|
|
until
|
|
Result /= Void
|
|
loop
|
|
if d.has_entry (c.item) then
|
|
fn := d.path.extended (c.item)
|
|
if f = Void then
|
|
create f.make_with_path (fn)
|
|
else
|
|
f.make_with_path (fn)
|
|
end
|
|
if f.exists and then f.is_readable then
|
|
Result := f
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
resource_filename (uri: READABLE_STRING_32): PATH
|
|
local
|
|
s: like uri_path_to_filename
|
|
do
|
|
Result := document_root
|
|
s := uri_path_to_filename (uri)
|
|
if not s.is_empty then
|
|
Result := Result.extended (s)
|
|
end
|
|
end
|
|
|
|
dirname (uri: READABLE_STRING_32): READABLE_STRING_32
|
|
local
|
|
p: INTEGER
|
|
do
|
|
p := uri.last_index_of ({CHARACTER_32} '/', uri.count)
|
|
if p > 0 then
|
|
Result := uri.substring (1, p - 1)
|
|
else
|
|
create {STRING_32} Result.make_empty
|
|
end
|
|
end
|
|
|
|
filename (uri: READABLE_STRING_32): READABLE_STRING_32
|
|
local
|
|
p: INTEGER
|
|
do
|
|
p := uri.last_index_of ({CHARACTER_32} '/', uri.count)
|
|
if p > 0 then
|
|
Result := uri.substring (p + 1, uri.count)
|
|
else
|
|
Result := uri.twin
|
|
end
|
|
end
|
|
|
|
extension (uri: READABLE_STRING_32): READABLE_STRING_32
|
|
local
|
|
p: INTEGER
|
|
do
|
|
p := uri.last_index_of ({CHARACTER_32} '.', uri.count)
|
|
if p > 0 then
|
|
Result := uri.substring (p + 1, uri.count)
|
|
else
|
|
create {STRING_32} Result.make_empty
|
|
end
|
|
end
|
|
|
|
uri_path_to_filename (fn: READABLE_STRING_32): STRING_32
|
|
-- Real filename from url-path `fn'
|
|
--| Find a better design for this piece of code
|
|
--| Eventually in a spec/$ISE_PLATFORM/ specific cluster
|
|
local
|
|
n: INTEGER
|
|
do
|
|
n := fn.count
|
|
create Result.make_from_string (fn)
|
|
if n > 0 and then Result.item (Result.count) = {CHARACTER_32} '/' then
|
|
Result.remove_tail (1)
|
|
n := n - 1
|
|
end
|
|
if n > 0 and then Result.item (1) = {CHARACTER_32} '/' then
|
|
Result.remove_head (1)
|
|
n := n - 1
|
|
end
|
|
|
|
if n > 0 then
|
|
if {PLATFORM}.is_windows then
|
|
Result.replace_substring_all ({STRING_32} "/", {STRING_32} "\")
|
|
end
|
|
end
|
|
end
|
|
|
|
feature {NONE} -- Implementation
|
|
|
|
extension_mime_mapping: HTTP_FILE_EXTENSION_MIME_MAPPING
|
|
local
|
|
f: RAW_FILE
|
|
once
|
|
create f.make_with_name ("mime.types")
|
|
if f.exists and then f.is_readable then
|
|
create Result.make_from_file (f.path.name)
|
|
else
|
|
create Result.make_default
|
|
end
|
|
end
|
|
|
|
feature {NONE} -- implementation: date time
|
|
|
|
file_date (f: FILE): DATE_TIME
|
|
do
|
|
Result := timestamp_to_date (f.date)
|
|
end
|
|
|
|
http_date_format_to_date (s: READABLE_STRING_8): detachable DATE_TIME
|
|
-- String representation of `dt' using the RFC 1123
|
|
-- HTTP-date = rfc1123-date | rfc850-date | asctime-date
|
|
-- rfc1123-date = wkday "," SP date1 SP time SP "GMT"
|
|
-- rfc850-date = weekday "," SP date2 SP time SP "GMT"
|
|
-- asctime-date = wkday SP date3 SP time SP 4DIGIT
|
|
-- date1 = 2DIGIT SP month SP 4DIGIT
|
|
-- ; day month year (e.g., 02 Jun 1982)
|
|
-- date2 = 2DIGIT "-" month "-" 2DIGIT
|
|
-- ; day-month-year (e.g., 02-Jun-82)
|
|
-- date3 = month SP ( 2DIGIT | ( SP 1DIGIT ))
|
|
-- ; month day (e.g., Jun 2)
|
|
-- time = 2DIGIT ":" 2DIGIT ":" 2DIGIT
|
|
-- ; 00:00:00 - 23:59:59
|
|
-- wkday = "Mon" | "Tue" | "Wed"
|
|
-- | "Thu" | "Fri" | "Sat" | "Sun"
|
|
-- weekday = "Monday" | "Tuesday" | "Wednesday"
|
|
-- | "Thursday" | "Friday" | "Saturday" | "Sunday"
|
|
-- month = "Jan" | "Feb" | "Mar" | "Apr"
|
|
-- | "May" | "Jun" | "Jul" | "Aug"
|
|
-- | "Sep" | "Oct" | "Nov" | "Dec"
|
|
--| Sun, 06 Nov 1994 08:49:37 GMT ; RFC 822, updated by RFC 1123
|
|
--| Sunday, 06-Nov-94 08:49:37 GMT ; RFC 850, obsoleted by RFC 1036
|
|
--| Sun Nov 6 08:49:37 1994 ; ANSI C's asctime() format
|
|
--|
|
|
--| "ddd, [0]dd mmm yyyy [0]hh:[0]mi:[0]ss.ff2"
|
|
--| ex: "WED, 30 JAN 2013 21:34:33 "
|
|
note
|
|
EIS: "name=RFC2616", "protocol=URI", "src=http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html"
|
|
local
|
|
d: HTTP_DATE
|
|
do
|
|
create d.make_from_string (s)
|
|
if not d.has_error then
|
|
Result := d.date_time
|
|
end
|
|
end
|
|
|
|
timestamp_to_date (n: INTEGER): DATE_TIME
|
|
local
|
|
d: HTTP_DATE
|
|
do
|
|
create d.make_from_timestamp (n)
|
|
Result := d.date_time
|
|
end
|
|
|
|
note
|
|
copyright: "2011-2013, Jocelyn Fiat, Javier Velilla, Olivier Ligot, Eiffel Software and others"
|
|
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
|
|
source: "[
|
|
Eiffel Software
|
|
5949 Hollister Ave., Goleta, CA 93117 USA
|
|
Telephone 805-685-1006, Fax 805-685-6869
|
|
Website http://www.eiffel.com
|
|
Customer support http://support.eiffel.com
|
|
]"
|
|
end
|