Cross-Origin Resource Sharing initial support

Initial support for the Cross-Origin Resource Sharing specification.
This allows JavaScript to make requests across domain boundaries.

Also reviewed the filter example to get rid of the context and
the generic classes (we can actually use {WSF_REQUEST}.execution_variable
and {WSF_REQUEST}.set_execution_variable).

Links:
* How to enable server-side: http://enable-cors.org/server.html
* Specification: http://www.w3.org/TR/cors/
* Github: http://developer.github.com/v3/#cross-origin-resource-sharing
This commit is contained in:
Olivier Ligot
2013-01-09 17:34:50 +01:00
parent 65d7545320
commit ff57d0ecd4
10 changed files with 324 additions and 82 deletions

View File

@@ -12,22 +12,23 @@
<assertions precondition="true" postcondition="true" invariant="true" supplier_precondition="true"/> <assertions precondition="true" postcondition="true" invariant="true" supplier_precondition="true"/>
</option> </option>
<setting name="concurrency" value="thread"/> <setting name="concurrency" value="thread"/>
<library name="base" location="$ISE_LIBRARY\library\base\base-safe.ecf"/> <library name="base" location="$ISE_LIBRARY\library\base\base-safe.ecf" readonly="true"/>
<library name="connector_nino" location="..\..\library\server\ewsgi\connectors\nino\nino-safe.ecf" readonly="false"> <library name="net" location="$ISE_LIBRARY\library\net\net-safe.ecf" readonly="true"/>
<library name="time" location="$ISE_LIBRARY\library\time\time-safe.ecf" readonly="true"/>
<library name="connector_nino" location="..\..\library\server\ewsgi\connectors\nino\nino-safe.ecf" readonly="true">
<option debug="true"> <option debug="true">
<debug name="nino" enabled="true"/> <debug name="nino" enabled="true"/>
</option> </option>
</library> </library>
<library name="default_nino" location="..\..\library\server\wsf\default\nino-safe.ecf" readonly="false"/> <library name="default_nino" location="..\..\library\server\wsf\default\nino-safe.ecf" readonly="true"/>
<library name="eel" location="..\..\contrib\ise_library\text\encryption\eel\eel-safe.ecf" readonly="false"/> <library name="eel" location="..\..\contrib\ise_library\text\encryption\eel\eel-safe.ecf" readonly="true"/>
<library name="encoder" location="..\..\library\text\encoder\encoder-safe.ecf" readonly="false"/> <library name="encoder" location="..\..\library\text\encoder\encoder-safe.ecf" readonly="true"/>
<library name="http" location="../../library/network/protocol/http/http-safe.ecf" readonly="false"/> <library name="http" location="../../library/network/protocol/http/http-safe.ecf" readonly="true"/>
<library name="json" location="..\..\contrib\library\text\parser\json\library\json-safe.ecf" readonly="false"/> <library name="json" location="..\..\contrib\library\text\parser\json\library\json-safe.ecf" readonly="true"/>
<library name="uri_template" location="../../library/text/parser/uri_template/uri_template-safe.ecf" readonly="false"/> <library name="uri_template" location="../../library/text/parser/uri_template/uri_template-safe.ecf" readonly="true"/>
<library name="wsf" location="..\..\library\server\wsf\wsf-safe.ecf" readonly="false"/> <library name="wsf" location="..\..\library\server\wsf\wsf-safe.ecf" readonly="true"/>
<library name="wsf_extension" location="..\..\library\server\wsf\wsf_extension-safe.ecf" readonly="false"/> <library name="wsf_extension" location="..\..\library\server\wsf\wsf_extension-safe.ecf" readonly="true"/>
<library name="http_authorization" location="..\..\library\server\authentication\http_authorization\http_authorization-safe.ecf" readonly="false"/> <library name="http_authorization" location="..\..\library\server\authentication\http_authorization\http_authorization-safe.ecf" readonly="true"/>
<library name="time" location="$ISE_LIBRARY\library\time\time-safe.ecf"/>
<cluster name="src" location="src\" recursive="true"/> <cluster name="src" location="src\" recursive="true"/>
</target> </target>
</system> </system>

