Compare commits
8 Commits
es_rev1003
...
dev_jwt
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0783049fb4 | ||
|
|
7e54825b84 | ||
|
|
40cbe7dfc9 | ||
|
|
d4b9301a57 | ||
|
|
06cda97535 | ||
|
|
c83b9d4231 | ||
|
|
69b5ce637e | ||
|
|
485a3812d9 |
24
CHANGELOG.md
24
CHANGELOG.md
@@ -8,12 +8,36 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
|
||||
## [Unreleased]
|
||||
### Added
|
||||
### Changed
|
||||
- http_network:
|
||||
Integrated changes on SOCKET so that EiffelWeb can be compiled with 16.05 to 17.05 and after.
|
||||
### Deprecated
|
||||
### Removed
|
||||
### Fixed
|
||||
- http_client:
|
||||
Improved query and form data encoding (based on a very early version of the general URI percent-encoding rules).
|
||||
- now correct encoding of space by '%20' in path segment, and '+' in query parameters.
|
||||
Unify and fixed query parameters handling for libcurl and net implementation.
|
||||
Fixed file uploading (various issue in libcurl, and net implementation).
|
||||
Fixed form multipart encoding by using correctly the boundary.
|
||||
- Code cleaning:
|
||||
Removed many obsolete calls, and added timestamp on EiffelWeb obsolete features to benefit from upcoming improvement on the EiffelStudio Inspector tool.
|
||||
### Security
|
||||
|
||||
|
||||
## [v1.0.5] - 2017-05-17
|
||||
### Changed
|
||||
- http_network:
|
||||
Integrated changes on SOCKET so that EiffelWeb can be compiled with 16.05 to 17.05 and after.
|
||||
### Fixed
|
||||
- http_client:
|
||||
Improved query and form data encoding (based on a very early version of the general URI percent-encoding rules).
|
||||
- now correct encoding of space by '%20' in path segment, and '+' in query parameters.
|
||||
Unify and fixed query parameters handling for libcurl and net implementation.
|
||||
Fixed file uploading (various issue in libcurl, and net implementation).
|
||||
Fixed form multipart encoding by using correctly the boundary.
|
||||
- Code cleaning:
|
||||
Removed many obsolete calls, and added timestamp on EiffelWeb obsolete features to benefit from upcoming improvement on the EiffelStudio Inspector tool.
|
||||
|
||||
## [v1.0.4] - 2017-03-15
|
||||
|
||||
### Added
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
</library>
|
||||
<library name="uri" location="$ISE_LIBRARY\library\text\uri\uri-safe.ecf"/>
|
||||
<cluster name="src" location=".\src\">
|
||||
<cluster name="implementation" location="$|implementation" recursive="true" hidden="true"/>
|
||||
<cluster name="spec_null" location="$|spec\null\" recursive="true"/>
|
||||
<cluster name="spec_net" location="$|spec\net\">
|
||||
<condition>
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
</library>
|
||||
<library name="uri" location="$ISE_LIBRARY\library\text\uri\uri.ecf"/>
|
||||
<cluster name="src" location=".\src\">
|
||||
<cluster name="implementation" location="$|implementation" recursive="true" hidden="true"/>
|
||||
<cluster name="spec_null" location="$|spec\null\" recursive="true"/>
|
||||
<cluster name="spec_net" location="$|spec\net\">
|
||||
<condition>
|
||||
|
||||
@@ -196,73 +196,8 @@ feature -- Settings
|
||||
Result := session.is_insecure
|
||||
end
|
||||
|
||||
feature {NONE} -- Utilities
|
||||
|
||||
append_parameters_to_url (a_parameters: HASH_TABLE [READABLE_STRING_32, READABLE_STRING_32]; a_url: STRING)
|
||||
-- Append parameters `a_parameters' to `a_url'
|
||||
require
|
||||
a_url_attached: a_url /= Void
|
||||
local
|
||||
l_first_param: BOOLEAN
|
||||
do
|
||||
if a_parameters.count > 0 then
|
||||
if a_url.index_of ('?', 1) > 0 then
|
||||
l_first_param := False
|
||||
elseif a_url.index_of ('&', 1) > 0 then
|
||||
l_first_param := False
|
||||
else
|
||||
l_first_param := True
|
||||
end
|
||||
|
||||
from
|
||||
a_parameters.start
|
||||
until
|
||||
a_parameters.after
|
||||
loop
|
||||
if l_first_param then
|
||||
a_url.append_character ('?')
|
||||
else
|
||||
a_url.append_character ('&')
|
||||
end
|
||||
a_url.append (urlencode (a_parameters.key_for_iteration))
|
||||
a_url.append_character ('=')
|
||||
a_url.append (urlencode (a_parameters.item_for_iteration))
|
||||
l_first_param := False
|
||||
a_parameters.forth
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
feature {NONE} -- Utilities: encoding
|
||||
|
||||
url_encoder: URL_ENCODER
|
||||
once
|
||||
create Result
|
||||
end
|
||||
|
||||
urlencode (s: READABLE_STRING_32): READABLE_STRING_8
|
||||
-- URL encode `s'
|
||||
do
|
||||
Result := url_encoder.encoded_string (s)
|
||||
end
|
||||
|
||||
urldecode (s: READABLE_STRING_8): READABLE_STRING_32
|
||||
-- URL decode `s'
|
||||
do
|
||||
Result := url_encoder.decoded_string (s)
|
||||
end
|
||||
|
||||
stripslashes (s: STRING): STRING
|
||||
do
|
||||
Result := s.string
|
||||
Result.replace_substring_all ("\%"", "%"")
|
||||
Result.replace_substring_all ("\'", "'")
|
||||
Result.replace_substring_all ("\/", "/")
|
||||
Result.replace_substring_all ("\\", "\")
|
||||
end
|
||||
|
||||
note
|
||||
copyright: "2011-2016, Jocelyn Fiat, Javier Velilla, Eiffel Software and others"
|
||||
copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others"
|
||||
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
|
||||
source: "[
|
||||
Eiffel Software
|
||||
|
||||
@@ -142,16 +142,16 @@ feature -- Element change
|
||||
end
|
||||
end
|
||||
|
||||
add_query_parameter (k: READABLE_STRING_32; v: READABLE_STRING_32)
|
||||
add_query_parameter (k: READABLE_STRING_GENERAL; v: READABLE_STRING_GENERAL)
|
||||
-- Add a query parameter `k=v'.
|
||||
do
|
||||
query_parameters.force (v, k)
|
||||
query_parameters.force (v.to_string_32, k.to_string_32)
|
||||
end
|
||||
|
||||
add_form_parameter (k: READABLE_STRING_32; v: READABLE_STRING_32)
|
||||
add_form_parameter (k: READABLE_STRING_GENERAL; v: READABLE_STRING_GENERAL)
|
||||
-- Add a form parameter `k'= `v'.
|
||||
do
|
||||
form_parameters.force (v, k)
|
||||
form_parameters.force (v.to_string_32, k.to_string_32)
|
||||
end
|
||||
|
||||
set_credentials_required (b: BOOLEAN)
|
||||
@@ -236,18 +236,62 @@ feature -- Status setting
|
||||
end
|
||||
end
|
||||
|
||||
feature -- URL helpers
|
||||
|
||||
append_query_parameters_to_url (a_url: STRING)
|
||||
-- Append parameters `a_parameters' to `a_url'
|
||||
require
|
||||
a_url_attached: a_url /= Void
|
||||
local
|
||||
l_first_param: BOOLEAN
|
||||
do
|
||||
if
|
||||
attached query_parameters as l_query_parameters and then
|
||||
not l_query_parameters.is_empty
|
||||
then
|
||||
if a_url.index_of ('?', 1) > 0 then
|
||||
l_first_param := False
|
||||
elseif a_url.index_of ('&', 1) > 0 then
|
||||
l_first_param := False
|
||||
else
|
||||
l_first_param := True
|
||||
end
|
||||
|
||||
across
|
||||
query_parameters as ic
|
||||
loop
|
||||
if l_first_param then
|
||||
a_url.append_character ('?')
|
||||
else
|
||||
a_url.append_character ('&')
|
||||
end
|
||||
l_first_param := False
|
||||
uri_percent_encoder.append_query_name_encoded_string_to (ic.key, a_url)
|
||||
a_url.append_character ('=')
|
||||
uri_percent_encoder.append_query_value_encoded_string_to (ic.item, a_url)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
feature -- Conversion helpers
|
||||
|
||||
query_parameters_to_url_encoded_string: STRING_8
|
||||
-- `query_parameters' as url-encoded string.
|
||||
do
|
||||
Result := parameters_to_url_encoded_string (query_parameters)
|
||||
Result := parameters_to_uri_percent_encoded_string (query_parameters)
|
||||
end
|
||||
|
||||
form_parameters_to_x_www_form_url_encoded_string: STRING_8
|
||||
-- `form_parameters' as x-www-form-urlencoded string.
|
||||
do
|
||||
Result := parameters_to_x_www_form_urlencoded_string (form_parameters)
|
||||
end
|
||||
|
||||
form_parameters_to_url_encoded_string: STRING_8
|
||||
-- `form_parameters' as url-encoded string.
|
||||
obsolete "Use form_parameters_to_x_www_form_url_encoded_string [2017-05-31]"
|
||||
do
|
||||
Result := parameters_to_url_encoded_string (form_parameters)
|
||||
Result := form_parameters_to_x_www_form_url_encoded_string
|
||||
end
|
||||
|
||||
feature {NONE} -- Implementation
|
||||
@@ -271,6 +315,52 @@ feature {NONE} -- Implementation
|
||||
end
|
||||
end
|
||||
|
||||
parameters_to_uri_percent_encoded_string (ht: HASH_TABLE [READABLE_STRING_32, READABLE_STRING_32]): STRING_8
|
||||
-- Build query urlencoded string using parameters from `ht'.
|
||||
do
|
||||
create Result.make (64)
|
||||
across
|
||||
ht as ic
|
||||
loop
|
||||
if not Result.is_empty then
|
||||
Result.append_character ('&')
|
||||
end
|
||||
uri_percent_encoder.append_query_name_encoded_string_to (ic.key, Result)
|
||||
Result.append_character ('=')
|
||||
uri_percent_encoder.append_query_value_encoded_string_to (ic.item, Result)
|
||||
end
|
||||
end
|
||||
|
||||
parameters_to_x_www_form_urlencoded_string (ht: HASH_TABLE [READABLE_STRING_32, READABLE_STRING_32]): STRING_8
|
||||
-- Build x-www-form-urlencoded string using parameters from `ht'.
|
||||
do
|
||||
create Result.make (64)
|
||||
from
|
||||
ht.start
|
||||
until
|
||||
ht.after
|
||||
loop
|
||||
if not Result.is_empty then
|
||||
Result.append_character ('&')
|
||||
end
|
||||
Result.append (x_www_form_url_encoder.encoded_string (ht.key_for_iteration))
|
||||
Result.append_character ('=')
|
||||
Result.append (x_www_form_url_encoder.encoded_string (ht.item_for_iteration))
|
||||
ht.forth
|
||||
end
|
||||
end
|
||||
|
||||
x_www_form_url_encoder: X_WWW_FORM_URL_ENCODER
|
||||
-- Shared x-www-form-urlencoded encoder.
|
||||
once
|
||||
create Result
|
||||
end
|
||||
|
||||
uri_percent_encoder: URI_PERCENT_ENCODER
|
||||
once
|
||||
create Result
|
||||
end
|
||||
|
||||
url_encoder: URL_ENCODER
|
||||
-- Shared URL encoder.
|
||||
once
|
||||
@@ -278,7 +368,7 @@ feature {NONE} -- Implementation
|
||||
end
|
||||
|
||||
note
|
||||
copyright: "2011-2015, Jocelyn Fiat, Javier Velilla, Eiffel Software and others"
|
||||
copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others"
|
||||
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
|
||||
source: "[
|
||||
Eiffel Software
|
||||
|
||||
@@ -60,28 +60,10 @@ feature -- Access
|
||||
|
||||
url (a_path: READABLE_STRING_8; ctx: detachable HTTP_CLIENT_REQUEST_CONTEXT): STRING_8
|
||||
-- Url computed from Current and `ctx' data.
|
||||
local
|
||||
s: STRING_8
|
||||
url_encoder: URL_ENCODER
|
||||
do
|
||||
Result := base_url + a_path
|
||||
if ctx /= Void then
|
||||
create s.make_empty
|
||||
create url_encoder
|
||||
across
|
||||
ctx.query_parameters as q
|
||||
loop
|
||||
if not s.is_empty then
|
||||
s.append_character ('&')
|
||||
end
|
||||
s.append (url_encoder.encoded_string (q.key))
|
||||
s.append_character ('=')
|
||||
s.append (url_encoder.encoded_string (q.item))
|
||||
end
|
||||
if not s.is_empty then
|
||||
Result.append_character ('?')
|
||||
Result.append (s)
|
||||
end
|
||||
ctx.append_query_parameters_to_url (Result)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -420,7 +402,7 @@ feature -- Element change
|
||||
end
|
||||
|
||||
note
|
||||
copyright: "2011-2016, Jocelyn Fiat, Javier Velilla, Eiffel Software and others"
|
||||
copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others"
|
||||
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
|
||||
source: "[
|
||||
Eiffel Software
|
||||
|
||||
@@ -0,0 +1,628 @@
|
||||
note
|
||||
description: "[
|
||||
Component to handle percent encoding
|
||||
|
||||
WARNING: THIS IS A COPY FROM $ISE_LIBRARY/library/text/uri library.
|
||||
In the future, http_client will use directly the `uri` library.
|
||||
]"
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
EIS: "name=Percent-encoding", "protocol=URI", "src=http://en.wikipedia.org/wiki/Percent-encoding"
|
||||
|
||||
class
|
||||
URI_PERCENT_ENCODER
|
||||
|
||||
feature -- Percent encoding
|
||||
|
||||
append_percent_encoded_string_to (s: READABLE_STRING_GENERAL; a_result: STRING_GENERAL)
|
||||
-- Append `s' as percent-encoded value to `a_result'
|
||||
local
|
||||
i,n: INTEGER
|
||||
do
|
||||
from
|
||||
i := 1
|
||||
n := s.count
|
||||
until
|
||||
i > n
|
||||
loop
|
||||
append_encoded_character_code_to (s.code (i), a_result)
|
||||
i := i + 1
|
||||
end
|
||||
end
|
||||
|
||||
append_query_name_encoded_string_to (s: READABLE_STRING_GENERAL; a_result: STRING_GENERAL)
|
||||
-- Append `s' as encoded for URI query name to `a_result'
|
||||
local
|
||||
i,n: INTEGER
|
||||
do
|
||||
from
|
||||
i := 1
|
||||
n := s.count
|
||||
until
|
||||
i > n
|
||||
loop
|
||||
append_query_name_encoded_character_code_to (s.code (i), a_result)
|
||||
i := i + 1
|
||||
end
|
||||
end
|
||||
|
||||
append_query_value_encoded_string_to (s: READABLE_STRING_GENERAL; a_result: STRING_GENERAL)
|
||||
-- Append `s' as encoded for URI query value to `a_result'.
|
||||
local
|
||||
i,n: INTEGER
|
||||
do
|
||||
from
|
||||
i := 1
|
||||
n := s.count
|
||||
until
|
||||
i > n
|
||||
loop
|
||||
append_query_value_encoded_character_code_to (s.code (i), a_result)
|
||||
i := i + 1
|
||||
end
|
||||
end
|
||||
|
||||
append_path_segment_encoded_string_to (s: READABLE_STRING_GENERAL; a_result: STRING_GENERAL)
|
||||
-- Append `a_string' as encoded for URI path segment to `a_result'
|
||||
local
|
||||
i,n: INTEGER
|
||||
do
|
||||
from
|
||||
i := 1
|
||||
n := s.count
|
||||
until
|
||||
i > n
|
||||
loop
|
||||
append_path_segment_encoded_character_code_to (s.code (i), a_result)
|
||||
i := i + 1
|
||||
end
|
||||
end
|
||||
|
||||
append_www_form_url_encoded_string_to (s: READABLE_STRING_GENERAL; a_result: STRING_GENERAL)
|
||||
-- Append `a_string' as www-form-urlencoded value to `a_result'.
|
||||
-- The main difference with `append_percent_encoded_string_to` is the encoding of space using '+'.
|
||||
local
|
||||
i,n: INTEGER
|
||||
do
|
||||
from
|
||||
i := 1
|
||||
n := s.count
|
||||
until
|
||||
i > n
|
||||
loop
|
||||
inspect s.code (i)
|
||||
when 32 then -- space: 32 ' '
|
||||
a_result.append_code (43) -- 43 '+'
|
||||
else
|
||||
append_encoded_character_code_to (s.code (i), a_result)
|
||||
end
|
||||
i := i + 1
|
||||
end
|
||||
end
|
||||
|
||||
feature {NONE} -- URI building helpers
|
||||
|
||||
append_encoded_character_code_to (c: NATURAL_32; a_result: STRING_GENERAL)
|
||||
-- Append character code `c' as query name encoded content into `a_result'.
|
||||
do
|
||||
if
|
||||
--| unreserved ALPHA / DIGIT
|
||||
(48 <= c and c <= 57) -- DIGIT: 0 .. 9
|
||||
or (65 <= c and c <= 90) -- ALPHA: A .. Z
|
||||
or (97 <= c and c <= 122) -- ALPHA: a .. z
|
||||
then
|
||||
a_result.append_code (c)
|
||||
else
|
||||
inspect c
|
||||
when
|
||||
45, 46, 95, 126 -- unreserved characters: -._~
|
||||
then
|
||||
a_result.append_code (c)
|
||||
when
|
||||
58, 64, -- reserved =+ gen-delims: : @
|
||||
33, 36, 38, 39, 40, 41, 42, -- reserved =+ sub-delims: ! $ & ' ( ) *
|
||||
43, 44, 59, 61, -- reserved = sub-delims: + , ; =
|
||||
37 -- percent encoding: %
|
||||
then
|
||||
append_percent_encoded_character_code_to (c, a_result)
|
||||
else
|
||||
append_percent_encoded_character_code_to (c, a_result)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
append_query_name_encoded_character_code_to (c: NATURAL_32; a_result: STRING_GENERAL)
|
||||
-- Append character code `a_code' as query name encoded content into `a_result'.
|
||||
do
|
||||
inspect c
|
||||
when 61 then -- equal sign: =
|
||||
append_percent_encoded_character_code_to (c, a_result)
|
||||
else
|
||||
append_query_value_encoded_character_code_to (c, a_result)
|
||||
end
|
||||
end
|
||||
|
||||
append_query_value_encoded_character_code_to (c: NATURAL_32; a_result: STRING_GENERAL)
|
||||
-- Append character code `a_code' as query value encoded content into `a_result'.
|
||||
do
|
||||
inspect c
|
||||
when 32 then -- Space
|
||||
a_result.append_code (43) -- 43 '+'
|
||||
when
|
||||
39, -- '
|
||||
58, 64, -- reserved =+ gen-delims: : @
|
||||
33, 36, 40, 41, 42, -- reserved =+ sub-delims: ! $ ( ) *
|
||||
44, 59, 61 -- reserved = sub-delims: , ; =
|
||||
then
|
||||
a_result.append_code (c)
|
||||
when
|
||||
47, -- slash: /
|
||||
63 -- question mark ?
|
||||
then
|
||||
a_result.append_code (c)
|
||||
else
|
||||
append_encoded_character_code_to (c, a_result)
|
||||
end
|
||||
end
|
||||
|
||||
append_path_segment_encoded_character_code_to (c: NATURAL_32; a_result: STRING_GENERAL)
|
||||
-- Append character code `a_code' as query name encoded content into `a_result'.
|
||||
do
|
||||
append_encoded_character_code_to (c, a_result)
|
||||
end
|
||||
|
||||
feature -- Percent encoding: character
|
||||
|
||||
append_percent_encoded_character_code_to (a_code: NATURAL_32; a_result: STRING_GENERAL)
|
||||
-- Append character code `a_code' as percent-encoded content into `a_result'
|
||||
do
|
||||
if a_code > 0xFF then
|
||||
-- Unicode
|
||||
append_percent_encoded_unicode_character_code_to (a_code, a_result)
|
||||
elseif a_code > 0x7F then
|
||||
-- Extended ASCII
|
||||
-- This requires percent-encoding on UTF-8 converted character.
|
||||
append_percent_encoded_unicode_character_code_to (a_code, a_result)
|
||||
else
|
||||
-- ASCII
|
||||
append_percent_encoded_ascii_character_code_to (a_code, a_result)
|
||||
end
|
||||
ensure
|
||||
appended: a_result.count > old a_result.count
|
||||
end
|
||||
|
||||
feature {NONE} -- Implementation: character encoding
|
||||
|
||||
append_percent_encoded_ascii_character_code_to (a_code: NATURAL_32; a_result: STRING_GENERAL)
|
||||
-- Append extended ascii character code `a_code' as percent-encoded content into `a_result'
|
||||
-- Note: it does not UTF-8 convert this extended ASCII.
|
||||
require
|
||||
is_extended_ascii: a_code <= 0xFF
|
||||
local
|
||||
c: INTEGER
|
||||
do
|
||||
if a_code > 0xFF then
|
||||
-- Unicode
|
||||
append_percent_encoded_unicode_character_code_to (a_code, a_result)
|
||||
else
|
||||
-- Extended ASCII
|
||||
c := a_code.to_integer_32
|
||||
a_result.append_code (37) -- 37 '%%'
|
||||
a_result.append_code (hex_digit [c |>> 4])
|
||||
a_result.append_code (hex_digit [c & 0xF])
|
||||
end
|
||||
ensure
|
||||
appended: a_result.count > old a_result.count
|
||||
end
|
||||
|
||||
append_percent_encoded_unicode_character_code_to (a_code: NATURAL_32; a_result: STRING_GENERAL)
|
||||
-- Append Unicode character code `a_code' as UTF-8 and percent-encoded content into `a_result'
|
||||
-- Note: it does include UTF-8 conversion of extended ASCII and Unicode.
|
||||
do
|
||||
if a_code <= 0x7F then
|
||||
-- 0xxxxxxx
|
||||
append_percent_encoded_ascii_character_code_to (a_code, a_result)
|
||||
elseif a_code <= 0x7FF then
|
||||
-- 110xxxxx 10xxxxxx
|
||||
append_percent_encoded_ascii_character_code_to ((a_code |>> 6) | 0xC0, a_result)
|
||||
append_percent_encoded_ascii_character_code_to ((a_code & 0x3F) | 0x80, a_result)
|
||||
elseif a_code <= 0xFFFF then
|
||||
-- 1110xxxx 10xxxxxx 10xxxxxx
|
||||
append_percent_encoded_ascii_character_code_to ((a_code |>> 12) | 0xE0, a_result)
|
||||
append_percent_encoded_ascii_character_code_to (((a_code |>> 6) & 0x3F) | 0x80, a_result)
|
||||
append_percent_encoded_ascii_character_code_to ((a_code & 0x3F) | 0x80, a_result)
|
||||
else
|
||||
-- c <= 1FFFFF - there are no higher code points
|
||||
-- 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
|
||||
append_percent_encoded_ascii_character_code_to ((a_code |>> 18) | 0xF0, a_result)
|
||||
append_percent_encoded_ascii_character_code_to (((a_code |>> 12) & 0x3F) | 0x80, a_result)
|
||||
append_percent_encoded_ascii_character_code_to (((a_code |>> 6) & 0x3F) | 0x80, a_result)
|
||||
append_percent_encoded_ascii_character_code_to ((a_code & 0x3F) | 0x80, a_result)
|
||||
end
|
||||
ensure
|
||||
appended: a_result.count > old a_result.count
|
||||
end
|
||||
|
||||
feature -- Percent decoding
|
||||
|
||||
append_percent_decoded_string_to (v: READABLE_STRING_GENERAL; a_result: STRING_GENERAL)
|
||||
-- Append to `a_result' a string equivalent to the percent-encoded string `v'
|
||||
--| Note that is `a_result' is a STRING_8, any Unicode character will be kept as UTF-8
|
||||
local
|
||||
i,n: INTEGER
|
||||
c: NATURAL_32
|
||||
pr: CELL [INTEGER]
|
||||
a_result_is_string_32: BOOLEAN
|
||||
do
|
||||
a_result_is_string_32 := attached {STRING_32} a_result
|
||||
from
|
||||
i := 1
|
||||
create pr.put (i)
|
||||
n := v.count
|
||||
until
|
||||
i > n
|
||||
loop
|
||||
c := v.code (i)
|
||||
inspect c
|
||||
when 43 then -- 43 '+'
|
||||
-- Some implementation are replacing spaces with "+" instead of "%20"
|
||||
a_result.append_code (32) -- 32 ' '
|
||||
when 37 then -- 37 '%%'
|
||||
-- An escaped character ?
|
||||
if i = n then -- Error?
|
||||
a_result.append_code (c)
|
||||
else
|
||||
if a_result_is_string_32 then
|
||||
-- Convert UTF-8 to UTF-32
|
||||
pr.replace (i)
|
||||
c := next_percent_decoded_unicode_character_code (v, pr)
|
||||
a_result.append_code (c)
|
||||
i := pr.item
|
||||
else
|
||||
-- Keep UTF-8
|
||||
pr.replace (i)
|
||||
c := next_percent_decoded_character_code (v, pr)
|
||||
a_result.append_code (c)
|
||||
i := pr.item
|
||||
end
|
||||
end
|
||||
else
|
||||
if c <= 0x7F then
|
||||
a_result.append_code (c)
|
||||
else
|
||||
if a_result_is_string_32 then
|
||||
a_result.append_code (c)
|
||||
else
|
||||
-- Keep the percent encoded char for non string 32.
|
||||
append_percent_encoded_character_code_to (c, a_result)
|
||||
end
|
||||
end
|
||||
end
|
||||
i := i + 1
|
||||
end
|
||||
end
|
||||
|
||||
feature {NONE} -- Implementation: decoding
|
||||
|
||||
next_percent_decoded_character_code (v: READABLE_STRING_GENERAL; a_position: CELL [INTEGER]): NATURAL_32
|
||||
-- Character decoded from string `v' starting from index `a_position.item'
|
||||
-- note: it also updates `a_position.item' to indicate the new index position.
|
||||
require
|
||||
valid_start: a_position.item <= v.count
|
||||
is_percent_char: v.code (a_position.item) = 37 -- 37 '%%'
|
||||
local
|
||||
c: NATURAL_32
|
||||
i, n: INTEGER
|
||||
not_a_digit: BOOLEAN
|
||||
ascii_pos: NATURAL_32
|
||||
ival: NATURAL_32
|
||||
pos: INTEGER
|
||||
c_is_digit: BOOLEAN
|
||||
do
|
||||
--| pos is index in stream of escape character ('%')
|
||||
pos := a_position.item
|
||||
c := v.code (pos + 1)
|
||||
if c = 85 or c = 117 then -- 117 'u' 85 'U'
|
||||
-- NOTE: this is not a standard, but it can occur, so use this for decoding only
|
||||
-- An escaped Unicode (ucs2) value, from ECMA scripts
|
||||
-- has the form: %u<n> where <n> is the UCS value
|
||||
-- of the character (two byte integer, one to 4 chars
|
||||
-- after escape sequence).
|
||||
-- See: http://en.wikipedia.org/wiki/Percent-encoding#Non-standard_implementations
|
||||
-- UTF-8 result can be 1 to 4 characters.
|
||||
from
|
||||
i := pos + 2
|
||||
n := v.count
|
||||
until
|
||||
(i > n) or not_a_digit
|
||||
loop
|
||||
c := v.code (i)
|
||||
c_is_digit := (48 <= c and c <= 57) -- DIGIT: 0 .. 9
|
||||
if
|
||||
c_is_digit
|
||||
or (97 <= c and c <= 102) -- ALPHA: a..f
|
||||
or (65 <= c and c <= 70) -- ALPHA: A..F
|
||||
then
|
||||
ival := ival * 16
|
||||
if c_is_digit then
|
||||
ival := ival + (c - 48) -- 48 '0'
|
||||
else
|
||||
if c > 70 then -- a..f
|
||||
ival := ival + (c - 97) + 10 -- 97 'a'
|
||||
else -- A..F
|
||||
ival := ival + (c - 65) + 10 -- 65 'A'
|
||||
end
|
||||
end
|
||||
i := i + 1
|
||||
else
|
||||
not_a_digit := True
|
||||
i := i - 1
|
||||
end
|
||||
end
|
||||
a_position.replace (i)
|
||||
Result := ival
|
||||
else
|
||||
-- ASCII char?
|
||||
ascii_pos := hexadecimal_string_to_natural_32 (v.substring (pos + 1, pos + 2))
|
||||
Result := ascii_pos
|
||||
a_position.replace (pos + 2)
|
||||
end
|
||||
end
|
||||
|
||||
next_percent_decoded_unicode_character_code (v: READABLE_STRING_GENERAL; a_position: CELL [INTEGER]): NATURAL_32
|
||||
-- Next decoded character from `v' at position `a_position.item'
|
||||
-- note: it also updates `a_position' to indicate the new index position.
|
||||
require
|
||||
valid_start: a_position.item <= v.count
|
||||
is_percent_char: v.code (a_position.item) = 37 -- 37 '%%'
|
||||
local
|
||||
n, j: INTEGER
|
||||
c: NATURAL_32
|
||||
c1, c2, c3, c4: NATURAL_32
|
||||
pr: CELL [INTEGER]
|
||||
do
|
||||
create pr.put (a_position.item)
|
||||
c1 := next_percent_decoded_character_code (v, pr)
|
||||
|
||||
j := pr.item
|
||||
n := v.count
|
||||
|
||||
Result := c1
|
||||
a_position.replace (j)
|
||||
|
||||
if c1 <= 0x7F then
|
||||
-- 0xxxxxxx
|
||||
Result := c1
|
||||
elseif c1 <= 0xDF then
|
||||
-- 110xxxxx 10xxxxxx
|
||||
if j + 2 <= n then
|
||||
c := v.code (j + 1)
|
||||
if c = 37 then -- 37 '%%'
|
||||
pr.replace (j + 1)
|
||||
c2 := next_percent_decoded_character_code (v, pr)
|
||||
j := pr.item
|
||||
Result := (
|
||||
((c1 & 0x1F) |<< 6) |
|
||||
( c2 & 0x3F )
|
||||
)
|
||||
a_position.replace (j)
|
||||
else
|
||||
-- Do not try to decode
|
||||
end
|
||||
end
|
||||
elseif c1 <= 0xEF then
|
||||
-- 1110xxxx 10xxxxxx 10xxxxxx
|
||||
if j + 2 <= n then
|
||||
c := v.code (j + 1)
|
||||
if c = 37 then -- 37 '%%'
|
||||
pr.replace (j + 1)
|
||||
c2 := next_percent_decoded_character_code (v, pr)
|
||||
j := pr.item
|
||||
if j + 2 <= n then
|
||||
c := v.code (j + 1)
|
||||
if c = 37 then -- 37 '%%'
|
||||
pr.replace (j + 1)
|
||||
c3 := next_percent_decoded_character_code (v, pr)
|
||||
j := pr.item
|
||||
|
||||
Result := (
|
||||
((c1 & 0xF) |<< 12) |
|
||||
((c2 & 0x3F) |<< 6) |
|
||||
( c3 & 0x3F )
|
||||
)
|
||||
a_position.replace (j)
|
||||
else
|
||||
-- Do not try to decode
|
||||
end
|
||||
end
|
||||
else
|
||||
-- Do not try to decode
|
||||
end
|
||||
end
|
||||
elseif c1 <= 0xF7 then
|
||||
-- 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
|
||||
|
||||
if j + 2 <= n then
|
||||
c := v.code (j + 1)
|
||||
if c = 37 then -- 37 '%%'
|
||||
pr.replace (j + 1)
|
||||
c2 := next_percent_decoded_character_code (v, pr)
|
||||
j := pr.item
|
||||
if j + 2 <= n then
|
||||
c := v.code (j + 1)
|
||||
if c = 37 then -- 37 '%%'
|
||||
pr.replace (j + 1)
|
||||
c3 := next_percent_decoded_character_code (v, pr)
|
||||
j := pr.item
|
||||
if j + 2 <= n then
|
||||
c := v.code (j + 1)
|
||||
if c = 37 then -- 37 '%%'
|
||||
pr.replace (j + 1)
|
||||
c4 := next_percent_decoded_character_code (v, pr)
|
||||
j := pr.item
|
||||
|
||||
a_position.replace (j)
|
||||
|
||||
Result := (
|
||||
((c1 & 0x7) |<< 18 ) |
|
||||
((c2 & 0x3F) |<< 12) |
|
||||
((c3 & 0x3F) |<< 6) |
|
||||
( c4 & 0x3F )
|
||||
)
|
||||
else
|
||||
-- Do not try to decode
|
||||
end
|
||||
end
|
||||
else
|
||||
-- Do not try to decode
|
||||
end
|
||||
end
|
||||
else
|
||||
-- Do not try to decode
|
||||
end
|
||||
end
|
||||
else
|
||||
Result := c1
|
||||
end
|
||||
end
|
||||
|
||||
feature -- RFC and characters
|
||||
|
||||
is_hexa_decimal_character (c: CHARACTER_32): BOOLEAN
|
||||
-- Is hexadecimal character ?
|
||||
do
|
||||
Result := ('a' <= c and c <= 'f') or ('A' <= c and c <= 'F') -- HEXA
|
||||
or ('0' <= c and c <= '9') -- DIGIT
|
||||
end
|
||||
|
||||
is_alpha_or_digit_character (c: CHARACTER_32): BOOLEAN
|
||||
-- Is ALPHA or DIGIT character ?
|
||||
do
|
||||
Result := ('a' <= c and c <= 'z') or ('A' <= c and c <= 'Z') -- ALPHA
|
||||
or ('0' <= c and c <= '9') -- DIGIT
|
||||
end
|
||||
|
||||
is_alpha_character (c: CHARACTER_32): BOOLEAN
|
||||
-- Is ALPHA character ?
|
||||
do
|
||||
Result := ('a' <= c and c <= 'z') or ('A' <= c and c <= 'Z')
|
||||
end
|
||||
|
||||
is_digit_character (c: CHARACTER_32): BOOLEAN
|
||||
-- Is DIGIT character ?
|
||||
do
|
||||
Result := ('0' <= c and c <= '9')
|
||||
end
|
||||
|
||||
is_unreserved_character (c: CHARACTER_32): BOOLEAN
|
||||
-- unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
|
||||
do
|
||||
if
|
||||
('a' <= c and c <= 'z') -- ALPHA
|
||||
or ('A' <= c and c <= 'Z') -- ALPHA
|
||||
or ('0' <= c and c <= '9') -- DIGIT
|
||||
then
|
||||
Result := True
|
||||
else
|
||||
inspect c
|
||||
when '-', '_', '.', '~' then -- unreserved
|
||||
Result := True
|
||||
else
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
is_reserved_character (c: CHARACTER_32): BOOLEAN
|
||||
-- reserved = gen-delims / sub-delims
|
||||
do
|
||||
Result := is_gen_delims_character (c) or is_sub_delims_character (c)
|
||||
end
|
||||
|
||||
is_gen_delims_character (c: CHARACTER_32): BOOLEAN
|
||||
-- gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@"
|
||||
do
|
||||
inspect c
|
||||
when ':' , '/', '?' , '#' , '[' , ']' , '@' then
|
||||
Result := True
|
||||
else
|
||||
end
|
||||
end
|
||||
|
||||
is_sub_delims_character (c: CHARACTER_32): BOOLEAN
|
||||
-- sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
|
||||
-- / "*" / "+" / "," / ";" / "="
|
||||
do
|
||||
inspect c
|
||||
when '!' , '$' , '&' , '%'' , '(' , ')' , '*' , '+' , ',' , ';' , '=' then -- sub-delims
|
||||
Result := True
|
||||
else
|
||||
end
|
||||
end
|
||||
|
||||
feature {NONE} -- Implementation
|
||||
|
||||
hex_digit: SPECIAL [NATURAL_32]
|
||||
-- Hexadecimal digits.
|
||||
once
|
||||
create Result.make_filled (0, 16)
|
||||
Result [0] := {NATURAL_32} 48 -- 48 '0'
|
||||
Result [1] := {NATURAL_32} 49 -- 49 '1'
|
||||
Result [2] := {NATURAL_32} 50 -- 50 '2'
|
||||
Result [3] := {NATURAL_32} 51 -- 51 '3'
|
||||
Result [4] := {NATURAL_32} 52 -- 52 '4'
|
||||
Result [5] := {NATURAL_32} 53 -- 53 '5'
|
||||
Result [6] := {NATURAL_32} 54 -- 54 '6'
|
||||
Result [7] := {NATURAL_32} 55 -- 55 '7'
|
||||
Result [8] := {NATURAL_32} 56 -- 56 '8'
|
||||
Result [9] := {NATURAL_32} 57 -- 57 '9'
|
||||
Result [10] := {NATURAL_32} 65 -- 65 'A'
|
||||
Result [11] := {NATURAL_32} 66 -- 66 'B'
|
||||
Result [12] := {NATURAL_32} 67 -- 67 'C'
|
||||
Result [13] := {NATURAL_32} 68 -- 68 'D'
|
||||
Result [14] := {NATURAL_32} 69 -- 69 'E'
|
||||
Result [15] := {NATURAL_32} 70 -- 70 'F'
|
||||
end
|
||||
|
||||
is_hexa_decimal (a_string: READABLE_STRING_GENERAL): BOOLEAN
|
||||
-- Is `a_string' a valid hexadecimal sequence?
|
||||
local
|
||||
l_convertor: like ctoi_convertor
|
||||
do
|
||||
l_convertor := ctoi_convertor
|
||||
l_convertor.parse_string_with_type (a_string, {NUMERIC_INFORMATION}.type_natural_32)
|
||||
Result := l_convertor.is_integral_integer
|
||||
end
|
||||
|
||||
hexadecimal_string_to_natural_32 (a_hex_string: READABLE_STRING_GENERAL): NATURAL_32
|
||||
-- Convert hexadecimal value `a_hex_string' to its corresponding NATURAL_32 value.
|
||||
require
|
||||
is_hexa: is_hexa_decimal (a_hex_string)
|
||||
local
|
||||
l_convertor: like ctoi_convertor
|
||||
do
|
||||
l_convertor := ctoi_convertor
|
||||
l_convertor.parse_string_with_type (a_hex_string, {NUMERIC_INFORMATION}.type_no_limitation)
|
||||
Result := l_convertor.parsed_natural_32
|
||||
end
|
||||
|
||||
ctoi_convertor: HEXADECIMAL_STRING_TO_INTEGER_CONVERTER
|
||||
-- Converter used to convert string to integer or natural.
|
||||
once
|
||||
create Result.make
|
||||
Result.set_leading_separators_acceptable (False)
|
||||
Result.set_trailing_separators_acceptable (False)
|
||||
ensure
|
||||
ctoi_convertor_not_void: Result /= Void
|
||||
end
|
||||
|
||||
note
|
||||
copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, 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
|
||||
@@ -82,9 +82,6 @@ feature -- Execution
|
||||
|
||||
--| URL
|
||||
l_url := url
|
||||
if ctx /= Void then
|
||||
append_parameters_to_url (ctx.query_parameters, l_url)
|
||||
end
|
||||
|
||||
if session.is_header_sent_verbose then
|
||||
io.error.put_string ("> Sending:%N")
|
||||
@@ -171,7 +168,7 @@ feature -- Execution
|
||||
then
|
||||
if l_ct.starts_with ("application/x-www-form-urlencoded") then
|
||||
-- Content-Type is already application/x-www-form-urlencoded
|
||||
l_upload_data := ctx.form_parameters_to_url_encoded_string
|
||||
l_upload_data := ctx.form_parameters_to_x_www_form_url_encoded_string
|
||||
elseif l_ct.starts_with ("multipart/form-data") then
|
||||
l_use_curl_form := True
|
||||
else
|
||||
@@ -179,7 +176,7 @@ feature -- Execution
|
||||
l_use_curl_form := True
|
||||
end
|
||||
else
|
||||
l_upload_data := ctx.form_parameters_to_url_encoded_string
|
||||
l_upload_data := ctx.form_parameters_to_x_www_form_url_encoded_string
|
||||
end
|
||||
else
|
||||
l_use_curl_form := True
|
||||
@@ -199,6 +196,14 @@ feature -- Execution
|
||||
)
|
||||
l_form_data.forth
|
||||
end
|
||||
if l_upload_filename /= Void then
|
||||
curl.formadd_string_string (l_form, l_last,
|
||||
{CURL_FORM_CONSTANTS}.curlform_copyname, "file",
|
||||
{CURL_FORM_CONSTANTS}.curlform_file, l_upload_filename,
|
||||
{CURL_FORM_CONSTANTS}.curlform_end
|
||||
)
|
||||
l_upload_filename := Void
|
||||
end
|
||||
l_last.release_item
|
||||
curl_easy.setopt_form (curl_handle, {CURL_OPT_CONSTANTS}.curlopt_httppost, l_form)
|
||||
end
|
||||
|
||||
@@ -41,7 +41,7 @@ feature -- Custom
|
||||
local
|
||||
req: HTTP_CLIENT_REQUEST
|
||||
do
|
||||
create {LIBCURL_HTTP_CLIENT_REQUEST} req.make (base_url + a_path, a_method, Current, ctx)
|
||||
create {LIBCURL_HTTP_CLIENT_REQUEST} req.make (url (a_path, ctx), a_method, Current, ctx)
|
||||
Result := req.response
|
||||
end
|
||||
|
||||
@@ -154,7 +154,7 @@ feature {NONE} -- Implementation
|
||||
end
|
||||
end
|
||||
|
||||
create {LIBCURL_HTTP_CLIENT_REQUEST} req.make (base_url + a_path, a_method, Current, ctx)
|
||||
create {LIBCURL_HTTP_CLIENT_REQUEST} req.make (url (a_path, ctx), a_method, Current, ctx)
|
||||
Result := req.response
|
||||
|
||||
if f /= Void then
|
||||
@@ -176,7 +176,7 @@ feature {LIBCURL_HTTP_CLIENT_REQUEST} -- Curl implementation
|
||||
|
||||
|
||||
;note
|
||||
copyright: "2011-2015, Jocelyn Fiat, Javier Velilla, Eiffel Software and others"
|
||||
copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others"
|
||||
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
|
||||
source: "[
|
||||
Eiffel Software
|
||||
|
||||
@@ -156,7 +156,7 @@ feature -- Access
|
||||
end
|
||||
|
||||
create l_request_uri.make_from_string (l_uri.path)
|
||||
if attached l_uri.query as l_query then
|
||||
if attached l_uri.query as l_query and then not l_query.is_empty then
|
||||
l_request_uri.append_character ('?')
|
||||
l_request_uri.append (l_query)
|
||||
end
|
||||
@@ -198,7 +198,7 @@ feature -- Access
|
||||
attached headers.item ("Content-Type") as l_ct
|
||||
then
|
||||
if l_ct.starts_with ("application/x-www-form-urlencoded") then
|
||||
l_upload_data := ctx.form_parameters_to_url_encoded_string
|
||||
l_upload_data := ctx.form_parameters_to_x_www_form_url_encoded_string
|
||||
elseif l_ct.starts_with ("multipart/form-data") then
|
||||
-- create form using multipart/form-data encoding
|
||||
l_boundary := new_mime_boundary (l_form_data)
|
||||
@@ -208,12 +208,12 @@ feature -- Access
|
||||
-- not supported !
|
||||
-- Send as form-urlencoded
|
||||
headers.extend ("application/x-www-form-urlencoded", "Content-Type")
|
||||
l_upload_data := ctx.form_parameters_to_url_encoded_string
|
||||
l_upload_data := ctx.form_parameters_to_x_www_form_url_encoded_string
|
||||
end
|
||||
else
|
||||
-- Send as form-urlencoded
|
||||
headers.extend ("application/x-www-form-urlencoded", "Content-Type")
|
||||
l_upload_data := ctx.form_parameters_to_url_encoded_string
|
||||
l_upload_data := ctx.form_parameters_to_x_www_form_url_encoded_string
|
||||
end
|
||||
headers.extend (l_upload_data.count.out, "Content-Length")
|
||||
if l_is_chunked_transfer_encoding then
|
||||
@@ -246,7 +246,7 @@ feature -- Access
|
||||
elseif l_upload_filename /= Void then
|
||||
check ctx.has_upload_filename end
|
||||
create l_upload_file.make_with_name (l_upload_filename)
|
||||
if l_upload_file.exists and then l_upload_file.readable then
|
||||
if l_upload_file.exists and then l_upload_file.is_access_readable then
|
||||
if not l_is_chunked_transfer_encoding then
|
||||
headers.extend (l_upload_file.count.out, "Content-Length")
|
||||
end
|
||||
@@ -495,6 +495,7 @@ feature {NONE} -- Helpers
|
||||
across
|
||||
a_form_parameters as ic
|
||||
loop
|
||||
Result.append ("--")
|
||||
Result.append (a_mime_boundary)
|
||||
Result.append (http_end_of_header_line)
|
||||
Result.append ("Content-Disposition: form-data; name=")
|
||||
@@ -519,7 +520,7 @@ feature {NONE} -- Helpers
|
||||
else
|
||||
l_mime_type := "application/octet-stream"
|
||||
end
|
||||
|
||||
Result.append ("--")
|
||||
Result.append (a_mime_boundary)
|
||||
Result.append (http_end_of_header_line)
|
||||
Result.append ("Content-Disposition: form-data; name=%"")
|
||||
@@ -542,6 +543,7 @@ feature {NONE} -- Helpers
|
||||
end
|
||||
Result.append (http_end_of_header_line)
|
||||
end
|
||||
Result.append ("--")
|
||||
Result.append (a_mime_boundary)
|
||||
Result.append ("--") --| end
|
||||
end
|
||||
@@ -907,7 +909,7 @@ feature {NONE} -- Helpers
|
||||
create ran.set_seed (i) -- FIXME: use a real random seed.
|
||||
ran.start
|
||||
ran.forth
|
||||
n := (20 * ran.real_item).truncated_to_integer
|
||||
n := (10 * ran.real_item).truncated_to_integer
|
||||
create Result.make_filled ('-', 3 + n)
|
||||
s := "_1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
from
|
||||
|
||||
@@ -67,7 +67,7 @@ feature -- Custom
|
||||
local
|
||||
req: HTTP_CLIENT_REQUEST
|
||||
do
|
||||
create {NET_HTTP_CLIENT_REQUEST} req.make (base_url + a_path, a_method, Current, ctx)
|
||||
create {NET_HTTP_CLIENT_REQUEST} req.make (url (a_path, ctx), a_method, Current, ctx)
|
||||
Result := req.response
|
||||
end
|
||||
|
||||
@@ -167,12 +167,12 @@ feature {NONE} -- Implementation
|
||||
end
|
||||
end
|
||||
|
||||
create {NET_HTTP_CLIENT_REQUEST} req.make (base_url + a_path, a_method, Current, ctx)
|
||||
create {NET_HTTP_CLIENT_REQUEST} req.make (url (a_path, ctx), a_method, Current, ctx)
|
||||
Result := req.response
|
||||
end
|
||||
|
||||
note
|
||||
copyright: "2011-2016, Jocelyn Fiat, Javier Velilla, Eiffel Software and others"
|
||||
copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others"
|
||||
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
|
||||
source: "[
|
||||
Eiffel Software
|
||||
|
||||
@@ -25,7 +25,7 @@ feature -- Custom
|
||||
|
||||
custom (a_method: READABLE_STRING_8; a_path: READABLE_STRING_8; ctx: detachable HTTP_CLIENT_REQUEST_CONTEXT): HTTP_CLIENT_RESPONSE
|
||||
do
|
||||
create Result.make (base_url + a_path)
|
||||
create Result.make (url (a_path, ctx))
|
||||
end
|
||||
|
||||
custom_with_upload_data (a_method: READABLE_STRING_8; a_path: READABLE_STRING_8; a_ctx: detachable HTTP_CLIENT_REQUEST_CONTEXT; data: READABLE_STRING_8): HTTP_CLIENT_RESPONSE
|
||||
@@ -98,7 +98,7 @@ feature -- Status report
|
||||
-- Is interface usable?
|
||||
|
||||
note
|
||||
copyright: "2011-2015, Jocelyn Fiat, Javier Velilla, Eiffel Software and others"
|
||||
copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others"
|
||||
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
|
||||
source: "[
|
||||
Eiffel Software
|
||||
|
||||
@@ -34,6 +34,11 @@ feature -- Tests
|
||||
test_post_with_form_data
|
||||
end
|
||||
|
||||
libcurl_test_post_with_uncommon_form_data
|
||||
do
|
||||
test_post_with_uncommon_form_data
|
||||
end
|
||||
|
||||
libcurl_test_post_with_file
|
||||
do
|
||||
test_post_with_file
|
||||
|
||||
@@ -34,6 +34,11 @@ feature -- Tests
|
||||
test_post_with_form_data
|
||||
end
|
||||
|
||||
net_test_post_with_uncommon_form_data
|
||||
do
|
||||
test_post_with_uncommon_form_data
|
||||
end
|
||||
|
||||
net_test_post_with_file
|
||||
do
|
||||
test_post_with_file
|
||||
@@ -69,4 +74,9 @@ feature -- Tests
|
||||
test_post_with_file_using_chunked_transfer_encoding
|
||||
end
|
||||
|
||||
net_test_get_with_query_parameters
|
||||
do
|
||||
test_get_with_query_parameters
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
note
|
||||
note
|
||||
description: "[
|
||||
Eiffel tests that can be executed by testing tool.
|
||||
]"
|
||||
@@ -21,7 +21,10 @@ feature -- Initialization
|
||||
on_prepare
|
||||
do
|
||||
Precursor
|
||||
global_requestbin_path := new_requestbin_path
|
||||
global_requestbin_path := "/s0jkhhs0"
|
||||
if global_requestbin_path = Void then
|
||||
global_requestbin_path := new_requestbin_path
|
||||
end
|
||||
end
|
||||
|
||||
feature -- Factory
|
||||
@@ -49,6 +52,9 @@ feature -- Requestbin
|
||||
i := l_content.substring_index ("%"name%":", 1)
|
||||
if i > 0 then
|
||||
j := l_content.index_of (',', i + 1)
|
||||
if j = 0 then
|
||||
j := l_content.index_of ('}', i + 1)
|
||||
end
|
||||
if j > 0 then
|
||||
Result := l_content.substring (i + 7, j - 1)
|
||||
Result.adjust
|
||||
@@ -61,6 +67,7 @@ feature -- Requestbin
|
||||
if not Result.starts_with ("/") then
|
||||
Result.prepend_character ('/')
|
||||
end
|
||||
print ("new_requestbin_path => http://requestb.in" + Result + "?inspect%N")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -108,8 +115,55 @@ feature -- Factory
|
||||
-- check requestbin to ensure the form parameters are correctly received
|
||||
sess := new_session ("http://requestb.in")
|
||||
create l_ctx.make
|
||||
l_ctx.form_parameters.extend ("First Value", "First Key")
|
||||
l_ctx.form_parameters.extend ("Second Value", "Second Key")
|
||||
l_ctx.add_form_parameter ("First Key", "First Value")
|
||||
l_ctx.add_form_parameter ("Second Key", "Second Value")
|
||||
l_ctx.add_form_parameter ("unicode", {STRING_32} "Hello / 你好 !")
|
||||
l_ctx.add_form_parameter ({STRING_32} "Field 你好 !", "How are you?")
|
||||
create h.make_empty
|
||||
if
|
||||
attached sess.post (requestbin_path, l_ctx, "") as res and then
|
||||
attached res.headers as hds
|
||||
then
|
||||
across
|
||||
hds as c
|
||||
loop
|
||||
h.append (c.item.name + ": " + c.item.value + "%R%N")
|
||||
end
|
||||
end
|
||||
print (h)
|
||||
else
|
||||
assert ("Has requestbin path", False)
|
||||
end
|
||||
end
|
||||
|
||||
test_post_with_uncommon_form_data
|
||||
local
|
||||
sess: HTTP_CLIENT_SESSION
|
||||
h: STRING_8
|
||||
l_ctx: HTTP_CLIENT_REQUEST_CONTEXT
|
||||
do
|
||||
if attached global_requestbin_path as requestbin_path then
|
||||
|
||||
-- POST REQUEST WITH FORM DATA
|
||||
-- check requestbin to ensure the form parameters are correctly received
|
||||
sess := new_session ("http://requestb.in")
|
||||
create l_ctx.make
|
||||
|
||||
l_ctx.add_form_parameter ("title", "Eiffel World!") -- space and !
|
||||
l_ctx.add_form_parameter ("path", "foo/bar") -- slash
|
||||
l_ctx.add_form_parameter ("unreserved", ":!@[]{}()*") -- ...
|
||||
l_ctx.add_form_parameter ("reserved", "+=?&_#_") -- ...
|
||||
l_ctx.add_form_parameter ("a=b", "a=b") -- equal sign
|
||||
l_ctx.add_form_parameter ("test", "!$&'()*") --
|
||||
l_ctx.add_form_parameter ("lst[a][b]", "[123][456]") -- brackets
|
||||
l_ctx.add_form_parameter ("pos{1,2}", "loc{a,b}") -- curly brackets
|
||||
l_ctx.add_form_parameter ("?foo", "?bar") -- question mark
|
||||
l_ctx.add_form_parameter ("?", "?") -- question mark
|
||||
l_ctx.add_form_parameter ("&bar", "&bar") -- ampersand
|
||||
l_ctx.add_form_parameter ("&", "&") -- ampersand
|
||||
|
||||
assert ("form data well generated", l_ctx.form_parameters_to_x_www_form_url_encoded_string.same_string ("title=Eiffel+World!&path=foo%%2Fbar&unreserved=%%3A!%%40%%5B%%5D%%7B%%7D()*&reserved=%%2B%%3D%%3F%%26_%%23_&a%%3Db=a%%3Db&test=!%%24%%26'()*&lst%%5Ba%%5D%%5Bb%%5D=%%5B123%%5D%%5B456%%5D&pos%%7B1%%2C2%%7D=loc%%7Ba%%2Cb%%7D&%%3Ffoo=%%3Fbar&%%3F=%%3F&%%26bar=%%26bar&%%26=%%26"))
|
||||
|
||||
create h.make_empty
|
||||
if
|
||||
attached sess.post (requestbin_path, l_ctx, "") as res and then
|
||||
@@ -143,7 +197,7 @@ feature -- Factory
|
||||
l_ctx.set_upload_filename ("test.txt")
|
||||
create h.make_empty
|
||||
if
|
||||
attached sess.post (requestbin_path, l_ctx, "") as res and then
|
||||
attached sess.post (requestbin_path, l_ctx, Void) as res and then
|
||||
attached res.headers as hds
|
||||
then
|
||||
across
|
||||
@@ -202,7 +256,7 @@ feature -- Factory
|
||||
-- set filename to a local file
|
||||
sess := new_session ("http://requestb.in")
|
||||
create l_ctx.make
|
||||
l_ctx.set_upload_data ("This is a test for http client.%N")
|
||||
l_ctx.set_upload_data ("name=This is a test for http client.%N")
|
||||
create h.make_empty
|
||||
if
|
||||
attached sess.put (requestbin_path, l_ctx, Void) as res and then
|
||||
@@ -232,10 +286,12 @@ feature -- Factory
|
||||
-- check requestbin to ensure the file and form parameters are correctly received
|
||||
-- set filename to a local file
|
||||
sess := new_session ("http://requestb.in")
|
||||
-- sess := new_session ("http://localhost:9090")
|
||||
create l_ctx.make
|
||||
l_ctx.set_upload_filename ("logo.jpg")
|
||||
l_ctx.form_parameters.extend ("First Value", "First Key")
|
||||
l_ctx.form_parameters.extend ("Second Value", "Second Key")
|
||||
-- l_ctx.set_upload_filename ("logo.jpg")
|
||||
l_ctx.set_upload_filename ("test.txt")
|
||||
l_ctx.add_form_parameter ("First", "Value")
|
||||
l_ctx.add_form_parameter ("Second", "and last value")
|
||||
create h.make_empty
|
||||
if
|
||||
attached sess.post (requestbin_path, l_ctx, Void) as res and then
|
||||
@@ -297,6 +353,7 @@ feature -- Factory
|
||||
sess := new_session ("http://google.com")
|
||||
create h.make_empty
|
||||
if attached sess.get ("/", Void) as res and then attached res.headers as hds then
|
||||
assert("was redirected", res.redirections_count > 0)
|
||||
across
|
||||
hds as c
|
||||
loop
|
||||
@@ -328,6 +385,53 @@ feature -- Factory
|
||||
end
|
||||
end
|
||||
|
||||
test_get_with_query_parameters
|
||||
local
|
||||
sess: HTTP_CLIENT_SESSION
|
||||
h: STRING_8
|
||||
l_ctx: HTTP_CLIENT_REQUEST_CONTEXT
|
||||
q: STRING
|
||||
do
|
||||
if attached global_requestbin_path as requestbin_path then
|
||||
|
||||
-- GET REQUEST WITH A FILE AND FORM DATA
|
||||
-- check requestbin to ensure the file and form parameters are correctly received
|
||||
-- set filename to a local file
|
||||
sess := new_session ("http://requestb.in")
|
||||
create l_ctx.make
|
||||
l_ctx.add_query_parameter ("?", "?first&arg")
|
||||
l_ctx.add_query_parameter ("title", "Eiffel World!")
|
||||
l_ctx.add_query_parameter ("path", "foo/bar")
|
||||
l_ctx.add_query_parameter ("reserved", "+=&?")
|
||||
l_ctx.add_query_parameter ("unreserved", ":!@'()*")
|
||||
l_ctx.add_query_parameter ("unsafe", "%"[]{}")
|
||||
l_ctx.add_query_parameter ("test", "!$&'()*")
|
||||
l_ctx.add_query_parameter ("a&b", "a&b")
|
||||
l_ctx.add_query_parameter ("lst[a][b]", "[abc][123]")
|
||||
l_ctx.add_query_parameter ("foo(a,b)", "bar(1,2)*pi")
|
||||
create q.make_empty
|
||||
l_ctx.append_query_parameters_to_url (q)
|
||||
assert("query", q.same_string ("??=?first%%26arg&title=Eiffel+World!&path=foo/bar&reserved=%%2B=%%26?&unreserved=:!@'()*&unsafe=%%22%%5B%%5D%%7B%%7D&test=!$%%26'()*&a%%26b=a%%26b&lst%%5Ba%%5D%%5Bb%%5D=%%5Babc%%5D%%5B123%%5D&foo(a,b)=bar(1,2)*pi"))
|
||||
|
||||
|
||||
create h.make_empty
|
||||
if
|
||||
attached sess.get (requestbin_path, l_ctx) as res and then
|
||||
attached res.headers as hds
|
||||
then
|
||||
across
|
||||
hds as c
|
||||
loop
|
||||
h.append (c.item.name + ": " + c.item.value + "%R%N")
|
||||
end
|
||||
end
|
||||
print (h)
|
||||
else
|
||||
assert ("Has requestbin path", False)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
end
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
<file_rule>
|
||||
<exclude>/http_stream_socket_ext.e$</exclude>
|
||||
<condition>
|
||||
<version type="compiler" max="16.9.9.9124"/>
|
||||
<version type="compiler" max="17.02"/>
|
||||
</condition>
|
||||
</file_rule>
|
||||
<cluster name="disabled_ssl_network" location="$|no_ssl\" recursive="true">
|
||||
@@ -114,6 +114,11 @@
|
||||
</condition>
|
||||
</cluster>
|
||||
</cluster>
|
||||
<cluster name="network_until_17_01" location=".\src\until_17_01\">
|
||||
<condition>
|
||||
<version type="compiler" min="16.9.9.9124" max="17.02"/>
|
||||
</condition>
|
||||
</cluster>
|
||||
</target>
|
||||
<target name="http_network_ssl" extends="http_network">
|
||||
<variable name="ssl_enabled" value="true"/>
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
<file_rule>
|
||||
<exclude>/http_stream_socket_ext.e$</exclude>
|
||||
<condition>
|
||||
<version type="compiler" max="16.9.9.9124"/>
|
||||
<version type="compiler" max="17.02"/>
|
||||
</condition>
|
||||
</file_rule>
|
||||
<cluster name="disabled_ssl_network" location="$|no_ssl\" recursive="true">
|
||||
@@ -114,6 +114,11 @@
|
||||
</condition>
|
||||
</cluster>
|
||||
</cluster>
|
||||
<cluster name="network_until_17_01" location=".\src\until_17_01\">
|
||||
<condition>
|
||||
<version type="compiler" min="16.9.9.9124" max="17.02"/>
|
||||
</condition>
|
||||
</cluster>
|
||||
</target>
|
||||
<target name="http_network_ssl" extends="http_network">
|
||||
<variable name="ssl_enabled" value="true"/>
|
||||
|
||||
@@ -36,13 +36,13 @@ feature -- Input
|
||||
-- Make result available in `last_character'.
|
||||
-- No exception raised!
|
||||
do
|
||||
read_to_managed_pointer_noexception (socket_buffer, 0, character_8_bytes)
|
||||
read_to_managed_pointer_noexception (read_socket_buffer, 0, character_8_bytes)
|
||||
if was_error then
|
||||
-- Socket error already set.
|
||||
elseif bytes_read /= character_8_bytes then
|
||||
socket_error := "Peer closed connection"
|
||||
else
|
||||
last_character := socket_buffer.read_character (0)
|
||||
last_character := read_socket_buffer.read_character (0)
|
||||
socket_error := Void
|
||||
end
|
||||
end
|
||||
@@ -208,8 +208,8 @@ feature -- Output
|
||||
put_character_noexception (c: CHARACTER)
|
||||
-- Write character `c' to socket.
|
||||
do
|
||||
socket_buffer.put_character (c, 0)
|
||||
put_managed_pointer_noexception (socket_buffer, 0, character_8_bytes)
|
||||
put_socket_buffer.put_character (c, 0)
|
||||
put_managed_pointer_noexception (put_socket_buffer, 0, character_8_bytes)
|
||||
end
|
||||
|
||||
put_string_8_noexception (s: READABLE_STRING_8)
|
||||
@@ -244,7 +244,7 @@ feature -- Status report
|
||||
end
|
||||
|
||||
note
|
||||
copyright: "2011-2016, Jocelyn Fiat, Javier Velilla, Olivier Ligot, Colin Adams, Eiffel Software and others"
|
||||
copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Olivier Ligot, Colin Adams, Eiffel Software and others"
|
||||
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
|
||||
source: "[
|
||||
Eiffel Software
|
||||
|
||||
@@ -2,12 +2,20 @@ note
|
||||
description: "[
|
||||
Extension to HTTPD_STREAM_SOCKET to support backward compatibility.
|
||||
|
||||
TO BE REMOVED IN THE FUTURE, WHEN 16.05 IS OLD.
|
||||
TO BE REMOVED IN THE FUTURE, When there is no need to support older compilers.
|
||||
]"
|
||||
|
||||
deferred class
|
||||
HTTP_STREAM_SOCKET_EXT
|
||||
|
||||
feature {NONE} -- No-Exception network operation
|
||||
|
||||
note
|
||||
copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Olivier Ligot, Colin Adams, 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
|
||||
|
||||
@@ -51,6 +51,20 @@ feature -- Access
|
||||
deferred
|
||||
end
|
||||
|
||||
socket_buffer: MANAGED_POINTER
|
||||
deferred
|
||||
end
|
||||
|
||||
read_socket_buffer: MANAGED_POINTER
|
||||
do
|
||||
Result := socket_buffer
|
||||
end
|
||||
|
||||
put_socket_buffer: MANAGED_POINTER
|
||||
do
|
||||
Result := socket_buffer
|
||||
end
|
||||
|
||||
feature -- Socket Recv and Send timeout.
|
||||
|
||||
set_recv_timeout (a_timeout_seconds: INTEGER)
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
note
|
||||
description: "[
|
||||
Extension to HTTPD_STREAM_SOCKET to support backward compatibility.
|
||||
|
||||
TO BE REMOVED IN THE FUTURE, WHEN 17.01 IS OLD.
|
||||
]"
|
||||
|
||||
deferred class
|
||||
HTTP_STREAM_SOCKET_EXT
|
||||
|
||||
feature -- Access
|
||||
|
||||
socket_buffer: MANAGED_POINTER
|
||||
deferred
|
||||
end
|
||||
|
||||
read_socket_buffer: MANAGED_POINTER
|
||||
do
|
||||
Result := socket_buffer
|
||||
end
|
||||
|
||||
put_socket_buffer: MANAGED_POINTER
|
||||
do
|
||||
Result := socket_buffer
|
||||
end
|
||||
|
||||
feature {NONE} -- No-Exception network operation
|
||||
|
||||
end
|
||||
26
library/security/jwt/README.md
Normal file
26
library/security/jwt/README.md
Normal file
@@ -0,0 +1,26 @@
|
||||
JSON Web Token (JWT)
|
||||
|
||||
http://jwt.io/
|
||||
|
||||
Note: supporting only HS256 and none algorithm for signature.
|
||||
|
||||
# How to use
|
||||
```eiffel
|
||||
local
|
||||
jwt: JWT
|
||||
do
|
||||
create jwt
|
||||
tok := jwt.encoded_string ("[
|
||||
{"iss":"joe", "exp":1200819380,"http://example.com/is_root":true}
|
||||
]", "secret", "HS256")
|
||||
if
|
||||
attached jwt.decoded_string (tok, "secret", Void) as l_tok_payload and
|
||||
not jwt.has_error
|
||||
then
|
||||
check verified: not jwt.has_unverified_token_error end
|
||||
check no_error: not jwt.has_error end
|
||||
print (l_tok_payload)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
15
library/security/jwt/jwt-safe.ecf
Normal file
15
library/security/jwt/jwt-safe.ecf
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="ISO-8859-1"?>
|
||||
<system xmlns="http://www.eiffel.com/developers/xml/configuration-1-15-0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.eiffel.com/developers/xml/configuration-1-15-0 http://www.eiffel.com/developers/xml/configuration-1-15-0.xsd" name="jwt" uuid="A75C2D84-D543-4708-BAF3-254C308376CC" library_target="jwt">
|
||||
<target name="jwt">
|
||||
<root all_classes="true"/>
|
||||
<option warning="true" void_safety="all">
|
||||
</option>
|
||||
<setting name="concurrency" value="scoop"/>
|
||||
<library name="base" location="$ISE_LIBRARY\library\base\base-safe.ecf"/>
|
||||
<library name="crypto" location="$ISE_LIBRARY\unstable\library\text\encryption\crypto\crypto-safe.ecf"/>
|
||||
<library name="encoder" location="$ISE_LIBRARY\contrib\library\web\framework\ewf\text\encoder\encoder-safe.ecf"/>
|
||||
<library name="json" location="$ISE_LIBRARY\contrib\library\text\parser\json\library\json-safe.ecf"/>
|
||||
<library name="time" location="$ISE_LIBRARY\library\time\time-safe.ecf"/>
|
||||
<cluster name="src" location="src\" recursive="true"/>
|
||||
</target>
|
||||
</system>
|
||||
15
library/security/jwt/jwt.ecf
Normal file
15
library/security/jwt/jwt.ecf
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="ISO-8859-1"?>
|
||||
<system xmlns="http://www.eiffel.com/developers/xml/configuration-1-15-0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.eiffel.com/developers/xml/configuration-1-15-0 http://www.eiffel.com/developers/xml/configuration-1-15-0.xsd" name="jwt" uuid="A75C2D84-D543-4708-BAF3-254C308376CC" library_target="jwt">
|
||||
<target name="jwt">
|
||||
<root all_classes="true"/>
|
||||
<option warning="true" void_safety="none">
|
||||
</option>
|
||||
<setting name="concurrency" value="scoop"/>
|
||||
<library name="base" location="$ISE_LIBRARY\library\base\base.ecf"/>
|
||||
<library name="crypto" location="$ISE_LIBRARY\unstable\library\text\encryption\crypto\crypto.ecf"/>
|
||||
<library name="encoder" location="$ISE_LIBRARY\contrib\library\web\framework\ewf\text\encoder\encoder.ecf"/>
|
||||
<library name="json" location="$ISE_LIBRARY\contrib\library\text\parser\json\library\json.ecf"/>
|
||||
<library name="time" location="$ISE_LIBRARY\library\time\time.ecf"/>
|
||||
<cluster name="src" location="src\" recursive="true"/>
|
||||
</target>
|
||||
</system>
|
||||
16
library/security/jwt/package.iron
Normal file
16
library/security/jwt/package.iron
Normal file
@@ -0,0 +1,16 @@
|
||||
package jwt
|
||||
|
||||
project
|
||||
jwt = "jwt.ecf"
|
||||
|
||||
note
|
||||
title: JSON Web Token
|
||||
description: JSON Web Token
|
||||
tags:jwt,web,jws,jwe,token,jose
|
||||
copyright: 2011-2017, Jocelyn Fiat, Eiffel Software and others
|
||||
license: Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)
|
||||
link[license]: http://www.eiffel.com/licensing/forum.txt
|
||||
link[source]: "github" https://github.com/EiffelWebFramework/EWF/tree/master/library/security/jwt
|
||||
link[doc]: "Documentation" https://github.com/EiffelWebFramework/EWF/tree/master/library/security/jwt/README.md
|
||||
|
||||
end
|
||||
33
library/security/jwt/src/errors/jwt_claim_validation_error.e
Normal file
33
library/security/jwt/src/errors/jwt_claim_validation_error.e
Normal file
@@ -0,0 +1,33 @@
|
||||
note
|
||||
description: "Summary description for {JWT_CLAIM_VALIDATION_ERROR}."
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
class
|
||||
JWT_CLAIM_VALIDATION_ERROR
|
||||
|
||||
inherit
|
||||
JWT_ERROR
|
||||
|
||||
create
|
||||
make
|
||||
|
||||
feature {NONE} -- Initialization
|
||||
|
||||
make (a_claim: READABLE_STRING_8)
|
||||
do
|
||||
claim_name := a_claim
|
||||
end
|
||||
|
||||
feature -- Access
|
||||
|
||||
claim_name: READABLE_STRING_8
|
||||
|
||||
id: STRING = "CLAIM"
|
||||
|
||||
message: READABLE_STRING_8
|
||||
do
|
||||
Result := "Claim [" + claim_name + "] not validated!"
|
||||
end
|
||||
|
||||
end
|
||||
29
library/security/jwt/src/errors/jwt_dev_error.e
Normal file
29
library/security/jwt/src/errors/jwt_dev_error.e
Normal file
@@ -0,0 +1,29 @@
|
||||
note
|
||||
description: "Summary description for {JWT_DEV_ERROR}."
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
class
|
||||
JWT_DEV_ERROR
|
||||
|
||||
inherit
|
||||
JWT_ERROR
|
||||
|
||||
create
|
||||
make
|
||||
|
||||
feature {NONE} -- Initialization
|
||||
|
||||
make (a_id: READABLE_STRING_8; msg: READABLE_STRING_8)
|
||||
do
|
||||
id := a_id
|
||||
message := msg
|
||||
end
|
||||
|
||||
feature -- Access
|
||||
|
||||
id: STRING
|
||||
|
||||
message: READABLE_STRING_8
|
||||
|
||||
end
|
||||
21
library/security/jwt/src/errors/jwt_invalid_token_error.e
Normal file
21
library/security/jwt/src/errors/jwt_invalid_token_error.e
Normal file
@@ -0,0 +1,21 @@
|
||||
note
|
||||
description: "Summary description for {JWT_INVALID_TOKEN_ERROR}."
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
class
|
||||
JWT_INVALID_TOKEN_ERROR
|
||||
|
||||
inherit
|
||||
JWT_ERROR
|
||||
|
||||
feature -- Access
|
||||
|
||||
id: STRING = "INVALID"
|
||||
|
||||
message: READABLE_STRING_8
|
||||
do
|
||||
Result := "Invalid token"
|
||||
end
|
||||
|
||||
end
|
||||
33
library/security/jwt/src/errors/jwt_unsupported_alg_error.e
Normal file
33
library/security/jwt/src/errors/jwt_unsupported_alg_error.e
Normal file
@@ -0,0 +1,33 @@
|
||||
note
|
||||
description: "Summary description for {JWT_UNSUPPORTED_ALG_ERROR}."
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
class
|
||||
JWT_UNSUPPORTED_ALG_ERROR
|
||||
|
||||
inherit
|
||||
JWT_ERROR
|
||||
|
||||
create
|
||||
make
|
||||
|
||||
feature {NONE} -- Initialization
|
||||
|
||||
make (a_alg: READABLE_STRING_8)
|
||||
do
|
||||
alg := a_alg
|
||||
end
|
||||
|
||||
feature -- Access
|
||||
|
||||
alg: READABLE_STRING_8
|
||||
|
||||
id: STRING = "ALG"
|
||||
|
||||
message: READABLE_STRING_8
|
||||
do
|
||||
Result := "Unsupported alg [" + alg + "]"
|
||||
end
|
||||
|
||||
end
|
||||
21
library/security/jwt/src/errors/jwt_unverified_token_error.e
Normal file
21
library/security/jwt/src/errors/jwt_unverified_token_error.e
Normal file
@@ -0,0 +1,21 @@
|
||||
note
|
||||
description: "Summary description for {JWT_UNVERIFIED_TOKEN_ERROR}."
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
class
|
||||
JWT_UNVERIFIED_TOKEN_ERROR
|
||||
|
||||
inherit
|
||||
JWT_ERROR
|
||||
|
||||
feature -- Access
|
||||
|
||||
id: STRING = "UNVERIFIED"
|
||||
|
||||
message: READABLE_STRING_8
|
||||
do
|
||||
Result := "Unverified token"
|
||||
end
|
||||
|
||||
end
|
||||
80
library/security/jwt/src/jws.e
Normal file
80
library/security/jwt/src/jws.e
Normal file
@@ -0,0 +1,80 @@
|
||||
note
|
||||
description: "Summary description for {JWS}."
|
||||
author: ""
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
class
|
||||
JWS
|
||||
|
||||
inherit
|
||||
JWT
|
||||
|
||||
JWT_UTILITIES
|
||||
undefine
|
||||
default_create
|
||||
end
|
||||
|
||||
create
|
||||
default_create,
|
||||
make_with_claims,
|
||||
make_with_json_payload
|
||||
|
||||
feature {NONE} -- Initialization
|
||||
|
||||
make_with_claims (tb: STRING_TABLE [READABLE_STRING_GENERAL])
|
||||
do
|
||||
default_create
|
||||
across
|
||||
tb as ic
|
||||
loop
|
||||
claimset.set_claim (ic.key, ic.item)
|
||||
end
|
||||
end
|
||||
|
||||
make_with_json_payload (a_json: READABLE_STRING_8)
|
||||
do
|
||||
default_create
|
||||
claimset.import_json (a_json)
|
||||
end
|
||||
|
||||
feature -- Access
|
||||
|
||||
algorithm: READABLE_STRING_8
|
||||
do
|
||||
Result := header.algorithm
|
||||
end
|
||||
|
||||
feature -- Conversion
|
||||
|
||||
encoded_string (a_secret: READABLE_STRING_8): STRING
|
||||
local
|
||||
alg, sign: READABLE_STRING_8
|
||||
l_enc_payload, l_enc_header: READABLE_STRING_8
|
||||
do
|
||||
reset_error
|
||||
alg := header.algorithm
|
||||
if not is_supporting_signature_algorithm (alg) then
|
||||
report_unsupported_alg_error (alg)
|
||||
alg := alg_hs256 -- Default ...
|
||||
end
|
||||
l_enc_header := base64url_encode (header.string)
|
||||
l_enc_payload := base64url_encode (claimset.string)
|
||||
sign := signature (l_enc_header, l_enc_payload, a_secret, alg)
|
||||
|
||||
create Result.make (l_enc_header.count + 1 + l_enc_payload.count + 1 + sign.count)
|
||||
Result.append (l_enc_header)
|
||||
Result.append_character ('.')
|
||||
Result.append (l_enc_payload)
|
||||
Result.append_character ('.')
|
||||
Result.append (sign)
|
||||
end
|
||||
|
||||
feature -- Element change
|
||||
|
||||
set_algorithm (alg: detachable READABLE_STRING_8)
|
||||
do
|
||||
header.set_algorithm (alg)
|
||||
end
|
||||
|
||||
end
|
||||
143
library/security/jwt/src/jwt.e
Normal file
143
library/security/jwt/src/jwt.e
Normal file
@@ -0,0 +1,143 @@
|
||||
note
|
||||
description: "JSON Web Token"
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
deferred class
|
||||
JWT
|
||||
|
||||
inherit
|
||||
ANY
|
||||
redefine
|
||||
default_create
|
||||
end
|
||||
|
||||
feature {NONE} -- Initialization
|
||||
|
||||
default_create
|
||||
do
|
||||
create header
|
||||
create claimset
|
||||
end
|
||||
|
||||
feature -- Access
|
||||
|
||||
header: JWT_HEADER
|
||||
|
||||
claimset: JWT_CLAIMSET
|
||||
|
||||
feature -- Status report
|
||||
|
||||
is_expired (dt: detachable DATE_TIME): BOOLEAN
|
||||
-- Is Current token expired?
|
||||
-- See "exp" claim.
|
||||
do
|
||||
if attached claimset.expiration_time as l_exp_time then
|
||||
if dt /= Void then
|
||||
Result := dt > l_exp_time
|
||||
else
|
||||
Result := (create {DATE_TIME}.make_now_utc) > l_exp_time
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
is_nbf_validated (dt: detachable DATE_TIME): BOOLEAN
|
||||
-- Does `dt` or now verify the "nbf" claim?
|
||||
-- See "nbf" claim.
|
||||
do
|
||||
Result := True
|
||||
if attached claimset.not_before_time as l_time then
|
||||
if dt /= Void then
|
||||
Result := dt >= l_time
|
||||
else
|
||||
Result := (create {DATE_TIME}.make_now_utc) >= l_time
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
is_iss_validated (a_issuer: detachable READABLE_STRING_8): BOOLEAN
|
||||
do
|
||||
if attached claimset.issuer as iss then
|
||||
Result := a_issuer = Void or else a_issuer.same_string (iss)
|
||||
end
|
||||
end
|
||||
|
||||
is_aud_validated (a_audience: detachable READABLE_STRING_8): BOOLEAN
|
||||
do
|
||||
if attached claimset.audience as aud then
|
||||
Result := a_audience = Void or else a_audience.same_string (aud)
|
||||
end
|
||||
end
|
||||
|
||||
feature -- Conversion
|
||||
|
||||
encoded_string (a_secret: READABLE_STRING_8): STRING
|
||||
deferred
|
||||
end
|
||||
|
||||
feature -- status report
|
||||
|
||||
has_error: BOOLEAN
|
||||
do
|
||||
Result := attached errors as errs and then not errs.is_empty
|
||||
end
|
||||
|
||||
has_unsupported_alg_error: BOOLEAN
|
||||
do
|
||||
Result := attached errors as errs and then across errs as ic some attached {JWT_UNSUPPORTED_ALG_ERROR} ic.item end
|
||||
end
|
||||
|
||||
has_unverified_token_error: BOOLEAN
|
||||
do
|
||||
Result := attached errors as errs and then across errs as ic some attached {JWT_UNVERIFIED_TOKEN_ERROR} ic.item end
|
||||
end
|
||||
|
||||
has_invalid_token_error: BOOLEAN
|
||||
do
|
||||
Result := attached errors as errs and then across errs as ic some attached {JWT_INVALID_TOKEN_ERROR} ic.item end
|
||||
end
|
||||
|
||||
errors: detachable ARRAYED_LIST [JWT_ERROR]
|
||||
|
||||
feature {JWT_UTILITIES} -- Error reporting
|
||||
|
||||
reset_error
|
||||
do
|
||||
errors := Void
|
||||
end
|
||||
|
||||
report_error (err: JWT_ERROR)
|
||||
local
|
||||
l_errors: like errors
|
||||
do
|
||||
l_errors := errors
|
||||
if l_errors = Void then
|
||||
create l_errors.make (1)
|
||||
errors := l_errors
|
||||
end
|
||||
l_errors.extend (err)
|
||||
end
|
||||
|
||||
report_unsupported_alg_error (alg: READABLE_STRING_8)
|
||||
do
|
||||
report_error (create {JWT_UNSUPPORTED_ALG_ERROR}.make (alg))
|
||||
end
|
||||
|
||||
report_unverified_token_error
|
||||
do
|
||||
report_error (create {JWT_UNVERIFIED_TOKEN_ERROR})
|
||||
end
|
||||
|
||||
report_invalid_token
|
||||
do
|
||||
report_error (create {JWT_INVALID_TOKEN_ERROR})
|
||||
end
|
||||
|
||||
report_claim_validation_error (a_claimname: READABLE_STRING_8)
|
||||
do
|
||||
report_error (create {JWT_CLAIM_VALIDATION_ERROR}.make (a_claimname))
|
||||
end
|
||||
|
||||
invariant
|
||||
|
||||
end
|
||||
287
library/security/jwt/src/jwt_claimset.e
Normal file
287
library/security/jwt/src/jwt_claimset.e
Normal file
@@ -0,0 +1,287 @@
|
||||
note
|
||||
description: "Summary description for {JWT_CLAIMSET}."
|
||||
author: ""
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
class
|
||||
JWT_CLAIMSET
|
||||
|
||||
inherit
|
||||
ANY
|
||||
redefine
|
||||
default_create
|
||||
end
|
||||
|
||||
create
|
||||
default_create
|
||||
|
||||
convert
|
||||
string: {READABLE_STRING_8, STRING_8}
|
||||
|
||||
feature {NONE} -- Initialization
|
||||
|
||||
default_create
|
||||
do
|
||||
create json.make_empty
|
||||
end
|
||||
|
||||
feature -- Element change
|
||||
|
||||
import_json (j: READABLE_STRING_8)
|
||||
local
|
||||
jp: JSON_PARSER
|
||||
do
|
||||
create jp.make_with_string (j)
|
||||
jp.parse_content
|
||||
if jp.is_valid and then attached jp.parsed_json_object as jo then
|
||||
across
|
||||
jo as ic
|
||||
loop
|
||||
json.put (ic.item, ic.key)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
feature -- Access
|
||||
|
||||
claim alias "[]" (a_name: READABLE_STRING_GENERAL): detachable ANY
|
||||
do
|
||||
if attached json.item (a_name) as jv then
|
||||
if attached {JSON_STRING} jv as js then
|
||||
Result := js.unescaped_string_32
|
||||
elseif attached {JSON_BOOLEAN} jv as jb then
|
||||
Result := jb.item
|
||||
elseif attached {JSON_NUMBER} jv as jnum then
|
||||
if jnum.is_integer then
|
||||
Result := jnum.integer_64_item
|
||||
elseif jnum.is_natural then
|
||||
Result := jnum.natural_64_item
|
||||
elseif jnum.is_real then
|
||||
Result := jnum.real_64_item
|
||||
else
|
||||
Result := jnum.item
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
string_32_claim (a_name: READABLE_STRING_GENERAL): detachable READABLE_STRING_32
|
||||
do
|
||||
if attached json.item (a_name) as jv then
|
||||
if attached {JSON_STRING} jv as js then
|
||||
Result := js.unescaped_string_32
|
||||
elseif attached {JSON_BOOLEAN} jv as jb then
|
||||
Result := jb.item.out
|
||||
elseif attached {JSON_NUMBER} jv as jnum then
|
||||
Result := jnum.item
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
string_8_claim (a_name: READABLE_STRING_GENERAL): detachable READABLE_STRING_8
|
||||
do
|
||||
if attached json.item (a_name) as jv then
|
||||
if attached {JSON_STRING} jv as js then
|
||||
Result := js.unescaped_string_8
|
||||
elseif attached {JSON_BOOLEAN} jv as jb then
|
||||
Result := jb.item.out
|
||||
elseif attached {JSON_NUMBER} jv as jnum then
|
||||
Result := jnum.item
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
issuer: detachable READABLE_STRING_8 assign set_issuer
|
||||
do
|
||||
Result := string_8_claim ("iss")
|
||||
end
|
||||
|
||||
subjet: detachable READABLE_STRING_32 assign set_subject
|
||||
do
|
||||
Result := string_32_claim ("sub")
|
||||
end
|
||||
|
||||
audience: detachable READABLE_STRING_8 assign set_audience
|
||||
do
|
||||
Result := string_8_claim ("aud")
|
||||
end
|
||||
|
||||
expiration_time: detachable DATE_TIME assign set_expiration_time
|
||||
do
|
||||
if attached {INTEGER_64} claim ("exp") as i64 then
|
||||
Result := numeric_date_value_to_datetime (i64)
|
||||
end
|
||||
end
|
||||
|
||||
not_before_time: detachable DATE_TIME assign set_not_before_time
|
||||
do
|
||||
if attached {INTEGER_64} claim ("nbf") as i64 then
|
||||
Result := numeric_date_value_to_datetime (i64)
|
||||
end
|
||||
end
|
||||
|
||||
issued_at: detachable DATE_TIME assign set_issued_at
|
||||
do
|
||||
if attached {INTEGER_64} claim ("iat") as i then
|
||||
Result := numeric_date_value_to_datetime (i)
|
||||
end
|
||||
end
|
||||
|
||||
jwd_id: detachable READABLE_STRING_8 assign set_jwt_id
|
||||
do
|
||||
Result := string_8_claim ("jti")
|
||||
end
|
||||
|
||||
feature -- Conversion
|
||||
|
||||
json: JSON_OBJECT
|
||||
|
||||
string: STRING
|
||||
do
|
||||
Result := json.representation
|
||||
end
|
||||
|
||||
feature -- Element change
|
||||
|
||||
set_claim (a_name: READABLE_STRING_GENERAL; a_val: detachable ANY)
|
||||
do
|
||||
if a_val = Void then
|
||||
json.remove (a_name)
|
||||
elseif attached {READABLE_STRING_GENERAL} a_val as str then
|
||||
json.put_string (str, a_name)
|
||||
elseif attached {BOOLEAN} a_val as b then
|
||||
json.put_boolean (b, a_name)
|
||||
elseif attached {DATE_TIME} a_val as dt then
|
||||
json.put_integer (datetime_to_numeric_date_value (dt), a_name)
|
||||
elseif attached {DATE} a_val as d then
|
||||
json.put_integer (datetime_to_numeric_date_value (create {DATE_TIME}.make_by_date (d)), a_name)
|
||||
elseif attached {NUMERIC} a_val as num then
|
||||
if attached {INTEGER_64} num as i64 then
|
||||
json.put_integer (i64, a_name)
|
||||
elseif attached {INTEGER_32} num as i32 then
|
||||
json.put_integer (i32.to_integer_64, a_name)
|
||||
elseif attached {NATURAL_64} num as n64 then
|
||||
json.put_natural (n64, a_name)
|
||||
elseif attached {INTEGER_32} num as n32 then
|
||||
json.put_natural (n32.to_natural_64, a_name)
|
||||
elseif attached {REAL_64} num as r64 then
|
||||
json.put_real (r64, a_name)
|
||||
elseif attached {REAL_32} num as r32 then
|
||||
json.put_real (r32, a_name)
|
||||
else
|
||||
json.put_string (a_val.out, a_name)
|
||||
end
|
||||
else
|
||||
json.put_string (a_val.out, a_name)
|
||||
end
|
||||
end
|
||||
|
||||
set_issuer (iss: detachable READABLE_STRING_8)
|
||||
-- The "iss" (issuer) claim identifies the principal that issued the
|
||||
-- JWT. The processing of this claim is generally application specific.
|
||||
-- The "iss" value is a case-sensitive string containing a StringOrURI
|
||||
-- value. Use of this claim is OPTIONAL.
|
||||
do
|
||||
set_claim ("iss", iss)
|
||||
end
|
||||
|
||||
set_subject (sub: detachable READABLE_STRING_32)
|
||||
-- The "sub" (subject) claim identifies the principal that is the
|
||||
-- subject of the JWT. The claims in a JWT are normally statements
|
||||
-- about the subject. The subject value MUST either be scoped to be
|
||||
-- locally unique in the context of the issuer or be globally unique.
|
||||
-- The processing of this claim is generally application specific. The
|
||||
-- "sub" value is a case-sensitive string containing a StringOrURI
|
||||
-- value. Use of this claim is OPTIONAL.
|
||||
do
|
||||
set_claim ("sub", sub)
|
||||
end
|
||||
|
||||
set_audience (aud: detachable READABLE_STRING_8)
|
||||
-- The "aud" (audience) claim identifies the recipients that the JWT is
|
||||
-- intended for. Each principal intended to process the JWT MUST
|
||||
-- identify itself with a value in the audience claim. If the principal
|
||||
-- processing the claim does not identify itself with a value in the
|
||||
-- "aud" claim when this claim is present, then the JWT MUST be
|
||||
-- rejected. In the general case, the "aud" value is an array of case-
|
||||
-- sensitive strings, each containing a StringOrURI value. In the
|
||||
-- special case when the JWT has one audience, the "aud" value MAY be a
|
||||
-- single case-sensitive string containing a StringOrURI value. The
|
||||
-- interpretation of audience values is generally application specific.
|
||||
-- Use of this claim is OPTIONAL.
|
||||
do
|
||||
set_claim ("aud", aud)
|
||||
end
|
||||
|
||||
set_expiration_time (exp: detachable DATE_TIME)
|
||||
-- The "exp" (expiration time) claim identifies the expiration time on
|
||||
-- or after which the JWT MUST NOT be accepted for processing. The
|
||||
-- processing of the "exp" claim requires that the current date/time
|
||||
-- MUST be before the expiration date/time listed in the "exp" claim.
|
||||
-- Implementers MAY provide for some small leeway, usually no more than
|
||||
-- a few minutes, to account for clock skew. Its value MUST be a number
|
||||
-- containing a NumericDate value. Use of this claim is OPTIONAL.
|
||||
do
|
||||
if exp = Void then
|
||||
set_claim ("exp", Void)
|
||||
else
|
||||
set_claim ("exp", datetime_to_numeric_date_value (exp))
|
||||
end
|
||||
end
|
||||
|
||||
set_not_before_time (nbf: detachable DATE_TIME)
|
||||
-- The "nbf" (not before) claim identifies the time before which the JWT
|
||||
-- MUST NOT be accepted for processing. The processing of the "nbf"
|
||||
-- claim requires that the current date/time MUST be after or equal to
|
||||
-- the not-before date/time listed in the "nbf" claim. Implementers MAY
|
||||
-- provide for some small leeway, usually no more than a few minutes, to
|
||||
-- account for clock skew. Its value MUST be a number containing a
|
||||
-- NumericDate value. Use of this claim is OPTIONAL.
|
||||
do
|
||||
if nbf = Void then
|
||||
set_claim ("nbf", Void)
|
||||
else
|
||||
set_claim ("nbf", datetime_to_numeric_date_value (nbf))
|
||||
end
|
||||
end
|
||||
|
||||
set_issued_at (iat: detachable DATE_TIME)
|
||||
-- The "iat" (issued at) claim identifies the time at which the JWT was
|
||||
-- issued. This claim can be used to determine the age of the JWT. Its
|
||||
-- value MUST be a number containing a NumericDate value. Use of this
|
||||
-- claim is OPTIONAL.
|
||||
do
|
||||
if iat = Void then
|
||||
set_claim ("iat", Void)
|
||||
else
|
||||
set_claim ("iat", datetime_to_numeric_date_value (iat))
|
||||
end
|
||||
end
|
||||
|
||||
set_jwt_id (jti: detachable READABLE_STRING_8)
|
||||
-- The "jti" (JWT ID) claim provides a unique identifier for the JWT.
|
||||
-- The identifier value MUST be assigned in a manner that ensures that
|
||||
-- there is a negligible probability that the same value will be
|
||||
-- accidentally assigned to a different data object; if the application
|
||||
-- uses multiple issuers, collisions MUST be prevented among values
|
||||
-- produced by different issuers as well. The "jti" claim can be used
|
||||
-- to prevent the JWT from being replayed. The "jti" value is a case-
|
||||
-- sensitive string. Use of this claim is OPTIONAL.
|
||||
do
|
||||
set_claim ("jti", jti)
|
||||
end
|
||||
|
||||
feature {NONE} -- Implementation
|
||||
|
||||
numeric_date_value_to_datetime (v: INTEGER_64): DATE_TIME
|
||||
do
|
||||
create Result.make_from_epoch (v.to_integer_32)
|
||||
end
|
||||
|
||||
datetime_to_numeric_date_value (dt: DATE_TIME): INTEGER_64
|
||||
do
|
||||
Result := dt.definite_duration (create {DATE_TIME}.make_from_epoch (0)).seconds_count
|
||||
end
|
||||
|
||||
end
|
||||
50
library/security/jwt/src/jwt_context.e
Normal file
50
library/security/jwt/src/jwt_context.e
Normal file
@@ -0,0 +1,50 @@
|
||||
note
|
||||
description: "Summary description for {JWT_CONTEXT}."
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
class
|
||||
JWT_CONTEXT
|
||||
|
||||
feature -- Access
|
||||
|
||||
time: detachable DATE_TIME
|
||||
-- Date time to use for validation, if Void, use current date time.
|
||||
|
||||
validation_ignored: BOOLEAN
|
||||
-- Read claimset of JWT without performing validation of the signature
|
||||
-- or any of the regustered claim names.
|
||||
-- Warning: - Use this setting with care, only if you clearly understand
|
||||
-- what you are doing.
|
||||
-- - Without digital signature information, the integrity or authenticity
|
||||
-- of the claimset cannot be trusted.
|
||||
|
||||
issuer: detachable READABLE_STRING_8
|
||||
|
||||
audience: detachable READABLE_STRING_8
|
||||
|
||||
feature -- Element change
|
||||
|
||||
ignore_validation (b: BOOLEAN)
|
||||
-- If `b` then ignore validations.
|
||||
do
|
||||
validation_ignored := b
|
||||
end
|
||||
|
||||
set_time (dt: detachable DATE_TIME)
|
||||
do
|
||||
time := dt
|
||||
end
|
||||
|
||||
set_issuer (iss: like issuer)
|
||||
do
|
||||
issuer := iss
|
||||
end
|
||||
|
||||
set_audience (aud: like audience)
|
||||
do
|
||||
audience := aud
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
276
library/security/jwt/src/jwt_encoder.e
Normal file
276
library/security/jwt/src/jwt_encoder.e
Normal file
@@ -0,0 +1,276 @@
|
||||
note
|
||||
description: "JSON Web Token encoder"
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
class
|
||||
JWT_ENCODER
|
||||
|
||||
feature -- Basic operations
|
||||
|
||||
encoded_values (a_values: STRING_TABLE [READABLE_STRING_GENERAL]; a_secret: READABLE_STRING_8; a_algo: READABLE_STRING_8): STRING
|
||||
local
|
||||
j: JSON_OBJECT
|
||||
do
|
||||
create j.make_with_capacity (a_values.count)
|
||||
across
|
||||
a_values as ic
|
||||
loop
|
||||
j.put_string (ic.item, ic.key)
|
||||
end
|
||||
Result := encoded_json (j, a_secret, a_algo)
|
||||
end
|
||||
|
||||
encoded_json (a_json: JSON_OBJECT; a_secret: READABLE_STRING_8; a_algo: READABLE_STRING_8): STRING
|
||||
local
|
||||
vis: JSON_PRETTY_STRING_VISITOR
|
||||
s: STRING
|
||||
do
|
||||
create s.make_empty
|
||||
create vis.make (s)
|
||||
vis.visit_json_object (a_json)
|
||||
Result := encoded_string (s, a_secret, a_algo)
|
||||
end
|
||||
|
||||
encoded_string (a_payload: READABLE_STRING_8; a_secret: READABLE_STRING_8; a_algo: READABLE_STRING_8): STRING
|
||||
local
|
||||
alg, sign: STRING_8
|
||||
l_enc_payload, l_enc_header: READABLE_STRING_8
|
||||
do
|
||||
reset_error
|
||||
if a_algo.is_case_insensitive_equal_general (alg_hs256) then
|
||||
alg := alg_hs256
|
||||
elseif a_algo.is_case_insensitive_equal_general (alg_none) then
|
||||
alg := alg_none
|
||||
else
|
||||
report_unsupported_alg_error (a_algo)
|
||||
alg := alg_hs256 -- Default ...
|
||||
end
|
||||
l_enc_header := base64url_encode (header ("JWT", alg))
|
||||
l_enc_payload := base64url_encode (a_payload)
|
||||
sign := signature (l_enc_header, l_enc_payload, a_secret, alg)
|
||||
create Result.make (l_enc_header.count + 1 + l_enc_payload.count + 1 + sign.count)
|
||||
Result.append (l_enc_header)
|
||||
Result.append_character ('.')
|
||||
Result.append (l_enc_payload)
|
||||
Result.append_character ('.')
|
||||
Result.append (sign)
|
||||
end
|
||||
|
||||
decoded_string (a_token: READABLE_STRING_8; a_secret: READABLE_STRING_8; a_algo: detachable READABLE_STRING_8): detachable STRING
|
||||
local
|
||||
i,j,n: INTEGER
|
||||
alg, l_enc_payload, l_enc_header, l_signature: READABLE_STRING_8
|
||||
do
|
||||
reset_error
|
||||
n := a_token.count
|
||||
i := a_token.index_of ('.', 1)
|
||||
if i > 0 then
|
||||
j := a_token.index_of ('.', i + 1)
|
||||
if j > 0 then
|
||||
l_enc_header := a_token.substring (1, i - 1)
|
||||
l_enc_payload := a_token.substring (i + 1, j - 1)
|
||||
l_signature := a_token.substring (j + 1, n)
|
||||
Result := base64url_decode (l_enc_payload)
|
||||
alg := a_algo
|
||||
if alg = Void then
|
||||
alg := signature_algorithm_from_encoded_header (l_enc_header)
|
||||
if alg = Void then
|
||||
-- Use default
|
||||
alg := alg_hs256
|
||||
end
|
||||
end
|
||||
check alg_set: alg /= Void end
|
||||
if alg.is_case_insensitive_equal (alg_hs256) then
|
||||
alg := alg_hs256
|
||||
elseif alg.is_case_insensitive_equal (alg_none) then
|
||||
alg := alg_none
|
||||
else
|
||||
alg := alg_hs256
|
||||
report_unsupported_alg_error (alg)
|
||||
end
|
||||
|
||||
if not l_signature.same_string (signature (l_enc_header, l_enc_payload, a_secret, alg)) then
|
||||
report_unverified_token_error
|
||||
end
|
||||
else
|
||||
report_invalid_token
|
||||
end
|
||||
else
|
||||
report_invalid_token
|
||||
end
|
||||
end
|
||||
|
||||
feature -- Error status
|
||||
|
||||
error_code: INTEGER
|
||||
-- Last error, if any.
|
||||
|
||||
has_error: BOOLEAN
|
||||
-- Last `encoded_string` reported an error?
|
||||
do
|
||||
Result := error_code /= 0
|
||||
end
|
||||
|
||||
has_unsupported_alg_error: BOOLEAN
|
||||
do
|
||||
Result := error_code = unsupported_alg_error
|
||||
end
|
||||
|
||||
has_unverified_token_error: BOOLEAN
|
||||
do
|
||||
Result := error_code = unverified_token_error
|
||||
end
|
||||
|
||||
has_invalid_token_error: BOOLEAN
|
||||
do
|
||||
Result := error_code = invalid_token_error
|
||||
end
|
||||
|
||||
feature {NONE} -- Error reporting
|
||||
|
||||
reset_error
|
||||
do
|
||||
error_code := 0
|
||||
end
|
||||
|
||||
report_unsupported_alg_error (alg: READABLE_STRING_8)
|
||||
do
|
||||
error_code := unsupported_alg_error
|
||||
end
|
||||
|
||||
report_unverified_token_error
|
||||
do
|
||||
error_code := unverified_token_error
|
||||
end
|
||||
|
||||
report_invalid_token
|
||||
do
|
||||
error_code := invalid_token_error
|
||||
end
|
||||
|
||||
feature {NONE} -- Constants
|
||||
|
||||
unsupported_alg_error: INTEGER = -2
|
||||
|
||||
unverified_token_error: INTEGER = -4
|
||||
|
||||
invalid_token_error: INTEGER = -8
|
||||
|
||||
alg_hs256: STRING = "HS256"
|
||||
-- HMAC SHA256.
|
||||
|
||||
alg_none: STRING = "none"
|
||||
-- for unsecured token.
|
||||
|
||||
feature -- Conversion
|
||||
|
||||
header (a_type: detachable READABLE_STRING_8; alg: READABLE_STRING_8): STRING
|
||||
do
|
||||
create Result.make_empty
|
||||
Result.append ("{%"typ%":%"")
|
||||
if a_type /= Void then
|
||||
Result.append (a_type)
|
||||
else
|
||||
Result.append ("JWT")
|
||||
end
|
||||
Result.append ("%",%"alg%":%"")
|
||||
Result.append (alg)
|
||||
Result.append ("%"}")
|
||||
end
|
||||
|
||||
feature {NONE} -- Conversion
|
||||
|
||||
signature_algorithm_from_encoded_header (a_enc_header: READABLE_STRING_8): detachable STRING_8
|
||||
local
|
||||
jp: JSON_PARSER
|
||||
do
|
||||
create jp.make_with_string (base64url_decode (a_enc_header))
|
||||
jp.parse_content
|
||||
if
|
||||
attached jp.parsed_json_object as jo and then
|
||||
attached {JSON_STRING} jo.item ("alg") as j_alg
|
||||
then
|
||||
Result := j_alg.unescaped_string_8
|
||||
end
|
||||
end
|
||||
|
||||
feature -- Base64
|
||||
|
||||
base64url_encode (s: READABLE_STRING_8): STRING_8
|
||||
local
|
||||
urlencoder: URL_ENCODER
|
||||
base64: BASE64
|
||||
do
|
||||
create urlencoder
|
||||
create base64
|
||||
Result := urlsafe_encode (base64.encoded_string (s))
|
||||
end
|
||||
|
||||
feature {NONE} -- Implementation
|
||||
|
||||
signature (a_enc_header, a_enc_payload: READABLE_STRING_8; a_secret: READABLE_STRING_8; alg: READABLE_STRING_8): STRING_8
|
||||
local
|
||||
s: STRING
|
||||
do
|
||||
if alg = alg_none then
|
||||
create Result.make_empty
|
||||
else
|
||||
create s.make (a_enc_header.count + 1 + a_enc_payload.count)
|
||||
s.append (a_enc_header)
|
||||
s.append_character ('.')
|
||||
s.append (a_enc_payload)
|
||||
if alg = alg_hs256 then
|
||||
Result := base64_hmacsha256 (s, a_secret)
|
||||
else
|
||||
Result := base64_hmacsha256 (s, a_secret)
|
||||
end
|
||||
Result := urlsafe_encode (Result)
|
||||
end
|
||||
end
|
||||
|
||||
base64url_decode (s: READABLE_STRING_8): STRING_8
|
||||
local
|
||||
urlencoder: URL_ENCODER
|
||||
base64: BASE64
|
||||
do
|
||||
create urlencoder
|
||||
create base64
|
||||
Result := base64.decoded_string (urlsafe_decode (s))
|
||||
end
|
||||
|
||||
urlsafe_encode (s: READABLE_STRING_8): STRING_8
|
||||
do
|
||||
create Result.make_from_string (s)
|
||||
Result.replace_substring_all ("=", "")
|
||||
Result.replace_substring_all ("+", "-")
|
||||
Result.replace_substring_all ("/", "_")
|
||||
end
|
||||
|
||||
urlsafe_decode (s: READABLE_STRING_8): STRING_8
|
||||
local
|
||||
i: INTEGER
|
||||
do
|
||||
create Result.make_from_string (s)
|
||||
Result.replace_substring_all ("-", "+")
|
||||
Result.replace_substring_all ("_", "/")
|
||||
from
|
||||
i := Result.count \\ 4
|
||||
until
|
||||
i = 0
|
||||
loop
|
||||
i := i - 1
|
||||
Result.extend ('=')
|
||||
end
|
||||
end
|
||||
|
||||
base64_hmacsha256 (s: READABLE_STRING_8; a_secret: READABLE_STRING_8): STRING_8
|
||||
local
|
||||
hs256: HMAC_SHA256
|
||||
do
|
||||
create hs256.make_ascii_key (a_secret)
|
||||
hs256.update_from_string (s)
|
||||
Result := hs256.base64_digest --lowercase_hexadecimal_string_digest
|
||||
end
|
||||
|
||||
end
|
||||
19
library/security/jwt/src/jwt_error.e
Normal file
19
library/security/jwt/src/jwt_error.e
Normal file
@@ -0,0 +1,19 @@
|
||||
note
|
||||
description: "Summary description for {JWT_ERROR}."
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
deferred class
|
||||
JWT_ERROR
|
||||
|
||||
feature -- Access
|
||||
|
||||
id: STRING
|
||||
deferred
|
||||
end
|
||||
|
||||
message: READABLE_STRING_8
|
||||
deferred
|
||||
end
|
||||
|
||||
end
|
||||
117
library/security/jwt/src/jwt_header.e
Normal file
117
library/security/jwt/src/jwt_header.e
Normal file
@@ -0,0 +1,117 @@
|
||||
note
|
||||
description: "[
|
||||
JOSE Header
|
||||
|
||||
See https://tools.ietf.org/html/rfc7515
|
||||
]"
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
class
|
||||
JWT_HEADER
|
||||
|
||||
inherit
|
||||
ANY
|
||||
redefine
|
||||
default_create
|
||||
end
|
||||
|
||||
create
|
||||
default_create,
|
||||
make_from_json
|
||||
|
||||
convert
|
||||
string: {READABLE_STRING_8, STRING_8}
|
||||
|
||||
feature {NONE} -- Initialization
|
||||
|
||||
default_create
|
||||
do
|
||||
type := "JWT"
|
||||
algorithm := "HS256"
|
||||
end
|
||||
|
||||
make_from_json (a_json: READABLE_STRING_8)
|
||||
do
|
||||
default_create
|
||||
import_json (a_json)
|
||||
end
|
||||
|
||||
feature -- Access
|
||||
|
||||
type: READABLE_STRING_8
|
||||
-- Token type (typ) - If present, it is recommended to set this to "JWT".
|
||||
|
||||
content_type: detachable READABLE_STRING_8
|
||||
-- Content type (cty)
|
||||
-- If nested signing or encryption is employed, it is recommended to set this to JWT,
|
||||
-- otherwise omit this field.
|
||||
|
||||
algorithm: READABLE_STRING_8
|
||||
-- Message authentication code algorithm (alg)
|
||||
-- The issuer can freely set an algorithm to verify the signature on the token.
|
||||
-- However, some supported algorithms are insecure.
|
||||
|
||||
feature -- Conversion
|
||||
|
||||
string: STRING
|
||||
do
|
||||
create Result.make_empty
|
||||
Result.append ("{%"typ%":%"")
|
||||
Result.append (type)
|
||||
Result.append ("%"")
|
||||
if attached content_type as cty then
|
||||
Result.append (",%"cty%":%"")
|
||||
Result.append (cty)
|
||||
Result.append ("%"")
|
||||
end
|
||||
Result.append (",%"alg%":%"")
|
||||
Result.append (algorithm)
|
||||
Result.append ("%"}")
|
||||
end
|
||||
|
||||
feature -- Element change
|
||||
|
||||
set_type (typ: READABLE_STRING_8)
|
||||
do
|
||||
type := typ
|
||||
end
|
||||
|
||||
set_content_type (cty: detachable READABLE_STRING_8)
|
||||
do
|
||||
content_type := cty
|
||||
end
|
||||
|
||||
set_algorithm (alg: detachable READABLE_STRING_8)
|
||||
do
|
||||
if alg = Void then
|
||||
algorithm := "none"
|
||||
else
|
||||
algorithm := alg
|
||||
end
|
||||
end
|
||||
|
||||
feature -- Element change
|
||||
|
||||
import_json (a_json: READABLE_STRING_8)
|
||||
local
|
||||
jp: JSON_PARSER
|
||||
do
|
||||
create jp.make_with_string (a_json)
|
||||
jp.parse_content
|
||||
if
|
||||
attached jp.parsed_json_object as jo
|
||||
then
|
||||
if attached {JSON_STRING} jo.item ("typ") as j_typ then
|
||||
set_type (j_typ.unescaped_string_8)
|
||||
end
|
||||
if attached {JSON_STRING} jo.item ("cty") as j_cty then
|
||||
set_content_type (j_cty.unescaped_string_8)
|
||||
end
|
||||
if attached {JSON_STRING} jo.item ("alg") as j_alg then
|
||||
set_algorithm (j_alg.unescaped_string_8)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
97
library/security/jwt/src/jwt_loader.e
Normal file
97
library/security/jwt/src/jwt_loader.e
Normal file
@@ -0,0 +1,97 @@
|
||||
note
|
||||
description: "Summary description for {JWT_LOADER}."
|
||||
author: ""
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
class
|
||||
JWT_LOADER
|
||||
|
||||
inherit
|
||||
JWT_UTILITIES
|
||||
|
||||
feature -- Access
|
||||
|
||||
token (a_token_input: READABLE_STRING_8; a_secret: READABLE_STRING_8; ctx: detachable JWT_CONTEXT): detachable JWT
|
||||
-- Decoded token from `a_token_input` given the secret `a_secret`, and optional context `ctx`
|
||||
-- used to specify eventual issuer and various parameters.
|
||||
local
|
||||
jws: JWS
|
||||
i,j,n: INTEGER
|
||||
alg, l_enc_payload, l_enc_header, l_signature: READABLE_STRING_8
|
||||
do
|
||||
n := a_token_input.count
|
||||
i := a_token_input.index_of ('.', 1)
|
||||
if i > 0 then
|
||||
j := a_token_input.index_of ('.', i + 1)
|
||||
if j > 0 then
|
||||
l_enc_header := a_token_input.substring (1, i - 1)
|
||||
l_enc_payload := a_token_input.substring (i + 1, j - 1)
|
||||
l_signature := a_token_input.substring (j + 1, n)
|
||||
create jws.make_with_json_payload (base64url_decode (l_enc_payload))
|
||||
|
||||
alg := signature_algorithm_from_encoded_header (l_enc_header)
|
||||
jws.set_algorithm (alg)
|
||||
if alg = Void then
|
||||
-- Use default
|
||||
alg := alg_hs256
|
||||
end
|
||||
check alg_set: alg /= Void end
|
||||
if ctx = Void or else not ctx.validation_ignored then
|
||||
if not is_supporting_signature_algorithm (alg) then
|
||||
jws.report_unsupported_alg_error (alg)
|
||||
alg := alg_hs256
|
||||
end
|
||||
if not l_signature.same_string (signature (l_enc_header, l_enc_payload, a_secret, alg)) then
|
||||
jws.report_unverified_token_error
|
||||
end
|
||||
if
|
||||
not jws.has_error and then
|
||||
ctx /= Void
|
||||
then
|
||||
check not ctx.validation_ignored end
|
||||
if jws.is_expired (ctx.time) then
|
||||
jws.report_claim_validation_error ("exp")
|
||||
end
|
||||
if not jws.is_nbf_validated (ctx.time) then
|
||||
jws.report_claim_validation_error ("nbf")
|
||||
end
|
||||
if
|
||||
not jws.is_iss_validated (ctx.issuer)
|
||||
then
|
||||
jws.report_claim_validation_error ("iss")
|
||||
end
|
||||
if
|
||||
not jws.is_aud_validated (ctx.audience)
|
||||
then
|
||||
jws.report_claim_validation_error ("aud")
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
else
|
||||
-- jws.report_invalid_token
|
||||
end
|
||||
else
|
||||
-- jws.report_invalid_token
|
||||
end
|
||||
Result := jws
|
||||
end
|
||||
|
||||
feature {NONE} -- Implementation
|
||||
|
||||
signature_algorithm_from_encoded_header (a_enc_header: READABLE_STRING_8): detachable STRING_8
|
||||
local
|
||||
jp: JSON_PARSER
|
||||
do
|
||||
create jp.make_with_string (base64url_decode (a_enc_header))
|
||||
jp.parse_content
|
||||
if
|
||||
attached jp.parsed_json_object as jo and then
|
||||
attached {JSON_STRING} jo.item ("alg") as j_alg
|
||||
then
|
||||
Result := j_alg.unescaped_string_8
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
112
library/security/jwt/src/jwt_utilities.e
Normal file
112
library/security/jwt/src/jwt_utilities.e
Normal file
@@ -0,0 +1,112 @@
|
||||
note
|
||||
description: "Summary description for {JWT_UTILITIES}."
|
||||
author: ""
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
class
|
||||
JWT_UTILITIES
|
||||
|
||||
feature -- Constants
|
||||
|
||||
alg_hs256: STRING = "HS256"
|
||||
-- HMAC SHA256.
|
||||
|
||||
alg_none: STRING = "none"
|
||||
-- for unsecured token.
|
||||
|
||||
feature -- Encoding
|
||||
|
||||
base64url_encode (s: READABLE_STRING_8): STRING_8
|
||||
local
|
||||
urlencoder: URL_ENCODER
|
||||
base64: BASE64
|
||||
do
|
||||
create urlencoder
|
||||
create base64
|
||||
Result := urlsafe_encode (base64.encoded_string (s))
|
||||
end
|
||||
|
||||
urlsafe_encode (s: READABLE_STRING_8): STRING_8
|
||||
do
|
||||
create Result.make_from_string (s)
|
||||
Result.replace_substring_all ("=", "")
|
||||
Result.replace_substring_all ("+", "-")
|
||||
Result.replace_substring_all ("/", "_")
|
||||
end
|
||||
|
||||
signature (a_enc_header, a_enc_payload: READABLE_STRING_8; a_secret: READABLE_STRING_8; alg: READABLE_STRING_8): STRING_8
|
||||
local
|
||||
s: STRING
|
||||
do
|
||||
if alg.is_case_insensitive_equal (alg_none) then
|
||||
create Result.make_empty
|
||||
else
|
||||
create s.make (a_enc_header.count + 1 + a_enc_payload.count)
|
||||
s.append (a_enc_header)
|
||||
s.append_character ('.')
|
||||
s.append (a_enc_payload)
|
||||
if alg.is_case_insensitive_equal (alg_hs256) then
|
||||
Result := base64_hmacsha256 (s, a_secret)
|
||||
else
|
||||
Result := base64_hmacsha256 (s, a_secret)
|
||||
end
|
||||
Result := urlsafe_encode (Result)
|
||||
end
|
||||
end
|
||||
|
||||
base64_hmacsha256 (s: READABLE_STRING_8; a_secret: READABLE_STRING_8): STRING_8
|
||||
local
|
||||
hs256: HMAC_SHA256
|
||||
do
|
||||
create hs256.make_ascii_key (a_secret)
|
||||
hs256.update_from_string (s)
|
||||
Result := hs256.base64_digest --lowercase_hexadecimal_string_digest
|
||||
end
|
||||
|
||||
feature -- Decoding
|
||||
|
||||
base64url_decode (s: READABLE_STRING_8): STRING_8
|
||||
local
|
||||
urlencoder: URL_ENCODER
|
||||
base64: BASE64
|
||||
do
|
||||
create urlencoder
|
||||
create base64
|
||||
Result := base64.decoded_string (urlsafe_decode (s))
|
||||
end
|
||||
|
||||
urlsafe_decode (s: READABLE_STRING_8): STRING_8
|
||||
local
|
||||
i: INTEGER
|
||||
do
|
||||
create Result.make_from_string (s)
|
||||
Result.replace_substring_all ("-", "+")
|
||||
Result.replace_substring_all ("_", "/")
|
||||
from
|
||||
i := Result.count \\ 4
|
||||
until
|
||||
i = 0
|
||||
loop
|
||||
i := i - 1
|
||||
Result.extend ('=')
|
||||
end
|
||||
end
|
||||
|
||||
feature -- Signature
|
||||
|
||||
supported_signature_algorithms: LIST [READABLE_STRING_8]
|
||||
-- Supported signature algorithm `alg`?
|
||||
do
|
||||
create {ARRAYED_LIST [READABLE_STRING_8]} Result.make (2)
|
||||
Result.extend (alg_hs256)
|
||||
Result.extend (alg_none)
|
||||
end
|
||||
|
||||
is_supporting_signature_algorithm (alg: READABLE_STRING_8): BOOLEAN
|
||||
-- Is supporting signature algorithm `alg`?
|
||||
do
|
||||
Result := across supported_signature_algorithms as ic some alg.is_case_insensitive_equal (ic.item) end
|
||||
end
|
||||
|
||||
end
|
||||
193
library/security/jwt/testing/test_jwt.e
Normal file
193
library/security/jwt/testing/test_jwt.e
Normal file
@@ -0,0 +1,193 @@
|
||||
note
|
||||
description: "Summary description for {TEST_JWT}."
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
class
|
||||
TEST_JWT
|
||||
|
||||
inherit
|
||||
EQA_TEST_SET
|
||||
|
||||
SHARED_EXECUTION_ENVIRONMENT
|
||||
undefine
|
||||
default_create
|
||||
end
|
||||
|
||||
feature -- Test
|
||||
|
||||
test_jwt_io
|
||||
local
|
||||
jwt: JWS
|
||||
ut: JWT_UTILITIES
|
||||
do
|
||||
create jwt
|
||||
jwt.set_algorithm ("HS256")
|
||||
jwt.claimset.set_subject ("1234567890")
|
||||
jwt.claimset.set_claim ("name", "John Doe")
|
||||
jwt.claimset.set_claim ("admin", True)
|
||||
create ut
|
||||
|
||||
assert ("header", ut.base64url_encode (jwt.header.string).same_string ("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9"))
|
||||
assert ("payload", ut.base64url_encode (jwt.claimset.string).same_string ("eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9"))
|
||||
assert ("signature", jwt.encoded_string ("secret").same_string ("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.pcHcZspUvuiqIPVB_i_qmcvCJv63KLUgIAKIlXI1gY8"))
|
||||
end
|
||||
|
||||
test_jwt
|
||||
local
|
||||
jwt: JWS
|
||||
jwt_loader: JWT_LOADER
|
||||
payload: STRING
|
||||
tok: STRING
|
||||
do
|
||||
payload := "[
|
||||
{"iss":"joe","exp":1300819380,"http://example.com/is_root":true}
|
||||
]"
|
||||
|
||||
-- payload := "[
|
||||
-- {"sub":"1234567890","name":"John Doe","admin":true}
|
||||
-- ]"
|
||||
|
||||
create jwt.make_with_json_payload (payload)
|
||||
jwt.set_algorithm ("HS256")
|
||||
tok := jwt.encoded_string ("secret")
|
||||
|
||||
create jwt_loader
|
||||
|
||||
if attached jwt_loader.token (tok, "secret", Void) as l_tok then
|
||||
assert ("no error", not l_tok.has_error)
|
||||
assert ("same payload", l_tok.claimset.string.same_string (payload))
|
||||
end
|
||||
end
|
||||
|
||||
test_jwt_with_claimset
|
||||
local
|
||||
jwt: JWS
|
||||
jwt_loader: JWT_LOADER
|
||||
payload: STRING
|
||||
tok: STRING
|
||||
now, dt: DATE_TIME
|
||||
ctx: JWT_CONTEXT
|
||||
do
|
||||
-- payload := "[
|
||||
-- {"iss":"joe","exp":1300819380,"http://example.com/is_root":true}
|
||||
-- ]"
|
||||
|
||||
payload := "[
|
||||
{"sub":"1234567890","name":"John Doe","admin":true}
|
||||
]"
|
||||
|
||||
create jwt.make_with_json_payload (payload)
|
||||
jwt.set_algorithm ("HS256")
|
||||
create now.make_now_utc
|
||||
jwt.claimset.set_issued_at (now)
|
||||
|
||||
dt := duplicated_time (now)
|
||||
dt.minute_add (60)
|
||||
jwt.claimset.set_expiration_time (dt)
|
||||
|
||||
jwt.claimset.set_issuer ("urn:foo")
|
||||
jwt.claimset.set_audience ("urn:foo")
|
||||
|
||||
tok := jwt.encoded_string ("secret")
|
||||
|
||||
payload := jwt.claimset.string
|
||||
|
||||
create jwt_loader
|
||||
|
||||
-- Test with validation + exp
|
||||
if attached jwt_loader.token (tok, "secret", Void) as l_tok then
|
||||
assert ("no error", not l_tok.has_error)
|
||||
assert ("same payload", l_tok.claimset.string.same_string (payload))
|
||||
end
|
||||
|
||||
create ctx
|
||||
ctx.set_time (now)
|
||||
if attached jwt_loader.token (tok, "secret", ctx) as l_tok then
|
||||
assert ("no error", not l_tok.has_error)
|
||||
end
|
||||
|
||||
dt := duplicated_time (now)
|
||||
dt.hour_add (5)
|
||||
ctx.set_time (dt)
|
||||
if attached jwt_loader.token (tok, "secret", ctx) as l_tok then
|
||||
assert ("exp error", l_tok.has_error)
|
||||
end
|
||||
|
||||
-- Test with validation + not before
|
||||
|
||||
dt := duplicated_time (now)
|
||||
dt.second_add (30)
|
||||
jwt.claimset.set_not_before_time (dt)
|
||||
tok := jwt.encoded_string ("secret")
|
||||
|
||||
ctx.set_time (now)
|
||||
if attached jwt_loader.token (tok, "secret", ctx) as l_tok then
|
||||
assert ("has nbf error", l_tok.has_error)
|
||||
end
|
||||
|
||||
dt := duplicated_time (now)
|
||||
dt.second_add (15)
|
||||
ctx.set_time (dt)
|
||||
|
||||
if attached jwt_loader.token (tok, "secret", ctx) as l_tok then
|
||||
assert ("has nbf error", l_tok.has_error)
|
||||
end
|
||||
|
||||
dt := duplicated_time (now)
|
||||
dt.minute_add (45)
|
||||
ctx.set_time (dt)
|
||||
|
||||
if attached jwt_loader.token (tok, "secret", ctx) as l_tok then
|
||||
assert ("no error", not l_tok.has_error)
|
||||
end
|
||||
|
||||
-- Test Issuer
|
||||
ctx.set_issuer ("urn:foobar")
|
||||
if attached jwt_loader.token (tok, "secret", ctx) as l_tok then
|
||||
assert ("has iss error", l_tok.has_error)
|
||||
end
|
||||
ctx.set_issuer ("urn:foo")
|
||||
if attached jwt_loader.token (tok, "secret", ctx) as l_tok then
|
||||
assert ("no error", not l_tok.has_error)
|
||||
end
|
||||
|
||||
-- Test Audience
|
||||
ctx.set_audience ("urn:foobar")
|
||||
if attached jwt_loader.token (tok, "secret", ctx) as l_tok then
|
||||
assert ("has aud error", l_tok.has_error)
|
||||
end
|
||||
ctx.set_audience ("urn:foo")
|
||||
if attached jwt_loader.token (tok, "secret", ctx) as l_tok then
|
||||
assert ("no error", not l_tok.has_error)
|
||||
end
|
||||
end
|
||||
|
||||
test_unsecured_jwt
|
||||
local
|
||||
jwt: JWS
|
||||
payload: STRING
|
||||
tok: STRING
|
||||
do
|
||||
payload := "[
|
||||
{"iss":"joe","exp":1300819380,"http://example.com/is_root":true}
|
||||
]"
|
||||
|
||||
create jwt.make_with_json_payload (payload)
|
||||
jwt.set_algorithm ("none")
|
||||
tok := jwt.encoded_string ("secret")
|
||||
|
||||
if attached (create {JWT_LOADER}).token (tok, "secret", Void) as l_tok then
|
||||
assert ("no error", not jwt.has_error)
|
||||
assert ("same payload", l_tok.claimset.string.same_string (payload))
|
||||
end
|
||||
end
|
||||
|
||||
feature -- Implementation
|
||||
|
||||
duplicated_time (dt: DATE_TIME): DATE_TIME
|
||||
do
|
||||
Result := dt.deep_twin
|
||||
end
|
||||
|
||||
end
|
||||
15
library/security/jwt/testing/testing.ecf
Normal file
15
library/security/jwt/testing/testing.ecf
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="ISO-8859-1"?>
|
||||
<system xmlns="http://www.eiffel.com/developers/xml/configuration-1-15-0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.eiffel.com/developers/xml/configuration-1-15-0 http://www.eiffel.com/developers/xml/configuration-1-15-0.xsd" name="testing" uuid="DB49E98A-0048-414A-A469-EE9B5B903BF3">
|
||||
<target name="testing">
|
||||
<root class="ANY" feature="default_create"/>
|
||||
<setting name="console_application" value="true"/>
|
||||
<option warning="true" void_safety="all">
|
||||
</option>
|
||||
<setting name="concurrency" value="none"/>
|
||||
<library name="base" location="$ISE_LIBRARY\library\base\base.ecf"/>
|
||||
<library name="jwt" location="..\jwt-safe.ecf" readonly="false"/>
|
||||
<library name="testing" location="$ISE_LIBRARY\library\testing\testing.ecf"/>
|
||||
<library name="time" location="$ISE_LIBRARY\library\time\time.ecf"/>
|
||||
<tests name="src" location=".\" recursive="true"/>
|
||||
</target>
|
||||
</system>
|
||||
@@ -57,7 +57,8 @@ feature -- Query
|
||||
-- if possible
|
||||
do
|
||||
if attached value as v then
|
||||
Result := generating_type.name_32
|
||||
-- FIXME: in the future, use the new `{TYPE}.name_32`
|
||||
Result := generating_type.name.to_string_32
|
||||
else
|
||||
Result := {STRING_32} "Void"
|
||||
end
|
||||
|
||||
47
library/text/encoder/src/x_www_form_url_encoder.e
Normal file
47
library/text/encoder/src/x_www_form_url_encoder.e
Normal file
@@ -0,0 +1,47 @@
|
||||
note
|
||||
description: "Summary description for {X_WWW_FORM_URL_ENCODER}."
|
||||
author: ""
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
class
|
||||
X_WWW_FORM_URL_ENCODER
|
||||
|
||||
inherit
|
||||
URL_ENCODER
|
||||
redefine
|
||||
append_percent_encoded_ascii_character_code_to
|
||||
end
|
||||
|
||||
feature {NONE} -- Implementation: character encoding
|
||||
|
||||
append_percent_encoded_ascii_character_code_to (a_code: NATURAL_32; a_result: STRING_GENERAL)
|
||||
-- <Precursor>
|
||||
-- Note: space is encoded with '+' for x-www-form-urlencoding.
|
||||
do
|
||||
inspect a_code
|
||||
when
|
||||
32 -- 32 ' '
|
||||
then
|
||||
a_result.append_code (43) -- 43 '+'
|
||||
when
|
||||
33, 39, -- ! '
|
||||
40, 41, 42 -- ( ) *
|
||||
then
|
||||
a_result.append_code (a_code) -- Keep slash
|
||||
else
|
||||
Precursor (a_code, a_result)
|
||||
end
|
||||
end
|
||||
|
||||
note
|
||||
copyright: "Copyright (c) 2011-2017, 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
|
||||
Reference in New Issue
Block a user