Compare commits
155 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d4bbdea5e4 | |||
| 8260336d6c | |||
| 19c14d28c7 | |||
| f1e8dfa40b | |||
| e73302639d | |||
| 1c58674523 | |||
| 7cfe0cc5ed | |||
| 4f8341e04e | |||
| 7f36e539f1 | |||
| 8241c0209a | |||
| 45179b58a3 | |||
| 31fa31bd53 | |||
| a8a3ca5b97 | |||
| 7c6fe5a04a | |||
| c8e2009638 | |||
| 627ec7aefc | |||
| e7087bcbc1 | |||
| 8d881bcd7d | |||
| ed3ad962d1 | |||
| d3e865cf6c | |||
| 9fcd30b4e1 | |||
| 0baa05cf63 | |||
| f97f59b703 | |||
| dc377b84d3 | |||
| f14431fc05 | |||
| 9577d7d82a | |||
| 99f8377721 | |||
| 73d5555532 | |||
| ce3c7ac57a | |||
|
|
8754c2d67d | ||
| 5e928b9a47 | |||
|
|
9cdfbd2538 | ||
| e4fcc863ca | |||
|
|
7c8d6b9eef | ||
| a97eb4b062 | |||
| bd5aba3db6 | |||
| d43c4edb7d | |||
| 9cdd676417 | |||
| cb273c3176 | |||
| ec7d504502 | |||
| 7ed1e815b0 | |||
| da2e26f697 | |||
| bc169d6b26 | |||
| cf2f0f09fa | |||
| 207a109e44 | |||
| 2f2e2067ba | |||
| 7aa7bf1ab2 | |||
| 8e8c3602c6 | |||
|
|
ffd7dd8540 | ||
|
|
947c94644e | ||
|
|
6a779797a5 | ||
|
|
7b0ccc0aec | ||
|
|
74001fe674 | ||
|
|
c7eb12ad8e | ||
|
|
36eeff9285 | ||
|
|
e9292b3eac | ||
|
|
30625d460f | ||
|
|
24eb0a4002 | ||
|
|
7d738a164d | ||
|
|
1037256ea6 | ||
|
|
4d79bba04b | ||
|
|
5de024923e | ||
|
|
8b90241986 | ||
|
|
da1c0b8545 | ||
|
|
603bedf71d | ||
|
|
5fedad7f2e | ||
|
|
e83f5654d8 | ||
|
|
25446cac12 | ||
|
|
ccff084642 | ||
|
|
830adbe10c | ||
|
|
e6d998953e | ||
|
|
6ca3cca88b | ||
|
|
f91a676f41 | ||
|
|
1c75e11e34 | ||
|
|
b5b4fa6b2f | ||
|
|
211fc425a3 | ||
|
|
95cebe26bb | ||
|
|
f770c236d5 | ||
|
|
503e5f7915 | ||
|
|
39f01e95fd | ||
|
|
c725159d7e | ||
|
|
e66f1cf7be | ||
|
|
c03d28cabc | ||
|
|
e834b2b360 | ||
|
|
d089fd3a03 | ||
|
|
a0c1ab5232 | ||
|
|
a8ddd10b46 | ||
|
|
db39068ceb | ||
|
|
a1b4337438 | ||
|
|
74121be470 | ||
|
|
1c9f5ac0e7 | ||
|
|
edec837c4e | ||
|
|
f1642a444a | ||
|
|
48af63af83 | ||
|
|
2f98d7031f | ||
|
|
70f00651c7 | ||
|
|
199f84c7ef | ||
|
|
9b97627c76 | ||
|
|
72c87cd74d | ||
|
|
2ed4d03168 | ||
|
|
18ed92a61d | ||
|
|
0a6a4281e7 | ||
|
|
38cf5d7a6f | ||
|
|
96648a16dc | ||
|
|
6f35ad7b16 | ||
|
|
8ff20d34a7 | ||
|
|
85c8a46c89 | ||
|
|
498e4a6ec2 | ||
|
|
ab507d543a | ||
|
|
20a90db2e3 | ||
|
|
97fe16b4c2 | ||
|
|
6ed91699b8 | ||
|
|
bb334aef80 | ||
|
|
c2764e25ff | ||
|
|
cdada71f7e | ||
|
|
a7d0398ec6 | ||
|
|
267655d7bc | ||
|
|
e735da1bcb | ||
|
|
6425482070 | ||
|
|
818c3fb460 | ||
|
|
dac50b490d | ||
|
|
16d5076fe5 | ||
|
|
2748e1d9ee | ||
|
|
27ee20f99b | ||
|
|
9a3164df70 | ||
|
|
02383810b4 | ||
|
|
dbf5e76047 | ||
|
|
5c31905427 | ||
|
|
21adef6998 | ||
|
|
c7ef652322 | ||
|
|
67bdcfb6ef | ||
|
|
ca4043b102 | ||
|
|
310e96e185 | ||
|
|
46a48625ac | ||
|
|
7feb45b549 | ||
|
|
aeb3392ad8 | ||
|
|
5bbd031275 | ||
|
|
90e60fad26 | ||
|
|
98c20ee7c1 | ||
|
|
9fc5a7c364 | ||
|
|
64027f56bd | ||
|
|
0b99e84728 | ||
|
|
c51a0a4788 | ||
|
|
a6806c676a | ||
|
|
9e5e8bb1bf | ||
|
|
10a83c6ad8 | ||
|
|
1ec3b8e7a4 | ||
|
|
0783049fb4 | ||
|
|
7e54825b84 | ||
|
|
40cbe7dfc9 | ||
|
|
d4b9301a57 | ||
|
|
06cda97535 | ||
| e14bb568d2 | |||
| 05d37439bc | |||
|
|
99bf552b89 |
18
.travis.yml
@@ -0,0 +1,18 @@
|
||||
language: eiffel
|
||||
before_script:
|
||||
- export current_dir=$PWD ; echo current_dir=$current_dir ; cd ..
|
||||
- curl -sSL https://www.eiffel.org/setup/install.sh | bash > eiffel.rc
|
||||
- source ./eiffel.rc
|
||||
- echo `ec -version`
|
||||
- cd $current_dir
|
||||
- echo Check projects compilation status...
|
||||
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- v1
|
||||
- develop
|
||||
|
||||
script: compile_all -ecb -melt -list_failures -log_verbose -clean -options dotnet=false
|
||||
group: stable
|
||||
os: linux
|
||||
|
||||
71
CHANGELOG.md
@@ -8,19 +8,70 @@ 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
|
||||
- Removed obsolete v0 code.
|
||||
### Fixed
|
||||
- `error`: Fixed custom error creation, do not create default message, otherwise the info will be duplicated in error output.
|
||||
- `jwt`: updated indexing notes, and readme.
|
||||
### Security
|
||||
|
||||
|
||||
## [v1.1.0] - 2018-10-29
|
||||
### Added
|
||||
- `examples`: Added an example to run the debug app with apache2+libfcgi inside a docker container.
|
||||
- `wsf`: Updated the default rescue response (i.e when exception or bad internal error occurs). Factorized the implementation in `WGI_RESCUE_EXECUTION`, and now by redefining the `WGI_EXECUTION.execute_rescue (...)` procedure, it is possible to have a custom response on such rescued execution.
|
||||
- `http`: Added `HTTP_COOKIE.set_expiration_from_max_age`, to add the "Expiration:" based on the max-age value.
|
||||
|
||||
### Changed
|
||||
- `wsf`: Adopted the nanoseconds timeout precision. And in config file added support for ns, us, ms, s timeout precision (without indication, it uses `seconds` precision).
|
||||
|
||||
### 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.
|
||||
- `websocket`: Allow (websocket) upgrade even without persistent connection for normal http request.
|
||||
(note: this allows to use websocket in single-threaded mode, and avoid the keep-alive-timeout delay before websocket begins its execution)
|
||||
|
||||
- `http`: Fixed `HTTP_HEADER.put_raw_header (..)` by ignoring any empty line of the argument value. (note: "%R" is considered as empty line here.)
|
||||
- `jwt`: updated to make JWT algorithm support more flexible, and simple to extend with specific algorithm.
|
||||
- `httpd`, `websocket`: Fixed setting of socket.timeout in httpd (it was not correctly set).
|
||||
|
||||
### Security
|
||||
|
||||
## [v1.0.6] - 2018-02-05
|
||||
### Added
|
||||
- `jwt`: new JSON Web Token (JWT) library (supports for claim exp, iat, nbf, iss, aud).
|
||||
- `http_client`: added support for ciphers setting in the libcurl implementation only.
|
||||
- `http_client`: added convenient `get` and `custom` functions on `HTTP_CLIENT` directly.
|
||||
- `wsf`: added `WSF_EXECUTE_HANDLER`, and `WSF_CGI_HANDLER`. Demonstration of `WSF_CGI_HANDLER` in the new `tools/httpd` project.
|
||||
- `wsf_security`: new security library, providing support for XSS injection protection and similar.
|
||||
- `wsf`: Support persistent connection, even in single thread mode (i.e concurrency=none).
|
||||
Warning: as there is no concurrent request handling in single threaded mode, it is recommended to either set the keep_alive_timeout to a small value, or disable persistent connection by setting max_keep_alive_requests to 0.
|
||||
Accept -1 as value of max_keep_alive_requests to have unlimited number of request in the same persistent connection.
|
||||
- `wsf_compression`: Introduced `WSF_COMPRESSION` and applied to `WSF_*_WITH_COMPRESSION` classes. Added `simple_compression` example.
|
||||
- `websocket`: added `on_timer` solution to allow the server to check for external events and send notification to websocket clients.
|
||||
- `wsf`: Added routing condition mapping.
|
||||
- `wsf_extension`: added handler to add support for CGI scripts.
|
||||
- Added a new tool `httpd` which is a basic httpd server product (with file server and CGI handler).
|
||||
- `wsf_security`: added security protections such as XSS injection protection support.
|
||||
|
||||
### Changed
|
||||
- adopted ecf version 1-16-0 and use a single .ecf file (the -safe.ecf are now redirection to normal .ecf)
|
||||
- `wsf_html`: Made interface of wsf forms and widgets a bit more flexible by accepting `READABLE_STRING_GENERAL`.
|
||||
|
||||
### Deprecated
|
||||
- removed support for Eiffel version before 17.05 .
|
||||
- SSL 2 or 3 is obsolete and will raise an exception if used.
|
||||
### Removed
|
||||
### Fixed
|
||||
- Removed a few obsolete calls.
|
||||
- `http_client`: Added support for multiple file in form data. Made clear what is the meaning of `upload_filename`, `upload_data` and `form_data`.
|
||||
- `authentication`: `HTTP_AUTHORIZATION` acceps now `READABLE_STRING_GENERAL` for username and password argument.
|
||||
- `http_client`: fixed curl implementation by setting `Content-Type` to `x-www-form-urlencoded` (if not set) when POST send data as `x-www-form-urlencoded`.
|
||||
- `notification_email`: fixed the SMTP support for multiple recipients address.
|
||||
- `http_network`: use proper ciphers settings for libcurl implementation.
|
||||
- `http_client`: Improved support of absolute/relative https:// and http:// in `http_client`.
|
||||
- `json_encoder.e`: Properly JSON encode null character as \u0000 .
|
||||
### Security
|
||||
|
||||
|
||||
|
||||
14
README.md
@@ -1,5 +1,7 @@
|
||||
# Eiffel Web Framework
|
||||
|
||||
[](https://travis-ci.org/EiffelWebFramework/EWF/)
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -11,14 +13,14 @@ Currently EWF offers a collection of Eiffel libraries designed to be integrated
|
||||
|
||||
There is a growing ecosystem around EWF, that provides useful components:
|
||||
* OpenID and OAuth consumer library
|
||||
* Various hypermedia format such as HAL, Collection+json, …
|
||||
* Various hypermedia format such as HAL, Collection+json, ...
|
||||
* Websocket server and client
|
||||
* Template engine
|
||||
* API Auto-documentation with swagger
|
||||
* A simple experimental CMS.
|
||||
* ...
|
||||
|
||||
So if you want to build a website, a web api, RESTful service, …or even if you want to consume other web api, EWF is a solution.
|
||||
So if you want to build a website, a web api, RESTful service, ... or even if you want to consume other web api, EWF is a solution.
|
||||
|
||||
EWF brings with it all the advantages of the Eiffel technology and tools with its powerful features such as Design by Contract, debugging, testing tools which enable to build efficient systems expected to be repeatedly refined, extended, and improved in a predictable and controllable way so as to become with time bugfree systems. Enjoy the full power of debugging your web server application from the IDE.
|
||||
|
||||
@@ -49,11 +51,11 @@ Tasks and issues are managed with github issue system
|
||||
## How to get the source code?
|
||||
|
||||
Using git
|
||||
* git clone https://github.com/EiffelWebFramework/EWF.git
|
||||
* `git clone https://github.com/EiffelWebFramework/EWF.git`
|
||||
|
||||
* And to build the required and related Clibs
|
||||
* cd contrib/ise_library/cURL
|
||||
* geant compile
|
||||
* `cd contrib/ise_library/cURL`
|
||||
* `geant compile`
|
||||
|
||||
## Libraries under 'library'
|
||||
|
||||
@@ -62,7 +64,7 @@ Using git
|
||||
* connectors: various web server connectors for EWSGI
|
||||
* libfcgi: Wrapper for libfcgi SDK
|
||||
* __wsf__: Web Server Framework [read more](library/server/wsf)
|
||||
* __router__: URL dispatching/routing based on uri, uri_template, or custom [read more](library/server/wsf/router)
|
||||
* __router__: URL dispatching/routing based on `uri`, `uri_template`, or custom [read more](library/server/wsf/router)
|
||||
|
||||
### protocol
|
||||
* __http__: HTTP related classes, constants for status code, content types, ... [read more](library/network/protocol/http)
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<?xml version="1.0" encoding="ISO-8859-1"?>
|
||||
<redirection xmlns="http://www.eiffel.com/developers/xml/configuration-1-16-0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.eiffel.com/developers/xml/configuration-1-16-0 http://www.eiffel.com/developers/xml/configuration-1-16-0.xsd" uuid="B1D3254D-A58E-4259-9796-8A2843A511A9" message="Obsolete: use web_server.ecf !" location="web_server.ecf">
|
||||
</redirection>
|
||||
<?xml version="1.0" encoding="ISO-8859-1"?>
|
||||
<redirection xmlns="http://www.eiffel.com/developers/xml/configuration-1-16-0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.eiffel.com/developers/xml/configuration-1-16-0 http://www.eiffel.com/developers/xml/configuration-1-16-0.xsd" uuid="B1D3254D-A58E-4259-9796-8A2843A511A9" message="Obsolete: use web_server.ecf !" location="web_server.ecf">
|
||||
</redirection>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package nino
|
||||
|
||||
project
|
||||
nino = "nino-safe.ecf"
|
||||
nino = "nino.ecf"
|
||||
|
||||
note
|
||||
|
||||
@@ -14,3 +14,9 @@ port: 9000
|
||||
repo: https://github.com/EiffelWebFramework/EWF
|
||||
version: v1
|
||||
download: https://github.com/EiffelWebFramework/EWF/archive/v1.zip
|
||||
|
||||
plugins:
|
||||
- jekyll-relative-links
|
||||
relative_links:
|
||||
enabled: true
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 9.3 KiB |
@@ -59,7 +59,7 @@ feature -- Basic operations
|
||||
end
|
||||
```
|
||||
|
||||
When using the "standalone" connector (or the deprecated "nino" connector), by default the service listens on port 80, but often this port is already used by other applications, so it is recommended to use another port.
|
||||
When using the [standalone](../connectors/standalone.md) connector (or the deprecated "nino" connector), by default the service listens on port 80, but often this port is already used by other applications, so it is recommended to use another port.
|
||||
To define another port, redefine the feature `initialize` and set up a new port number using the service options (see below).
|
||||
|
||||
|
||||
@@ -86,17 +86,20 @@ feature {NONE} -- Initialization
|
||||
end
|
||||
```
|
||||
|
||||
Learn more about the [Standalone](../connectors/standalone.md) connector.
|
||||
|
||||
The **WSF_REQUEST** gives access to the incoming data; the class provides features to get information such as request method, form data, query parameters, uploaded files, HTTP request headers, and hostname of the client among others.
|
||||
|
||||
The **WSF_RESPONSE** provides features to define the response with information such as HTTP status codes (10x,20x, 30x, 40x, and 50x), response headers (Content-Type, Content-Length, etc.) and obviously the body of the message itself.
|
||||
|
||||
**APPLICATION** is the root class of our example, it launches the application, using the corresponding connector, Which connector? this depends how do you want to run it `cgi`, `fcgi`, `standalone`. For development is recommended to use a standalone web server written in Eiffel, and run the execution within the EiffelStudio debugger. For production fcgi (or cgi) using Apache or another popular web server.
|
||||
|
||||

|
||||

|
||||
|
||||
**WS_LAUNCHABLE_SERVICE** inherit from **WS_SERVICE** class, which is a marker interface in EWF. And also provides a way to launch our application using different kind of connectors. The class **WSF_DEFAULT_SERVICE_I**, inherit from **WS_LAUNCHABLE_SERVICE** and has a formal generic that should conform to **WSF_SERVICE_LAUNCHER [WSF_EXECUTION]**. Below a [BON diagram](http://www.bon-method.com/index_normal.htm) showing one of the possible options.
|
||||
|
||||

|
||||

|
||||
|
||||
Other connectors:
|
||||
|
||||
**WSF_STANDALONE_SERVICE_LAUNCHER**
|
||||
@@ -109,7 +112,7 @@ The **APPLICATION_EXECUTION** class inherits from **WSF_EXECUTION** interface,
|
||||
|
||||
In the **APPLICATION_EXECUTION** class class you will need to implement the **execute** feature, get data from the request *req* and write the response in *res*.
|
||||
|
||||

|
||||

|
||||
|
||||
The WSF_EXECUTION instance, in this case ```APPLICATION_EXECUTION``` is created per request, with two main attributes request: ```WSF_REQUEST``` and response: ```WSF_RESPONSE```.
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
BIN
docs/workbook/basics/images/APPLICATION_EXECUTION.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
docs/workbook/basics/images/Launcher_Hierarchy.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
docs/workbook/basics/images/WSF_SERVICE_LAUNCHER_CGI.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
docs/workbook/basics/images/WSF_SERVICE_LAUNCHER_FCGI.png
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
docs/workbook/basics/images/WSF_SERVICE_LAUNCHER_STANDALONE.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
27
docs/workbook/connectors/standalone.md
Normal file
@@ -0,0 +1,27 @@
|
||||
Nav: [Workbook](../workbook.md)
|
||||
|
||||
## The EiffelWeb standalone connector
|
||||
|
||||
It provides a standalone httpd server for the EiffelWeb framework.
|
||||
It implements HTTP/1.1 with persistent connection, concurrent connection, ...
|
||||
|
||||
To easily set the standalone connector, see class `WSF_STANDALONE_SERVICE_OPTIONS`.
|
||||
|
||||
### Main settings:
|
||||
|
||||
* `port`: Listening port number (defaut: 80).
|
||||
* `max_concurrent_connections`: maximum of concurrent connections (default: 100)
|
||||
* `max_tcp_clients`: Listen on socket for at most `max_tcp_clients` connections (default: 100)
|
||||
* `socket_timeout`: Amount of seconds the server waits for receipts and transmissions during communications. With timeout of 0, socket can wait for ever. (default: 60)
|
||||
* `socket_recv_timeout`: Amount of seconds the server waits for receiving data during communications. With timeout of 0, socket can waits for ever. (default: 5)
|
||||
* `keep_alive_timeout`: Persistent connection timeout. Number of seconds the server waits after a request has been served before it closes the connection (default: 5)
|
||||
* `max_keep_alive_requests`: Maximum number of requests allowed per persistent connection. To disable KeepAlive, set `max_keep_alive_requests` to `0`. To have no limit, set `max_keep_alive_requests` to `-1` (default: 300).
|
||||
|
||||
* `is_secure`: check SSL certificate?
|
||||
* `secure_certificate`: path to SSL certificate.
|
||||
* `secure_certificate_key`: certificate key
|
||||
|
||||
* `verbose`: display verbose output (Default: false)
|
||||
|
||||
See also `WGI_STANDALONE_CONSTANTS` for default values.
|
||||
|
||||
@@ -113,14 +113,13 @@ feature -- Implementation
|
||||
across
|
||||
l_values as c
|
||||
loop
|
||||
s.replace_substring_all ({STRING_32} "${" + c.key.as_string_32 + "}", c.item)
|
||||
s.replace_substring_all ({STRING_32} "${" + c.key.to_string_32 + "}", c.item.to_string_32)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
note
|
||||
copyright: "2011-2013, Jocelyn Fiat, Javier Velilla, Olivier Ligot, Eiffel Software and others"
|
||||
copyright: "2011-2017, 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
|
||||
|
||||
50
examples/docker/Dockerfile
Normal file
@@ -0,0 +1,50 @@
|
||||
FROM debian
|
||||
#ubuntu:xenial
|
||||
MAINTAINER Jocelyn Fiat <jfiat@eiffel.com>
|
||||
LABEL description="EiffelWeb debug example hosted using apache2+libfcgi"
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get -y install \
|
||||
curl bzip2 make gcc git-core \
|
||||
apache2 libapache2-mod-fcgid libfcgi-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
#RUN apt-get update && apt-get -y install tmux git-all vim && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
RUN a2enmod rewrite suexec include fcgid
|
||||
|
||||
RUN export uid=1000 gid=1000 && \
|
||||
mkdir -p /home/eifweb && \
|
||||
echo "eifweb:x:${uid}:${gid}:eifweb,,,:/home/eifweb:/bin/bash" >> /etc/passwd && \
|
||||
echo "eifweb:x:${uid}:" >> /etc/group && \
|
||||
chown ${uid}:${gid} -R /home/eifweb
|
||||
|
||||
USER eifweb
|
||||
ENV HOME /home/eifweb
|
||||
ENV WEBDIR /home/eifweb/www
|
||||
WORKDIR $HOME
|
||||
|
||||
# Create expected folders
|
||||
RUN mkdir $WEBDIR
|
||||
|
||||
#Build the debug EiffelWeb example and copy the executable to $HOME/
|
||||
COPY files/build_service_fcgi $HOME/build_service_fcgi
|
||||
USER root
|
||||
RUN chown eifweb:eifweb $HOME/build_service_fcgi && chmod 700 $HOME/build_service_fcgi
|
||||
USER eifweb
|
||||
RUN $HOME/build_service_fcgi $HOME/www
|
||||
|
||||
USER root
|
||||
COPY ./files/httpd.conf /etc/apache2/sites-enabled/000-default.conf
|
||||
COPY ./files/html/.htaccess $WEBDIR/html/.htaccess
|
||||
COPY ./files/html/index.html $WEBDIR/html/index.html
|
||||
RUN echo > $WEBDIR/html/service.ews
|
||||
RUN chown www-data:www-data -R $WEBDIR
|
||||
|
||||
#Setup apache as foreground (for docker purpose)
|
||||
RUN mkdir -p /etc/service/apache
|
||||
ADD ./files/apache.sh /etc/service/apache/run
|
||||
RUN chmod +x /etc/service/apache/run
|
||||
ENTRYPOINT ["/etc/service/apache/run"]
|
||||
25
examples/docker/README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
Docker container for EiffelWeb debug example with apache2+libfcgi.
|
||||
==================================================================
|
||||
|
||||
This example demonstrates the use of EiffelWeb with Apache2, using the libfcgi connector.
|
||||
For that, it is using the Docker solution to show the compilation and configuration steps.
|
||||
|
||||
To build the docker image:
|
||||
|
||||
```
|
||||
docker build -t local/ewf-debug-httpd .
|
||||
```
|
||||
|
||||
To run the docker image in a self-destroyed container:
|
||||
```
|
||||
docker run --rm -dit -p 8080:80 --name my-ewf-debug local/ewf-debug-httpd
|
||||
```
|
||||
|
||||
Notes:
|
||||
- `--rm` : to remove the container after the execution
|
||||
- `-p 8080:80` : to map internal listening port 80 to localhost port 8080.
|
||||
- on Linux, you may need to use `sudo` to be able to use `docker`
|
||||
- depending on the docker installation, you may need to add an extra `-e ISE_PLATFORM=linux-x86` to force execution in 32bits.
|
||||
- This docker example is simple on purpose. For production it should be improved and optimized to keep the image smaller and more customizable.
|
||||
- For more advanced Docker usage, please refer to official https://www.docker.com/ website.
|
||||
|
||||
3
examples/docker/files/apache.sh
Normal file
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
exec /usr/sbin/apache2ctl -D FOREGROUND
|
||||
25
examples/docker/files/build_service_fcgi
Normal file
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
|
||||
web_dir=$1
|
||||
|
||||
mkdir $web_dir/bin
|
||||
mkdir $web_dir/html
|
||||
|
||||
# Install latest EiffelStudio suite.
|
||||
curl -sSL https://www.eiffel.org/setup/install.sh > install_eiffel.sh \
|
||||
&& bash ./install_eiffel.sh latest > $HOME/eiffel.rc
|
||||
|
||||
# Setup Eiffel environment
|
||||
source $HOME/eiffel.rc
|
||||
|
||||
# Get source code
|
||||
git clone https://github.com/EiffelWebFramework/EWF.git $web_dir/src
|
||||
|
||||
# Build executable
|
||||
eiffel build -v --target debug_libfcgi $web_dir/src/examples/debug/debug.ecf $web_dir/html/service.fcgi
|
||||
|
||||
# Clean files
|
||||
rm -rf $ISE_EIFFEL
|
||||
rm -rf $web_dir/src
|
||||
|
||||
exit $?
|
||||
20
examples/docker/files/html/.htaccess
Normal file
@@ -0,0 +1,20 @@
|
||||
<IfModule mod_fcgid.c>
|
||||
AddHandler fcgid-script .ews
|
||||
FcgidWrapper /home/eifweb/www/html/service.fcgi .ews
|
||||
</IfModule>
|
||||
|
||||
Options +ExecCGI +Includes +FollowSymLinks
|
||||
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine on
|
||||
|
||||
#RewriteRule ^.*$ maintenance.html [L]
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule ^$ service.ews/
|
||||
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_URI} !=/favicon.ico
|
||||
RewriteRule ^(.*)$ service.ews/$1 [C]
|
||||
|
||||
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization},L]
|
||||
</IfModule>
|
||||
19
examples/docker/files/html/index.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<title>EiffelWeb running on Apache2+libfcgi</title>
|
||||
</head>
|
||||
<body>
|
||||
EiffelWeb debug example hosted on apache2+libfcgi .
|
||||
<ul>
|
||||
<li>Send a GET request <a href="get?foo=bar">get?foo=bar</a></li>
|
||||
<li>Send a POST request <form action="post?foo=bar" method="POST">
|
||||
<input type="text" name="one" value="value_1"/>
|
||||
<input type="text" name="two" value="value_2"/>
|
||||
<input type="submit" value="Submit"/>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
53
examples/docker/files/httpd.conf
Normal file
@@ -0,0 +1,53 @@
|
||||
<IfModule mod_fcgid.c>
|
||||
FcgidIdleTimeout 60
|
||||
FcgidBusyScanInterval 120
|
||||
FcgidProcessLifeTime 1800
|
||||
#7200
|
||||
FcgidMaxProcesses 10
|
||||
FcgidMaxProcessesPerClass 100
|
||||
FcgidMinProcessesPerClass 100
|
||||
FcgidConnectTimeout 8
|
||||
FcgidIOTimeout 3000
|
||||
FcgidBusyTimeout 300
|
||||
FcgidPassHeader Authorization
|
||||
FcgidMaxRequestLen 10000000
|
||||
FcgidMaxRequestsPerProcess 100
|
||||
</IfModule>
|
||||
|
||||
<VirtualHost *:80>
|
||||
# The ServerName directive sets the request scheme, hostname and port that
|
||||
# the server uses to identify itself. This is used when creating
|
||||
# redirection URLs. In the context of virtual hosts, the ServerName
|
||||
# specifies what hostname must appear in the request's Host: header to
|
||||
# match this virtual host. For the default virtual host (this file) this
|
||||
# value is not decisive as it is used as a last resort host regardless.
|
||||
# However, you must set it for any further virtual host explicitly.
|
||||
#ServerName www.example.com
|
||||
|
||||
ServerAdmin webmaster@localhost
|
||||
DocumentRoot /home/eifweb/www/html
|
||||
|
||||
<Directory /home/eifweb/www/html>
|
||||
Options Indexes FollowSymLinks MultiViews
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
# Available loglevels: trace8, ..., trace1, debug, info, notice, warn,
|
||||
# error, crit, alert, emerg.
|
||||
# It is also possible to configure the loglevel for particular
|
||||
# modules, e.g.
|
||||
#LogLevel info ssl:warn
|
||||
|
||||
ErrorLog ${APACHE_LOG_DIR}/error.log
|
||||
CustomLog ${APACHE_LOG_DIR}/access.log combined
|
||||
|
||||
# For most configuration files from conf-available/, which are
|
||||
# enabled or disabled at a global level, it is possible to
|
||||
# include a line for only one particular virtual host. For example the
|
||||
# following line enables the CGI configuration for this host only
|
||||
# after it has been globally disabled with "a2disconf".
|
||||
#Include conf-available/serve-cgi-bin.conf
|
||||
|
||||
|
||||
</VirtualHost>
|
||||
@@ -1,6 +0,0 @@
|
||||
Obsolete example based on EWF v0
|
||||
================================
|
||||
|
||||
Those examples are using the old EWF v0 interface, including use of deprecated "nino" connector.
|
||||
They serves as example for existing project based on EWF v0, but willing to use latest EiffelStudio, and latest EWF repository.
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
<?xml version="1.0" encoding="ISO-8859-1"?>
|
||||
<system xmlns="http://www.eiffel.com/developers/xml/configuration-1-16-0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.eiffel.com/developers/xml/configuration-1-16-0 http://www.eiffel.com/developers/xml/configuration-1-16-0.xsd" name="filter" uuid="52FF4B77-0614-4D8B-9B96-C07EC852793E">
|
||||
<target name="common" abstract="true">
|
||||
<file_rule>
|
||||
<exclude>/EIFGENs$</exclude>
|
||||
<exclude>/\.git$</exclude>
|
||||
<exclude>/\.svn$</exclude>
|
||||
</file_rule>
|
||||
<capability>
|
||||
<concurrency support="thread" use="thread"/>
|
||||
</capability>
|
||||
<option debug="true" warning="true">
|
||||
<debug name="nino" enabled="true"/>
|
||||
<assertions precondition="true" postcondition="true" invariant="true" supplier_precondition="true"/>
|
||||
</option>
|
||||
<library name="base" location="$ISE_LIBRARY\library\base\base.ecf" readonly="true"/>
|
||||
<library name="http" location="..\..\..\..\library\network\protocol\http\http.ecf" readonly="true"/>
|
||||
<library name="http_authorization" location="..\..\..\..\library\server\authentication\http_authorization\http_authorization.ecf" readonly="true"/>
|
||||
<library name="json" location="$ISE_LIBRARY\contrib\library\text\parser\json\library\json.ecf"/>
|
||||
<library name="net" location="$ISE_LIBRARY\library\net\net.ecf" readonly="true"/>
|
||||
<library name="wsf" location="..\..\..\..\library\server\obsolete\v0\wsf\wsf.ecf" readonly="true"/>
|
||||
<library name="wsf_extension" location="..\..\..\..\library\server\obsolete\v0\wsf\wsf_extension.ecf" readonly="true"/>
|
||||
<library name="wsf_router_context" location="..\..\..\..\library\server\obsolete\v0\wsf\wsf_router_context.ecf" readonly="true"/>
|
||||
</target>
|
||||
<target name="filter_nino" extends="common">
|
||||
<root class="FILTER_SERVER" feature="make"/>
|
||||
<capability>
|
||||
<concurrency support="thread" use="thread"/>
|
||||
</capability>
|
||||
<library name="default_nino" location="..\..\..\..\library\server\obsolete\v0\wsf\default\nino.ecf" readonly="true"/>
|
||||
<cluster name="filter" location="src\" recursive="true"/>
|
||||
</target>
|
||||
<target name="filter_fcgi" extends="common">
|
||||
<root class="FILTER_SERVER" feature="make"/>
|
||||
<capability>
|
||||
<concurrency support="thread" use="thread"/>
|
||||
</capability>
|
||||
<library name="default_libfcgi" location="..\..\..\..\library\server\obsolete\v0\wsf\default\libfcgi.ecf"/>
|
||||
<cluster name="filter" location="src\" recursive="true"/>
|
||||
</target>
|
||||
</system>
|
||||
@@ -1,4 +0,0 @@
|
||||
${NOTE_KEYWORD}
|
||||
copyright: "2011-${YEAR}, Olivier Ligot, Jocelyn Fiat and others"
|
||||
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
Filter example
|
||||
|
||||
To test the example, you can just run in a terminal:
|
||||
> curl -u foo:bar http://localhost:9090/user/1 -v
|
||||
@@ -1,57 +0,0 @@
|
||||
note
|
||||
description: "Summary description for {DATABASE_API}."
|
||||
author: ""
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
class
|
||||
DATABASE_API
|
||||
create
|
||||
make
|
||||
|
||||
feature -- Initialization
|
||||
|
||||
make
|
||||
local
|
||||
l_user: USER
|
||||
do
|
||||
create users.make (10)
|
||||
create l_user.make (1, "foo", "bar")
|
||||
users.put (l_user, l_user.id)
|
||||
create l_user.make (2, "demo", "demo")
|
||||
users.put (l_user, l_user.id)
|
||||
end
|
||||
|
||||
feature -- Access
|
||||
|
||||
user (a_id: INTEGER; a_name: detachable READABLE_STRING_GENERAL): detachable USER
|
||||
-- User with id `a_id' or name `a_name'.
|
||||
require
|
||||
a_id > 0 xor a_name /= Void
|
||||
local
|
||||
n: like {USER}.name
|
||||
do
|
||||
if a_id > 0 then
|
||||
Result := users.item (a_id)
|
||||
elseif a_name /= Void then
|
||||
n := a_name.as_string_8
|
||||
across
|
||||
users as c
|
||||
until
|
||||
Result /= Void
|
||||
loop
|
||||
if attached c.item as u and then u.name.same_string (n) then
|
||||
Result := u
|
||||
end
|
||||
end
|
||||
end
|
||||
ensure
|
||||
Result /= Void implies ((a_id > 0 and then Result.id = a_id) xor (a_name /= Void and then Result.name.same_string_general (a_name)))
|
||||
end
|
||||
|
||||
users: HASH_TABLE [USER, INTEGER]
|
||||
|
||||
;note
|
||||
copyright: "2011-2012, Olivier Ligot, Jocelyn Fiat and others"
|
||||
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
|
||||
end
|
||||
@@ -1,20 +0,0 @@
|
||||
note
|
||||
description: "Summary description for {SHARED_DATABASE_API}."
|
||||
author: ""
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
class
|
||||
SHARED_DATABASE_API
|
||||
|
||||
feature -- Access
|
||||
|
||||
db_access: DATABASE_API
|
||||
once
|
||||
create Result.make
|
||||
end
|
||||
|
||||
note
|
||||
copyright: "2011-2012, Olivier Ligot, Jocelyn Fiat and others"
|
||||
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
|
||||
end
|
||||
@@ -1,52 +0,0 @@
|
||||
note
|
||||
description: "JSON user converter."
|
||||
author: "Olivier Ligot"
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
class
|
||||
JSON_USER_CONVERTER
|
||||
|
||||
inherit
|
||||
JSON_CONVERTER
|
||||
|
||||
create
|
||||
make
|
||||
|
||||
feature {NONE} -- Initialization
|
||||
|
||||
make
|
||||
do
|
||||
create object.make (0, "", "")
|
||||
end
|
||||
|
||||
feature -- Access
|
||||
|
||||
object: USER
|
||||
|
||||
value: detachable JSON_OBJECT
|
||||
|
||||
feature -- Conversion
|
||||
|
||||
from_json (j: attached like value): detachable like object
|
||||
-- Convert from JSON value.
|
||||
do
|
||||
end
|
||||
|
||||
to_json (o: like object): like value
|
||||
-- Convert to JSON value.
|
||||
do
|
||||
create Result.make
|
||||
Result.put (json.value (o.id), id_key)
|
||||
Result.put (json.value (o.name), name_key)
|
||||
end
|
||||
|
||||
feature {NONE} -- Implementation
|
||||
|
||||
id_key: STRING = "id"
|
||||
name_key: STRING = "name"
|
||||
|
||||
note
|
||||
copyright: "2011-2012, Olivier Ligot, Jocelyn Fiat and others"
|
||||
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
|
||||
end
|
||||
@@ -1,59 +0,0 @@
|
||||
note
|
||||
description: "User."
|
||||
author: ""
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
class
|
||||
USER
|
||||
|
||||
inherit
|
||||
ANY
|
||||
redefine
|
||||
is_equal
|
||||
end
|
||||
|
||||
create
|
||||
make
|
||||
|
||||
feature {NONE} -- Initialization
|
||||
|
||||
make (an_id: INTEGER; a_name, a_password: STRING)
|
||||
do
|
||||
id := an_id
|
||||
name := a_name
|
||||
password := a_password
|
||||
ensure
|
||||
id_set: id = an_id
|
||||
name_set: name = a_name
|
||||
password_set: password = a_password
|
||||
end
|
||||
|
||||
feature -- Access
|
||||
|
||||
id: INTEGER
|
||||
-- Identifier
|
||||
|
||||
name: STRING
|
||||
-- Name
|
||||
|
||||
password: STRING
|
||||
-- Password
|
||||
|
||||
feature -- Comparison
|
||||
|
||||
is_equal (other: like Current): BOOLEAN
|
||||
-- Is `other' attached to an object considered
|
||||
-- equal to current object?
|
||||
do
|
||||
if Current = other then
|
||||
Result := True
|
||||
else
|
||||
Result := (id = other.id) and (name = other.name) and (password = other.password)
|
||||
end
|
||||
end
|
||||
|
||||
;note
|
||||
copyright: "2011-2012, Olivier Ligot, Jocelyn Fiat and others"
|
||||
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
|
||||
end
|
||||
@@ -1,65 +0,0 @@
|
||||
note
|
||||
description: "Authentication filter."
|
||||
author: "Olivier Ligot"
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
class
|
||||
AUTHENTICATION_FILTER
|
||||
|
||||
inherit
|
||||
WSF_FILTER
|
||||
|
||||
WSF_URI_TEMPLATE_HANDLER
|
||||
|
||||
SHARED_DATABASE_API
|
||||
|
||||
SHARED_EJSON
|
||||
|
||||
feature -- Basic operations
|
||||
|
||||
execute (req: WSF_REQUEST; res: WSF_RESPONSE)
|
||||
-- Execute the filter
|
||||
local
|
||||
l_auth: detachable HTTP_AUTHORIZATION
|
||||
do
|
||||
if attached req.http_authorization as l_http_authorization then
|
||||
create l_auth.make (l_http_authorization)
|
||||
end
|
||||
if
|
||||
l_auth /= Void and then
|
||||
l_auth.is_basic and then
|
||||
attached l_auth.login as l_auth_login and then
|
||||
attached Db_access.user (0, l_auth_login) as l_user and then
|
||||
l_auth_login.same_string (l_user.name) and then
|
||||
attached l_auth.password as l_auth_password and then
|
||||
l_auth_password.same_string (l_user.password)
|
||||
then
|
||||
req.set_execution_variable ("user", l_user)
|
||||
execute_next (req, res)
|
||||
else
|
||||
handle_unauthorized ("Unauthorized", req, res)
|
||||
end
|
||||
end
|
||||
|
||||
feature {NONE} -- Implementation
|
||||
|
||||
handle_unauthorized (a_description: STRING; req: WSF_REQUEST; res: WSF_RESPONSE)
|
||||
-- Handle forbidden.
|
||||
local
|
||||
h: HTTP_HEADER
|
||||
do
|
||||
create h.make
|
||||
h.put_content_type_text_plain
|
||||
h.put_content_length (a_description.count)
|
||||
h.put_current_date
|
||||
h.put_header_key_value ({HTTP_HEADER_NAMES}.header_www_authenticate, "Basic realm=%"User%"")
|
||||
res.set_status_code ({HTTP_STATUS_CODE}.unauthorized)
|
||||
res.put_header_text (h.string)
|
||||
res.put_string (a_description)
|
||||
end
|
||||
|
||||
note
|
||||
copyright: "2011-2014, Olivier Ligot, Jocelyn Fiat and others"
|
||||
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
|
||||
end
|
||||
@@ -1,34 +0,0 @@
|
||||
note
|
||||
description: "Summary description for {FILTER_HANDLER_CONTEXT}."
|
||||
author: ""
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
class
|
||||
FILTER_HANDLER_CONTEXT
|
||||
|
||||
inherit
|
||||
WSF_HANDLER_CONTEXT
|
||||
|
||||
create
|
||||
make
|
||||
|
||||
feature -- Access
|
||||
|
||||
user: detachable USER
|
||||
-- Authenticated user
|
||||
|
||||
feature -- Element change
|
||||
|
||||
set_user (a_user: USER)
|
||||
-- Set `user' to `a_user'
|
||||
do
|
||||
user := a_user
|
||||
ensure
|
||||
user_set: user = a_user
|
||||
end
|
||||
|
||||
note
|
||||
copyright: "2011-2012, Olivier Ligot, Jocelyn Fiat and others"
|
||||
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
|
||||
end
|
||||
@@ -1,111 +0,0 @@
|
||||
note
|
||||
description : "Filter example."
|
||||
author : "Olivier Ligot"
|
||||
date : "$Date$"
|
||||
revision : "$Revision$"
|
||||
|
||||
class
|
||||
FILTER_SERVER
|
||||
|
||||
inherit
|
||||
ANY
|
||||
|
||||
WSF_DEFAULT_SERVICE
|
||||
|
||||
WSF_ROUTED_SERVICE
|
||||
undefine
|
||||
execute
|
||||
end
|
||||
|
||||
WSF_FILTERED_SERVICE
|
||||
|
||||
SHARED_EJSON
|
||||
|
||||
create
|
||||
make
|
||||
|
||||
feature {NONE} -- Initialization
|
||||
|
||||
make
|
||||
local
|
||||
l_message: STRING
|
||||
l_factory: INET_ADDRESS_FACTORY
|
||||
do
|
||||
initialize_router
|
||||
initialize_filter
|
||||
initialize_json
|
||||
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
|
||||
end
|
||||
|
||||
create_filter
|
||||
-- Create `filter'
|
||||
do
|
||||
create {WSF_CORS_FILTER} filter
|
||||
end
|
||||
|
||||
setup_filter
|
||||
-- Setup `filter'
|
||||
local
|
||||
l_routing_filter: WSF_ROUTING_FILTER
|
||||
l_logging_filter: WSF_LOGGING_FILTER
|
||||
do
|
||||
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
|
||||
l_routing_filter.set_next (l_logging_filter)
|
||||
end
|
||||
|
||||
setup_router
|
||||
-- Setup `router'
|
||||
local
|
||||
l_options_filter: WSF_CORS_OPTIONS_FILTER
|
||||
l_authentication_filter: AUTHENTICATION_FILTER
|
||||
l_user_filter: USER_HANDLER
|
||||
l_methods: WSF_REQUEST_METHODS
|
||||
do
|
||||
create l_options_filter.make (router)
|
||||
create l_authentication_filter
|
||||
create l_user_filter
|
||||
|
||||
l_options_filter.set_next (l_authentication_filter)
|
||||
l_authentication_filter.set_next (l_user_filter)
|
||||
|
||||
create l_methods
|
||||
l_methods.enable_options
|
||||
l_methods.enable_get
|
||||
router.handle ("/user/{userid}", create {WSF_URI_TEMPLATE_AGENT_HANDLER}.make (agent l_options_filter.execute), l_methods)
|
||||
end
|
||||
|
||||
initialize_json
|
||||
-- Initialize `json'.
|
||||
do
|
||||
json.add_converter (create {JSON_USER_CONVERTER}.make)
|
||||
end
|
||||
|
||||
feature {NONE} -- Implementation
|
||||
|
||||
port: INTEGER = 9090
|
||||
-- Port number
|
||||
|
||||
note
|
||||
copyright: "2011-2017, Olivier Ligot, Jocelyn Fiat 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
|
||||
@@ -1,97 +0,0 @@
|
||||
note
|
||||
description: "User handler."
|
||||
author: "Olivier Ligot"
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
class
|
||||
USER_HANDLER
|
||||
|
||||
inherit
|
||||
WSF_FILTER
|
||||
|
||||
WSF_URI_TEMPLATE_HANDLER
|
||||
|
||||
WSF_RESOURCE_HANDLER_HELPER
|
||||
redefine
|
||||
do_get
|
||||
end
|
||||
|
||||
SHARED_DATABASE_API
|
||||
|
||||
SHARED_EJSON
|
||||
|
||||
feature -- Basic operations
|
||||
|
||||
execute (req: WSF_REQUEST; res: WSF_RESPONSE)
|
||||
-- Execute request handler
|
||||
do
|
||||
execute_methods (req, res)
|
||||
execute_next (req, res)
|
||||
end
|
||||
|
||||
do_get (req: WSF_REQUEST; res: WSF_RESPONSE)
|
||||
-- Using GET to retrieve resource information.
|
||||
-- If the GET request is SUCCESS, we response with
|
||||
-- 200 OK, and a representation of the user
|
||||
-- If the GET request is not SUCCESS, we response with
|
||||
-- 404 Resource not found
|
||||
require else
|
||||
authenticated_user_attached: attached {USER} req.execution_variable ("user")
|
||||
local
|
||||
id : STRING
|
||||
do
|
||||
if attached req.orig_path_info as orig_path then
|
||||
id := get_user_id_from_path (orig_path.as_string_32)
|
||||
if attached retrieve_user (id) as l_user then
|
||||
if l_user ~ req.execution_variable ("user") then
|
||||
compute_response_get (req, res, l_user)
|
||||
elseif attached {USER} req.execution_variable ("user") as l_auth_user then
|
||||
-- Trying to access another user that the authenticated one,
|
||||
-- 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)
|
||||
end
|
||||
else
|
||||
handle_resource_not_found_response ("The following resource " + orig_path + " is not found ", req, res)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
feature {NONE} -- Implementation
|
||||
|
||||
compute_response_get (req: WSF_REQUEST; res: WSF_RESPONSE; l_user : USER)
|
||||
local
|
||||
h: HTTP_HEADER
|
||||
l_msg : STRING
|
||||
do
|
||||
create h.make
|
||||
h.put_content_type_application_json
|
||||
if attached {JSON_VALUE} json.value (l_user) as jv then
|
||||
l_msg := jv.representation
|
||||
h.put_content_length (l_msg.count)
|
||||
if attached req.request_time as time then
|
||||
h.put_utc_date (time)
|
||||
end
|
||||
res.set_status_code ({HTTP_STATUS_CODE}.ok)
|
||||
res.put_header_text (h.string)
|
||||
res.put_string (l_msg)
|
||||
end
|
||||
end
|
||||
|
||||
get_user_id_from_path (a_path: READABLE_STRING_32): STRING
|
||||
do
|
||||
Result := a_path.split ('/').at (3)
|
||||
end
|
||||
|
||||
retrieve_user (id: STRING) : detachable USER
|
||||
-- Retrieve the user by id if it exist, in other case, Void
|
||||
do
|
||||
if id.is_integer and then Db_access.users.has (id.to_integer) then
|
||||
Result := db_access.users.item (id.to_integer)
|
||||
end
|
||||
end
|
||||
|
||||
note
|
||||
copyright: "2011-2017, Olivier Ligot, Jocelyn Fiat and others"
|
||||
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
|
||||
end
|
||||
@@ -1,3 +0,0 @@
|
||||
The current example has a main target for the server: "restbucks"
|
||||
But we also provide "policy_driven_restbucks" target which is using the
|
||||
policy-driven framework than help coder fulfill HTTP expectations.
|
||||
@@ -1,11 +0,0 @@
|
||||
Make sure to have the Clib generated in the related cURL library
|
||||
|
||||
- if you use EiffelStudio >= 7.0
|
||||
check %ISE_LIBRARY%\library\cURL\spec\%ISE_C_COMPILER%\$ISE_PLATFORM
|
||||
or $ISE_LIBRARY/library/cURL/spec/$ISE_PLATFORM
|
||||
|
||||
- otherwise if you use earlier version
|
||||
check under ext/ise_library/curl/spec/...
|
||||
|
||||
And on Windows, be sure to get the libcurl.dll from %ISE_LIBRARY%\studio\spec\%ISE_PLATFORM%\bin\libcurl.dll
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="ISO-8859-1"?>
|
||||
<system xmlns="http://www.eiffel.com/developers/xml/configuration-1-16-0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.eiffel.com/developers/xml/configuration-1-16-0 http://www.eiffel.com/developers/xml/configuration-1-16-0.xsd" name="client" uuid="D0059CEB-5F5C-4D21-8C71-842BD0F88468">
|
||||
<target name="client">
|
||||
<root class="RESTBUCK_CLIENT" feature="make"/>
|
||||
<file_rule>
|
||||
<exclude>/\.git$</exclude>
|
||||
<exclude>/\.svn$</exclude>
|
||||
<exclude>/EIFGENs$</exclude>
|
||||
</file_rule>
|
||||
<option warning="true">
|
||||
</option>
|
||||
<capability>
|
||||
<concurrency support="thread" use="thread"/>
|
||||
<void_safety support="all"/>
|
||||
</capability>
|
||||
<library name="base" location="$ISE_LIBRARY\library\base\base.ecf"/>
|
||||
<library name="http_client" location="..\..\..\..\..\library\network\http_client\http_client.ecf" readonly="false"/>
|
||||
<library name="json" location="$ISE_LIBRARY\contrib\library\text\parser\json\library\json.ecf"/>
|
||||
<library name="thread" location="$ISE_LIBRARY\library\thread\thread.ecf"/>
|
||||
<cluster name="src" location=".\src\" recursive="true"/>
|
||||
</target>
|
||||
</system>
|
||||
@@ -1,154 +0,0 @@
|
||||
note
|
||||
description : "Objects that ..."
|
||||
author : "$Author$"
|
||||
date : "$Date$"
|
||||
revision : "$Revision$"
|
||||
|
||||
class
|
||||
RESTBUCK_CLIENT
|
||||
|
||||
create
|
||||
make
|
||||
|
||||
feature {NONE} -- Initialization
|
||||
|
||||
make
|
||||
-- Initialize `Current'.
|
||||
local
|
||||
h: LIBCURL_HTTP_CLIENT
|
||||
sess: HTTP_CLIENT_SESSION
|
||||
resp : detachable HTTP_CLIENT_RESPONSE
|
||||
l_location : detachable READABLE_STRING_8
|
||||
body : STRING
|
||||
do
|
||||
create h.make
|
||||
sess := h.new_session ("http://127.0.0.1:9090")
|
||||
-- Uncomment the following 2 lines, if you use fiddler2 web debugging tool
|
||||
-- sess.set_is_debug (True)
|
||||
-- sess.set_proxy ("127.0.0.1", 8888)
|
||||
|
||||
-- Create Order
|
||||
print ("%N Create Order %N")
|
||||
resp := create_order (sess)
|
||||
|
||||
|
||||
-- Read the Order
|
||||
print ("%N Read Order %N")
|
||||
l_location := resp.header ("Location")
|
||||
resp := read_order (sess, l_location)
|
||||
|
||||
|
||||
-- Update the Order
|
||||
if resp /= Void and then attached resp.body as l_body then
|
||||
body := l_body.as_string_8
|
||||
body.replace_substring_all ("takeAway", "in Shop")
|
||||
print ("%N Update Order %N")
|
||||
resp := update_order (sess, l_location, body)
|
||||
end
|
||||
end
|
||||
|
||||
update_order ( sess: HTTP_CLIENT_SESSION; uri : detachable READABLE_STRING_8; a_body : STRING): detachable HTTP_CLIENT_RESPONSE
|
||||
local
|
||||
context : HTTP_CLIENT_REQUEST_CONTEXT
|
||||
do
|
||||
if attached uri as l_uri then
|
||||
sess.set_base_url (l_uri)
|
||||
create context.make
|
||||
context.headers.put ("application/json", "Content-Type")
|
||||
Result := sess.put ("", context, a_body )
|
||||
-- Show headers
|
||||
across
|
||||
Result.headers as l_headers
|
||||
loop
|
||||
print (l_headers.item.name)
|
||||
print (":")
|
||||
print (l_headers.item.value)
|
||||
io.put_new_line
|
||||
end
|
||||
|
||||
-- Show body
|
||||
print (Result.body)
|
||||
io.put_new_line
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
read_order ( sess: HTTP_CLIENT_SESSION; uri : detachable READABLE_STRING_8): detachable HTTP_CLIENT_RESPONSE
|
||||
do
|
||||
if attached uri as l_uri then
|
||||
sess.set_base_url (l_uri)
|
||||
Result := sess.get ("", Void)
|
||||
-- Show headers
|
||||
across
|
||||
Result.headers as l_headers
|
||||
loop
|
||||
print (l_headers.item.name)
|
||||
print (":")
|
||||
print (l_headers.item.value)
|
||||
io.put_new_line
|
||||
end
|
||||
|
||||
-- Show body
|
||||
print (Result.body)
|
||||
io.put_new_line
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
create_order (sess: HTTP_CLIENT_SESSION) : HTTP_CLIENT_RESPONSE
|
||||
local
|
||||
s: READABLE_STRING_8
|
||||
j: JSON_PARSER
|
||||
id: detachable STRING
|
||||
context : HTTP_CLIENT_REQUEST_CONTEXT
|
||||
do
|
||||
s := "[
|
||||
{
|
||||
"location":"takeAway",
|
||||
"items":[
|
||||
{
|
||||
"name":"Late",
|
||||
"option":"skim",
|
||||
"size":"Small",
|
||||
"quantity":1
|
||||
}
|
||||
]
|
||||
}
|
||||
]"
|
||||
|
||||
create context.make
|
||||
context.headers.put ("application/json", "Content-Type")
|
||||
Result := sess.post ("/order", context, s)
|
||||
-- Show the Headers
|
||||
across
|
||||
Result.headers as l_headers
|
||||
loop
|
||||
print (l_headers.item.name)
|
||||
print (":")
|
||||
print (l_headers.item.value)
|
||||
io.put_new_line
|
||||
end
|
||||
|
||||
|
||||
-- Show the Response body
|
||||
if attached Result.body as m then
|
||||
create j.make_with_string (m)
|
||||
j.parse_content
|
||||
if j.is_valid and then attached j.parsed_json_object as j_o then
|
||||
if attached {JSON_STRING} j_o.item ("id") as l_id then
|
||||
id := l_id.item
|
||||
end
|
||||
print (m)
|
||||
io.put_new_line
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
feature {NONE} -- Implementation
|
||||
|
||||
invariant
|
||||
-- invariant_clause: True
|
||||
|
||||
end
|
||||
@@ -1,4 +0,0 @@
|
||||
${NOTE_KEYWORD}
|
||||
copyright: "2011-${YEAR}, Javier Velilla and others"
|
||||
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
|
||||
|
||||
@@ -1,297 +0,0 @@
|
||||
Restbuck Eiffel Implementation based on the book of REST in Practice
|
||||
====================================================================
|
||||
This is an implementation of CRUD pattern for manipulate resources, this is the first step to use
|
||||
the HTTP protocol as an application protocol instead of a transport protocol.
|
||||
|
||||
Restbuck Protocol
|
||||
-----------------
|
||||
|
||||
<table>
|
||||
<TR><TH>Verb</TH> <TH>URI or template</TH> <TH>Use</TH></TR>
|
||||
<TR><TD>POST</TD> <TD>/order</TD> <TD>Create a new order, and upon success, receive a Locationheader specifying the new order's URI.</TD></TR>
|
||||
<TR><TD>GET</TD> <TD>/order/{orderId}</TD> <TD>Request the current state of the order specified by the URI.</TD></TR>
|
||||
<TR><TD>PUT</TD> <TD>/order/{orderId}</TD> <TD>Update an order at the given URI with new information, providing the full representation.</TD></TR>
|
||||
<TR><TD>DELETE</TD> <TD>/order/{orderId}</TD> <TD>Logically remove the order identified by the given URI.</TD></TR>
|
||||
</table>
|
||||
|
||||
Resource Represenation
|
||||
----------------------
|
||||
The previous tables shows a contrat, the URI or URI template, allows us to indentify resources, now we will chose a
|
||||
representacion, for this particular case we will use JSON.
|
||||
|
||||
Note: <br/>
|
||||
1. *A resource can have multiple URIs*.<br/>
|
||||
2. *A resource can have multiple Representations*.<br/>
|
||||
|
||||
RESTBUCKS_SERVER
|
||||
----------------
|
||||
This class implement the main entry of our REST CRUD service, we are using a default connector (Nino Connector,
|
||||
using a WebServer written in Eiffel).
|
||||
We are inheriting from URI_TEMPLATE_ROUTED_SERVICE, this allows us to map our service contrat, as is shown in the previous
|
||||
table, the mapping is defined in the feature setup_router, this also show that the class ORDER_HANDLER will be encharge
|
||||
of to handle different type of request to the ORDER resource.
|
||||
|
||||
|
||||
class
|
||||
RESTBUCKS_SERVER
|
||||
|
||||
inherit
|
||||
ANY
|
||||
|
||||
URI_TEMPLATE_ROUTED_SERVICE
|
||||
|
||||
DEFAULT_SERVICE
|
||||
-- Here we are using a default connector using the default Nino Connector,
|
||||
-- but it's possible to use other connector (CGI or FCGI).
|
||||
|
||||
create
|
||||
make
|
||||
|
||||
feature {NONE} -- Initialization
|
||||
|
||||
make
|
||||
-- Initialize the router (this will have the request handler and
|
||||
-- their context).
|
||||
do
|
||||
initialize_router
|
||||
make_and_launch
|
||||
end
|
||||
|
||||
create_router
|
||||
do
|
||||
create router.make (2)
|
||||
end
|
||||
|
||||
setup_router
|
||||
local
|
||||
order_handler: ORDER_HANDLER [REQUEST_URI_TEMPLATE_HANDLER_CONTEXT]
|
||||
do
|
||||
create order_handler
|
||||
router.map_with_request_methods ("/order", order_handler, <<"POST">>)
|
||||
router.map_with_request_methods ("/order/{orderid}", order_handler, <<"GET", "DELETE", "PUT">>)
|
||||
end
|
||||
|
||||
feature -- Execution
|
||||
|
||||
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
|
||||
h : HTTP_HEADER
|
||||
l_description : STRING
|
||||
l_api_doc : STRING
|
||||
do
|
||||
if req.content_length_value > 0 then
|
||||
req.input.read_string (req.content_length_value.as_integer_32)
|
||||
end
|
||||
create h.make
|
||||
h.put_status ({HTTP_STATUS_CODE}.method_not_allowed)
|
||||
h.put_content_type_text_plain
|
||||
l_api_doc := "%NPlease check the API%NURI:/order METHOD: POST%NURI:/order/{orderid} METHOD: GET, PUT, DELETE%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.write_header_text (h.string)
|
||||
res.write_string (l_description)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
|
||||
How to Create an order with POST
|
||||
--------------------------------
|
||||
|
||||
Here is the convention that we are using:
|
||||
POST is used for creation and the server determines the URI of the created resource.
|
||||
If the request POST is SUCCESS, the server will create the order and will response with
|
||||
201 CREATED, the Location header will contains the newly created order's URI,
|
||||
if the request POST is not SUCCESS, the server will response with
|
||||
400 BAD REQUEST, the client send a bad request or
|
||||
500 INTERNAL_SERVER_ERROR, when the server can deliver the request.
|
||||
|
||||
POST /order HTTP/1.1
|
||||
Host: 127.0.0.1:8080
|
||||
Connection: keep-alive
|
||||
Content-Length: 196
|
||||
Origin: chrome-extension://fhjcajmcbmldlhcimfajhfbgofnpcjmb
|
||||
Content-Type: application/json
|
||||
Accept: */*
|
||||
Accept-Encoding: gzip,deflate,sdch
|
||||
Accept-Language: es-419,es;q=0.8,en;q=0.6
|
||||
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3
|
||||
|
||||
{
|
||||
"location":"takeAway",
|
||||
"items":[
|
||||
{
|
||||
"name":"Late",
|
||||
"option":"skim",
|
||||
"size":"Small",
|
||||
"quantity":1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Response success
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Status 201 Created
|
||||
Content-Type application/json
|
||||
Content-Length 123
|
||||
Location http://localhost:8080/order/1
|
||||
Date FRI,09 DEC 2011 20:34:20.00 GMT
|
||||
|
||||
{
|
||||
"location" : "takeAway",
|
||||
"status" : "submitted",
|
||||
"items" : [ {
|
||||
"name" : "late",
|
||||
"size" : "small",
|
||||
"quantity" : 1,
|
||||
"option" : "skim"
|
||||
} ]
|
||||
}
|
||||
|
||||
note:
|
||||
curl -vv http://localhost:9090/order -H "Content-Type: application/json" -d "{\"location\":\"takeAway\",\"items\":[{\"name\":\"Late\",\"option\":\"skim\",\"size\":\"Small\",\"quantity\":1}]}" -X POST
|
||||
|
||||
|
||||
How to Read an order with GET
|
||||
-----------------------------
|
||||
Using GET to retrieve resource information.
|
||||
If the GET request is SUCCESS, we response with 200 OK, and a representation of the order
|
||||
If the GET request is not SUCCESS, we response with 404 Resource not found
|
||||
If is a Conditional GET and the resource does not change we send a 304, Resource not modifed
|
||||
|
||||
GET /order/1 HTTP/1.1
|
||||
Host: 127.0.0.1:8080
|
||||
Connection: keep-alive
|
||||
Accept: */*
|
||||
Accept-Encoding: gzip,deflate,sdch
|
||||
Accept-Language: es-419,es;q=0.8,en;q=0.6
|
||||
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3
|
||||
If-None-Match: 6542EF270D91D3EAF39CFB382E4CEBA7
|
||||
|
||||
Response
|
||||
HTTP/1.1 200 OK
|
||||
|
||||
Status 200 OK
|
||||
Content-Type application/json
|
||||
Content-Length 123
|
||||
Date FRI,09 DEC 2011 20:53:46.00 GMT
|
||||
etag 2ED3A40954A95D766FC155682DC8BB52
|
||||
|
||||
{
|
||||
"location" : "takeAway",
|
||||
"status" : "submitted",
|
||||
"items" : [ {
|
||||
"name" : "late",
|
||||
"size" : "small",
|
||||
"quantity" : 1,
|
||||
"option" : "skim"
|
||||
} ]
|
||||
}
|
||||
|
||||
note:
|
||||
curl -vv http://localhost:9090/order/1
|
||||
|
||||
How to Update an order with PUT
|
||||
-------------------------------
|
||||
A successful PUT request will not create a new resource, instead it will change the state of the resource identified by the current uri.
|
||||
If success we response with 200 and the updated order.
|
||||
404 if the order is not found
|
||||
400 in case of a bad request
|
||||
500 internal server error
|
||||
If the request is a Conditional PUT, and it does not mat we response 415, precondition failed.
|
||||
|
||||
Suposse that we had created an Order with the values shown in the _How to create an order with POST_
|
||||
But we change our decision and we want to stay in the shop.
|
||||
|
||||
|
||||
|
||||
PUT /order/1 HTTP/1.1
|
||||
Content-Length: 122
|
||||
Content-Type: application/json; charset=UTF-8
|
||||
Host: localhost:8080
|
||||
Connection: Keep-Alive
|
||||
Expect: 100-Continue
|
||||
|
||||
{
|
||||
"location" : "in shop",
|
||||
"status" : "submitted",
|
||||
"items" : [ {
|
||||
"name" : "late",
|
||||
"size" : "small",
|
||||
"quantity" : 1,
|
||||
"option" : "skim"
|
||||
} ]
|
||||
}
|
||||
|
||||
|
||||
Response success
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Status 200 OK
|
||||
Content-Type application/json
|
||||
Date FRI,09 DEC 2011 21:06:26.00 GMT
|
||||
etag 8767F900674B843E1F3F70BCF3E62403
|
||||
Content-Length 122
|
||||
|
||||
{
|
||||
"location" : "in shop",
|
||||
"status" : "submitted",
|
||||
"items" : [ {
|
||||
"name" : "late",
|
||||
"size" : "small",
|
||||
"quantity" : 1,
|
||||
"option" : "skim"
|
||||
} ]
|
||||
}
|
||||
|
||||
How to Delete an order with DELETE
|
||||
----------------------------------
|
||||
Here we use DELETE to cancel an order, if that order is in state where it can still be canceled.
|
||||
204 if is ok
|
||||
404 Resource not found
|
||||
405 if consumer and service's view of the resouce state is inconsisent
|
||||
500 if we have an internal server error
|
||||
|
||||
|
||||
DELETE /order/1 HTTP/1.1
|
||||
Host: localhost:8080
|
||||
Connection: Keep-Alive
|
||||
|
||||
Response success
|
||||
|
||||
HTTP/1.1 204 No Content
|
||||
|
||||
Status 204 No Content
|
||||
Content-Type application/json
|
||||
Date FRI,09 DEC 2011 21:10:51.00 GMT
|
||||
|
||||
If we want to check that the resource does not exist anymore we can try to retrieve a GET /order/1 and we will receive a
|
||||
404 No Found
|
||||
|
||||
GET /order/1 HTTP/1.1
|
||||
Host: localhost:8080
|
||||
Connection: Keep-Alive
|
||||
|
||||
Response
|
||||
|
||||
HTTP/1.1 404 Not Found
|
||||
|
||||
Status 404 Not Found
|
||||
Content-Type application/json
|
||||
Content-Length 44
|
||||
Date FRI,09 DEC 2011 21:14:17.79 GMT
|
||||
|
||||
The following resource/order/1 is not found
|
||||
|
||||
|
||||
References
|
||||
----------
|
||||
1. [How to get a cup of coffe](http://www.infoq.com/articles/webber-rest-workflow)
|
||||
2. [Rest in Practice] (http://restinpractice.com/default.aspx)
|
||||
@@ -1,55 +0,0 @@
|
||||
<?xml version="1.0" encoding="ISO-8859-1"?>
|
||||
<system xmlns="http://www.eiffel.com/developers/xml/configuration-1-16-0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.eiffel.com/developers/xml/configuration-1-16-0 http://www.eiffel.com/developers/xml/configuration-1-16-0.xsd" name="restbucks" uuid="2773FEAA-448F-410E-BEDE-9298C4749066">
|
||||
<target name="restbucks_common">
|
||||
<file_rule>
|
||||
<exclude>/EIFGENs$</exclude>
|
||||
<exclude>/\.git$</exclude>
|
||||
<exclude>/\.svn$</exclude>
|
||||
</file_rule>
|
||||
<capability>
|
||||
<concurrency support="thread" use="thread"/>
|
||||
</capability>
|
||||
<library name="base" location="$ISE_LIBRARY\library\base\base.ecf"/>
|
||||
<library name="connector_nino" location="..\..\..\..\library\server\obsolete\v0\ewsgi\connectors\nino\nino.ecf" readonly="false">
|
||||
<option debug="true">
|
||||
<debug name="nino" enabled="true"/>
|
||||
</option>
|
||||
</library>
|
||||
<library name="conneg" location="..\..\..\..\library\network\protocol\content_negotiation\conneg.ecf"/>
|
||||
<library name="crypto" location="$ISE_LIBRARY\unstable\library\text\encryption\crypto\crypto.ecf" readonly="false"/>
|
||||
<library name="default_nino" location="..\..\..\..\library\server\obsolete\v0\wsf\default\nino.ecf" readonly="false"/>
|
||||
<library name="encoder" location="..\..\..\..\library\text\encoder\encoder.ecf" readonly="false"/>
|
||||
<library name="http" location="..\..\..\..\library\network\protocol\http\http.ecf" readonly="false"/>
|
||||
<library name="json" location="$ISE_LIBRARY\contrib\library\text\parser\json\library\json.ecf"/>
|
||||
<library name="time" location="$ISE_LIBRARY\library\time\time.ecf"/>
|
||||
<library name="uri" location="$ISE_LIBRARY\library\text\uri\uri.ecf"/>
|
||||
<library name="uri_template" location="..\..\..\..\library\text\parser\uri_template\uri_template.ecf" readonly="false"/>
|
||||
<library name="wsf" location="..\..\..\..\library\server\obsolete\v0\wsf\wsf.ecf" readonly="false"/>
|
||||
<library name="wsf_extension" location="..\..\..\..\library\server\obsolete\v0\wsf\wsf_extension.ecf" readonly="false"/>
|
||||
</target>
|
||||
<target name="restbucks" extends="restbucks_common">
|
||||
<root class="RESTBUCKS_SERVER" feature="make"/>
|
||||
<option debug="true" warning="true">
|
||||
<debug name="nino" enabled="true"/>
|
||||
<assertions precondition="true" postcondition="true" check="true" invariant="true" loop="true" supplier_precondition="true"/>
|
||||
</option>
|
||||
<cluster name="src" location="src\" recursive="true">
|
||||
<file_rule>
|
||||
<exclude>/policy_driven_resource$</exclude>
|
||||
</file_rule>
|
||||
</cluster>
|
||||
</target>
|
||||
<target name="policy_driven_restbucks" extends="restbucks_common">
|
||||
<root class="RESTBUCKS_SERVER" feature="make"/>
|
||||
<option debug="true" warning="true">
|
||||
<debug name="nino" enabled="true"/>
|
||||
<assertions precondition="true" postcondition="true" check="true" invariant="true" loop="true" supplier_precondition="true"/>
|
||||
</option>
|
||||
<library name="wsf_policy_driven" location="..\..\..\..\library\server\obsolete\v0\wsf\wsf_policy_driven.ecf" readonly="false"/>
|
||||
<cluster name="src" location="src\" recursive="true">
|
||||
<file_rule>
|
||||
<exclude>/resource$</exclude>
|
||||
</file_rule>
|
||||
</cluster>
|
||||
</target>
|
||||
</system>
|
||||
@@ -1,26 +0,0 @@
|
||||
note
|
||||
description: "Summary description for {DATABASE_API}."
|
||||
author: ""
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
class
|
||||
DATABASE_API
|
||||
create
|
||||
make
|
||||
|
||||
feature -- Initialization
|
||||
|
||||
make
|
||||
do
|
||||
create orders.make (10)
|
||||
end
|
||||
|
||||
feature -- Access
|
||||
|
||||
orders: HASH_TABLE [ORDER, STRING]
|
||||
|
||||
;note
|
||||
copyright: "2011-2012, Javier Velilla and others"
|
||||
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
|
||||
end
|
||||
@@ -1,19 +0,0 @@
|
||||
note
|
||||
description: "Summary description for {SHARED_DATABASE_API}."
|
||||
author: ""
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
class
|
||||
SHARED_DATABASE_API
|
||||
|
||||
feature -- Access
|
||||
|
||||
db_access: DATABASE_API
|
||||
once
|
||||
create Result.make
|
||||
end
|
||||
note
|
||||
copyright: "2011-2012, Javier Velilla and others"
|
||||
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
|
||||
end
|
||||
@@ -1,90 +0,0 @@
|
||||
note
|
||||
description: "Summary description for {ITEM}."
|
||||
author: ""
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
class
|
||||
ITEM
|
||||
inherit
|
||||
ITEM_CONSTANTS
|
||||
create
|
||||
make
|
||||
feature -- Initialization
|
||||
make ( a_name : STRING_32 ; a_size:STRING_32; a_option: STRING_32; a_quantity:INTEGER_8)
|
||||
do
|
||||
set_name (a_name)
|
||||
set_size (a_size)
|
||||
set_option (a_option)
|
||||
set_quantity (a_quantity)
|
||||
end
|
||||
|
||||
feature -- Access
|
||||
name : STRING
|
||||
-- product name type of Coffee(Late, Cappuccino, Expresso)
|
||||
|
||||
option : STRING
|
||||
-- customization option Milk (skim, semi, whole)
|
||||
|
||||
size : STRING
|
||||
-- small, mediumm large
|
||||
|
||||
quantity :INTEGER
|
||||
|
||||
|
||||
|
||||
|
||||
feature -- Element Change
|
||||
set_name (a_name: STRING)
|
||||
require
|
||||
valid_name: is_valid_coffee_type (a_name)
|
||||
do
|
||||
name := a_name
|
||||
ensure
|
||||
name_assigned : name.same_string(a_name)
|
||||
end
|
||||
|
||||
set_size (a_size: STRING)
|
||||
require
|
||||
valid_size : is_valid_size_option (a_size)
|
||||
do
|
||||
size := a_size
|
||||
ensure
|
||||
size_assigned : size.same_string(a_size)
|
||||
end
|
||||
|
||||
set_option (an_option: STRING)
|
||||
require
|
||||
valid_option : is_valid_milk_type (an_option)
|
||||
do
|
||||
option := an_option
|
||||
ensure
|
||||
option_assigned : option.same_string (an_option)
|
||||
end
|
||||
|
||||
set_quantity (a_quantity: INTEGER)
|
||||
require
|
||||
valid_quantity : a_quantity > 0
|
||||
do
|
||||
quantity := a_quantity
|
||||
ensure
|
||||
quantity_assigned : quantity = a_quantity
|
||||
end
|
||||
|
||||
feature -- Report
|
||||
hash_code: INTEGER
|
||||
--Hash code value
|
||||
do
|
||||
Result := option.hash_code + name.hash_code + size.hash_code + quantity.hash_code
|
||||
end
|
||||
|
||||
|
||||
invariant
|
||||
valid_size : is_valid_size_option (size)
|
||||
valid_coffe : is_valid_coffee_type (name)
|
||||
valid_customization : is_valid_milk_type (option)
|
||||
valid_quantity : quantity > 0
|
||||
note
|
||||
copyright: "2011-2012, Javier Velilla and others"
|
||||
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
|
||||
end
|
||||
@@ -1,54 +0,0 @@
|
||||
note
|
||||
description: "Summary description for {ITEM_CONSTANTS}."
|
||||
author: ""
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
class
|
||||
ITEM_CONSTANTS
|
||||
feature -- Access
|
||||
is_valid_coffee_type (a_type: STRING) : BOOLEAN
|
||||
--is `a_type' a valid coffee type
|
||||
do
|
||||
a_type.to_lower
|
||||
coffe_types.compare_objects
|
||||
Result := coffe_types.has (a_type)
|
||||
end
|
||||
|
||||
Coffe_types : ARRAY[STRING]
|
||||
-- List of valid Coffee types
|
||||
once
|
||||
Result := <<"late","cappuccino", "expresso">>
|
||||
end
|
||||
|
||||
is_valid_milk_type (a_type: STRING) : BOOLEAN
|
||||
--is `a_type' a valid milk type
|
||||
do
|
||||
a_type.to_lower
|
||||
milk_types.compare_objects
|
||||
Result := milk_types.has (a_type)
|
||||
end
|
||||
|
||||
Milk_types : ARRAY[STRING]
|
||||
-- List of valid Milk types
|
||||
once
|
||||
Result := <<"skim","semi", "whole">>
|
||||
end
|
||||
|
||||
is_valid_size_option (an_option: STRING) : BOOLEAN
|
||||
--is `an_option' a valid size option
|
||||
do
|
||||
an_option.to_lower
|
||||
size_options.compare_objects
|
||||
Result := size_options.has (an_option)
|
||||
end
|
||||
|
||||
Size_options : ARRAY[STRING]
|
||||
-- List of valid Size_options
|
||||
once
|
||||
Result := <<"small","mediumn", "large">>
|
||||
end
|
||||
note
|
||||
copyright: "2011-2012, Javier Velilla and others"
|
||||
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
|
||||
end
|
||||
@@ -1,176 +0,0 @@
|
||||
note
|
||||
description: "Summary description for {JSON_ORDER_CONVERTER}."
|
||||
author: ""
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
class
|
||||
JSON_ORDER_CONVERTER
|
||||
inherit
|
||||
JSON_CONVERTER
|
||||
create
|
||||
make
|
||||
feature -- Initialization
|
||||
make
|
||||
do
|
||||
create object.make ("","","")
|
||||
end
|
||||
feature -- Access
|
||||
object : ORDER
|
||||
|
||||
|
||||
value : detachable JSON_OBJECT
|
||||
feature -- Conversion
|
||||
|
||||
from_json (j: attached like value): detachable like object
|
||||
-- Convert from JSON value. Returns Void if unable to convert
|
||||
local
|
||||
s_id, s_location, s_status: detachable STRING_32
|
||||
q: INTEGER_8
|
||||
o: ORDER
|
||||
i : ITEM
|
||||
l_array : detachable ARRAYED_LIST [JSON_VALUE]
|
||||
is_valid_from_json : BOOLEAN
|
||||
do
|
||||
is_valid_from_json := True
|
||||
|
||||
if attached {STRING_32} json.object (j.item (id_key), Void) as l_id then
|
||||
s_id := s_id
|
||||
end
|
||||
if attached {STRING_32} json.object (j.item (location_key), Void) as l_location then
|
||||
s_location := l_location
|
||||
end
|
||||
if attached {STRING_32} json.object (j.item (status_key), Void) as l_status then
|
||||
s_status := l_status
|
||||
end
|
||||
|
||||
create o.make ("", s_location, s_status)
|
||||
|
||||
if attached {JSON_ARRAY} j.item (items_key) as l_val then
|
||||
l_array := l_val.array_representation
|
||||
from
|
||||
l_array.start
|
||||
until
|
||||
l_array.after
|
||||
loop
|
||||
if attached {JSON_OBJECT} l_array.item_for_iteration as jv then
|
||||
if attached {INTEGER_8} json.object (jv.item (quantity_key), Void) as l_integer then
|
||||
q := l_integer
|
||||
else
|
||||
q := 0
|
||||
end
|
||||
if
|
||||
attached {STRING_32} json.object (jv.item (name_key), Void) as s_name and then
|
||||
attached {STRING_32} json.object (jv.item (size_key), Void) as s_key and then
|
||||
attached {STRING_32} json.object (jv.item (option_key), Void) as s_option
|
||||
then
|
||||
if is_valid_item_customization (s_name, s_key, s_option,q) then
|
||||
create i.make (s_name, s_key, s_option, q)
|
||||
o.add_item (i)
|
||||
else
|
||||
is_valid_from_json := False
|
||||
end
|
||||
else
|
||||
is_valid_from_json := False
|
||||
end
|
||||
end
|
||||
|
||||
l_array.forth
|
||||
end
|
||||
end
|
||||
if not is_valid_from_json or o.items.is_empty then
|
||||
Result := Void
|
||||
else
|
||||
Result := o
|
||||
end
|
||||
end
|
||||
|
||||
to_json (o: like object): like value
|
||||
-- Convert to JSON value
|
||||
local
|
||||
ja : JSON_ARRAY
|
||||
i : ITEM
|
||||
jv: JSON_OBJECT
|
||||
do
|
||||
create Result.make
|
||||
-- Result.put (json.value (o.id), id_key)
|
||||
Result.put (json.value (o.location), location_key)
|
||||
Result.put (json.value (o.status), status_key)
|
||||
from
|
||||
create ja.make_empty
|
||||
o.items.start
|
||||
until
|
||||
o.items.after
|
||||
loop
|
||||
i := o.items.item_for_iteration
|
||||
create jv.make
|
||||
jv.put (json.value (i.name), name_key)
|
||||
jv.put (json.value (i.size),size_key)
|
||||
jv.put (json.value (i.quantity), quantity_key)
|
||||
jv.put (json.value (i.option), option_key)
|
||||
ja.add (jv)
|
||||
o.items.forth
|
||||
end
|
||||
Result.put (ja, items_key)
|
||||
end
|
||||
|
||||
feature {NONE} -- Implementation
|
||||
id_key: JSON_STRING
|
||||
once
|
||||
create Result.make_from_string ("id")
|
||||
end
|
||||
|
||||
location_key: JSON_STRING
|
||||
once
|
||||
create Result.make_from_string ("location")
|
||||
end
|
||||
|
||||
status_key: JSON_STRING
|
||||
once
|
||||
create Result.make_from_string ("status")
|
||||
end
|
||||
|
||||
items_key : JSON_STRING
|
||||
once
|
||||
create Result.make_from_string ("items")
|
||||
end
|
||||
|
||||
|
||||
name_key : JSON_STRING
|
||||
|
||||
once
|
||||
create Result.make_from_string ("name")
|
||||
end
|
||||
|
||||
size_key : JSON_STRING
|
||||
|
||||
once
|
||||
create Result.make_from_string ("size")
|
||||
end
|
||||
|
||||
quantity_key : JSON_STRING
|
||||
|
||||
once
|
||||
create Result.make_from_string ("quantity")
|
||||
end
|
||||
|
||||
|
||||
option_key : JSON_STRING
|
||||
|
||||
once
|
||||
create Result.make_from_string ("option")
|
||||
end
|
||||
feature -- Validation
|
||||
|
||||
is_valid_item_customization ( name : STRING_32; size: STRING_32; option : STRING_32; quantity : INTEGER_8 ) : BOOLEAN
|
||||
local
|
||||
ic : ITEM_CONSTANTS
|
||||
do
|
||||
create ic
|
||||
Result := ic.is_valid_coffee_type (name) and ic.is_valid_milk_type (option) and ic.is_valid_size_option (size) and quantity > 0
|
||||
end
|
||||
|
||||
note
|
||||
copyright: "2011-2015, Javier Velilla and others"
|
||||
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
|
||||
end
|
||||
@@ -1,114 +0,0 @@
|
||||
note
|
||||
description: "Summary description for {ORDER}."
|
||||
author: ""
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
class
|
||||
ORDER
|
||||
create
|
||||
make
|
||||
feature -- Initialization
|
||||
|
||||
make ( an_id : detachable STRING_32; a_location: detachable STRING_32; a_status: detachable STRING_32)
|
||||
do
|
||||
create {ARRAYED_LIST [ITEM]} items.make (10)
|
||||
if an_id /= Void then
|
||||
set_id (an_id)
|
||||
else
|
||||
set_id ("")
|
||||
end
|
||||
if a_location /= Void then
|
||||
set_location (a_location)
|
||||
else
|
||||
set_location ("")
|
||||
end
|
||||
if a_status /= Void then
|
||||
set_status (a_status)
|
||||
else
|
||||
set_status ("")
|
||||
end
|
||||
revision := 0
|
||||
end
|
||||
|
||||
feature -- Access
|
||||
|
||||
id : STRING_32
|
||||
location : STRING_32
|
||||
items: LIST[ITEM]
|
||||
status : STRING_32
|
||||
revision : INTEGER
|
||||
|
||||
feature -- element change
|
||||
|
||||
set_id (an_id : STRING_32)
|
||||
do
|
||||
id := an_id
|
||||
ensure
|
||||
id_assigned : id.same_string (an_id)
|
||||
end
|
||||
|
||||
set_location (a_location : STRING_32)
|
||||
do
|
||||
location := a_location
|
||||
ensure
|
||||
location_assigned : location.same_string (a_location)
|
||||
end
|
||||
|
||||
set_status (a_status : STRING_32)
|
||||
do
|
||||
status := a_status
|
||||
ensure
|
||||
status_asigned : status.same_string (a_status)
|
||||
end
|
||||
|
||||
add_item (a_item : ITEM)
|
||||
require
|
||||
valid_item: a_item /= Void
|
||||
do
|
||||
items.force (a_item)
|
||||
ensure
|
||||
has_item : items.has (a_item)
|
||||
end
|
||||
|
||||
add_revision
|
||||
do
|
||||
revision := revision + 1
|
||||
ensure
|
||||
revision_incremented : old revision + 1 = revision
|
||||
end
|
||||
|
||||
feature -- Etag
|
||||
|
||||
etag : STRING_32
|
||||
-- Etag generation for Order objects
|
||||
do
|
||||
Result := hash_code.out + revision.out
|
||||
end
|
||||
|
||||
|
||||
feature -- Output
|
||||
|
||||
feature -- Report
|
||||
|
||||
hash_code: INTEGER_32
|
||||
-- Hash code value
|
||||
do
|
||||
from
|
||||
items.start
|
||||
Result := items.item.hash_code
|
||||
until
|
||||
items.off
|
||||
loop
|
||||
Result:= ((Result \\ 8388593) |<< 8) + items.item.hash_code
|
||||
items.forth
|
||||
end
|
||||
if items.count > 1 then
|
||||
Result := Result \\ items.count
|
||||
end
|
||||
end
|
||||
|
||||
note
|
||||
copyright: "2011-2012, Javier Velilla and others"
|
||||
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
|
||||
end
|
||||
@@ -1,56 +0,0 @@
|
||||
note
|
||||
description: "Summary description for {ORDER_TRANSITIONS}."
|
||||
author: ""
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
class
|
||||
ORDER_VALIDATION
|
||||
feature -- Access
|
||||
|
||||
is_valid_status_state (a_status: STRING) : BOOLEAN
|
||||
--is `a_status' a valid coffee order state
|
||||
do
|
||||
a_status.to_lower
|
||||
Order_states.compare_objects
|
||||
Result := Order_states.has (a_status)
|
||||
end
|
||||
|
||||
Order_states : ARRAY[STRING]
|
||||
-- List of valid status states
|
||||
once
|
||||
Result := <<"submitted","pay","payed", "cancel","canceled","prepare","prepared","deliver","completed">>
|
||||
end
|
||||
|
||||
|
||||
is_valid_transition (order:ORDER a_status : STRING) :BOOLEAN
|
||||
-- Given the current order state, determine if the transition is valid
|
||||
do
|
||||
a_status.to_lower
|
||||
if order.status.same_string ("submitted") then
|
||||
Result := a_status.same_string ("pay") or a_status.same_string ("cancel") or order.status.same_string (a_status)
|
||||
elseif order.status.same_string ("pay") then
|
||||
Result := a_status.same_string ("payed") or order.status.same_string (a_status)
|
||||
elseif order.status.same_string ("cancel") then
|
||||
Result := a_status.same_string ("canceled") or order.status.same_string (a_status)
|
||||
elseif order.status.same_string ("payed") then
|
||||
Result := a_status.same_string ("prepared") or order.status.same_string (a_status)
|
||||
elseif order.status.same_string ("prepared") then
|
||||
Result := a_status.same_string ("deliver") or order.status.same_string (a_status)
|
||||
elseif order.status.same_string ("deliver") then
|
||||
Result := a_status.same_string ("completed") or order.status.same_string (a_status)
|
||||
end
|
||||
end
|
||||
|
||||
is_state_valid_to_update ( a_status : STRING) : BOOLEAN
|
||||
-- Given the current state `a_status' of an order, is possible to update the order?
|
||||
do
|
||||
if a_status.same_string ("submitted") or else a_status.same_string ("pay") or else a_status.same_string ("payed") then
|
||||
Result := true
|
||||
end
|
||||
end
|
||||
|
||||
note
|
||||
copyright: "2011-2012, Javier Velilla and others"
|
||||
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
|
||||
end
|
||||
@@ -1,19 +0,0 @@
|
||||
note
|
||||
description: "Summary description for {SHARED_ORDER_VALIDATION}."
|
||||
author: ""
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
class
|
||||
SHARED_ORDER_VALIDATION
|
||||
|
||||
feature
|
||||
order_validation : ORDER_VALIDATION
|
||||
once
|
||||
create Result
|
||||
end
|
||||
|
||||
note
|
||||
copyright: "2011-2012, Javier Velilla and others"
|
||||
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
|
||||
end
|
||||
@@ -1,574 +0,0 @@
|
||||
note
|
||||
description: "{ORDER_HANDLER} handle the resources that we want to expose"
|
||||
author: ""
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
class ORDER_HANDLER
|
||||
|
||||
inherit
|
||||
|
||||
WSF_SKELETON_HANDLER
|
||||
|
||||
SHARED_DATABASE_API
|
||||
|
||||
SHARED_EJSON
|
||||
|
||||
REFACTORING_HELPER
|
||||
|
||||
SHARED_ORDER_VALIDATION
|
||||
|
||||
WSF_RESOURCE_HANDLER_HELPER
|
||||
rename
|
||||
execute_options as helper_execute_options,
|
||||
handle_internal_server_error as helper_handle_internal_server_error
|
||||
end
|
||||
|
||||
create
|
||||
|
||||
make_with_router
|
||||
|
||||
|
||||
feature -- Execution variables
|
||||
|
||||
Order_execution_variable: STRING = "ORDER"
|
||||
-- Execution variable used by application
|
||||
|
||||
Generated_content_execution_variable: STRING = "GENERATED_CONTENT"
|
||||
-- Execution variable used by application
|
||||
|
||||
Extracted_order_execution_variable: STRING = "EXTRACTED_ORDER"
|
||||
-- Execution variable used by application
|
||||
|
||||
feature -- Documentation
|
||||
|
||||
description: READABLE_STRING_GENERAL
|
||||
-- General description for self-generated documentation;
|
||||
-- The specific URI templates supported will be described automatically
|
||||
do
|
||||
Result := "Create, Read, Update or Delete an ORDER."
|
||||
end
|
||||
|
||||
feature -- Access
|
||||
|
||||
is_chunking (req: WSF_REQUEST): BOOLEAN
|
||||
-- Will the response to `req' using chunked transfer encoding?
|
||||
do
|
||||
-- No.
|
||||
end
|
||||
|
||||
includes_response_entity (req: WSF_REQUEST): BOOLEAN
|
||||
-- Does the response to `req' include an entity?
|
||||
-- Method will be DELETE, POST, PUT or an extension method.
|
||||
do
|
||||
Result := False
|
||||
-- At present, there is no support for this except for DELETE.
|
||||
end
|
||||
|
||||
conneg (req: WSF_REQUEST): SERVER_CONTENT_NEGOTIATION
|
||||
-- Content negotiatior for all requests
|
||||
once
|
||||
create Result.make ({HTTP_MIME_TYPES}.application_json, "en", "UTF-8", "identity")
|
||||
end
|
||||
|
||||
mime_types_supported (req: WSF_REQUEST): LIST [STRING]
|
||||
-- All values for Accept header that `Current' can serve
|
||||
do
|
||||
create {ARRAYED_LIST [STRING]} Result.make_from_array (<<{HTTP_MIME_TYPES}.application_json>>)
|
||||
Result.compare_objects
|
||||
end
|
||||
|
||||
languages_supported (req: WSF_REQUEST): LIST [STRING]
|
||||
-- All values for Accept-Language header that `Current' can serve
|
||||
do
|
||||
create {ARRAYED_LIST [STRING]} Result.make_from_array (<<"en">>)
|
||||
Result.compare_objects
|
||||
end
|
||||
|
||||
charsets_supported (req: WSF_REQUEST): LIST [STRING]
|
||||
-- All values for Accept-Charset header that `Current' can serve
|
||||
do
|
||||
create {ARRAYED_LIST [STRING]} Result.make_from_array (<<"UTF-8">>)
|
||||
Result.compare_objects
|
||||
end
|
||||
|
||||
encodings_supported (req: WSF_REQUEST): LIST [STRING]
|
||||
-- All values for Accept-Encoding header that `Current' can serve
|
||||
do
|
||||
create {ARRAYED_LIST [STRING]} Result.make_from_array (<<"identity">>)
|
||||
Result.compare_objects
|
||||
end
|
||||
|
||||
max_age (req: WSF_REQUEST): NATURAL
|
||||
-- Maximum age in seconds before response to `req` is considered stale;
|
||||
-- This is used to generate a Cache-Control: max-age header.
|
||||
-- Return 0 to indicate already expired.
|
||||
-- Return Never_expires to indicate never expires.
|
||||
do
|
||||
-- All our responses are considered stale.
|
||||
end
|
||||
|
||||
is_freely_cacheable (req: WSF_REQUEST): BOOLEAN
|
||||
-- Should the response to `req' be freely cachable in shared caches?
|
||||
-- If `True', then a Cache-Control: public header will be generated.
|
||||
do
|
||||
-- definitely not!
|
||||
end
|
||||
|
||||
private_headers (req: WSF_REQUEST): detachable LIST [READABLE_STRING_8]
|
||||
-- Header names intended for a single user.
|
||||
-- If non-Void, then a Cache-Control: private header will be generated.
|
||||
-- Returning an empty list prevents the entire response from being served from a shared cache.
|
||||
do
|
||||
create {ARRAYED_LIST [READABLE_STRING_8]} Result.make (0)
|
||||
end
|
||||
|
||||
non_cacheable_headers (req: WSF_REQUEST): detachable LIST [READABLE_STRING_8]
|
||||
-- Header names that will not be sent from a cache without revalidation;
|
||||
-- If non-Void, then a Cache-Control: no-cache header will be generated.
|
||||
-- Returning an empty list prevents the response being served from a cache
|
||||
-- without revalidation.
|
||||
do
|
||||
create {ARRAYED_LIST [READABLE_STRING_8]} Result.make (0)
|
||||
end
|
||||
|
||||
is_sensitive (req: WSF_REQUEST): BOOLEAN
|
||||
-- Is the response to `req' of a sensitive nature?
|
||||
-- If `True' then a Cache-Control: no-store header will be generated.
|
||||
do
|
||||
Result := True
|
||||
-- since it's commercial data.
|
||||
end
|
||||
|
||||
allowed_cross_origins (req: WSF_REQUEST): detachable STRING
|
||||
-- Value for Access-Control-Allow-Origin header;
|
||||
-- If supplied, should be a single URI, or the values "*" or "null".
|
||||
-- This is currently supported only for GET requests, and POSTs that functions as GET.
|
||||
do
|
||||
if req.is_get_head_request_method then
|
||||
Result := "*"
|
||||
end
|
||||
end
|
||||
|
||||
matching_etag (req: WSF_REQUEST; a_etag: READABLE_STRING_32; a_strong: BOOLEAN): BOOLEAN
|
||||
-- Is `a_etag' a match for resource requested in `req'?
|
||||
-- If `a_strong' then the strong comparison function must be used.
|
||||
local
|
||||
l_id: STRING
|
||||
l_etag_util: ETAG_UTILS
|
||||
do
|
||||
l_id := order_id_from_request (req)
|
||||
if db_access.orders.has_key (l_id) then
|
||||
check attached db_access.orders.item (l_id) as l_order then
|
||||
-- postcondition of `has_key'
|
||||
create l_etag_util
|
||||
Result := a_etag.same_string (l_etag_util.md5_digest (l_order.out).as_string_32)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
etag (req: WSF_REQUEST): detachable READABLE_STRING_8
|
||||
-- Optional Etag for `req' in the requested variant
|
||||
local
|
||||
l_etag_utils: ETAG_UTILS
|
||||
do
|
||||
create l_etag_utils
|
||||
if attached {ORDER} req.execution_variable (Order_execution_variable) as l_order then
|
||||
Result := l_etag_utils.md5_digest (l_order.out)
|
||||
end
|
||||
end
|
||||
|
||||
last_modified (req: WSF_REQUEST): detachable DATE_TIME
|
||||
-- When representation of resource selected in `req' was last modified;
|
||||
-- SHOULD be set whenever it can reasonably be determined.
|
||||
do
|
||||
end
|
||||
|
||||
modified_since (req: WSF_REQUEST; a_date_time: DATE_TIME): BOOLEAN
|
||||
-- Has resource requested in `req' been modified since `a_date_time' (UTC)?
|
||||
do
|
||||
-- We don't track this information. It is safe to always say yes.
|
||||
Result := True
|
||||
end
|
||||
|
||||
feature -- Measurement
|
||||
|
||||
content_length (req: WSF_REQUEST): NATURAL
|
||||
-- Length of entity-body of the response to `req'
|
||||
do
|
||||
check attached {READABLE_STRING_8} req.execution_variable (Generated_content_execution_variable) as l_response then
|
||||
-- postcondition generated_content_set_for_get_head of `ensure_content_available'
|
||||
-- We only call this for GET/HEAD in this example.
|
||||
Result := l_response.count.as_natural_32
|
||||
end
|
||||
end
|
||||
|
||||
allow_post_to_missing_resource (req: WSF_REQUEST): BOOLEAN
|
||||
-- The resource named in `req' does not exist, and this is a POST. Do we allow it?
|
||||
do
|
||||
-- No.
|
||||
end
|
||||
|
||||
feature -- Status report
|
||||
|
||||
finished (req: WSF_REQUEST): BOOLEAN
|
||||
-- Has the last chunk been generated for `req'?
|
||||
do
|
||||
-- precondition is never met
|
||||
end
|
||||
|
||||
feature -- Execution
|
||||
|
||||
check_resource_exists (req: WSF_REQUEST; a_helper: WSF_METHOD_HELPER)
|
||||
-- Call `a_helper.set_resource_exists' to indicate that `req.path_translated'
|
||||
-- is the name of an existing resource.
|
||||
-- We also put the order into `req.execution_variable (Order_execution_variable)' for GET or HEAD responses.
|
||||
local
|
||||
l_id: STRING
|
||||
do
|
||||
if req.is_post_request_method then
|
||||
a_helper.set_resource_exists
|
||||
-- because only /order is defined to this handler for POST
|
||||
else
|
||||
-- the request is of the form /order/{orderid}
|
||||
l_id := order_id_from_request (req)
|
||||
if db_access.orders.has_key (l_id) then
|
||||
a_helper.set_resource_exists
|
||||
if req.is_get_head_request_method then
|
||||
check attached db_access.orders.item (l_id) as l_order then
|
||||
-- postcondition `item_if_found' of `has_key'
|
||||
req.set_execution_variable (Order_execution_variable, l_order)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
ensure then
|
||||
order_saved_only_for_get_head: attached {ORDER} req.execution_variable (Order_execution_variable) implies req.is_get_head_request_method
|
||||
end
|
||||
|
||||
feature -- GET/HEAD content
|
||||
|
||||
ensure_content_available (req: WSF_REQUEST)
|
||||
-- Commence generation of response text (entity-body).
|
||||
-- If not chunked, then this will create the entire entity-body so as to be available
|
||||
-- for a subsequent call to `content'.
|
||||
-- If chunked, only the first chunk will be made available to `next_chunk'. If chunk extensions
|
||||
-- are used, then this will also generate the chunk extension for the first chunk.
|
||||
-- We save the text in `req.execution_variable (Generated_content_execution_variable)'
|
||||
-- We ignore the results of content negotiation, as there is only one possible combination.
|
||||
do
|
||||
check attached {ORDER} req.execution_variable (Order_execution_variable) as l_order then
|
||||
-- precondition get_or_head and postcondition order_saved_only_for_get_head of `check_resource_exists' and
|
||||
if attached {JSON_VALUE} json.value (l_order) as jv then
|
||||
req.set_execution_variable (Generated_content_execution_variable, jv.representation)
|
||||
else
|
||||
req.set_execution_variable (Generated_content_execution_variable, "")
|
||||
end
|
||||
end
|
||||
ensure then
|
||||
generated_content_set_for_get_head: req.is_get_head_request_method implies
|
||||
attached {READABLE_STRING_8} req.execution_variable (Generated_content_execution_variable)
|
||||
end
|
||||
|
||||
content (req: WSF_REQUEST): READABLE_STRING_8
|
||||
-- Non-chunked entity body in response to `req';
|
||||
-- We only call this for GET/HEAD in this example.
|
||||
do
|
||||
check attached {READABLE_STRING_8} req.execution_variable (Generated_content_execution_variable) as l_response then
|
||||
-- postcondition generated_content_set_for_get_head of `ensure_content_available'
|
||||
Result := l_response
|
||||
end
|
||||
end
|
||||
|
||||
next_chunk (req: WSF_REQUEST): TUPLE [a_chunk: READABLE_STRING_8; a_extension: detachable READABLE_STRING_8]
|
||||
-- Next chunk of entity body in response to `req';
|
||||
-- The second field of the result is an optional chunk extension.
|
||||
do
|
||||
-- precondition `is_chunking' is never met, but we need a dummy `Result'
|
||||
-- to satisfy the compiler in void-safe mode
|
||||
Result := ["", Void]
|
||||
end
|
||||
|
||||
generate_next_chunk (req: WSF_REQUEST)
|
||||
-- Prepare next chunk (including optional chunk extension) of entity body in response to `req'.
|
||||
-- This is not called for the first chunk.
|
||||
do
|
||||
-- precondition `is_chunking' is never met
|
||||
end
|
||||
|
||||
feature -- DELETE
|
||||
|
||||
delete (req: WSF_REQUEST)
|
||||
-- Delete resource named in `req' or set an error on `req.error_handler'.
|
||||
local
|
||||
l_id: STRING
|
||||
do
|
||||
l_id := order_id_from_request (req)
|
||||
if db_access.orders.has_key (l_id) then
|
||||
if is_valid_to_delete (l_id) then
|
||||
delete_order (l_id)
|
||||
else
|
||||
req.error_handler.add_custom_error ({HTTP_STATUS_CODE}.method_not_allowed, "DELETE not valid",
|
||||
"There is conflict while trying to delete the order, the order could not be deleted in the current state")
|
||||
end
|
||||
else
|
||||
req.error_handler.add_custom_error ({HTTP_STATUS_CODE}.not_found, "DELETE not valid",
|
||||
"There is no such order to delete")
|
||||
end
|
||||
end
|
||||
|
||||
delete_queued (req: WSF_REQUEST): BOOLEAN
|
||||
-- Has resource named by `req' been queued for deletion?
|
||||
do
|
||||
-- No
|
||||
end
|
||||
|
||||
|
||||
feature -- PUT/POST
|
||||
|
||||
is_entity_too_large (req: WSF_REQUEST): BOOLEAN
|
||||
-- Is the entity stored in `req.execution_variable (Request_entity_execution_variable)' too large for the application?
|
||||
do
|
||||
-- No. We don't care for this example.
|
||||
end
|
||||
|
||||
check_content_headers (req: WSF_REQUEST)
|
||||
-- Check we can support all content headers on request entity.
|
||||
-- Set `req.execution_variable (Content_check_code_execution_variable)' to {NATURAL} zero if OK, or 415 or 501 if not.
|
||||
do
|
||||
-- We don't bother for this example. Note that this is equivalent to setting zero.
|
||||
end
|
||||
|
||||
create_resource (req: WSF_REQUEST; res: WSF_RESPONSE)
|
||||
-- Create new resource in response to a PUT request when `check_resource_exists' returns `False'.
|
||||
-- Implementor must set error code of 200 OK or 500 Server Error.
|
||||
do
|
||||
-- We don't support creating a new resource with PUT. But this can't happen
|
||||
-- with our router mappings, so we don't bother to set a 500 response.
|
||||
end
|
||||
|
||||
append_resource (req: WSF_REQUEST; res: WSF_RESPONSE)
|
||||
-- Create new resource in response to a POST request.
|
||||
-- Implementor must set error code of 200 OK or 204 No Content or 303 See Other or 500 Server Error.
|
||||
do
|
||||
if attached {ORDER} req.execution_variable (Extracted_order_execution_variable) as l_order then
|
||||
save_order (l_order)
|
||||
compute_response_post (req, res, l_order)
|
||||
else
|
||||
handle_bad_request_response ("Not a valid order", req, res)
|
||||
end
|
||||
end
|
||||
|
||||
check_conflict (req: WSF_REQUEST; res: WSF_RESPONSE)
|
||||
-- Check we can support all content headers on request entity.
|
||||
-- Set `req.execution_variable (Conflict_check_code_execution_variable)' to {NATURAL} zero if OK, or 409 if not.
|
||||
-- In the latter case, write the full error response to `res'.
|
||||
do
|
||||
if attached {ORDER} req.execution_variable (Extracted_order_execution_variable) as l_order then
|
||||
if not is_valid_to_update (l_order) then
|
||||
req.set_execution_variable (Conflict_check_code_execution_variable, {NATURAL} 409)
|
||||
handle_resource_conflict_response (l_order.out +"%N There is conflict while trying to update the order, the order could not be update in the current state", req, res)
|
||||
end
|
||||
else
|
||||
req.set_execution_variable (Conflict_check_code_execution_variable, {NATURAL} 409)
|
||||
--| This ought to be a 500, as if attached should probably be check attached. But as yet I lack a proof.
|
||||
handle_resource_conflict_response ("There is conflict while trying to update the order, the order could not be update in the current state", req, res)
|
||||
end
|
||||
end
|
||||
|
||||
check_request (req: WSF_REQUEST; res: WSF_RESPONSE)
|
||||
-- Check that the request entity is a valid request.
|
||||
-- The entity is available as `req.execution_variable (Conflict_check_code_execution_variable)'.
|
||||
-- Set `req.execution_variable (Request_check_code_execution_variable)' to {NATURAL} zero if OK, or 400 if not.
|
||||
-- In the latter case, write the full error response to `res'.
|
||||
local
|
||||
l_order: detachable ORDER
|
||||
l_id: STRING
|
||||
do
|
||||
if attached {READABLE_STRING_8} req.execution_variable (Request_entity_execution_variable) as l_request then
|
||||
l_order := extract_order_request (l_request)
|
||||
if req.is_put_request_method then
|
||||
l_id := order_id_from_request (req)
|
||||
if l_order /= Void and then db_access.orders.has_key (l_id) then
|
||||
l_order.set_id (l_id)
|
||||
req.set_execution_variable (Request_check_code_execution_variable, {NATURAL} 0)
|
||||
req.set_execution_variable (Extracted_order_execution_variable, l_order)
|
||||
else
|
||||
req.set_execution_variable (Request_check_code_execution_variable, {NATURAL} 400)
|
||||
handle_bad_request_response (l_request +"%N is not a valid ORDER, maybe the order does not exist in the system", req, res)
|
||||
end
|
||||
else
|
||||
req.set_execution_variable (Request_check_code_execution_variable, {NATURAL} 0)
|
||||
req.set_execution_variable (Extracted_order_execution_variable, l_order)
|
||||
end
|
||||
else
|
||||
req.set_execution_variable (Request_check_code_execution_variable, {NATURAL} 400)
|
||||
handle_bad_request_response ("Request is not a valid ORDER", req, res)
|
||||
end
|
||||
end
|
||||
|
||||
update_resource (req: WSF_REQUEST; res: WSF_RESPONSE)
|
||||
-- Perform the update requested in `req'.
|
||||
-- Write a response to `res' with a code of 204 or 500.
|
||||
do
|
||||
if attached {ORDER} req.execution_variable (Extracted_order_execution_variable) as l_order then
|
||||
update_order (l_order)
|
||||
compute_response_put (req, res, l_order)
|
||||
else
|
||||
handle_internal_server_error (res)
|
||||
end
|
||||
end
|
||||
|
||||
feature -- HTTP Methods
|
||||
|
||||
compute_response_put (req: WSF_REQUEST; res: WSF_RESPONSE; l_order : ORDER)
|
||||
local
|
||||
h: HTTP_HEADER
|
||||
joc : JSON_ORDER_CONVERTER
|
||||
etag_utils : ETAG_UTILS
|
||||
do
|
||||
create h.make
|
||||
create joc.make
|
||||
create etag_utils
|
||||
json.add_converter(joc)
|
||||
|
||||
create h.make
|
||||
h.put_content_type_application_json
|
||||
if attached req.request_time as time then
|
||||
h.add_header ("Date:" +time.formatted_out ("ddd,[0]dd mmm yyyy [0]hh:[0]mi:[0]ss.ff2") + " GMT")
|
||||
end
|
||||
h.add_header ("etag:" + etag_utils.md5_digest (l_order.out))
|
||||
if attached {JSON_VALUE} json.value (l_order) as jv then
|
||||
h.put_content_length (jv.representation.count)
|
||||
res.set_status_code ({HTTP_STATUS_CODE}.ok)
|
||||
res.put_header_text (h.string)
|
||||
res.put_string (jv.representation)
|
||||
end
|
||||
end
|
||||
|
||||
compute_response_post (req: WSF_REQUEST; res: WSF_RESPONSE; l_order : ORDER)
|
||||
local
|
||||
h: HTTP_HEADER
|
||||
l_msg : STRING
|
||||
l_location : STRING
|
||||
joc : JSON_ORDER_CONVERTER
|
||||
do
|
||||
create h.make
|
||||
|
||||
create joc.make
|
||||
json.add_converter(joc)
|
||||
|
||||
h.put_content_type_application_json
|
||||
if attached {JSON_VALUE} json.value (l_order) as jv then
|
||||
l_msg := jv.representation
|
||||
h.put_content_length (l_msg.count)
|
||||
if attached req.http_host as host then
|
||||
l_location := "http://" + host + req.request_uri + "/" + l_order.id
|
||||
h.put_location (l_location)
|
||||
end
|
||||
if attached req.request_time as time then
|
||||
h.put_utc_date (time)
|
||||
end
|
||||
res.set_status_code ({HTTP_STATUS_CODE}.created)
|
||||
res.put_header_text (h.string)
|
||||
res.put_string (l_msg)
|
||||
end
|
||||
end
|
||||
|
||||
feature {NONE} -- URI helper methods
|
||||
|
||||
order_id_from_request (req: WSF_REQUEST): STRING
|
||||
-- Value of "orderid" template URI variable in `req'
|
||||
require
|
||||
req_attached: req /= Void
|
||||
do
|
||||
if attached {WSF_VALUE} req.path_parameter ("orderid") as l_value then
|
||||
Result := l_value.as_string.value.as_string_8
|
||||
else
|
||||
Result := ""
|
||||
end
|
||||
end
|
||||
|
||||
feature {NONE} -- Implementation Repository Layer
|
||||
|
||||
retrieve_order ( id : STRING) : detachable ORDER
|
||||
-- get the order by id if it exist, in other case, Void
|
||||
do
|
||||
Result := db_access.orders.item (id)
|
||||
end
|
||||
|
||||
save_order (an_order: ORDER)
|
||||
-- save the order to the repository
|
||||
local
|
||||
i : INTEGER
|
||||
do
|
||||
from
|
||||
i := 1
|
||||
until
|
||||
not db_access.orders.has_key ((db_access.orders.count + i).out)
|
||||
loop
|
||||
i := i + 1
|
||||
end
|
||||
an_order.set_id ((db_access.orders.count + i).out)
|
||||
an_order.set_status ("submitted")
|
||||
an_order.add_revision
|
||||
db_access.orders.force (an_order, an_order.id)
|
||||
end
|
||||
|
||||
|
||||
is_valid_to_delete ( an_id : STRING) : BOOLEAN
|
||||
-- Is the order identified by `an_id' in a state whre it can still be deleted?
|
||||
do
|
||||
if attached retrieve_order (an_id) as l_order then
|
||||
if order_validation.is_state_valid_to_update (l_order.status) then
|
||||
Result := True
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
is_valid_to_update (an_order: ORDER) : BOOLEAN
|
||||
-- Check if there is a conflict while trying to update the order
|
||||
do
|
||||
if attached retrieve_order (an_order.id) as l_order then
|
||||
if order_validation.is_state_valid_to_update (l_order.status) and then order_validation.is_valid_status_state (an_order.status) and then
|
||||
order_validation.is_valid_transition (l_order, an_order.status) then
|
||||
Result := True
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
update_order (an_order: ORDER)
|
||||
-- update the order to the repository
|
||||
do
|
||||
an_order.add_revision
|
||||
db_access.orders.force (an_order, an_order.id)
|
||||
end
|
||||
|
||||
delete_order (an_order: STRING)
|
||||
-- update the order to the repository
|
||||
do
|
||||
db_access.orders.remove (an_order)
|
||||
end
|
||||
|
||||
extract_order_request (l_post : STRING) : detachable ORDER
|
||||
-- extract an object Order from the request, or Void
|
||||
-- if the request is invalid
|
||||
local
|
||||
parser : JSON_PARSER
|
||||
joc : JSON_ORDER_CONVERTER
|
||||
do
|
||||
create joc.make
|
||||
json.add_converter(joc)
|
||||
create parser.make_with_string (l_post)
|
||||
parser.parse_content
|
||||
if parser.is_valid and then attached parser.parsed_json_value as jv then
|
||||
if attached {like extract_order_request} json.object (jv, "ORDER") as res then
|
||||
Result := res
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
note
|
||||
copyright: "2011-2015, Javier Velilla and others"
|
||||
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
|
||||
end
|
||||
@@ -1,403 +0,0 @@
|
||||
note
|
||||
description: "{ORDER_HANDLER} handle the resources that we want to expose"
|
||||
author: ""
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
class ORDER_HANDLER
|
||||
inherit
|
||||
|
||||
WSF_URI_TEMPLATE_HANDLER
|
||||
|
||||
WSF_RESOURCE_HANDLER_HELPER
|
||||
redefine
|
||||
do_get,
|
||||
do_post,
|
||||
do_put,
|
||||
do_delete
|
||||
end
|
||||
|
||||
SHARED_DATABASE_API
|
||||
|
||||
SHARED_EJSON
|
||||
|
||||
REFACTORING_HELPER
|
||||
|
||||
SHARED_ORDER_VALIDATION
|
||||
|
||||
WSF_SELF_DOCUMENTED_HANDLER
|
||||
|
||||
create
|
||||
make_with_router
|
||||
|
||||
feature {NONE} -- Initialization
|
||||
|
||||
make_with_router (a_router: WSF_ROUTER)
|
||||
-- Initialize `router'.
|
||||
require
|
||||
a_router_attached: a_router /= Void
|
||||
do
|
||||
router := a_router
|
||||
ensure
|
||||
router_aliased: router = a_router
|
||||
end
|
||||
|
||||
feature -- Router
|
||||
|
||||
router: WSF_ROUTER
|
||||
-- Associated router that could be used for advanced strategy
|
||||
|
||||
feature -- Execute
|
||||
|
||||
execute (req: WSF_REQUEST; res: WSF_RESPONSE)
|
||||
-- Execute request handler
|
||||
do
|
||||
execute_methods (req, res)
|
||||
end
|
||||
|
||||
feature -- API DOC
|
||||
|
||||
api_doc : STRING = "URI:/order METHOD: POST%N URI:/order/{orderid} METHOD: GET, PUT, DELETE%N"
|
||||
|
||||
|
||||
feature -- Documentation
|
||||
|
||||
mapping_documentation (m: WSF_ROUTER_MAPPING; a_request_methods: detachable WSF_REQUEST_METHODS): WSF_ROUTER_MAPPING_DOCUMENTATION
|
||||
do
|
||||
create Result.make (m)
|
||||
if a_request_methods /= Void then
|
||||
if a_request_methods.has_method_post then
|
||||
Result.add_description ("URI:/order METHOD: POST")
|
||||
elseif
|
||||
a_request_methods.has_method_get
|
||||
or a_request_methods.has_method_put
|
||||
or a_request_methods.has_method_delete
|
||||
then
|
||||
Result.add_description ("URI:/order/{orderid} METHOD: GET, PUT, DELETE")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
feature -- HTTP Methods
|
||||
|
||||
do_get (req: WSF_REQUEST; res: WSF_RESPONSE)
|
||||
-- <Precursor>
|
||||
local
|
||||
id: STRING
|
||||
do
|
||||
if attached req.path_info as l_path_info then
|
||||
id := get_order_id_from_path (l_path_info)
|
||||
if attached retrieve_order (id) as l_order then
|
||||
if is_conditional_get (req, l_order) then
|
||||
handle_resource_not_modified_response ("The resource" + l_path_info + "does not change", req, res)
|
||||
else
|
||||
compute_response_get (req, res, l_order)
|
||||
end
|
||||
else
|
||||
handle_resource_not_found_response ("The following resource" + l_path_info + " is not found ", req, res)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
is_conditional_get (req : WSF_REQUEST; l_order : ORDER) : BOOLEAN
|
||||
-- Check if If-None-Match is present and then if there is a representation that has that etag
|
||||
-- if the representation hasn't changed, we return TRUE
|
||||
-- then the response is a 304 with no entity body returned.
|
||||
local
|
||||
etag_util : ETAG_UTILS
|
||||
do
|
||||
if attached req.meta_string_variable ("HTTP_IF_NONE_MATCH") as if_none_match then
|
||||
create etag_util
|
||||
if if_none_match.same_string (etag_util.md5_digest (l_order.out).as_string_32) then
|
||||
Result := True
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
compute_response_get (req: WSF_REQUEST; res: WSF_RESPONSE; l_order: ORDER)
|
||||
local
|
||||
h: HTTP_HEADER
|
||||
l_msg : STRING
|
||||
etag_utils : ETAG_UTILS
|
||||
do
|
||||
create h.make
|
||||
create etag_utils
|
||||
h.put_content_type_application_json
|
||||
if attached {JSON_VALUE} json.value (l_order) as jv then
|
||||
l_msg := jv.representation
|
||||
h.put_content_length (l_msg.count)
|
||||
if attached req.request_time as time then
|
||||
h.add_header ("Date:" + time.formatted_out ("ddd,[0]dd mmm yyyy [0]hh:[0]mi:[0]ss.ff2") + " GMT")
|
||||
end
|
||||
h.add_header ("etag:" + etag_utils.md5_digest (l_order.out))
|
||||
res.set_status_code ({HTTP_STATUS_CODE}.ok)
|
||||
res.put_header_text (h.string)
|
||||
res.put_string (l_msg)
|
||||
end
|
||||
end
|
||||
|
||||
do_put (req: WSF_REQUEST; res: WSF_RESPONSE)
|
||||
-- Updating a resource with PUT
|
||||
-- A successful PUT request will not create a new resource, instead it will
|
||||
-- change the state of the resource identified by the current uri.
|
||||
-- If success we response with 200 and the updated order.
|
||||
-- 404 if the order is not found
|
||||
-- 400 in case of a bad request
|
||||
-- 500 internal server error
|
||||
-- If the request is a Conditional PUT, and it does not mat we response
|
||||
-- 415, precondition failed.
|
||||
local
|
||||
l_put: STRING
|
||||
l_order : detachable ORDER
|
||||
id : STRING
|
||||
do
|
||||
if attached req.path_info as l_path_info then
|
||||
id := get_order_id_from_path (l_path_info)
|
||||
l_put := retrieve_data (req)
|
||||
l_order := extract_order_request(l_put)
|
||||
if l_order /= Void and then db_access.orders.has_key (id) then
|
||||
l_order.set_id (id)
|
||||
if is_valid_to_update(l_order) then
|
||||
if is_conditional_put (req, l_order) then
|
||||
update_order( l_order)
|
||||
compute_response_put (req, res, l_order)
|
||||
else
|
||||
handle_precondition_fail_response ("", req, res)
|
||||
end
|
||||
else
|
||||
--| FIXME: Here we need to define the Allow methods
|
||||
handle_resource_conflict_response (l_put +"%N There is conflict while trying to update the order, the order could not be update in the current state", req, res)
|
||||
end
|
||||
else
|
||||
handle_bad_request_response (l_put +"%N is not a valid ORDER, maybe the order does not exist in the system", req, res)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
is_conditional_put (req : WSF_REQUEST; order : ORDER) : BOOLEAN
|
||||
-- Check if If-Match is present and then if there is a representation that has that etag
|
||||
-- if the representation hasn't changed, we return TRUE
|
||||
local
|
||||
etag_util : ETAG_UTILS
|
||||
do
|
||||
if attached retrieve_order (order.id) as l_order then
|
||||
if attached req.meta_string_variable ("HTTP_IF_MATCH") as if_match then
|
||||
create etag_util
|
||||
if if_match.same_string (etag_util.md5_digest (l_order.out).as_string_32) then
|
||||
Result := True
|
||||
end
|
||||
else
|
||||
Result := True
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
compute_response_put (req: WSF_REQUEST; res: WSF_RESPONSE; l_order : ORDER)
|
||||
local
|
||||
h: HTTP_HEADER
|
||||
joc : JSON_ORDER_CONVERTER
|
||||
etag_utils : ETAG_UTILS
|
||||
do
|
||||
create h.make
|
||||
create joc.make
|
||||
create etag_utils
|
||||
json.add_converter(joc)
|
||||
|
||||
create h.make
|
||||
h.put_content_type_application_json
|
||||
if attached req.request_time as time then
|
||||
h.add_header ("Date:" +time.formatted_out ("ddd,[0]dd mmm yyyy [0]hh:[0]mi:[0]ss.ff2") + " GMT")
|
||||
end
|
||||
h.add_header ("etag:" + etag_utils.md5_digest (l_order.out))
|
||||
if attached {JSON_VALUE} json.value (l_order) as jv then
|
||||
h.put_content_length (jv.representation.count)
|
||||
res.set_status_code ({HTTP_STATUS_CODE}.ok)
|
||||
res.put_header_text (h.string)
|
||||
res.put_string (jv.representation)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
do_delete (req: WSF_REQUEST; res: WSF_RESPONSE)
|
||||
-- Here we use DELETE to cancel an order, if that order is in state where
|
||||
-- it can still be canceled.
|
||||
-- 200 if is ok
|
||||
-- 404 Resource not found
|
||||
-- 405 if consumer and service's view of the resouce state is inconsisent
|
||||
-- 500 if we have an internal server error
|
||||
local
|
||||
id: STRING
|
||||
do
|
||||
if attached req.path_info as l_path_info then
|
||||
id := get_order_id_from_path (l_path_info)
|
||||
if db_access.orders.has_key (id) then
|
||||
if is_valid_to_delete (id) then
|
||||
delete_order( id)
|
||||
compute_response_delete (req, res)
|
||||
else
|
||||
--| FIXME: Here we need to define the Allow methods
|
||||
handle_method_not_allowed_response (l_path_info + "%N There is conflict while trying to delete the order, the order could not be deleted in the current state", req, res)
|
||||
end
|
||||
else
|
||||
handle_resource_not_found_response (l_path_info + " not found in this server", req, res)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
compute_response_delete (req: WSF_REQUEST; res: WSF_RESPONSE)
|
||||
local
|
||||
h : HTTP_HEADER
|
||||
do
|
||||
create h.make
|
||||
h.put_content_type_application_json
|
||||
if attached req.request_time as time then
|
||||
h.put_utc_date (time)
|
||||
end
|
||||
res.set_status_code ({HTTP_STATUS_CODE}.no_content)
|
||||
res.put_header_text (h.string)
|
||||
end
|
||||
|
||||
do_post (req: WSF_REQUEST; res: WSF_RESPONSE)
|
||||
-- Here the convention is the following.
|
||||
-- POST is used for creation and the server determines the URI
|
||||
-- of the created resource.
|
||||
-- If the request post is SUCCESS, the server will create the order and will response with
|
||||
-- HTTP_RESPONSE 201 CREATED, the Location header will contains the newly created order's URI
|
||||
-- if the request post is not SUCCESS, the server will response with
|
||||
-- HTTP_RESPONSE 400 BAD REQUEST, the client send a bad request
|
||||
-- HTTP_RESPONSE 500 INTERNAL_SERVER_ERROR, when the server can deliver the request
|
||||
local
|
||||
l_post: STRING
|
||||
do
|
||||
l_post := retrieve_data (req)
|
||||
if attached extract_order_request (l_post) as l_order then
|
||||
save_order (l_order)
|
||||
compute_response_post (req, res, l_order)
|
||||
else
|
||||
handle_bad_request_response (l_post +"%N is not a valid ORDER", req, res)
|
||||
end
|
||||
end
|
||||
|
||||
compute_response_post (req: WSF_REQUEST; res: WSF_RESPONSE; l_order : ORDER)
|
||||
local
|
||||
h: HTTP_HEADER
|
||||
l_msg : STRING
|
||||
l_location : STRING
|
||||
joc : JSON_ORDER_CONVERTER
|
||||
do
|
||||
create h.make
|
||||
|
||||
create joc.make
|
||||
json.add_converter(joc)
|
||||
|
||||
h.put_content_type_application_json
|
||||
if attached {JSON_VALUE} json.value (l_order) as jv then
|
||||
l_msg := jv.representation
|
||||
h.put_content_length (l_msg.count)
|
||||
if attached req.http_host as host then
|
||||
l_location := "http://" + host + req.request_uri + "/" + l_order.id
|
||||
h.put_location (l_location)
|
||||
end
|
||||
if attached req.request_time as time then
|
||||
h.put_utc_date (time)
|
||||
end
|
||||
res.set_status_code ({HTTP_STATUS_CODE}.created)
|
||||
res.put_header_text (h.string)
|
||||
res.put_string (l_msg)
|
||||
end
|
||||
end
|
||||
|
||||
feature {NONE} -- URI helper methods
|
||||
|
||||
get_order_id_from_path (a_path: READABLE_STRING_32) : STRING
|
||||
do
|
||||
Result := a_path.split ('/').at (3)
|
||||
end
|
||||
|
||||
feature {NONE} -- Implementation Repository Layer
|
||||
|
||||
retrieve_order ( id : STRING) : detachable ORDER
|
||||
-- get the order by id if it exist, in other case, Void
|
||||
do
|
||||
Result := db_access.orders.item (id)
|
||||
end
|
||||
|
||||
save_order (an_order: ORDER)
|
||||
-- save the order to the repository
|
||||
local
|
||||
i : INTEGER
|
||||
do
|
||||
from
|
||||
i := 1
|
||||
until
|
||||
not db_access.orders.has_key ((db_access.orders.count + i).out)
|
||||
loop
|
||||
i := i + 1
|
||||
end
|
||||
an_order.set_id ((db_access.orders.count + i).out)
|
||||
an_order.set_status ("submitted")
|
||||
an_order.add_revision
|
||||
db_access.orders.force (an_order, an_order.id)
|
||||
end
|
||||
|
||||
|
||||
is_valid_to_delete ( an_id : STRING) : BOOLEAN
|
||||
-- Is the order identified by `an_id' in a state whre it can still be deleted?
|
||||
do
|
||||
if attached retrieve_order (an_id) as l_order then
|
||||
if order_validation.is_state_valid_to_update (l_order.status) then
|
||||
Result := True
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
is_valid_to_update (an_order: ORDER) : BOOLEAN
|
||||
-- Check if there is a conflict while trying to update the order
|
||||
do
|
||||
if attached retrieve_order (an_order.id) as l_order then
|
||||
if order_validation.is_state_valid_to_update (l_order.status) and then order_validation.is_valid_status_state (an_order.status) and then
|
||||
order_validation.is_valid_transition (l_order, an_order.status) then
|
||||
Result := True
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
update_order (an_order: ORDER)
|
||||
-- update the order to the repository
|
||||
do
|
||||
an_order.add_revision
|
||||
db_access.orders.force (an_order, an_order.id)
|
||||
end
|
||||
|
||||
delete_order (an_order: STRING)
|
||||
-- update the order to the repository
|
||||
do
|
||||
db_access.orders.remove (an_order)
|
||||
end
|
||||
|
||||
extract_order_request (l_post : STRING) : detachable ORDER
|
||||
-- extract an object Order from the request, or Void
|
||||
-- if the request is invalid
|
||||
local
|
||||
parser : JSON_PARSER
|
||||
joc : JSON_ORDER_CONVERTER
|
||||
do
|
||||
create joc.make
|
||||
json.add_converter(joc)
|
||||
create parser.make_with_string (l_post)
|
||||
parser.parse_content
|
||||
if
|
||||
parser.is_valid and then
|
||||
attached parser.parsed_json_value as jv
|
||||
then
|
||||
if attached {like extract_order_request} json.object (jv, "ORDER") as res then
|
||||
Result := res
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
note
|
||||
copyright: "2011-2015, Javier Velilla and others"
|
||||
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
|
||||
end
|
||||
@@ -1,58 +0,0 @@
|
||||
note
|
||||
description : "REST Buck server"
|
||||
date : "$Date$"
|
||||
revision : "$Revision$"
|
||||
|
||||
class RESTBUCKS_SERVER
|
||||
|
||||
inherit
|
||||
|
||||
WSF_ROUTED_SKELETON_SERVICE
|
||||
undefine
|
||||
requires_proxy
|
||||
end
|
||||
|
||||
WSF_URI_TEMPLATE_HELPER_FOR_ROUTED_SERVICE
|
||||
|
||||
WSF_HANDLER_HELPER
|
||||
|
||||
WSF_DEFAULT_SERVICE
|
||||
|
||||
WSF_NO_PROXY_POLICY
|
||||
|
||||
create
|
||||
make
|
||||
|
||||
feature {NONE} -- Initialization
|
||||
|
||||
make
|
||||
do
|
||||
initialize_router
|
||||
set_service_option ("port", 9090)
|
||||
make_and_launch
|
||||
end
|
||||
|
||||
setup_router
|
||||
local
|
||||
order_handler: ORDER_HANDLER
|
||||
doc: WSF_ROUTER_SELF_DOCUMENTATION_HANDLER
|
||||
do
|
||||
create order_handler.make_with_router (router)
|
||||
router.handle_with_request_methods ("/order", order_handler, router.methods_POST)
|
||||
router.handle_with_request_methods ("/order/{orderid}", order_handler, router.methods_GET + router.methods_DELETE + router.methods_PUT)
|
||||
create doc.make_hidden (router)
|
||||
router.handle_with_request_methods ("/api/doc", doc, router.methods_GET)
|
||||
end
|
||||
|
||||
|
||||
note
|
||||
copyright: "2011-2013, Javier Velilla 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
|
||||
@@ -1,24 +0,0 @@
|
||||
note
|
||||
description: "Summary description for {ETAG_UTILS}."
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
class
|
||||
ETAG_UTILS
|
||||
|
||||
feature -- Access
|
||||
|
||||
md5_digest (a_string: STRING): STRING
|
||||
-- Cryptographic hash function that produces a 128-bit (16-byte) hash value, based on `a_string'
|
||||
local
|
||||
md5: MD5
|
||||
do
|
||||
create md5.make
|
||||
md5.update_from_string (a_string)
|
||||
Result := md5.digest_as_string
|
||||
end
|
||||
|
||||
note
|
||||
copyright: "2011-2014, Javier Velilla and others"
|
||||
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
|
||||
end
|
||||
@@ -9,41 +9,98 @@ class
|
||||
inherit
|
||||
WSF_EXECUTION
|
||||
|
||||
WSF_URI_REWRITER
|
||||
rename
|
||||
uri as proxy_uri
|
||||
end
|
||||
|
||||
create
|
||||
make
|
||||
|
||||
feature -- Basic operations
|
||||
|
||||
execute
|
||||
local
|
||||
l_forwarded: BOOLEAN
|
||||
do
|
||||
-- NOTE: please enter the target server uri here
|
||||
-- replace "http://localhost:8080/foobar"
|
||||
send_proxy_response ("http://localhost:8080/foobar", Current)
|
||||
-- NOTE: please edit the proxy.conf file
|
||||
across
|
||||
proxy_map as ic
|
||||
until
|
||||
l_forwarded
|
||||
loop
|
||||
if request.path_info.starts_with_general (ic.key) then
|
||||
l_forwarded := True
|
||||
send_proxy_response (ic.key, ic.item, agent proxy_uri (ic.key, ?))
|
||||
end
|
||||
end
|
||||
if not l_forwarded then
|
||||
response.send (create {WSF_PAGE_RESPONSE}.make_with_body ("EiffelWeb proxy: not forwarded!"))
|
||||
end
|
||||
end
|
||||
|
||||
send_proxy_response (a_remote: READABLE_STRING_8; a_rewriter: detachable WSF_URI_REWRITER)
|
||||
proxy_map: HASH_TABLE [STRING, STRING]
|
||||
-- location => target
|
||||
local
|
||||
f: PLAIN_TEXT_FILE
|
||||
l_line: STRING
|
||||
p: INTEGER
|
||||
once ("thread")
|
||||
create Result.make (1)
|
||||
-- Load proxy.conf
|
||||
create f.make_with_name ("proxy.conf")
|
||||
if f.exists and then f.is_access_readable then
|
||||
f.open_read
|
||||
from
|
||||
until
|
||||
f.end_of_file or f.exhausted
|
||||
loop
|
||||
f.read_line
|
||||
l_line := f.last_string
|
||||
if l_line.starts_with ("#") then
|
||||
-- ignore
|
||||
else
|
||||
-- Format:
|
||||
-- path%Tserver
|
||||
p := l_line.index_of ('%T', 1)
|
||||
if p > 0 then
|
||||
Result.force (l_line.substring (p + 1, l_line.count), l_line.head (p - 1))
|
||||
end
|
||||
end
|
||||
end
|
||||
f.close
|
||||
end
|
||||
end
|
||||
|
||||
send_proxy_response (a_location, a_remote: READABLE_STRING_8; a_rewriter: detachable FUNCTION [WSF_REQUEST, STRING])
|
||||
local
|
||||
h: WSF_SIMPLE_REVERSE_PROXY_HANDLER
|
||||
do
|
||||
create h.make (a_remote)
|
||||
h.set_uri_rewriter (a_rewriter)
|
||||
h.set_uri_rewriter (create {WSF_AGENT_URI_REWRITER}.make (agent proxy_uri))
|
||||
h.set_timeout (30) -- 30 seconds
|
||||
if a_rewriter /= Void then
|
||||
h.set_uri_rewriter (create {WSF_AGENT_URI_REWRITER}.make (a_rewriter))
|
||||
end
|
||||
h.set_timeout_ns (10_000_000_000) -- 10 seconds
|
||||
h.set_connect_timeout (5_000) -- milliseconds = 5 seconds
|
||||
|
||||
-- Uncomment following, if you want to provide proxy information
|
||||
-- h.set_header_via (True)
|
||||
-- h.set_header_forwarded (True)
|
||||
-- h.set_header_x_forwarded (True)
|
||||
-- Uncomment following line to keep the original Host value.
|
||||
-- h.keep_proxy_host (True)
|
||||
|
||||
h.execute (request, response)
|
||||
end
|
||||
|
||||
feature -- Helpers
|
||||
|
||||
proxy_uri (a_request: WSF_REQUEST): STRING
|
||||
proxy_uri (a_location: READABLE_STRING_8; a_request: WSF_REQUEST): STRING
|
||||
-- Request uri rewriten as url.
|
||||
do
|
||||
Result := a_request.request_uri
|
||||
-- If related proxy setting is
|
||||
-- a_location=/foo -> http://foo.com
|
||||
-- and if request was http://example.com/foo/bar, it will use http://foo.com/bar
|
||||
-- so the Result here, is "/bar"
|
||||
if Result.starts_with (a_location) then
|
||||
Result.remove_head (a_location.count)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
2
examples/proxy/proxy.conf
Normal file
@@ -0,0 +1,2 @@
|
||||
/google/ http://www.google.com/search?q=eiffel
|
||||
/ http://localhost:8080/testproxy
|
||||
@@ -2,8 +2,8 @@ verbose=true
|
||||
verbose_level=ALERT
|
||||
port=9090
|
||||
#max_concurrent_connections=100
|
||||
keep_alive_timeout=3
|
||||
#max_tcp_clients=100
|
||||
socket_timeout=60
|
||||
socket_recv_timeout=15
|
||||
#keep_alive_timeout=5
|
||||
#max_keep_alive_requests=300
|
||||
#max_tcp_clients=100
|
||||
#socket_timeout=60
|
||||
#socket_recv_timeout=15
|
||||
|
||||
4
examples/simple_compression/service.ini
Normal file
@@ -0,0 +1,4 @@
|
||||
port=9090
|
||||
verbose=true
|
||||
socket_recv_timeout=15
|
||||
keep_alive_timeout=30
|
||||
26
examples/simple_compression/service_compression.e
Normal file
@@ -0,0 +1,26 @@
|
||||
note
|
||||
description : "simple application root class"
|
||||
date : "$Date$"
|
||||
revision : "$Revision$"
|
||||
|
||||
class
|
||||
SERVICE_COMPRESSION
|
||||
|
||||
inherit
|
||||
WSF_DEFAULT_SERVICE [SERVICE_COMPRESSION_EXECUTION]
|
||||
redefine
|
||||
initialize
|
||||
end
|
||||
|
||||
create
|
||||
make_and_launch
|
||||
|
||||
feature {NONE} -- Initialization
|
||||
|
||||
initialize
|
||||
do
|
||||
Precursor
|
||||
import_service_options (create {WSF_SERVICE_LAUNCHER_OPTIONS_FROM_INI}.make_from_file ("service.ini"))
|
||||
end
|
||||
|
||||
end
|
||||
22
examples/simple_compression/service_compression.ecf
Normal file
@@ -0,0 +1,22 @@
|
||||
<?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="service_compression" uuid="C28C4F53-9963-46C0-A080-8F13E94E7486">
|
||||
<target name="service_compression">
|
||||
<root class="SERVICE_COMPRESSION" feature="make_and_launch"/>
|
||||
<option warning="true" void_safety="all">
|
||||
<assertions precondition="true" postcondition="true" check="true" invariant="true" loop="true" supplier_precondition="true"/>
|
||||
</option>
|
||||
<setting name="concurrency" value="scoop"/>
|
||||
<library name="base" location="$ISE_LIBRARY\library\base\base-safe.ecf"/>
|
||||
<library name="default_standalone" location="..\..\library\server\wsf\default\standalone-safe.ecf"/>
|
||||
<library name="http" location="..\..\library\network\protocol\http\http-safe.ecf"/>
|
||||
<library name="wsf" location="..\..\library\server\wsf\wsf-safe.ecf" readonly="false"/>
|
||||
<library name="wsf_compression" location="..\..\library\server\wsf\wsf_compression-safe.ecf" readonly="false"/>
|
||||
<cluster name="service_compression" location=".\" recursive="true">
|
||||
<file_rule>
|
||||
<exclude>/EIFGENs$</exclude>
|
||||
<exclude>/CVS$</exclude>
|
||||
<exclude>/.svn$</exclude>
|
||||
</file_rule>
|
||||
</cluster>
|
||||
</target>
|
||||
</system>
|
||||
68
examples/simple_compression/service_compression_execution.e
Normal file
@@ -0,0 +1,68 @@
|
||||
note
|
||||
description: "Simple file execution, serving home.html, ewf.png and 404.html"
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
class
|
||||
SERVICE_COMPRESSION_EXECUTION
|
||||
|
||||
inherit
|
||||
WSF_ROUTED_EXECUTION
|
||||
redefine
|
||||
initialize,
|
||||
execute_default
|
||||
end
|
||||
|
||||
create
|
||||
make
|
||||
|
||||
feature {NONE} -- Initialization
|
||||
|
||||
|
||||
initialize
|
||||
-- Initialize current service.
|
||||
do
|
||||
Precursor
|
||||
initialize_router
|
||||
end
|
||||
|
||||
setup_router
|
||||
local
|
||||
fhdl_with_compression: WSF_FILE_SYSTEM_HANDLER_WITH_COMPRESSION
|
||||
fhdl: WSF_FILE_SYSTEM_HANDLER
|
||||
do
|
||||
create fhdl_with_compression.make_hidden ("www")
|
||||
fhdl_with_compression.set_directory_index (<<"index.html">>)
|
||||
fhdl_with_compression.compression.set_default_compression_format
|
||||
fhdl_with_compression.compression.enable_compression_for_media_type ({HTTP_MIME_TYPES}.image_jpg)
|
||||
fhdl_with_compression.set_not_found_handler (agent (ia_uri: READABLE_STRING_8; ia_req: WSF_REQUEST; ia_res: WSF_RESPONSE)
|
||||
do
|
||||
execute_default (ia_req, ia_res)
|
||||
end)
|
||||
router.handle ("/compressed/", fhdl_with_compression, router.methods_GET)
|
||||
|
||||
create fhdl.make_hidden ("www")
|
||||
fhdl.set_directory_index (<<"index.html">>)
|
||||
fhdl.set_not_found_handler (agent (ia_uri: READABLE_STRING_8; ia_req: WSF_REQUEST; ia_res: WSF_RESPONSE)
|
||||
do
|
||||
execute_default (ia_req, ia_res)
|
||||
end)
|
||||
router.handle ("/", fhdl, router.methods_GET)
|
||||
end
|
||||
|
||||
|
||||
execute_default (req: WSF_REQUEST; res: WSF_RESPONSE)
|
||||
-- Dispatch requests without a matching handler.
|
||||
local
|
||||
not_found: WSF_NOT_FOUND_RESPONSE
|
||||
mesg: WSF_RESPONSE_MESSAGE
|
||||
do
|
||||
create not_found.make (request)
|
||||
not_found.add_suggested_location (request.absolute_script_url (""), "Home", "Back to home page")
|
||||
mesg := not_found
|
||||
res.send (mesg)
|
||||
end
|
||||
|
||||
|
||||
|
||||
end
|
||||
77705
examples/simple_compression/www/big_file.html
Normal file
88332
examples/simple_compression/www/big_file2.html
Normal file
BIN
examples/simple_compression/www/eiffel.jpg
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
examples/simple_compression/www/ewf.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
27
examples/simple_compression/www/index.html
Normal file
@@ -0,0 +1,27 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>EWF simple_file example</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>EWF simple_file example</h1>
|
||||
<p>This is a static html file served by EWF.</p>
|
||||
<p>Try to <a href="nowhere.html">get lost</a>.</p>
|
||||
<div width="45%" style="display: inline-block; border: solid 1px black; padding: 10px; margin: 10px;">
|
||||
<h2>Without any compression</h2>
|
||||
<a href="ewf.png"><img src="ewf.png"/></a>
|
||||
<p>This is the real Eiffel tower.</p>
|
||||
<a href="eiffel.jpg"><img src="eiffel.jpg"/></a>
|
||||
<p>Try to <a href="big_file2.html">load a big file</a>.</p>
|
||||
</div>
|
||||
|
||||
<div width="45%" style="display: inline-block; border: solid 1px black; padding: 10px; margin: 10px;">
|
||||
<h2>With gzip compression</h2>
|
||||
<a href="compressed/ewf.png"><img src="compressed/ewf.png"/></a>
|
||||
<p>This is the real Eiffel tower.</p>
|
||||
<a href="compressed/eiffel.jpg"><img src="compressed/eiffel.jpg"/></a>
|
||||
<p>Try to <a href="compressed/big_file2.html">load a compressed big file</a>.</p>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -10,6 +10,9 @@ inherit
|
||||
WSF_WEBSOCKET_EXECUTION
|
||||
|
||||
WEB_SOCKET_EVENT_I
|
||||
redefine
|
||||
on_timer
|
||||
end
|
||||
|
||||
create
|
||||
make
|
||||
@@ -23,22 +26,27 @@ feature -- Basic operations
|
||||
do
|
||||
-- To send a response we need to setup, the status code and
|
||||
-- the response headers.
|
||||
if request.path_info.same_string_general ("/app") then
|
||||
s := websocket_app_html (9090)
|
||||
if request.path_info.same_string_general ("/favicon.ico") then
|
||||
response.put_header ({HTTP_STATUS_CODE}.not_found, <<["Content-Length", "0"]>>)
|
||||
else
|
||||
s := "Hello World!"
|
||||
create dt.make_now_utc
|
||||
s.append (" (UTC time is " + dt.rfc850_string + ").")
|
||||
s.append ("<p><a href=%"/app%">Websocket demo</a></p>")
|
||||
if request.path_info.same_string_general ("/app") then
|
||||
s := websocket_app_html (request.server_name, request.server_port)
|
||||
|
||||
else
|
||||
s := "Hello World!"
|
||||
create dt.make_now_utc
|
||||
s.append (" (UTC time is " + dt.rfc850_string + ").")
|
||||
s.append ("<p><a href=%"/app%">Websocket demo</a></p>")
|
||||
end
|
||||
response.put_header ({HTTP_STATUS_CODE}.ok, <<["Content-Type", "text/html"], ["Content-Length", s.count.out]>>)
|
||||
response.set_status_code ({HTTP_STATUS_CODE}.ok)
|
||||
response.header.put_content_type_text_html
|
||||
response.header.put_content_length (s.count)
|
||||
if request.is_keep_alive_http_connection then
|
||||
response.header.put_connection_keep_alive
|
||||
end
|
||||
response.put_string (s)
|
||||
end
|
||||
response.put_header ({HTTP_STATUS_CODE}.ok, <<["Content-Type", "text/html"], ["Content-Length", s.count.out]>>)
|
||||
response.set_status_code ({HTTP_STATUS_CODE}.ok)
|
||||
response.header.put_content_type_text_html
|
||||
response.header.put_content_length (s.count)
|
||||
if request.is_keep_alive_http_connection then
|
||||
response.header.put_connection_keep_alive
|
||||
end
|
||||
response.put_string (s)
|
||||
end
|
||||
|
||||
feature -- Websocket execution
|
||||
@@ -52,6 +60,9 @@ feature -- Websocket execution
|
||||
|
||||
on_open (ws: WEB_SOCKET)
|
||||
do
|
||||
initialize_commands
|
||||
set_timer_delay (1) -- Every 1 second.
|
||||
|
||||
ws.put_error ("Connecting")
|
||||
ws.send (Text_frame, "Hello, this is a simple demo with Websocket using Eiffel. (/help for more information).%N")
|
||||
end
|
||||
@@ -62,12 +73,26 @@ feature -- Websocket execution
|
||||
end
|
||||
|
||||
on_text (ws: WEB_SOCKET; a_message: READABLE_STRING_8)
|
||||
local
|
||||
i: INTEGER
|
||||
cmd_name: READABLE_STRING_8
|
||||
do
|
||||
if a_message.same_string_general ("/help") then
|
||||
-- Echo the message for testing.
|
||||
ws.send (Text_frame, "Help: available commands%N - /time : return the server UTC time.%N")
|
||||
elseif a_message.starts_with_general ("/time") then
|
||||
ws.send (Text_frame, "Server time is " + (create {HTTP_DATE}.make_now_utc).string)
|
||||
if a_message.starts_with_general ("/") then
|
||||
from
|
||||
i := 1
|
||||
until
|
||||
i >= a_message.count or else a_message[i + 1].is_space
|
||||
loop
|
||||
i := i + 1
|
||||
end
|
||||
cmd_name := a_message.substring (2, i)
|
||||
if attached command (cmd_name) as cmd then
|
||||
cmd (ws, a_message.substring (i + 1, a_message.count))
|
||||
elseif a_message.same_string_general ("/help") then
|
||||
on_help_command (ws, Void)
|
||||
else
|
||||
ws.send (Text_frame, "Error: unknown command '/" + cmd_name + "'!%N")
|
||||
end
|
||||
else
|
||||
-- Echo the message for testing.
|
||||
ws.send (Text_frame, a_message)
|
||||
@@ -80,9 +105,91 @@ feature -- Websocket execution
|
||||
ws.put_error ("Connection closed")
|
||||
end
|
||||
|
||||
on_timer (ws: WEB_SOCKET)
|
||||
-- <Precursor>.
|
||||
-- If ever the file ".stop" exists, stop gracefully the connection.
|
||||
local
|
||||
fut: FILE_UTILITIES
|
||||
f: RAW_FILE
|
||||
do
|
||||
if fut.file_exists (".stop") then
|
||||
ws.send_text ("End of the communication ...%N")
|
||||
ws.send_connection_close ("")
|
||||
create f.make_with_name (".stop")
|
||||
f.delete
|
||||
end
|
||||
end
|
||||
|
||||
feature -- Command
|
||||
|
||||
initialize_commands
|
||||
do
|
||||
register_command (agent on_help_command, "help", "Display this help.")
|
||||
register_command (agent on_time_command, "time", "Return the server UTC time.")
|
||||
register_command (agent on_shutdown_command, "shutdown", "Shutdown the service (ends the websocket).")
|
||||
end
|
||||
|
||||
register_command (a_cmd: attached like command; a_name: READABLE_STRING_8; a_description: READABLE_STRING_8)
|
||||
local
|
||||
tb: like commands
|
||||
do
|
||||
tb := commands
|
||||
if tb = Void then
|
||||
create tb.make_caseless (1)
|
||||
commands := tb
|
||||
end
|
||||
tb.force ([a_cmd, a_name, a_description], a_name)
|
||||
end
|
||||
|
||||
commands: detachable STRING_TABLE [TUPLE [cmd: attached like command; name, description: READABLE_STRING_8]]
|
||||
|
||||
command (a_name: READABLE_STRING_GENERAL): detachable PROCEDURE [TUPLE [ws: WEB_SOCKET; args: detachable READABLE_STRING_GENERAL]]
|
||||
do
|
||||
if
|
||||
attached commands as tb and then
|
||||
attached tb.item (a_name) as d
|
||||
then
|
||||
Result := d.cmd
|
||||
end
|
||||
end
|
||||
|
||||
on_help_command (ws: WEB_SOCKET; args: detachable READABLE_STRING_GENERAL)
|
||||
local
|
||||
s: STRING
|
||||
do
|
||||
create s.make_from_string ("Help: available commands:%N")
|
||||
if attached commands as tb then
|
||||
across
|
||||
tb as ic
|
||||
loop
|
||||
s.append ("<li> /")
|
||||
s.append (ic.item.name)
|
||||
s.append (" : ")
|
||||
s.append (ic.item.description)
|
||||
s.append ("</li>%N")
|
||||
end
|
||||
end
|
||||
ws.send_text (s)
|
||||
end
|
||||
|
||||
on_time_command (ws: WEB_SOCKET; args: detachable READABLE_STRING_GENERAL)
|
||||
do
|
||||
ws.send_text ("Server time is " + (create {HTTP_DATE}.make_now_utc).string)
|
||||
end
|
||||
|
||||
on_shutdown_command (ws: WEB_SOCKET; args: detachable READABLE_STRING_GENERAL)
|
||||
local
|
||||
f: RAW_FILE
|
||||
do
|
||||
ws.send_text ("Active websockets will end soon.%N")
|
||||
create f.make_create_read_write (".stop")
|
||||
f.put_string ("stop%N")
|
||||
f.close
|
||||
end
|
||||
|
||||
feature -- HTML Resource
|
||||
|
||||
websocket_app_html (a_port: INTEGER): STRING
|
||||
websocket_app_html (a_host: STRING; a_port: INTEGER): STRING
|
||||
do
|
||||
Result := "[
|
||||
<!DOCTYPE html>
|
||||
@@ -96,7 +203,7 @@ $(document).ready(function() {
|
||||
|
||||
function connect(){
|
||||
|
||||
var host = "##WSSCHEME##://127.0.0.1:##PORTNUMBER##/app";
|
||||
var host = "##WSSCHEME##://##HOSTNAME##:##PORTNUMBER##/app";
|
||||
|
||||
try{
|
||||
|
||||
@@ -178,6 +285,7 @@ body {font-family:Arial, Helvetica, sans-serif;}
|
||||
</body>
|
||||
</html>
|
||||
]"
|
||||
Result.replace_substring_all ("##HOSTNAME##", a_host)
|
||||
Result.replace_substring_all ("##PORTNUMBER##", a_port.out)
|
||||
if request.is_https then
|
||||
Result.replace_substring_all ("##HTTPSCHEME##", "https")
|
||||
@@ -188,5 +296,4 @@ body {font-family:Arial, Helvetica, sans-serif;}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
<target name="websocket_app">
|
||||
<root class="APPLICATION" feature="make_and_launch"/>
|
||||
<file_rule>
|
||||
<exclude>/\.svn$</exclude>
|
||||
<exclude>/CVS$</exclude>
|
||||
<exclude>/EIFGENs$</exclude>
|
||||
<exclude>/\.svn$</exclude>
|
||||
</file_rule>
|
||||
<option warning="true">
|
||||
<assertions precondition="true" postcondition="true" check="true" invariant="true" loop="true" supplier_precondition="true"/>
|
||||
@@ -17,6 +17,14 @@
|
||||
<library name="wsf" location="..\..\library\server\wsf\wsf.ecf"/>
|
||||
<cluster name="app" location=".\" recursive="true"/>
|
||||
</target>
|
||||
<target name="websocket_app_st" extends="websocket_app">
|
||||
<description>Single thread solution.
|
||||
Warning: as it can not handle concurrent request, it is recommended to set Keep-Alive-Timeout to very low value, as browser will keep persistent connection open too long.
|
||||
</description>
|
||||
<capability>
|
||||
<concurrency use="none"/>
|
||||
</capability>
|
||||
</target>
|
||||
<target name="websocket_app_ssl" extends="websocket_app">
|
||||
<variable name="ssl_enabled" value="true"/>
|
||||
</target>
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
verbose=true
|
||||
verbose_level=INFORMATION
|
||||
port=9090
|
||||
|
||||
#Socket and timeout
|
||||
max_concurrent_connections=100
|
||||
keep_alive_timeout=35
|
||||
max_tcp_clients=100
|
||||
socket_timeout=30
|
||||
socket_recv_timeout=5
|
||||
max_keep_alive_requests=300
|
||||
socket_recv_timeout=100 ms
|
||||
|
||||
#Persistent connections
|
||||
keep_alive_timeout=50 ms
|
||||
max_keep_alive_requests=-1
|
||||
|
||||
#SSL
|
||||
is_secure=false
|
||||
secure_certificate=ca.crt
|
||||
secure_certificate_key=ca.key
|
||||
|
||||
#Debug
|
||||
verbose=true
|
||||
verbose_level=INFORMATION
|
||||
|
||||
@@ -10,6 +10,9 @@ It provides simple routine to perform http requests, and get response.
|
||||
- Eiffel Net library
|
||||
- and optionally Eiffel NetSSL library to support `https://...`
|
||||
|
||||
* Note: set ciphers setting is supported only with libcurl implementation for now, net implementation
|
||||
set all the ciphers as part of the OpenSSL initialization.
|
||||
|
||||
This means on Windows, do not forget to copy the libcurl.dll (and related) either in the same directory of the executable, or ensure the .dll are in the PATH environment.
|
||||
|
||||
It is possible to exclude the libcurl implementation xor the Eiffel Net implementation:
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
<exclude>/\.svn$</exclude>
|
||||
<exclude>/EIFGENs$</exclude>
|
||||
</file_rule>
|
||||
<option warning="true">
|
||||
</option>
|
||||
<library name="base" location="$ISE_LIBRARY\library\base\base.ecf"/>
|
||||
<library name="curl" location="$ISE_LIBRARY\library\cURL\cURL.ecf">
|
||||
<condition>
|
||||
@@ -32,7 +30,8 @@
|
||||
</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="implementation" location="$|implementation\" recursive="true" hidden="true"/>
|
||||
<cluster name="parameters" location="$|parameters\" recursive="true"/>
|
||||
<cluster name="spec_null" location="$|spec\null\" recursive="true"/>
|
||||
<cluster name="spec_net" location="$|spec\net\">
|
||||
<condition>
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
<exclude>/\.svn$</exclude>
|
||||
<exclude>/EIFGENs$</exclude>
|
||||
</file_rule>
|
||||
<option warning="true">
|
||||
</option>
|
||||
<library name="base" location="$ISE_LIBRARY\library\base\base.ecf"/>
|
||||
<library name="curl" location="$ISE_LIBRARY\library\cURL\cURL.ecf"/>
|
||||
<library name="encoder" location="..\..\text\encoder\encoder.ecf"/>
|
||||
@@ -16,6 +14,8 @@
|
||||
<library name="net" location="$ISE_LIBRARY\library\net\net.ecf"/>
|
||||
<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="parameters" location="$|parameters\" recursive="true"/>
|
||||
<cluster name="spec_libcurl" location="$|spec\libcurl\" recursive="true"/>
|
||||
<cluster name="default_libcurl" location="$|default\libcurl\"/>
|
||||
</cluster>
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
<exclude>/\.svn$</exclude>
|
||||
<exclude>/EIFGENs$</exclude>
|
||||
</file_rule>
|
||||
<option warning="true">
|
||||
</option>
|
||||
<library name="base" location="$ISE_LIBRARY\library\base\base.ecf"/>
|
||||
<library name="encoder" location="..\..\text\encoder\encoder.ecf"/>
|
||||
<library name="http" location="..\protocol\http\http.ecf"/>
|
||||
@@ -25,6 +23,8 @@
|
||||
</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="parameters" location="$|parameters\" recursive="true"/>
|
||||
<cluster name="spec_net" location="$|spec\net\">
|
||||
<cluster name="net_implementation" location="$|implementation\" hidden="true"/>
|
||||
</cluster>
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
package http_client
|
||||
|
||||
project
|
||||
http_client = "http_client-safe.ecf"
|
||||
http_client = "http_client.ecf"
|
||||
libcurl_http_client = "libcurl_http_client-safe.ecf"
|
||||
libcurl_http_client = "libcurl_http_client.ecf"
|
||||
net_http_client = "net_http_client-safe.ecf"
|
||||
net_http_client = "net_http_client.ecf"
|
||||
|
||||
note
|
||||
title: HTTP client
|
||||
description: "[
|
||||
Provides simple routines to perform http requests, and get associated response.
|
||||
It has two implementations:
|
||||
- using Eiffel cURL (i.e libcurl)
|
||||
- using EiffelNET (and the EiffelNET SSL extension)
|
||||
]"
|
||||
collection:EWF
|
||||
description: "[
|
||||
Provides simple routines to perform http requests, and get associated response.
|
||||
It has two implementations:
|
||||
- using Eiffel cURL (i.e libcurl)
|
||||
- using EiffelNET (and the EiffelNET SSL extension)
|
||||
]"
|
||||
|
||||
collection: EWF
|
||||
tags: http,client,network,request,web,curl,EWF
|
||||
copyright: 1984-2016 Eiffel Software and others
|
||||
license: Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)
|
||||
|
||||
@@ -16,8 +16,19 @@ feature -- Access
|
||||
deferred
|
||||
end
|
||||
|
||||
get (a_url: READABLE_STRING_8; ctx: detachable HTTP_CLIENT_REQUEST_CONTEXT): HTTP_CLIENT_RESPONSE
|
||||
do
|
||||
Result := new_session (a_url).get ("", ctx)
|
||||
end
|
||||
|
||||
custom (a_method: READABLE_STRING_8; a_url: READABLE_STRING_8; ctx: detachable HTTP_CLIENT_REQUEST_CONTEXT): HTTP_CLIENT_RESPONSE
|
||||
-- Response for `a_method' request based on `a_url' and optional `ctx'.
|
||||
do
|
||||
Result := new_session (a_url).custom (a_method, "", ctx)
|
||||
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
|
||||
|
||||
@@ -30,8 +30,19 @@ feature {NONE} -- Initialization
|
||||
-- Initialize Current with `a_url' and `ctx'.
|
||||
-- This can be used to reset/reinitialize Current with new url
|
||||
-- in the case of redirection.
|
||||
local
|
||||
i: INTEGER
|
||||
do
|
||||
url := a_url
|
||||
i := a_url.substring_index ("://", 1)
|
||||
if i > 0 then
|
||||
check
|
||||
a_url.head (i - 1).same_string ("http")
|
||||
or a_url.head (i - 1).same_string ("https")
|
||||
end
|
||||
url := a_url
|
||||
else
|
||||
url := session.url (a_url, Void)
|
||||
end
|
||||
headers := session.headers.twin
|
||||
if ctx /= Void then
|
||||
context := ctx
|
||||
@@ -140,14 +151,14 @@ feature -- Settings
|
||||
|
||||
timeout: INTEGER
|
||||
-- HTTP transaction timeout in seconds.
|
||||
--| 0 means it nevers timeout
|
||||
--| 0 means it never timeouts
|
||||
do
|
||||
Result := session.timeout
|
||||
end
|
||||
|
||||
connect_timeout: INTEGER
|
||||
-- HTTP connection timeout in seconds.
|
||||
--| 0 means it nevers timeout
|
||||
-- HTTP connection timeout in milliseconds.
|
||||
--| 0 means it never timeouts
|
||||
do
|
||||
Result := session.connect_timeout
|
||||
end
|
||||
@@ -197,7 +208,7 @@ feature -- Settings
|
||||
end
|
||||
|
||||
note
|
||||
copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others"
|
||||
copyright: "2011-2018, Jocelyn Fiat, Javier Velilla, Eiffel Software and others"
|
||||
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
|
||||
source: "[
|
||||
Eiffel Software
|
||||
|
||||
@@ -58,11 +58,11 @@ feature -- Access
|
||||
-- Specific headers to use in addition to the one set in the related HTTP_CLIENT_SESSION
|
||||
--| note: the value from Current context override the one from the session in case of conflict
|
||||
|
||||
query_parameters: HASH_TABLE [READABLE_STRING_32, READABLE_STRING_32]
|
||||
query_parameters: HTTP_CLIENT_REQUEST_QUERY_PARAMETERS
|
||||
-- Query parameters to be appended to the url
|
||||
--| note: if the url already contains a query_string, the `query_parameters' will be appended to the url
|
||||
|
||||
form_parameters: HASH_TABLE [READABLE_STRING_32, READABLE_STRING_32]
|
||||
form_parameters: HTTP_CLIENT_REQUEST_FORM_PARAMETERS
|
||||
-- Form parameters
|
||||
|
||||
upload_data: detachable READABLE_STRING_8
|
||||
@@ -145,13 +145,25 @@ feature -- Element change
|
||||
add_query_parameter (k: READABLE_STRING_GENERAL; v: READABLE_STRING_GENERAL)
|
||||
-- Add a query parameter `k=v'.
|
||||
do
|
||||
query_parameters.force (v.to_string_32, k.to_string_32)
|
||||
query_parameters.force (create {HTTP_CLIENT_REQUEST_STRING_PARAMETER}.make (k, v))
|
||||
end
|
||||
|
||||
add_form_parameter (k: READABLE_STRING_GENERAL; v: READABLE_STRING_GENERAL)
|
||||
-- Add a form parameter `k'= `v'.
|
||||
do
|
||||
form_parameters.force (v.to_string_32, k.to_string_32)
|
||||
form_parameters.force (create {HTTP_CLIENT_REQUEST_STRING_PARAMETER}.make (k, v))
|
||||
end
|
||||
|
||||
add_file_form_parameter (k: READABLE_STRING_GENERAL; a_location: READABLE_STRING_GENERAL; a_content_type: detachable READABLE_STRING_8)
|
||||
-- Add a form file parameter named `k`, located at `a_location`, with optional content type `a_content_type`.
|
||||
require
|
||||
has_no_upload_data_or_filename: not has_upload_data and not has_upload_filename
|
||||
local
|
||||
param: HTTP_CLIENT_REQUEST_FILE_PARAMETER
|
||||
do
|
||||
create param.make_with_path (k, create {PATH}.make_from_string (a_location))
|
||||
param.set_content_type (a_content_type)
|
||||
form_parameters.force (param)
|
||||
end
|
||||
|
||||
set_credentials_required (b: BOOLEAN)
|
||||
@@ -164,7 +176,8 @@ feature -- Element change
|
||||
-- Set `upload_data' to `a_data'
|
||||
--| note: the Current context can have upload_data XOR upload_filename, but not both.
|
||||
require
|
||||
has_upload_filename: (a_data /= Void and then not a_data.is_empty) implies not has_upload_filename
|
||||
has_no_upload_filename: (a_data /= Void and then not a_data.is_empty) implies not has_upload_filename
|
||||
has_no_form_data: (a_data /= Void and then not a_data.is_empty) implies not has_form_data
|
||||
do
|
||||
if a_data = Void or else a_data.is_empty then
|
||||
upload_data := Void
|
||||
@@ -180,6 +193,7 @@ feature -- Element change
|
||||
--| note: the Current context can have upload_data XOR upload_filename, but not both.
|
||||
require
|
||||
has_no_upload_data: (a_fn /= Void and then not a_fn.is_empty) implies not has_upload_data
|
||||
has_no_form_data: (a_fn /= Void and then not a_fn.is_empty) implies not has_form_data
|
||||
do
|
||||
if a_fn = Void or else a_fn.is_empty then
|
||||
upload_filename := Void
|
||||
@@ -266,9 +280,9 @@ feature -- URL helpers
|
||||
a_url.append_character ('&')
|
||||
end
|
||||
l_first_param := False
|
||||
uri_percent_encoder.append_query_name_encoded_string_to (ic.key, a_url)
|
||||
uri_percent_encoder.append_query_name_encoded_string_to (ic.item.name, a_url)
|
||||
a_url.append_character ('=')
|
||||
uri_percent_encoder.append_query_value_encoded_string_to (ic.item, a_url)
|
||||
ic.item.append_query_value_encoded_to (a_url)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -315,38 +329,35 @@ 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'.
|
||||
parameters_to_uri_percent_encoded_string (a_params: HTTP_CLIENT_REQUEST_PARAMETERS [HTTP_CLIENT_REQUEST_PARAMETER]): STRING_8
|
||||
-- Build query urlencoded string using parameters from `a_params'.
|
||||
do
|
||||
create Result.make (64)
|
||||
across
|
||||
ht as ic
|
||||
a_params 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)
|
||||
uri_percent_encoder.append_query_name_encoded_string_to (ic.item.name, Result)
|
||||
Result.append_character ('=')
|
||||
uri_percent_encoder.append_query_value_encoded_string_to (ic.item, Result)
|
||||
ic.item.append_query_value_encoded_to (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'.
|
||||
parameters_to_x_www_form_urlencoded_string (a_params: HTTP_CLIENT_REQUEST_PARAMETERS [HTTP_CLIENT_REQUEST_PARAMETER]): STRING_8
|
||||
-- Build x-www-form-urlencoded string using parameters from `a_params'.
|
||||
do
|
||||
create Result.make (64)
|
||||
from
|
||||
ht.start
|
||||
until
|
||||
ht.after
|
||||
across
|
||||
a_params as ic
|
||||
loop
|
||||
if not Result.is_empty then
|
||||
Result.append_character ('&')
|
||||
end
|
||||
Result.append (x_www_form_url_encoder.encoded_string (ht.key_for_iteration))
|
||||
x_www_form_url_encoder.append_percent_encoded_string_to (ic.item.name, Result)
|
||||
Result.append_character ('=')
|
||||
Result.append (x_www_form_url_encoder.encoded_string (ht.item_for_iteration))
|
||||
ht.forth
|
||||
ic.item.append_form_url_encoded_to (Result)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -61,12 +61,40 @@ feature -- Access
|
||||
url (a_path: READABLE_STRING_8; ctx: detachable HTTP_CLIENT_REQUEST_CONTEXT): STRING_8
|
||||
-- Url computed from Current and `ctx' data.
|
||||
do
|
||||
Result := base_url + a_path
|
||||
if is_absolute_url (a_path) then
|
||||
-- Is Absolute url
|
||||
Result := a_path
|
||||
else
|
||||
Result := base_url + a_path
|
||||
end
|
||||
if ctx /= Void then
|
||||
ctx.append_query_parameters_to_url (Result)
|
||||
end
|
||||
end
|
||||
|
||||
is_absolute_url (s: READABLE_STRING_GENERAL): BOOLEAN
|
||||
-- Does `s` represent an absolute url?
|
||||
local
|
||||
i, pos: INTEGER
|
||||
sch: READABLE_STRING_GENERAL
|
||||
do
|
||||
pos := s.substring_index ("://", 1)
|
||||
if pos > 0 then
|
||||
sch := s.head (pos - 1)
|
||||
if not sch.is_whitespace then
|
||||
from
|
||||
i := 1
|
||||
Result := True
|
||||
until
|
||||
not Result or i > sch.count
|
||||
loop
|
||||
Result := sch[i].is_alpha_numeric
|
||||
i := i + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
feature {NONE} -- Access: verbose
|
||||
|
||||
verbose_mode: INTEGER
|
||||
@@ -272,6 +300,14 @@ feature -- Authentication
|
||||
-- Associated optional credentials value.
|
||||
-- Computed as `username':`password'.
|
||||
|
||||
ciphers_setting: detachable READABLE_STRING_8
|
||||
-- SSL cipher preference lists
|
||||
-- examples: DEFAULT, ALL, TLSv1
|
||||
-- check https://www.openssl.org/docs/man1.1.0/apps/ciphers.html
|
||||
-- Warning: At the moment only used for LIB_CURL_HTTP_CLIENT
|
||||
-- Net implementation set all the ciphers using the OpenSSL at
|
||||
-- initialization time.
|
||||
|
||||
feature -- Status setting
|
||||
|
||||
set_is_debug (b: BOOLEAN)
|
||||
@@ -401,6 +437,14 @@ feature -- Element change
|
||||
chunk_size := a_size
|
||||
end
|
||||
|
||||
set_ciphers_setting (a_ciphers_setting: READABLE_STRING_8)
|
||||
-- Set 'ciphers_setting' with 'a_ciphers_setting'.
|
||||
do
|
||||
create {STRING_8} ciphers_setting.make_from_string (a_ciphers_setting)
|
||||
ensure
|
||||
ciphers_setting_set: attached ciphers_setting as c_setting and then c_setting.same_string (a_ciphers_setting)
|
||||
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)"
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
note
|
||||
description: "Summary description for {HTTP_CLIENT_REQUEST_FORM_PARAMETERS}."
|
||||
author: ""
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
class
|
||||
HTTP_CLIENT_REQUEST_FORM_PARAMETERS
|
||||
|
||||
inherit
|
||||
HTTP_CLIENT_REQUEST_PARAMETERS [HTTP_CLIENT_REQUEST_PARAMETER]
|
||||
|
||||
create
|
||||
make
|
||||
|
||||
feature -- Status report
|
||||
|
||||
has_file_parameter: BOOLEAN
|
||||
-- Has any file parameter?
|
||||
do
|
||||
Result := across items as ic some attached {HTTP_CLIENT_REQUEST_FILE_PARAMETER} ic.item end
|
||||
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
|
||||
@@ -0,0 +1,83 @@
|
||||
note
|
||||
description: "Summary description for {HTTP_CLIENT_REQUEST_PARAMETER}."
|
||||
author: ""
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
deferred class
|
||||
HTTP_CLIENT_REQUEST_PARAMETER
|
||||
|
||||
inherit
|
||||
DEBUG_OUTPUT
|
||||
|
||||
feature -- Access
|
||||
|
||||
name: READABLE_STRING_32
|
||||
|
||||
content_type: detachable READABLE_STRING_8
|
||||
|
||||
count: INTEGER
|
||||
-- Integer representing the length of source value.
|
||||
deferred
|
||||
end
|
||||
|
||||
feature -- Status report
|
||||
|
||||
debug_output: STRING_32
|
||||
do
|
||||
create Result.make_empty
|
||||
Result.append (name)
|
||||
Result.append ("=...")
|
||||
end
|
||||
|
||||
feature -- Conversion
|
||||
|
||||
append_form_url_encoded_to (a_output: STRING_8)
|
||||
-- Append as form url encoded string to `a_output`.
|
||||
deferred
|
||||
end
|
||||
|
||||
append_query_value_encoded_to (a_output: STRING_8)
|
||||
deferred
|
||||
end
|
||||
|
||||
append_as_mime_encoded_to (a_output: STRING_8)
|
||||
deferred
|
||||
end
|
||||
|
||||
feature -- Element change
|
||||
|
||||
set_name (a_name: READABLE_STRING_GENERAL)
|
||||
do
|
||||
name := a_name.as_string_32
|
||||
end
|
||||
|
||||
set_content_type (ct: detachable READABLE_STRING_8)
|
||||
do
|
||||
content_type := ct
|
||||
end
|
||||
|
||||
feature {NONE} -- Implementation
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
@@ -0,0 +1,67 @@
|
||||
note
|
||||
description: "Summary description for {HTTP_CLIENT_REQUEST_PARAMETERS}."
|
||||
author: ""
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
deferred class
|
||||
HTTP_CLIENT_REQUEST_PARAMETERS [G -> HTTP_CLIENT_REQUEST_PARAMETER]
|
||||
|
||||
inherit
|
||||
ITERABLE [G]
|
||||
|
||||
feature {NONE} -- Initialization
|
||||
|
||||
make (nb: INTEGER)
|
||||
do
|
||||
create items.make (nb)
|
||||
end
|
||||
|
||||
feature -- Access
|
||||
|
||||
is_empty: BOOLEAN
|
||||
do
|
||||
Result := items.is_empty
|
||||
end
|
||||
|
||||
count: INTEGER
|
||||
do
|
||||
Result := items.count
|
||||
end
|
||||
|
||||
has (a_parameter_name: READABLE_STRING_GENERAL): BOOLEAN
|
||||
do
|
||||
Result := across items as ic some a_parameter_name.same_string (ic.item.name) end
|
||||
end
|
||||
|
||||
feature -- Element change
|
||||
|
||||
extend, force (i: G)
|
||||
do
|
||||
items.force (i)
|
||||
end
|
||||
|
||||
feature -- Iteration
|
||||
|
||||
new_cursor: ARRAYED_LIST_ITERATION_CURSOR [G]
|
||||
-- <Precursor>
|
||||
do
|
||||
Result := items.new_cursor
|
||||
end
|
||||
|
||||
feature {NONE} -- Implementation
|
||||
|
||||
items: ARRAYED_LIST [G]
|
||||
|
||||
invariant
|
||||
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
|
||||
@@ -1,23 +1,20 @@
|
||||
note
|
||||
description: "Summary description for {WSF_DEFAULT_SERVICE_LAUNCHER}."
|
||||
description: "Summary description for {HTTP_CLIENT_REQUEST_QUERY_PARAMETERS}."
|
||||
author: ""
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
class
|
||||
WSF_DEFAULT_SERVICE_LAUNCHER
|
||||
HTTP_CLIENT_REQUEST_QUERY_PARAMETERS
|
||||
|
||||
inherit
|
||||
WSF_LIBFCGI_SERVICE_LAUNCHER
|
||||
HTTP_CLIENT_REQUEST_PARAMETERS [HTTP_CLIENT_REQUEST_STRING_PARAMETER]
|
||||
|
||||
create
|
||||
make,
|
||||
make_and_launch,
|
||||
make_callback,
|
||||
make_callback_and_launch
|
||||
make
|
||||
|
||||
note
|
||||
copyright: "2011-2012, 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
|
||||
@@ -26,5 +23,4 @@ note
|
||||
Website http://www.eiffel.com
|
||||
Customer support http://support.eiffel.com
|
||||
]"
|
||||
|
||||
end
|
||||
@@ -0,0 +1,155 @@
|
||||
note
|
||||
description: "Summary description for {HTTP_CLIENT_REQUEST_FILE_PARAMETER}."
|
||||
author: ""
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
class
|
||||
HTTP_CLIENT_REQUEST_FILE_PARAMETER
|
||||
|
||||
inherit
|
||||
HTTP_CLIENT_REQUEST_PARAMETER
|
||||
|
||||
create
|
||||
make_with_path
|
||||
|
||||
feature {NONE} -- Initialization
|
||||
|
||||
make_with_path (a_name: READABLE_STRING_GENERAL; a_path: PATH)
|
||||
do
|
||||
set_name (a_name)
|
||||
location := a_path
|
||||
if attached a_path.entry as e then
|
||||
file_name := e.name
|
||||
end
|
||||
set_content_type ("application/octet-stream") -- Default
|
||||
end
|
||||
|
||||
feature -- Access
|
||||
|
||||
count: INTEGER
|
||||
local
|
||||
f: RAW_FILE
|
||||
do
|
||||
create f.make_with_path (location)
|
||||
if f.exists and then f.is_access_readable then
|
||||
Result := f.count
|
||||
end
|
||||
end
|
||||
|
||||
location: PATH
|
||||
|
||||
file_name: detachable READABLE_STRING_32
|
||||
|
||||
feature -- Element change
|
||||
|
||||
set_file_name (fn: detachable READABLE_STRING_GENERAL)
|
||||
do
|
||||
if fn = Void then
|
||||
file_name := Void
|
||||
else
|
||||
file_name := fn.to_string_32
|
||||
end
|
||||
end
|
||||
|
||||
feature -- Status report
|
||||
|
||||
exists: BOOLEAN
|
||||
local
|
||||
fut: FILE_UTILITIES
|
||||
do
|
||||
Result := fut.file_path_exists (location)
|
||||
end
|
||||
|
||||
feature {NONE} -- Data
|
||||
|
||||
file_content: detachable STRING_8
|
||||
require
|
||||
exists: exists
|
||||
local
|
||||
f: RAW_FILE
|
||||
do
|
||||
create f.make_with_path (location)
|
||||
if f.exists and then f.is_access_readable then
|
||||
create Result.make (f.count)
|
||||
f.open_read
|
||||
from
|
||||
until
|
||||
f.exhausted or f.end_of_file
|
||||
loop
|
||||
f.read_stream_thread_aware (2_048)
|
||||
Result.append (f.last_string)
|
||||
end
|
||||
f.close
|
||||
end
|
||||
end
|
||||
|
||||
feature -- Data
|
||||
|
||||
append_file_content_to (a_output: STRING)
|
||||
-- Append content of file located at `location`to `a_output'.
|
||||
require
|
||||
exists: exists
|
||||
local
|
||||
f: RAW_FILE
|
||||
l_buffer_size: INTEGER
|
||||
do
|
||||
create f.make_with_path (location)
|
||||
if f.exists and then f.is_access_readable then
|
||||
f.open_read
|
||||
from
|
||||
l_buffer_size := 2_048
|
||||
until
|
||||
f.exhausted or f.end_of_file
|
||||
loop
|
||||
f.read_stream_thread_aware (l_buffer_size)
|
||||
a_output.append (f.last_string)
|
||||
end
|
||||
f.close
|
||||
end
|
||||
end
|
||||
|
||||
feature -- Conversion
|
||||
|
||||
append_form_url_encoded_to (a_output: STRING_8)
|
||||
-- Append as form url encoded string to `a_output`.
|
||||
do
|
||||
if exists and then attached file_content as s then
|
||||
x_www_form_url_encoder.append_percent_encoded_string_to (s, a_output)
|
||||
else
|
||||
check exists: False end
|
||||
end
|
||||
end
|
||||
|
||||
append_query_value_encoded_to (a_output: STRING_8)
|
||||
do
|
||||
if exists and then attached file_content as s then
|
||||
uri_percent_encoder.append_query_value_encoded_string_to (s, a_output)
|
||||
else
|
||||
check exists: False end
|
||||
end
|
||||
end
|
||||
|
||||
append_as_mime_encoded_to (a_output: STRING_8)
|
||||
-- Encoded unicode string for mime value.
|
||||
-- For instance uploaded filename, or form data key or values.
|
||||
do
|
||||
-- FIXME: find the proper encoding!
|
||||
if exists then
|
||||
append_file_content_to (a_output)
|
||||
else
|
||||
check exists: False end
|
||||
end
|
||||
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
|
||||
@@ -0,0 +1,68 @@
|
||||
note
|
||||
description: "Summary description for {HTTP_CLIENT_REQUEST_STRING_PARAMETER}."
|
||||
author: ""
|
||||
date: "$Date$"
|
||||
revision: "$Revision$"
|
||||
|
||||
class
|
||||
HTTP_CLIENT_REQUEST_STRING_PARAMETER
|
||||
|
||||
inherit
|
||||
HTTP_CLIENT_REQUEST_PARAMETER
|
||||
|
||||
create
|
||||
make
|
||||
|
||||
feature {NONE} -- Initialization
|
||||
|
||||
make (a_name, a_value: READABLE_STRING_GENERAL)
|
||||
do
|
||||
set_name (a_name)
|
||||
value := a_value.as_string_32
|
||||
end
|
||||
|
||||
feature -- Access
|
||||
|
||||
value: READABLE_STRING_32
|
||||
|
||||
count: INTEGER
|
||||
do
|
||||
Result := value.count
|
||||
end
|
||||
|
||||
feature -- Conversion
|
||||
|
||||
append_form_url_encoded_to (a_output: STRING_8)
|
||||
-- Append as form url encoded string to `a_output`.
|
||||
do
|
||||
x_www_form_url_encoder.append_percent_encoded_string_to (value, a_output)
|
||||
end
|
||||
|
||||
append_query_value_encoded_to (a_output: STRING_8)
|
||||
do
|
||||
uri_percent_encoder.append_query_value_encoded_string_to (value, a_output)
|
||||
end
|
||||
|
||||
append_as_mime_encoded_to (a_output: STRING_8)
|
||||
-- Encoded unicode string for mime value.
|
||||
-- For instance uploaded filename, or form data key or values.
|
||||
local
|
||||
utf: UTF_CONVERTER
|
||||
do
|
||||
-- FIXME: find the proper encoding!
|
||||
utf.utf_32_string_into_utf_8_string_8 (value, a_output)
|
||||
end
|
||||
|
||||
invariant
|
||||
|
||||
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
|
||||
@@ -31,9 +31,12 @@ feature {NONE} -- Initialization
|
||||
|
||||
apply_workaround
|
||||
-- Due to issue with Eiffel cURL on Windows 32bits
|
||||
-- we need to do the following workaround
|
||||
-- we need to do the following workaround.
|
||||
local
|
||||
add: detachable INET_ADDRESS
|
||||
once
|
||||
if attached (create {INET_ADDRESS_FACTORY}).create_localhost then
|
||||
if {PLATFORM}.is_windows then
|
||||
add := (create {INET_ADDRESS_FACTORY}).create_localhost
|
||||
end
|
||||
end
|
||||
|
||||
@@ -58,7 +61,6 @@ feature -- Execution
|
||||
ctx: like context
|
||||
p_slist: POINTER
|
||||
retried: BOOLEAN
|
||||
l_form_data: detachable HASH_TABLE [READABLE_STRING_32, READABLE_STRING_32]
|
||||
l_upload_data: detachable READABLE_STRING_8
|
||||
l_upload_filename: detachable READABLE_STRING_GENERAL
|
||||
l_headers: like headers
|
||||
@@ -151,70 +153,19 @@ feature -- Execution
|
||||
--| Credentials not provided ...
|
||||
end
|
||||
end
|
||||
|
||||
if ctx.has_upload_data then
|
||||
l_upload_data := ctx.upload_data
|
||||
end
|
||||
if ctx.has_upload_filename then
|
||||
l_upload_filename := ctx.upload_filename
|
||||
end
|
||||
if ctx.has_form_data then
|
||||
l_form_data := ctx.form_parameters
|
||||
check non_empty_form_data: not l_form_data.is_empty end
|
||||
if l_upload_data = Void and l_upload_filename = Void then
|
||||
-- Send as form-urlencoded
|
||||
if
|
||||
attached l_headers.item ("Content-Type") as l_ct
|
||||
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_x_www_form_url_encoded_string
|
||||
elseif l_ct.starts_with ("multipart/form-data") then
|
||||
l_use_curl_form := True
|
||||
else
|
||||
-- Not supported, use libcurl form.
|
||||
l_use_curl_form := True
|
||||
end
|
||||
else
|
||||
l_upload_data := ctx.form_parameters_to_x_www_form_url_encoded_string
|
||||
end
|
||||
else
|
||||
l_use_curl_form := True
|
||||
end
|
||||
if l_use_curl_form then
|
||||
create l_form.make
|
||||
create l_last.make
|
||||
from
|
||||
l_form_data.start
|
||||
until
|
||||
l_form_data.after
|
||||
loop
|
||||
curl.formadd_string_string (l_form, l_last,
|
||||
{CURL_FORM_CONSTANTS}.curlform_copyname, l_form_data.key_for_iteration,
|
||||
{CURL_FORM_CONSTANTS}.curlform_copycontents, l_form_data.item_for_iteration,
|
||||
{CURL_FORM_CONSTANTS}.curlform_end
|
||||
)
|
||||
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
|
||||
end
|
||||
|
||||
if l_upload_data /= Void then
|
||||
check
|
||||
post_or_put_request_method: request_method.is_case_insensitive_equal ("POST")
|
||||
or request_method.is_case_insensitive_equal ("PUT")
|
||||
or request_method.is_case_insensitive_equal ("PATCH")
|
||||
end
|
||||
check no_form_data: not ctx.has_form_data end
|
||||
|
||||
curl_easy.setopt_string (curl_handle, {CURL_OPT_CONSTANTS}.curlopt_postfields, l_upload_data)
|
||||
curl_easy.setopt_integer (curl_handle, {CURL_OPT_CONSTANTS}.curlopt_postfieldsize, l_upload_data.count)
|
||||
@@ -224,6 +175,7 @@ feature -- Execution
|
||||
or request_method.is_case_insensitive_equal ("PUT")
|
||||
or request_method.is_case_insensitive_equal ("PATCH")
|
||||
end
|
||||
check no_form_data: not ctx.has_form_data end
|
||||
|
||||
create l_upload_file.make_with_name (l_upload_filename)
|
||||
if l_upload_file.exists and then l_upload_file.is_readable then
|
||||
@@ -238,12 +190,62 @@ feature -- Execution
|
||||
l_upload_file.open_read
|
||||
curl_easy.set_curl_function (l_custom_function)
|
||||
end
|
||||
elseif
|
||||
ctx.has_form_data and
|
||||
attached ctx.form_parameters as l_form_data
|
||||
then
|
||||
check non_empty_form_data: not l_form_data.is_empty end
|
||||
-- Send as form-urlencoded
|
||||
if
|
||||
attached l_headers.item ("Content-Type") as l_ct
|
||||
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_x_www_form_url_encoded_string
|
||||
elseif l_ct.starts_with ("multipart/form-data") or l_form_data.has_file_parameter then
|
||||
l_use_curl_form := True
|
||||
else
|
||||
-- Not supported, use libcurl form.
|
||||
l_use_curl_form := True
|
||||
end
|
||||
else
|
||||
l_headers.force ("application/x-www-form-urlencoded", "Content-Type")
|
||||
l_upload_data := ctx.form_parameters_to_x_www_form_url_encoded_string
|
||||
curl_easy.setopt_string (curl_handle, {CURL_OPT_CONSTANTS}.curlopt_postfields, l_upload_data)
|
||||
curl_easy.setopt_integer (curl_handle, {CURL_OPT_CONSTANTS}.curlopt_postfieldsize, l_upload_data.count)
|
||||
end
|
||||
if l_use_curl_form then
|
||||
create l_form.make
|
||||
create l_last.make
|
||||
across
|
||||
l_form_data as ic
|
||||
loop
|
||||
if attached {HTTP_CLIENT_REQUEST_STRING_PARAMETER} ic.item as strparam then
|
||||
curl.formadd_string_string (l_form, l_last,
|
||||
{CURL_FORM_CONSTANTS}.curlform_copyname, strparam.name,
|
||||
{CURL_FORM_CONSTANTS}.curlform_copycontents, strparam.value,
|
||||
{CURL_FORM_CONSTANTS}.curlform_end
|
||||
)
|
||||
elseif attached {HTTP_CLIENT_REQUEST_FILE_PARAMETER} ic.item as fileparam then
|
||||
curl.formadd_string_string (l_form, l_last,
|
||||
{CURL_FORM_CONSTANTS}.curlform_copyname, "file",
|
||||
{CURL_FORM_CONSTANTS}.curlform_file, fileparam.location.name,
|
||||
{CURL_FORM_CONSTANTS}.curlform_end
|
||||
)
|
||||
else
|
||||
check supported_parameter_type: False end
|
||||
end
|
||||
end
|
||||
l_last.release_item
|
||||
curl_easy.setopt_form (curl_handle, {CURL_OPT_CONSTANTS}.curlopt_httppost, l_form)
|
||||
end
|
||||
else
|
||||
check no_upload_data: l_upload_data = Void and l_upload_filename = Void end
|
||||
-- No form, or upload data to send!
|
||||
check no_data: not (ctx.has_upload_data or ctx.has_upload_filename or ctx.has_form_data) end
|
||||
end
|
||||
end -- ctx /= Void
|
||||
|
||||
--| Header
|
||||
--| Header
|
||||
across
|
||||
l_headers as curs
|
||||
loop
|
||||
@@ -376,6 +378,11 @@ feature -- Execution
|
||||
curl_easy.setopt_integer (curl_handle, {CURL_OPT_CONSTANTS}.curlopt_ssl_verifypeer, 0)
|
||||
end
|
||||
|
||||
--| Cipher List
|
||||
if attached session.ciphers_setting as c_list then
|
||||
curl_easy.setopt_string (curl_handle, {CURL_OPT_CONSTANTS}.curlopt_ssl_cipher_list, c_list )
|
||||
end
|
||||
|
||||
--| Request method
|
||||
if request_method.is_case_insensitive_equal ("GET") then
|
||||
curl_easy.setopt_integer (curl_handle, {CURL_OPT_CONSTANTS}.curlopt_httpget, 1)
|
||||
@@ -445,7 +452,7 @@ feature {NONE} -- Implementation
|
||||
end
|
||||
|
||||
note
|
||||
copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others"
|
||||
copyright: "2011-2018, Jocelyn Fiat, Javier Velilla, Eiffel Software and others"
|
||||
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
|
||||
source: "[
|
||||
Eiffel Software
|
||||
|
||||
@@ -64,7 +64,7 @@ feature {NONE} -- Internal
|
||||
create Result.make_client_by_port (a_port, a_host)
|
||||
end
|
||||
Result.set_connect_timeout (connect_timeout)
|
||||
Result.set_timeout (timeout)
|
||||
Result.set_timeout_ns (Result.seconds_to_nanoseconds (timeout))
|
||||
Result.connect
|
||||
end
|
||||
end
|
||||
@@ -91,8 +91,8 @@ feature -- Access
|
||||
l_authorization: HTTP_AUTHORIZATION
|
||||
l_platform: STRING
|
||||
l_upload_data: detachable READABLE_STRING_8
|
||||
l_form_data: detachable HASH_TABLE [READABLE_STRING_32, READABLE_STRING_32]
|
||||
ctx: like context
|
||||
l_ct: detachable READABLE_STRING_8
|
||||
l_upload_file: detachable RAW_FILE
|
||||
l_upload_filename: detachable READABLE_STRING_GENERAL
|
||||
l_form_string: STRING
|
||||
@@ -113,6 +113,7 @@ feature -- Access
|
||||
-- Get URL data
|
||||
l_is_https := url.starts_with_general ("https://")
|
||||
create l_uri.make_from_string (url)
|
||||
check valid_url: l_uri.is_valid end
|
||||
l_port := l_uri.port
|
||||
if l_port = 0 then
|
||||
if l_is_https then
|
||||
@@ -149,7 +150,7 @@ feature -- Access
|
||||
then
|
||||
create l_authorization.make_basic_auth (u_name, u_pass)
|
||||
if attached l_authorization.http_authorization as auth then
|
||||
headers.extend (auth, "Authorization")
|
||||
headers.force (auth, "Authorization")
|
||||
end
|
||||
check headers.has_key ("Authorization") end
|
||||
end
|
||||
@@ -176,7 +177,7 @@ feature -- Access
|
||||
else
|
||||
l_platform := "Unknown"
|
||||
end
|
||||
headers.extend ("eiffelhttpclient/" + net_http_client_version + " (" + l_platform + ")", "User-Agent")
|
||||
headers.force ("eiffelhttpclient/" + net_http_client_version + " (" + l_platform + ")", "User-Agent")
|
||||
end
|
||||
|
||||
-- handle sending data
|
||||
@@ -191,67 +192,52 @@ feature -- Access
|
||||
l_upload_data := ctx.upload_data
|
||||
end
|
||||
|
||||
if ctx.has_form_data then
|
||||
l_form_data := ctx.form_parameters
|
||||
if l_upload_data = Void and l_upload_filename = Void then
|
||||
if
|
||||
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_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)
|
||||
headers.extend ("multipart/form-data; boundary=" + l_boundary, "Content-Type")
|
||||
l_upload_data := form_date_and_uploaded_files_to_mime_string (l_form_data, l_upload_filename, l_boundary)
|
||||
else
|
||||
-- not supported !
|
||||
-- Send as form-urlencoded
|
||||
headers.extend ("application/x-www-form-urlencoded", "Content-Type")
|
||||
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_x_www_form_url_encoded_string
|
||||
end
|
||||
headers.extend (l_upload_data.count.out, "Content-Length")
|
||||
if l_is_chunked_transfer_encoding then
|
||||
-- Discard chunked transfer encoding
|
||||
headers.remove ("Transfer-Encoding")
|
||||
l_is_chunked_transfer_encoding := False
|
||||
end
|
||||
elseif l_form_data /= Void then
|
||||
check l_upload_data = Void end
|
||||
|
||||
-- create form using multipart/form-data encoding
|
||||
l_boundary := new_mime_boundary (l_form_data)
|
||||
headers.extend ("multipart/form-data; boundary=" + l_boundary, "Content-Type")
|
||||
l_upload_data := form_date_and_uploaded_files_to_mime_string (l_form_data, l_upload_filename, l_boundary)
|
||||
headers.extend (l_upload_data.count.out, "Content-Length")
|
||||
if l_is_chunked_transfer_encoding then
|
||||
-- Discard chunked transfer encoding
|
||||
headers.remove ("Transfer-Encoding")
|
||||
l_is_chunked_transfer_encoding := False
|
||||
end
|
||||
end
|
||||
elseif l_upload_data /= Void then
|
||||
if l_upload_data /= Void then
|
||||
check ctx.has_upload_data end
|
||||
check no_form_data: not ctx.has_form_data end
|
||||
if not headers.has ("Content-Type") then
|
||||
headers.extend ("application/x-www-form-urlencoded", "Content-Type")
|
||||
headers.force ("application/x-www-form-urlencoded", "Content-Type")
|
||||
end
|
||||
if not l_is_chunked_transfer_encoding then
|
||||
headers.extend (l_upload_data.count.out, "Content-Length")
|
||||
headers.force (l_upload_data.count.out, "Content-Length")
|
||||
end
|
||||
elseif l_upload_filename /= Void then
|
||||
check ctx.has_upload_filename end
|
||||
check no_form_data: not ctx.has_form_data end
|
||||
create l_upload_file.make_with_name (l_upload_filename)
|
||||
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")
|
||||
headers.force (l_upload_file.count.out, "Content-Length")
|
||||
end
|
||||
end
|
||||
check l_upload_file /= Void end
|
||||
elseif
|
||||
ctx.has_form_data and
|
||||
attached ctx.form_parameters as l_form_data
|
||||
then
|
||||
l_ct := headers.item ("Content-Type")
|
||||
if l_ct /= Void and then l_ct.starts_with ("application/x-www-form-urlencoded") then
|
||||
l_upload_data := ctx.form_parameters_to_x_www_form_url_encoded_string
|
||||
elseif
|
||||
(l_ct /= Void and then l_ct.starts_with ("multipart/form-data"))
|
||||
or l_form_data.has_file_parameter
|
||||
then
|
||||
-- create form using multipart/form-data encoding
|
||||
l_boundary := new_mime_boundary (l_form_data)
|
||||
headers.force ("multipart/form-data; boundary=" + l_boundary, "Content-Type")
|
||||
l_upload_data := form_date_and_uploaded_files_to_mime_string (l_form_data, l_boundary)
|
||||
else
|
||||
-- not supported !
|
||||
-- Send as form-urlencoded
|
||||
headers.force ("application/x-www-form-urlencoded", "Content-Type")
|
||||
l_upload_data := ctx.form_parameters_to_x_www_form_url_encoded_string
|
||||
end
|
||||
headers.force (l_upload_data.count.out, "Content-Length")
|
||||
if l_is_chunked_transfer_encoding then
|
||||
-- Discard chunked transfer encoding
|
||||
headers.remove ("Transfer-Encoding")
|
||||
l_is_chunked_transfer_encoding := False
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -482,14 +468,9 @@ feature {NONE} -- Helpers
|
||||
Result := a_status >= 300 and a_status < 400
|
||||
end
|
||||
|
||||
form_date_and_uploaded_files_to_mime_string (a_form_parameters: HASH_TABLE [READABLE_STRING_32, READABLE_STRING_32]; a_upload_filename: detachable READABLE_STRING_GENERAL; a_mime_boundary: READABLE_STRING_8): STRING
|
||||
form_date_and_uploaded_files_to_mime_string (a_form_parameters: ITERABLE [HTTP_CLIENT_REQUEST_PARAMETER]; a_mime_boundary: READABLE_STRING_8): STRING
|
||||
-- Form data and uploaded files converted to mime string.
|
||||
-- TODO: design a proper MIME... component.
|
||||
local
|
||||
l_path: PATH
|
||||
l_mime_type: READABLE_STRING_8
|
||||
l_upload_file: detachable RAW_FILE
|
||||
l_mime_type_mapping: HTTP_FILE_EXTENSION_MIME_MAPPING
|
||||
do
|
||||
create Result.make (100)
|
||||
across
|
||||
@@ -500,48 +481,26 @@ feature {NONE} -- Helpers
|
||||
Result.append (http_end_of_header_line)
|
||||
Result.append ("Content-Disposition: form-data; name=")
|
||||
Result.append_character ('%"')
|
||||
Result.append (string_to_mime_encoded_string (ic.key))
|
||||
Result.append (string_to_mime_encoded_string (ic.item.name))
|
||||
Result.append_character ('%"')
|
||||
Result.append (http_end_of_header_line)
|
||||
Result.append (http_end_of_header_line)
|
||||
Result.append (string_to_mime_encoded_string (ic.item))
|
||||
Result.append (http_end_of_header_line)
|
||||
end
|
||||
|
||||
if a_upload_filename /= Void then
|
||||
-- get file extension, otherwise set default
|
||||
create l_mime_type_mapping.make_default
|
||||
create l_path.make_from_string (a_upload_filename)
|
||||
if
|
||||
attached l_path.extension as ext and then
|
||||
attached l_mime_type_mapping.mime_type (ext) as l_mt
|
||||
attached {HTTP_CLIENT_REQUEST_FILE_PARAMETER} ic.item as fileparam and then
|
||||
attached fileparam.file_name as fn
|
||||
then
|
||||
l_mime_type := l_mt
|
||||
else
|
||||
l_mime_type := "application/octet-stream"
|
||||
Result.append ("; filename=")
|
||||
Result.append_character ('%"')
|
||||
Result.append (string_to_mime_encoded_string (fn))
|
||||
Result.append_character ('%"')
|
||||
end
|
||||
Result.append ("--")
|
||||
Result.append (a_mime_boundary)
|
||||
Result.append (http_end_of_header_line)
|
||||
Result.append ("Content-Disposition: form-data; name=%"")
|
||||
Result.append (string_to_mime_encoded_string (a_upload_filename))
|
||||
Result.append_character ('%"')
|
||||
Result.append ("; filename=%"")
|
||||
Result.append (string_to_mime_encoded_string (a_upload_filename))
|
||||
Result.append_character ('%"')
|
||||
Result.append (http_end_of_header_line)
|
||||
Result.append ("Content-Type: ")
|
||||
Result.append (l_mime_type)
|
||||
Result.append (http_end_of_header_line)
|
||||
Result.append (http_end_of_header_line)
|
||||
|
||||
create l_upload_file.make_with_path (l_path)
|
||||
if l_upload_file.exists and then l_upload_file.is_access_readable then
|
||||
append_file_content_to (l_upload_file, l_upload_file.count, Result)
|
||||
-- Reset l_upload_file to Void, since the related content is already processed.
|
||||
l_upload_file := Void
|
||||
if attached ic.item.content_type as ct then
|
||||
Result.append (http_end_of_header_line)
|
||||
Result.append ("Content-Type: ")
|
||||
Result.append (ct)
|
||||
end
|
||||
Result.append (http_end_of_header_line)
|
||||
Result.append (http_end_of_header_line)
|
||||
ic.item.append_as_mime_encoded_to (Result)
|
||||
Result.append (http_end_of_header_line)
|
||||
end
|
||||
Result.append ("--")
|
||||
Result.append (a_mime_boundary)
|
||||
@@ -893,7 +852,7 @@ feature {NONE} -- Helpers
|
||||
end
|
||||
end
|
||||
|
||||
new_mime_boundary (a_data: HASH_TABLE [READABLE_STRING_32, READABLE_STRING_32]): STRING
|
||||
new_mime_boundary (a_data: ITERABLE [HTTP_CLIENT_REQUEST_PARAMETER]): STRING
|
||||
-- New MIME boundary.
|
||||
local
|
||||
s: STRING
|
||||
@@ -904,7 +863,7 @@ feature {NONE} -- Helpers
|
||||
across
|
||||
a_data as ic
|
||||
loop
|
||||
i := i + ic.item.count + ic.key.count
|
||||
i := i + ic.item.count + ic.item.name.count
|
||||
end
|
||||
create ran.set_seed (i) -- FIXME: use a real random seed.
|
||||
ran.start
|
||||
@@ -928,7 +887,7 @@ feature {NONE} -- Helpers
|
||||
|
||||
invariant
|
||||
note
|
||||
copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others"
|
||||
copyright: "2011-2018, Jocelyn Fiat, Javier Velilla, Eiffel Software and others"
|
||||
license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)"
|
||||
source: "[
|
||||
Eiffel Software
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
<capability>
|
||||
<concurrency support="thread"/>
|
||||
</capability>
|
||||
<variable name="netssl_http_client_enabled" value="false"/>
|
||||
<variable name="ssl_enabled" value="true"/>
|
||||
<variable name="netssl_http_client_enabled" value="true"/>
|
||||
<variable name="net_http_client_disabled" value="false"/>
|
||||
<variable name="libcurl_http_client_disabled" value="false"/>
|
||||
<library name="base" location="$ISE_LIBRARY\library\base\base.ecf"/>
|
||||
|
||||
@@ -74,6 +74,22 @@ feature -- Test routines
|
||||
end
|
||||
end
|
||||
|
||||
test_abs_url
|
||||
local
|
||||
sess: like new_session
|
||||
h: STRING_8
|
||||
l_url: STRING
|
||||
do
|
||||
sess := new_session ("https://www.eiffel.org")
|
||||
l_url := "/foo/bar"
|
||||
assert ("abs rel", sess.url (l_url, Void).same_string (sess.base_url + l_url))
|
||||
|
||||
l_url := "https://www.eiffel.org/foo/bar"
|
||||
assert ("abs 1", sess.url (l_url, Void).same_string (l_url))
|
||||
l_url := "https://example.com/foo/bar"
|
||||
assert ("abs 2", sess.url (l_url, Void).same_string (l_url))
|
||||
end
|
||||
|
||||
test_headers
|
||||
local
|
||||
res: HTTP_CLIENT_RESPONSE
|
||||
|
||||
@@ -32,6 +32,11 @@ feature -- Tests
|
||||
test_http_client_ssl
|
||||
end
|
||||
|
||||
test_libcurl_abs_url
|
||||
do
|
||||
test_abs_url
|
||||
end
|
||||
|
||||
test_libcurl_headers
|
||||
do
|
||||
test_headers
|
||||
|
||||
@@ -59,6 +59,11 @@ feature -- Tests
|
||||
test_post_with_file_and_form_data
|
||||
end
|
||||
|
||||
libcurl_test_post_with_multiple_file_and_form_data
|
||||
do
|
||||
test_post_with_multiple_file_and_form_data
|
||||
end
|
||||
|
||||
libcurl_test_get_with_redirection
|
||||
do
|
||||
test_get_with_redirection
|
||||
|
||||
@@ -32,6 +32,11 @@ feature -- Tests
|
||||
test_http_client_ssl
|
||||
end
|
||||
|
||||
test_net_abs_url
|
||||
do
|
||||
test_abs_url
|
||||
end
|
||||
|
||||
test_net_headers
|
||||
do
|
||||
test_headers
|
||||
|
||||
@@ -59,6 +59,11 @@ feature -- Tests
|
||||
test_post_with_file_and_form_data
|
||||
end
|
||||
|
||||
net_test_post_with_multiple_file_and_form_data
|
||||
do
|
||||
test_post_with_multiple_file_and_form_data
|
||||
end
|
||||
|
||||
net_test_get_with_redirection
|
||||
do
|
||||
test_get_with_redirection
|
||||
|
||||
@@ -21,8 +21,7 @@ feature -- Initialization
|
||||
on_prepare
|
||||
do
|
||||
Precursor
|
||||
global_requestbin_path := "/s0jkhhs0"
|
||||
if global_requestbin_path = Void then
|
||||
if is_using_requestbin and global_requestbin_path = Void then
|
||||
global_requestbin_path := new_requestbin_path
|
||||
end
|
||||
end
|
||||
@@ -33,7 +32,13 @@ feature -- Factory
|
||||
deferred
|
||||
end
|
||||
|
||||
feature -- Requestbin
|
||||
feature -- Requestbin
|
||||
|
||||
is_using_requestbin: BOOLEAN = False
|
||||
is_using_mockbincom: BOOLEAN
|
||||
do
|
||||
Result := not is_using_requestbin
|
||||
end
|
||||
|
||||
global_requestbin_path: detachable READABLE_STRING_8
|
||||
|
||||
@@ -42,7 +47,7 @@ feature -- Requestbin
|
||||
i,j: INTEGER
|
||||
do
|
||||
if
|
||||
attached new_session ("http://requestb.in") as sess and then
|
||||
attached new_session ("https://requestb.in") as sess and then
|
||||
attached sess.post ("/api/v1/bins", Void, Void) as resp
|
||||
then
|
||||
if resp.error_occurred then
|
||||
@@ -67,13 +72,30 @@ feature -- Requestbin
|
||||
if not Result.starts_with ("/") then
|
||||
Result.prepend_character ('/')
|
||||
end
|
||||
print ("new_requestbin_path => http://requestb.in" + Result + "?inspect%N")
|
||||
print ("new_requestbin_path => " + sess.base_url + Result + "?inspect%N")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
new_web_session: like new_session
|
||||
do
|
||||
if is_using_mockbincom then
|
||||
Result := new_session ("http://mockbin.com/request")
|
||||
end
|
||||
if Result = Void and is_using_requestbin then
|
||||
if attached global_requestbin_path as l_path then
|
||||
Result := new_session ("https://requestb.in" + l_path)
|
||||
else
|
||||
assert ("Has requestbin path", False)
|
||||
end
|
||||
end
|
||||
if Result = Void then
|
||||
Result := new_session ("http://mockbin.com/request") -- Default
|
||||
end
|
||||
end
|
||||
|
||||
feature -- Factory
|
||||
|
||||
test_post_url_encoded
|
||||
@@ -81,288 +103,200 @@ feature -- Factory
|
||||
sess: HTTP_CLIENT_SESSION
|
||||
h: STRING_8
|
||||
do
|
||||
if attached global_requestbin_path as requestbin_path then
|
||||
-- URL ENCODED POST REQUEST
|
||||
-- check requestbin to ensure the "Hello World" has been received in the raw body
|
||||
-- also check that User-Agent was sent
|
||||
create h.make_empty
|
||||
sess := new_session ("http://requestb.in")
|
||||
if
|
||||
attached sess.post (requestbin_path, Void, "Hello World") 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)
|
||||
-- URL ENCODED POST REQUEST
|
||||
-- check requestbin to ensure the "Hello World" has been received in the raw body
|
||||
-- also check that User-Agent was sent
|
||||
create h.make_empty
|
||||
sess := new_web_session
|
||||
if
|
||||
attached sess.post ("", Void, "Hello World") as res
|
||||
then
|
||||
check_response (res)
|
||||
end
|
||||
end
|
||||
|
||||
test_post_with_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 ("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)
|
||||
-- POST REQUEST WITH FORM DATA
|
||||
-- check requestbin to ensure the form parameters are correctly received
|
||||
sess := new_web_session
|
||||
create l_ctx.make
|
||||
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?")
|
||||
if
|
||||
attached sess.post ("", l_ctx, "") as res
|
||||
then
|
||||
check_response (res)
|
||||
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_web_session
|
||||
create l_ctx.make
|
||||
|
||||
-- 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
|
||||
|
||||
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"))
|
||||
|
||||
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
|
||||
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)
|
||||
if
|
||||
attached sess.post ("", l_ctx, "") as res
|
||||
then
|
||||
check_response (res)
|
||||
end
|
||||
end
|
||||
|
||||
test_post_with_file
|
||||
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 A FILE
|
||||
-- check requestbin to ensure the form parameters are correctly received
|
||||
-- set filename to a local file
|
||||
sess := new_session ("http://requestb.in")
|
||||
create l_ctx.make
|
||||
l_ctx.set_upload_filename ("test.txt")
|
||||
create h.make_empty
|
||||
if
|
||||
attached sess.post (requestbin_path, l_ctx, Void) 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)
|
||||
-- POST REQUEST WITH A FILE
|
||||
-- check requestbin to ensure the form parameters are correctly received
|
||||
-- set filename to a local file
|
||||
sess := new_web_session
|
||||
create l_ctx.make
|
||||
l_ctx.set_upload_filename ("test.txt")
|
||||
if
|
||||
attached sess.post ("", l_ctx, Void) as res
|
||||
then
|
||||
check_response (res)
|
||||
end
|
||||
end
|
||||
|
||||
test_put_with_file
|
||||
local
|
||||
sess: HTTP_CLIENT_SESSION
|
||||
h: STRING_8
|
||||
l_ctx: HTTP_CLIENT_REQUEST_CONTEXT
|
||||
do
|
||||
if attached global_requestbin_path as requestbin_path then
|
||||
|
||||
-- PUT REQUEST WITH A FILE
|
||||
-- check requestbin to ensure the file is correctly received
|
||||
-- set filename to a local file
|
||||
sess := new_session ("http://requestb.in")
|
||||
create l_ctx.make
|
||||
l_ctx.set_upload_filename ("test.txt")
|
||||
create h.make_empty
|
||||
if
|
||||
attached sess.put (requestbin_path, l_ctx, Void) 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)
|
||||
-- PUT REQUEST WITH A FILE
|
||||
-- check requestbin to ensure the file is correctly received
|
||||
-- set filename to a local file
|
||||
sess := new_web_session
|
||||
create l_ctx.make
|
||||
l_ctx.set_upload_filename ("test.txt")
|
||||
if
|
||||
attached sess.put ("", l_ctx, Void) as res
|
||||
then
|
||||
check_response (res)
|
||||
end
|
||||
end
|
||||
|
||||
test_put_with_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
|
||||
|
||||
-- PUT REQUEST WITH A FILE
|
||||
-- check requestbin to ensure the file is correctly received
|
||||
-- set filename to a local file
|
||||
sess := new_session ("http://requestb.in")
|
||||
create l_ctx.make
|
||||
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
|
||||
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)
|
||||
-- PUT REQUEST WITH A FILE
|
||||
-- check requestbin to ensure the file is correctly received
|
||||
-- set filename to a local file
|
||||
sess := new_web_session
|
||||
create l_ctx.make
|
||||
l_ctx.set_upload_data ("name=This is a test for http client.%N")
|
||||
if
|
||||
attached sess.put ("", l_ctx, Void) as res
|
||||
then
|
||||
check_response (res)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
test_post_with_file_and_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 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_web_session
|
||||
create l_ctx.make
|
||||
-- l_ctx.add_file_form_parameter ("image", "test.txt", "image/jpeg")
|
||||
l_ctx.add_file_form_parameter ("text", "test.txt", "plain/text")
|
||||
l_ctx.add_form_parameter ("First", "Value")
|
||||
l_ctx.add_form_parameter ("Second", "and last value")
|
||||
if
|
||||
attached sess.post ("", l_ctx, Void) as res
|
||||
then
|
||||
check_response (res)
|
||||
end
|
||||
end
|
||||
|
||||
-- POST 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")
|
||||
-- sess := new_session ("http://localhost:9090")
|
||||
create l_ctx.make
|
||||
-- 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
|
||||
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)
|
||||
test_post_with_multiple_file_and_form_data
|
||||
local
|
||||
sess: HTTP_CLIENT_SESSION
|
||||
l_ctx: HTTP_CLIENT_REQUEST_CONTEXT
|
||||
do
|
||||
-- POST 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_web_session
|
||||
create l_ctx.make
|
||||
l_ctx.add_header ("Content-Type", "multipart/form-data")
|
||||
|
||||
l_ctx.add_file_form_parameter ("first_file", "test.txt", "plain/text")
|
||||
l_ctx.add_file_form_parameter ("image", "logo.jpg", "image/jpeg")
|
||||
l_ctx.add_form_parameter ("First", "Value")
|
||||
l_ctx.add_form_parameter ("Second", "and last value")
|
||||
l_ctx.add_file_form_parameter ("last_file", "test.txt", Void)
|
||||
|
||||
if
|
||||
attached sess.post ("", l_ctx, Void) as res
|
||||
then
|
||||
check_response (res)
|
||||
end
|
||||
end
|
||||
|
||||
test_post_with_file_using_chunked_transfer_encoding
|
||||
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 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_header ("Transfer-Encoding", "chunked")
|
||||
l_ctx.set_upload_filename ("logo.jpg")
|
||||
create h.make_empty
|
||||
if
|
||||
attached sess.post (requestbin_path, l_ctx, Void) 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)
|
||||
-- POST 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_web_session
|
||||
create l_ctx.make
|
||||
l_ctx.add_header ("Transfer-Encoding", "chunked")
|
||||
l_ctx.set_upload_filename ("logo.jpg")
|
||||
if
|
||||
attached sess.post ("", l_ctx, Void) as res
|
||||
then
|
||||
check_response (res)
|
||||
end
|
||||
end
|
||||
|
||||
test_get_with_redirection
|
||||
local
|
||||
sess: HTTP_CLIENT_SESSION
|
||||
h: STRING_8
|
||||
do
|
||||
if attached global_requestbin_path as requestbin_path then
|
||||
|
||||
-- GET REQUEST, Forwarding (google's first answer is a forward)
|
||||
-- check headers received (printed in console)
|
||||
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
|
||||
h.append (c.item.name + ": " + c.item.value + "%R%N")
|
||||
end
|
||||
end
|
||||
print (h)
|
||||
else
|
||||
assert ("Has requestbin path", False)
|
||||
-- GET REQUEST, Forwarding (google's first answer is a forward)
|
||||
-- check headers received (printed in console)
|
||||
sess := new_session ("http://google.com")
|
||||
if attached sess.get ("/", Void) as res then
|
||||
check_response (res)
|
||||
assert("was redirected", res.redirections_count > 0)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -377,6 +311,7 @@ feature -- Factory
|
||||
sess.set_credentials ("test", "test")
|
||||
create ctx.make_with_credentials_required
|
||||
if attached sess.get ("/password-ok.php", ctx) as res then
|
||||
check_response (res)
|
||||
if attached {READABLE_STRING_8} res.body as l_body then
|
||||
assert ("Fetch all body, including closing html tag", l_body.has_substring ("</html>"))
|
||||
else
|
||||
@@ -388,50 +323,58 @@ feature -- Factory
|
||||
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"))
|
||||
-- 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_web_session
|
||||
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)
|
||||
if
|
||||
attached sess.get ("", l_ctx) as res
|
||||
then
|
||||
check_response (res)
|
||||
end
|
||||
end
|
||||
|
||||
feature {NONE} -- Implementation
|
||||
|
||||
check_response (res: HTTP_CLIENT_RESPONSE)
|
||||
local
|
||||
h: STRING
|
||||
do
|
||||
assert ("ok", not res.error_occurred)
|
||||
create h.make_empty
|
||||
if
|
||||
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)
|
||||
if attached res.body as b then
|
||||
print (b)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
@@ -7,117 +7,44 @@
|
||||
<exclude>/\.svn$</exclude>
|
||||
<exclude>/EIFGENs$</exclude>
|
||||
</file_rule>
|
||||
<option warning="true">
|
||||
</option>
|
||||
<external_include location="$ECF_CONFIG_PATH/spec/include">
|
||||
<condition>
|
||||
<version type="compiler" min="16.9.9.9124"/>
|
||||
</condition>
|
||||
</external_include>
|
||||
<external_include location="$ECF_CONFIG_PATH/spec/include_until_16_05">
|
||||
<condition>
|
||||
<version type="compiler" max="16.9.9.9124"/>
|
||||
</condition>
|
||||
</external_include>
|
||||
<external_include location="$(ISE_LIBRARY)/unstable/library/network/socket/netssl/spec/include">
|
||||
<condition>
|
||||
<platform excluded_value="windows"/>
|
||||
<version type="compiler" max="16.9.9.9124"/>
|
||||
<custom name="ssl_enabled" value="true"/>
|
||||
</condition>
|
||||
</external_include>
|
||||
<external_include location="$(ISE_LIBRARY)\unstable\library\network\socket\netssl\spec\include">
|
||||
<condition>
|
||||
<platform value="windows"/>
|
||||
<version type="compiler" max="16.9.9.9124"/>
|
||||
<custom name="ssl_enabled" value="true"/>
|
||||
</condition>
|
||||
</external_include>
|
||||
<external_cflag value="-D_WINSOCKAPI_">
|
||||
<condition>
|
||||
<platform value="windows"/>
|
||||
<version type="compiler" max="16.9.9.9124"/>
|
||||
<custom name="ssl_enabled" value="true"/>
|
||||
</condition>
|
||||
</external_cflag>
|
||||
<external_include location="$ECF_CONFIG_PATH/spec/include"/>
|
||||
<library name="base" location="$ISE_LIBRARY\library\base\base.ecf"/>
|
||||
<library name="net" location="$ISE_LIBRARY\library\net\net.ecf"/>
|
||||
<library name="net_ssl" location="$ISE_LIBRARY\unstable\library\network\socket\netssl\net_ssl.ecf">
|
||||
<condition>
|
||||
<custom name="ssl_enabled" value="true"/>
|
||||
</condition>
|
||||
</library>
|
||||
<library name="openssl" location="$ISE_LIBRARY\unstable\library\network\openssl\openssl.ecf">
|
||||
<condition>
|
||||
<custom name="net_ssl_enabled" value="true"/>
|
||||
</condition>
|
||||
<condition>
|
||||
<custom name="httpd_ssl_enabled" value="true"/>
|
||||
<version type="compiler" min="17.10.0.0"/>
|
||||
<custom name="ssl_enabled" value="true"/>
|
||||
</condition>
|
||||
</library>
|
||||
<cluster name="network" location=".\src\">
|
||||
<file_rule>
|
||||
<exclude>/http_stream_socket_ext.e$</exclude>
|
||||
<condition>
|
||||
<version type="compiler" max="17.02"/>
|
||||
</condition>
|
||||
</file_rule>
|
||||
<cluster name="disabled_ssl_network" location="$|no_ssl\" recursive="true">
|
||||
<condition>
|
||||
<custom name="ssl_enabled" excluded_value="true"/>
|
||||
<custom name="net_ssl_enabled" excluded_value="true"/>
|
||||
<custom name="httpd_ssl_enabled" excluded_value="true"/>
|
||||
</condition>
|
||||
</cluster>
|
||||
<cluster name="ssl_network" location="$|ssl\" recursive="true">
|
||||
<cluster name="ssl_network" location="$|ssl\" recursive="false">
|
||||
<condition>
|
||||
<custom name="ssl_enabled" value="true"/>
|
||||
</condition>
|
||||
<condition>
|
||||
<custom name="net_ssl_enabled" value="true"/>
|
||||
</condition>
|
||||
<condition>
|
||||
<custom name="httpd_ssl_enabled" value="true"/>
|
||||
</condition>
|
||||
<file_rule>
|
||||
<exclude>/http_stream_secure_socket_ext.e$</exclude>
|
||||
<exclude>/http_stream_secure_socket_ext\.e$</exclude>
|
||||
<condition>
|
||||
<version type="compiler" max="16.9.9.9124"/>
|
||||
<custom name="ssl_enabled" value="true"/>
|
||||
</condition>
|
||||
<condition>
|
||||
<version type="compiler" max="16.9.9.9124"/>
|
||||
<custom name="net_ssl_enabled" value="true"/>
|
||||
</condition>
|
||||
<condition>
|
||||
<version type="compiler" max="16.9.9.9124"/>
|
||||
<custom name="httpd_ssl_enabled" value="true"/>
|
||||
<version type="compiler" max="18.05.0.0"/>
|
||||
</condition>
|
||||
</file_rule>
|
||||
</cluster>
|
||||
</cluster>
|
||||
<cluster name="network_until_16_05" location=".\src\until_16_05\">
|
||||
<condition>
|
||||
<version type="compiler" max="16.9.9.9124"/>
|
||||
</condition>
|
||||
<cluster name="ssl_network_until_16_05" location="$|ssl\" recursive="true">
|
||||
<cluster name="ssl_network_until_18_05" location="$|ssl\until_18.05" recursive="true">
|
||||
<condition>
|
||||
<version type="compiler" max="16.9.9.9124"/>
|
||||
<custom name="ssl_enabled" value="true"/>
|
||||
</condition>
|
||||
<condition>
|
||||
<version type="compiler" max="16.9.9.9124"/>
|
||||
<custom name="net_ssl_enabled" value="true"/>
|
||||
</condition>
|
||||
<condition>
|
||||
<version type="compiler" max="16.9.9.9124"/>
|
||||
<custom name="httpd_ssl_enabled" value="true"/>
|
||||
<version type="compiler" max="18.05.0.0"/>
|
||||
</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"/>
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
/*
|
||||
indexing
|
||||
description: "Functions used by the EiffelWeb http networking classes. "
|
||||
copyright: "Copyright (c) 2011-2016, Jocelyn Fiat, 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
|
||||
]"
|
||||
*/
|
||||
|
||||
#ifndef _ew_network_h_
|
||||
#define _ew_network_h_
|
||||
|
||||
#include "eif_config.h"
|
||||
|
||||
#ifdef EIF_WINDOWS
|
||||
# ifndef _WINSOCKAPI_
|
||||
# define FD_SETSIZE 256
|
||||
# include <winsock2.h>
|
||||
# include <Ws2tcpip.h>
|
||||
# include <stdio.h>
|
||||
# endif
|
||||
#else /* unix-specific */
|
||||
# include <sys/socket.h>
|
||||
# include <unistd.h>
|
||||
#endif
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* extern declarations ... */
|
||||
#ifdef EIF_WINDOWS
|
||||
extern int setsockopt(int, int, int, char*, int);
|
||||
extern int recv(int, char *, int, int);
|
||||
extern int send(int, char *, int, int);
|
||||
#else
|
||||
extern int setsockopt(int, int, int, const void*, socklen_t);
|
||||
extern ssize_t recv(int, void *, size_t, int);
|
||||
extern ssize_t send(int, const void *, size_t, int);
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||