View File

@@ -8,9 +8,9 @@ class
AUTHENTICATION_FILTER AUTHENTICATION_FILTER
inherit inherit
WSF_FILTER_CONTEXT_HANDLER [FILTER_HANDLER_CONTEXT] WSF_FILTER
WSF_URI_TEMPLATE_CONTEXT_HANDLER [FILTER_HANDLER_CONTEXT] WSF_URI_TEMPLATE_HANDLER
SHARED_DATABASE_API SHARED_DATABASE_API
@@ -18,7 +18,7 @@ inherit
feature -- Basic operations feature -- Basic operations
execute (ctx: FILTER_HANDLER_CONTEXT; req: WSF_REQUEST; res: WSF_RESPONSE) execute (req: WSF_REQUEST; res: WSF_RESPONSE)
-- Execute the filter -- Execute the filter
local local
l_auth: HTTP_AUTHORIZATION l_auth: HTTP_AUTHORIZATION
@@ -31,8 +31,8 @@ feature -- Basic operations
attached l_auth.password as l_auth_password and then attached l_auth.password as l_auth_password and then
l_auth_password.same_string (l_user.password) l_auth_password.same_string (l_user.password)
then then
ctx.set_user (l_user) req.set_execution_variable ("user", l_user)
execute_next (ctx, req, res) execute_next (req, res)
else else
handle_unauthorized ("Unauthorized", req, res) handle_unauthorized ("Unauthorized", req, res)
end end
@@ -56,6 +56,6 @@ feature {NONE} -- Implementation
end end
note note
copyright: "2011-2012, Olivier Ligot, Jocelyn Fiat and others" copyright: "2011-2013, Olivier Ligot, Jocelyn Fiat and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
end end

View File

@@ -24,39 +24,60 @@ create
feature {NONE} -- Initialization feature {NONE} -- Initialization
make make
local
l_message: STRING
l_factory: INET_ADDRESS_FACTORY
do do
create router.make (1)
initialize_filter initialize_filter
initialize_json initialize_json
set_service_option ("port", 9090) set_service_option ("port", port)
create l_message.make_empty
l_message.append_string ("Launching filter server at ")
create l_factory
l_message.append_string (l_factory.create_localhost.host_name)
l_message.append_string (" port ")
l_message.append_integer (port)
io.put_string (l_message)
io.put_new_line
make_and_launch make_and_launch
end end
create_filter create_filter
-- Create `filter' -- Create `filter'
local local
l_router: WSF_ROUTER l_cors_filter: WSF_CORS_FILTER
l_authentication_filter_hdl: AUTHENTICATION_FILTER
l_user_filter: USER_HANDLER
l_routing_filter: WSF_ROUTING_FILTER
do do
create l_router.make (1) create l_cors_filter
create l_authentication_filter_hdl filter := l_cors_filter
create l_user_filter
l_authentication_filter_hdl.set_next (l_user_filter)
l_router.handle_with_request_methods ("/user/{userid}", l_authentication_filter_hdl, l_router.methods_get)
create l_routing_filter.make (l_router)
l_routing_filter.set_execute_default_action (agent execute_default)
filter := l_routing_filter
end end
setup_filter setup_filter
-- Setup `filter' -- Setup `filter'
local local
l_options_filter: WSF_CORS_OPTIONS_FILTER
l_authentication_filter: AUTHENTICATION_FILTER
l_user_filter: USER_HANDLER
l_methods: WSF_REQUEST_METHODS
l_routing_filter: WSF_ROUTING_FILTER
l_logging_filter: WSF_LOGGING_FILTER l_logging_filter: WSF_LOGGING_FILTER
do do
create l_options_filter.make (router)
create l_authentication_filter
l_options_filter.set_next (l_authentication_filter)
create l_user_filter
l_authentication_filter.set_next (l_user_filter)
create l_methods
l_methods.enable_options
l_methods.enable_get
router.handle_with_request_methods ("/user/{userid}", l_options_filter, l_methods)
create l_routing_filter.make (router)
l_routing_filter.set_execute_default_action (agent execute_default)
filter.set_next (l_routing_filter)
create l_logging_filter create l_logging_filter
filter.set_next (l_logging_filter) l_routing_filter.set_next (l_logging_filter)
end end
initialize_json initialize_json
@@ -73,30 +94,24 @@ feature -- Basic operations
end end
execute_default (req: WSF_REQUEST; res: WSF_RESPONSE) execute_default (req: WSF_REQUEST; res: WSF_RESPONSE)
-- I'm using this method to handle the method not allowed response
-- in the case that the given uri does not have a corresponding http method
-- to handle it.
local local
h : HTTP_HEADER l_message: WSF_DEFAULT_ROUTER_RESPONSE
l_description : STRING
l_api_doc : STRING
do do
if req.content_length_value > 0 then create l_message.make_with_router (req, router)
req.input.read_string (req.content_length_value.as_integer_32) l_message.set_documentation_included (True)
end res.send (l_message)
create h.make
h.put_content_type_text_plain
l_api_doc := "%NPlease check the API%NURI:/user/{userid} METHOD: GET%N"
l_description := req.request_method + req.request_uri + " is not allowed" + "%N" + l_api_doc
h.put_content_length (l_description.count)
h.put_current_date
res.set_status_code ({HTTP_STATUS_CODE}.method_not_allowed)
res.put_header_text (h.string)
res.put_string (l_description)
end end
feature {NONE} -- Implementation
port: INTEGER = 9090
-- Port number
router: WSF_ROUTER;
-- Router
note note
copyright: "2011-2012, Olivier Ligot, Jocelyn Fiat and others" copyright: "2011-2013, Olivier Ligot, Jocelyn Fiat and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
source: "[ source: "[
Eiffel Software Eiffel Software

View File

@@ -8,11 +8,11 @@ class
USER_HANDLER USER_HANDLER
inherit inherit
WSF_FILTER_CONTEXT_HANDLER [FILTER_HANDLER_CONTEXT] WSF_FILTER
WSF_URI_TEMPLATE_CONTEXT_HANDLER [FILTER_HANDLER_CONTEXT] WSF_URI_TEMPLATE_HANDLER
WSF_RESOURCE_CONTEXT_HANDLER_HELPER [FILTER_HANDLER_CONTEXT] WSF_RESOURCE_HANDLER_HELPER
redefine redefine
do_get do_get
end end
@@ -23,30 +23,30 @@ inherit
feature -- Basic operations feature -- Basic operations
execute (ctx: FILTER_HANDLER_CONTEXT; req: WSF_REQUEST; res: WSF_RESPONSE) execute (req: WSF_REQUEST; res: WSF_RESPONSE)
-- Execute request handler -- Execute request handler
do do
execute_methods (ctx, req, res) execute_methods (req, res)
execute_next (ctx, req, res) execute_next (req, res)
end end
do_get (ctx: FILTER_HANDLER_CONTEXT; req: WSF_REQUEST; res: WSF_RESPONSE) do_get (req: WSF_REQUEST; res: WSF_RESPONSE)
-- Using GET to retrieve resource information. -- Using GET to retrieve resource information.
-- If the GET request is SUCCESS, we response with -- If the GET request is SUCCESS, we response with
-- 200 OK, and a representation of the user -- 200 OK, and a representation of the user
-- If the GET request is not SUCCESS, we response with -- If the GET request is not SUCCESS, we response with
-- 404 Resource not found -- 404 Resource not found
require else require else
authenticated_user_attached: attached ctx.user authenticated_user_attached: attached {USER} req.execution_variable ("user")
local local
id : STRING id : STRING
do do
if attached req.orig_path_info as orig_path then if attached req.orig_path_info as orig_path then
id := get_user_id_from_path (orig_path) id := get_user_id_from_path (orig_path)
if attached retrieve_user (id) as l_user then if attached retrieve_user (id) as l_user then
if l_user ~ ctx.user then if l_user ~ req.execution_variable ("user") then
compute_response_get (req, res, l_user) compute_response_get (req, res, l_user)
elseif attached ctx.user as l_auth_user then elseif attached {USER} req.execution_variable ("user") as l_auth_user then
-- Trying to access another user that the authenticated one, -- Trying to access another user that the authenticated one,
-- which is forbidden in this example... -- which is forbidden in this example...
handle_forbidden ("You try to access the user " + id.out + " while authenticating with the user " + l_auth_user.id.out, req, res) handle_forbidden ("You try to access the user " + id.out + " while authenticating with the user " + l_auth_user.id.out, req, res)
@@ -92,6 +92,6 @@ feature {NONE} -- Implementation
end end
note note
copyright: "2011-2012, Olivier Ligot, Jocelyn Fiat and others" copyright: "2011-2013, Olivier Ligot, Jocelyn Fiat and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
end end

View File

@@ -213,6 +213,25 @@ feature -- Header change: general
put_header (k + colon_space + v) put_header (k + colon_space + v)
end end
put_header_key_methods (k: READABLE_STRING_8; a_methods: ITERABLE [READABLE_STRING_8])
-- Add header `k: a_methods', or replace existing header of same header methods/key
local
s: STRING_8
do
create s.make_empty
across
a_methods as c
loop
if not s.is_empty then
s.append_string (", ")
end
s.append (c.item)
end
if not s.is_empty then
put_header_key_value (k, s)
end
end
feature -- Content related header feature -- Content related header
put_content_type (t: READABLE_STRING_8) put_content_type (t: READABLE_STRING_8)
@@ -397,26 +416,38 @@ feature -- Content-type helpers
put_content_type_multipart_encrypted do put_content_type ({HTTP_MIME_TYPES}.multipart_encrypted) end put_content_type_multipart_encrypted do put_content_type ({HTTP_MIME_TYPES}.multipart_encrypted) end
put_content_type_application_x_www_form_encoded do put_content_type ({HTTP_MIME_TYPES}.application_x_www_form_encoded) end put_content_type_application_x_www_form_encoded do put_content_type ({HTTP_MIME_TYPES}.application_x_www_form_encoded) end
feature -- Cross-Origin Resource Sharing
put_access_control_allow_origin (s: READABLE_STRING_8)
-- Put "Access-Control-Allow-Origin" header.
do
put_header_key_value ({HTTP_HEADER_NAMES}.header_access_control_allow_origin, s)
end
put_access_control_allow_all_origin
-- Put "Access-Control-Allow-Origin: *" header.
do
put_access_control_allow_origin ("*")
end
put_access_control_allow_methods (a_methods: ITERABLE [READABLE_STRING_8])
-- If `a_methods' is not empty, put `Access-Control-Allow-Methods' header with list `a_methods' of methods
do
put_header_key_methods ({HTTP_HEADER_NAMES}.header_access_control_allow_methods, a_methods)
end
put_access_control_allow_headers (s: READABLE_STRING_8)
-- Put "Access-Control-Allow-Headers" header.
do
put_header_key_value ({HTTP_HEADER_NAMES}.header_access_control_allow_headers, s)
end
feature -- Method related feature -- Method related
put_allow (a_methods: ITERABLE [READABLE_STRING_8]) put_allow (a_methods: ITERABLE [READABLE_STRING_8])
-- If `a_methods' is not empty, put `Allow' header with list `a_methods' of methods -- If `a_methods' is not empty, put `Allow' header with list `a_methods' of methods
local
s: STRING_8
do do
create s.make_empty put_header_key_methods ({HTTP_HEADER_NAMES}.header_allow, a_methods)
across
a_methods as c
loop
if not s.is_empty then
s.append_character (',')
end
s.append_character (' ')
s.append (c.item)
end
if not s.is_empty then
put_header_key_value ({HTTP_HEADER_NAMES}.header_allow, s)
end
end end
feature -- Date feature -- Date
@@ -738,7 +769,7 @@ feature {NONE} -- Constants
semi_colon_space: STRING = "; " semi_colon_space: STRING = "; "
note note
copyright: "2011-2012, Jocelyn Fiat, Eiffel Software and others" copyright: "2011-2013, Jocelyn Fiat, Eiffel Software and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
source: "[ source: "[
Eiffel Software Eiffel Software

View File

@@ -194,6 +194,23 @@ feature -- Response header name
-- Indicates the authentication scheme that should be used to access the requested entity. -- Indicates the authentication scheme that should be used to access the requested entity.
--| Example: WWW-Authenticate: Basic --| Example: WWW-Authenticate: Basic
feature -- Cross-Origin Resource Sharing
header_access_control_allow_origin: STRING = "Access-Control-Allow-Origin"
-- Indicates whether a resource can be shared based by returning
-- the value of the Origin request header in the response.
-- | Example: Access-Control-Allow-Origin: http://example.org
header_access_control_allow_methods: STRING = "Access-Control-Allow-Methods"
-- Indicates, as part of the response to a preflight request,
-- which methods can be used during the actual request.
-- | Example: Access-Control-Allow-Methods: PUT, DELETE
header_access_control_allow_headers: STRING = "Access-Control-Allow-Headers"
-- Indicates, as part of the response to a preflight request,
-- which header field names can be used during the actual request.
-- | Example: Access-Control-Allow-Headers: Authorization
feature -- Request or Response header name feature -- Request or Response header name
header_cache_control: STRING = "Cache-Control" header_cache_control: STRING = "Cache-Control"
@@ -248,7 +265,7 @@ feature -- MIME related
header_content_transfer_encoding: STRING = "Content-Transfer-Encoding" header_content_transfer_encoding: STRING = "Content-Transfer-Encoding"
note note
copyright: "2011-2012, Jocelyn Fiat, Eiffel Software and others" copyright: "2011-2013, Jocelyn Fiat, Eiffel Software and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
source: "[ source: "[
Eiffel Software Eiffel Software

View File

@@ -0,0 +1,33 @@
note
description: "Cross-Origin Resource Sharing filter."
author: "Olivier Ligot"
date: "$Date$"
revision: "$Revision$"
EIS: "name=Cross-Origin Resource Sharing", "src=http://www.w3.org/TR/cors/", "tag=W3C"
class
WSF_CORS_FILTER
inherit
WSF_FILTER
feature -- Basic operations
execute (req: WSF_REQUEST; res: WSF_RESPONSE)
-- Execute the filter.
do
res.header.put_access_control_allow_all_origin
execute_next (req, res)
end
note
copyright: "2011-2013, Jocelyn Fiat, Javier Velilla, Olivier Ligot, Eiffel Software and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
source: "[
Eiffel Software
5949 Hollister Ave., Goleta, CA 93117 USA
Telephone 805-685-1006, Fax 805-685-6869
Website http://www.eiffel.com
Customer support http://support.eiffel.com
]"
end

View File

@@ -0,0 +1,59 @@
note
description: "Filter that handles an OPTIONS request, with Cross-Origin Resource Sharing support."
author: "Olvier Ligot"
date: "$Date$"
revision: "$Revision$"
EIS: "name=Cross-Origin Resource Sharing", "src=http://www.w3.org/TR/cors/", "tag=W3C"
class
WSF_CORS_OPTIONS_FILTER
inherit
WSF_FILTER
WSF_URI_TEMPLATE_HANDLER
create
make
feature {NONE} -- Initialization
make (a_router: like router)
-- Initialize Current with `a_router'.
do
router := a_router
ensure
router_set: router = a_router
end
feature -- Access
router: WSF_ROUTER
-- Associated router
feature -- Basic operations
execute (req: WSF_REQUEST; res: WSF_RESPONSE)
-- Execute the filter.
local
msg: WSF_CORS_OPTIONS_RESPONSE
do
if req.is_request_method ({HTTP_REQUEST_METHODS}.method_options) then
create msg.make (req, router)
res.send (msg)
else
execute_next (req, res)
end
end
note
copyright: "2011-2013, Jocelyn Fiat, Javier Velilla, Olivier Ligot, Eiffel Software and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
source: "[
Eiffel Software
5949 Hollister Ave., Goleta, CA 93117 USA
Telephone 805-685-1006, Fax 805-685-6869
Website http://www.eiffel.com
Customer support http://support.eiffel.com
]"
end

View File

@@ -0,0 +1,64 @@
note
description: "Response to an OPTIONS request, with Cross-Origin Resource Sharing support."
author: "Olivier Ligt"
date: "$Date$"
revision: "$Revision$"
EIS: "name=Cross-Origin Resource Sharing", "src=http://www.w3.org/TR/cors/", "tag=W3C"
class
WSF_CORS_OPTIONS_RESPONSE
inherit
WSF_RESPONSE_MESSAGE
create
make
feature {NONE} -- Initialization
make (req: WSF_REQUEST; a_router: like router)
do
request := req
router := a_router
create header.make
end
feature -- Access
request: WSF_REQUEST
-- Associated request
router: WSF_ROUTER
-- Associated router
header: HTTP_HEADER
-- Response' header
feature {WSF_RESPONSE} -- Output
send_to (res: WSF_RESPONSE)
local
l_methods: WSF_REQUEST_METHODS
do
res.set_status_code ({HTTP_STATUS_CODE}.No_content)
header.put_current_date
header.put_access_control_allow_headers ({HTTP_HEADER_NAMES}.header_authorization)
l_methods := router.allowed_methods_for_request (request)
if not l_methods.is_empty then
header.put_allow (l_methods)
header.put_access_control_allow_methods (l_methods)
end
res.put_header_text (header.string)
end
note
copyright: "2011-2013, Jocelyn Fiat, Javier Velilla, Olivier Ligot, Eiffel Software and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
source: "[
Eiffel Software
5949 Hollister Ave., Goleta, CA 93117 USA
Telephone 805-685-1006, Fax 805-685-6869
Website http://www.eiffel.com
Customer support http://support.eiffel.com
]"
end

View File

@@ -31,6 +31,7 @@ feature {NONE} -- Initialization
do do
transfered_content_length := 0 transfered_content_length := 0
wgi_response := r wgi_response := r
create header.make
end end
feature {WSF_RESPONSE_EXPORTER} -- Properties feature {WSF_RESPONSE_EXPORTER} -- Properties
@@ -114,6 +115,13 @@ feature -- Status setting
feature -- Header output operation feature -- Header output operation
header: HTTP_HEADER
-- Header
-- This is useful when we want to fill the `header'
-- in two pass (i.e. in two different classes).
-- We first call features of `header', and finally
-- we call `put_header_text'
put_header_text (a_text: READABLE_STRING_8) put_header_text (a_text: READABLE_STRING_8)
-- Sent `a_text' and just before send the status code -- Sent `a_text' and just before send the status code
require require
@@ -121,9 +129,23 @@ feature -- Header output operation
header_not_committed: not header_committed header_not_committed: not header_committed
a_text_ends_with_single_crlf: a_text.count > 2 implies not a_text.substring (a_text.count - 2, a_text.count).same_string ("%R%N") a_text_ends_with_single_crlf: a_text.count > 2 implies not a_text.substring (a_text.count - 2, a_text.count).same_string ("%R%N")
a_text_does_not_end_with_double_crlf: a_text.count > 4 implies not a_text.substring (a_text.count - 4, a_text.count).same_string ("%R%N%R%N") a_text_does_not_end_with_double_crlf: a_text.count > 4 implies not a_text.substring (a_text.count - 4, a_text.count).same_string ("%R%N%R%N")
local
l_text: READABLE_STRING_8
l_header: HTTP_HEADER
do do
wgi_response.set_status_code (status_code, status_reason_phrase) wgi_response.set_status_code (status_code, status_reason_phrase)
wgi_response.put_header_text (a_text) if header.is_empty then
l_text := a_text
else
create l_header.make_from_raw_header_data (a_text)
across
l_header as c
loop
header.put_header (c.item.string)
end
l_text := header.string
end
wgi_response.put_header_text (l_text)
ensure ensure
status_set: status_is_set status_set: status_is_set
status_committed: status_committed status_committed: status_committed
@@ -376,7 +398,7 @@ feature -- Error reporting
end end
note note
copyright: "2011-2012, Jocelyn Fiat, Javier Velilla, Olivier Ligot, Eiffel Software and others" copyright: "2011-2013, Jocelyn Fiat, Javier Velilla, Olivier Ligot, Eiffel Software and others"
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
source: "[ source: "[
Eiffel Software Eiffel Software