diff --git a/LICENSE.txt b/LICENSE similarity index 98% rename from LICENSE.txt rename to LICENSE index 9cecc1d..f288702 100644 --- a/LICENSE.txt +++ b/LICENSE @@ -1,7 +1,7 @@ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 - Copyright (C) 2007 Free Software Foundation, Inc. + Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. @@ -631,8 +631,8 @@ to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. - {one line to give the program's name and a brief idea of what it does.} - Copyright (C) {year} {name of author} + + Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -645,14 +645,14 @@ the "copyright" line and a pointer to where the full notice is found. GNU General Public License for more details. You should have received a copy of the GNU General Public License - along with this program. If not, see . + along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: - {project} Copyright (C) {year} {fullname} + Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. @@ -664,11 +664,11 @@ might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see -. +. The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read -. +. diff --git a/README.md b/README.md deleted file mode 100644 index 3e0f6e3..0000000 --- a/README.md +++ /dev/null @@ -1,579 +0,0 @@ -Nexus-Backend Service -===================== - - -## An Advanced and Secure RestApi Backend Service Gateway - -**The Nexus-Backend Service** acts as an intermediary between a **REST Client application** and a **Backend REST API service**. -It forwards Requests from the client to the **Backend Service** and returns the Responses back to the client. -The Nexus-Backend integrate a HttpFirewall and **WAF Filter** for a protection against evasion on the **Http Request Headers, -Request Map parameters and Json BodyRequest.** - -**Inside a Servlet Container a Rest Controller ApiBackend and its BackendService, Secure and Replicate all the HTTP -Requests to a RestApi Backend Server.** - -**All HttpRequests methods supported:** Get, Post, Post Multipart File, Put, Put Multipart File, Patch, Patch Multipart File, Delete. - -* Full support **Request Json Entity Object**: application/json, application/x-www-form-urlencoded -* Full support **MultipartRequest Resources and Map parameters**, and embedded form **Json Entity Object**: multipart/form-data -* Full support **Response in Json Entity Object**: application/json -* Full support **Response in ByteArray Resource file**: application/octet-stream -* Full support **Streaming Http Response Json Entity Object**: application/octet-stream, accept header Range bytes -* Full support **Cookie manage** during a redirection Http status 3xx - -**Tomcat Servlet Containers under Servlet version 4.x** - -**Examples forwarded requests and responses through the Nexus-Backend Service:** - -| REST Clients | RestApi Nexus-Backend Service | Backend Server Services | -|-----------------------|:---------------------------------------------------|:------------------------------------------------| -| Ajax / XMLHttpRequest | http://localhost:8082/nexus-backend/api/** | https://secure.jservlet.com:9092/api/v1/service | -| HttpClient | https://front.jservlet.com:80/nexus-backend/api/** | https://secure.jservlet.com:9092/api/v1/service | -| FeedService | https://intra.jservlet.com:80/nexus-backend/api/** | https://10.100.100.50:9092/api/v1/service | - -***An Ajax Single Page Application communicate through the Rest Controller ApiBackend and its BackendService to a RestApi Backend Server.*** - - -### Ability to Secure all RestApi Request to a Backend Server - - * Implements a **BackendService**, ability to request typed response Object class or ParameterizedTypeReference, requested on all HTTP methods to a RestApi Backend Server. - * Implements an **EntityBackend** Json Object or Resource, transfer back headers, manage error HttpStatus 400, 401, 405 or 500 coming from the Backend Server. - * Implements a **HttpFirewall** filter protection against evasion, rejected any suspicious Requests, Headers, Parameters, and log IP address at fault. - * Implements a **WAF** filter protection against evasion on the Http Json BodyRequest, and log IP address at fault. - * Implements a **CORS Security Request** filter, authorize request based on Origin Domains and Methods. - * Implements a **Content Security Policy** filter, define your own policy rules CSP. - * Implements a **RateLimit** interceptor, allows 1000 requests per minutes and per-IP-address. - * Implements a **Fingerprint** for each Http header Request, generate a unique trackable Token APP-REQUEST-ID in the access logs. - * Implements a **Method Override** filter, PUT or PATCH request can be switched in POST or DELETE switched in GET with header X-HTTP-Method-Override. - * Implements a **Forwarded Header** filter, set removeOnly at true by default, remove "Forwarded" and "X-Forwarded-*" headers. - * Implements a **FormContent** filter, parses form data for Http PUT, PATCH, and DELETE requests and exposes it as Servlet request parameters. - * Implements a **Compressing** filter Gzip compression for the Http Responses. - * Implements a **CharacterEncoding** filter, UTF-8 default encoding for requests. - - -### Specials config Http Headers - - * **HTTP headers:** reset all Headers, remove host or origin header. - * **Basic Authentication:** set any security ACL **Access Control List** - * **Bearer Authorization:** set any security **Bearer Token**. - * **Cookie:** set any **security session Cookie**. - * **CORS:** Bypass locally all **CORS Security** (Cross-origin resource sharing) from a Navigator, - not restricted to accessing resources from the same origin through what is known as same-origin policy. - - -### The Nexus Backend application can be configured by the following keys SpringBoot and Settings properties - - **SpringBoot keys application.properties:** - -| **Keys** | **Default value** | **Descriptions** | -|--------------------------------------------------|:------------------|:---------------------------------------------------| -| nexus.api.backend.enabled | true | Activated the Nexus-Backend Service | -| nexus.api.backend.filter.waf.enabled | true | Activated the WAF filter Json RequestBody | -| nexus.api.backend.listener.requestid.enabled | true | Activated the Fingerprint for each Http Request | -| nexus.api.backend.filter.httpoverride.enabled | true | Activated the Http Override Method | -| nexus.api.backend.interceptor.ratelimit.enabled | true | Activated the RateLimit | -| nexus.backend.filter.forwardedHeader.enabled | true | Activated the ForwardedHeader filter | -| nexus.backend.filter.gzip.enabled | true | Activated the Gzip compression filter | -| spring.mvc.formcontent.filter.enabled | true | Activated the FormContent parameterMap Support | -| nexus.backend.tomcat.connector.https.enable | false | Activated a Connector TLS/SSL in a Embedded Tomcat | -| nexus.backend.tomcat.accesslog.valve.enable | false | Activated an Accesslog in a Embedded Tomcat | - -#### Noted the Spring config location can be overridden - -* -Dspring.config.location=/your/config/dir/ -* -Dspring.config.name=spring.properties - - -### The Nexus-Backend Url Server and miscellaneous options can be configured by the following keys Settings - - **Settings keys settings.properties:** - -| **Keys** | **Default value** | **Example value** | **Descriptions** | -|-------------------------------------------------|:-------------------------|:--------------------------------|:----------------------------------------------------| -| **nexus.backend.url** | https://postman-echo.com | https://nexus6.jservlet.com/api | The API Backend
Server targeted | -| **nexus.backend.uri.alive** | /get | /health/info | The endpoint alive
Backend Server | -| nexus.backend.http.response.truncated | false | true | Truncated the Json
output in the logs | -| nexus.backend.http.response.truncated.maxLength | 1000 | 100 | MaxLength truncated | -| **WAF** | | | | -| nexus.api.backend.filter.waf.reactive.mode | STRICT | PASSIVE | Default Strict HttpFirewall
+ Json RequestBody | -| nexus.api.backend.filter.waf.deepscan.cookie | false | true | Activated Deep Scan Cookie | -| **Headers** | | | | -| nexus.backend.header.remove | **true** | true | Remove all Headers | -| nexus.backend.header.host.remove | false | false | Remove just host Header | -| nexus.backend.header.origin.remove | false | false | Remove just origin Header | -| nexus.backend.header.cookie | - | XSession=0XX1YY2ZZ3XX4YY5ZZ6XX | Set a Cookie Request Header | -| nexus.backend.header.bearer | - | eyJhbGciO | Activated Bearer
Authorization request | -| nexus.backend.header.user-agent | JavaNexus | Apache HttpClient/4.5 | User Agent header | -| nexus.backend.header.authorization.username | - | XUsername | Activated Basic
Authorization request | -| nexus.backend.header.authorization.password | - | XPassword | " | -| **Backend Headers** | | | | -| nexus.api.backend.transfer.headers | test | test,Link,Content-Range | Headers list back
from Backend Server | -| **Mapper** | | | | -| nexus.backend.mapper.indentOutput | false | true | Indent Output Json | -| **Debug** | | | | -| nexus.spring.web.security.debug | false | true | Debug the Spring FilterChain | - -**Noted**: About the list HttpHeaders transfer back, the CORS can expose these Headers see key security.cors.exposedHeaders - -#### Noted the settings.properties can be overridden by a file Path config.properties - -* **${user.home}**/conf-global/config.properties -* **${user.home}**/conf/config.properties -* **${user.home}**/cfg/**${servletContextPath}**/config.properties - -### The ApiBackend Configuration Json Entity Object or a ByteArray Resource - -**ApiBackend ResponseType** can be now a **ByteArray Resource.** - -**Download** any content in a **ByteArray** included commons extensions files (see **[MediaTypes](#The-MediaTypes-safe-extensions-configuration)** section) - -The **ResourceMatchers** Config can be configured on specific ByteArray Resources path -and on specific Methods **GET, POST, PUT, PATCH** and Ant Path pattern: - -**Settings keys settings.properties:** - -| **Keys Methods** and **Keys Path pattern** | **Default value** | **Content-Type** | -|---------------------------------------------------------------|:-----------------------|:-------------------------| -| nexus.backend.api-backend-resource.matchers.1.method | GET | | -| nexus.backend.api-backend-resource.matchers.1.pattern | /api/encoding/** | text/html;charset=utf-8 | -| nexus.backend.api-backend-resource.matchers.2.method | GET | | -| nexus.backend.api-backend-resource.matchers.2.pattern | /api/streaming/** | application/octet-stream | -| nexus.backend.api-backend-resource.matchers.3.method | GET | | -| nexus.backend.api-backend-resource.matchers.3.pattern | /api/time/now | text/html;charset=utf-8 | -| nexus.backend.api-backend-resource.matchers.{name}[X].method | Methods | | -| nexus.backend.api-backend-resource.matchers.{name}[X].pattern | Patterns | | - -**Http Responses** are considerate as **Resources**, the Http header **"Accept-Ranges: bytes"** is injected and allow you to use -the Http header **'Range: bytes=1-100'** in the request and grabbed only range of Bytes desired.
-And the Http Responses didn't come back with a HttpHeader **"Transfer-Encoding: chunked"** cause the header **Content-Length**. - - -**Noted:** For configure **all the Responses** in **Resource** put an empty Method and use the path pattern=/api/** - -| **Keys Methods** and **Keys Path pattern** | **Default value** | -|---------------------------------------------------------------|:------------------| -| nexus.backend.api-backend-resource.matchers.matchers1.method | | -| nexus.backend.api-backend-resource.matchers.matchers1.pattern | /api/** | - -**Noted bis:** For remove the Http header **"Transfer-Encoding: chunked"** the header Content-Length need to be calculated. - -Enable the **ShallowEtagHeader Filter** in the configuration for force to calculate the header **Content-Length** -for all the **Response Json Entity Object**, no more HttpHeader **"Transfer-Encoding: chunked"**. - -### The MediaTypes safe extensions configuration - -**MediaTypes safe extensions** - -The Spring ContentNegotiation load the safe extensions files that can be extended. -A commons MediaTypes properties file is loaded [resources/mime/MediaTypes_commons.properties](https://github.com/javaguru/nexus-backend/blob/master/src/main/resources/mime/MediaTypes_commons.properties) -and can be disabled: - -**Settings keys settings.properties:** - -Default Header ContentNegotiation Strategy: - -| **ContentNegotiation Strategy** | **Default value** | **Descriptions Strategy** | -|---------------------------------------------------------------|:------------------|:----------------------------| -| **Header Strategy** | | | -| nexus.backend.content.negotiation.ignoreAcceptHeader | false | Header Strategy Enabled | -| **Parameter Strategy** | | | -| nexus.backend.content.negotiation.favorParameter | false | Parameter Strategy Disabled | -| nexus.backend.content.negotiation.parameterName | mediaType | | -| **Registered Extensions** | | | -| nexus.backend.content.negotiation.useRegisteredExtensionsOnly | true | Registered Only Enabled | -| **Load commons MediaTypes** | | | -| nexus.backend.content.negotiation.commonMediaTypes | true | Enabled | - - -### The CORS Security configuration - -**CORS Security configuration, allow Control Request on Domains and Methods** - -**Settings keys settings.properties:** - -The default Cors Configuration: - -| **Cors Configuration** | **Default value** | **Example value** | **Descriptions** | -|---------------------------------------------------|:---------------------------------------------------------------------------|:---------------------------------------------------|:-----------------------| -| nexus.backend.security.cors.credentials | false | true | Enable credentials | -| nexus.backend.security.cors.allowedHttpMethods | GET,POST,PUT
,OPTIONS,HEAD,
DELETE,PATCH | GET,POST,PUT,OPTIONS | List Http Methods | -| nexus.backend.security.cors.allowedOriginPatterns | | | Regex Patterns domains | -| nexus.backend.security.cors.allowedOrigins | * | http://localhost:4042,
http://localhost:4083 | List domains | -| nexus.backend.security.cors.allowedHeaders | Authorization,Cache-Control,
Content-Type,
X-Requested-With,Accept | Authorization,
Cache-Control,
Content-Type | List Allowed Headers | -| nexus.backend.security.cors.exposedHeaders | | Link,X-Custom-Header | List Exposed Headers | -| nexus.backend.security.cors.maxAge | 3600 | 1800 | Max Age cached | - -**Noted:** allowedOrigins cannot be a wildcard '*' if credentials is at true, a list of domains need to be provided. - -Exposed headers - -### The RateLimit Configuration - -**Rate limit** 1000 per minutes and per-IP-address. - -**SpringBoot key** *nexus.api.backend.interceptor.ratelimit.enabled* at **true** for activated the RateLimit. - -**Settings keys settings.properties:** - -The default Cors Configuration: - -| **Cors Configuration** | **Default value** | **Example value** | **Descriptions** | -|--------------------------------------------------------|:------------------|:------------------|:-----------------| -| nexus.backend.interceptor.ratelimit.refillToken | 1000 | 100 | Filled tokens | -| nexus.backend.interceptor.ratelimit.refillMinutes | 1 | 1 | Duration minutes | -| nexus.backend.interceptor.ratelimit.bandwidthCapacity | 1000 | 100 | Bucket capacity | - - -### The Nexus-Backend provides a full support MultipartRequest and Map parameters inside a form-data HttpRequest - -#### MultipartConfig - -**SpringBoot keys application.properties:** - -| **Keys** | **Default value** | **Example value** | **Descriptions** | -|----------------------------------------------|:------------------|:------------------|:--------------------| -| spring.servlet.multipart.enabled | true | true | Enabled multipart | -| spring.servlet.multipart.file-size-threshold | 2MB | 5MB | File size threshold | -| spring.servlet.multipart.max-file-size | 15MB | 150MB | Max file size | -| spring.servlet.multipart.max-request-size | 15MB | 150MB | Max request size | - -**Noted** All the HttpRequests with a **Content-Type multipart/form-data** will be managed by a temporary **BackendResource**. - -~~This BackendResource can convert a **MultipartFile** to a temporary **Resource**, ready to be sent to the **Backend Server**.~~ - -Since version 1.0.24 no more BackendResource and temporary file, all is in memory. - -### The BackendService HttpFactory Client Configuration - - **Settings keys settings.properties:** - -| **Keys** | **Default value** | **Example value** | **Descriptions** | -|-----------------------------------------------------|:------------------|:------------------|:-------------------------------| -| nexus.backend.client.header.user-agent | JavaNexus | curl | User Agent Header | -| nexus.backend.client.connectTimeout | 10 | 5 | Connection timeout in second | -| nexus.backend.client.requestTimeout | 20 | 10 | Request timeout in second | -| nexus.backend.client.socketTimeout | 10 | 5 | Socket timeout in second | -| nexus.backend.client.max_connections_per_route | 20 | 30 | Max Connections per route | -| nexus.backend.client.max_connections | 100 | 300 | Max Connections in the Pool | -| nexus.backend.client.close_idle_connections_timeout | 0 | 0 | Close idle connections timeout | -| nexus.backend.client.validate_after_inactivity | 2 | 2 | Validate after inactivity | -| nexus.backend.client.requestSentRetryEnabled | false | true | Request Sent Retry Enabled | -| nexus.backend.client.retryCount | 3 | 2 | Retry Count | -| nexus.backend.client.redirectsEnabled | true | true | Redirects enabled | -| nexus.backend.client.maxRedirects | 5 | 2 | Maximum redirections | -| nexus.backend.client.authenticationEnabled | false | true | Authentication enabled | -| nexus.backend.client.circularRedirectsAllowed | false | true | Circular redirections allowed | - - -### The Nexus-Backend Firewall and the WAF Filter Configuration - -The **Nexus-Backend** implements a **HttpFirewall** protection against evasion and rejected any suspicious Http Request -on the Headers and Cookies, the Parameters, the keys and Values. - -The **WAF Filter** implements a secure WAF protection against evasion on a **Json Http RequestBody**. - -**Un-normalized** Http requests are automatically rejected by the **StrictHttpFirewall**, -and path parameters and duplicate slashes are removed for matching purposes. - -**Noted** the valid characters are defined in **RFC 7230** and **RFC 3986** are checked -by the **Apache Coyote http11 processor** (see coyote Error parsing HTTP request header) - -All the Http request with **Cookies, Headers, Parameters and RequestBody** will be filtered and the suspicious **IP address** in fault will be logged. - - **Settings keys settings.properties:** - -| **Keys** | **Default value** | **Descriptions** | -|------------------------------------------------------------|:--------------------------------------------|:--------------------------------------| -| nexus.backend.security.allowedHttpMethods | GET,POST,PUT,OPTIONS,
HEAD,DELETE,PATCH | Allowed Http Methods | -| nexus.backend.security.allowSemicolon | false | Allowed Semi Colon | -| nexus.backend.security.allowUrlEncodedSlash | false | Allow url encoded Slash | -| nexus.backend.security.allowUrlEncodedDoubleSlash | false | Allow url encoded double Slash | -| nexus.backend.security.allowUrlEncodedPeriod | false | Allow url encoded Period | -| nexus.backend.security.allowBackSlash | false | Allow BackSlash | -| nexus.backend.security.allowNull | false | Allow Null | -| nexus.backend.security.allowUrlEncodedPercent | false | Allow url encoded Percent | -| nexus.backend.security.allowUrlEncodedCarriageReturn | false | Allow url encoded Carriage Return | -| nexus.backend.security.allowUrlEncodedLineFeed | false | Allow url encoded Line Feed | -| nexus.backend.security.allowUrlEncodedParagraphSeparator | false | Allow url encoded Paragraph Separator | -| nexus.backend.security.allowUrlEncodedLineSeparator | false | Allow url encoded Line Separator | - -**The WAF Utilities Predicates checked for potential evasion:** - -* XSS script injection -* SQL injection -* Google injection -* Command injection -* File injection -* Link injection - -**Implements a WAF Predicate for potential evasion by Headers or Parameters:** - - * Header Names / Header Values - * Parameter Names / Parameter Values - * Hostnames - * UserAgent - -**And check for Buffer Overflow evasion by the Length:** - - * Parameter Names 255 characters max. / Values 1000000 characters max. - * Header Names 255 characters max. / Values 25000 characters max. - * Hostnames 255 characters max. - - **The WAF Reactive mode configuration:** - - * **STRICT**: Strict HttpFirewall + Json RequestBody - * **PASSIVE**: Strict HttpFirewall + Clean Json RequestBody and Parameters Map - * **UNSAFE**: Strict HttpFirewall + No check Json RequestBody! - -**Settings keys settings.properties:** Define a max length for Keys/Values Headers or Parameters - -| **Keys** | **Default value** | **Descriptions** | -|----------------------------------------------------------|:------------------|:--------------------------------| -| nexus.backend.security.predicate.parameterNamesLength | 255 | Parameter names length max | -| nexus.backend.security.predicate.parameterValuesLength | 1000000 | Parameter values length max | -| nexus.backend.security.predicate.headerNamesLength | 255 | Header names length max | -| nexus.backend.security.predicate.headerNamesValuesLength | 25000 | Header values length max | -| nexus.backend.security.predicate.hostNamesLength | 255 | Host names length max | -| nexus.backend.security.predicate.hostName.pattern | | Hostname pattern filter | -| nexus.backend.security.predicate.userAgent.blocked | false | Active Scanner UserAgent filter | -| nexus.backend.security.predicate.aiUserAgent.blocked | true | Active AI UserAgent filter | - - -### Activated the Mutual Authentication or mTLS connection on the HttpFactory Client - - **Settings keys settings.properties:** *nexus.backend.client.ssl.mtls.enable* at **true** for activated the mTLS connection - -| **Keys** | **Default value** | **Descriptions** | -|---------------------------------------------|:-----------------------|:--------------------------| -| nexus.backend.client.ssl.mtls.enable | **false** | Activated the Mutual TLS | -| nexus.backend.client.ssl.key-store | nexus-default.jks | Path to the Java KeyStore | -| nexus.backend.client.ssl.key-store-password | changeit | The password | -| nexus.backend.client.ssl.certificate.alias | key_server | The certificate alias | -| nexus.backend.client.ssl.https.protocols | TLSv1.3 | The protocols | -| nexus.backend.client.ssl.https.cipherSuites | TLS_AES_256_GCM_SHA384 | The Cipher Suites | - - -### Activated Tomcat Catalina Connector TLS/SSL on a wildcard domain Certificate - - **Settings keys settings.properties:** - - **SpringBoot key** *nexus.backend.tomcat.connector.https.enable* at **true** for activated the TLS/SSL protocol - -| **Keys** | **Default value** | **Descriptions** | -|-------------------------------------------------------|:---------------------|:--------------------------| -| nexus.backend.tomcat.ssl.keystore-path | /home/root/.keystore | Path to the Java KeyStore | -| nexus.backend.tomcat.ssl.keystore-password | changeit | The password | -| nexus.backend.tomcat.ssl.certificate.alias | key_server | The certificate alias | -| nexus.backend.tomcat.ssl.https.port | 8443 | The Https port | -| nexus.backend.tomcat.ssl.ajp.connector.enable | false | Start the Ajp connector | -| nexus.backend.tomcat.ssl.ajp.connector.port | 8009 | The Ajp port | -| nexus.backend.tomcat.ssl.ajp.connector.protocol | AJP/1.3 | AJP version 1.3 | -| nexus.backend.tomcat.ssl.ajp.connector.secretRequired | false | A secret is Required | - - -### Activated Tomcat Catalina Extended AccessLog Valve - - **Settings keys settings.properties:** - - **SpringBoot key** *nexus.backend.tomcat.accesslog.valve.enable* at **true** for activated the Accesslogs - -| **Keys** | **Default value** | **Descriptions** | -|-----------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------------| -| nexus.backend.tomcat.accesslog.directory | /tmp/logs/tomcat-nexus | Directory access log | -| nexus.backend.tomcat.accesslog.suffix | .log | The suffix | -| nexus.backend.tomcat.accesslog.encoding | UTF-8 | The suffix | -| nexus.backend.tomcat.accesslog.pattern | date time x-threadname c-ip cs-method cs-uri
sc-status bytes x-H(contentLength)time-taken
x-H(authType) cs(Authorization) cs(User-Agent) | The pattern | -| nexus.backend.tomcat.accesslog.checkExists | true | Check if file exists | -| nexus.backend.tomcat.accesslog.asyncSupported | true | Support async requests | -| nexus.backend.tomcat.accesslog.renameOnRotate | true | Rename on rotate | -| nexus.backend.tomcat.accesslog.throwOnFailure | true | Throw on failure | -| nexus.backend.tomcat.accesslog.maxDay | -1 | Max day file retention | - -**Noted** the Full access logs are available with the **CommonsRequestLoggingFilter**, included the **RequestBody**. - -Already initialized, activated by setting the logback.xml at **level="DEBUG"**. - - -## Build Nexus-Backend - -[SpringBoot](https://projects.spring.io/spring-boot/) - -### Build requirements - - * Java 13 - * SpringBoot 2.7.18 - * Tomcat 9.0.116 & Servlet 4.0.1 - * Maven 3.9.x - -### Build war external Tomcat 9 - -with the profile withoutTomcat: - -* `mvn clean compile -P withoutTomcat` -* `mvn clean package -P withoutTomcat` -* `mvn clean install -P withoutTomcat` - -and look for the jar at `target/nexus-backend-{version}.war` - -### Build jar embedded Tomcat 9 - -with the profile withTomcat: - -* `mvn clean compile -P withTomcat` -* `mvn clean install -P withTomcat` -* `mvn clean package -P withTomcat` - -and look for the jar at `target/nexus-backend-{version}.jar` - -### Get javadoc - -`mvn javadoc:javadoc` - -### Run SpringBoot App - -with maven: - -`mvn spring-boot:run -P withTomcat` - -### The Configuration - -By default, it uses `8082` port and the Servlet Context `/nexus-backend`. - -The default SpringBoot config is in `/src/main/resources/application.properties` file. - -The default NexusBackend config in `/src/main/resources/settings.properties` file. - -The Config keys and values can be modified or override by external path files, here: - - * file `{user.home}/conf-global/config.properties` - * file `{user.home}/conf/config.properties` - * file `{user.home}/cfg/nexus-backend/config.properties` - -### Swagger Tests environment - -See RestControllerTest is in interaction with the MockController, run the tests with a local Tomcat running on localhost:8082/nexus-backend - -The Swagger Mock-Api is only available in Dev mode, added in JVM Options: -Denvironment=development - -## The BackendService API Implementation - -This API implementation is used for the communication to a backend server. -It provides methods for all supported http protocols on the backend side. -Normally, it communicates to an API interface Backend. - -### Available HTTP methods: - - * Get - * Post - * Post Multipart File - * Put - * Put Multipart File - * Patch - * Patch Multipart File - * Delete - -### Sample BackendService API - -#### Prerequisites: - -* **RestOperations** should be configured with an Apache-HttpClient and a Pooling connection should be properly configured. -* **HttpMessageConverter** are also mandatory, StringHttp, FormHttp, ByteArrayHttp, ResourceHttp and MappingJackson2Http are the minimal. -* **Typed Response** parameter Class Object or a ParameterizedTypeReference are mandatory -* **Object.class** cannot be converted in a Resource or ByteArray directly without a minimal support Typed Response. - -#### Initialize the RestApi BackendService - -``` -BackendService backendService = new BackendServiceImpl(); -backendService.setBackendURL("https://internal.domain.com:9094"); -backendService.setRestOperations(new RestTemplate()); -backendService.setObjectMapper(new ObjectMapper()); -``` - -#### Get Data - -``` -Data data = backendService.get("/mock/v1/data", backendService.createResponseType(Data.class)); -``` - -#### Get List Data - -``` -ResponseType> typeReference = backendService.createResponseType(new ParameterizedTypeReference<>(){}); -List list = backendService.get("/mock/v1/dataList", typeReference); -``` - -#### Get Resource File - -``` -Resource image = backendService.getFile("/static/images/logo-marianne.svg"); -FileUtils.copyInputStreamToFile(image.getInputStream(), new File(System.getProperty("java.io.tmpdir") + "/logo-marianne.svg")); -``` - - -#### Do Request List Data -``` -ResponseType> typeReference = backendService.createResponseType(new ParameterizedTypeReference<>(){}); -Object obj = backendService.doRequest("/mock/v1/dataList", HttpMethod.GET, typeReference, null, null); -System.out.println(obj); -``` - -#### Do Request Resource -``` -Resource resource = backendService.doRequest("/mock/v1/datafile", HttpMethod.GET, -backendService.createResponseType(Resource.class), null, headers); // WARN mandatory typed Resource.class -String data = StreamUtils.copyToString(resource.getInputStream(), Charset.defaultCharset()); -System.out.println(data); -``` - -#### Do Request Byte Array -``` -ResponseType typeReference = backendService.createResponseType(byte[].class) -byte[] bytes = backendService.doRequest("/mock/v1/dataBytes", HttpMethod.GET, typeReference , null, null); // WARN mandatory typed byte[].class -System.out.println(new String(bytes, StandardCharsets.UTF_8)); -``` - - -## Last News -* Last version **1.0.25**, released at 22/03/2026 Modern WAF Defense, XSS, SQL, Google, Command, File, Java RCE, XXE, AI User-Agent -* Version **1.0.24**, released at 01/09/2025 Forwarded headers Client and Transfer headers Backend Server, Cors headers exposed -* Version **1.0.23**, released at 22/08/2025 Reorganize WAFFilter Multipart, CorsConfiguration, Cookie client stateful -* Version **1.0.22**, released at 31/07/2025 Fix Security RateLimit, Content Security Policy and Referrer-Policy -* Version **1.0.21**, released at 29/07/2025 Fix Predicate for Hostnames, Shared CookieRedirectInterceptor, Postman-Echo performance -* Version **1.0.20**, released at 26/07/2025 Fix Spring Security dependencies, Improve security WAFFilter and WAFPredicate - Bis -* Version **1.0.19**, released at 26/07/2025 Fix Spring Security dependencies, Improve security WAFFilter and WAFPredicate -* Version **1.0.18**, released at 10/05/2025 Fix manage Cookie during a redirection 3xx -* Version **1.0.17**, released at 04/05/2025 Fix manage Cookie, Gateway is stateless! -* Version **1.0.16**, released at 03/11/2024 Fix CORS Security configuration Spring 5/6 -* Version **1.0.15**, released at 23/10/2024 Fix missing method addCorsMappings -* Version **1.0.14**, released at 14/10/2024 Support Backend Headers and Support ContentNegotiation Header Strategy for Resources -* Version **1.0.13**, released at 06/10/2024 Full support Response in ByteArray Resource and Streaming Http Response Range Bytes -* Version **1.0.12**, released at 02/10/2024 Fix ApiBase error Message super.getResponseEntity -* Version **1.0.11**, released at 30/09/2024 Does not encode the URI template! -* Version **1.0.10**, released at 29/09/2024 Add full support MultipartRequest content type multipart/form-data -* Version **1.0.9**, released at 24/09/2024 Fix replicate requests ApiBackend.requestEntity -* Version **1.0.8**, released at 13/08/2024 Re-encoding HttpUrl, Special Characters are re-interpreted -* Version **1.0.7**, released at 03/08/2024 All is Bytes. -* Version **1.0.6**, released at 14/07/2024 Clarify Byte Array deserialization. -* Version **1.0.5**, released at 13/07/2024 Optimize build war/jar. -* Version **1.0.4**, released at 08/07/2024. -* Version **1.0.3**, released at 23/06/2024 Reinit project. -* Version **1.0.2** released at 28/04/2024. -* Version **1.0.1** released at 21/11/2022. -* Initial release **1.0.0** at 03/06/2021. - -## Support -If you need help using Nexus-Backend Service feel free to drop an email or create an issue in GitHub.com (preferred). - -## Contributions -To help **Nexus-Backend / ApiBackend / BackendService** development you are encouraged to -* Provide suggestion/feedback/Issue -* pull requests for new features -* Star :star2: the project - -## License - -This project is an Open Source Software released under the [GPL-3.0 license](https://github.com/javaguru/nexus-backend/blob/master/LICENSE.txt). - -Copyright (c) 2001-2025 JServlet.com [Franck ANDRIANO.](http://jservlet.com) - diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index 1e2bfa1..0000000 --- a/SECURITY.md +++ /dev/null @@ -1,9 +0,0 @@ -# Security Policy - -## Supported Versions - -| Version | Supported | -|---------|---------------------| -| 1.0.24 | :white_check_mark: | - -## Reporting a Vulnerability diff --git a/pom.xml b/pom.xml deleted file mode 100644 index d278be7..0000000 --- a/pom.xml +++ /dev/null @@ -1,726 +0,0 @@ - - - 4.0.0 - - com.jservlet.nexus.backend - nexus-backend - ${packaging} - 1.0.25 - - nexus-backend - The Java Nexus BackendService, an advanced and secure Rest Backend Gateway - https://github.com/javaguru/nexus-backend - - - JServlet.com - https://github.com/javaguru - - 2020 - - - - GNU General Public License (GPL) version 3.0 - https://www.gnu.org/licenses/gpl-3.0.txt - repo - - - - - - fan - Franck Andriano. - franck@jservlet.com - JServlet.com - https://www.jservlet.com - - architect - developer - - - - - - scm:git:git://github.com/javaguru/nexus-backend.git - scm:git:git@github.com:javaguru/nexus-backend.git - https://github.com/javaguru/nexus-backend - HEAD - - - - - ossrh - Sonatype Nexus Snapshots - https://oss.sonatype.org/content/repositories/snapshots - - - ossrh - Nexus Release Repository - https://oss.sonatype.org/service/local/staging/deploy/maven2/ - - - - - Github - https://github.com/javaguru/nexus-backend/issues - - - - SNAPSHOT - NOW - NOW - unknown - unknown - unknown - - 13 - UTF-8 - UTF-8 - - exec - com.jservlet.nexus.config.Application - - - 9.0.116 - - - 2.7.18 - 5.3.44 - 5.3.39 - 5.3.39 - 5.8.16 - 4.0.1 - - 3.0.0 - 1.8.0 - 2.21.1 - 2.4.1 - - 3.8.1 - 2.22.2 - 3.3.1 - - 3.4.0 - 3.4.1 - 3.0.1 - 3.7.0 - 2.0.0 - - 2.5.3 - 2.8.2 - 1.6.7 - - 1.6 - 1.11.2 - - - - - - - release-sign-artifacts - - jar - - - - performRelease - true - - - - - - org.apache.maven.plugins - maven-gpg-plugin - ${maven-gpg-plugin.version} - - - sign-artifacts - verify - - sign - - - - - - - - - - withoutTomcat - - war - withoutTomcat - - - - - org.springframework.boot - spring-boot-starter-web - ${spring-boot.version} - provided - - - spring-boot-starter-tomcat - org.springframework.boot - - - - - - - - org.apache.maven.plugins - maven-war-plugin - ${maven-war-plugin.version} - - src/main/webapp - false - - - - true - lib/ - ${mainClass} - - - ${project.groupId} - ${project.artifactId} - ${project.version} - ${project.url} - development - - - - - - - - - - withTomcat - - true - - - jar - withTomcat - - - - - org.springframework.boot - spring-boot-starter-web - ${spring-boot.version} - - - org.springframework.boot - spring-boot-starter-tomcat - ${spring-boot.version} - provided - - - - - - - - org.apache.maven.plugins - maven-dependency-plugin - - - copy-dependencies - prepare-package - - copy-dependencies - - - ${project.build.outputDirectory}/lib - false - false - true - - - - - - - - - - - ${project.artifactId} - - - - src/main/resources - - **/*.properties - - true - - - - src/main/resources - - logback.xml - logo-marianne.svg - persistence.xml - api-ui/api-docs.yaml - mime/*.properties - META-INF/services/javax.servlet.ServletContainerInitializer - - false - - - - - - org.springframework.boot - spring-boot-maven-plugin - ${spring-boot.version} - - - - repackage - - - ${packageClassifier} - true - ${mainClass} - ${project.build.directory}/docker - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - ${maven-compiler-plugin.version} - - ${project.build.sourceEncoding} - ${java.version} - ${java.version} - true - -Xlint:unchecked - - - - org.apache.maven.plugins - maven-surefire-plugin - ${maven-surefire-plugin.version} - - - - org.apache.maven.plugins - maven-resources-plugin - ${maven-resources-plugin.version} - - ISO-8859-1 - - - - - org.apache.maven.plugins - maven-jar-plugin - ${maven-jar-plugin.version} - - - - - true - lib/ - ${mainClass} - - - ${project.groupId} - ${project.artifactId} - ${project.version} - ${project.url} - development - lib/ - - - - - - - package - - jar - - - - - true - lib/ - ${mainClass} - - - ${project.groupId} - ${project.artifactId} - ${project.version} - ${project.url} - shared - lib/ - - - shared - - settings.properties - logback.xml - com/jservlet/nexus/config/**/*.* - com/jservlet/nexus/controller/**/*.* - - - /META-INF/*.properties - /META-INF/*.MF - com/jservlet/nexus/shared/**/*.class - - - - - - - org.apache.maven.plugins - maven-source-plugin - ${maven-source-plugin.version} - - - attach-sources - - jar - - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - ${maven-javadoc-plugin.version} - - UTF-8 - false - ${java.home}/bin/javadoc - - - - - attach-javadoc - - jar - - - - - - org.apache.maven.plugins - maven-release-plugin - ${maven-release-plugin.version} - - true - false - forked-path - -Dgpg.passphrase=${gpg.passphrase} - - - - org.apache.maven.scm - maven-scm-provider-gitexe - ${maven-scm-provider-gitexe.version} - - - - - org.codehaus.mojo - buildnumber-maven-plugin - 1.4 - - - validate - - create - - - - - gitRevision - buildtime - {0,date,yyyy-MM-dd HH:mm:ss} - gitBranch - - - - maven-deploy-plugin - ${maven-deploy-plugin.version} - - - default-deploy - deploy - - deploy - - - - - - org.sonatype.plugins - nexus-staging-maven-plugin - ${nexus-staging-maven-plugin.version} - true - - ossrh - https://oss.sonatype.org/ - true - - - - - - - - - - org.springframework.boot - spring-boot-starter - ${spring-boot.version} - pom - import - - - org.springframework - spring-framework-bom - ${spring.bom.version} - pom - import - - - org.springframework.security - spring-security-bom - ${spring.security.version} - pom - import - - - - - - - - org.apache.tomcat.embed - tomcat-embed-jasper - ${tomcat.version} - - - - - org.yaml - snakeyaml - 2.0 - - - - - ch.qos.logback - logback-classic - 1.5.25 - - - ch.qos.logback - logback-core - 1.5.25 - - - - - org.springframework.boot - spring-boot-configuration-processor - ${spring-boot.version} - - - - - org.springframework.boot - spring-boot-starter-validation - ${spring-boot.version} - - - - - org.springframework.boot - spring-boot-starter-actuator - ${spring-boot.version} - - - - - com.giffing.bucket4j.spring.boot.starter - bucket4j-spring-boot-starter - 0.9.0 - - - - - org.springframework - spring-context-support - ${spring.context.version} - - - - - org.springframework.security - spring-security-web - ${spring.security.version} - - - org.springframework.security - spring-security-config - ${spring.security.version} - - - - - javax.annotation - javax.annotation-api - 1.3.2 - - - - - javax.servlet - javax.servlet-api - ${servlet.api.version} - provided - - - - javax.servlet - jstl - 1.2 - - - - - io.swagger.parser.v3 - swagger-parser - 2.1.39 - - - - - org.codehaus.janino - janino - 2.6.1 - - - - - org.springdoc - springdoc-openapi-ui - ${springdoc-openapi.version} - - - org.springdoc - springdoc-openapi-security - ${springdoc-openapi.version} - - - - - com.fasterxml.jackson.core - jackson-core - ${jackson.version} - - - - - org.apache.logging.log4j - log4j-to-slf4j - 2.19.0 - - - - - commons-io - commons-io - 2.16.1 - - - org.apache.commons - commons-collections4 - 4.5.0-M1 - - - - commons-codec - commons-codec - 1.17.0 - - - - org.apache.commons - commons-lang3 - 3.18.0 - - - - - com.github.ziplet - ziplet - ${ziplet.version} - - - - - junit - junit - 4.13.2 - test - - - org.springframework - spring-test - test - - - - - diff --git a/src/license/gnu/header.txt b/src/license/gnu/header.txt deleted file mode 100644 index 46306d8..0000000 --- a/src/license/gnu/header.txt +++ /dev/null @@ -1,14 +0,0 @@ -Copyright (C) 2001-2024 JServlet.com Franck Andriano. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . diff --git a/src/main/java/com/jservlet/nexus/config/Application.java b/src/main/java/com/jservlet/nexus/config/Application.java deleted file mode 100644 index 83889fb..0000000 --- a/src/main/java/com/jservlet/nexus/config/Application.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (C) 2001-2024 JServlet.com Franck Andriano. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - */ - -package com.jservlet.nexus.config; - -import io.swagger.v3.oas.annotations.OpenAPIDefinition; -import org.springframework.boot.Banner; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; -import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.boot.web.servlet.ServletComponentScan; -import org.springframework.context.annotation.Import; - -/** - * SpringBoot Application - */ - -//@OpenAPIDefinition(servers = {@Server(url = "/nexus-backend", description = "Default Server URL")}) -@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class, UserDetailsServiceAutoConfiguration.class}) -@ServletComponentScan // Scan all @WebListener, @WebServlet or @WebFilter! -@Import({ - ApplicationConfig.class -}) -public class Application { - - /** - * -Denvironment=development -Dserver.servlet.context-path=/nexus-backend -Dserver.port=8082 - * - * @param args {@link String[]} - */ - public static void main(String[] args) { - //System.setProperty("spring.devtools.restart.enabled", "false"); - - // Fix ClassNotFoundException: org.slf4j.impl.StaticLoggerBinder - System.setProperty("org.springframework.boot.logging.LoggingSystem", "none"); - // Swagger is only available in dev! - String env = System.getProperty("environment", "development"); - if ("development".equals(env)) System.setProperty("springdoc.swagger-ui.enabled", "true"); - // Server path and port - System.setProperty("server.servlet.context-path", System.getProperty("server.servlet.context-path", "/nexus-backend")); - System.setProperty("server.port", System.getProperty("server.port", "8082")); - new SpringApplicationBuilder(Application.class).bannerMode(Banner.Mode.CONSOLE).run(args); - } - -} diff --git a/src/main/java/com/jservlet/nexus/config/ApplicationConfig.java b/src/main/java/com/jservlet/nexus/config/ApplicationConfig.java deleted file mode 100644 index 7362d44..0000000 --- a/src/main/java/com/jservlet/nexus/config/ApplicationConfig.java +++ /dev/null @@ -1,321 +0,0 @@ -/* - * Copyright (C) 2001-2024 JServlet.com Franck Andriano. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - */ - -package com.jservlet.nexus.config; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.databind.*; -import com.jservlet.nexus.config.web.WebConfig; -import com.jservlet.nexus.config.web.WebSecurityConfig; -import com.jservlet.nexus.config.web.tomcat.ssl.TomcatConnectorConfig; -import com.jservlet.nexus.shared.config.annotation.ConfigProperties; -import com.jservlet.nexus.shared.service.backend.BackendService; -import com.jservlet.nexus.shared.service.backend.BackendServiceImpl; -import com.jservlet.nexus.shared.web.controller.api.ApiBackend; -import com.jservlet.nexus.shared.web.interceptor.CookieRedirectInterceptor; -import org.apache.http.Header; -import org.apache.http.HttpResponse; -import org.apache.http.ParseException; -import org.apache.http.client.config.CookieSpecs; -import org.apache.http.client.config.RequestConfig; -import org.apache.http.config.RegistryBuilder; -import org.apache.http.conn.HttpConnectionFactory; -import org.apache.http.conn.ManagedHttpClientConnection; -import org.apache.http.conn.routing.HttpRoute; -import org.apache.http.conn.socket.ConnectionSocketFactory; -import org.apache.http.conn.socket.PlainConnectionSocketFactory; -import org.apache.http.conn.ssl.SSLConnectionSocketFactory; -import org.apache.http.impl.DefaultConnectionReuseStrategy; -import org.apache.http.impl.DefaultHttpResponseFactory; -import org.apache.http.impl.client.*; -import org.apache.http.impl.conn.DefaultHttpResponseParserFactory; -import org.apache.http.impl.conn.ManagedHttpClientConnectionFactory; -import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; -import org.apache.http.impl.io.DefaultHttpRequestWriterFactory; -import org.apache.http.message.BasicHeader; -import org.apache.http.message.BasicLineParser; -import org.apache.http.protocol.HttpContext; -import org.apache.http.ssl.PrivateKeyDetails; -import org.apache.http.ssl.PrivateKeyStrategy; -import org.apache.http.ssl.SSLContexts; -import org.apache.http.util.CharArrayBuffer; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.http.*; -import org.springframework.http.client.*; -import org.springframework.http.converter.*; -import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; -import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; -import org.springframework.web.client.*; -import org.springframework.web.util.DefaultUriBuilderFactory; - -import javax.net.ssl.SSLContext; -import java.io.*; -import java.net.Socket; -import java.security.KeyStore; -import java.util.*; -import java.util.concurrent.TimeUnit; - -import static java.nio.charset.StandardCharsets.UTF_8; - -/** - * Application Config Nexus Backend Application - * Loader ConfigProperties file "classpath:settings.properties" - */ -@Configuration -@ConfigProperties("classpath:settings.properties") -@Import({ - WebConfig.class, - WebSecurityConfig.class, - TomcatConnectorConfig.class -}) -@EnableConfigurationProperties(ApiBackend.ResourceMatchersConfig.class) -public class ApplicationConfig { - - @Bean - public BackendService backendService(@Value("${nexus.backend.url}") String backendUrl, - RestOperations restOperations, - ObjectMapper objectMapper) { - final BackendServiceImpl backendService = new BackendServiceImpl(true); // return a Generics Object! - backendService.setBackendURL(backendUrl); - backendService.setRestOperations(restOperations); - backendService.setObjectMapper(objectMapper); - return backendService; - } - - @Bean - public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter(ObjectMapper objectMapper) { - return new MappingJackson2HttpMessageConverter(objectMapper); - } - - @Value("${nexus.backend.mapper.indentOutput:false}") - private boolean indentOutput; - - - @Bean - public ObjectMapper objectMapper() { - return new Jackson2ObjectMapperBuilder() - // fields not null globally! - .serializationInclusion(JsonInclude.Include.NON_NULL) - // to allow serialization of "empty" POJOs (no properties to serialize) - .failOnEmptyBeans(false) - // to prevent exception when encountering unknown property: - .failOnUnknownProperties(false) - - // disable, not thrown an exception if an unknown property - .featuresToDisable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) - - // to enable standard indentation ("pretty-printing"): - .indentOutput(indentOutput) - .build(); - } - - - @Bean - public RestOperations backendRestOperations(MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter, - ClientHttpRequestFactory httpRequestFactory) throws Exception { - - RestTemplate restTemplate = new RestTemplate(httpRequestFactory); - restTemplate.setInterceptors(List.of(new CookieRedirectInterceptor(maxRedirects))); - - // Does not encode the URI template, prevent to re-encode again the Uri with percent encoded in %25 - DefaultUriBuilderFactory uriFactory = new DefaultUriBuilderFactory(); - uriFactory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.NONE); - restTemplate.setUriTemplateHandler(uriFactory); - - // MediaType.ALL now! Json + Json wildcard, pdf, gif etc... - mappingJackson2HttpMessageConverter.setSupportedMediaTypes(List.of(MediaType.ALL)); - - // List HttpMessage Converters - restTemplate.setMessageConverters(Arrays.asList( - new StringHttpMessageConverter(UTF_8), // String - new FormHttpMessageConverter(), // Form x-www-form-urlencoded, multipart/form-data multipart/mixed - new ByteArrayHttpMessageConverter(), // byte[] octet-stream - new ResourceHttpMessageConverter(), // Resource, ByteArrayResource - mappingJackson2HttpMessageConverter // JSON - )); - return restTemplate; - } - - - @Value("${nexus.backend.client.connectTimeout:10}") - private int connectTimeout; - @Value("${nexus.backend.client.requestTimeout:20}") - private int requestTimeout; - @Value("${nexus.backend.client.socketTimeout:10}") - private int socketTimeout; - - @Value("${nexus.backend.client.max_connections_per_route:20}") - private int defaultMaxConnectionsPerRoute; - - @Value("${nexus.backend.client.max_connections:100}") - private int maxConnections; - - @Value("${nexus.backend.client.close_idle_connections_timeout:0}") - private int closeIdleConnectionsTimeout; - - @Value("${nexus.backend.client.validate_after_inactivity:2}") - private int validateAfterInactivity; - - @Value("${nexus.backend.client.retryCount:3}") - private int retryCount; - @Value("${nexus.backend.client.requestSentRetryEnabled:false}") - private boolean requestSentRetryEnabled; - - @Value("${nexus.backend.client.redirectsEnabled:false}") - private boolean redirectsEnabled; - @Value("${nexus.backend.client.maxRedirects:5}") - private int maxRedirects; - @Value("${nexus.backend.client.authenticationEnabled:false}") - private boolean authenticationEnabled; - @Value("${nexus.backend.client.circularRedirectsAllowed:false}") - private boolean circularRedirectsAllowed; - - - /** - * User-Agent - */ - @Value("${nexus.backend.client.header.user-agent:JavaNexus}") - private String userAgent; - - - /** - * Activated the Mutual Authentication or mTLS, default protocol TLSv1.3 - */ - @Value("${nexus.backend.client.ssl.mtls.enable:false}") - private boolean isMTLS; - - @Value("${nexus.backend.client.ssl.key-store:nexus-default.jks}") - private String pathJKS; - @Value("${nexus.backend.client.ssl.key-store-password:changeit}") - private String keyStorePassword; - @Value("${nexus.backend.client.ssl.certificate.alias:key_server}") - private String certificateAlias; - /** - * SpEL reads allow method delimited with a comma and splits into a List of Strings - */ - @Value("#{'${nexus.backend.client.ssl.https.protocols:TLSv1.3}'.split(',')}") - private List httpsProtocols; - @Value("#{'${nexus.backend.client.ssl.https.cipherSuites:TLS_AES_256_GCM_SHA384}'.split(',')}") - private List httpsCipherSuites; - - @Bean - public ClientHttpRequestFactory httpRequestFactory() throws Exception { - - final DefaultConnectionKeepAliveStrategy myStrategy = new DefaultConnectionKeepAliveStrategy() { - @Override - public long getKeepAliveDuration(HttpResponse response, HttpContext context) { - return super.getKeepAliveDuration(response, context); - } - }; - - final HttpConnectionFactory connFactory = - new ManagedHttpClientConnectionFactory( - new DefaultHttpRequestWriterFactory(), - new DefaultHttpResponseParserFactory( - new CompliantLineParser(), new DefaultHttpResponseFactory())); - - final PoolingHttpClientConnectionManager cm; - - if (isMTLS) { - final KeyStore identityKeyStore = KeyStore.getInstance("jks"); - final FileInputStream identityKeyStoreFile = new FileInputStream(pathJKS); - identityKeyStore.load(identityKeyStoreFile, keyStorePassword.toCharArray()); - - final KeyStore trustKeyStore = KeyStore.getInstance("jks"); - final FileInputStream trustKeyStoreFile = new FileInputStream(pathJKS); - trustKeyStore.load(trustKeyStoreFile, keyStorePassword.toCharArray()); - - final SSLContext sslContext = SSLContexts.custom() - // load identity keystore - .loadKeyMaterial(identityKeyStore, keyStorePassword.toCharArray(), new PrivateKeyStrategy() { - @Override - public String chooseAlias(Map aliases, Socket socket) { - return certificateAlias; - } - }) - // load trust keystore - .loadTrustMaterial(trustKeyStore, null) - .build(); - - // WARN only protocol TLSv1.3, Not a mix with TLSv1.2,TLSv1.1 cause SSLSocket duplex close failed!!! - // WARN only CipherSuites TLS_AES_256_GCM_SHA384 or TLS_AES_128_GCM_SHA256 - final SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext, - httpsProtocols.toArray(new String[0]), - httpsCipherSuites.toArray(new String[0]), // TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256 - SSLConnectionSocketFactory.getDefaultHostnameVerifier()); - - // WARN Not set a sslConnectionSocketFactory cause a HandshakeContext with a dummy KeyManager!!! - cm = new PoolingHttpClientConnectionManager(RegistryBuilder.create() - .register("http", PlainConnectionSocketFactory.getSocketFactory()) - .register("https", sslConnectionSocketFactory) - .build(), connFactory); - } else { - cm = new PoolingHttpClientConnectionManager(connFactory); - } - - cm.setDefaultMaxPerRoute(defaultMaxConnectionsPerRoute); - cm.setMaxTotal(maxConnections); - cm.setValidateAfterInactivity(validateAfterInactivity * 1000); - cm.closeIdleConnections(closeIdleConnectionsTimeout, TimeUnit.SECONDS); - - return new HttpComponentsClientHttpRequestFactory(HttpClientBuilder.create() - .setUserAgent(userAgent) - .setConnectionManager(cm) - .setDefaultRequestConfig(RequestConfig.custom() - .setCookieSpec(CookieSpecs.STANDARD) // Optional specs standard! - .setConnectTimeout(connectTimeout * 1000) - .setConnectionRequestTimeout(requestTimeout * 1000) - .setSocketTimeout(socketTimeout * 1000) - .setRedirectsEnabled(redirectsEnabled) // mandatory disabled by default! - .setMaxRedirects(maxRedirects) - .setAuthenticationEnabled(authenticationEnabled) - .setCircularRedirectsAllowed(circularRedirectsAllowed) - .build()) - .setConnectionReuseStrategy(new DefaultConnectionReuseStrategy()) - .setKeepAliveStrategy(myStrategy) - .setRedirectStrategy(new LaxRedirectStrategy()) - .setRetryHandler(new DefaultHttpRequestRetryHandler(retryCount, requestSentRetryEnabled)) - .disableRedirectHandling() - // Cookie client stateful managed, the Gateway is Stateless! Session state is temporary and only affects the transaction! - //.disableCookieManagement() - .disableAuthCaching() - .disableConnectionState() - .build()); - } - - /** - * Force HttpClient into accepting malformed response heads in order to salvage the content of the messages. - * (Deal non-standard and non-compliant behaviours!) - */ - static class CompliantLineParser extends BasicLineParser { - @Override - public Header parseHeader(CharArrayBuffer buffer) throws ParseException { - try { - return super.parseHeader(buffer); - } catch (ParseException ex) { - // Suppress ParseException exception - return new BasicHeader(buffer.toString(), null); - } - } - } - -} diff --git a/src/main/java/com/jservlet/nexus/config/JServletBanner.java b/src/main/java/com/jservlet/nexus/config/JServletBanner.java deleted file mode 100644 index 53616d1..0000000 --- a/src/main/java/com/jservlet/nexus/config/JServletBanner.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (C) 2001-2024 JServlet.com Franck Andriano. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - */ - -package com.jservlet.nexus.config; - -import org.springframework.boot.Banner; -import org.springframework.boot.ansi.AnsiBackground; -import org.springframework.boot.ansi.AnsiColor; -import org.springframework.boot.ansi.AnsiOutput; -import org.springframework.boot.ansi.AnsiStyle; -import org.springframework.context.ResourceLoaderAware; -import org.springframework.core.env.Environment; -import org.springframework.core.io.Resource; -import org.springframework.core.io.ResourceLoader; -import org.springframework.lang.NonNull; -import org.springframework.stereotype.Component; - -import javax.annotation.PostConstruct; -import java.io.IOException; -import java.io.PrintStream; -import java.util.MissingResourceException; -import java.util.Properties; - -/** - * JServlet NexusBackend Banner - */ -@Component -public class JServletBanner implements Banner, ResourceLoaderAware { - - private ResourceLoader resourceLoader; - private String VERSION; - - @PostConstruct - public void postConstruct() throws IOException { - Resource resource = resourceLoader.getResource("classpath:META-INF/version.properties"); - if (!resource.exists()) - throw new MissingResourceException("Unable to find \"version.properties\" on classpath!", - getClass().getName(), "version.properties"); - - Properties properties = new Properties(); - properties.load(resource.getInputStream()); - VERSION = properties.getProperty("version"); - } - - private final String[] BANNER = { "", // line break! - " _ _ ___ _ _ \n" + - " | \\| | ___ ____ _ _ ___ | _ ) __ _ __ | |__ ___ _ _ __| |\n" + - " | .` |/ -_)\\ \\ /| || |(_-< | _ \\/ _` |/ _|| / // -_)| ' \\ / _` |\n" + - " |_|\\_|\\___|/_\\_\\ \\_,_| /__/|___/\\__,_|\\__||_\\_\\\\___||_||_|\\__,_| " - }; - - private static final String NEXUS_BACKEND = " :: NexusBackend :: Secure RestApi Backend Gateway"; - private static final int STRAP_LINE_SIZE = 63; - - @Override - public void printBanner(Environment environment, Class sourceClass, PrintStream printStream) { - for (String line : BANNER) { - printStream.println(AnsiOutput.toString(AnsiColor.CYAN, AnsiBackground.DEFAULT, AnsiStyle.BOLD, line)); - } - String version = " (v" + VERSION + ")"; - StringBuilder padding = new StringBuilder(); - while (padding.length() < STRAP_LINE_SIZE - (version.length() + NEXUS_BACKEND.length())) { - padding.append(" "); - } - printStream.println(AnsiOutput.toString(AnsiColor.GREEN, NEXUS_BACKEND, AnsiColor.DEFAULT, padding.toString(), - AnsiStyle.FAINT, version)); - printStream.println(); - } - - @Override - public void setResourceLoader(@NonNull ResourceLoader resourceLoader) { - this.resourceLoader = resourceLoader; - } -} diff --git a/src/main/java/com/jservlet/nexus/config/JServletContainerInitializer.java b/src/main/java/com/jservlet/nexus/config/JServletContainerInitializer.java deleted file mode 100644 index b90d59d..0000000 --- a/src/main/java/com/jservlet/nexus/config/JServletContainerInitializer.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (C) 2001-2024 JServlet.com Franck Andriano. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - */ - -package com.jservlet.nexus.config; - -import org.springframework.web.WebApplicationInitializer; - -import javax.servlet.ServletContainerInitializer; -import javax.servlet.ServletContext; -import javax.servlet.ServletException; -import javax.servlet.annotation.HandlesTypes; -import java.util.*; - -/** - * JServlet Container Initializer only with an external Tomcat (Not embedded) - */ -@HandlesTypes(WebApplicationInitializer.class) -public class JServletContainerInitializer implements ServletContainerInitializer { - - @Override - public void onStartup(Set> c, ServletContext ctx) throws ServletException { - // All Servlets Initializer - System.out.println(); - System.out.println("Started Servlets Container Initializer:"); - for (Class clazz : c) { - System.out.println("- " + clazz); - } - } -} - diff --git a/src/main/java/com/jservlet/nexus/config/JServletContextListener.java b/src/main/java/com/jservlet/nexus/config/JServletContextListener.java deleted file mode 100644 index 09f3a4f..0000000 --- a/src/main/java/com/jservlet/nexus/config/JServletContextListener.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright (C) 2001-2024 JServlet.com Franck Andriano. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - */ - -package com.jservlet.nexus.config; - -import ch.qos.logback.classic.LoggerContext; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.boot.ansi.AnsiColor; -import org.springframework.boot.ansi.AnsiOutput; -import org.springframework.context.ApplicationContext; -import org.springframework.lang.NonNull; -import org.springframework.stereotype.Component; -import org.springframework.web.context.ConfigurableWebApplicationContext; -import org.springframework.web.context.ContextCleanupListener; -import org.springframework.web.context.support.WebApplicationContextUtils; - -import javax.servlet.ServletContext; -import javax.servlet.ServletContextEvent; - -/** - * NexusBackend ServletContext Listener - */ -@Component -public class JServletContextListener extends ContextCleanupListener { - - private final static Logger logger = LoggerFactory.getLogger(JServletContextListener.class); - - private final JServletBanner jServletBanner; - - public JServletContextListener(JServletBanner jServletBanner) { - this.jServletBanner = jServletBanner; - } - - @Override - public void contextInitialized(@NonNull ServletContextEvent event) { - logger.info("Starting NexusBackend ServletContext Listener"); - // Banner - jServletBanner.printBanner(null, null, System.out); - - if (logger.isInfoEnabled()) { - System.out.println(" Started NexusBackend ServletContext"); - ServletContext servletContext = event.getServletContext(); - ApplicationContext appContext = WebApplicationContextUtils.getWebApplicationContext(event.getServletContext()); - // Server Info - System.out.println(" ServerInfo: " + - AnsiOutput.toString(AnsiColor.GREEN, servletContext.getServerInfo())); - System.out.println(" ServletApi: " + - AnsiOutput.toString(AnsiColor.GREEN, "Specifications v" + servletContext.getMajorVersion() + "."+ servletContext.getMinorVersion())); - System.out.println(); - } - } - - @Override - public void contextDestroyed(ServletContextEvent event) { - - // Assume SLF4J is bound to logback-classic in the current environment - LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); - loggerContext.stop(); - - /*ClassLoader webAppClassLoader = this.getClass().getClassLoader(); - Class clazz = null; - try { - clazz = Class.forName("com..xxx.XMLRequestInterface",false, webAppClassLoader); - } catch (ClassNotFoundException ignore) { - //log exception or ignore - } - - if (clazz != null) { - if ((clazz.getClassLoader() == webAppClassLoader)) { - XMLRequestInterface.clearStaticTableCache(); - } - }*/ - - // Close webapp context - closeWebApplicationContext(event.getServletContext()); - - // Call super destroy - super.contextDestroyed(event); - - // console logs, no more logs! - System.out.println("Nexus-Backend ServletContext destroyed"); - } - - private void closeWebApplicationContext(ServletContext servletContext) { - if (servletContext instanceof ConfigurableWebApplicationContext) { - ((ConfigurableWebApplicationContext) servletContext).close(); - } - } - -} diff --git a/src/main/java/com/jservlet/nexus/config/JServletInitializer.java b/src/main/java/com/jservlet/nexus/config/JServletInitializer.java deleted file mode 100644 index 355fef8..0000000 --- a/src/main/java/com/jservlet/nexus/config/JServletInitializer.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (C) 2001-2025 JServlet.com Franck Andriano. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - */ - -package com.jservlet.nexus.config; - -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; -import org.springframework.context.annotation.Profile; - -/** - * SpringBoot Tomcat Container ServletInitializer - * Only a Spring profile 'withoutTomcat' (or 'withTomcat' with a Tomcat Embedded by SpringBoot!) - */ -@Profile("withoutTomcat") -public class JServletInitializer extends SpringBootServletInitializer { - - @Override - protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { - // Swagger is only available in dev! - String env = System.getProperty("environment", "development"); - if ("development".equals(env)) System.setProperty("springdoc.swagger-ui.enabled", "true"); - // run app - return application.sources(Application.class); - } -} diff --git a/src/main/java/com/jservlet/nexus/config/web/SwaggerConfig.java b/src/main/java/com/jservlet/nexus/config/web/SwaggerConfig.java deleted file mode 100644 index 0e3eca1..0000000 --- a/src/main/java/com/jservlet/nexus/config/web/SwaggerConfig.java +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright (C) 2001-2024 JServlet.com Franck Andriano. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - */ - -package com.jservlet.nexus.config.web; - -import io.swagger.v3.oas.models.info.*; -import io.swagger.v3.oas.models.Components; -import io.swagger.v3.oas.models.OpenAPI; -import io.swagger.v3.oas.models.info.Info; -import io.swagger.v3.parser.OpenAPIV3Parser; -import org.springdoc.core.GroupedOpenApi; -import org.springdoc.core.customizers.OpenApiCustomiser; -import org.springframework.context.ResourceLoaderAware; -import org.springframework.context.annotation.*; -import org.springframework.core.io.Resource; -import org.springframework.core.io.ResourceLoader; -import org.springframework.core.type.AnnotatedTypeMetadata; -import org.springframework.lang.NonNull; - -import javax.annotation.PostConstruct; -import java.io.IOException; -import java.util.MissingResourceException; -import java.util.Properties; - -/** - * Swagger mock-api only available in Dev mode, added in JVM Options: -Denvironment=development - */ -@Conditional(SwaggerConfig.IsDevEnvCondition.class) -@Configuration -public class SwaggerConfig implements ResourceLoaderAware { - - private ResourceLoader resourceLoader; - - private String VERSION; - - @PostConstruct - public void postConstruct() throws IOException { - Resource resource = resourceLoader.getResource("classpath:META-INF/version.properties"); - if (!resource.exists()) - throw new MissingResourceException("Unable to find \"version.properties\" on classpath!", - getClass().getName(), "version.properties"); - Properties properties = new Properties(); - properties.load(resource.getInputStream()); - VERSION = properties.getProperty("version"); - //System.getProperties().list(System.out); - } - - @Override - public void setResourceLoader(@NonNull ResourceLoader resourceLoader) { - this.resourceLoader = resourceLoader; - } - - @Bean - public GroupedOpenApi mockOpenApi() { - return GroupedOpenApi.builder() - .group("mock-api") - .packagesToScan("com.jservlet.nexus.controller") - .addOpenApiCustomiser(new OpenApiCustomised("api-ui/api-docs.yaml")) - .build(); - } - - /** - * OpenApi customised config with the file api-ui/api-docs.yaml - */ - private class OpenApiCustomised implements OpenApiCustomiser { - - private final String specLocation; - - public OpenApiCustomised(String specLocation) { - this.specLocation = specLocation; - } - - @Override - public void customise(OpenAPI openApi) { - - // Load raw data from file format openapi 3.0.1 - OpenAPI api = new OpenAPIV3Parser().read(specLocation); - - // Keep all the existing schemas.. - Components components = openApi.getComponents(); - - // Get the component that we just parse, but not securitySchemes! - Components componentsApi = api.getComponents(); - if (componentsApi != null) { - componentsApi.schemas(components.getSchemas()); - componentsApi.headers(components.getHeaders()); - componentsApi.extensions(components.getExtensions()); - componentsApi.callbacks(components.getCallbacks()); - componentsApi.links(components.getLinks()); - //componentsApi.securitySchemes(components.getSecuritySchemes()); - - // Set the customised Components Security Schemes - openApi.setComponents(componentsApi); - } - - // Set the customised Security, see https://swagger.io/docs/specification/authentication/ - openApi.setSecurity(api.getSecurity()); - - // Apply the info now! - openApi.setInfo(apiInfo()); - - // not, in all case, let the scan package do it for us! - //openApi.setPaths(api.getPaths()); - - // not used! - openApi.setExtensions(api.getExtensions()); - openApi.setExternalDocs(api.getExternalDocs()); - openApi.setServers(api.getServers()); - openApi.setTags(api.getTags()); - } - } - - - private Info apiInfo() { - return new Info() - .title("Mock Test API") - .description( "The Mock Test Api Nexus Backend Application\n" + - "- 1 Test GET, POST, PUT, PATCH, DELETE\n" + - "- 2 Test POST, PUT, PATCH file\n" + - "- 3 Test Error 400, 401 and 500\n" + - "- 4 Test Security GET and POST XSS data\n") - .version(VERSION) - .contact(new Contact().name("Franck Andriano.").email("franck@jservlet.com")) - .license(new License().name("Copyright (c) JServlet.com").url("https://jservlet.com")); - } - - static class IsDevEnvCondition implements Condition { - @Override - public boolean matches(ConditionContext context, @NonNull AnnotatedTypeMetadata metadata) { - final String env = context.getEnvironment().getProperty("environment"); - if (env == null || env.isEmpty()) { - return false; - } - return env.contains("development"); - } - } - -} diff --git a/src/main/java/com/jservlet/nexus/config/web/WebConfig.java b/src/main/java/com/jservlet/nexus/config/web/WebConfig.java deleted file mode 100644 index 1915435..0000000 --- a/src/main/java/com/jservlet/nexus/config/web/WebConfig.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright (C) 2001-2025 JServlet.com Franck Andriano. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - */ - -package com.jservlet.nexus.config.web; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.jservlet.nexus.shared.web.interceptor.RateLimitInterceptor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.env.OriginTrackedMapPropertySource; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.env.*; -import org.springframework.lang.NonNull; - -import java.util.LinkedHashMap; -import java.util.Map; - -/* - * Web App Configuration - */ -@Configuration -@ComponentScan({ - "com.jservlet.nexus.controller", - // Scan from shared server components - "com.jservlet.nexus.shared.web.controller", - "com.jservlet.nexus.shared.web.filter", - "com.jservlet.nexus.shared.web.listener" -}) -public class WebConfig implements ApplicationContextAware { - - private final static Logger logger = LoggerFactory.getLogger(WebConfig.class); - - private static ApplicationContext appContext; - - private final Environment env; - - private final ObjectMapper objectMapper; - - public WebConfig(Environment env, ObjectMapper objectMapper) { - this.env = env; - this.objectMapper = objectMapper; - } - - @Override - public synchronized void setApplicationContext(@NonNull ApplicationContext ac) { - logger.info("SpringBoot set ApplicationContext -> ac: ['{}']", ac.getId()); - WebConfig.appContext = ac; - - if (logger.isInfoEnabled()) { - Map map = getApplicationProperties(env); - // Debug also log system out! - logger.info("*** SpringBoot ApplicationProperties ***"); - for (Map.Entry entry : map.entrySet()) { - logger.info("{} = {}", entry.getKey(), entry.getValue()); - } - } - } - - /** - * Get the current ApplicationContext - * @return ApplicationContext - */ - public static synchronized ApplicationContext getApplicationContext() { - return appContext; - } - - /* Stuff, get loaded SpringBoot Application Properties */ - private static Map getApplicationProperties(Environment env) { - Map map = new LinkedHashMap<>(); // keep order! - if (env instanceof ConfigurableEnvironment) { - for (PropertySource propertySource : ((ConfigurableEnvironment) env).getPropertySources()) { - // WARN all tracked SpringBoot config file *.properties! - if (propertySource instanceof OriginTrackedMapPropertySource) { - for (String key : ((EnumerablePropertySource) propertySource).getPropertyNames()) { - map.put(key, propertySource.getProperty(key)); - } - } - // Include Spring devtools - if (propertySource instanceof MapPropertySource && "devtools".equals(propertySource.getName())) { - for (String key : ((MapPropertySource) propertySource).getPropertyNames()) { - map.put(key, propertySource.getProperty(key)); - } - } - } - } - return map; - } - - - @Value("${nexus.backend.interceptor.ratelimit.refillToken:1000}") - private int refillToken; - @Value("${nexus.backend.interceptor.ratelimit.refillMinutes:1}") - private int refillMinutes; - @Value("${nexus.backend.interceptor.ratelimit.bandwidthCapacity:1000}") - private int bandwidthCapacity; - - /** - * Rate Limit interceptor checks incoming requests against a rate limit defined on a per-IP-address basis. - * @return RateLimitInterceptor - */ - @Bean - @ConditionalOnProperty(value = "nexus.api.backend.interceptor.ratelimit.enabled") - public RateLimitInterceptor limitInterceptor() { - RateLimitInterceptor limitInterceptor = new RateLimitInterceptor(); - limitInterceptor.setObjectMapper(objectMapper); - limitInterceptor.setRefillToken(refillToken); - limitInterceptor.setRefillMinutes(refillMinutes); - limitInterceptor.setBandwidthCapacity(bandwidthCapacity); - return limitInterceptor; - } -} diff --git a/src/main/java/com/jservlet/nexus/config/web/WebConstants.java b/src/main/java/com/jservlet/nexus/config/web/WebConstants.java deleted file mode 100644 index 795e04f..0000000 --- a/src/main/java/com/jservlet/nexus/config/web/WebConstants.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (C) 2001-2024 JServlet.com Franck Andriano. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - */ - -package com.jservlet.nexus.config.web; - -/* - * Web Constants - */ -public interface WebConstants { - /** - * CONSTANTS RESPONSE - */ - String $200 = "200"; - String $201 = "201"; - String $204 = "204"; - - String $400 = "400"; - String $401 = "401"; - String $403 = "403"; - String $404 = "404"; - String $405 = "405"; - String $415 = "415"; - String $422 = "422"; - - String $500 = "500"; - - String REQ_SUCCESSFULLY = "Request executed successfully, returning the requested item(s)"; - String REQ_NOT_CORRECTLY = "Request is not formed correctly"; - String USER_NOT_AUTH = "User not authenticated"; - String INTERNAL_SERVER = "Internal server error, see error code and documentation for more details"; - - String PERM_DENIED = "Permission denied for this resource"; - String HTTP_NOT_ALLOWED = "HTTP Method (GET, POST, ...) not allowed for this resource"; - String REQ_SUCC_EMPTY_RESP = "Request executed successfully, returning an empty response"; - String ITEM_NOT_FOUND = "Requested item not found, see error code and documentation for more details"; - String PERMISSION_DENIED = "Permission denied for this resource"; - String UNSUPPORTED_MEDIA_TYPE = "Unsupported Media Type"; - String VALIDATION_ERROR = "Error while validating request's content, see error code and documentation for more details"; - -} diff --git a/src/main/java/com/jservlet/nexus/config/web/WebFilterConfig.java b/src/main/java/com/jservlet/nexus/config/web/WebFilterConfig.java deleted file mode 100644 index 63af2f8..0000000 --- a/src/main/java/com/jservlet/nexus/config/web/WebFilterConfig.java +++ /dev/null @@ -1,95 +0,0 @@ -package com.jservlet.nexus.config.web; - -import com.github.ziplet.filter.compression.CompressingFilter; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.web.servlet.FilterRegistrationBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.annotation.Order; -import org.springframework.core.env.Environment; -import org.springframework.lang.NonNull; -import org.springframework.web.context.ServletContextAware; -import org.springframework.web.filter.CommonsRequestLoggingFilter; -import org.springframework.web.filter.ForwardedHeaderFilter; - -import javax.servlet.Filter; -import javax.servlet.ServletContext; - -/* - * Web Filter Chain Configuration - */ -@Configuration -public class WebFilterConfig implements ServletContextAware { - - private ServletContext servletContext; - private final Environment env; - - @Value("${nexus.backend.filter.forwardedHeader.removeOnly:true}") - private boolean forwardedHeaderRemoveOnly; - - public WebFilterConfig(Environment env) { - this.env = env; - } - - @Override - public void setServletContext(@NonNull ServletContext servletContext) { - this.servletContext = servletContext; - } - - /** - * Forwarded Header Filter, see rfc7239 - * RemoveOnly at true, discard and ignore forwarded headers - * - * @return Forwarded Filter Bean - */ - @Bean - @Order(1) - @ConditionalOnProperty(value="nexus.backend.filter.forwardedHeader.enabled") - public FilterRegistrationBean forwardedInfoFilter() { - FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); - ForwardedHeaderFilter filter = new ForwardedHeaderFilter(); - filter.setRemoveOnly(forwardedHeaderRemoveOnly); - registrationBean.setFilter(filter); - registrationBean.setOrder(1); - return registrationBean; - } - - /** - * Generally use gzip for all resources ! - * - * @return a gzip implementation (only response) - */ - @Bean - @Order(2) - @ConditionalOnProperty(value="nexus.backend.filter.gzip.enabled") - public FilterRegistrationBean gzipFilter() { - FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); - registrationBean.setFilter(new CompressingFilter()); - registrationBean.setOrder(2); - return registrationBean; - } - - - /** - * The full logs request with payload! - * - * @return The Logging filter - */ - @Bean - public CommonsRequestLoggingFilter logFilter() { - CommonsRequestLoggingFilter filter = new CommonsRequestLoggingFilter(); - filter.setIncludeClientInfo(true); - filter.setIncludeQueryString(true); - filter.setIncludePayload(true); - filter.setIncludeHeaders(true); - filter.setHeaderPredicate(header -> !header.equalsIgnoreCase("authorization")); - filter.setMaxPayloadLength(10000); - filter.setBeforeMessagePrefix("INCOMING REQUEST : "); - filter.setAfterMessagePrefix("OUTGOING REQUEST : "); - filter.setServletContext(servletContext); - filter.setEnvironment(env); - return filter; - } - -} diff --git a/src/main/java/com/jservlet/nexus/config/web/WebMvcConfig.java b/src/main/java/com/jservlet/nexus/config/web/WebMvcConfig.java deleted file mode 100644 index c677ac3..0000000 --- a/src/main/java/com/jservlet/nexus/config/web/WebMvcConfig.java +++ /dev/null @@ -1,167 +0,0 @@ -package com.jservlet.nexus.config.web; - -import com.jservlet.nexus.shared.web.interceptor.RateLimitInterceptor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.web.server.WebServerFactoryCustomizer; -import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory; -import org.springframework.context.ResourceLoaderAware; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.core.env.Environment; -import org.springframework.core.io.Resource; -import org.springframework.core.io.ResourceLoader; -import org.springframework.http.HttpMethod; -import org.springframework.http.MediaType; -import org.springframework.lang.NonNull; -import org.springframework.util.StringUtils; -import org.springframework.web.multipart.MultipartResolver; -import org.springframework.web.multipart.support.StandardServletMultipartResolver; -import org.springframework.web.servlet.config.annotation.*; -import org.springframework.web.servlet.view.InternalResourceViewResolver; - -import javax.servlet.http.HttpServletRequest; -import java.io.IOException; -import java.util.EnumSet; -import java.util.Optional; -import java.util.Properties; -import java.util.Set; - -/* - * Web Mvc Configuration - */ -@Configuration -@EnableWebMvc -@Import(SwaggerConfig.class) -public class WebMvcConfig implements WebMvcConfigurer, ResourceLoaderAware { - - private final static Logger logger = LoggerFactory.getLogger(WebMvcConfig.class); - private ResourceLoader resourceLoader; - - private final Environment env; - private static final String ENV_VAR = "environment"; - - private final RateLimitInterceptor rateLimitInterceptor; - - public WebMvcConfig(Optional rateLimitInterceptor, Environment env) { - this.rateLimitInterceptor = rateLimitInterceptor.orElse(null); - this.env = env; - } - - @Override - public void setResourceLoader(@NonNull ResourceLoader resourceLoader) { - this.resourceLoader = resourceLoader; - } - - @Override - public void addInterceptors(@NonNull InterceptorRegistry registry) { - if (rateLimitInterceptor != null) { - registry.addInterceptor(rateLimitInterceptor).addPathPatterns("/api/**"); - } - } - - @Override - public void configureViewResolvers(ViewResolverRegistry registry) { - InternalResourceViewResolver viewResolver = new InternalResourceViewResolver(); - viewResolver.setPrefix("/WEB-INF/views/"); - viewResolver.setSuffix(".jsp"); - registry.viewResolver(viewResolver); - } - - @Override - public void addResourceHandlers(@NonNull ResourceHandlerRegistry registry) { - if ("development".equals(env.getProperty(ENV_VAR))) { - registry.addResourceHandler("/swagger-ui/**").resourceChain(false); - } - registry.addResourceHandler("/resources/api-ui/**").addResourceLocations("/resources/"); - registry.addResourceHandler("/static/**").addResourceLocations("/static/"); - } - - @Override - public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { - configurer.enable(); - } - - @Value("${nexus.backend.content.negotiation.favorParameter:false}") - private boolean favorParameter; - @Value("${nexus.backend.content.negotiation.parameterName:mediaType}") - private String parameterName; - - @Value("${nexus.backend.content.negotiation.ignoreAcceptHeader:false}") - private boolean ignoreAcceptHeader; - - @Value("${nexus.backend.content.negotiation.useRegisteredExtensionsOnly:true}") - private boolean useRegisteredExtensionsOnly; - - @Value("${nexus.backend.content.negotiation.commonMediaTypes:true}") - private boolean commonMediaTypes; - - - /** - * Configure a default ContentNegotiation with a default HeaderContentNegotiationStrategy (ignoreAcceptHeader at false) - * Force the defaultContentType by application/octet-stream - * @see org.springframework.web.accept.ContentNegotiationManagerFactoryBean - * - * @param configurer The current ContentNegotiationConfigurer - */ - @Override - public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { - configurer // JSON Mandatory forced for Resource 404 not found from the Backend - .defaultContentType(MediaType.APPLICATION_JSON, MediaType.APPLICATION_OCTET_STREAM) - //.defaultContentTypeStrategy(new HeaderContentNegotiationStrategy()) - .favorParameter(favorParameter) - .parameterName(parameterName) - .ignoreAcceptHeader(ignoreAcceptHeader) - .useRegisteredExtensionsOnly(useRegisteredExtensionsOnly) - .mediaType("pdf", MediaType.APPLICATION_PDF); // Add pdf as safe extensions by default! - - if (commonMediaTypes) { - extractedMediaTypes(configurer, "classpath:mime/MediaTypes_commons.properties"); - } - } - - @SuppressWarnings("SameParameterValue") - private void extractedMediaTypes(ContentNegotiationConfigurer configurer, String classpath) { - Resource resource = resourceLoader.getResource(classpath); - if (resource.exists()) { - try { - Properties properties = new Properties(); - properties.load(resource.getInputStream()); - properties.forEach((key, value) -> - configurer.mediaType(key.toString(), MediaType.parseMediaType(value.toString()))); - } catch (IOException e) { - logger.error("Failed to load '{}' media types {}", classpath, e.getMessage()); - } - } - } - - - /** - * Use Servlet 4 style MultipartResolver: StandardServletMultipartResolver
- * Strict Servlet compliance only multipart/form-data - * @return the MultipartResolver, - */ - @Bean - public MultipartResolver multipartResolver() { - return new StandardServletMultipartResolver() { - private final Set SUPPORTED_METHODS = EnumSet.of(HttpMethod.POST, HttpMethod.PUT, HttpMethod.PATCH); - @Override - public boolean isMultipart(@NonNull HttpServletRequest request) { - if (!SUPPORTED_METHODS.contains(HttpMethod.resolve(request.getMethod()))) return false; - return StringUtils.startsWithIgnoreCase(request.getContentType(), MediaType.MULTIPART_FORM_DATA_VALUE); - } - }; - } - - /** - * Springboot only, WebServerFactory register Servlet (and Jsp!) - * @return the factory - */ - @Bean - public WebServerFactoryCustomizer enableDefaultServlet() { - return (factory) -> factory.setRegisterDefaultServlet(true); - } - -} diff --git a/src/main/java/com/jservlet/nexus/config/web/WebSecurityConfig.java b/src/main/java/com/jservlet/nexus/config/web/WebSecurityConfig.java deleted file mode 100644 index 64cb839..0000000 --- a/src/main/java/com/jservlet/nexus/config/web/WebSecurityConfig.java +++ /dev/null @@ -1,328 +0,0 @@ -/* - * Copyright (C) 2001-2025 JServlet.com Franck Andriano. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - */ - -package com.jservlet.nexus.config.web; - -import com.jservlet.nexus.shared.web.filter.WAFPredicate; -import com.jservlet.nexus.shared.web.security.WebHttpFirewall; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; -import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; -import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; -import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.firewall.*; -import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; -import org.springframework.web.cors.CorsConfiguration; -import org.springframework.web.cors.CorsConfigurationSource; -import org.springframework.web.cors.UrlBasedCorsConfigurationSource; - -import javax.servlet.ServletContext; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.util.List; -import java.util.regex.Pattern; - -/* - * Web Security Configuration: HttpFirewall, FilterChain and Customizer - */ -@Configuration -@EnableWebSecurity //(debug = true) -public class WebSecurityConfig { - - private final static Logger logger = LoggerFactory.getLogger(WebSecurityConfig.class); - - private final ServletContext servletContext; - - @Value("${nexus.spring.web.security.debug:false}") - private boolean webSecurityDebug; - - public WebSecurityConfig(ServletContext servletContext) { - this.servletContext = servletContext; - } - - // WARN no UserDetailsServiceAutoConfiguration - - - /** - * Performs some WebSecurity customizations: - * - debug - * - httpFirewall - * - servletContext - * @param webHttpFirewall The HttpFirewall - * @return A WebSecurity customizer! - */ - @Bean - public WebSecurityCustomizer webSecurityCustomizer(HttpFirewall webHttpFirewall) { - return (web) -> web.debug(webSecurityDebug).httpFirewall(webHttpFirewall) - .setServletContext(servletContext); - } - - @Value("${nexus.spring.web.security.csp.policy:default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none'; object-src 'none';}") - private String cspPolicyDirectives; - - @Value("${nexus.spring.web.security.hsts.maxAgeInSeconds:31536000}") - private long maxAgeInSeconds; - @Value("${nexus.spring.web.security.hsts.includeSubDomains:false}") - private boolean includeSubDomains; - - @Value("${nexus.spring.web.security.referrer.policy:NO_REFERRER}") - private String referrerPolicy; - - /** - * SecurityFilterChain - * - * @param http The HttpSecurity - * @param corsConfigurationSource The Cors config - * @throws Exception An exception - * @return SecurityFilterChain A WebSecurity customizer! - */ - @Bean - public SecurityFilterChain filterChain(HttpSecurity http, CorsConfigurationSource corsConfigurationSource) throws Exception { - // cors config, csrf disable - http.cors(c -> c.configurationSource(corsConfigurationSource)) - .csrf(AbstractHttpConfigurer::disable); - // session STATELESS - http.sessionManagement(session -> - session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); - // authorize HttpRequests - http.authorizeHttpRequests(auth -> auth - .requestMatchers( - // Use AntPathRequestMatcher for remove ambiguity with Servlet/JSP - new AntPathRequestMatcher("/"), - new AntPathRequestMatcher("/mock/**"), - new AntPathRequestMatcher("/static/**"), - new AntPathRequestMatcher("/actuator/**"), - new AntPathRequestMatcher("/swagger-ui/index.html"), - new AntPathRequestMatcher("/api/**"), - new AntPathRequestMatcher("/error") - ).permitAll() - ); - - // security headers - http.headers(headers -> { - headers.contentTypeOptions(); // X-Content-Type-Options: nosniff - headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin); // X-Frame-Options: SAMEORIGIN - headers.cacheControl(); // Cache-Control: no-cache, no-store, max-age=0, must-revalidate - headers.xssProtection(); // X-XSS-Protection: 1; mode=block - if (!cspPolicyDirectives.isEmpty()) { // Content Security Policy: default-src 'none'; frame-ancestors 'none'; - headers.contentSecurityPolicy(csp -> csp - .policyDirectives(cspPolicyDirectives) - ); - } - headers.httpStrictTransportSecurity( - hsts -> { // Strict-Transport-Security: max-age=31536000; includeSubDomains - hsts.maxAgeInSeconds(maxAgeInSeconds); - hsts.includeSubDomains(includeSubDomains); - } - ); - // Referrer-Policy: no-referrer - headers.referrerPolicy(ReferrerPolicyHeaderWriter.ReferrerPolicy.valueOf(referrerPolicy)); - }); - return http.build(); - } - - - /** - * Spring EL reads allow method delimited with a comma and splits into a List of Strings (or an empty List) - */ - // Provide a List of HttpMethods - @Value("#{T(java.util.Arrays).asList('${nexus.backend.security.cors.allowedHttpMethods:GET,POST,PUT,OPTION,HEAD,DELETE,PATCH}')}") - private List allowedCorsHttpMethods; - // Provide a Regex Patterns domains - @Value("${nexus.backend.security.cors.allowedOriginPatterns:#{null}") - private String allowedOriginPatterns; - // Provide a List of domains - @Value("#{T(java.util.Arrays).asList('${nexus.backend.security.cors.allowedOrigins:*}')}") - private List allowedOrigins; - // Headers: Authorization,Cache-Control,Content-Type,X-Requested-With,Accept - @Value("#{T(java.util.Arrays).asList('${nexus.backend.security.cors.allowedHeaders:Authorization,Cache-Control,Content-Type,X-Requested-With,Accept}')}") - private List allowedHeaders; - // At true Origin cannot be a wildcard '*' a list of domains need to be provided. - @Value("${nexus.backend.security.cors.credentials:false}") - private boolean credentials; - // Headers: Authorization - @Value("#{T(java.util.Arrays).asList('${nexus.backend.security.cors.exposedHeaders:}')}") - private List exposedHeaders; - // Max age in second - @Value("${nexus.backend.security.cors.maxAge:3600}") - private Long maxAgeCors; - - /* - * CORS Security configuration, allow Control Request by Headers
- * Access-Control-Allow-Origin: *
- * Access-Control-Allow-Methods: GET,POST,PUT,OPTION,HEAD,DELETE,PATCH
- * Access-Control-Max-Age: 3600
- * Test OPTIONS request on the local domain: - *
- * curl -v -H "Access-Control-Request-Method: GET" -H "Origin: http://localhost:8082" -X OPTIONS http://localhost:8082/nexus-backend/api/get - * or -H "Origin: http://localhost:4200" - */ - @Bean - public CorsConfigurationSource corsConfigurationSource() { - final CorsConfiguration configuration = new CorsConfiguration(); - if (allowedOriginPatterns != null && !allowedOriginPatterns.isEmpty()) configuration.addAllowedOriginPattern(allowedOriginPatterns); - if (allowedOrigins != null && !allowedOrigins.isEmpty()) configuration.setAllowedOrigins(allowedOrigins); - if (allowedCorsHttpMethods != null && !allowedCorsHttpMethods.isEmpty()) configuration.setAllowedMethods(allowedCorsHttpMethods); - // setAllowCredentials(true) is important, otherwise: - // The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. - configuration.setAllowCredentials(credentials); - // setAllowedHeaders is important! Without it, OPTIONS preflight request will fail with 403 Invalid CORS request - if (allowedHeaders != null && !allowedHeaders.isEmpty()) configuration.setAllowedHeaders(allowedHeaders); // "Authorization", "Cache-Control", "Content-Type" - if (exposedHeaders != null && !exposedHeaders.isEmpty()) configuration.setExposedHeaders(exposedHeaders); // "Authorization" - configuration.setMaxAge(maxAgeCors); - // register source Cors pattern - final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/api/**", configuration); - return source; - } - - - @Value("${nexus.backend.security.allowSemicolon:false}") - public boolean isAllowSemicolon; - @Value("${nexus.backend.security.allowUrlEncodedSlash:false}") - public boolean isAllowUrlEncodedSlash; - @Value("${nexus.backend.security.allowUrlEncodedDoubleSlash:false}") - public boolean isAllowUrlEncodedDoubleSlash; - @Value("${nexus.backend.security.allowUrlEncodedPeriod:false}") - public boolean isAllowUrlEncodedPeriod; - @Value("${nexus.backend.security.allowBackSlash:false}") - public boolean isAllowBackSlash; - @Value("${nexus.backend.security.allowNull:false}") - public boolean isAllowNull; - @Value("${nexus.backend.security.allowUrlEncodedPercent:false}") - public boolean isAllowUrlEncodedPercent; - @Value("${nexus.backend.security.allowUrlEncodedCarriageReturn:false}") - public boolean isAllowUrlEncodedCarriageReturn; - @Value("${nexus.backend.security.allowUrlEncodedLineFeed:false}") - public boolean isAllowUrlEncodedLineFeed; - @Value("${nexus.backend.security.allowUrlEncodedParagraphSeparator:false}") - public boolean isAllowUrlEncodedParagraphSeparator; - @Value("${nexus.backend.security.allowUrlEncodedLineSeparator:false}") - public boolean isAllowUrlEncodedLineSeparator; - - /** - * SpEL reads allow method delimited with a comma and splits into a List of Strings - */ - @Value("#{'${nexus.backend.security.allowedHttpMethods:GET,POST,PUT,OPTIONS,HEAD,DELETE,PATCH}'.split(',')}") - private List allowedHttpMethods; - - @Value("${nexus.backend.security.predicate.parameterNamesLength:255}") - private int parameterNamesLength = 255; - @Value("${nexus.backend.security.predicate.parameterValuesLength:1000000}") - private int parameterValuesLength = 1000000; - @Value("${nexus.backend.security.predicate.headerNamesLength:255}") - private int headerNamesLength = 255; - @Value("${nexus.backend.security.predicate.headerNamesValuesLength:25000}") - private int headerNamesValuesLength = 25000; - @Value("${nexus.backend.security.predicate.hostNamesLength:255}") - private int hostNamesLength = 255; - @Value("${nexus.backend.security.predicate.hostName.pattern:}") // "^(www\\.)?nexus\\.jservlet\\.com(:[0-9]+)?$" - private String hostNamesPattern; - @Value("${nexus.backend.security.predicate.userAgent.blocked:false}") - private boolean userAgentBlocked; - @Value("${nexus.backend.security.predicate.aiUserAgent.blocked:true}") - private boolean aiUserAgentBlocked; - - @Bean - public WAFPredicate wafPredicate() { - WAFPredicate wafPredicate = new WAFPredicate(); - wafPredicate.setParameterNamesLength(parameterNamesLength); - wafPredicate.setParameterValuesLength(parameterValuesLength); - wafPredicate.setHeaderNamesLength(headerNamesLength); - wafPredicate.setHeaderValuesLength(headerNamesValuesLength); - wafPredicate.setHostNamesLength(hostNamesLength); - - if (hostNamesPattern.isEmpty()) { - wafPredicate.setAllowedHostnames(Pattern.compile(hostNamesPattern,Pattern.CASE_INSENSITIVE | Pattern.DOTALL)); - } - - // - wafPredicate.setBlockDisallowedUserAgents(userAgentBlocked); - wafPredicate.setBlockDisallowedAIUserAgents(aiUserAgentBlocked); - - return wafPredicate; - } - - /** - * Web HttpFirewall, allow semicolon by example - * @param waf WAFPredicate - * @return HttpFirewall - */ - @Bean - public HttpFirewall webHttpFirewall(WAFPredicate waf) { - WebHttpFirewall firewall = new WebHttpFirewall(); - - firewall.setAllowedHttpMethods(allowedHttpMethods); - - firewall.setAllowSemicolon(isAllowSemicolon); - firewall.setAllowUrlEncodedSlash(isAllowUrlEncodedSlash); - firewall.setAllowUrlEncodedDoubleSlash(isAllowUrlEncodedDoubleSlash); - firewall.setAllowUrlEncodedPeriod(isAllowUrlEncodedPeriod); - firewall.setAllowBackSlash(isAllowBackSlash); - firewall.setAllowNull(isAllowNull); - firewall.setAllowUrlEncodedPercent(isAllowUrlEncodedPercent); - firewall.setAllowUrlEncodedCarriageReturn(isAllowUrlEncodedCarriageReturn); - firewall.setAllowUrlEncodedLineFeed(isAllowUrlEncodedLineFeed); - firewall.setAllowUrlEncodedParagraphSeparator(isAllowUrlEncodedParagraphSeparator); - firewall.setAllowUrlEncodedLineSeparator(isAllowUrlEncodedLineSeparator); - - // Predicate Parameter Name/Value - firewall.setAllowedParameterNames(waf.getWAFParameterNames()); - firewall.setAllowedParameterValues(waf.getWAFParameterValues()); - - // Predicate Header Name/Value - firewall.setAllowedHeaderNames(waf.getWAFHeaderNames()); - firewall.setAllowedHeaderValues(waf.getWAFHeaderValues()); - - // Predicate Hostnames - firewall.setAllowedHostnames(waf.getWAFHostnames()); - - return firewall; - } - - - /** - * A simple implementation of {@link RequestRejectedHandler} that sends an error with configurable status code - * @return RequestRejectedHandler - */ - @Bean - public RequestRejectedHandler requestRejectedHandler() { - return new HttpStatusRequestRejectedHandler() { - @Override - public void handle(HttpServletRequest request, HttpServletResponse response, RequestRejectedException requestRejectedException) throws IOException { - String message = (String) request.getAttribute("javax.servlet.error.message"); - String uri = (String) request.getAttribute("javax.servlet.error.request_uri"); - // WARN path /error can be reject cause can also contained the reject parameter.. - if (message != null && uri != null) { - logger.error("Rejecting request due to: {} uri: {}", requestRejectedException.getMessage(), uri); - } - response.sendError(HttpServletResponse.SC_BAD_REQUEST, message); // msg never displayed !? - } - }; - } - -} diff --git a/src/main/java/com/jservlet/nexus/config/web/tomcat/TomcatCustomContainer.java b/src/main/java/com/jservlet/nexus/config/web/tomcat/TomcatCustomContainer.java deleted file mode 100644 index e215f48..0000000 --- a/src/main/java/com/jservlet/nexus/config/web/tomcat/TomcatCustomContainer.java +++ /dev/null @@ -1,301 +0,0 @@ -/* - * Copyright (C) 2001-2025 JServlet.com Franck Andriano. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - */ - -package com.jservlet.nexus.config.web.tomcat; - -import org.apache.catalina.authenticator.BasicAuthenticator; -import org.apache.catalina.realm.MemoryRealm; -import org.apache.catalina.valves.ErrorReportValve; -import org.apache.catalina.valves.ExtendedAccessLogValve; -import org.apache.tomcat.util.descriptor.web.LoginConfig; -import org.apache.tomcat.util.descriptor.web.SecurityCollection; -import org.apache.tomcat.util.descriptor.web.SecurityConstraint; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; -import org.springframework.boot.web.server.WebServerFactoryCustomizer; -import org.springframework.context.annotation.Profile; -import org.springframework.core.io.ClassPathResource; -import org.springframework.stereotype.Component; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.StandardCopyOption; - -/** - * Custom Container with an Embedded Tomcat:
- * - Customizes Connector Http
- * - Extended Access Log Valve
- * - Error ReportValve
- * - Configuration ACL / Security constraint
- *
- *
- * server.xml - *
- * <Connector port="8080" protocol="HTTP/1.1"
- *             executor="tomcatThreadPool"
- *             redirectPort="8443"
- *             enableLookups="false"
- *             acceptCount="100"
- *             connectionTimeout="20000"
- *             maxPostSize="10485760"
- *             disableUploadTimeout="true"
- *             compression="on"
- *             compressableMimeType="text/html,text/xml,text/plain,text/javascript,text/css,application/json"
- *             URIEncoding="UTF-8"
- *             maxHttpHeaderSize="32768"
- * 			   rejectIllegalHeader="true"
- *             server="git di nexus a"
- *     />
- * 
- *
- *      <Valve className="org.apache.catalina.valves.ErrorReportValve"
- *            showReport="false"
- *            showServerInfo="false"/>
- * 
- *
- *      <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
- *          prefix="localhost_access_log" suffix=".txt"
- *          pattern="%h %l %u %t "%r" %s %b" />
- * 
- *
- *
- * tomcat-users.xml - *
- *      <role rolename="manager-gui"/>
- *      <role rolename="admin-gui"/>
- *      <user username="admin" password="admin" roles="manager-gui,admin-gui"/>
- * 
- */ -@Profile("withTomcat") -@Component -public class TomcatCustomContainer implements WebServerFactoryCustomizer { - - private final static Logger logger = LoggerFactory.getLogger(TomcatCustomContainer.class); - - @Value("${nexus.backend.tomcat.accesslog.valve.enable:false}") - private boolean accessLogEnabled; - - // Default Accesslog properties - @Value("${nexus.backend.tomcat.accesslog.directory:/tmp/logs/tomcat-nexus}") - private String directory; - @Value("${nexus.backend.tomcat.accesslog.suffix:.log}") - private String suffix; - @Value("${nexus.backend.tomcat.accesslog.pattern:date time x-threadname c-ip cs-method cs-uri sc-status x-H(locale) x-H(remoteUser) bytes x-H(contentLength) time-taken x-H(authType) cs(Authorization) cs(User-Agent)}") - private String pattern; - @Value("${nexus.backend.tomcat.accesslog.encoding:UTF-8}") - private String encoding; - @Value("${nexus.backend.tomcat.accesslog.checkExists:true}") - private boolean checkExists; - @Value("${nexus.backend.tomcat.accesslog.asyncSupported:true}") - private boolean asyncSupported; - @Value("${nexus.backend.tomcat.accesslog.renameOnRotate:true}") - private boolean renameOnRotate; - @Value("${nexus.backend.tomcat.accesslog.throwOnFailure:true}") - private boolean throwOnFailure; - @Value("${nexus.backend.tomcat.accesslog.maxDays:-1}") - private int maxDays; - - // Default errorReport properties - @Value("${nexus.backend.tomcat.error-report.showReport:false}") - private boolean showReport; - @Value("${nexus.backend.tomcat.error-report.showServerInfo:false}") - private boolean showServerInfo; - - // Default Security constraint properties - @Value("${nexus.backend.tomcat.security.patterns:/actuator/*,/health/*}") - private String[] securityPatterns; - @Value("${nexus.backend.tomcat.security.admin.acl.enable:false}") - private boolean adminAclEnabled; - @Value("${nexus.backend.tomcat.security.users.file:}") - private String customUsersFilePath; - @Value("${nexus.backend.tomcat.security.role:admin-gui}") - private String securityRole; - - // Default Connector properties (HTTP) - @Value("${nexus.backend.tomcat.connector.accept-count:100}") - private int acceptCount; - @Value("${nexus.backend.tomcat.connector.connection-timeout:20000}") - private int connectionTimeout; - @Value("${nexus.backend.tomcat.connector.max-post-size:10485760}") - private int maxPostSize; - @Value("${nexus.backend.tomcat.connector.disable-upload-timeout:true}") - private boolean disableUploadTimeout; - @Value("${nexus.backend.tomcat.connector.compression:on}") - private String compression; - @Value("${nexus.backend.tomcat.connector.compressable-mime-type:text/html,text/xml,text/plain,text/javascript,text/css,application/json}") - private String compressableMimeType; - @Value("${nexus.backend.tomcat.connector.uri-encoding:UTF-8}") - private String uriEncoding; - @Value("${nexus.backend.tomcat.connector.max-http-header-size:10240}") - private int maxHttpHeaderSize; - @Value("${nexus.backend.tomcat.connector.reject-illegal-header:true}") - private boolean rejectIllegalHeader; - @Value("${nexus.backend.tomcat.connector.server:git di nexus a}") - private String serverHeader; - - - @Override - public void customize(TomcatServletWebServerFactory factory) { - - // Configuration Http connector by default - factory.addConnectorCustomizers(connector -> { - connector.setProperty("acceptCount", String.valueOf(acceptCount)); - connector.setProperty("connectionTimeout", String.valueOf(connectionTimeout)); - connector.setProperty("maxPostSize", String.valueOf(maxPostSize)); - connector.setProperty("disableUploadTimeout", String.valueOf(disableUploadTimeout)); - - // Compression, encoding - connector.setProperty("compression", compression); - connector.setProperty("compressableMimeType", compressableMimeType); - connector.setURIEncoding(uriEncoding); - - // Security and Headers - connector.setProperty("maxHttpHeaderSize", String.valueOf(maxHttpHeaderSize)); - connector.setProperty("rejectIllegalHeader", String.valueOf(rejectIllegalHeader)); - connector.setProperty("server", serverHeader); - - logger.info("Configured Default HTTP Connector: rejectIllegalHeader={}, serverHeader={}, compression={}", - rejectIllegalHeader, serverHeader, compression); - }); - - // Access log - if (accessLogEnabled) { - logger.info("Starting Tomcat Catalina Extended AccessLog Valve"); - ExtendedAccessLogValve accessLogValve = getExtendedAccessLogValve(); - factory.addContextValves(accessLogValve); - } else { - logger.debug("Tomcat AccessLog Valve is disabled"); - } - - // Error report - ErrorReportValve errorReportValve = new ErrorReportValve(); - errorReportValve.setShowReport(showReport); - errorReportValve.setShowServerInfo(showServerInfo); - factory.addEngineValves(errorReportValve); // GLOBAL! - logger.info("Starting Tomcat Catalina ErrorReport Valve: showReport {} - ShowServerInfo {}", showReport, showServerInfo); - - // Configuration ACL / Security constraint - if (adminAclEnabled) { - logger.info("Configuring Tomcat Security Constraint Pattern path"); - factory.addContextCustomizers(context -> { - MemoryRealm realm = new MemoryRealm(); - File userConfigFile = null; - - try { - // specific path - if (customUsersFilePath != null && !customUsersFilePath.isEmpty()) { - userConfigFile = new File(customUsersFilePath); - } - - // auto-detect catalina.base - if ((userConfigFile == null || !userConfigFile.exists()) && System.getProperty("catalina.base") != null) { - File candidate = new File(System.getProperty("catalina.base"), "conf/tomcat-users.xml"); - if (candidate.exists()) { - userConfigFile = candidate; - } - } - // auto-detect catalina.home - if ((userConfigFile == null || !userConfigFile.exists()) && System.getProperty("catalina.home") != null) { - File candidate = new File(System.getProperty("catalina.home"), "conf/tomcat-users.xml"); - if (candidate.exists()) { - userConfigFile = candidate; - } - } - - // Application or Fallback Classpath - if (userConfigFile != null && userConfigFile.exists()) { - logger.info("Loading tomcat-users.xml from path {}", userConfigFile.getAbsolutePath()); - realm.setPathname(userConfigFile.getAbsolutePath()); - } else { - // File embedded src/main/resources - logger.info("Loading embedded tomcat-users.xml from classpath"); - ClassPathResource resource = new ClassPathResource("tomcat-users.xml"); - - if (resource.exists()) { - userConfigFile = File.createTempFile("tomcat-users-embedded", ".xml"); - userConfigFile.deleteOnExit(); - - try (InputStream inputStream = resource.getInputStream()) { - Files.copy(inputStream, userConfigFile.toPath(), StandardCopyOption.REPLACE_EXISTING); - } - realm.setPathname(userConfigFile.getAbsolutePath()); // IMPORTANT: Ne pas oublier cette ligne - logger.info("Embedded tomcat-users.xml extracted to: {}", userConfigFile.getAbsolutePath()); - } else { - logger.error("File tomcat-users.xml not found in classpath!"); - } - } - - } catch (IOException e) { - logger.error("Failed to load tomcat-users.xml configuration", e); - } - - context.setRealm(realm); - - SecurityConstraint securityConstraint = new SecurityConstraint(); - securityConstraint.setUserConstraint("NONE"); - securityConstraint.setDisplayName("Admin Access Constraint"); - securityConstraint.setAuthConstraint(true); - - SecurityCollection collection = new SecurityCollection(); - - if (securityPatterns != null) { - for (String p : securityPatterns) { - if (!p.trim().isEmpty()) { - collection.addPattern(p.trim()); - logger.info("- Constraint Pattern {}", p.trim()); - } - } - } - - securityConstraint.addCollection(collection); - securityConstraint.addAuthRole(securityRole); // Utilisation de la variable - - context.addConstraint(securityConstraint); - context.addSecurityRole(securityRole); - - // Authentication config method (Browser Pop-up) - LoginConfig loginConfig = new LoginConfig(); - loginConfig.setAuthMethod("BASIC"); // ou "DIGEST", "FORM" - loginConfig.setRealmName("Nexus Backend Realm"); - context.setLoginConfig(loginConfig); - - // Inject now the basic Authentication - BasicAuthenticator basicAuthenticator = new BasicAuthenticator(); - context.getPipeline().addValve(basicAuthenticator); - }); - } - } - - private ExtendedAccessLogValve getExtendedAccessLogValve() { - ExtendedAccessLogValve accessLogValve = new ExtendedAccessLogValve(); - accessLogValve.setDirectory(directory); - accessLogValve.setSuffix(suffix); - accessLogValve.setCheckExists(checkExists); - accessLogValve.setPattern(pattern); - accessLogValve.setEncoding(encoding); - accessLogValve.setAsyncSupported(asyncSupported); - accessLogValve.setRenameOnRotate(renameOnRotate); - accessLogValve.setThrowOnFailure(throwOnFailure); - accessLogValve.setMaxDays(maxDays); - return accessLogValve; - } -} diff --git a/src/main/java/com/jservlet/nexus/config/web/tomcat/ssl/TomcatConnectorConfig.java b/src/main/java/com/jservlet/nexus/config/web/tomcat/ssl/TomcatConnectorConfig.java deleted file mode 100644 index d49fb71..0000000 --- a/src/main/java/com/jservlet/nexus/config/web/tomcat/ssl/TomcatConnectorConfig.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright (C) 2001-2025 JServlet.com Franck Andriano. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - */ - -package com.jservlet.nexus.config.web.tomcat.ssl; - -import org.apache.catalina.connector.Connector; -import org.apache.coyote.ajp.AbstractAjpProtocol; -import org.apache.tomcat.util.net.SSLHostConfig; -import org.apache.tomcat.util.net.SSLHostConfigCertificate; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; -import org.springframework.boot.web.servlet.server.ServletWebServerFactory; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Profile; - -/** - * Config Embedded Tomcat Connector TLS/SSL on the default port 8443 (HTTP/1.1) - *
- * And Config Embedded Tomcat Connector AJP on the port 8009 with redirection on 8443 - *
- * Only a Spring profile 'withTomcat' (with a Tomcat Embedded by SpringBoot!) - *
- *
- *      <Connector SSLEnabled="true" acceptCount="100" clientAuth="false"
- *                disableUploadTimeout="true" enableLookups="false" maxThreads="25"
- *                port="8443" keystoreFile="/home/root/.keystore"
- *                keystorePass="changeit"
- *                protocol="org.apache.coyote.http11.Http11NioProtocol" scheme="https"
- *                secure="true" sslProtocol="TLS" />
- * 
- *
- *      <Connector port="8009" protocol="AJP/1.3" redirectPort="8443"/>
- * 
- */ -@Profile("withTomcat") -@ConditionalOnProperty(value = "nexus.backend.tomcat.connector.https.enable") -@Configuration -public class TomcatConnectorConfig { - - private final static Logger logger = LoggerFactory.getLogger(TomcatConnectorConfig.class); - - @Value("${nexus.backend.tomcat.ssl.keystore-path:/home/root/.keystore}") - private String pathJKS; - @Value("${nexus.backend.tomcat.ssl.keystore-password:changeit}") - private String keyStorePassword; - @Value("${nexus.backend.tomcat.ssl.certificate.alias:key_server}") - private String certificateAlias; - @Value("${nexus.backend.tomcat.ssl.lookups.enable:false}") - private boolean enableLookups; - - @Value("${nexus.backend.tomcat.ssl.https.port:8443}") - private int HTTPS_PORT; - @Value("${nexus.backend.tomcat.ssl.ajp.connector.port:8009}") - private int AJP_PORT; - - @Value("${nexus.backend.tomcat.ssl.ajp.connector.enable:false}") - private boolean enableAjp; - @Value("${nexus.backend.tomcat.ssl.ajp.connector.secretRequired:false}") - private boolean secretRequiredAjp; - @Value("${nexus.backend.tomcat.ssl.ajp.connector.protocol:AJP/1.3}") - private String AJP_PROTOCOL; - - @Bean - public ServletWebServerFactory servletContainer() { - logger.info("Starting TLS/SSL Tomcat Factory"); - TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory(); - if (enableAjp) tomcat.addAdditionalTomcatConnectors(sslConnector(), ajpConnector()); - else tomcat.addAdditionalTomcatConnectors(sslConnector()); - return tomcat; - } - - private Connector ajpConnector() { - // Define an AJP 1.3 Connector on port 8009 - Connector connector = new Connector(AJP_PROTOCOL); - connector.setPort(AJP_PORT); - connector.setRedirectPort(HTTPS_PORT); - // Disabled required secret - ((AbstractAjpProtocol) connector.getProtocolHandler()).setSecretRequired(secretRequiredAjp); - return connector; - } - - private Connector sslConnector(){ - // Connector Apache Coyote Http11NioProtocol - Connector httpsConnector = new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL); - httpsConnector.setPort(HTTPS_PORT); - httpsConnector.setSecure(true); - httpsConnector.setScheme("https"); - httpsConnector.setProperty("SSLEnabled", "true"); - httpsConnector.setEnableLookups(enableLookups); - logger.info("Protocol: {}", httpsConnector.getProtocol()); - - // Now create a SSLHostConfig with the Root RSA Certificate - SSLHostConfig sslConfig = new SSLHostConfig(); - SSLHostConfigCertificate certConfig = new SSLHostConfigCertificate(sslConfig, SSLHostConfigCertificate.Type.RSA); - certConfig.setCertificateKeystoreFile(pathJKS); - certConfig.setCertificateKeystorePassword(keyStorePassword); - certConfig.setCertificateKeyAlias(certificateAlias); - sslConfig.addCertificate(certConfig); - - // Add the SSLHostConfig in the Tomcat Connector - httpsConnector.addSslHostConfig(sslConfig); - - return httpsConnector; - } -} diff --git a/src/main/java/com/jservlet/nexus/controller/DefaultErrorController.java b/src/main/java/com/jservlet/nexus/controller/DefaultErrorController.java deleted file mode 100644 index 9cf843a..0000000 --- a/src/main/java/com/jservlet/nexus/controller/DefaultErrorController.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright (C) 2001-2025 JServlet.com Franck Andriano. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - */ - -package com.jservlet.nexus.controller; - -import com.jservlet.nexus.shared.web.controller.ApiBase; -import io.swagger.v3.oas.annotations.Hidden; -import org.apache.commons.lang3.StringUtils; -import org.springframework.boot.web.error.ErrorAttributeOptions; -import org.springframework.boot.web.servlet.error.ErrorAttributes; -import org.springframework.boot.web.servlet.error.ErrorController; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Controller; -import org.springframework.util.Assert; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.context.request.ServletWebRequest; -import org.springframework.web.context.request.WebRequest; - -import javax.servlet.http.HttpServletRequest; -import java.util.Map; - -/* - * The Default Error Controller - */ -@Hidden -@Controller -public class DefaultErrorController extends ApiBase implements ErrorController { // Thank Phillip! - - private final ErrorAttributes errorAttributes; - - private static final String SOURCE = "ERROR-REST-NEXUS-BACKEND"; - - public DefaultErrorController(ErrorAttributes errorAttributes) { - super(SOURCE); - Assert.notNull(errorAttributes, "ErrorAttributes must not be null"); - this.errorAttributes = errorAttributes; - } - - @ResponseBody - @RequestMapping(value = {"/error"}, method = { - RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, - RequestMethod.DELETE, RequestMethod.PATCH }, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity error(HttpServletRequest request) { - Map body = getErrorAttributes(request, getTraceParameter(request)); - final HttpStatus status = getStatus(request); - return super.getResponseEntity(String.valueOf(status.value()), "ERROR", - String.valueOf(body.get("message")), status); - } - - private boolean getTraceParameter(HttpServletRequest request) { - String parameter = request.getParameter("trace"); - return StringUtils.isNotBlank(parameter) && !"false".equalsIgnoreCase(parameter); - } - - private Map getErrorAttributes(HttpServletRequest request, boolean includeStackTrace) { - WebRequest webRequest = new ServletWebRequest(request); - ErrorAttributeOptions options = ErrorAttributeOptions.defaults(); - if (includeStackTrace) { - options.including(ErrorAttributeOptions.Include.STACK_TRACE); - } - return this.errorAttributes.getErrorAttributes(webRequest, options); - } - - private HttpStatus getStatus(HttpServletRequest request) { - Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code"); - if (statusCode != null) { - try { - return HttpStatus.valueOf(statusCode); - } catch (Exception ignore) { - } - } - return HttpStatus.INTERNAL_SERVER_ERROR; - } - -} diff --git a/src/main/java/com/jservlet/nexus/controller/GlobalDefaultExceptionHandler.java b/src/main/java/com/jservlet/nexus/controller/GlobalDefaultExceptionHandler.java deleted file mode 100644 index 55a8908..0000000 --- a/src/main/java/com/jservlet/nexus/controller/GlobalDefaultExceptionHandler.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright (C) 2001-2025 JServlet.com Franck Andriano. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - */ - -package com.jservlet.nexus.controller; - -import com.jservlet.nexus.shared.web.controller.ApiBase; -import org.apache.catalina.connector.ClientAbortException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.http.converter.HttpMessageNotReadableException; -import org.springframework.security.web.firewall.RequestRejectedException; -import org.springframework.web.HttpMediaTypeNotAcceptableException; -import org.springframework.web.HttpRequestMethodNotSupportedException; -import org.springframework.web.bind.annotation.ControllerAdvice; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.client.RestClientException; - -import javax.servlet.http.HttpServletRequest; - -import java.util.Objects; - -import static org.springframework.http.HttpStatus.*; - -/** - * Global DefaultExceptionHandler - */ -@ControllerAdvice -public class GlobalDefaultExceptionHandler extends ApiBase { - - private static final Logger logger = LoggerFactory.getLogger(GlobalDefaultExceptionHandler.class); - - private static final String GLOBAL_SOURCE = "GLOBAL-ERROR-NEXUS-BACKEND"; - - public GlobalDefaultExceptionHandler() { - super(GLOBAL_SOURCE); - } - - @ExceptionHandler(value = { RestClientException.class }) - public ResponseEntity handleResourceAccessException(HttpServletRequest request, RestClientException e) { - logger.error("Intercepted RestClientException: {} RemoteAddr: {} RequestURL: {} {} UserAgent: {}", - e.getMessage(), request.getRemoteAddr(), request.getMethod(), - request.getServletPath(), request.getHeader("User-Agent")); - // ByeArray Resource request could not be completed due to a conflict with the current state of the target resource! - if (Objects.requireNonNull(e.getMessage()).startsWith("Error while extracting response for type [class java.lang.Object] and content type")) { - String msg = "No ResourceMatchers configured. Open the Method '" + request.getMethod() + "' and the Uri '" + request.getServletPath() + "' as ByteArray Resource!"; - return super.getResponseEntity("409", "ERROR", msg, CONFLICT); - } - String errorMsg = e.getMessage(); - return super.getResponseEntity("503", "ERROR", "Remote Service Unavailable: " + - errorMsg.substring(0, Math.min(errorMsg.length(), 1000)), SERVICE_UNAVAILABLE); - } - - @ExceptionHandler(value = { HttpMessageNotReadableException.class }) - public ResponseEntity handleNotReadableException(HttpServletRequest request, HttpMessageNotReadableException e) { - logger.error("Intercepted HttpMessageNotReadableException: {} RemoteAddr: {} RequestURL: {} {} UserAgent: {}", - e.getMessage(), request.getRemoteAddr(), request.getMethod(), request.getServletPath(), request.getHeader("User-Agent")); - return super.getResponseEntity(METHOD_NOT_ALLOWED);// 405 or 406 NOT_ACCEPTABLE !? - } - - @ExceptionHandler(value = { RequestRejectedException.class }) - public ResponseEntity handleRejectedException(HttpServletRequest request, RequestRejectedException e) { - // Log security, the WAFFilter return a request not readable anymore - logger.error("Intercepted RequestRejectedException: {} RemoteAddr: {} RequestURL: {} {} UserAgent: {}", - e.getMessage(), request.getRemoteAddr(), request.getMethod(), request.getServletPath(), request.getHeader("User-Agent")); - return super.getResponseEntity("400", "ERROR", "Request rejected!", BAD_REQUEST); - } - - - @ExceptionHandler(value = { HttpRequestMethodNotSupportedException.class }) - public ResponseEntity handleMethodNotSupportedException(HttpServletRequest request, HttpRequestMethodNotSupportedException e) { - logger.error("Intercepted HttpRequestMethodNotSupportedException: {} RemoteAddr: {} RequestURL: {} {} UserAgent: {}", - e.getMessage(), request.getRemoteAddr(), request.getMethod(), request.getServletPath(), request.getHeader("User-Agent")); - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_JSON); // Mandatory forced ContentType can be null - return super.getResponseEntity("404", "ERROR", e, headers, NOT_FOUND); - } - - /* - * For no acceptable representation for a Resource, but will never more happens with the ContentNegotiation - */ - @ExceptionHandler(value = { HttpMediaTypeNotAcceptableException.class }) - public ResponseEntity handleMediaTypeNotAcceptableException(HttpServletRequest request, HttpMediaTypeNotAcceptableException e) { - logger.error("Intercepted HttpMediaTypeNotAcceptableException: {} RemoteAddr: {} RequestURL: {} {} UserAgent: {}", - e.getMessage(), request.getRemoteAddr(), request.getMethod(), request.getServletPath(), request.getHeader("User-Agent")); - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_JSON); // Mandatory forced ContentType can be null - return super.getResponseEntity("406", "ERROR", e, headers, NOT_ACCEPTABLE); - } - - /* - * Any other's error - */ - @ExceptionHandler(value = { Exception.class }) - public ResponseEntity handleGlobalException(HttpServletRequest request, Exception e) { - // Special case ClientAbort - if (e instanceof ClientAbortException || (e.getCause() != null && e.getCause() instanceof ClientAbortException)) { - logger.warn("Intercepted ClientAbortException: {} RemoteAddr: {} RequestURL: {} {} UserAgent: {}", - e.getMessage(), request.getRemoteAddr(), request.getMethod(), request.getServletPath(), request.getHeader("User-Agent")); - return null; // Spring will do nothing we manage the response! - } - logger.error("Intercepted GlobalException: {} RemoteAddr: {} RequestURL: {} {} UserAgent: {}", - e.getMessage(), request.getRemoteAddr(), request.getMethod(), request.getServletPath(), request.getHeader("User-Agent")); - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_JSON); // Mandatory forced ContentType can be null or application/octet-stream - return super.getResponseEntity("500", "ERROR", e, headers, INTERNAL_SERVER_ERROR); - } -} diff --git a/src/main/java/com/jservlet/nexus/controller/IndexController.java b/src/main/java/com/jservlet/nexus/controller/IndexController.java deleted file mode 100644 index 7ff6bdd..0000000 --- a/src/main/java/com/jservlet/nexus/controller/IndexController.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (C) 2001-2024 JServlet.com Franck Andriano. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - */ - -package com.jservlet.nexus.controller; - -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.GetMapping; - -/* - * The Default Index Controller - */ -@Controller -public class IndexController { - - @GetMapping(path = {"", "/"}) - public String index() { return "index"; } - -} diff --git a/src/main/java/com/jservlet/nexus/controller/MockController.java b/src/main/java/com/jservlet/nexus/controller/MockController.java deleted file mode 100644 index 4b4a407..0000000 --- a/src/main/java/com/jservlet/nexus/controller/MockController.java +++ /dev/null @@ -1,678 +0,0 @@ -/* - * Copyright (C) 2001-2025 JServlet.com Franck Andriano. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - */ - -package com.jservlet.nexus.controller; - -import com.fasterxml.jackson.annotation.JsonFormat; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.jservlet.nexus.shared.web.controller.ApiBase; -import io.swagger.v3.oas.annotations.Hidden; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.ArraySchema; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import org.apache.commons.io.FileUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; -import org.springframework.core.env.Environment; -import org.springframework.core.io.ClassPathResource; -import org.springframework.core.io.InputStreamResource; -import org.springframework.http.*; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.client.HttpClientErrorException; -import org.springframework.web.client.RestTemplate; -import org.springframework.web.multipart.MultipartFile; -import org.springframework.web.util.UriComponentsBuilder; - -import javax.servlet.http.HttpServletRequest; -import java.io.File; -import java.io.IOException; -import java.io.Serializable; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.*; -import java.util.stream.Collectors; - -import static com.jservlet.nexus.config.web.WebConstants.*; -import static java.nio.charset.StandardCharsets.UTF_8; - -/* - * The Mock Controller VM Options: -Denvironment=development - */ -@Controller -@ConditionalOnExpression("'${environment}' == 'development'") -@Tag(name = "Mock", description = "Mock Application") -@RequestMapping("/mock") -public class MockController extends ApiBase { - - private static final Logger logger = LoggerFactory.getLogger(MockController.class); - - private static final String ENV_VAR = "environment"; - - private final Environment env; - - private static final String SOURCE = "MOCK-REST-NEXUS-BACKEND"; - - private static final String fileName = "logo-marianne.svg"; - private static final String fileTest = System.getProperty("java.io.tmpdir") + fileName; - - public MockController(Environment env) { - super(SOURCE); - this.env = env; - } - - @GetMapping(path = {"", "/"}) - public String mock() { - return "development".equals(env.getProperty(ENV_VAR)) ? "mock" : "error/error403"; - } - - @Operation(summary = "Get ByteArray data", description = "Get ByteArray data") - @ApiResponses(value = { - @ApiResponse(responseCode = $200, description = REQ_SUCCESSFULLY, content = {@Content(schema = @Schema(implementation = byte[].class))}), - }) - @GetMapping(path = "/v1/dataBytes")//, produces = MediaType.APPLICATION_OCTET_STREAM_VALUE - public ResponseEntity getBytes() { - return new ResponseEntity<>("GET_BYTES".getBytes(UTF_8), HttpStatus.OK); - } - - @Operation(summary = "Get a data", description = "Get a data") - @ApiResponses(value = { - @ApiResponse(responseCode = $200, description = REQ_SUCCESSFULLY, content = {@Content(schema = @Schema(implementation = Data.class))}), - }) - @GetMapping(path = "/v1/data", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity get() { - return new ResponseEntity<>(new Data("info1","info2", 10.00), HttpStatus.OK); - } - - @Operation(summary = "Post a data", description = "Post a data") - @ApiResponses(value = { - @ApiResponse(responseCode = $200, description = REQ_SUCCESSFULLY, content = {@Content(schema = @Schema(implementation = Data.class))}), - }) - @PostMapping(path = "/v1/data", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity post(@RequestBody Data data) { - return new ResponseEntity<>(true, HttpStatus.OK); - } - - @Operation(summary = "Put a data", description = "Put a data") - @ApiResponses(value = { - @ApiResponse(responseCode = $200, description = REQ_SUCCESSFULLY, content = {@Content(schema = @Schema(implementation = Data.class))}), - }) - @PutMapping(path = "/v1/data", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity put(@RequestBody Data data) { - return new ResponseEntity<>(true, HttpStatus.OK); - } - - @Operation(summary = "Get data Xss", description = "Get data Xss") - @ApiResponses(value = { - @ApiResponse(responseCode = $200, description = REQ_SUCCESSFULLY, content = {@Content(schema = @Schema(implementation = Data.class))}), - }) - @GetMapping(path = "/v1/dataXss", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getXss(@RequestParam String param1) { - return new ResponseEntity<>(new Data(param1,"info2",10.05, new Date()), HttpStatus.OK); - } - - @Operation(summary = "Post data Xss", description = "Post data Xss") - @ApiResponses(value = { - @ApiResponse(responseCode = $200, description = REQ_SUCCESSFULLY, content = {@Content(schema = @Schema(implementation = Data.class))}), - }) - @PostMapping(path = "/v1/dataPostXss", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity postXss(@RequestParam String param1, @RequestBody Data data) { - return new ResponseEntity<>(new Data(param1, data.data2, data.data3, data.data4), HttpStatus.OK); - - } - - @Operation(summary = "Get data List", description = "Get data List") - @ApiResponses(value = { - @ApiResponse(responseCode = $200, description = REQ_SUCCESSFULLY, content = {@Content(schema = @Schema(implementation = Data[].class))}), - }) - @GetMapping(path = "/v1/dataList", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getList() { - List dataList = new ArrayList<>(); - dataList.add(new Data("info1","info2",10.00)); - dataList.add(new Data("info4","info5",10.05)); - return new ResponseEntity<>(dataList, HttpStatus.OK); - } - - /* VULNERABILITY Personal John Doe */ - @Operation(summary = "Get Personal Joe ", description = "Get data Joe List") - @ApiResponses(value = { - @ApiResponse(responseCode = $200, description = REQ_SUCCESSFULLY, content = {@Content(schema = @Schema(implementation = DataJoe.class))}), - }) - @GetMapping(path = "/v1/dataJoe", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getListJoe() { - DataJoe dataJoe = new DataJoe(); - HashMap hashJoe = new HashMap<>(); - hashJoe.put("userid", 123); - hashJoe.put("name", "John Doe"); - hashJoe.put("email", "john.doe@example.com"); - hashJoe.put("phone", "+1-555-123-4567"); - hashJoe.put("address", "123 Fake Street"); - dataJoe.setDataJoe(hashJoe); - return new ResponseEntity<>(dataJoe, HttpStatus.OK); - } - @Operation(summary = "Get a data Joe2", description = "Get a data Joe2") - @ApiResponses(value = { - @ApiResponse(responseCode = $200, description = REQ_SUCCESSFULLY, content = {@Content(schema = @Schema(implementation = HashMap.class))}), - }) - @GetMapping(path = "/v1/dataJoe2", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getDataJoe2() { - HashMap hashJoe = new HashMap<>(); - hashJoe.put("user_id", 123); - hashJoe.put("name", "John Doe"); - hashJoe.put("age", 25); - hashJoe.put("akia_key", "AKIAQWERTYUIOPASDF"); - - return new ResponseEntity<>(hashJoe, HttpStatus.OK); - } - @Operation(summary = "Get a data Joe2", description = "Get a data Joe2") - @ApiResponses(value = { - @ApiResponse(responseCode = $200, description = REQ_SUCCESSFULLY, content = {@Content(schema = @Schema(implementation = HashMap.class))}), - }) - @GetMapping(path = "/v1/dataJoe3", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getDataJoe3() { - HashMap dataJoe = new HashMap<>(); // no user_id! - dataJoe.put("name", "Joe"); - dataJoe.put("age", 25); - dataJoe.put("AKIAQWERTYUIOPASDF", "AKIAQWERTYUIOPASDF"); - return new ResponseEntity<>(dataJoe, HttpStatus.OK); - } - @Operation(summary = "Get Financial Data Joe", description = "Returns mock financial data including IBAN and Credit Card numbers.") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Request successful", content = {@Content(schema = @Schema(implementation = HashMap.class))}), - }) - @GetMapping(path = "/v1/financialDataJoe", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getFinancialData() { - HashMap hashJoe = new HashMap<>(); - hashJoe.put("transaction_id", "TXN-12345"); - hashJoe.put("beneficiary_iban", "00:1A:2B:3C:4D:5E"); - hashJoe.put("payment_method", "credit_card"); - hashJoe.put("card_number", "4971 1234 5678 9010"); - return new ResponseEntity<>(hashJoe, HttpStatus.OK); - } - @Operation(summary = "Get Identity Data", description = "Returns mock identity data including Passport and SSN.") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Request successful", content = {@Content(schema = @Schema(implementation = HashMap.class))}), - }) - @GetMapping(path = "/v1/identityData", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getIdentityData() { - List dataList = new ArrayList<>(); - dataList.add(new Data("user_name", "Jane Doe", 0.0)); - dataList.add(new Data("passport_number", "AB123456", 0.0)); - dataList.add(new Data("ssn", "123-45-6789", 0.0)); - return new ResponseEntity<>(dataList, HttpStatus.OK); - } - @Operation(summary = "Get Technical Data", description = "Returns mock technical data with IP and MAC addresses.") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Request successful", content = {@Content(schema = @Schema(implementation = HashMap.class))}), - }) - @GetMapping(path = "/v1/techData", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getTechData() { - HashMap hashJoe = new HashMap<>(); - hashJoe.put("last_login_ip", "8.8.8.8"); - hashJoe.put("device_mac_address", "00:1A:2B:3C:4D:5E"); - return new ResponseEntity<>(hashJoe, HttpStatus.OK); - } - @Operation(summary = "Get Vehicle Data", description = "Returns mock vehicle data with French and US license plates.") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Request successful", content = {@Content(schema = @Schema(implementation = HashMap.class))}), - }) - @GetMapping(path = "/v1/vehicleData", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getVehicleData() { - HashMap hashJoe = new HashMap<>(); - hashJoe.put("vehicle_fr", "Peugeot 208"); - hashJoe.put("license_plate_fr", "AB-123-CD"); - hashJoe.put("vehicle_us", "ABC 1234"); - hashJoe.put("license_plate_us", "ABC 1234"); - return new ResponseEntity<>(hashJoe, HttpStatus.OK); - } - - private static final Map userDatabase = new HashMap<>(); - static { - userDatabase.put("122", new UserProfile("122", "Bob", "bob@example.com", "Secret de Bob")); - userDatabase.put("123", new UserProfile("123", "Alice", "alice@example.com", "Secret d'Alice")); - userDatabase.put("124", new UserProfile("124", "Joe", "joe@example.com", "Secret de Joe")); - userDatabase.put("125", new UserProfile("125", "Tina", "tina@example.com", "Secret de Joe")); - } - // BOLA - @Operation(summary = "Get User Profile by ID (BOLA Vulnerable)", description = "Get a user's profile by their ID.") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Request successful", content = {@Content(schema = @Schema(implementation = UserProfile.class))}), - @ApiResponse(responseCode = "404", description = "User not found") - }) - @GetMapping(path = "/v1/users/{userId}/profile", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getUserProfile(@PathVariable String userId) { - // No check to see if the authenticated user is the same as the requested userId! - UserProfile user = userDatabase.get(userId); - if (user != null) { - return new ResponseEntity<>(user, HttpStatus.OK); - } else { - return new ResponseEntity<>("User not found", HttpStatus.NOT_FOUND); - } - } - //BFLA - @Operation(summary = "Delete a user by ID (BFLA Vulnerable)", description = "Deletes a user from the system. This endpoint should be restricted to administrators.") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "User deleted successfully"), - @ApiResponse(responseCode = "404", description = "User not found") - }) - @PreAuthorize("hasRole('ADMIN')") - @DeleteMapping(path = "/v1/admin/users/{userId}/delete", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity deleteUser(@PathVariable String userId) { - // VULNERABILITY: Check to see if the user is admin - if (userDatabase.containsKey(userId)) { - userDatabase.remove(userId); - return new ResponseEntity<>("User " + userId + " deleted.", HttpStatus.OK); - } else { - return new ResponseEntity<>("User not found.", HttpStatus.NOT_FOUND); - } - } - @Operation(summary = "Get Application Configuration (BFLA Vulnerable)", description = "Returns sensitive application configuration details. Should be admin-only.") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Request successful") - }) - @GetMapping(path = "/v1/admin/config", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getAppConfig() { - // VULNERABILITY: No role check. A normal user can access this. - Map config = new HashMap<>(); - config.put("database.url", "jdbc:postgresql://prod-db.internal:5432/main"); - config.put("payment.gateway.apikey", "sk_live_xxxxxxxxxxxxxx"); // Sensitive data! - config.put("log.level", "DEBUG"); - - return new ResponseEntity<>(config, HttpStatus.OK); - } - @Operation(summary = "Trigger Cache Refresh (BFLA Vulnerable)", description = "Forces a refresh of the application's internal cache. Should be admin-only.") - @ApiResponses(value = { - @ApiResponse(responseCode = "202", description = "Cache refresh initiated") - }) - @PostMapping(path = "/v1/admin/cache/refresh") - public ResponseEntity refreshCache() { - // VULNERABILITY: No role check. A normal user can trigger this task. - System.out.println("INFO: Admin task 'refreshCache' triggered by user."); - // Refresh cache !? - return new ResponseEntity<>("Cache refresh initiated.", HttpStatus.ACCEPTED); - } - @Operation(summary = "Update User Status (BFLA Vulnerable)", description = "Updates a user's status. Should be admin-only.") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Status updated"), - @ApiResponse(responseCode = "404", description = "User not found") - }) - @PutMapping(path = "/v1/admin/users/{userId}/status") - public ResponseEntity setUserStatus(@PathVariable String userId, @RequestBody Map status) { - // No role check. - if (userDatabase.containsKey(userId)) { - System.out.println("INFO: User " + userId + " status changed to: " + status.get("status")); - return new ResponseEntity<>("Status updated for user " + userId, HttpStatus.OK); - } else { - return new ResponseEntity<>("User not found.", HttpStatus.NOT_FOUND); - } - } - @Operation(summary = "Update current user profile (BOPLA Vulnerable)", description = "Updates the profile of the current user. Allows for mass assignment.") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "User updated successfully", content = {@Content(schema = @Schema(implementation = User.class))}), - }) - @PutMapping(path = "/v1/users/me", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity updateUser(@RequestBody User updatedUserData) { - // VULNERABILITY: Mass Assignment. - currentUser.setUsername(updatedUserData.getUsername()); - currentUser.setEmail(updatedUserData.getEmail()); - // add "role": "admin" in the JSON for an elevation of privileges! - if (updatedUserData.getRole() != null) { - currentUser.setRole(updatedUserData.getRole()); - } - System.out.println("INFO: User updated. New role: " + currentUser.getRole()); - return new ResponseEntity<>(currentUser, HttpStatus.OK); - } - @Operation(summary = "Get a list of products (URC Vulnerable)", description = "Returns a list of products with unsafe pagination.") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Request successful", content = {@Content(array = @ArraySchema(schema = @Schema(implementation = Product.class)))}), - }) - @GetMapping(path = "/v1/products", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getProducts(@RequestParam(defaultValue = "100") int limit) { - // VULNERABILITY: No validation is performed on the 'limit' parameter. - // An attacker can provide a very large value (e.g., 999999) to trigger a DoS. - List productList = new ArrayList<>(); - for (int i = 0; i < limit; i++) { - productList.add(new Product("prod-" + i, "Product Name " + i, 19.99)); - } - try { - Thread.sleep(6000); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - return new ResponseEntity<>(productList, HttpStatus.OK); - } - // Joe Product - static class Product { - public String productId; - public String name; - public double price; - public Product() {} - public Product(String productId, String name, double price) { - this.productId = productId; - this.name = name; - this.price = price; - } - } - private static final User currentUser = new User("testuser", "test@example.com"); - // Joe User - static class User { - public String username; - public String email; - public String role = "user"; - - public User() {} - - public User(String username, String email) { - this.username = username; - this.email = email; - } - // Getters et Setters pour la sérialisation JSON - public String getUsername() { return username; } - public void setUsername(String username) { this.username = username; } - public String getEmail() { return email; } - public void setEmail(String email) { this.email = email; } - public String getRole() { return role; } - public void setRole(String role) { this.role = role; } - } - // Joe profile - static class UserProfile { - public String userId; - public String name; - public String email; - public String secretInfo; - - public UserProfile(String userId, String name, String email, String secretInfo) { - this.userId = userId; - this.name = name; - this.email = email; - this.secretInfo = secretInfo; - } - } - - /* end John Doe */ - - - @Operation(summary = "Post and get data List", description = "Post and get data List") - @ApiResponses(value = { - @ApiResponse(responseCode = $200, description = REQ_SUCCESSFULLY, content = {@Content(schema = @Schema(implementation = Data[].class))}), - }) - @PostMapping(path = "/v1/dataList", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity postDataList() { - List dataList = new ArrayList<>(); - dataList.add(new Data("info1","info2",10.00)); - dataList.add(new Data("info4","info5",0.0006)); - return new ResponseEntity<>(dataList, HttpStatus.OK); - } - @Operation(summary = "Post data List", description = "Post data List") - @ApiResponses(value = { - @ApiResponse(responseCode = $200, description = REQ_SUCCESSFULLY, content = {@Content(schema = @Schema(implementation = Data[].class))}), - }) - @PostMapping(path = "/v1/dataPostList", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity postDataList(@RequestBody List dataList) { - return new ResponseEntity<>(true, HttpStatus.OK); - } - - @Operation(summary = "Post datafile", description = "Post datafile") - @ApiResponses(value = { - @ApiResponse(responseCode = $200, description = REQ_SUCCESSFULLY, content = {@Content(schema = @Schema(implementation = HttpStatus.class))}), - }) - @PostMapping(path = "/v1/datafile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity postFile(@RequestParam("file") MultipartFile file) throws IOException { - FileUtils.copyInputStreamToFile(file.getInputStream(), new File(fileTest)); - return new ResponseEntity<>(HttpStatus.CREATED); - } - - @Operation(summary = "Put datafile", description = "Put datafile") - @ApiResponses(value = { - @ApiResponse(responseCode = $200, description = REQ_SUCCESSFULLY, content = {@Content(schema = @Schema(implementation = HttpStatus.class))}), - }) - @PutMapping(path = "/v1/datafile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity putFile(@RequestParam("file") MultipartFile file) throws IOException { - FileUtils.copyInputStreamToFile(file.getInputStream(), new File(fileTest)); - return new ResponseEntity<>(HttpStatus.CREATED); - } - - @Operation(summary = "Delete datafile", description = "Delete datafile") - @ApiResponses(value = { - @ApiResponse(responseCode = $200, description = REQ_SUCCESSFULLY, content = {@Content(schema = @Schema(implementation = HttpStatus.class))}), - }) - @DeleteMapping(path = "/v1/datafile", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity deleteFile() { - return new ResponseEntity<>(FileUtils.deleteQuietly(new File(fileTest)), HttpStatus.OK); - } - - @Operation(summary = "Get datafile", description = "Get datafile") - @ApiResponses(value = { - @ApiResponse(responseCode = $200, description = REQ_SUCCESSFULLY, content = {@Content(schema = @Schema(implementation = byte[].class))}), - }) - @GetMapping(path = "/v1/datafile", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) - public ResponseEntity getFile() throws IOException { - return ResponseEntity.ok() - .header("Content-Disposition", - "attachment; filename=" + new File(fileTest).getName().toLowerCase()) - .body(new InputStreamResource(new ClassPathResource(fileName).getInputStream())); - } - - @Operation(summary = "Patch a data", description = "Patch a data") - @ApiResponses(value = { - @ApiResponse(responseCode = $200, description = REQ_SUCCESSFULLY, content = {@Content(schema = @Schema(implementation = Data.class))}), - }) - - @PatchMapping(path = "/v1/data", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity patch(@RequestBody Data data) { - return new ResponseEntity<>(true, HttpStatus.OK); - } - @Operation(summary = "Patch datafile", description = "Patch datafile") - @ApiResponses(value = { - @ApiResponse(responseCode = $204, description = REQ_SUCCESSFULLY, content = {@Content(schema = @Schema(implementation = HttpStatus.class))}), - }) - @PatchMapping(path = "/v1/datafile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity patchFile(@RequestParam("file") MultipartFile file) throws IOException { - FileUtils.copyInputStreamToFile(file.getInputStream(), new File(fileTest)); - return new ResponseEntity<>(HttpStatus.NO_CONTENT); - } - - @Operation(summary = "Get Error 400", description = "Get Error 400") - @ApiResponses(value = { - @ApiResponse(responseCode = $400, description = REQ_NOT_CORRECTLY, content = {@Content(schema = @Schema(implementation = ResponseEntity.class))}), - }) - @GetMapping(path = "/v1/dataError400", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getError400() { - return super.getResponseEntity("400", "ERROR", "Bad Request", HttpStatus.BAD_REQUEST); - } - - @Operation(summary = "Get Error 401", description = "Get Error 401") - @ApiResponses(value = { - @ApiResponse(responseCode = $401, description = USER_NOT_AUTH, content = {@Content(schema = @Schema(implementation = Message.class))}), - }) - @GetMapping(path = "/v1/dataError401", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getError401() { - return super.getResponseEntity("401", "ERROR", "Unauthorized", HttpStatus.UNAUTHORIZED); - } - - @Operation(summary = "Get Error 500", description = "Get Error 500") - @ApiResponses(value = { - @ApiResponse(responseCode = $500, description = INTERNAL_SERVER, content = {@Content(schema = @Schema(implementation = Message.class))}), - }) - @GetMapping(path = "/v1/dataError500", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getError500() { - return super.getResponseEntity("500", "ERROR", "Internal Server Error", HttpStatus.INTERNAL_SERVER_ERROR); - } - - /* - * Proxy in ByteArray - * The data can be retrieved as a byte[] is a direct proxy without any in-app data conversion. - * (See WebMvcConfigurer.configureMessageConverters(List> converters) need to be disabled) - */ - @Operation(summary = "Proxy Post data ", description = "Proxy Post data") - @ApiResponses(value = { - @ApiResponse(responseCode = $200, description = REQ_SUCCESSFULLY, content = {@Content(schema = @Schema(implementation = byte[].class))}), - }) - @PostMapping(value = "/v1/proxy", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) - public ResponseEntity redirect(@RequestBody(required = false) String body, HttpServletRequest request) throws URISyntaxException { - // Switch url /v1/proxy --> /v1/redirect - String url = request.getRequestURL().toString().replaceAll("/proxy", "/redirect"); - URI uri = UriComponentsBuilder.fromHttpUrl(url).build().toUri(); - RequestEntity req = new RequestEntity<>(body, extractHeaders(request), HttpMethod.POST, uri); - try { - ResponseEntity responseEntity = new RestTemplate().exchange(req, byte[].class); - return new ResponseEntity<>(responseEntity.getBody(), filterHeaders(responseEntity.getHeaders()), responseEntity.getStatusCode()); - } catch (HttpClientErrorException e) { - logger.info("Error occured during proxying: {}", e.getMessage()); - return new ResponseEntity<>(e.getResponseBodyAsByteArray(), filterHeaders(e.getResponseHeaders()), e.getStatusCode()); - } - } - /* Hidden Redirect endpoint */ - @Hidden - @RequestMapping(value = "/v1/redirect", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) - public ResponseEntity redirect(@RequestBody(required = false) String body, - HttpMethod method, - HttpServletRequest request) { - // Switch url /v1/redirect --> /v1/echo - String url = request.getRequestURL().toString().replaceAll("/redirect", "/echo"); - URI uri = UriComponentsBuilder.fromHttpUrl(url).build().toUri(); - return new RestTemplate().exchange(uri, method, new HttpEntity<>(body, extractHeaders(request)), byte[].class); // All is Bytes! - - } - /* Hidden Echo endpoint */ - @Hidden - @RequestMapping(path = "/v1/echo", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) - public ResponseEntity echo(@RequestBody(required = false) String body, HttpMethod method, HttpServletRequest request) - throws JsonProcessingException { - Map map = request.getParameterMap(); - Map headers = extractHeaders(request).entrySet().stream() - .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().toArray(new String[0]))); - return new ResponseEntity<>(new EchoEntity(method.name(), body, map, headers), HttpStatus.OK); - } - - private static class EchoEntity implements Serializable { - public Map headers; - public Map map; - public String body; - public String method; - - public EchoEntity(String method, String body, Map map, Map headers) { - this.method = method; - this.body = body; - this.headers = headers; - this.map = map; - } - } - - @Schema - public static class Data { - - @Parameter(name = "data1", description = "data1 field") - public String data1; - @Parameter(name = "data2", description = "data2 field") - public String data2; - @Parameter(name = "data3", description = "data3 field") - public double data3; - - @Parameter(name = "data4", description = "Date field") - @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") - public Date data4; - - public Data() { - } - public Data(String data1, String data2, double data3) { - this.data1 = data1; - this.data2 = data2; - this.data3 = data3; - } - public Data(String data1, String data2, double data3, Date data4) { - this.data1 = data1; - this.data2 = data2; - this.data3 = data3; - this.data4 = data4; - } - @Override - public String toString() { - return "Data{" + - "data1='" + data1 + '\'' + - ", data2='" + data2 + '\'' + - ", data3='" + data3 + '\'' + - ", data4='" + data4 + '\'' + - '}'; - } - } - - @Schema - public static class DataJoe { - - @Parameter(name = "dataJoe", description = "dataJoe field") - public Map dataJoe ; - - - public DataJoe() { - } - public DataJoe(Map dataJoe, String data2, double data3) { - this.dataJoe = dataJoe; - } - @Override - public String toString() { - return "Data{" + - "dataJoe='" + dataJoe + '\'' + - '}'; - } - - public Map getDataJoe() { - return dataJoe; - } - - public void setDataJoe(Map dataJoe) { - this.dataJoe = dataJoe; - } - } - - private static HttpHeaders extractHeaders(HttpServletRequest request) { - HttpHeaders headers = new HttpHeaders(); - headers.setOrigin(request.getRequestURL().toString()); - Enumeration headerNames = request.getHeaderNames(); - while (headerNames.hasMoreElements()) { - String headerName = headerNames.nextElement(); - headers.add(headerName, request.getHeader(headerName)); - } - return headers; - } - - private HttpHeaders filterHeaders(HttpHeaders originalHeaders) { - HttpHeaders responseHeaders = new HttpHeaders(); - responseHeaders.putAll(originalHeaders); - // chunked and length headers must not be forwarded - responseHeaders.remove(HttpHeaders.TRANSFER_ENCODING); - responseHeaders.remove(HttpHeaders.CONTENT_LENGTH); - //responseHeaders.remove("Accept-Encoding"); // Gzip !? - //Following headers must not be included, because they would appear twice - responseHeaders.remove(HttpHeaders.CACHE_CONTROL); - responseHeaders.remove(HttpHeaders.PRAGMA); - responseHeaders.remove(HttpHeaders.VARY); - responseHeaders.remove(HttpHeaders.EXPIRES); - responseHeaders.remove("X-Content-Type-Options"); - responseHeaders.remove("X-XSS-Protection"); - responseHeaders.remove("X-Frame-Options"); - return responseHeaders; - } -} diff --git a/src/main/java/com/jservlet/nexus/controller/StatusController.java b/src/main/java/com/jservlet/nexus/controller/StatusController.java deleted file mode 100644 index f2cea2b..0000000 --- a/src/main/java/com/jservlet/nexus/controller/StatusController.java +++ /dev/null @@ -1,212 +0,0 @@ -/* - * Copyright (C) 2001-2025 JServlet.com Franck Andriano. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - */ - -package com.jservlet.nexus.controller; - -import com.jservlet.nexus.config.web.WebConfig; -import com.jservlet.nexus.shared.service.backend.BackendService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.core.env.Environment; -import org.springframework.core.io.Resource; -import org.springframework.http.*; -import org.springframework.stereotype.Controller; -import org.springframework.util.MimeType; -import org.springframework.util.MimeTypeUtils; -import org.springframework.web.bind.annotation.GetMapping; - -import javax.annotation.PostConstruct; -import javax.servlet.http.HttpServletRequest; -import java.io.IOException; -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.nio.charset.Charset; -import java.text.SimpleDateFormat; -import java.util.*; -import java.util.concurrent.TimeUnit; - -import static java.nio.charset.StandardCharsets.UTF_8; - -/* - * The Status Controller over Http - * curl -v http://localhost:8082/nexus-backend/health/status - * curl -v -u admin:admin -H "Accept: text/plain" http://localhost:8082/nexus-backend/health/status - * or over Https - * curl -v -k https://nexus6.jservlet.com:8443/nexus-backend/health/status - * curl -v -k -u admin:admin -H "Accept: text/plain" https://nexus6.jservlet.com:8443/nexus-backend/health/status - */ -@Controller -public class StatusController { - - private static final Logger logger = LoggerFactory.getLogger(StatusController.class); - - private String application; - private String version; - private String build; - private String buildTime; - private String revision; - private String branch; - private String profile; - private String javaVmVersion; - private String javaIoTmpdir; - private String fileEncoding; - - private static final String sep = "\n"; - - @Value("${serverName:#{null}}") - private String serverName; - - @Value("${nexus.backend.uri.alive:#{null}}") - private String uriAlive; - - private final BackendService backendService; - private final Environment env; - - private static final String ENV_VAR = "environment"; - - public StatusController(BackendService backendService, Environment env) { - this.backendService = backendService; - this.env = env; - } - - @PostConstruct - public void postConstruct() throws IOException { - Resource resource = WebConfig.getApplicationContext().getResource("classpath:META-INF/version.properties"); - if (!resource.exists()) - throw new MissingResourceException("Unable to find \"version.properties\" on classpath!", - getClass().getName(), "version.properties"); - - Properties properties = new Properties(); - properties.load(resource.getInputStream()); - application = properties.getProperty("application.name"); - version = properties.getProperty("version"); - build = properties.getProperty("build.number"); - buildTime = properties.getProperty("build.time"); - revision = properties.getProperty("build.revision"); - branch = properties.getProperty("build.branch"); - profile = properties.getProperty("build.profile"); - // only for Dev env.! - javaVmVersion = System.getProperty("java.vm.version"); - javaIoTmpdir = System.getProperty("java.io.tmpdir"); - fileEncoding = Charset.defaultCharset().displayName(); - - if (serverName == null) { - serverName = System.getenv("COMPUTERNAME"); - if (serverName == null) { - serverName = System.getenv("HOSTNAME"); - if (serverName == null) { - try { serverName = InetAddress.getLocalHost().getHostName(); } - catch (UnknownHostException ignored) {} - } - } - } - } - - @GetMapping(value = "/health/status", produces = MimeTypeUtils.TEXT_PLAIN_VALUE) - public ResponseEntity status(HttpServletRequest request) { return createHealthStatusResponseEntity(request, true); } - - @GetMapping(value = "/health/controlpage", produces = MimeTypeUtils.TEXT_PLAIN_VALUE) - public ResponseEntity controlpage(HttpServletRequest request) { return createHealthStatusResponseEntity(request, false); } - - private ResponseEntity createHealthStatusResponseEntity(HttpServletRequest request, boolean withServices) { - SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - String now = sdf.format(System.currentTimeMillis()); - long currentTime = System.currentTimeMillis(); - long startup = WebConfig.getApplicationContext().getStartupDate(); - String started = sdf.format(startup); - long days = getDaysBetween(currentTime, startup); - - final Health health = checkBackendServiceHealth() ? Health.OK : Health.ERROR; - final Health lbHealth = health != Health.ERROR ? Health.OK : Health.ERROR; - - Map status = new LinkedHashMap<>(); - status.put("Status timestamp", now); - status.put("Application", application); - status.put("Version", version); - status.put("Startup date", started); - status.put("Startup days", String.valueOf(days)); - status.put("Build profile", profile); - status.put("Build number", build); - status.put("Build timestamp", buildTime); - status.put("Revision", revision); - status.put("Branch", branch); - status.put("Health", health.name()); - status.put("LB-Health", lbHealth.name()); - status.put("Server", serverName); - // Dev only! - if ("development".equals(env.getProperty(ENV_VAR))) { - status.put("Jvm-version", javaVmVersion); - status.put("Java io-tmpdir", javaIoTmpdir); - status.put("Java fileEncoding", fileEncoding); - } - - StringBuilder sb = new StringBuilder(); - for (Map.Entry entry : status.entrySet()) { - if (sb.length() > 0) sb.append(sep); - sb.append(entry.getKey()).append(": ").append(entry.getValue()); - } - if (withServices) { - sb.append(sep).append(sep).append(sep); - // Add services - sb.append("Health-Services").append(sep); - sb.append("--------------------------------------------------------------").append(sep); - sb.append("Service-Name: ").append("Nexus-Backend Service").append(sep); - sb.append("Service-Status: ").append(health).append(sep); - sb.append("Backend-URL: ").append(backendService.getBackendURL()).append(sep); - sb.append("Backend-Test-Uri: ").append(uriAlive).append(sep); - sb.append("Removed-Header: ").append(backendService.isRemovedHeaders()).append(sep); - } - return ResponseEntity.ok() - .header(HttpHeaders.CONTENT_TYPE, new MimeType(MimeTypeUtils.TEXT_PLAIN, UTF_8).toString()) - .body(sb.toString()); - } - - private long getDaysBetween(long startTimeMillis, long endTimeMillis) { - long diffInMillis = Math.abs(endTimeMillis - startTimeMillis); - return TimeUnit.MILLISECONDS.toDays(diffInMillis); - } - - private boolean checkBackendServiceHealth() { - try { - return backendService.get(uriAlive, backendService.createResponseType(Object.class)) != null; - } catch (Exception e) { - logger.error("BackendService uriAlive: '{}' Endpoint does not work properly: {}", uriAlive, e.getMessage()); - return false; - } - } - - private enum Health { - OK(0), - WARN(1), - ERROR(2); - - private final int value; - - Health(int value) { this.value = value; } - - public int getValue() { - return value; - } - - @Override - public String toString() { - return this.name(); - } - } -} diff --git a/src/main/java/com/jservlet/nexus/shared/config/annotation/ConfigProperties.java b/src/main/java/com/jservlet/nexus/shared/config/annotation/ConfigProperties.java deleted file mode 100644 index bc9da72..0000000 --- a/src/main/java/com/jservlet/nexus/shared/config/annotation/ConfigProperties.java +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright (C) 2001-2024 JServlet.com Franck Andriano. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - */ - -package com.jservlet.nexus.shared.config.annotation; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.beans.factory.support.BeanDefinitionBuilder; -import org.springframework.beans.factory.support.BeanDefinitionRegistry; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.context.ResourceLoaderAware; -import org.springframework.context.annotation.Import; -import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; -import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; -import org.springframework.core.annotation.AnnotationAttributes; -import org.springframework.core.env.*; -import org.springframework.core.io.FileSystemResource; -import org.springframework.core.io.Resource; -import org.springframework.core.io.ResourceLoader; -import org.springframework.core.io.support.ResourcePropertySource; -import org.springframework.core.type.AnnotationMetadata; -import org.springframework.lang.NonNull; -import org.springframework.util.Assert; -import org.springframework.web.context.ServletContextAware; - -import javax.servlet.ServletContext; -import java.io.IOException; -import java.lang.annotation.*; -import java.util.*; - -/** - * Annotation for interfaces declaring a config properties file as PropertySourcesPlaceholder - */ -@Target(ElementType.TYPE) -@Retention(RetentionPolicy.RUNTIME) -@Documented -@Import(ConfigProperties.ConfigPropertiesRegistrar.class) -public @interface ConfigProperties { - - String[] value(); - - class ConfigPropertiesRegistrar implements ImportBeanDefinitionRegistrar { - - @Override - public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { - Class annotationType = ConfigProperties.class; - AnnotationAttributes annotationAttributes = AnnotationAttributes.fromMap( - importingClassMetadata.getAnnotationAttributes(annotationType.getName(), false)); - Assert.notNull(annotationAttributes, String.format("@%s is not present on import class '%s' as expected", - annotationType.getSimpleName(), importingClassMetadata.getClassName())); - // Register property sources placeholder configurer - registry.registerBeanDefinition("nexusPropertySourcesPlaceHolderConfigurer", - BeanDefinitionBuilder.rootBeanDefinition(NexusPropertySourcesPlaceHolderConfigurer.class) - .addPropertyValue("projectConfigProperties", annotationAttributes.getStringArray("value")) - .getBeanDefinition()); - } - } - - class NexusPropertySourcesPlaceHolderConfigurer extends PropertySourcesPlaceholderConfigurer - implements InitializingBean, ResourceLoaderAware, ServletContextAware, ApplicationContextAware { - // Set Logger ConfigProperties! - private static final Logger logger = LoggerFactory.getLogger(ConfigProperties.class); - - private String[] projectConfigProperties; - - private Environment environment; - - private ResourceLoader resourceLoader; - - private ServletContext servletContext; - - private ApplicationContext applicationContext; - - public void setProjectConfigProperties(String[] projectConfigProperties) { - this.projectConfigProperties = projectConfigProperties; - } - - @Override - public void setEnvironment(@NonNull Environment environment) { - //set system properties! - super.setEnvironment(environment); - this.environment = environment; - } - - @Override - public void setResourceLoader(@NonNull ResourceLoader resourceLoader) { - this.resourceLoader = resourceLoader; - } - - @Override - public void setServletContext(@NonNull ServletContext servletContext) { - this.servletContext = servletContext; - } - - @Override - public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException { - this.applicationContext = applicationContext; - } - - @Override - public void afterPropertiesSet() throws Exception { - MutablePropertySources propertySources = new MutablePropertySources(); - // Required resources! - List requiredSources = new ArrayList<>(projectConfigProperties.length); - for (String configProperty : projectConfigProperties) { - requiredSources.add(resourceLoader.getResource(configProperty)); - } - addPropertySource(propertySources, requiredSources, true); - // Existing resources ? - List listResources = new ArrayList<>(Arrays.asList( - new FileSystemResource(environment.resolveRequiredPlaceholders( - "${user.home}/conf-global/config.properties")), - new FileSystemResource(environment.resolveRequiredPlaceholders( - "${user.home}/conf/config.properties")))); - // servletContext available! - if (servletContext != null) { - Resource resource = new FileSystemResource(environment.resolveRequiredPlaceholders( - "${user.home}/cfg" + servletContext.getContextPath() + "/config.properties")); - listResources.add(resource); - } - else // or getId applicationContext, see spring.application.name - if (applicationContext != null) { - Resource resource = new FileSystemResource(environment.resolveRequiredPlaceholders( - "${user.home}/cfg/" + applicationContext.getId() + "/config.properties")); - listResources.add(resource); - } - addPropertySource(propertySources, listResources, false); - setPropertySources(propertySources); - - // logged all the keys/values - if (logger.isInfoEnabled()) { - Map map = new TreeMap<>(); // alpha order! - for (PropertySource propertySource : propertySources) { - if (propertySource instanceof EnumerablePropertySource) { - final String[] propertyNames = ((EnumerablePropertySource) propertySource).getPropertyNames(); - for (String propertyName : propertyNames) { - map.put(propertyName, propertySource.getProperty(propertyName)); - } - } - } - logger.info("*** Loaded ConfigProperties ***"); - for (Map.Entry entry : map.entrySet()) { - logger.info("{} = {}", entry.getKey(), entry.getValue()); - } - } - } - - private void addPropertySource(MutablePropertySources propertySources, List resources, boolean required) - throws IOException { - for (Resource resource : resources) { - if (required || resource.exists()) { - logger.info("Load properties from: {}", resource); - propertySources.addFirst(new ResourcePropertySource(resource)); - } else { - logger.info("Resource doesn't exist: {}", resource); - } - } - } - - - } -} diff --git a/src/main/java/com/jservlet/nexus/shared/exceptions/NexusCreationException.java b/src/main/java/com/jservlet/nexus/shared/exceptions/NexusCreationException.java deleted file mode 100644 index 70fb55f..0000000 --- a/src/main/java/com/jservlet/nexus/shared/exceptions/NexusCreationException.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (C) 2001-2024 JServlet.com Franck Andriano. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - */ - -package com.jservlet.nexus.shared.exceptions; - -/** - * Thrown when a creation of an entity fails. - */ -public class NexusCreationException extends Exception { - - public NexusCreationException(String message) { super(message); } - public NexusCreationException(String message, Throwable cause) { super(message, cause); } - public NexusCreationException(Throwable cause) { super(cause); } - -} diff --git a/src/main/java/com/jservlet/nexus/shared/exceptions/NexusDeleteException.java b/src/main/java/com/jservlet/nexus/shared/exceptions/NexusDeleteException.java deleted file mode 100644 index 7c67b75..0000000 --- a/src/main/java/com/jservlet/nexus/shared/exceptions/NexusDeleteException.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (C) 2001-2024 JServlet.com Franck Andriano. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - */ - -package com.jservlet.nexus.shared.exceptions; - -/** - * Thrown when a deletion of an entity fails. - */ -public class NexusDeleteException extends Exception { - - public NexusDeleteException(String message) { super(message); } - public NexusDeleteException(String message, Throwable cause) { super(message, cause); } - public NexusDeleteException(Throwable cause) { super(cause); } - -} diff --git a/src/main/java/com/jservlet/nexus/shared/exceptions/NexusGetException.java b/src/main/java/com/jservlet/nexus/shared/exceptions/NexusGetException.java deleted file mode 100644 index 960e7af..0000000 --- a/src/main/java/com/jservlet/nexus/shared/exceptions/NexusGetException.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (C) 2001-2024 JServlet.com Franck Andriano. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - */ - -package com.jservlet.nexus.shared.exceptions; - -/** - * Thrown when a get of entity/entities fails. - */ -public class NexusGetException extends Exception { - - public NexusGetException(String message) { super(message); } - public NexusGetException(String message, Throwable cause) { super(message, cause); } - public NexusGetException(Throwable cause) { super(cause); } - -} diff --git a/src/main/java/com/jservlet/nexus/shared/exceptions/NexusHttpException.java b/src/main/java/com/jservlet/nexus/shared/exceptions/NexusHttpException.java deleted file mode 100644 index 138c038..0000000 --- a/src/main/java/com/jservlet/nexus/shared/exceptions/NexusHttpException.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (C) 2001-2024 JServlet.com Franck Andriano. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - */ - -package com.jservlet.nexus.shared.exceptions; - -/** - * Thrown when a http request to the backend fails. - */ -public class NexusHttpException extends Exception { - - public NexusHttpException(String message) { super(message); } - public NexusHttpException(String message, Throwable cause) { super(message, cause); } - public NexusHttpException(Throwable cause) { super(cause); } - -} diff --git a/src/main/java/com/jservlet/nexus/shared/exceptions/NexusIllegalUrlException.java b/src/main/java/com/jservlet/nexus/shared/exceptions/NexusIllegalUrlException.java deleted file mode 100644 index 0c48136..0000000 --- a/src/main/java/com/jservlet/nexus/shared/exceptions/NexusIllegalUrlException.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (C) 2001-2024 JServlet.com Franck Andriano. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - */ - -package com.jservlet.nexus.shared.exceptions; - -/** - * Thrown when an illegal url will be requested. - */ -public class NexusIllegalUrlException extends Exception { - - public NexusIllegalUrlException(String message) { super(message); } - public NexusIllegalUrlException(String message, Throwable cause) { super(message, cause); } - public NexusIllegalUrlException(Throwable cause) { super(cause); } - -} diff --git a/src/main/java/com/jservlet/nexus/shared/exceptions/NexusModificationException.java b/src/main/java/com/jservlet/nexus/shared/exceptions/NexusModificationException.java deleted file mode 100644 index 54886d5..0000000 --- a/src/main/java/com/jservlet/nexus/shared/exceptions/NexusModificationException.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (C) 2001-2024 JServlet.com Franck Andriano. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - */ - -package com.jservlet.nexus.shared.exceptions; - -/** - * Thrown when a modification of an entity fails. - */ -public class NexusModificationException extends Exception { - - public NexusModificationException(String message) { super(message); } - public NexusModificationException(String message, Throwable cause) { super(message, cause); } - public NexusModificationException(Throwable cause) { super(cause); } - -} diff --git a/src/main/java/com/jservlet/nexus/shared/exceptions/NexusNotAuthorizedException.java b/src/main/java/com/jservlet/nexus/shared/exceptions/NexusNotAuthorizedException.java deleted file mode 100644 index f2406f5..0000000 --- a/src/main/java/com/jservlet/nexus/shared/exceptions/NexusNotAuthorizedException.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (C) 2001-2024 JServlet.com Franck Andriano. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - */ - -package com.jservlet.nexus.shared.exceptions; - -/** - * Thrown when a method is called which use the current logged user. - */ -public class NexusNotAuthorizedException extends Exception { - - public NexusNotAuthorizedException(String message) { super(message); } - public NexusNotAuthorizedException(String message, Throwable cause) { super(message, cause); } - public NexusNotAuthorizedException(Throwable cause) { super(cause); } - -} diff --git a/src/main/java/com/jservlet/nexus/shared/exceptions/NexusResourceExistsException.java b/src/main/java/com/jservlet/nexus/shared/exceptions/NexusResourceExistsException.java deleted file mode 100644 index 94e8a37..0000000 --- a/src/main/java/com/jservlet/nexus/shared/exceptions/NexusResourceExistsException.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (C) 2001-2024 JServlet.com Franck Andriano. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - */ - -package com.jservlet.nexus.shared.exceptions; - -/** - * Thrown when a method is called which a resource already exists (due to conflict) - */ -public class NexusResourceExistsException extends NexusHttpException { - - public NexusResourceExistsException(String message) { super(message); } - -} diff --git a/src/main/java/com/jservlet/nexus/shared/exceptions/NexusResourceNotFoundException.java b/src/main/java/com/jservlet/nexus/shared/exceptions/NexusResourceNotFoundException.java deleted file mode 100644 index 01a8c76..0000000 --- a/src/main/java/com/jservlet/nexus/shared/exceptions/NexusResourceNotFoundException.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (C) 2001-2024 JServlet.com Franck Andriano. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - */ - -package com.jservlet.nexus.shared.exceptions; - -/** - * Thrown when a method is called which a resource is not found - */ -public class NexusResourceNotFoundException extends Exception { - - public NexusResourceNotFoundException(String message) { super(message); } - public NexusResourceNotFoundException(String message, Throwable cause) { super(message, cause); } - public NexusResourceNotFoundException(Throwable cause) { super(cause); } - -} diff --git a/src/main/java/com/jservlet/nexus/shared/exceptions/NexusServiceException.java b/src/main/java/com/jservlet/nexus/shared/exceptions/NexusServiceException.java deleted file mode 100644 index 95bf165..0000000 --- a/src/main/java/com/jservlet/nexus/shared/exceptions/NexusServiceException.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (C) 2001-2024 JServlet.com Franck Andriano. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - */ - -package com.jservlet.nexus.shared.exceptions; - -/** - * Thrown when a conversion of an entity fails. - */ -public class NexusServiceException extends Exception { - - public NexusServiceException(String message) { super(message); } - public NexusServiceException(String message, Throwable cause) { super(message, cause); } - public NexusServiceException(Throwable cause) { super(cause); } - -} diff --git a/src/main/java/com/jservlet/nexus/shared/service/backend/BackendService.java b/src/main/java/com/jservlet/nexus/shared/service/backend/BackendService.java deleted file mode 100644 index 464a514..0000000 --- a/src/main/java/com/jservlet/nexus/shared/service/backend/BackendService.java +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright (C) 2001-2024 JServlet.com Franck Andriano. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - */ - -package com.jservlet.nexus.shared.service.backend; - -import com.jservlet.nexus.shared.exceptions.*; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.core.io.Resource; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; - -/** - * Rest BackendService - */ -public interface BackendService { - - /** - * Execute a get request and returns the response as String - * - * @param The expected class of the value - * @param url The url to the backend to be executed - * @param responseType The response type the result should be converted to - * @return The parsed response as an instance of type specified using the responseType parameter - * @throws NexusGetException When the backend returns a 500 - * @throws NexusResourceNotFoundException When the backend returns a 404 - */ - T get(String url, ResponseType responseType) throws NexusGetException, NexusResourceNotFoundException; - - /** - * Execute a get request and returns the response as File - * The response is of type Resource which contain the feed of type multipart and the headers - * - * @param url The url to the backend to be executed - * @return The File downloaded from the backend as a Resource - * @throws NexusGetException When the backend returns a 500 - * @throws NexusResourceNotFoundException When the backend returns a 404 - */ - Resource getFile(String url) throws NexusGetException, NexusResourceNotFoundException; - - /** - * Execute a post request and returns the response as String - * A post request is usually create a new entry and return it the response - * - * @param The expected class of the value. - * @param url The url to the backend to be executed - * @param data The entity to be created - * @param responseType The response type the result should be converted to - * @return The parsed response as an instance of type specified using the responseType parameter - * @throws NexusCreationException When the backend returns a 500 - * @throws NexusResourceExistsException When the backend returns a 404 - */ - T post(String url, Object data, ResponseType responseType) throws NexusCreationException, NexusResourceExistsException; - - /** - * Execute a post with a File as multipart/form-data and returns the response. - * - * @param The expected class of the value - * @param url The url to the backend to be executed - * @param resource The file/data to be sent - * @param responseType The response type the result should be converted to - * @return The parsed response as an instance of type specified using the responseType parameter - * @throws NexusCreationException When the backend returns a 500 - * @throws NexusResourceExistsException When the backend returns a 404 - */ - T postFile(String url, Resource resource, ResponseType responseType) throws NexusCreationException, NexusResourceExistsException; - - /** - * Execute a put request and returns the response as String - * A post request is usually modify an entry and return it the response - * - * @param The expected class of the value - * @param url The url to the backend to be executed - * @param data The entity to be modified - * @param responseType The response type the result should be converted to - * @return The parsed response as an instance of type specified using the responseType parameter - * @throws NexusModificationException When the backend returns a 500 - * @throws NexusResourceExistsException When the backend returns a 409 - * @throws NexusResourceNotFoundException When the backend returns a 404 - */ - T put(String url, Object data, ResponseType responseType) throws NexusModificationException, NexusResourceExistsException, NexusResourceNotFoundException; - - /** - * Execute a putFile request and returns the response as Resource - * A putFile request is usually modify an entry and return it the response - * - * @param The expected class of the value - * @param url The url to the backend to be executed - * @param resource The file/data to be sent - * @param responseType The response type the result should be converted to - * @return The parsed response as an instance of type specified using the responseType parameter - * @throws NexusModificationException When the backend returns a 500 - * @throws NexusResourceExistsException When the backend returns a 409 - * @throws NexusResourceNotFoundException When the backend returns a 404 - */ - T putFile(String url, Resource resource, ResponseType responseType) throws NexusModificationException, NexusResourceNotFoundException, NexusResourceExistsException; - - /** - * Execute a delete request, its a void - * A post request is usually delete an entry and returns a http status - * - * @param url The url to the backend to be executed - * @throws NexusDeleteException When the backend returns a 500 - * @throws NexusResourceNotFoundException When the backend returns a 404 - */ - void delete(String url) throws NexusDeleteException, NexusResourceNotFoundException; - - /** - * Execute a patch request and returns the response as String - * A patch request is usually update an entry and return it the response (Normally 204 No Content) - * - * @param The expected class of the value. - * @param url The url to the backend to be executed - * @param data The entity to be updated - * @param responseType The response type the result should be converted to - * @return The parsed response as an instance of type specified using the responseType parameter - * @throws NexusModificationException When the backend returns a 500 - * @throws NexusResourceExistsException When the backend returns a 500 - * @throws NexusResourceNotFoundException When the backend returns a 404 - */ - T patch(String url, Object data, ResponseType responseType) throws NexusModificationException, NexusResourceExistsException, NexusResourceNotFoundException; - - /** - * Execute a patchFile request and returns the response as Resource - * A patchFile request is usually modify an entry and return it the response (Normally 204 No Content) - * - * @param The expected class of the value - * @param url The url to the backend to be executed - * @param resource The file/data to be sent - * @param responseType The response type the result should be converted to - * @return The parsed response as an instance of type specified using the responseType parameter - * @throws NexusModificationException When the backend returns a 500 - * @throws NexusResourceExistsException When the backend returns a 409 - * @throws NexusResourceNotFoundException When the backend returns a 404 - */ - T patchFile(String url, Resource resource, ResponseType responseType) throws NexusModificationException, NexusResourceNotFoundException, NexusResourceExistsException; - - - /** - * Execute a request Rest BackendService - * - * @param The expected class of the value. - * @param url The url to the backend to be executed - * @param method The method - * @param responseType The response type the result should be converted to - * @param body The body if exist or null - * @param headers The headers - * @return The parsed response as an instance of type specified using the responseType parameter - * @throws NexusResourceNotFoundException When the backend returns a 404 - * @throws NexusHttpException When a http request to th backend fails. - * @throws NexusIllegalUrlException When an illegal url will be requested. - */ - - T doRequest(String url, HttpMethod method, ResponseType responseType, Object body, HttpHeaders headers) - throws NexusResourceNotFoundException, NexusHttpException, NexusIllegalUrlException; - - ResponseType createResponseType(Class responseType); - - ResponseType createResponseType(ParameterizedTypeReference responseType); - - interface ResponseType { - Class getResponseClass(); - ParameterizedTypeReference getResponseParameterizedTypeReference(); - } - - String getBackendURL(); - boolean isRemovedHeaders(); - -} diff --git a/src/main/java/com/jservlet/nexus/shared/service/backend/BackendServiceImpl.java b/src/main/java/com/jservlet/nexus/shared/service/backend/BackendServiceImpl.java deleted file mode 100644 index a4c4260..0000000 --- a/src/main/java/com/jservlet/nexus/shared/service/backend/BackendServiceImpl.java +++ /dev/null @@ -1,581 +0,0 @@ -/* - * Copyright (C) 2001-2025 JServlet.com Franck Andriano. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - */ - -package com.jservlet.nexus.shared.service.backend; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.jservlet.nexus.shared.exceptions.*; -import com.jservlet.nexus.shared.service.backend.api.ErrorMessage; -import com.jservlet.nexus.shared.service.backend.api.IBackendErrorMessage; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.core.io.Resource; -import org.springframework.core.log.LogFormatUtils; -import org.springframework.http.*; -import org.springframework.stereotype.Service; -import org.springframework.util.*; -import org.springframework.web.client.HttpStatusCodeException; -import org.springframework.web.client.RestOperations; - -import javax.annotation.PostConstruct; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.*; - -/** - * This implementation Rest BackendService is used for the communication to a backend. - * It provides methods for all supported http protocols on the backend side. - * Normally it communicate to an api interface backend. - *

- * The backend will be configured by the following properties - *

- * BackendServiceImpl#setBackendURL - *

    - *
  • nexus.backend.url: set the URL to access to the backend
  • - *
- */ -@Service -public final class BackendServiceImpl implements BackendService { - - private static final Logger logger = LoggerFactory.getLogger(BackendServiceImpl.class); - - private static final ResponseType EMPTY_RESPONSE = new ResponseTypeImpl<>(); - - private String backendURL; - - private RestOperations restOperations; - - private ObjectMapper objectMapper; - - @Value("${nexus.backend.error.message.class:com.jservlet.nexus.shared.service.backend.api.ErrorMessage}") - private String errorMessageClassName; - - private Class errorClass; - - @PostConstruct - @SuppressWarnings("unchecked") - public void initErrorClass() { - try { - errorClass = (Class) Class.forName(errorMessageClassName); - } catch (ClassNotFoundException e) { - logger.error("Configured error message class not found: " + errorMessageClassName + ". Falling back to default.", e); - errorClass = ErrorMessage.class; - } - } - - /** - * Return by the default a Json Entity Object or Resource, else if true a Generics Object. - */ - private boolean isHandleBackendEntity = false; - - public BackendServiceImpl() { - } - - public BackendServiceImpl(boolean isHandleBackendEntity) { - this.isHandleBackendEntity = isHandleBackendEntity; - } - - public BackendServiceImpl(String backendURL, RestOperations restOperations, ObjectMapper objectMapper) { - Assert.hasText(backendURL, "Backend URL must not be empty."); - Assert.notNull(restOperations, "RestOperations must not be null."); - Assert.notNull(objectMapper, "ObjectMapper must not be null."); - this.backendURL = backendURL; - this.restOperations = restOperations; - this.objectMapper = objectMapper; - initErrorClass(); - } - - /** - * Constructor complete - * - * @param backendURL The targeted backend URL - * @param restOperations The RestOperations - * @param objectMapper The ObjectMapper - * @param isHandleBackendEntity True a Generics Object, false a Json Entity Object or a Resource - */ - public BackendServiceImpl(String backendURL, RestOperations restOperations, ObjectMapper objectMapper, boolean isHandleBackendEntity) { - Assert.hasText(backendURL, "Backend URL must not be empty."); - Assert.notNull(restOperations, "RestOperations must not be null."); - Assert.notNull(objectMapper, "ObjectMapper must not be null."); - this.backendURL = backendURL; - this.restOperations = restOperations; - this.objectMapper = objectMapper; - this.isHandleBackendEntity = isHandleBackendEntity; - initErrorClass(); - } - - public void setBackendURL(String backendURL) { - this.backendURL = backendURL; - } - - public void setRestOperations(RestOperations restOperations) { - this.restOperations = restOperations; - } - - public void setObjectMapper(ObjectMapper objectMapper) { - this.objectMapper = objectMapper; - } - - - @Value("${nexus.backend.header.authorization.username:}") - private String username; - - @Value("${nexus.backend.header.authorization.password:}") - private String password; - - @Value("${nexus.backend.header.cookie:}") - private String cookie; - - @Value("${nexus.backend.header.bearer:}") - private String bearer; - - @Value("${nexus.backend.header.remove:true}") - private boolean removeHeaders; - @Value("${nexus.backend.forwarded.headers:true}") - private boolean forwardedHeaders; - @Value("#{'${nexus.backend.forwarded.client.headers:}'.split(',')}") - private List forwardedClientHeaders; - - @Value("${nexus.backend.header.host.remove:false}") - private boolean removeHostHeader; - @Value("${nexus.backend.header.origin.remove:false}") - private boolean removeOriginHeader; - - @Value("${nexus.backend.http.response.truncated:false}") - private boolean truncated; - @Value("${nexus.backend.http.response.truncated.maxLength:1000}") - private int maxLengthTruncated; - - @Value("${nexus.backend.header.user-agent:JavaNexus}") - private String userAgent = "JavaNexus"; - - @PostConstruct - public void init() { - forwardedClientHeaders.addAll(STATIC_FORWARDED_HEADERS); - } - - - @Override - public T get(String url, ResponseType responseType) - throws NexusGetException, NexusResourceNotFoundException { - try { - return doRequest(url, HttpMethod.GET, responseType, null, null); - } catch (NexusHttpException | NexusIllegalUrlException | HttpStatusCodeException e) { - logger.error("Failed to GET entity/entities on backend by url '{}'", url); - throw new NexusGetException("An error occurred while GET entity/entities", e); - } catch (NexusResourceNotFoundException e) { - logger.error("Failed to GET the entity on backend because the resource couldn't be found {}", url); - throw e; - } - } - - @Override - public Resource getFile(String url) throws NexusGetException, NexusResourceNotFoundException { - try { - HttpHeaders headers = new HttpHeaders(); - headers.setAccept(Collections.singletonList(MediaType.APPLICATION_OCTET_STREAM)); - return doRequest(url, HttpMethod.GET, createResponseType(Resource.class), null, headers); - } catch (NexusHttpException | NexusIllegalUrlException | HttpStatusCodeException e) { - logger.error("Failed to GET File entity/entities on backend by url '{}'", url); - throw new NexusGetException("An error occurred while GET an entity", e); - } catch (NexusResourceNotFoundException e) { - logger.error("Failed to GET File the entity on backend because the resource couldn't be found {}", url); - throw e; - } - } - - @Override - public T post(String url, Object data, ResponseType responseType) - throws NexusCreationException, NexusResourceExistsException { - try { - return doRequest(url, HttpMethod.POST, responseType, data, null); - } catch (NexusResourceExistsException e) { - throw e; - } catch (NexusHttpException | NexusIllegalUrlException | NexusResourceNotFoundException | HttpStatusCodeException e) { - logger.error("Failed to POST entity/entities on backend by url '{}'", url); - throw new NexusCreationException("An error occurred while POST an entity", e); - } - } - - @Override - public T postFile(String url, Resource resource, ResponseType responseType) - throws NexusCreationException, NexusResourceExistsException { - try { - MultiValueMap map = new LinkedMultiValueMap<>(); - map.add("file", resource); - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.MULTIPART_FORM_DATA); - headers.add(HttpHeaders.USER_AGENT, userAgent); - return doRequest(url, HttpMethod.POST, responseType, map, headers); - } catch (NexusResourceExistsException e) { - throw e; - } catch (NexusHttpException | NexusIllegalUrlException | NexusResourceNotFoundException | HttpStatusCodeException e) { - logger.error("Failed to POST File entity/entities on backend by url '{}'", url); - throw new NexusCreationException("An error occurred while POST an entity", e); - } - } - - @Override - public T put(String url, Object data, ResponseType responseType) - throws NexusModificationException, NexusResourceExistsException, NexusResourceNotFoundException { - try { - return doRequest(url, HttpMethod.PUT, responseType, data, null); - } catch (NexusResourceExistsException e) { - logger.error("Failed to PUT the entity on backend due to conflict (url '{}')", url); - throw e; - } catch (NexusHttpException | NexusIllegalUrlException | HttpStatusCodeException e) { - logger.error("Failed to PUT entity/entities on backend by url '{}'", url); - throw new NexusModificationException("An error occurred while PUT an entity", e); - } catch (NexusResourceNotFoundException e) { - logger.error("Failed to PUT the entity on backend because the resource couldn't be found {}", url); - throw e; - } - } - - @Override - public T putFile(String url, Resource resource, ResponseType responseType) - throws NexusResourceNotFoundException, NexusModificationException, NexusResourceExistsException { - try { - MultiValueMap map = new LinkedMultiValueMap<>(); - map.add("file", resource); - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.MULTIPART_FORM_DATA); - headers.add(HttpHeaders.USER_AGENT, userAgent); - return doRequest(url, HttpMethod.PUT, responseType, map, headers); - } catch (NexusResourceExistsException e) { - throw e; - } catch (NexusHttpException | NexusIllegalUrlException | HttpStatusCodeException e) { - logger.error("Failed to PUT File entity/entities on backend by url '{}'", url); - throw new NexusModificationException("An error occurred while PUT an entity", e); - } catch (NexusResourceNotFoundException e) { - logger.error("Failed to PUT File the entity on backend because the resource couldn't be found {}", url); - throw e; - } - } - - @Override - public void delete(String url) throws NexusDeleteException, NexusResourceNotFoundException { - try { - doRequest(url, HttpMethod.DELETE, EMPTY_RESPONSE, null, null); - } catch (NexusHttpException | NexusIllegalUrlException | HttpStatusCodeException e) { - logger.error("Failed to DELETE entity/entities on backend by url '{}'", url); - throw new NexusDeleteException("An error occurred while DELETE entity/entities", e); - } catch (NexusResourceNotFoundException e) { - logger.error("Failed to DELETE the entity on backend because the resource couldn't be found {}", url); - throw e; - } - } - - @Override - public T patch(String url, Object data, ResponseType responseType) - throws NexusModificationException, NexusResourceExistsException, NexusResourceNotFoundException { - try { - return doRequest(url, HttpMethod.PATCH, responseType, data, null); - } catch (NexusResourceExistsException e) { - logger.error("Failed to PATCH the entity on backend due to conflict (url '{}')", url); - throw e; - } catch (NexusHttpException | NexusIllegalUrlException | HttpStatusCodeException e) { - logger.error("Failed to PATCH entity/entities on backend by url '{}'", url); - throw new NexusModificationException("An error occurred while PATCH an entity", e); - } catch (NexusResourceNotFoundException e) { - logger.error("Failed to PATCH the entity on backend because the resource couldn't be found {}", url); - throw e; - } - } - - @Override - public T patchFile(String url, Resource resource, ResponseType responseType) - throws NexusResourceNotFoundException, NexusModificationException, NexusResourceExistsException { - try { - MultiValueMap map = new LinkedMultiValueMap<>(); - map.add("file", resource); - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.MULTIPART_FORM_DATA); - headers.add(HttpHeaders.USER_AGENT, userAgent); - return doRequest(url, HttpMethod.PATCH, responseType, map, headers); - } catch (NexusResourceExistsException e) { - throw e; - } catch (NexusHttpException | NexusIllegalUrlException | HttpStatusCodeException e) { - logger.error("Failed to PATCH File entity/entities on backend by url '{}'", url); - throw new NexusModificationException("An error occurred while PATCH an entity", e); - } catch (NexusResourceNotFoundException e) { - logger.error("Failed to PATCH File the entity on backend because the resource couldn't be found {}", url); - throw e; - } - } - - @SuppressWarnings("unchecked") - public T doRequest(String url, HttpMethod method, ResponseType responseType, Object body, HttpHeaders headers) - throws NexusResourceNotFoundException, NexusHttpException, NexusIllegalUrlException { - try { - ParameterizedTypeReference typeReference = responseType.getResponseParameterizedTypeReference(); - if (typeReference != null) { - return handleResponse(restOperations.exchange(getBackendURL(url), method, createRequestEntity(body, headers), typeReference)); - } else { - Class responseClass = responseType.getResponseClass(); - return handleResponse(restOperations.exchange(getBackendURL(url), method, createRequestEntity(body, headers), responseClass)); - } - } catch (HttpStatusCodeException e) { - // WARN RestClientResponseException use now the default Charset UTF-8 vs ISO_8859_1 in Spring < 5.1.18 - if (isHandleHttpStatus(e.getStatusCode())) { - // Test the ResponseHeaders and the Content-Type - HttpHeaders responseHeaders = e.getResponseHeaders(); - if (responseHeaders == null) responseHeaders = new HttpHeaders(); - if (responseHeaders.getContentType() == null) responseHeaders.set("Content-Type", MediaType.APPLICATION_JSON_VALUE); - return handleResponse(new ResponseEntity<>( - (T) e.getResponseBodyAsByteArray(), e.getResponseHeaders(), e.getStatusCode())); - } - // handle the default ErrorMessage or an Exception... - return handleResponseError(url, e); - } - } - - private final static EnumSet listHttpStatusError = EnumSet.of( - HttpStatus.BAD_REQUEST, - HttpStatus.UNAUTHORIZED, - HttpStatus.METHOD_NOT_ALLOWED, - HttpStatus.INTERNAL_SERVER_ERROR); - - /** - * Returns true if a specific HttpStatus is contained in th list - * - * @param status Ht tpStatus - * @return true Returns true if HttpStatus has to be handled - */ - private boolean isHandleHttpStatus(HttpStatus status) { - return isHandleBackendEntity && listHttpStatusError.contains(status); - } - - /** - * Returns the final request url to connect to the backend. - * The url will be composed with the backend context and the specified url. - * - * @param url The url to finalize - * @return The finalized url to connect to the backend - * @throws NexusIllegalUrlException When the url is empty - */ - private String getBackendURL(String url) throws NexusIllegalUrlException { - if (ObjectUtils.isEmpty(url)) throw new NexusIllegalUrlException("The parameter 'url' should not be empty!"); - if (!url.startsWith("/")) url = "/" + url; - logger.debug("BackendURL: {}", backendURL + url); - return backendURL + url; - } - - @SuppressWarnings("unchecked") - private T handleResponse(ResponseEntity exchange) { - T responseBody = exchange.getBody(); - HttpStatus httpStatus = exchange.getStatusCode(); - HttpHeaders httpHeaders = exchange.getHeaders(); - if (logger.isDebugEnabled()) logger(httpHeaders, responseBody, httpStatus, maxLengthTruncated, truncated); - if (isHandleBackendEntity || isHandleHttpStatus(httpStatus)) return (T) new EntityBackend<>(responseBody, httpHeaders, httpStatus); - if (responseBody == null) return (T) httpStatus; - return responseBody; - } - - private static void logger(HttpHeaders httpHeaders, T responseBody, HttpStatus httpStatus, int maxLengthTruncated, boolean truncated) { - logger.debug("Headers response: {}", httpHeaders); - if (responseBody != null) { - if (responseBody instanceof Resource) { - Resource resource = (Resource) responseBody; - String body = null; - try { - body = StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8); - } catch (IOException e) { - logger.error("Resource not readable: {}", e.getMessage()); - } finally { - logger.debug("The response is: {} {} {}", httpStatus, resource.getDescription(), LogFormatUtils.formatValue(body, maxLengthTruncated, truncated)); - } - } else { - logger.debug("The response is: {} {}", httpStatus, LogFormatUtils.formatValue(responseBody, maxLengthTruncated, truncated)); - } - } else { - logger.debug("The response is empty with HttpState: {}", httpStatus); - } - } - - private T handleResponseError(String url, HttpStatusCodeException e) throws NexusResourceNotFoundException, NexusHttpException { - switch (e.getStatusCode()) { - case NOT_FOUND: throw new NexusResourceNotFoundException("Failed to request to the backend. Resource not found. URI: " + url); - case CONFLICT: throw new NexusResourceExistsException("Resource already exist. URI: " + url); - case METHOD_NOT_ALLOWED: - case UNAUTHORIZED: - case INTERNAL_SERVER_ERROR: - try { - // ErrorMessage from the Backend! - final IBackendErrorMessage errorMessage = objectMapper.readValue(e.getResponseBodyAsByteArray(), errorClass); - logger.info("The request to the backend failed. Reason id '{}: {}' Details: {} ", e.getStatusCode(), e.getStatusText(), errorMessage); - } catch (Exception jx) { - // Unable to parse response body - logger.info("The request to the backend failed. URI: {} Reason id '{}: {}' Message: {}", url, e.getStatusCode(), e.getStatusText(), jx.getMessage()); - } - if (e.getStatusCode() != HttpStatus.BAD_REQUEST) { - throw new NexusHttpException("An internal error occurred on the backend. URI: " + url + " Reason id '" + e.getStatusCode()+ "'"); - } - default: - throw e; - } - } - - - /** - * Default allowed List of Headers can be forwarded from the Client to the Backend Server - */ - private final static List STATIC_FORWARDED_HEADERS = - List.of( - // X-Forwarding - "X-Forwarded-For", - "X-Forwarded-Proto", - "X-Forwarded-Host", - // Tracing & ID - "X-APP-REQUEST-ID", - HttpHeaders.AUTHORIZATION, - // Body & Content Negotiation - HttpHeaders.CONTENT_TYPE, - HttpHeaders.ACCEPT, - HttpHeaders.ACCEPT_LANGUAGE, - // Partial content handling downloads & uploads (Client -> Server) - HttpHeaders.RANGE, - HttpHeaders.CONTENT_RANGE, - // Caching - HttpHeaders.IF_NONE_MATCH, - HttpHeaders.IF_MODIFIED_SINCE - ); - - - /** - * Create a new ${@link RequestEntity} with additional user and product headers - * - * @param body Object - * @param incomingHeaders HttpHeaders - * @return RequestEntity - */ - public HttpEntity createRequestEntity(Object body, HttpHeaders incomingHeaders) { - HttpHeaders headers; - if (removeHeaders) { - headers = new HttpHeaders(); - headers.add(HttpHeaders.USER_AGENT, userAgent); // mandatory forced, some RestApi filter the User-Agent! - } else { - headers = (incomingHeaders != null) ? new HttpHeaders(incomingHeaders) : new HttpHeaders(); - } - - // Transfer X-Forwarded headers and configured client headers - if (forwardedHeaders && incomingHeaders != null) { - for (String headerName : forwardedClientHeaders) { - if (incomingHeaders.containsKey(headerName)) { - List headerNames = incomingHeaders.get(headerName); - if (headerNames != null) { - headers.put(headerName, new ArrayList<>(headerNames)); - } - } - } - } - - if (headers.getContentType() == null) { - headers.setContentType(MediaType.APPLICATION_JSON); // mandatory forced! - } - - // Some RestApi can filter the Host header! (localhost) - if (removeHostHeader) headers.remove(HttpHeaders.HOST); - if (removeOriginHeader) headers.remove(HttpHeaders.ORIGIN); - - // Basic Authentication, Bearer Authentication and Cookies - // WARN BasicAuth Not UTF-8! see https://datatracker.ietf.org/doc/html/rfc7617#page-14 - if (!ObjectUtils.isEmpty(username) && !ObjectUtils.isEmpty(password)) - headers.setBasicAuth(HttpHeaders.encodeBasicAuth(username, password, StandardCharsets.ISO_8859_1)); - if (!ObjectUtils.isEmpty(bearer)) - headers.setBearerAuth(bearer); - if (!ObjectUtils.isEmpty(cookie)) - headers.add(HttpHeaders.COOKIE, cookie); - - logger.debug("Requested Headers: {}", headers); - if (body != null) return new HttpEntity<>(body, headers); - return new HttpEntity<>(headers); - } - - @Override - public ResponseType createResponseType(Class responseType) { - Assert.notNull(responseType, "responseType may not be null!"); - return new ResponseTypeImpl<>(responseType); - } - - @Override - public ResponseType createResponseType(ParameterizedTypeReference responseType) { - Assert.notNull(responseType, "responseType may not be null!"); - return new ResponseTypeImpl<>(responseType); - } - - @Override - public String getBackendURL() { - return this.backendURL; - } - - @Override - public boolean isRemovedHeaders() { - return this.removeHeaders || this.removeHostHeader || this.removeOriginHeader; - } - - private static class ResponseTypeImpl implements ResponseType { - - private final Class responseClass; - private final ParameterizedTypeReference parameterizedType; - - private ResponseTypeImpl() { this(null, null); } - - ResponseTypeImpl(Class responseClass) { this(responseClass, null); } - - ResponseTypeImpl(ParameterizedTypeReference parameterizedType) { this(null, parameterizedType); } - - private ResponseTypeImpl(Class responseClass, ParameterizedTypeReference parameterizedType) { - this.responseClass = responseClass; - this.parameterizedType = parameterizedType; - } - - @Override - public Class getResponseClass() { return responseClass; } - - @Override - public ParameterizedTypeReference getResponseParameterizedTypeReference() { return parameterizedType; } - } - - public static class EntityBackend { - private final T body; - private final HttpHeaders headers; - private final HttpStatus status; - - public EntityBackend(T body, HttpHeaders headers, HttpStatus status) { - this.body = body; - this.headers = headers; - this.status = status; - } - - public T getBody() { - return this.body; - } - - public HttpHeaders getHttpHeaders() { - return this.headers; - } - - public HttpStatus getStatus() { - return this.status; - } - } - -} diff --git a/src/main/java/com/jservlet/nexus/shared/service/backend/api/ErrorMessage.java b/src/main/java/com/jservlet/nexus/shared/service/backend/api/ErrorMessage.java deleted file mode 100644 index 757415a..0000000 --- a/src/main/java/com/jservlet/nexus/shared/service/backend/api/ErrorMessage.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (C) 2001-2024 JServlet.com Franck Andriano. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - */ - -package com.jservlet.nexus.shared.service.backend.api; - -import com.fasterxml.jackson.annotation.JsonInclude; - -import java.util.HashMap; -import java.util.Map; - -/** - * ErrorMessage from the Backend, example 401: {"code":"401","level":"ERROR","source":"MOCK-REST","message":"Unauthorized"} - */ -@JsonInclude(JsonInclude.Include.NON_NULL) -public class ErrorMessage implements IBackendErrorMessage { - - private String code; - private String level; - private String source; - private String message; - private String cause; - - @JsonInclude(JsonInclude.Include.NON_EMPTY) - private Map parameters = new HashMap<>(); - - public ErrorMessage() { - } - - public String getCode() { - return code; - } - - public void setCode(String code) { - this.code = code; - } - - public String getLevel() { - return level; - } - - public void setLevel(String level) { - this.level = level; - } - - public String getSource() { - return source; - } - - public void setSource(String source) { - this.source = source; - } - - public String getMessage() { - return message; - } - - public void setMessage(String message) { - this.message = message; - } - - public String getCause() { - return cause; - } - - public void setCause(String cause) { - this.cause = cause; - } - - public Map getParameters() { - return parameters; - } - - public void setParameters(Map parameters) { - this.parameters = parameters; - } - - @Override - public String toString() { - return "ErrorMessage{" + - "code='" + code + '\'' + - ", level='" + level + '\'' + - ", source='" + source + '\'' + - ", message='" + message + '\'' + - ", cause='" + cause + '\'' + - ", parameters=" + parameters + - '}'; - } -} diff --git a/src/main/java/com/jservlet/nexus/shared/service/backend/api/IBackendErrorMessage.java b/src/main/java/com/jservlet/nexus/shared/service/backend/api/IBackendErrorMessage.java deleted file mode 100644 index d4c3686..0000000 --- a/src/main/java/com/jservlet/nexus/shared/service/backend/api/IBackendErrorMessage.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.jservlet.nexus.shared.service.backend.api; - -/** - * Generic Interface ErrorMessage managed by the Backend service. - */ -public interface IBackendErrorMessage { - // log - String toString(); -} diff --git a/src/main/java/com/jservlet/nexus/shared/web/controller/ApiBase.java b/src/main/java/com/jservlet/nexus/shared/web/controller/ApiBase.java deleted file mode 100644 index 875e9f0..0000000 --- a/src/main/java/com/jservlet/nexus/shared/web/controller/ApiBase.java +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Copyright (C) 2001-2024 JServlet.com Franck Andriano. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - */ - -package com.jservlet.nexus.shared.web.controller; - -import com.fasterxml.jackson.annotation.JsonInclude; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.security.access.AccessDeniedException; - -import java.io.Serializable; -import java.util.HashMap; -import java.util.Map; - -public abstract class ApiBase { - - private final String source; - - public ApiBase(String source) { this.source = source; } - - protected final ResponseEntity getResponseEntity(String code, Exception e) { - if (e instanceof AccessDeniedException) { - // let spring security (FilterChain) handle this - throw (AccessDeniedException) e; - } - String cause = e.getCause() != null ? e.getCause().getMessage() : "NA"; - Message message = getMessageObject(code, "ERROR"); - message.setMessage(e.getMessage()); - message.setCause(cause); - return new ResponseEntity<>(message, HttpStatus.INTERNAL_SERVER_ERROR); - } - - protected final ResponseEntity getResponseEntity(String code, String level, String msg, HttpStatus httpStatus) { - Message message = getMessageObject(code, level); - message.setMessage(msg); - return new ResponseEntity<>(message, httpStatus); - } - - protected final ResponseEntity getResponseEntity(String code, String level, Exception e, HttpStatus httpStatus) { - Message message = getMessageObject(code, level); - message.setMessage(e.getMessage()); - return new ResponseEntity<>(message, httpStatus); - } - - protected final ResponseEntity getResponseEntity(String code, String level, Exception e, HttpHeaders headers, HttpStatus httpStatus) { - Message message = getMessageObject(code, level); - message.setMessage(e.getMessage()); - return new ResponseEntity<>(message, headers, httpStatus); - } - - protected final ResponseEntity getResponseEntity(String code, String level, HttpStatus httpStatus) { - return new ResponseEntity<>(getMessageObject(code, level), httpStatus); - } - - protected final ResponseEntity getResponseEntity(T object, HttpStatus httpStatus) { - return new ResponseEntity<>(object, httpStatus); - } - - protected final ResponseEntity getResponseEntity(HttpStatus httpStatus) { - return new ResponseEntity<>(httpStatus); - } - - protected Message getMessageObject(String code, String level) { - return new Message(code, level, this.source); - } - - @JsonInclude(JsonInclude.Include.NON_NULL) - protected static class Message implements Serializable { - - private static final long serialVersionUID = -5490061086438597077L; - - private String code; - private String level; - private String source; - private String message; - private String cause; - - @JsonInclude(JsonInclude.Include.NON_EMPTY) - private Map parameters = new HashMap<>(); - - public Message(String code, String level, String source) { - this.code = code; - this.level = level; - this.source = source; - } - - public Message(String code, String level, String source, String message) { - this.code = code; - this.level = level; - this.source = source; - this.message = message; - } - - public String getCode() { - return code; - } - - public void setCode(String code) { - this.code = code; - } - - public String getLevel() { - return level; - } - - public void setLevel(String level) { - this.level = level; - } - - public String getSource() { - return source; - } - - public void setSource(String source) { - this.source = source; - } - - public String getMessage() { - return message; - } - - public void setMessage(String message) { - this.message = message; - } - - public String getCause() { - return cause; - } - - public void setCause(String cause) { - this.cause = cause; - } - - public Map getParameters() { - return parameters; - } - - public void setParameters(Map parameters) { - this.parameters = parameters; - } - - @Override - public String toString() { - return "Message{" + - "code='" + code + '\'' + - ", level='" + level + '\'' + - ", source='" + source + '\'' + - ", message='" + message + '\'' + - ", cause='" + cause + '\'' + - ", parameters=" + parameters + - '}'; - } - } -} diff --git a/src/main/java/com/jservlet/nexus/shared/web/controller/api/ApiBackend.java b/src/main/java/com/jservlet/nexus/shared/web/controller/api/ApiBackend.java deleted file mode 100644 index a39a805..0000000 --- a/src/main/java/com/jservlet/nexus/shared/web/controller/api/ApiBackend.java +++ /dev/null @@ -1,460 +0,0 @@ -/* - * Copyright (C) 2001-2025 JServlet.com Franck Andriano. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - */ - -package com.jservlet.nexus.shared.web.controller.api; - -import com.jservlet.nexus.shared.exceptions.NexusHttpException; -import com.jservlet.nexus.shared.exceptions.NexusIllegalUrlException; -import com.jservlet.nexus.shared.exceptions.NexusResourceNotFoundException; -import com.jservlet.nexus.shared.service.backend.BackendService; -import com.jservlet.nexus.shared.service.backend.BackendService.ResponseType; -import com.jservlet.nexus.shared.service.backend.BackendServiceImpl.EntityBackend; -import com.jservlet.nexus.shared.web.controller.ApiBase; -import io.swagger.v3.oas.annotations.Hidden; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.core.io.ByteArrayResource; -import org.springframework.core.io.Resource; -import org.springframework.http.*; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; -import org.springframework.security.web.util.matcher.OrRequestMatcher; -import org.springframework.security.web.util.matcher.RequestMatcher; -import org.springframework.util.*; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.context.request.NativeWebRequest; -import org.springframework.web.multipart.MultipartFile; -import org.springframework.web.multipart.MultipartRequest; -import org.springframework.web.servlet.HandlerMapping; -import org.springframework.http.HttpHeaders; - -import javax.annotation.PostConstruct; -import javax.servlet.http.HttpServletRequest; -import java.io.IOException; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; -import java.util.*; - - -/** - * Rest control ApiBackend, replicate all HttpRequests to the Backend Server.
- * Provides a full support for MultiPart HttpRequests and Parameters inside a form with - * Content-Type: multipart/form-data. - *

- * All HttpRequests methods: Get, Post, Post Multipart File, Put, Put Multipart File, - * Patch, Patch Multipart File, Delete - *
- * Full support Request Json Entity Object: application/json, application/x-www-form-urlencoded
- * Full support MultipartRequest Resources and Map parameters, embedded form Json Entity Object: multipart/form-data
- * Full support Response in Json Entity Object: application/json
- * Full support Response in ByteArray Resource file: application/octet-stream
- * Full support Streaming Http Response Json Entity Object: application/octet-stream, accept header Range bytes - *

- *

- * ApiBackend ResponseType is now a Resource ByteArray by default (see settings.properties). All is Bytes! - *
- * The ResourceMatchers Config can be configured on specific ByteArray Resources path
- * and on specific methods GET, POST, PUT, PATCH and Ant paths pattern:
- * nexus.backend.api-backend-resource.matchers.matchers1.method=GET
- * nexus.backend.api-backend-resource.matchers.matchers1.pattern=/api/encoding/**
- * nexus.backend.api-backend-resource.matchers.matchers2.method=GET
- * nexus.backend.api-backend-resource.matchers.matchers2.pattern=/api/decoding/**
- * etc... - *

- *

- * The Http Responses can be considerate as Resources, the Http header "Accept-Ranges: bytes" is injected and allow you to use - * the Http header 'Range:bytes=-1000' in the request and by example grabbed the last 1000 bytes (or a range of Bytes).
- * And the Http Responses will come back without a "Transfer-Encoding: chunked" HttpHeader cause now the header Content-Length - * is calculated. - *

- * For configure all the Responses in Resource put eh Method empty and use the path pattern=/api/**
- * nexus.backend.api-backend-resource.matchers.matchers1.method=
- * nexus.backend.api-backend-resource.matchers.matchers1.pattern=/api/**
- *

- * For remove the Http header "Transfer-Encoding: chunked" the header Content-Length need to be calculated, - * enable the ShallowEtagHeader Filter in the configuration for force to calculate the header Content-Length - * for all the Response Json Entity Object. - *

- * Activated by the key 'nexus.api.backend.enabled=true' in the Configuration properties. - */ -@RestController -@RequestMapping(value = "/api") -@ConditionalOnProperty(value = "nexus.api.backend.enabled") -@Hidden -public class ApiBackend extends ApiBase { - - private static final Logger logger = LoggerFactory.getLogger(ApiBackend.class); - - private static final String SOURCE = "REST-API-NEXUS-BACKEND"; - - private final BackendService backendService; - - private final ResourceMatchersConfig matchersConfig; - - private OrRequestMatcher orRequestResourceMatcher; - - public ApiBackend(BackendService backendService, ResourceMatchersConfig matchersConfig) { - super(SOURCE); - this.backendService = backendService; - this.matchersConfig = matchersConfig; - } - - /** - * Allow a list of headers transfer back from the Backend Server (see default headers ApiBackend.STATIC_TRANSFER_HEADERS) - * SpEL reads allow method delimited with a comma (or other character) and splits into a List of Strings - */ - @Value("#{'${nexus.api.backend.transfer.headers:}'.split(',')}") - private List transferHeaders; - - /** - * Forward some client infos to the Backend server - */ - @Value("${nexus.api.backend.x-forwarded-headers:true}") - private boolean xForwardedHeaders; - - /** - * Prepare matchers Methods and Ant paths pattern dedicated only for the Resources - */ - @PostConstruct - public void postConstruct() { - List requestMatchers = new ArrayList<>(); - Map map = matchersConfig.getMatchers(); - for (Map.Entry entry : map.entrySet()) { - requestMatchers.add(new AntPathRequestMatcher(entry.getValue().getPattern(), entry.getValue().getMethod())); - logger.info("Config ResourceMatchers: {} '{}'", entry.getValue().getMethod(), entry.getValue().getPattern()); - } - // Mandatory, not an empty RequestMatcher! - if (requestMatchers.isEmpty()) { - requestMatchers.add(new AntPathRequestMatcher("*/**", null)); - logger.warn("Config ResourceMatchers: No ByteArray Resource specified!"); - } - orRequestResourceMatcher = new OrRequestMatcher(requestMatchers); - - // Load transfer headers with preconfigured static headers - transferHeaders.addAll(STATIC_TRANSFER_HEADERS); - } - - /** - * Inner ConfigurationProperties keys prefixed with 'nexus.backend.api-backend-resource' and - * Lopping on incremental keys 'matchers.{name}[X].method' and 'matchers.{name}[X].pattern' - */ - @ConfigurationProperties("nexus.backend.api-backend-resource") - public static class ResourceMatchersConfig { - private final Map matchers = new HashMap<>(); - - public Map getMatchers() { - return matchers; - } - - public static class Matcher { - private String method; - private String pattern; - - public String getMethod() { - return method; - } - - public void setMethod(String method) { - this.method = method; - } - - public String getPattern() { - return pattern; - } - - public void setPattern(String pattern) { - this.pattern = pattern; - } - } - } - - /** - * Manage a Request Json Entity Object and a Request Map parameters.
- * Or a MultipartRequest encapsulated a List of BackendResource and a Request Map parameters, and form Json Entity Object
- * And return a ResponseEntity Json Entity Object or a ByteArray Resource file or any others content in ByteArray...
- *
- * For a @RequestMapping allow headers is set to GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS - * - * @param body String representing the RequestBody Object, just transfer the RequestBody - * @param method HttpMethod GET, POST, PUT, PATCH or DELETE - * @param request The current HttpServletRequest - * @param nativeWebRequest The current NativeWebRequest for get the MultipartRequest - * @return Object Return a ResponseEntity Object or a ByteArray Resource - * @throws IOException IO Exception when a MultiPartFiles fails - * @throws NexusHttpException Exception when a http request to the backend fails - * @throws NexusIllegalUrlException Exception when an illegal url will be requested - */ - @RequestMapping(value = "/**") - public final Object requestEntity(@RequestBody(required = false) String body, HttpMethod method, - HttpServletRequest request, NativeWebRequest nativeWebRequest) - throws NexusHttpException, NexusIllegalUrlException, IOException { - try { - // Any path within handler mapping without "api/" and with its query - String url = ((String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE)).replaceAll("api/", ""); - if (request.getQueryString() != null) url = url + "?" + request.getQueryString(); - - // Get the MultipartRequest from the NativeRequest! - MultipartRequest multipartRequest = nativeWebRequest.getNativeRequest(MultipartRequest.class); - // MultiValueMap store the MultiPartFiles and the Parameters Map - MultiValueMap map = processMapResources(multipartRequest, request.getParameterMap()); - - // Optimize logs writing, log methods can take time! - if (logger.isDebugEnabled()) { - logger.debug("Requested Url: {} '{}' args: {}, form: {}, body: {}, files: {}", - method, url, printQueryString(request.getQueryString()), printParameterMap(request.getParameterMap()), body, map.entrySet()); - } - - // Create a ResponseType Object or Resource by RequestMatcher - ResponseType responseType; - if (orRequestResourceMatcher.matches(request)) { - responseType = backendService.createResponseType(Resource.class); - } else { - responseType = backendService.createResponseType(Object.class); - } - - // Return an EntityBackend - Object obj = backendService.doRequest(url, method, responseType, - !map.isEmpty() ? map : body, getAllHeaders(request, xForwardedHeaders)); - - // Manage a Generics EntityBackend embedded a Json Entity Object or a Resource! - EntityBackend entityBackend = (EntityBackend) obj; - final HttpHeaders newHeaders = getBackendHeaders(entityBackend.getHttpHeaders()); - if (entityBackend.getBody() instanceof Resource) { - Resource resource = (Resource) entityBackend.getBody(); - return new ResponseEntity<>(resource, newHeaders, entityBackend.getStatus()); - } else { - return new ResponseEntity<>(entityBackend.getBody(), newHeaders, entityBackend.getStatus()); - } - } catch (NexusResourceNotFoundException e) { - // Return an error Message NOT_FOUND - return super.getResponseEntity("404", "ERROR", e, HttpStatus.NOT_FOUND); - } - } - - /** - * Get all Headers from the HttpServletRequest, apply ID headers set "X-ID" and inject headers X-Forwarded-* - */ - private static final String REQUEST_ID_HEADER = "APP-REQUEST-ID"; - - private static HttpHeaders getAllHeaders(HttpServletRequest request, boolean forwardHeaders) { - HttpHeaders headers = new HttpHeaders(); - Enumeration headerNames = request.getHeaderNames(); - while (headerNames.hasMoreElements()) { - String headerName = headerNames.nextElement(); - headers.add(headerName, request.getHeader(headerName)); - } - /*// Backup and remove the original Origin! - if (headers.getOrigin() != null) { - headers.set(HttpHeaders.ORIGIN + "-Client", headers.getOrigin()); - headers.setOrigin(null); // remove Origin - } - - // Set the current Request as Origin - headers.setOrigin(request.getRequestURL().toString());*/ - - // Injection headers X-Forwarded-* - if (forwardHeaders) { - // X-Forwarded-For Ip client - String clientIp = request.getRemoteAddr(); - String xffHeader = request.getHeader("X-Forwarded-For"); - if (xffHeader != null) { - headers.set("X-Forwarded-For", xffHeader + ", " + clientIp); - } else { - headers.set("X-Forwarded-For", clientIp); - } - - // X-Forwarded-Proto client - String protocol = request.getScheme(); // "http" ou "https" - headers.set("X-Forwarded-Proto", protocol); - - // X-Forwarded-Host origin client - String hostHeader = request.getHeader("Host"); - if (hostHeader != null) { - headers.set("X-Forwarded-Host", hostHeader); - } - } - - // Apply X-APP-REQUEST-ID - if (request.getAttribute(REQUEST_ID_HEADER) != null) { - headers.set("X-" + REQUEST_ID_HEADER, (String) request.getAttribute(REQUEST_ID_HEADER)); - } - return headers; - } - - - /** - * Default transfer List of Headers - */ - private final static List STATIC_TRANSFER_HEADERS = List.of( - // Date server - HttpHeaders.DATE, - // Caching - HttpHeaders.ETAG, - // Authentication - HttpHeaders.WWW_AUTHENTICATE - ); - - /** - * Transfer some headers from the Backend RestOperations, including by default the original Backend CONTENT_TYPE Server. - * And including all SET_COOKIE, Not CONTENT_LENGTH or TRANSFER_ENCODING or CONTENT_RANGE. Cause already sent in their own Context. - */ - private HttpHeaders getBackendHeaders(HttpHeaders readHeaders) { - HttpHeaders newHeaders = new HttpHeaders(); - if (readHeaders == null || readHeaders.isEmpty()) return newHeaders; - - // for propagation add Set-Cookie headers, avoid rules getFirst! - List setCookieHeaders = readHeaders.get(HttpHeaders.SET_COOKIE); - if (setCookieHeaders != null) { - for (String cookieHeaderValue : setCookieHeaders) { - newHeaders.add(HttpHeaders.SET_COOKIE, cookieHeaderValue); - } - } - - // set the Original CONTENT_TYPE for a Resource and its charset if it exists - if (readHeaders.getFirst(HttpHeaders.CONTENT_TYPE) != null) { - newHeaders.set(HttpHeaders.CONTENT_TYPE, readHeaders.getFirst(HttpHeaders.CONTENT_TYPE)); - } else {// ByteArray by default! - newHeaders.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE); - } - - // Transfer static headers and the others from the config - for (String headerName : transferHeaders) { - if (readHeaders.getFirst(headerName) != null) { - // add Date-Backend - if (HttpHeaders.DATE.equalsIgnoreCase(headerName)) { - newHeaders.add(HttpHeaders.DATE + "-Backend", readHeaders.getFirst(HttpHeaders.DATE)); - } - else { - List list = readHeaders.get(headerName); - if (list != null && !list.isEmpty()) { - for (String value : list) { - newHeaders.add(headerName, value); - } - } - } - } - } - return newHeaders; - } - - /** - * Print the parameterMap in a "Json" object style - */ - private static String printParameterMap(Map map) { - if (map == null || map.isEmpty()) return "{}"; - StringBuilder sb = new StringBuilder(); - Iterator> it = map.entrySet().iterator(); - sb.append("{"); - while (it.hasNext()) { - Map.Entry entry = it.next(); - sb.append("\"").append(entry.getKey()).append("\"").append(":"); - Iterator sIt = Arrays.asList(entry.getValue()).iterator(); - sb.append("["); - while (sIt.hasNext()) { - sb.append("\"").append(sIt.next()).append("\""); - if (sIt.hasNext()) sb.append(","); - } - sb.append("]"); - if (it.hasNext()) sb.append(","); - } - return sb + "}"; - } - - /** - * Print the raw queryString decoded - */ - private static String printRawQueryString(String queryString) { - return URLDecoder.decode(queryString, StandardCharsets.UTF_8); - } - - /** - * Print the queryString decoded - */ - private static String printQueryString(String queryString) { - if (queryString == null || queryString.isEmpty()) return "{}"; - Map map = new LinkedHashMap<>(); - StringTokenizer st = new StringTokenizer(queryString, "&"); - while (st.hasMoreTokens()) { - String token = st.nextToken(); - int idx = token.indexOf("="); - if (idx != -1) { - String key = idx > 0 ? token.substring(0, idx) : ""; - String value = (idx > 0 && token.length() > idx + 1) || (token.indexOf('=') == token.length()-1) || (token.indexOf('=') == 0) - ? token.substring(idx + 1) : token.substring(idx); - String[] values; - if (map.get(key) != null) { - values = appendInArray(map.get(key), URLDecoder.decode(value, StandardCharsets.UTF_8)); - } else { - if (!value.isEmpty()){ - values = new String[] { URLDecoder.decode(value, StandardCharsets.UTF_8) }; - } else { - values = new String[0]; - } - } - if (!key.isEmpty()) map.put(key, values); - } // none! - } - return printParameterMap(map); - } - - /** - * Append an element inside an Array - */ - private static T[] appendInArray(T[] array, T element) { - final int len = array.length; - array = Arrays.copyOf(array, len + 1); - array[len] = element; - return array; - } - - /** - * Prepare a LinkedMultiValueMap from a MultipartRequest - * And inject the parameterMap inside the LinkedMultiValueMap from a multipart HttpRequest. - * - * @param multipartRequest The MultipartRequest from the request. - * @param mapParams The parameter map from the request. - * @return A MultiValueMap containing the resources and parameters. - * @throws IOException If there is an issue reading the file content. - */ - private static MultiValueMap processMapResources(MultipartRequest multipartRequest, Map mapParams) throws IOException { - MultiValueMap linkedMap = new LinkedMultiValueMap<>(); - if (multipartRequest != null) { - MultiValueMap multiFileMap = multipartRequest.getMultiFileMap(); - for (Map.Entry> entry : multiFileMap.entrySet()) { - List files = entry.getValue(); - for (MultipartFile file : files) { - ByteArrayResource resource = new ByteArrayResource(file.getBytes()) { - @Override - public String getFilename() { - return file.getOriginalFilename(); - } - }; - linkedMap.add(entry.getKey(), resource); - } - } - // Inject all the parameterMap now! - for (String key : mapParams.keySet()) { - linkedMap.addAll(key, Arrays.asList(mapParams.get(key))); // MultiValue - } - } - return linkedMap; - } - -} diff --git a/src/main/java/com/jservlet/nexus/shared/web/filter/HTTPMethodOverrideFilter.java b/src/main/java/com/jservlet/nexus/shared/web/filter/HTTPMethodOverrideFilter.java deleted file mode 100644 index 8b8f45c..0000000 --- a/src/main/java/com/jservlet/nexus/shared/web/filter/HTTPMethodOverrideFilter.java +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright (C) 2001-2024 JServlet.com Franck Andriano. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - */ - -package com.jservlet.nexus.shared.web.filter; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.http.HttpMethod; -import org.springframework.lang.NonNull; -import org.springframework.stereotype.Component; -import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; -import org.springframework.web.filter.OncePerRequestFilter; - -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletRequestWrapper; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.util.*; - -/** - * WebFilter class that aims to remap blocked RequestMethod using an specialized http-header - * which contain the blocked (overridden) request method. - * The default herder name is HTTPMethodOverrideFilter.#DEFAULT_HEADER_NAME - *

- * POST: Allowed to override as PUT or PATCH request - * GET: Allowed to override as DELETE request - *

- * The header name as well as the method override mapping can be defined using the constructor - * HTTPMethodOverrideFilter#HTTPMethodOverrideFilter(string, map) - *

- * For example the method PUT is blocked .. - * Send the request with POST and the header X-HTTP-Method-Override: PUT - *

- * Activated WebFilter by only 'nexus.api.backend.filter.httpoverride.enabled=true' in the configuration - */ -@Component -@ConditionalOnProperty(value = "nexus.api.backend.filter.httpoverride.enabled") -public class HTTPMethodOverrideFilter extends OncePerRequestFilter { - - private static final Logger logger = LoggerFactory.getLogger(HTTPMethodOverrideFilter.class); - - private static final String DEFAULT_HEADER_NAME = "X-HTTP-Method-Override"; - - private static final Map> DEFAULT_METHOD_MAPPING; - - static { - final Map> defaultMethodMapping = new HashMap<>(); - defaultMethodMapping.put(HttpMethod.POST, Set.of(HttpMethod.PATCH, HttpMethod.PUT)); - defaultMethodMapping.put(HttpMethod.GET, Set.copyOf(Collections.singletonList(HttpMethod.DELETE))); - DEFAULT_METHOD_MAPPING = Collections.unmodifiableMap(defaultMethodMapping); - } - - private final String headerName; - - private final Map> methodMapping; - - public HTTPMethodOverrideFilter() { this(DEFAULT_HEADER_NAME, DEFAULT_METHOD_MAPPING); } - - public HTTPMethodOverrideFilter(String headerName, Map> methodMapping) { - Assert.hasText(headerName, "headerName may not be null!"); - this.headerName = headerName; - this.methodMapping = convertMethodMapping(methodMapping); - logger.info("Started MethodOverride OncePerRequest Filter"); - } - - private Map> convertMethodMapping(Map> methodMapping) { - - if (methodMapping != null && !methodMapping.isEmpty()) { - final Map> converted = new HashMap<>(); - for (Map.Entry> entry : methodMapping.entrySet()) { - final Set overriddenHttpMethods = entry.getValue(); - if (overriddenHttpMethods != null && !overriddenHttpMethods.isEmpty()) { - Set convertedOverridings = new HashSet<>(overriddenHttpMethods.size()); - for (HttpMethod method : overriddenHttpMethods) { - convertedOverridings.add(method.name()); - } - converted.put(entry.getKey().name(), convertedOverridings); - } - } - if (!converted.isEmpty()) - return converted; - } - return null; - } - - - @Override - protected void doFilterInternal(HttpServletRequest request, @NonNull HttpServletResponse response, - @NonNull FilterChain filterChain) throws ServletException, IOException { - String overriddenMethod = request.getHeader(headerName); - if (shouldApplyMethodOverriding(overriddenMethod, request)) { - filterChain.doFilter(new HttpMethodRequestWrapper(request, overriddenMethod), response); - } else { - filterChain.doFilter(request, response); - } - } - - private boolean shouldApplyMethodOverriding(String overriddenMethod, HttpServletRequest request) { - if (methodMapping != null) { - if (!ObjectUtils.isEmpty(overriddenMethod)) { - final String requestMethod = request.getMethod(); - final Set allowedOverridingMethod = methodMapping.get(requestMethod); - if (allowedOverridingMethod != null) { - if (allowedOverridingMethod.contains(overriddenMethod)) { - logger.debug("Valid overridden http-request-method found for this request {}. Method id '{}'", - request.getRequestURI(), overriddenMethod); - return true; - } - logger.debug("Invalid overridden http-request-method detected for request {} {}."+ - "Method id '{}' but should be one of this '{}'", requestMethod, - request.getRequestURI(), overriddenMethod, allowedOverridingMethod); - - } else if (logger.isDebugEnabled()) { - logger.debug("No overridden http-request-method allowed for request {} {}. But found: {} ", - requestMethod, request.getRequestURI(), overriddenMethod); - } - } else if (logger.isDebugEnabled()) { - logger.debug("No overridden http-request-method found for request {} {}", request.getMethod(), - request.getRequestURI()); - } - } - return false; - } - - private static class HttpMethodRequestWrapper extends HttpServletRequestWrapper { - - private final String method; - - HttpMethodRequestWrapper(HttpServletRequest request, String method) { - super(request); - this.method = method; - } - - public String getMethod() { - return this.method; - } - } -} diff --git a/src/main/java/com/jservlet/nexus/shared/web/filter/WAFFilter.java b/src/main/java/com/jservlet/nexus/shared/web/filter/WAFFilter.java deleted file mode 100644 index 0e83e5b..0000000 --- a/src/main/java/com/jservlet/nexus/shared/web/filter/WAFFilter.java +++ /dev/null @@ -1,503 +0,0 @@ -/* - * Copyright (C) 2001-2025 JServlet.com Franck Andriano. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - */ - -package com.jservlet.nexus.shared.web.filter; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.jservlet.nexus.shared.web.controller.ApiBase; -import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.core.log.LogFormatUtils; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.lang.NonNull; -import org.springframework.security.web.firewall.RequestRejectedException; -import org.springframework.stereotype.Component; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; -import org.springframework.web.multipart.MultipartFile; -import org.springframework.web.multipart.MultipartRequest; - -import javax.annotation.PostConstruct; -import javax.servlet.*; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.util.*; -import java.util.regex.Pattern; - -/** - * WebFilter class implements a secure WAF protection for request Body.
- * Http request Cookies, Headers, Parameters and Body can be filtered. - *

- * Un-normalized requests are automatically rejected by the StrictHttpFirewall, - * and path parameters and duplicate slashes are removed for matching purposes.
- * Noted the valid characters are defined in RFC 7230 and RFC 3986 are checked - * by the Apache Coyote http11 processor (see coyote Error parsing HTTP request header)
- *

- * Default reactive mode is STRICT mode - *

    - *
  • STRICT: StrictHttpFirewall + Rejects requests with malicious patterns.
  • - *
  • PASSIVE: StrictHttpFirewall + Cleans malicious patterns from request body and parameters.
  • - *
  • UNSAFE: StrictHttpFirewall + No checks on request body.
  • - *
- *

- * Activated WebFilter by only 'nexus.api.backend.filter.waf.enabled=true' in the configuration - */ -@Component -@ConditionalOnProperty(value = "nexus.api.backend.filter.waf.enabled") -public class WAFFilter extends ApiBase implements Filter { - - private static final Logger logger = LoggerFactory.getLogger(WAFFilter.class); - - private static final String SOURCE = "INTERNAL-WAF-NEXUS-BACKEND"; - - public enum Reactive { - STRICT, // Rejects requests with malicious patterns. - PASSIVE, // Cleans malicious patterns from the request. - UNSAFE // Performs no checks on the request body. - } - - @Value("${nexus.api.backend.filter.waf.reactive.mode:STRICT}") - private Reactive reactiveMode; - - @Value("${nexus.api.backend.filter.waf.deepscan.cookie:false}") - private boolean isDeepScanCookie; - - // Max WAF file scan limit (ex: 15MB) to prevent RAM OutOfMemory (DoS attack) - @Value("${nexus.api.backend.filter.waf.maxInMemoryFileSize:15728640}") - private long maxInMemoryFileSize = 15 * 1024 * 1024; - - private final WAFPredicate wafPredicate; - private final ObjectMapper objectMapper; - - // Set xml MimeTypes - private final Set xmlMimeTypes = new HashSet<>(); - - // Map magic numbers binaries files - private final Map magicNumbers = new HashMap<>(); - - public WAFFilter(WAFPredicate wafPredicate, ObjectMapper objectMapper) { - super(SOURCE); - this.wafPredicate = wafPredicate; - this.objectMapper = objectMapper; - } - - @PostConstruct - private void postConstruct() { - xmlMimeTypes.addAll(Arrays.asList("image/svg+xml", "text/xml", "application/xml", "application/vnd.mozilla.xul+xml")); - - magicNumbers.put("FFD8FF", "image/jpeg"); - magicNumbers.put("47494638", "image/gif"); - magicNumbers.put("89504E47", "image/png"); // Simplified PNG magic number for matching - magicNumbers.put("424D", "image/bmp"); - magicNumbers.put("255044462D", "application/pdf"); - magicNumbers.put("504B0304", "application/zip"); - } - - @Override - public void init(FilterConfig filterConfig) { - logger.info("Starting WAF Filter with reactive mode: {}", reactiveMode); - } - - @Override - public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) - throws IOException, ServletException { - final HttpServletRequest req = (HttpServletRequest) request; - final HttpServletResponse resp = (HttpServletResponse) response; - - try { - if (reactiveMode == Reactive.UNSAFE) { // WARN UNSAFE mode bypasses all checks. - chain.doFilter(req, resp); - return; - } - - // Wrap the request to allow reading input stream multiple times and modifying parameters - WAFRequestWrapper wrappedRequest = new WAFRequestWrapper(req); - - // Validate Hostname & User-Agent (Gateways must check Host headers) - validateHostAndUserAgent(wrappedRequest); - - // Wrap & Scan Multipart Files safely - HttpServletRequest processedRequest = scanAndWrapMultipartFiles(wrappedRequest); - - // Apply WAF Policies - if (reactiveMode == Reactive.STRICT) { - handleStrict(processedRequest); - } else if (reactiveMode == Reactive.PASSIVE) { - handlePassive(processedRequest); - } - - // Continue the filter chain with the (potentially wrapped) request. - chain.doFilter(processedRequest, response); - - } catch (RequestRejectedException ex) { - handleRequestRejected(ex, req, resp); - } - } - - /** - * Scans and wraps multipart files. Reads the file content into memory to - * avoid FileNotFoundException in subsequent filters or controllers. - * - * @param request The current HttpServletRequest. - * @return A new HttpServletRequest wrapper with processed multipart files. - * @throws IOException IO error - */ - private HttpServletRequest scanAndWrapMultipartFiles(HttpServletRequest request) throws IOException { - if (request instanceof MultipartRequest) { - MultipartRequest originalMultipartRequest = (MultipartRequest) request; - MultiValueMap newMultipartFiles = new LinkedMultiValueMap<>(); - - for (Map.Entry> entry : originalMultipartRequest.getMultiFileMap().entrySet()) { - for (MultipartFile file : entry.getValue()) { - if (file.isEmpty()) continue; - - // ANTI-DOS: Prevent loading massive files into RAM - if (file.getSize() > maxInMemoryFileSize) { - throw new RequestRejectedException("Request rejected: File size exceeds WAF inspection limit."); - } - // Check magic numbers and file type - byte[] fileContent = file.getBytes(); - String declaredContentType = file.getContentType(); - - // Magic Number Validation - if (fileContent.length >= 4) { - String hexHeader = bytesToHex(Arrays.copyOfRange(fileContent, 0, 8)); - String magicMimeType = magicNumbers.entrySet().stream() - .filter(e -> hexHeader.toUpperCase().startsWith(e.getKey())) - .map(Map.Entry::getValue) - .findFirst().orElse(null); - - if (magicMimeType != null && declaredContentType != null && !magicMimeType.equals(declaredContentType)) { - throw new RequestRejectedException("Request rejected: Mime-type spoofing detected for file '" + file.getOriginalFilename() + "'."); - } - } - - // XML/SVG Injection Validation - if (declaredContentType != null && xmlMimeTypes.contains(declaredContentType.toLowerCase())) { - String content = new String(fileContent, StandardCharsets.UTF_8); - // Using WAF Parameter validation for file content - if (!wafPredicate.getWAFParameterValues().test(content)) { - throw new RequestRejectedException("Request rejected: Disallowed pattern found in XML file '" + file.getOriginalFilename() + "'."); - } - } - - newMultipartFiles.add(entry.getKey(), new WAFTempMultipartFile(file.getName(), file.getOriginalFilename(), declaredContentType, fileContent)); - } - } - return new WAFMultipartRequestWrapper(request, newMultipartFiles); - } - return request; - } - - /** - * Handles STRICT mode. Rejects requests on pattern match. - * - * @param request The processed HttpServletRequest. - */ - private void handleStrict(HttpServletRequest request) throws IOException { - // Validate http headers (Critical for Log4Shell/SpEL/Deserialization) - Enumeration headerNames = request.getHeaderNames(); - while (headerNames != null && headerNames.hasMoreElements()) { - String headerName = headerNames.nextElement(); - if (!wafPredicate.getWAFHeaderNames().test(headerName)) { - throw new RequestRejectedException("Request rejected: Disallowed pattern in Header name."); - } - Enumeration headers = request.getHeaders(headerName); - while (headers != null && headers.hasMoreElements()) { - if (!wafPredicate.getWAFHeaderValues().test(headers.nextElement())) { - throw new RequestRejectedException("Request rejected: Disallowed pattern in Header value (" + headerName + ")."); - } - } - } - - // Validate url / form parameters (XSS, SQLi in Query String) - for (Map.Entry entry : request.getParameterMap().entrySet()) { - if (!wafPredicate.getWAFParameterNames().test(entry.getKey())) { - throw new RequestRejectedException("Request rejected: Disallowed pattern in Parameter name."); - } - for (String value : entry.getValue()) { - if (!wafPredicate.getWAFParameterValues().test(value)) { - throw new RequestRejectedException("Request rejected: Disallowed pattern in Parameter value."); - } - } - } - - // Validate cookies - if (isDeepScanCookie) { - validateCookies(request); - } - - // Validate json/rest body - String body = IOUtils.toString(request.getReader()); - if (!StringUtils.isBlank(body)) { - // Note: Assume you added getWAFRestApiBody() in WAFPredicate as suggested, otherwise use getWAFParameterValues() - if (!wafPredicate.getWAFParameterValues().test(body)) { - throw new RequestRejectedException("Request rejected: Disallowed WAF pattern found in Request Body."); - } - } - } - - /** - * Handles PASSIVE mode. Cleans the request on pattern match. - * - * @param request The processed HttpServletRequest. - */ - private void handlePassive(HttpServletRequest request) throws IOException { - // Warning: Sanitizing input by stripping patterns (Regex replaceAll) can lead to bypasses (e.g. ", - HttpMethod.GET, backendService.createResponseType(Data.class), null, null); - System.out.println(objError); - } catch (NexusHttpException | NexusIllegalUrlException | HttpStatusCodeException e) { - System.out.println("Failed to GET EntityError entity/entities on backend: " + e.getMessage()); - } catch (NexusResourceNotFoundException ex) { - System.out.println("ResourceNotFound: " + ex.getMessage()); - } - } - - - @Test - public void testGetString() { - try { - // get String - String obj = backendService.doRequest("/mock/v1/dataList", HttpMethod.GET, - backendService.createResponseType(String.class), null, null); - System.out.println(obj); - } catch (NexusHttpException | NexusIllegalUrlException | HttpStatusCodeException e) { - System.out.println("Failed to GET String entity/entities on backend: " + e.getMessage()); - } catch (NexusResourceNotFoundException ex) { - System.out.println("ResourceNotFound: " + ex.getMessage()); - } - } - - @Test - public void testGetArrayList() { - try { - // get ArrayList - Object obj = backendService.doRequest("/mock/v1/dataList", HttpMethod.GET, - backendService.createResponseType(Object.class), null, null); - if (obj instanceof ArrayList) { - System.out.println(obj); - } - } catch (NexusHttpException | NexusIllegalUrlException | HttpStatusCodeException e) { - System.out.println("Failed to GET ArrayList entity/entities on backend: " + e.getMessage()); - } catch (NexusResourceNotFoundException ex) { - System.out.println("ResourceNotFound: " + ex.getMessage()); - } - } - - - @Test - public void testGetResource() { - try { - // get Resource file in ByteArray! - HttpHeaders headers = new HttpHeaders(); - headers.setAccept(Collections.singletonList(MediaType.APPLICATION_OCTET_STREAM)); - Resource resource = backendService.doRequest("/mock/v1/datafile", HttpMethod.GET, - backendService.createResponseType(Resource.class), null, headers); // WARN mandatory typed Resource.class - String data = StreamUtils.copyToString(resource.getInputStream(), Charset.defaultCharset()); - System.out.println(data); - } - catch (NexusHttpException | NexusIllegalUrlException | HttpStatusCodeException e) { - System.out.println("Failed to GET Resource entity/entities on backend: " + e.getMessage()); - } catch (NexusResourceNotFoundException | IOException e) { - System.out.println("ResourceNotFound: " + e.getMessage()); - } - } - - - @Test - public void testGetByteArray() { - try { - // get data in ByteArray! - byte[] obj = backendService.doRequest("/mock/v1/dataBytes", HttpMethod.GET, - backendService.createResponseType(byte[].class), null, null); // WARN mandatory typed byte[].class - System.out.println(new String(obj, StandardCharsets.UTF_8)); - } catch (NexusHttpException | NexusIllegalUrlException | HttpStatusCodeException e) { - System.out.println("Failed to GET Bytes entity/entities on backend: " + e.getMessage()); - } catch (NexusResourceNotFoundException ex) { - System.out.println("ResourceNotFound: " + ex.getMessage()); - } - } - - @Test - public void testEchoProxy() { - try { - HttpHeaders headers = new HttpHeaders(); - headers.add("Content-Type", MediaType.APPLICATION_FORM_URLENCODED_VALUE); - // get Echo data in ByteArray through the proxy! - byte[] obj = backendService.doRequest("/mock/v1/proxy", HttpMethod.POST, - backendService.createResponseType(byte[].class), "echo=Hello Proxy! Héhè hàhâ ", headers); // WARN mandatory typed byte[].class - System.out.println(new String(obj, StandardCharsets.UTF_8)); - } catch (NexusHttpException | NexusIllegalUrlException | HttpStatusCodeException e) { - System.out.println("Failed to POST Echo Bytes entity/entities on backend: " + e.getMessage()); - } catch (NexusResourceNotFoundException ex) { - System.out.println("ResourceNotFound: " + ex.getMessage()); - } - } - - @Test - public void testGetEchoEntityProxy() { - try { - // get Echo data in ByteArray through the proxy! - byte[] obj = backendService.doRequest("/mock/v1/echo?echo=Hello+Proxy!", HttpMethod.GET, - backendService.createResponseType(byte[].class), null, null); // WARN mandatory typed byte[].class - System.out.println(new String(obj, StandardCharsets.UTF_8)); - } catch (NexusHttpException | NexusIllegalUrlException | HttpStatusCodeException e) { - System.out.println("Failed to POST Echo Bytes entity/entities on backend: " + e.getMessage()); - } catch (NexusResourceNotFoundException ex) { - System.out.println("ResourceNotFound: " + ex.getMessage()); - } - } - - @Test - public void testPostEchoEntityProxy() { - try { - HttpHeaders headers = new HttpHeaders(); - headers.add("Content-Type", MediaType.APPLICATION_FORM_URLENCODED_VALUE); // WARN mandatory! - // get Echo data in ByteArray through the proxy! - byte[] obj = backendService.doRequest("/mock/v1/proxy", HttpMethod.POST, - backendService.createResponseType(byte[].class), "Hello Echo!", headers); // WARN mandatory typed byte[].class - System.out.println(new String(obj, StandardCharsets.UTF_8)); - } catch (NexusHttpException | NexusIllegalUrlException | HttpStatusCodeException e) { - System.out.println("Failed to POST Echo Bytes entity/entities on backend: " + e.getMessage()); - } catch (NexusResourceNotFoundException ex) { - System.out.println("ResourceNotFound: " + ex.getMessage()); - } - } - - @Test - public void testNotFoundGetEntity() { - try { - // get Object Entity - Object obj = backendService.doRequest("/mock/v1/dataNotFound", HttpMethod.GET, - backendService.createResponseType(Data.class), null, null); - System.out.println(obj); - } catch (NexusHttpException | NexusIllegalUrlException | HttpStatusCodeException e) { - System.out.println("Failed to GET Data entity/entities on backend: " + e.getMessage()); - } catch (NexusResourceNotFoundException ex) { - System.out.println("ResourceNotFound: " + ex.getMessage()); - } - } - - @Test - public void testXErrorBackend500() { - try { - // get Object Entity - Object obj = backendService.doRequest("/mock/v1/dataError500", HttpMethod.GET, - backendService.createResponseType(Data.class), null, null); - System.out.println(obj); - } catch (NexusHttpException | NexusIllegalUrlException | HttpStatusCodeException e) { - System.out.println("Failed to GET Data entity/entities on backend: " + e.getMessage()); - } catch (NexusResourceNotFoundException ex) { - System.out.println("ResourceNotFound: " + ex.getMessage()); - } - } - - @Test - public void testXErrorBackend401() { - try { - // get Object Entity - Object obj = backendService.doRequest("/mock/v1/dataError401", HttpMethod.GET, - backendService.createResponseType(Data.class), null, null); - System.out.println(obj); - } catch (NexusHttpException | NexusIllegalUrlException | HttpStatusCodeException e) { - System.out.println("Failed to GET Data entity/entities on backend: " + e.getMessage()); - } catch (NexusResourceNotFoundException ex) { - System.out.println("ResourceNotFound: " + ex.getMessage()); - } - } - - @Test - public void testXErrorBackend400() throws NexusGetException { - try { - // get Object Entity - Object obj = backendService.doRequest("/v1/dataError400", HttpMethod.GET, - backendService.createResponseType(Data.class), null, null); - System.out.println(obj); - } catch (NexusHttpException | NexusIllegalUrlException | HttpStatusCodeException e) { - System.out.println("Failed to GET Data entity/entities on backend: " + e.getMessage()); - } catch (NexusResourceNotFoundException ex) { - System.out.println("ResourceNotFound: " + ex.getMessage()); - //throw new NexusGetException(ex.getMessage()); - } - } - - -} diff --git a/src/test/java/com/jservlet/nexus/test/RestControllerTest.java b/src/test/java/com/jservlet/nexus/test/RestControllerTest.java deleted file mode 100644 index 1e76c44..0000000 --- a/src/test/java/com/jservlet/nexus/test/RestControllerTest.java +++ /dev/null @@ -1,286 +0,0 @@ -package com.jservlet.nexus.test; - -import com.jservlet.nexus.controller.MockController.Data; -import com.jservlet.nexus.shared.exceptions.*; -import com.jservlet.nexus.shared.service.backend.BackendService; -import com.jservlet.nexus.shared.service.backend.BackendService.ResponseType; -import com.jservlet.nexus.test.config.ApplicationTestConfig; -import junit.framework.TestCase; -import org.apache.commons.io.FileUtils; -import org.apache.commons.io.IOUtils; -import org.hamcrest.CoreMatchers; -import org.hamcrest.MatcherAssert; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ResourceLoaderAware; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.core.io.FileSystemResource; -import org.springframework.core.io.Resource; -import org.springframework.core.io.ResourceLoader; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.lang.NonNull; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.TestPropertySource; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.test.context.web.WebAppConfiguration; -import org.springframework.web.client.HttpClientErrorException; -import org.springframework.web.client.RestClientException; -import org.springframework.web.client.RestTemplate; - -import java.io.File; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.List; - -/** - * Run tests with a local Tomcat localhost:8082/nexus-backend - */ -@Ignore // Remove for unit test -@RunWith(SpringJUnit4ClassRunner.class) -@WebAppConfiguration -@ContextConfiguration(classes={ApplicationTestConfig.class}) -@TestPropertySource(properties = { "nexus.backend.url=http://localhost:8082/nexus-backend" }) -public class RestControllerTest extends TestCase implements ResourceLoaderAware { - - private static final Logger logger = LoggerFactory.getLogger(RestControllerTest.class); - - @Autowired - private BackendService backendService; - - private ResponseType NO_RESPONSE_TYPE; - private ResponseType DATA_RESPONSE_TYPE; - private ResponseType> DATA_LIST_RESPONSE_TYPE; - private ResponseType STATUS_RESPONSE_TYPE; - private ResponseType BOOL_RESPONSE_TYPE; - private ResponseType OBJECT_RESPONSE_TYPE; - private ResponseType BYTES_RESPONSE_TYPE; - - private ResourceLoader resourceLoader; - - private final static String pathImage = "/static/images"; - private final static String nameImage = "/logo-marianne.svg"; - - private final static String urlImage = pathImage + nameImage; - private final static String fileImage = System.getProperty("java.io.tmpdir") + nameImage; - - @Override - public void setResourceLoader(@NonNull ResourceLoader resourceLoader) { - this.resourceLoader = resourceLoader; - } - - @Before - public void setUp() { - NO_RESPONSE_TYPE = backendService.createResponseType(Void.class); - DATA_RESPONSE_TYPE = backendService.createResponseType(Data.class); - DATA_LIST_RESPONSE_TYPE = backendService.createResponseType(new ParameterizedTypeReference<>(){}); - BOOL_RESPONSE_TYPE = backendService.createResponseType(Boolean.class); - STATUS_RESPONSE_TYPE = backendService.createResponseType(HttpStatus.class); - OBJECT_RESPONSE_TYPE = backendService.createResponseType(Object.class); - BYTES_RESPONSE_TYPE = backendService.createResponseType(byte[].class); - } - - @Test - public void testGetBytesBackend() throws NexusGetException, NexusResourceNotFoundException { - String url = "/mock/v1/dataBytes"; - byte[] data = backendService.get(url, BYTES_RESPONSE_TYPE); - logger.debug(Arrays.toString(data)); - logger.debug(new String(data, StandardCharsets.UTF_8)); - MatcherAssert.assertThat(new String(data, StandardCharsets.UTF_8), - CoreMatchers.containsString("GET_BYTES")); - } - - @Test - public void testGetBackend() throws NexusGetException, NexusResourceNotFoundException { - String url = "/mock/v1/data"; - Data data = backendService.get(url, DATA_RESPONSE_TYPE); - assertNotNull(data); - } - - @Test - public void testGetListBackend() throws NexusGetException, NexusResourceNotFoundException { - String url = "/mock/v1/dataList"; - List list = backendService.get(url, DATA_LIST_RESPONSE_TYPE); - assertNotNull(list); - } - - @Test - public void testPostBackend() throws NexusGetException, NexusResourceNotFoundException, - NexusCreationException, NexusResourceExistsException { - String url = "/mock/v1/data"; - Data data = backendService.get(url, DATA_RESPONSE_TYPE); - Boolean status = backendService.post(url, data, BOOL_RESPONSE_TYPE); - assertNotNull(status); - assertTrue(status); - } - - @Test - public void testPutBackend() throws NexusGetException, NexusResourceNotFoundException, - NexusModificationException, NexusResourceExistsException { - String url = "/mock/v1/data"; - Data data = backendService.get(url, DATA_RESPONSE_TYPE); - Boolean status = backendService.put(url, data, BOOL_RESPONSE_TYPE); - assertNotNull(status); - assertTrue(status); - } - - @Test - public void testPatchBackend() throws NexusGetException, NexusResourceNotFoundException, - NexusModificationException, NexusResourceExistsException { - String url = "/mock/v1/data"; - Data data = backendService.get(url, DATA_RESPONSE_TYPE); - Boolean status = backendService.patch(url, data, BOOL_RESPONSE_TYPE); - assertNotNull(status); - assertTrue(status); - } - - @Test - public void testPatchFileBackend() throws NexusGetException, NexusResourceNotFoundException, - NexusModificationException, NexusResourceExistsException { - Resource image = backendService.getFile(urlImage); - Assert.assertNotNull(image); - if (image.exists()) { - String url = "/mock/v1/datafile"; - HttpStatus status = backendService.patchFile(url, new FileSystemResource(new File(fileImage)), STATUS_RESPONSE_TYPE); - assertNotNull(status); - assertTrue(status.is2xxSuccessful()); - } - } - - @Test - public void testGetFileBackend() throws NexusGetException, NexusResourceNotFoundException, IOException { - Resource image = backendService.getFile(urlImage); - logger.debug(String.valueOf(image.contentLength())); - logger.debug(image.getFilename()); - logger.debug(new String(IOUtils.toCharArray(image.getInputStream(), StandardCharsets.UTF_8))); - FileUtils.copyInputStreamToFile(image.getInputStream(), new File(fileImage)); - assertNotNull(image); - } - - @Test - public void testPostFileBackend() throws NexusGetException, NexusResourceNotFoundException, - NexusCreationException, NexusResourceExistsException { - Resource image = backendService.getFile(urlImage); - assertNotNull(image); - if (image.exists()) { - String url = "/mock/v1/datafile"; - HttpStatus status = backendService.postFile(url, new FileSystemResource(new File(fileImage)), STATUS_RESPONSE_TYPE); - assertNotNull(status); - assertTrue(status.is2xxSuccessful()); - } - } - - @Test - public void testPutFileBackend() throws NexusGetException, NexusResourceNotFoundException, - NexusModificationException, NexusResourceExistsException { - Resource image = backendService.getFile(urlImage); - Assert.assertNotNull(image); - if (image.exists()) { - String url = "/mock/v1/datafile"; - HttpStatus status = backendService.putFile(url, new FileSystemResource(new File(fileImage)), STATUS_RESPONSE_TYPE); - assertNotNull(status); - assertTrue(status.is2xxSuccessful()); - } - } - - @Test - public void testDeleteFileBackend() throws NexusResourceNotFoundException, NexusDeleteException { - String url = "/mock/v1/datafile"; - backendService.delete(url); - assertTrue(true); - } - - - @Test - public void testEchoBackend() throws NexusGetException, NexusResourceNotFoundException { - String url = "/mock/v1/echo"; - byte[] data = backendService.get(url, BYTES_RESPONSE_TYPE); - logger.debug(Arrays.toString(data)); - logger.debug(new String(data, StandardCharsets.UTF_8)); - MatcherAssert.assertThat(new String(data, StandardCharsets.UTF_8), - CoreMatchers.containsString("echo")); - } - - @Test - public void testEchoProxyBackend() throws NexusCreationException, NexusResourceExistsException { - String url = "/mock/v1/proxy"; - byte[] data = backendService.post(url, null, BYTES_RESPONSE_TYPE); - logger.debug(Arrays.toString(data)); - logger.debug(new String(data, StandardCharsets.UTF_8)); - MatcherAssert.assertThat(new String(data, StandardCharsets.UTF_8), - CoreMatchers.containsString("echo")); - } - - - /* Http Errors */ - @Test - public void testGetXssBackend() throws NexusResourceNotFoundException { - try { - String url = "/mock/v1/dataXss?param1="; // - backendService.get(url, OBJECT_RESPONSE_TYPE); - } catch (NexusGetException e) { - testException(e); - } - } - - private static void testException(NexusGetException e) { - if (e.getCause() instanceof HttpClientErrorException) { - HttpClientErrorException cause = (HttpClientErrorException) e.getCause(); - assertNotNull(cause.getStatusCode()); - logger.debug("Error: {} {}", cause.getStatusCode(), cause.getResponseBodyAsString()); - } else { - assertNotNull(e.getMessage()); - logger.debug("Error: {}", e.getCause().getMessage()); - } - } - - @Test - public void testPostXssBackend() throws NexusResourceExistsException { - try { - // try also security exception \\","info2", 0.0006); - backendService.post(url, data, OBJECT_RESPONSE_TYPE); - } catch (NexusCreationException e) { - assertNotNull(e.getMessage()); - logger.debug("Error: {}", e.getCause().getMessage()); - } - } - - @Test - public void testXErrorBackend400() throws NexusResourceNotFoundException { - try { - String url = "/mock/v1/dataError400"; - backendService.get(url, OBJECT_RESPONSE_TYPE); - } catch (NexusGetException e) { - testException(e); - } - } - - @Test - public void testXErrorBackend500() throws NexusResourceNotFoundException { - try { - String url = "/mock/v1/dataError500"; - backendService.get(url, OBJECT_RESPONSE_TYPE); - } catch (NexusGetException e) { - testException(e); - } - } - - @Test - public void testXErrorBackend401() throws NexusResourceNotFoundException { - try { - String url = "/mock/v1/dataError401"; - backendService.get(url, OBJECT_RESPONSE_TYPE); - } catch (NexusGetException e) { - testException(e); - } - } -} diff --git a/src/test/java/com/jservlet/nexus/test/config/ApplicationTestConfig.java b/src/test/java/com/jservlet/nexus/test/config/ApplicationTestConfig.java deleted file mode 100644 index 908f1db..0000000 --- a/src/test/java/com/jservlet/nexus/test/config/ApplicationTestConfig.java +++ /dev/null @@ -1,336 +0,0 @@ -package com.jservlet.nexus.test.config; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.jservlet.nexus.shared.service.backend.BackendService; -import com.jservlet.nexus.shared.service.backend.BackendServiceImpl; -import com.jservlet.nexus.shared.web.interceptor.CookieRedirectInterceptor; -import org.apache.commons.io.IOUtils; -import org.apache.http.Header; -import org.apache.http.HttpResponse; -import org.apache.http.ParseException; -import org.apache.http.client.config.CookieSpecs; -import org.apache.http.client.config.RequestConfig; -import org.apache.http.config.RegistryBuilder; -import org.apache.http.conn.HttpConnectionFactory; -import org.apache.http.conn.ManagedHttpClientConnection; -import org.apache.http.conn.routing.HttpRoute; -import org.apache.http.conn.socket.ConnectionSocketFactory; -import org.apache.http.conn.socket.PlainConnectionSocketFactory; -import org.apache.http.conn.ssl.SSLConnectionSocketFactory; -import org.apache.http.impl.DefaultConnectionReuseStrategy; -import org.apache.http.impl.DefaultHttpResponseFactory; -import org.apache.http.impl.client.DefaultConnectionKeepAliveStrategy; -import org.apache.http.impl.client.DefaultHttpRequestRetryHandler; -import org.apache.http.impl.client.HttpClientBuilder; -import org.apache.http.impl.client.LaxRedirectStrategy; -import org.apache.http.impl.conn.DefaultHttpResponseParserFactory; -import org.apache.http.impl.conn.ManagedHttpClientConnectionFactory; -import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; -import org.apache.http.impl.io.DefaultHttpRequestWriterFactory; -import org.apache.http.message.BasicHeader; -import org.apache.http.message.BasicLineParser; -import org.apache.http.protocol.HttpContext; -import org.apache.http.ssl.PrivateKeyDetails; -import org.apache.http.ssl.PrivateKeyStrategy; -import org.apache.http.ssl.SSLContexts; -import org.apache.http.util.CharArrayBuffer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.io.AbstractResource; -import org.springframework.core.io.Resource; -import org.springframework.http.*; -import org.springframework.http.client.*; -import org.springframework.http.converter.*; -import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; -import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; -import org.springframework.lang.NonNull; -import org.springframework.web.client.RestOperations; -import org.springframework.web.client.RestTemplate; - -import javax.net.ssl.SSLContext; -import java.io.*; -import java.net.Socket; -import java.security.KeyStore; -import java.util.*; -import java.util.concurrent.TimeUnit; - -import static java.nio.charset.StandardCharsets.UTF_8; - -@Configuration -@ComponentScan(basePackages = {"com.jservlet.nexus.shared.service"}) -public class ApplicationTestConfig { - - private static final Logger logger = LoggerFactory.getLogger(ApplicationTestConfig.class); - - @Bean - public BackendService backendService(@Value("${nexus.backend.url}") String backendUrl, - RestOperations restOperations, - ObjectMapper objectMapper) { - final BackendServiceImpl backendService = new BackendServiceImpl(); - backendService.setBackendURL(backendUrl); - backendService.setRestOperations(restOperations); - backendService.setObjectMapper(objectMapper); - return backendService; - } - - @Bean - public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter(ObjectMapper objectMapper) { - return new MappingJackson2HttpMessageConverter(objectMapper); - } - - @Bean - public ObjectMapper objectMapper() { - return new Jackson2ObjectMapperBuilder() - // fields not null globally! - .serializationInclusion(JsonInclude.Include.NON_NULL) - // to allow serialization of "empty" POJOs (no properties to serialize) - .failOnEmptyBeans(false) - // to prevent exception when encountering unknown property: - .failOnUnknownProperties(false) - - // disable, not thrown an exception if an unknown property - .featuresToDisable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) - - // to enable standard indentation ("pretty-printing"): - .indentOutput(true) - //.modules(jacksonModule()) - .build(); - } - - @Bean - public RestOperations backendRestOperations(MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter) throws Exception { - RestTemplate restTemplate = new RestTemplate(httpRequestFactory()); - restTemplate.setInterceptors(List.of(new CookieRedirectInterceptor(maxRedirects))); - - // No DefaultUriBuilderFactory! Let display the security policy violation... - - // MediaType.ALL now! Json + Json wildcard, pdf, gif etc... - mappingJackson2HttpMessageConverter.setSupportedMediaTypes(List.of(MediaType.ALL)); - - restTemplate.setMessageConverters(Arrays.asList( - new StringHttpMessageConverter(UTF_8), - new FormHttpMessageConverter(), - new ByteArrayHttpMessageConverter(), - //new ResourceHttpMessageConverter(), // WARN mandatory or use HttpMessageConverter - new HttpMessageConverter() { - @Override - public boolean canRead(@NonNull Class clazz, MediaType mediaType) { - return Resource.class.isAssignableFrom(clazz); - } - - @Override - public boolean canWrite(@NonNull Class clazz, MediaType mediaType) { - return false; - } - - @Override - public @NonNull List getSupportedMediaTypes() { - return List.of(MediaType.APPLICATION_OCTET_STREAM); - } - - @Override - public @NonNull Resource read(@NonNull Class clazz, @NonNull HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { - InputStream inputStream = inputMessage.getBody(); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(inputStream.available()); - try (inputStream; outputStream) { - IOUtils.copy(inputStream, outputStream); - } - - final byte[] content = outputStream.toByteArray(); - return new AbstractResource() { - @Override - public @NonNull String getDescription() { - return "HttpInputMessage resource"; - } - - @Override - public @NonNull InputStream getInputStream() throws IOException { - return new ByteArrayInputStream(content); - } - - @Override - public String getFilename() { - return inputMessage.getHeaders().getContentDisposition().getFilename(); - } - }; - } - - @Override - public void write(@NonNull Resource resource, MediaType contentType, @NonNull HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { - - } - }, - mappingJackson2HttpMessageConverter - )); - return restTemplate; - } - - @Value("${nexus.backend.client.connectTimeout:10}") - private int connectTimeout; - @Value("${nexus.backend.client.requestTimeout:20}") - private int requestTimeout; - @Value("${nexus.backend.client.socketTimeout:10}") - private int socketTimeout; - - @Value("${nexus.backend.client.max_connections_per_route:20}") - private int defaultMaxConnectionsPerRoute; - - @Value("${nexus.backend.client.max_connections:100}") - private int maxConnections; - - @Value("${nexus.backend.client.close_idle_connections_timeout:0}") - private int closeIdleConnectionsTimeout; - - @Value("${nexus.backend.client.validate_after_inactivity:2}") - private int validateAfterInactivity; - - @Value("${nexus.backend.client.retryCount:3}") - private int retryCount; - @Value("${nexus.backend.client.requestSentRetryEnabled:false}") - private boolean requestSentRetryEnabled; - - @Value("${nexus.backend.client.redirectsEnabled:true}") - private boolean redirectsEnabled; - @Value("${nexus.backend.client.maxRedirects:5}") - private int maxRedirects; - @Value("${nexus.backend.client.authenticationEnabled:false}") - private boolean authenticationEnabled; - @Value("${nexus.backend.client.circularRedirectsAllowed:false}") - private boolean circularRedirectsAllowed; - - - /** - * User-Agent - */ - @Value("${nexus.backend.client.header.user-agent:JavaNexus}") - private String userAgent; - - - /** - * Activated the Mutual Authentication or mTLS, default protocol TLSv1.3 - */ - @Value("${nexus.backend.client.ssl.mtls.enable:false}") - private boolean isMTLS; - - @Value("${nexus.backend.client.ssl.key-store:nexus-default.jks}") - private String pathJKS; - @Value("${nexus.backend.client.ssl.key-store-password:changeit}") - private String keyStorePassword; - @Value("${nexus.backend.client.ssl.certificate.alias:key_server}") - private String certificateAlias; - /** - * SpEL reads allow method delimited with a comma and splits into a List of Strings - */ - @Value("#{'${nexus.backend.client.ssl.https.protocols:TLSv1.3}'.split(',')}") - private List httpsProtocols; - @Value("#{'${nexus.backend.client.ssl.https.cipherSuites:TLS_AES_256_GCM_SHA384}'.split(',')}") - private List httpsCipherSuites; - - @Bean - public ClientHttpRequestFactory httpRequestFactory() throws Exception { - - final DefaultConnectionKeepAliveStrategy myStrategy = new DefaultConnectionKeepAliveStrategy() { - @Override - public long getKeepAliveDuration(HttpResponse response, HttpContext context) { - return super.getKeepAliveDuration(response, context); - } - }; - - final HttpConnectionFactory connFactory = - new ManagedHttpClientConnectionFactory( - new DefaultHttpRequestWriterFactory(), - new DefaultHttpResponseParserFactory( - new CompliantLineParser(), new DefaultHttpResponseFactory())); - - final PoolingHttpClientConnectionManager cm; - - if (isMTLS) { - final KeyStore identityKeyStore = KeyStore.getInstance("jks"); - final FileInputStream identityKeyStoreFile = new FileInputStream(pathJKS); - identityKeyStore.load(identityKeyStoreFile, keyStorePassword.toCharArray()); - - final KeyStore trustKeyStore = KeyStore.getInstance("jks"); - final FileInputStream trustKeyStoreFile = new FileInputStream(pathJKS); - trustKeyStore.load(trustKeyStoreFile, keyStorePassword.toCharArray()); - - final SSLContext sslContext = SSLContexts.custom() - // load identity keystore - .loadKeyMaterial(identityKeyStore, keyStorePassword.toCharArray(), new PrivateKeyStrategy() { - @Override - public String chooseAlias(Map aliases, Socket socket) { - return certificateAlias; - } - }) - // load trust keystore - .loadTrustMaterial(trustKeyStore, null) - .build(); - - // WARN only protocol TLSv1.3, Not a mix with TLSv1.2,TLSv1.1 cause SSLSocket duplex close failed!!! - // WARN only CipherSuites TLS_AES_256_GCM_SHA384 or TLS_AES_128_GCM_SHA256 - final SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext, - httpsProtocols.toArray(new String[0]), - httpsCipherSuites.toArray(new String[0]), // TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256 - SSLConnectionSocketFactory.getDefaultHostnameVerifier()); - - // WARN Not set a sslConnectionSocketFactory cause a HandshakeContext with a dummy KeyManager!!! - cm = new PoolingHttpClientConnectionManager(RegistryBuilder.create() - .register("http", PlainConnectionSocketFactory.getSocketFactory()) - .register("https", sslConnectionSocketFactory) - .build(), connFactory); - } else { - cm = new PoolingHttpClientConnectionManager(connFactory); - } - - cm.setDefaultMaxPerRoute(defaultMaxConnectionsPerRoute); - cm.setMaxTotal(maxConnections); - cm.setValidateAfterInactivity(validateAfterInactivity * 1000); - cm.closeIdleConnections(closeIdleConnectionsTimeout, TimeUnit.SECONDS); - - return new HttpComponentsClientHttpRequestFactory(HttpClientBuilder.create() - .setUserAgent(userAgent) - .setConnectionManager(cm) - .setDefaultRequestConfig(RequestConfig.custom() - .setCookieSpec(CookieSpecs.STANDARD) // Optional specs standard! - .setConnectTimeout(connectTimeout * 1000) - .setConnectionRequestTimeout(requestTimeout * 1000) - .setSocketTimeout(socketTimeout * 1000) - .setRedirectsEnabled(redirectsEnabled) // mandatory disabled by default! - .setMaxRedirects(maxRedirects) - .setAuthenticationEnabled(authenticationEnabled) - .setCircularRedirectsAllowed(circularRedirectsAllowed) - .build()) - .setConnectionReuseStrategy(new DefaultConnectionReuseStrategy()) - .setKeepAliveStrategy(myStrategy) - .setRedirectStrategy(new LaxRedirectStrategy()) - .setRetryHandler(new DefaultHttpRequestRetryHandler(retryCount, requestSentRetryEnabled)) - .disableRedirectHandling() - // Cookie client stateful managed, the Gateway is Stateless! Session state is temporary and only affects the transaction! - //.disableCookieManagement() - .disableAuthCaching() - .disableConnectionState() - .build()); - } - - /** - * Force HttpClient into accepting malformed response heads in order to salvage the content of the messages. - * (Deal non-standard and non-compliant behaviours!) - */ - static class CompliantLineParser extends BasicLineParser { - @Override - public Header parseHeader(CharArrayBuffer buffer) throws ParseException { - try { - return super.parseHeader(buffer); - } catch (ParseException ex) { - // Suppress ParseException exception - return new BasicHeader(buffer.toString(), null); - } - } - } -} - diff --git a/src/test/java/com/jservlet/nexus/test/openapi/OpenApiReader.java b/src/test/java/com/jservlet/nexus/test/openapi/OpenApiReader.java deleted file mode 100644 index 2c6f248..0000000 --- a/src/test/java/com/jservlet/nexus/test/openapi/OpenApiReader.java +++ /dev/null @@ -1,225 +0,0 @@ -package com.jservlet.nexus.test.openapi; - -import io.swagger.v3.oas.models.*; -import io.swagger.v3.oas.models.media.Content; -import io.swagger.v3.oas.models.media.MediaType; -import io.swagger.v3.oas.models.parameters.Parameter; -import io.swagger.v3.oas.models.parameters.RequestBody; -import io.swagger.v3.parser.OpenAPIV3Parser; -/*import org.apache.poi.ss.usermodel.*; -import org.apache.poi.xssf.usermodel.XSSFFont; -import org.apache.poi.xssf.usermodel.XSSFRichTextString; -import org.apache.poi.xssf.usermodel.XSSFSheet; -import org.apache.poi.xssf.usermodel.XSSFWorkbook;*/ - -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.util.*; - -/** - * Read an OpenApi 3.0.1 file yaml or json (File or Url) and Export OpenApi data to an Excel file - * Columns: - * "Uri","Method","Id","Parameters Name,Type,Example,Required","RegExp","BodyRequest","Required","Media","Type" - * - * @author franck@jservlet.com, 06.2024 - */ -public class OpenApiReader { - - /* // Output Excel file /src/test/resources - private static final String FILE_NAME = "src/test/resources/mock-api.xlsx"; - private static final String PATH_CONTEXT = "/nexus-backend"; - private static final String SHEET_NAME = "OpenAPI"; - - *//* - * Main OpenApiReader - * args[0] src\test\resources\mock-api.json - * or - * args[0] http://localhost:8082/nexus-backend/v3/api-docs/mock-api - *//* - public static void main(String[] args) throws IOException { - // Load raw data from file format openapi 3.0.1 - OpenAPI api = new OpenAPIV3Parser().read(args[0]); - // Get the component - Components components = api.getComponents(); - if (components != null) { - components.getSchemas(); - } - // Scan the package - for (Map.Entry entry : api.getPaths().entrySet()) { - PathItem item = entry.getValue(); - System.out.println(item.getDescription()); - System.out.println(entry.getKey()); - Operation opeGet = item.getGet(); - readParameters(opeGet); - Operation opePost = item.getPost(); - readParameters(opePost); - - } - Map data = fillData(api.getPaths().entrySet()); - saveFileExcel(data); - - } - private static void readParameters(Operation operation) { - if (operation != null) { - System.out.println(operation.getSummary()); - System.out.println(operation.getOperationId()); - if (operation.getRequestBody() != null) { - System.out.println(operation.getRequestBody().get$ref()); - } - - List params = operation.getParameters(); - if (params != null) { - for (Parameter param : params) { - System.out.println(param.getName()); - System.out.println(param.getDescription()); - System.out.println(param.getSchema().getType()); - System.out.println(param.getSchema().getDefault()); - System.out.println(param.getRequired()); - } - } - } - } - - - private static Map fillData(Set> entrySet) { - Map data = new TreeMap<>(); - - XSSFFont font1 = new XSSFFont(); - font1.setBold(true); - XSSFRichTextString s0 = new XSSFRichTextString("Uri"); - s0.applyFont(font1); - XSSFRichTextString s1 = new XSSFRichTextString("Method"); - s1.applyFont(font1); - XSSFRichTextString s2 = new XSSFRichTextString("Id"); - s2.applyFont(font1); - XSSFRichTextString s3 = new XSSFRichTextString("Parameters" + sep + "Name,Type,Example,Required"); - s3.applyFont(font1); - XSSFRichTextString s4 = new XSSFRichTextString("RegExp"); - s4.applyFont(font1); - XSSFRichTextString s5 = new XSSFRichTextString("BodyRequest"); - s5.applyFont(font1); - XSSFRichTextString s6 = new XSSFRichTextString("Required"); - s6.applyFont(font1); - XSSFRichTextString s7 = new XSSFRichTextString("Media"); - s7.applyFont(font1); - XSSFRichTextString s8 = new XSSFRichTextString("Type"); - s8.applyFont(font1); - // Headers columns - data.put("0", new Object[] {s0,s1,s2,s3,s4,s5,s6,s7,s8}); - - // Body operations columns - for (Map.Entry entry : entrySet) { - PathItem item = entry.getValue(); - System.out.println(entry.getKey()); - Operation opeGet = item.getGet(); - if (opeGet != null) saveOperation(data, opeGet, entry.getKey(), "GET"); - Operation opePost = item.getPost(); - if (opePost != null) saveOperation(data, opePost, entry.getKey(), "POST"); - Operation opePut = item.getPut(); - if (opePut != null) saveOperation(data, opePut, entry.getKey(), "PUT"); - Operation opeDel = item.getDelete(); - if (opeDel != null) saveOperation(data, opeDel, entry.getKey(), "DELETE"); - Operation opePatch = item.getPatch(); - if (opePatch != null) saveOperation(data, opePatch, entry.getKey(), "PATCH"); - Operation opeOptions = item.getOptions(); - if (opeOptions != null) saveOperation(data, opeOptions, entry.getKey(), "OPTIONS"); - } - return data; - } - - private static int count = 0; - private static final String sep = "\n"; - - private static void saveOperation(Map data, Operation operation, String uri, String method) { - // Body operations columns - StringBuilder sb = new StringBuilder(); - List params = operation.getParameters(); - if (params != null) { - for (Parameter param : params) { - sb.append(param.getName()).append(" "); - sb.append(param.getSchema().getType()).append(" "); - if (param.getSchema().getDefault() != null)sb.append("\"").append(param.getSchema().getDefault()).append("\"").append(" "); - else sb.append(param.getSchema().getDefault()).append(" "); - sb.append(param.getRequired()).append(sep); - } - } - String ref = ""; - Boolean required = false; - StringBuilder media = new StringBuilder(); - StringBuilder mediaType = new StringBuilder(); - Object body = null; - if (operation.getRequestBody() != null) { - RequestBody requestBody = operation.getRequestBody(); - ref = requestBody.get$ref(); - required = requestBody.getRequired(); - if (required != null && required) { - Content content = requestBody.getContent(); - for (Map.Entry entry : content.entrySet()) { - media.append(entry.getKey()).append(sep); - mediaType.append(entry.getValue().getSchema().getType()).append(sep); - } - } - } - count++; - data.put(Integer.toString(count), new Object[] { - PATH_CONTEXT + uri, - operation.getOperationId(), - method, - sb.toString(), - body, - ref, - required, - media.toString(), - mediaType.toString() - }); - } - - private static void saveFileExcel(Map datatypes) throws IOException { - XSSFWorkbook workbook = new XSSFWorkbook(); - XSSFSheet sheet = workbook.createSheet(SHEET_NAME); - int rowNum = 0; - System.out.println("Creating excel"); - Set keyset = datatypes.keySet(); - for (String key : keyset) { - Row row = sheet.createRow(rowNum++); - int colNum = 0; - Object[] objArr = datatypes.get(key); - for (Object field : objArr) { - Cell cell = row.createCell(colNum++); - if (field instanceof String) { - cell.setCellValue((String) field); - } else if (field instanceof Double) { - cell.setCellValue((Double) field); - } else if (field instanceof Integer) { - cell.setCellValue((Integer) field); - } else if (field instanceof Boolean) { - cell.setCellValue((Boolean) field); - } else if (field instanceof Date) { - cell.setCellValue((Date) field); - } else if (field instanceof Calendar) { - cell.setCellValue((Calendar) field); - } else if (field instanceof RichTextString) { - cell.setCellValue((RichTextString) field); - } else if (field instanceof LocalDate) { - cell.setCellValue((LocalDate) field); - } else if (field instanceof LocalDateTime) { - cell.setCellValue((LocalDateTime) field); - } - else cell.setBlank(); - } - } - - try { - workbook.write(new FileOutputStream(FILE_NAME)); - System.out.println(FILE_NAME); - workbook.close(); - } catch (FileNotFoundException e) { - e.printStackTrace(); - } - - System.out.println("Done"); - }*/ -} diff --git a/src/test/resources/Postman-Echo-performance-report-2025.pdf b/src/test/resources/Postman-Echo-performance-report-2025.pdf deleted file mode 100644 index 535f1af..0000000 Binary files a/src/test/resources/Postman-Echo-performance-report-2025.pdf and /dev/null differ diff --git a/src/test/resources/Postman-Echo-performance-report-2025.png b/src/test/resources/Postman-Echo-performance-report-2025.png deleted file mode 100644 index 536d70d..0000000 Binary files a/src/test/resources/Postman-Echo-performance-report-2025.png and /dev/null differ diff --git a/src/test/resources/Postman_Echo.postman_collection.json b/src/test/resources/Postman_Echo.postman_collection.json deleted file mode 100644 index 4cb0d9b..0000000 --- a/src/test/resources/Postman_Echo.postman_collection.json +++ /dev/null @@ -1,4294 +0,0 @@ -{ - "info": { - "_postman_id": "55548714-bfe1-4d87-bc97-cd915e82fcf1", - "name": "Postman Echo", - "description": "Postman Echo is service you can use to test your REST clients and make sample API calls. It provides endpoints for `GET`, `POST`, `PUT`, various auth mechanisms and other utility endpoints.\n\nThe documentation for the endpoints as well as example responses can be found at [https://postman-echo.com](https://postman-echo.com/?source=echo-collection-app-onboarding)", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "5266980" - }, - "item": [ - { - "name": "Auth: Digest", - "item": [ - { - "name": "DigestAuth Request", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "tests[\"response code is 401\"] = responseCode.code === 401;", - "tests[\"response has WWW-Authenticate header\"] = (postman.getResponseHeader('WWW-Authenticate'));", - "", - "var authenticateHeader = postman.getResponseHeader('WWW-Authenticate'),", - " realmStart = authenticateHeader.indexOf('\"',authenticateHeader.indexOf(\"realm\")) + 1 ,", - " realmEnd = authenticateHeader.indexOf('\"',realmStart),", - " realm = authenticateHeader.slice(realmStart,realmEnd),", - " nonceStart = authenticateHeader.indexOf('\"',authenticateHeader.indexOf(\"nonce\")) + 1,", - " nonceEnd = authenticateHeader.indexOf('\"',nonceStart),", - " nonce = authenticateHeader.slice(nonceStart,nonceEnd);", - " ", - "postman.setGlobalVariable('echo_digest_realm', realm);", - "postman.setGlobalVariable('echo_digest_nonce', nonce);" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "code", - "value": "xWnkliVQJURqB2x1", - "type": "text" - }, - { - "key": "grant_type", - "value": "authorization_code", - "type": "text" - }, - { - "key": "redirect_uri", - "value": "https://www.getpostman.com/oauth2/callback", - "type": "text" - }, - { - "key": "client_id", - "value": "abc123", - "type": "text" - }, - { - "key": "client_secret", - "value": "ssh-secret", - "type": "text" - } - ] - }, - "url": { - "raw": "{{base_url}}/{{app_url}}/digest-auth", - "host": [ - "{{base_url}}" - ], - "path": [ - "{{app_url}}", - "digest-auth" - ] - }, - "description": "Performing a simple `GET` request to this endpoint returns status code `401 Unauthorized` with `WWW-Authenticate` header containing information to successfully authenticate subsequent requests.\nThe `WWW-Authenticate` header must be processed to extract `realm` and `nonce` values to hash subsequent requests.\n\nWhen this request is executed within Postman, the script attached with this request does the hard work of extracting realm and nonce from the header and set it as [global variables](https://www.getpostman.com/docs/environments#global-variables?source=echo-collection-app-onboarding) named `echo_digest_nonce` and `echo_digest_realm`.\nThese variables are re-used in subsequent request for seamless integration of the two requests." - }, - "response": [] - }, - { - "name": "DigestAuth Success", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "tests[\"response code is 200\"] = responseCode.code === 200;", - "tests[\"body contains authenticated\"] = responseBody.has(\"authenticated\");" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "auth": { - "type": "digest", - "digest": [ - { - "key": "algorithm", - "value": "MD5", - "type": "string" - }, - { - "key": "username", - "value": "postman", - "type": "string" - }, - { - "key": "realm", - "value": "{{echo_digest_realm}}", - "type": "string" - }, - { - "key": "password", - "value": "password", - "type": "string" - }, - { - "key": "nonce", - "value": "{{echo_digest_nonce}}", - "type": "string" - }, - { - "key": "nonceCount", - "value": "", - "type": "string" - }, - { - "key": "clientNonce", - "value": "", - "type": "string" - }, - { - "key": "opaque", - "value": "", - "type": "string" - }, - { - "key": "qop", - "value": "", - "type": "string" - }, - { - "key": "disableRetryRequest", - "type": "any" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Digest username=\"postman\", realm=\"Users\", nonce=\"ni1LiL0O37PRRhofWdCLmwFsnEtH1lew\", uri=\"/digest-auth\", response=\"254679099562cf07df9b6f5d8d15db44\", opaque=\"\"" - } - ], - "body": { - "mode": "formdata", - "formdata": [] - }, - "url": { - "raw": "{{base_url}}/{{app_url}}/digest-auth", - "host": [ - "{{base_url}}" - ], - "path": [ - "{{app_url}}", - "digest-auth" - ] - }, - "description": "This endpoint sends a hashed Digest Authorization header to gain access to a valid `200 Ok` response code. In Postman, it uses the stored [global variables](https://www.getpostman.com/docs/environments#gloval-variables?source=echo-collection-app-onboarding), `echo_digest_realm` and `echo_digest_nonce`, to generate the hashed authorisation header.\n\nWithin Postman, for this request to successfully authenticate, running the previous request \"DigestAuth Request\" stores the relevant information within the global variables." - }, - "response": [ - { - "name": "200", - "originalRequest": { - "auth": { - "type": "digest", - "digest": [ - { - "key": "algorithm", - "value": "MD5", - "type": "string" - }, - { - "key": "username", - "value": "postman", - "type": "string" - }, - { - "key": "realm", - "value": "{{echo_digest_realm}}", - "type": "string" - }, - { - "key": "password", - "value": "password", - "type": "string" - }, - { - "key": "nonce", - "value": "{{echo_digest_nonce}}", - "type": "string" - }, - { - "key": "nonceCount", - "value": "", - "type": "string" - }, - { - "key": "clientNonce", - "value": "", - "type": "string" - }, - { - "key": "opaque", - "value": "", - "type": "string" - }, - { - "key": "qop", - "value": "", - "type": "string" - }, - { - "key": "disableRetryRequest", - "type": "any" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Digest username=\"postman\", realm=\"Users\", nonce=\"ni1LiL0O37PRRhofWdCLmwFsnEtH1lew\", uri=\"/digest-auth\", response=\"254679099562cf07df9b6f5d8d15db44\", opaque=\"\"" - } - ], - "body": { - "mode": "formdata", - "formdata": [] - }, - "url": { - "raw": "{{base_url}}/{{app_url}}/digest-auth", - "host": [ - "{{base_url}}" - ], - "path": [ - "{{app_url}}", - "digest-auth" - ] - }, - "description": "This endpoint sends a hashed Digest Authorization header to gain access to a valid `200 Ok` response code. In Postman, it uses the stored [global variables](https://www.getpostman.com/docs/environments#gloval-variables?source=echo-collection-app-onboarding), `echo_digest_realm` and `echo_digest_nonce`, to generate the hashed authorisation header.\n\nWithin Postman, for this request to successfully authenticate, running the previous request \"DigestAuth Request\" stores the relevant information within the global variables." - }, - "status": "OK", - "code": 200, - "_postman_previewlanguage": "javascript", - "header": [ - { - "key": "Access-Control-Allow-Credentials", - "value": "", - "name": "Access-Control-Allow-Credentials", - "description": "" - }, - { - "key": "Access-Control-Allow-Headers", - "value": "", - "name": "Access-Control-Allow-Headers", - "description": "" - }, - { - "key": "Access-Control-Allow-Methods", - "value": "", - "name": "Access-Control-Allow-Methods", - "description": "" - }, - { - "key": "Access-Control-Allow-Origin", - "value": "", - "name": "Access-Control-Allow-Origin", - "description": "" - }, - { - "key": "Connection", - "value": "keep-alive", - "name": "Connection", - "description": "" - }, - { - "key": "Content-Encoding", - "value": "gzip", - "name": "Content-Encoding", - "description": "" - }, - { - "key": "Content-Length", - "value": "42", - "name": "Content-Length", - "description": "" - }, - { - "key": "Content-Type", - "value": "application/json; charset=utf-8", - "name": "Content-Type", - "description": "" - }, - { - "key": "Date", - "value": "Thu, 29 Oct 2015 06:17:51 GMT", - "name": "Date", - "description": "" - }, - { - "key": "Server", - "value": "nginx/1.6.2", - "name": "Server", - "description": "" - }, - { - "key": "Vary", - "value": "Accept-Encoding", - "name": "Vary", - "description": "" - }, - { - "key": "X-Powered-By", - "value": "Sails ", - "name": "X-Powered-By", - "description": "" - } - ], - "cookie": [], - "body": "{\"authenticated\":true}" - } - ] - } - ], - "description": "Digest authentication protects an endpoint with a username and password without actually transmitting the password over network.\nOne has to apply a hash function (like MD5, etc) to the username and password before sending them over the network.\n\n> Username: `postman`\n>\n> Password: `password`\n\nUnlike Basic-Auth, authentication happens using two consecutive requests where the first request returns `401 Unauthorised` along with `WWW-Authenticate` header containing information that needs to be used to authenticate subsequent calls.\n\nTo know more about digest authentication, refer to the [Digest Access Authentication](https://en.wikipedia.org/wiki/Digest_access_authentication) wikipedia article.\nThe article on [authentication helpers](https://www.getpostman.com/docs/helpers#digest-auth) elaborates how to use the same within the Postman app." - }, - { - "name": "Auth: Others", - "item": [ - { - "name": "Basic Auth", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "tests[\"response code is 200\"] = responseCode.code === 200;", - "tests[\"Body contains authenticated\"] = responseBody.has(\"authenticated\");" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "username", - "value": "postman", - "type": "string" - }, - { - "key": "password", - "value": "password", - "type": "string" - }, - { - "key": "saveHelperData", - "value": true, - "type": "boolean" - }, - { - "key": "showPassword", - "value": false, - "type": "boolean" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Basic cG9zdG1hbjpwYXNzd29yZA==" - } - ], - "body": { - "mode": "formdata", - "formdata": [] - }, - "url": { - "raw": "{{base_url}}/{{app_url}}/basic-auth", - "host": [ - "{{base_url}}" - ], - "path": [ - "{{app_url}}", - "basic-auth" - ] - }, - "description": "This endpoint simulates a **basic-auth** protected endpoint. \nThe endpoint accepts a default username and password and returns a status code of `200 ok` only if the same is provided. \nOtherwise it will return a status code `401 unauthorized`.\n\n> Username: `postman`\n> \n> Password: `password`\n\nTo use this endpoint, send a request with the header `Authorization: Basic cG9zdG1hbjpwYXNzd29yZA==`. \nThe cryptic latter half of the header value is a base64 encoded concatenation of the default username and password. \nUsing Postman, to send this request, you can simply fill in the username and password in the \"Authorization\" tab and Postman will do the rest for you.\n\nTo know more about basic authentication, refer to the [Basic Access Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication) wikipedia article.\nThe article on [authentication helpers](https://www.getpostman.com/docs/helpers#basic-auth?source=echo-collection-app-onboarding) elaborates how to use the same within the Postman app." - }, - "response": [ - { - "name": "200", - "originalRequest": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "username", - "value": "postman", - "type": "string" - }, - { - "key": "password", - "value": "password", - "type": "string" - }, - { - "key": "saveHelperData", - "value": true, - "type": "boolean" - }, - { - "key": "showPassword", - "value": false, - "type": "boolean" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Basic cG9zdG1hbjpwYXNzd29yZA==" - } - ], - "body": { - "mode": "formdata", - "formdata": [] - }, - "url": { - "raw": "{{base_url}}/{{app_url}}/basic-auth", - "host": [ - "{{base_url}}" - ], - "path": [ - "{{app_url}}", - "basic-auth" - ] - }, - "description": "This endpoint simulates a **basic-auth** protected endpoint. \nThe endpoint accepts a default username and password and returns a status code of `200 ok` only if the same is provided. \nOtherwise it will return a status code `401 unauthorized`.\n\n> Username: `postman`\n> \n> Password: `password`\n\nTo use this endpoint, send a request with the header `Authorization: Basic cG9zdG1hbjpwYXNzd29yZA==`. \nThe cryptic latter half of the header value is a base64 encoded concatenation of the default username and password. \nUsing Postman, to send this request, you can simply fill in the username and password in the \"Authorization\" tab and Postman will do the rest for you.\n\nTo know more about basic authentication, refer to the [Basic Access Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication) wikipedia article.\nThe article on [authentication helpers](https://www.getpostman.com/docs/helpers#basic-auth?source=echo-collection-app-onboarding) elaborates how to use the same within the Postman app." - }, - "status": "OK", - "code": 200, - "_postman_previewlanguage": "javascript", - "header": [ - { - "key": "Access-Control-Allow-Credentials", - "value": "", - "name": "Access-Control-Allow-Credentials", - "description": "" - }, - { - "key": "Access-Control-Allow-Headers", - "value": "", - "name": "Access-Control-Allow-Headers", - "description": "" - }, - { - "key": "Access-Control-Allow-Methods", - "value": "", - "name": "Access-Control-Allow-Methods", - "description": "" - }, - { - "key": "Access-Control-Allow-Origin", - "value": "", - "name": "Access-Control-Allow-Origin", - "description": "" - }, - { - "key": "Connection", - "value": "keep-alive", - "name": "Connection", - "description": "" - }, - { - "key": "Content-Encoding", - "value": "gzip", - "name": "Content-Encoding", - "description": "" - }, - { - "key": "Content-Length", - "value": "42", - "name": "Content-Length", - "description": "" - }, - { - "key": "Content-Type", - "value": "application/json; charset=utf-8", - "name": "Content-Type", - "description": "" - }, - { - "key": "Date", - "value": "Sat, 31 Oct 2015 06:38:25 GMT", - "name": "Date", - "description": "" - }, - { - "key": "Server", - "value": "nginx/1.6.2", - "name": "Server", - "description": "" - }, - { - "key": "Vary", - "value": "Accept-Encoding", - "name": "Vary", - "description": "" - }, - { - "key": "X-Powered-By", - "value": "Sails ", - "name": "X-Powered-By", - "description": "" - } - ], - "cookie": [], - "body": "{\"authenticated\":true}" - } - ] - }, - { - "name": "OAuth1.0 Verify Signature", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "tests[\"response code is 200\"] = responseCode.code === 200;", - "var body = JSON.parse(responseBody);", - "tests[\"Body contains status pass\"] = body[\"status\"] == \"pass\"" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "auth": { - "type": "oauth1", - "oauth1": [ - { - "key": "consumerKey", - "value": "RKCGzna7bv9YD57c", - "type": "string" - }, - { - "key": "consumerSecret", - "value": "D+EdQ-gs$-%@2Nu7", - "type": "string" - }, - { - "key": "token", - "value": "", - "type": "string" - }, - { - "key": "tokenSecret", - "value": "", - "type": "string" - }, - { - "key": "signatureMethod", - "value": "HMAC-SHA1", - "type": "string" - }, - { - "key": "timestamp", - "value": 1472121255, - "type": "number" - }, - { - "key": "nonce", - "value": "e5VR16", - "type": "string" - }, - { - "key": "version", - "value": "1.0", - "type": "string" - }, - { - "key": "realm", - "value": "", - "type": "string" - }, - { - "key": "addParamsToHeader", - "value": true, - "type": "boolean" - }, - { - "key": "autoAddParam", - "type": "any" - }, - { - "key": "addEmptyParamsToSign", - "value": false, - "type": "boolean" - } - ] - }, - "method": "GET", - "header": [], - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "code", - "value": "xWnkliVQJURqB2x1", - "type": "text" - }, - { - "key": "grant_type", - "value": "authorization_code", - "type": "text" - }, - { - "key": "redirect_uri", - "value": "https://www.getpostman.com/oauth2/callback", - "type": "text" - }, - { - "key": "client_id", - "value": "abc123", - "type": "text" - }, - { - "key": "client_secret", - "value": "ssh-secret", - "type": "text" - } - ] - }, - "url": { - "raw": "{{base_url}}/{{app_url}}/oauth1", - "host": [ - "{{base_url}}" - ], - "path": [ - "{{app_url}}", - "oauth1" - ] - }, - "description": "OAuth1.0a is a specification that defines a protocol that can be used by one\nservice to access \"protected\" resources (endpoints) on another service. A\nmajor part of OAuth1.0 is HTTP Request Signing. This endpoint allows you to \ncheck whether the request calculation works properly in the client. \n\nThe endpoint supports the HTTP ``Authorization`` header. In case the signature\nverification fails, the endpoint provides the four debug values,\n\n* ``base_uri``\n* ``normalized_param_string``\n* ``base_string``\n* ``signing_key``\n\nFor more details about these parameters, check the [OAuth1.0a Specification](http://oauth.net/core/1.0a/)\n\nIn order to use this endpoint, you can set the following values:\n\n> Consumer Key: ``RKCGzna7bv9YD57c``\n>\n> Consumer Secret: ``D+EdQ-gs$-%@2Nu7``\n\nIf you are using Postman, also check the \"Add params to header\" and \n\"Auto add parameters\" boxes." - }, - "response": [ - { - "name": "200", - "originalRequest": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "name": "Authorization", - "value": "OAuth oauth_consumer_key=\"RKCGzna7bv9YD57c\",oauth_signature_method=\"HMAC-SHA1\",oauth_timestamp=\"1472121261\",oauth_nonce=\"ki0RQW\",oauth_version=\"1.0\",oauth_signature=\"s0rK92Myxx7ceUBVzlMaxiiXU00%3D\"" - } - ], - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "code", - "value": "xWnkliVQJURqB2x1", - "type": "text" - }, - { - "key": "grant_type", - "value": "authorization_code", - "type": "text" - }, - { - "key": "redirect_uri", - "value": "https://www.getpostman.com/oauth2/callback", - "type": "text" - }, - { - "key": "client_id", - "value": "abc123", - "type": "text" - }, - { - "key": "client_secret", - "value": "ssh-secret", - "type": "text" - } - ] - }, - "url": { - "raw": "https://echo.getpostman.com/oauth1", - "protocol": "https", - "host": [ - "echo", - "getpostman", - "com" - ], - "path": [ - "oauth1" - ] - } - }, - "status": "OK", - "code": 200, - "_postman_previewlanguage": "javascript", - "header": [ - { - "key": "Access-Control-Allow-Credentials", - "value": "", - "name": "Access-Control-Allow-Credentials", - "description": "" - }, - { - "key": "Access-Control-Allow-Headers", - "value": "", - "name": "Access-Control-Allow-Headers", - "description": "" - }, - { - "key": "Access-Control-Allow-Methods", - "value": "", - "name": "Access-Control-Allow-Methods", - "description": "" - }, - { - "key": "Access-Control-Allow-Origin", - "value": "", - "name": "Access-Control-Allow-Origin", - "description": "" - }, - { - "key": "Access-Control-Expose-Headers", - "value": "", - "name": "Access-Control-Expose-Headers", - "description": "" - }, - { - "key": "Connection", - "value": "keep-alive", - "name": "Connection", - "description": "" - }, - { - "key": "Content-Encoding", - "value": "gzip", - "name": "Content-Encoding", - "description": "" - }, - { - "key": "Content-Length", - "value": "95", - "name": "Content-Length", - "description": "" - }, - { - "key": "Content-Type", - "value": "application/json; charset=utf-8", - "name": "Content-Type", - "description": "" - }, - { - "key": "Date", - "value": "Thu, 25 Aug 2016 10:34:23 GMT", - "name": "Date", - "description": "" - }, - { - "key": "ETag", - "value": "W/\"4e-Cq3UhvpVSyl6R6204lPVIA\"", - "name": "ETag", - "description": "" - }, - { - "key": "Server", - "value": "nginx/1.8.1", - "name": "Server", - "description": "" - }, - { - "key": "Vary", - "value": "Accept-Encoding", - "name": "Vary", - "description": "" - } - ], - "cookie": [], - "body": "{\"status\":\"pass\",\"message\":\"OAuth-1.0a signature verification was successful\"}" - }, - { - "name": "401", - "originalRequest": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "type": "text", - "name": "Authorization", - "value": "OAuth oauth_consumer_key=\"RKCGzna7bv9YD57c_wrong\",oauth_signature_method=\"HMAC-SHA1\",oauth_timestamp=\"1472121295\",oauth_nonce=\"8LTsU2\",oauth_version=\"1.0\",oauth_signature=\"tSUexpY%2B7EhSY7cFXiFN5EMx2zw%3D\"" - } - ], - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "code", - "value": "xWnkliVQJURqB2x1", - "type": "text" - }, - { - "key": "grant_type", - "value": "authorization_code", - "type": "text" - }, - { - "key": "redirect_uri", - "value": "https://www.getpostman.com/oauth2/callback", - "type": "text" - }, - { - "key": "client_id", - "value": "abc123", - "type": "text" - }, - { - "key": "client_secret", - "value": "ssh-secret", - "type": "text" - } - ] - }, - "url": { - "raw": "https://echo.getpostman.com/oauth1", - "protocol": "https", - "host": [ - "echo", - "getpostman", - "com" - ], - "path": [ - "oauth1" - ] - } - }, - "status": "Unauthorized", - "code": 401, - "_postman_previewlanguage": "javascript", - "header": [ - { - "key": "Access-Control-Allow-Credentials", - "value": "", - "name": "Access-Control-Allow-Credentials", - "description": "" - }, - { - "key": "Access-Control-Allow-Headers", - "value": "", - "name": "Access-Control-Allow-Headers", - "description": "" - }, - { - "key": "Access-Control-Allow-Methods", - "value": "", - "name": "Access-Control-Allow-Methods", - "description": "" - }, - { - "key": "Access-Control-Allow-Origin", - "value": "", - "name": "Access-Control-Allow-Origin", - "description": "" - }, - { - "key": "Access-Control-Expose-Headers", - "value": "", - "name": "Access-Control-Expose-Headers", - "description": "" - }, - { - "key": "Connection", - "value": "keep-alive", - "name": "Connection", - "description": "" - }, - { - "key": "Content-Length", - "value": "536", - "name": "Content-Length", - "description": "" - }, - { - "key": "Content-Type", - "value": "application/json; charset=utf-8", - "name": "Content-Type", - "description": "" - }, - { - "key": "Date", - "value": "Thu, 25 Aug 2016 10:34:55 GMT", - "name": "Date", - "description": "" - }, - { - "key": "ETag", - "value": "W/\"218-SGnurnTsu5qV5cCYWxsJlg\"", - "name": "ETag", - "description": "" - }, - { - "key": "Server", - "value": "nginx/1.8.1", - "name": "Server", - "description": "" - }, - { - "key": "Vary", - "value": "Accept-Encoding", - "name": "Vary", - "description": "" - } - ], - "cookie": [], - "body": "{\"status\":\"fail\",\"message\":\"HMAC-SHA1 verification failed\",\"base_uri\":\"https://echo.getpostman.com/oauth1\",\"normalized_param_string\":\"oauth_consumer_key=RKCGzna7bv9YD57c_wrong&oauth_nonce=8LTsU2&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1472121295&oauth_version=1.0\",\"base_string\":\"GET&https%3A%2F%2Fecho.getpostman.com%2Foauth1&oauth_consumer_key%3DRKCGzna7bv9YD57c_wrong%26oauth_nonce%3D8LTsU2%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1472121295%26oauth_version%3D1.0\",\"signing_key\":\"D%2BEdQ-gs%24-%25%402Nu7&\"}" - } - ] - }, - { - "name": "Hawk Auth", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "tests[\"Status code is 200\"] = responseCode.code === 200;" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "auth": { - "type": "hawk", - "hawk": [ - { - "key": "authId", - "value": "dh37fgj492je", - "type": "string" - }, - { - "key": "authKey", - "value": "werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn", - "type": "string" - }, - { - "key": "algorithm", - "value": "sha256", - "type": "string" - }, - { - "key": "user", - "value": "", - "type": "string" - }, - { - "key": "saveHelperData", - "value": true, - "type": "boolean" - }, - { - "key": "nonce", - "value": "RZKGNz", - "type": "string" - }, - { - "key": "extraData", - "type": "any" - }, - { - "key": "appId", - "type": "any" - }, - { - "key": "delegation", - "type": "any" - }, - { - "key": "timestamp", - "value": "", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "access_token", - "value": "xyz1", - "type": "text" - }, - { - "key": "id", - "value": "U1", - "type": "text" - }, - { - "key": "server_secret", - "value": "zeppelin", - "type": "text" - }, - { - "key": "admin", - "value": "true", - "type": "text" - } - ] - }, - "url": { - "raw": "{{base_url}}/{{app_url}}/auth/hawk", - "host": [ - "{{base_url}}" - ], - "path": [ - "{{app_url}}", - "auth", - "hawk" - ] - }, - "description": "This endpoint is a Hawk Authentication protected endpoint. [Hawk authentication](https://github.com/hueniverse/hawk) is a widely used protocol for protecting API endpoints. One of Hawk's main goals is to enable HTTP authentication for services that do not use TLS (although it can be used in conjunction with TLS as well).\n\nIn order to use this endpoint, select the \"Hawk Auth\" helper inside Postman, and set the following values:\n\nHawk Auth ID: `dh37fgj492je`\n\nHawk Auth Key: `werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn`\n\nAlgorithm: `sha256`\n\nThe rest of the values are optional, and can be left blank. Hitting send should give you a response with a status code of 200 OK." - }, - "response": [ - { - "name": "Success", - "originalRequest": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "type": "text", - "name": "Authorization", - "value": "Hawk id=\"dh37fgj492je\", ts=\"1459422734\", nonce=\"XiwiCU\", mac=\"KzMHk67BYCC9zZqRy5hRdWFEFLHX5bNlRWGdmOAWKp0=\"" - } - ], - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "access_token", - "value": "xyz1", - "type": "text" - }, - { - "key": "id", - "value": "U1", - "type": "text" - }, - { - "key": "server_secret", - "value": "zeppelin", - "type": "text" - }, - { - "key": "admin", - "value": "true", - "type": "text" - } - ] - }, - "url": { - "raw": "https://echo.getpostman.com/auth/hawk", - "protocol": "https", - "host": [ - "echo", - "getpostman", - "com" - ], - "path": [ - "auth", - "hawk" - ] - } - }, - "status": "OK", - "code": 200, - "_postman_previewlanguage": "javascript", - "header": [ - { - "key": "Access-Control-Allow-Credentials", - "value": "", - "name": "Access-Control-Allow-Credentials", - "description": "" - }, - { - "key": "Access-Control-Allow-Headers", - "value": "", - "name": "Access-Control-Allow-Headers", - "description": "" - }, - { - "key": "Access-Control-Allow-Methods", - "value": "", - "name": "Access-Control-Allow-Methods", - "description": "" - }, - { - "key": "Access-Control-Allow-Origin", - "value": "", - "name": "Access-Control-Allow-Origin", - "description": "" - }, - { - "key": "Connection", - "value": "keep-alive", - "name": "Connection", - "description": "" - }, - { - "key": "Content-Encoding", - "value": "gzip", - "name": "Content-Encoding", - "description": "" - }, - { - "key": "Content-Type", - "value": "application/json", - "name": "Content-Type", - "description": "" - }, - { - "key": "Date", - "value": "Thu, 31 Mar 2016 11:12:16 GMT", - "name": "Date", - "description": "" - }, - { - "key": "Server", - "value": "nginx/1.6.2", - "name": "Server", - "description": "" - }, - { - "key": "Server-Authorization", - "value": "Hawk mac=\"vRrUzDdcHu2NaNts/r4zg2xmXMdX8wPiTGTM398BDRg=\", hash=\"qmtflETMybaZiOQ2dLT17yiRunFT5OCIxZRZ0boQaiE=\"", - "name": "Server-Authorization", - "description": "" - }, - { - "key": "Vary", - "value": "Accept-Encoding", - "name": "Vary", - "description": "" - }, - { - "key": "X-Powered-By", - "value": "Sails ", - "name": "X-Powered-By", - "description": "" - }, - { - "key": "transfer-encoding", - "value": "chunked", - "name": "transfer-encoding", - "description": "" - } - ], - "cookie": [ - { - "expires": "Fri Apr 15 2016 12:54:28 GMT+0200 (heure d’été d’Europe centrale)", - "hostOnly": false, - "httpOnly": false, - "domain": ".getpostman.com", - "path": "/", - "secure": false, - "session": false, - "value": "yes", - "key": "getpostmanlogin" - }, - { - "expires": "Fri Apr 15 2016 12:54:28 GMT+0200 (heure d’été d’Europe centrale)", - "hostOnly": false, - "httpOnly": false, - "domain": ".getpostman.com", - "path": "/", - "secure": false, - "session": false, - "value": "9f887f3b7f14b8c29ac4dc4109381b0b89a76e785c7b34251d6c8025b41b24013d2aa49f40e2deac19cbf0594dd984169455534d91ff98d4d1868d67ac857017629f137926e3a04a616bb83a2fb5ab9e6cbea9579ed5d5c1155d47545d72aad5be99f4abd0a7130805b3807d70cd507171dbe9d950d8e35a853f9ec075f5a767c95df4d57f7d521b66605b3bda3801700e26e651d1129c798b729ee3b91702d43ae64ab226c3f426893753def772c15442a7552dc84a3c773d6099a50b0a6af940b64c8176fedfcecd5fc31ccfc3bbc0124bfdaa0d62e4252d4aafb46a3c10963d12391e1fa97a1c0f19a636f57a3ac8cc35567d1cb6cb53b77f8adde3f6754a765596d7d00bdeb9acb5cc8d115e7c3f50ec3228e34d3e6c7464e9039b01868e03d10e9f87772397602453e9e91205de7b86021fad06eb26e69298e99ff1597a670faeb310f8c092041d544851de84f2bee89a92123da6eea286210524035c85361e2af42166a6", - "key": "postman.sid" - }, - { - "expires": "Invalid Date", - "hostOnly": true, - "httpOnly": true, - "domain": "echo.getpostman.com", - "path": "/", - "secure": false, - "session": true, - "value": "s%3AryJV7v-PE4PuTjBK6nH5XOynQ4atuATV.n17KcaLhVmV8TBHNLwdwXgGR7lmqs3i478WPlTbRgZ4", - "key": "sails.sid" - } - ], - "body": "{\"status\":\"pass\",\"message\":\"Hawk Authentication successful\"}" - } - ] - } - ] - }, - { - "name": "Cookies", - "item": [ - { - "name": "Set Cookies", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// handle case where it is 304", - "", - "if (responseCode.code === 200) {", - " tests[\"Status code is 302 or 200\"] = true;", - " tests[\"Body contains cookies\"] = responseBody.has(\"cookies\");", - " ", - " var body = JSON.parse(responseBody);", - " tests[\"Body contains cookie foo1\"] = 'foo1' in body.cookies;", - " tests[\"Body contains cookie foo2\"] = 'foo2' in body.cookies;", - "", - "}", - "else if (responseCode.code === 302) {", - " tests[\"Status code is 302 or 200\"] = true;", - " tests[\"Body has redirection message\"] = responseBody.has(\"Found. Redirecting to /cookies\")", - "}", - "else {", - " tests[\"Status code is 302 or 200\"] = false;", - "}", - "", - "tests[\"foo1 cookie is set\"] = _.get(postman.getResponseCookie('foo1'), 'value') === 'bar1';", - "", - "tests[\"foo2 cookie is set\"] = _.get(postman.getResponseCookie('foo2'), 'value') === 'bar2';" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "formdata", - "formdata": [] - }, - "url": { - "raw": "{{base_url}}/{{app_url}}/cookies/set?foo1=bar1&foo2=bar2", - "host": [ - "{{base_url}}" - ], - "path": [ - "{{app_url}}", - "cookies", - "set" - ], - "query": [ - { - "key": "foo1", - "value": "bar1" - }, - { - "key": "foo2", - "value": "bar2" - } - ] - }, - "description": "The cookie setter endpoint accepts a list of cookies and their values as part of URL parameters of a `GET` request. These cookies are saved and can be subsequently retrieved or deleted. The response of this request returns a JSON with all cookies listed.\n\nTo set your own set of cookies, simply replace the URL parameters \"foo1=bar1&foo2=bar2\" with your own set of key-value pairs." - }, - "response": [ - { - "name": "Cookies", - "originalRequest": { - "method": "GET", - "header": [], - "body": { - "mode": "formdata", - "formdata": [] - }, - "url": { - "raw": "{{base_url}}/{{app_url}}/cookies/set?foo1=bar1&foo2=bar2", - "host": [ - "{{base_url}}" - ], - "path": [ - "{{app_url}}", - "cookies", - "set" - ], - "query": [ - { - "key": "foo1", - "value": "bar1" - }, - { - "key": "foo2", - "value": "bar2" - } - ] - }, - "description": "The cookie setter endpoint accepts a list of cookies and their values as part of URL parameters of a `GET` request. These cookies are saved and can be subsequently retrieved or deleted. The response of this request returns a JSON with all cookies listed.\n\nTo set your own set of cookies, simply replace the URL parameters \"foo1=bar1&foo2=bar2\" with your own set of key-value pairs." - }, - "status": "OK", - "code": 200, - "_postman_previewlanguage": "javascript", - "header": [ - { - "key": "Access-Control-Allow-Credentials", - "value": "", - "name": "Access-Control-Allow-Credentials", - "description": "" - }, - { - "key": "Access-Control-Allow-Headers", - "value": "", - "name": "Access-Control-Allow-Headers", - "description": "" - }, - { - "key": "Access-Control-Allow-Methods", - "value": "", - "name": "Access-Control-Allow-Methods", - "description": "" - }, - { - "key": "Access-Control-Allow-Origin", - "value": "", - "name": "Access-Control-Allow-Origin", - "description": "" - }, - { - "key": "Connection", - "value": "keep-alive", - "name": "Connection", - "description": "" - }, - { - "key": "Content-Encoding", - "value": "gzip", - "name": "Content-Encoding", - "description": "" - }, - { - "key": "Content-Length", - "value": "51", - "name": "Content-Length", - "description": "" - }, - { - "key": "Content-Type", - "value": "application/json; charset=utf-8", - "name": "Content-Type", - "description": "" - }, - { - "key": "Date", - "value": "Thu, 29 Oct 2015 06:15:28 GMT", - "name": "Date", - "description": "" - }, - { - "key": "Server", - "value": "nginx/1.6.2", - "name": "Server", - "description": "" - }, - { - "key": "Vary", - "value": "Accept-Encoding", - "name": "Vary", - "description": "" - }, - { - "key": "X-Powered-By", - "value": "Sails ", - "name": "X-Powered-By", - "description": "" - } - ], - "cookie": [], - "body": "{\"cookies\":{\"foo1\":\"bar\",\"foo2\":\"bar\"}}" - } - ] - }, - { - "name": "Get Cookies", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "var responseJSON;", - "try {", - " tests[\"Body contains cookies\"] = responseBody.has(\"cookies\");", - " responseJSON = JSON.parse(responseBody);", - " tests[\"Cookies object is empty\"] = (Object.keys(responseJSON.cookies).length > 0)", - "}", - "catch (e) { }", - "", - "tests[\"Status code is 200\"] = responseCode.code === 200;", - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "formdata", - "formdata": [] - }, - "url": { - "raw": "{{base_url}}/{{app_url}}/cookies", - "host": [ - "{{base_url}}" - ], - "path": [ - "{{app_url}}", - "cookies" - ] - }, - "description": "Use this endpoint to get a list of all cookies that are stored with respect to this domain. Whatever key-value pairs that has been previously set by calling the \"Set Cookies\" endpoint, will be returned as response JSON." - }, - "response": [ - { - "name": "Cookies", - "originalRequest": { - "method": "GET", - "header": [], - "body": { - "mode": "formdata", - "formdata": [] - }, - "url": { - "raw": "{{base_url}}/{{app_url}}/cookies", - "host": [ - "{{base_url}}" - ], - "path": [ - "{{app_url}}", - "cookies" - ] - }, - "description": "Use this endpoint to get a list of all cookies that are stored with respect to this domain. Whatever key-value pairs that has been previously set by calling the \"Set Cookies\" endpoint, will be returned as response JSON." - }, - "status": "OK", - "code": 200, - "_postman_previewlanguage": "javascript", - "header": [ - { - "key": "Access-Control-Allow-Credentials", - "value": "", - "name": "Access-Control-Allow-Credentials", - "description": "" - }, - { - "key": "Access-Control-Allow-Headers", - "value": "", - "name": "Access-Control-Allow-Headers", - "description": "" - }, - { - "key": "Access-Control-Allow-Methods", - "value": "", - "name": "Access-Control-Allow-Methods", - "description": "" - }, - { - "key": "Access-Control-Allow-Origin", - "value": "", - "name": "Access-Control-Allow-Origin", - "description": "" - }, - { - "key": "Connection", - "value": "keep-alive", - "name": "Connection", - "description": "" - }, - { - "key": "Content-Encoding", - "value": "gzip", - "name": "Content-Encoding", - "description": "" - }, - { - "key": "Content-Length", - "value": "46", - "name": "Content-Length", - "description": "" - }, - { - "key": "Content-Type", - "value": "application/json; charset=utf-8", - "name": "Content-Type", - "description": "" - }, - { - "key": "Date", - "value": "Thu, 29 Oct 2015 06:16:29 GMT", - "name": "Date", - "description": "" - }, - { - "key": "Server", - "value": "nginx/1.6.2", - "name": "Server", - "description": "" - }, - { - "key": "Vary", - "value": "Accept-Encoding", - "name": "Vary", - "description": "" - }, - { - "key": "X-Powered-By", - "value": "Sails ", - "name": "X-Powered-By", - "description": "" - } - ], - "cookie": [], - "body": "{\"cookies\":{\"foo2\":\"bar\"}}" - } - ] - }, - { - "name": "Delete Cookies", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// handle case where it is 304", - "", - "if (responseCode.code === 200) {", - " tests[\"Status code is 302 or 200\"] = true;", - " tests[\"Body contains cookies\"] = responseBody.has(\"cookies\");", - " ", - " var body = JSON.parse(responseBody);", - " tests[\"Body contains cookie foo1\"] = 'foo1' in body.cookies;", - " tests[\"Body contains cookie foo2\"] = 'foo2' in body.cookies;", - "", - "}", - "else if (responseCode.code === 302) {", - " tests[\"Status code is 302 or 200\"] = true;", - " tests[\"Body has redirection message\"] = responseBody.has(\"Found. Redirecting to /cookies\")", - "}", - "else {", - " tests[\"Status code is 302 or 200\"] = false;", - "}", - "", - "tests[\"foo1 cookie is set\"] = _.get(postman.getResponseCookie('foo1'), 'value') === undefined;", - "", - "tests[\"foo2 cookie is set\"] = _.get(postman.getResponseCookie('foo2'), 'value') === undefined;" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "formdata", - "formdata": [] - }, - "url": { - "raw": "{{base_url}}/{{app_url}}/cookies/delete?foo1&foo2", - "host": [ - "{{base_url}}" - ], - "path": [ - "{{app_url}}", - "cookies", - "delete" - ], - "query": [ - { - "key": "foo1", - "value": null - }, - { - "key": "foo2", - "value": null - } - ] - }, - "description": "One or more cookies that has been set for this domain can be deleted by providing the cookie names as part of the URL parameter. The response of this request is a JSON containing the list of currently set cookies." - }, - "response": [ - { - "name": "Cookies Response", - "originalRequest": { - "method": "GET", - "header": [], - "body": { - "mode": "formdata", - "formdata": [] - }, - "url": { - "raw": "{{base_url}}/{{app_url}}/cookies/delete?foo1&foo2", - "host": [ - "{{base_url}}" - ], - "path": [ - "{{app_url}}", - "cookies", - "delete" - ], - "query": [ - { - "key": "foo1", - "value": null - }, - { - "key": "foo2", - "value": null - } - ] - }, - "description": "One or more cookies that has been set for this domain can be deleted by providing the cookie names as part of the URL parameter. The response of this request is a JSON containing the list of currently set cookies." - }, - "status": "OK", - "code": 200, - "_postman_previewlanguage": "javascript", - "header": [ - { - "key": "Access-Control-Allow-Credentials", - "value": "", - "name": "Access-Control-Allow-Credentials", - "description": "" - }, - { - "key": "Access-Control-Allow-Headers", - "value": "", - "name": "Access-Control-Allow-Headers", - "description": "" - }, - { - "key": "Access-Control-Allow-Methods", - "value": "", - "name": "Access-Control-Allow-Methods", - "description": "" - }, - { - "key": "Access-Control-Allow-Origin", - "value": "", - "name": "Access-Control-Allow-Origin", - "description": "" - }, - { - "key": "Connection", - "value": "keep-alive", - "name": "Connection", - "description": "" - }, - { - "key": "Content-Encoding", - "value": "gzip", - "name": "Content-Encoding", - "description": "" - }, - { - "key": "Content-Length", - "value": "46", - "name": "Content-Length", - "description": "" - }, - { - "key": "Content-Type", - "value": "application/json; charset=utf-8", - "name": "Content-Type", - "description": "" - }, - { - "key": "Date", - "value": "Thu, 29 Oct 2015 06:16:00 GMT", - "name": "Date", - "description": "" - }, - { - "key": "Server", - "value": "nginx/1.6.2", - "name": "Server", - "description": "" - }, - { - "key": "Vary", - "value": "Accept-Encoding", - "name": "Vary", - "description": "" - }, - { - "key": "X-Powered-By", - "value": "Sails ", - "name": "X-Powered-By", - "description": "" - } - ], - "cookie": [], - "body": "{\"cookies\":{\"foo2\":\"bar\"}}" - } - ] - } - ], - "description": "The cookie related endpoints allow one to get, set and delete simple cookies.\n\nCookies are small snippets of information that is stored in the browser and sent back to the server with every subsequent requests in order to store useful information between requests.\nIf you want to know more about cookies, read the [HTTP Cookie](https://en.wikipedia.org/wiki/HTTP_cookie) article on wikipedia." - }, - { - "name": "Headers", - "item": [ - { - "name": "Request Headers", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "var responseJSON;", - "try {", - " tests[\"Body contains headers\"] = responseBody.has(\"headers\");", - " responseJSON = JSON.parse(responseBody);", - " tests[\"Header contains host\"] = \"host\" in responseJSON.headers;", - " tests[\"Header contains test parameter sent as part of request header\"] = \"my-sample-header\" in responseJSON.headers;", - "}", - "catch (e) { }", - "", - "", - "", - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "my-sample-header", - "value": "Lorem ipsum dolor sit amet" - } - ], - "body": { - "mode": "formdata", - "formdata": [] - }, - "url": { - "raw": "{{base_url}}/{{app_url}}/headers", - "host": [ - "{{base_url}}" - ], - "path": [ - "{{app_url}}", - "headers" - ] - }, - "description": "A `GET` request to this endpoint returns the list of all request headers as part of the response JSON.\nIn Postman, sending your own set of headers through the [Headers tab](https://www.getpostman.com/docs/requests#headers?source=echo-collection-app-onboarding) will reveal the headers as part of the response." - }, - "response": [ - { - "name": "my-sample-header", - "originalRequest": { - "method": "GET", - "header": [ - { - "key": "my-sample-header", - "value": "Lorem ipsum dolor sit amet", - "enabled": true - } - ], - "body": { - "mode": "formdata", - "formdata": [] - }, - "url": { - "raw": "{{base_url}}/{{app_url}}/headers", - "host": [ - "{{base_url}}" - ], - "path": [ - "{{app_url}}", - "headers" - ] - } - }, - "status": "OK", - "code": 200, - "_postman_previewlanguage": "javascript", - "header": [ - { - "key": "Access-Control-Allow-Credentials", - "value": "", - "name": "Access-Control-Allow-Credentials", - "description": "" - }, - { - "key": "Access-Control-Allow-Headers", - "value": "", - "name": "Access-Control-Allow-Headers", - "description": "" - }, - { - "key": "Access-Control-Allow-Methods", - "value": "", - "name": "Access-Control-Allow-Methods", - "description": "" - }, - { - "key": "Access-Control-Allow-Origin", - "value": "", - "name": "Access-Control-Allow-Origin", - "description": "" - }, - { - "key": "Connection", - "value": "keep-alive", - "name": "Connection", - "description": "" - }, - { - "key": "Content-Encoding", - "value": "gzip", - "name": "Content-Encoding", - "description": "" - }, - { - "key": "Content-Length", - "value": "342", - "name": "Content-Length", - "description": "" - }, - { - "key": "Content-Type", - "value": "application/json; charset=utf-8", - "name": "Content-Type", - "description": "" - }, - { - "key": "Date", - "value": "Thu, 31 Mar 2016 11:14:00 GMT", - "name": "Date", - "description": "" - }, - { - "key": "Server", - "value": "nginx/1.6.2", - "name": "Server", - "description": "" - }, - { - "key": "Vary", - "value": "Accept-Encoding", - "name": "Vary", - "description": "" - }, - { - "key": "X-Powered-By", - "value": "Sails ", - "name": "X-Powered-By", - "description": "" - } - ], - "cookie": [ - { - "expires": "Invalid Date", - "hostOnly": true, - "httpOnly": true, - "domain": "echo.getpostman.com", - "path": "/", - "secure": false, - "session": true, - "value": "s%3A9stja5zKmIILxq9Jvtha7Lp9LIz1VIdK.Vp8MHC%2BEUJe4ICZPXn2JAoXaV2bTgtoQd%2B3XJLNr51Y", - "key": "sails.sid" - } - ], - "body": "{\"headers\":{\"host\":\"echo.getpostman.com\",\"accept\":\"*/*\",\"accept-encoding\":\"gzip, deflate, sdch\",\"accept-language\":\"en-US,en;q=0.8\",\"cache-control\":\"no-cache\",\"my-sample-header\":\"Lorem ipsum dolor sit amet\",\"postman-token\":\"3c8ea80b-f599-fba6-e0b4-a0910440e7b6\",\"user-agent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.110 Safari/537.36\",\"x-forwarded-port\":\"443\",\"x-forwarded-proto\":\"https\"}}" - } - ] - }, - { - "name": "Response Headers", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "tests[\"Body contains Content-Type\"] = responseBody.has(\"Content-Type\");", - "tests[\"response headers have key sent as part of request\"] = (postman.getResponseHeader('test') == 'response_headers')" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "formdata", - "formdata": [] - }, - "url": { - "raw": "{{base_url}}/{{app_url}}/response-headers?Content-Type=text/html&test=response_headers", - "host": [ - "{{base_url}}" - ], - "path": [ - "{{app_url}}", - "response-headers" - ], - "query": [ - { - "key": "Content-Type", - "value": "text/html" - }, - { - "key": "test", - "value": "response_headers" - } - ] - }, - "description": "This endpoint causes the server to send custom set of response headers. Providing header values as part of the URL parameters of a `GET` request to this endpoint returns the same as part of response header.\n\nTo send your own set of headers, simply add or replace the the URL parameters with your own set." - }, - "response": [ - { - "name": "Response headers", - "originalRequest": { - "method": "GET", - "header": [], - "body": { - "mode": "formdata", - "formdata": [] - }, - "url": { - "raw": "{{base_url}}/{{app_url}}/response-headers?Content-Type=text/html&test=response_headers", - "host": [ - "{{base_url}}" - ], - "path": [ - "{{app_url}}", - "response-headers" - ], - "query": [ - { - "key": "Content-Type", - "value": "text/html" - }, - { - "key": "test", - "value": "response_headers" - } - ] - } - }, - "status": "OK", - "code": 200, - "_postman_previewlanguage": "html", - "header": [ - { - "key": "Access-Control-Allow-Credentials", - "value": "", - "name": "Access-Control-Allow-Credentials", - "description": "" - }, - { - "key": "Access-Control-Allow-Headers", - "value": "", - "name": "Access-Control-Allow-Headers", - "description": "" - }, - { - "key": "Access-Control-Allow-Methods", - "value": "", - "name": "Access-Control-Allow-Methods", - "description": "" - }, - { - "key": "Access-Control-Allow-Origin", - "value": "", - "name": "Access-Control-Allow-Origin", - "description": "" - }, - { - "key": "Connection", - "value": "keep-alive", - "name": "Connection", - "description": "" - }, - { - "key": "Content-Encoding", - "value": "gzip", - "name": "Content-Encoding", - "description": "" - }, - { - "key": "Content-Length", - "value": "71", - "name": "Content-Length", - "description": "" - }, - { - "key": "Content-Type", - "value": "text/html; charset=utf-8", - "name": "Content-Type", - "description": "" - }, - { - "key": "Date", - "value": "Thu, 31 Mar 2016 11:14:18 GMT", - "name": "Date", - "description": "" - }, - { - "key": "Server", - "value": "nginx/1.6.2", - "name": "Server", - "description": "" - }, - { - "key": "Vary", - "value": "Accept-Encoding", - "name": "Vary", - "description": "" - }, - { - "key": "X-Powered-By", - "value": "Sails ", - "name": "X-Powered-By", - "description": "" - }, - { - "key": "test", - "value": "response_headers", - "name": "test", - "description": "" - } - ], - "cookie": [ - { - "expires": "Invalid Date", - "hostOnly": true, - "httpOnly": true, - "domain": "echo.getpostman.com", - "path": "/", - "secure": false, - "session": true, - "value": "s%3A9stja5zKmIILxq9Jvtha7Lp9LIz1VIdK.Vp8MHC%2BEUJe4ICZPXn2JAoXaV2bTgtoQd%2B3XJLNr51Y", - "key": "sails.sid" - } - ], - "body": "{\"Content-Type\":\"text/html\",\"test\":\"response_headers\"}" - } - ] - } - ], - "description": "The following set of endpoints allow one to see the headers being sent as part of a request and to get a custom set of headers as part of response.\n\nHTTP header fields provide required information about the request or response, or about the object sent in the message body. Both request headers and response headers can be controlled using these endpoints." - }, - { - "name": "Request Methods", - "item": [ - { - "name": "GET Request", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "var responseJSON;", - "", - "try { ", - " responseJSON = JSON.parse(responseBody); ", - " tests['response is valid JSON'] = true;", - "}", - "catch (e) { ", - " responseJSON = {}; ", - " tests['response is valid JSON'] = false;", - "}", - "", - "tests['response json contains headers'] = _.has(responseJSON, 'headers');", - "tests['response json contains args'] = _.has(responseJSON, 'args');", - "tests['response json contains url'] = _.has(responseJSON, 'url');", - "", - "tests['args key contains argument passed as url parameter'] = ('test' in responseJSON.args);", - "tests['args passed via request url params has value \"123\"'] = (_.get(responseJSON, 'args.test') === \"123\");" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "protocolProfileBehavior": { - "disableBodyPruning": true - }, - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "formdata", - "formdata": [] - }, - "url": { - "raw": "{{base_url}}/{{app_url}}/get?test=123", - "host": [ - "{{base_url}}" - ], - "path": [ - "{{app_url}}", - "get" - ], - "query": [ - { - "key": "test", - "value": "123" - } - ] - }, - "description": "The HTTP `GET` request method is meant to retrieve data from a server. The data\nis identified by a unique URI (Uniform Resource Identifier). \n\nA `GET` request can pass parameters to the server using \"Query String \nParameters\". For example, in the following request,\n\n> http://example.com/hi/there?hand=wave\n\nThe parameter \"hand\" has the value \"wave\".\n\nThis endpoint echoes the HTTP headers, request parameters and the complete\nURI requested." - }, - "response": [] - }, - { - "name": "POST Raw Text", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "var responseJSON;", - "", - "try { ", - " responseJSON = JSON.parse(responseBody); ", - " tests['response is valid JSON'] = true;", - "}", - "catch (e) { ", - " responseJSON = {}; ", - " tests['response is valid JSON'] = false;", - "}", - "", - "", - "tests['response has post data'] = _.has(responseJSON, 'data');", - "tests['response matches the data posted'] = (responseJSON.data && responseJSON.data.length === 256);", - "", - "tests[\"content-type equals text/plain\"] = responseJSON && responseJSON.headers && (responseJSON.headers[\"content-type\"] === 'text/plain');" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "text/plain" - } - ], - "body": { - "mode": "raw", - "raw": "Duis posuere augue vel cursus pharetra. In luctus a ex nec pretium. Praesent neque quam, tincidunt nec leo eget, rutrum vehicula magna.\nMaecenas consequat elementum elit, id semper sem tristique et. Integer pulvinar enim quis consectetur interdum volutpat." - }, - "url": { - "raw": "{{base_url}}/{{app_url}}/post", - "host": [ - "{{base_url}}" - ], - "path": [ - "{{app_url}}", - "post" - ] - }, - "description": "The HTTP `POST` request method is meant to transfer data to a server \n(and elicit a response). What data is returned depends on the implementation\nof the server.\n\nA `POST` request can pass parameters to the server using \"Query String \nParameters\", as well as the Request Body. For example, in the following request,\n\n> POST /hi/there?hand=wave\n>\n> \n\nThe parameter \"hand\" has the value \"wave\". The request body can be in multiple\nformats. These formats are defined by the MIME type of the request. The MIME \nType can be set using the ``Content-Type`` HTTP header. The most commonly used \nMIME types are:\n\n* `multipart/form-data`\n* `application/x-www-form-urlencoded`\n* `application/json`\n\nThis endpoint echoes the HTTP headers, request parameters, the contents of\nthe request body and the complete URI requested." - }, - "response": [] - }, - { - "name": "POST Form Data", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "var responseJSON;", - "", - "try { ", - " responseJSON = JSON.parse(responseBody); ", - " tests['response is valid JSON'] = true;", - "}", - "catch (e) { ", - " responseJSON = {}; ", - " tests['response is valid JSON'] = false;", - "}", - "", - "", - "tests['response has post data'] = _.has(responseJSON, 'form');", - "tests['response matches the data posted'] = (responseJSON.form && responseJSON.form.strange === 'boom');" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded" - } - ], - "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "strange", - "value": "boom", - "type": "text" - } - ] - }, - "url": { - "raw": "{{base_url}}/{{app_url}}/post", - "host": [ - "{{base_url}}" - ], - "path": [ - "{{app_url}}", - "post" - ] - }, - "description": "The HTTP `POST` request method is meant to transfer data to a server \n(and elicit a response). What data is returned depends on the implementation\nof the server.\n\nA `POST` request can pass parameters to the server using \"Query String \nParameters\", as well as the Request Body. For example, in the following request,\n\n> POST /hi/there?hand=wave\n>\n> \n\nThe parameter \"hand\" has the value \"wave\". The request body can be in multiple\nformats. These formats are defined by the MIME type of the request. The MIME \nType can be set using the ``Content-Type`` HTTP header. The most commonly used \nMIME types are:\n\n* `multipart/form-data`\n* `application/x-www-form-urlencoded`\n* `application/json`\n\nThis endpoint echoes the HTTP headers, request parameters, the contents of\nthe request body and the complete URI requested when data is sent as a form parameter." - }, - "response": [] - }, - { - "name": "PUT Request", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "var responseJSON;", - "", - "try { ", - " responseJSON = JSON.parse(responseBody); ", - " tests['response is valid JSON'] = true;", - "}", - "catch (e) { ", - " responseJSON = {}; ", - " tests['response is valid JSON'] = false;", - "}", - "", - "", - "tests['response has PUT data'] = _.has(responseJSON, 'data');", - "tests['response matches the data sent in request'] = (responseJSON.data && responseJSON.data.length === 256);" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "PUT", - "header": [], - "body": { - "mode": "raw", - "raw": "Etiam mi lacus, cursus vitae felis et, blandit pellentesque neque. Vestibulum eget nisi a tortor commodo dignissim.\nQuisque ipsum ligula, faucibus a felis a, commodo elementum nisl. Mauris vulputate sapien et tincidunt viverra. Donec vitae velit nec metus." - }, - "url": { - "raw": "{{base_url}}/{{app_url}}/put", - "host": [ - "{{base_url}}" - ], - "path": [ - "{{app_url}}", - "put" - ] - }, - "description": "The HTTP `PUT` request method is similar to HTTP `POST`. It too is meant to \ntransfer data to a server (and elicit a response). What data is returned depends on the implementation\nof the server.\n\nA `PUT` request can pass parameters to the server using \"Query String \nParameters\", as well as the Request Body. For example, in the following \nraw HTTP request,\n\n> PUT /hi/there?hand=wave\n>\n> \n\n\n" - }, - "response": [] - }, - { - "name": "PATCH Request", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "var responseJSON;", - "", - "try { ", - " responseJSON = JSON.parse(responseBody); ", - " tests['response is valid JSON'] = true;", - "}", - "catch (e) { ", - " responseJSON = {}; ", - " tests['response is valid JSON'] = false;", - "}", - "", - "", - "tests['response has PUT data'] = _.has(responseJSON, 'data');", - "tests['response matches the data sent in request'] = (responseJSON.data && responseJSON.data.length === 256);" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "PATCH", - "header": [], - "body": { - "mode": "raw", - "raw": "Curabitur auctor, elit nec pulvinar porttitor, ex augue condimentum enim, eget suscipit urna felis quis neque.\nSuspendisse sit amet luctus massa, nec venenatis mi. Suspendisse tincidunt massa at nibh efficitur fringilla. Nam quis congue mi. Etiam volutpat." - }, - "url": { - "raw": "{{base_url}}/{{app_url}}/patch", - "host": [ - "{{base_url}}" - ], - "path": [ - "{{app_url}}", - "patch" - ] - }, - "description": "The HTTP `PATCH` method is used to update resources on a server. The exact\nuse of `PATCH` requests depends on the server in question. There are a number\nof server implementations which handle `PATCH` differently. Technically, \n`PATCH` supports both Query String parameters and a Request Body.\n\nThis endpoint accepts an HTTP `PATCH` request and provides debug information\nsuch as the HTTP headers, Query String arguments, and the Request Body." - }, - "response": [] - }, - { - "name": "DELETE Request", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "var responseJSON;", - "", - "try { ", - " responseJSON = JSON.parse(responseBody); ", - " tests['response is valid JSON'] = true;", - "}", - "catch (e) { ", - " responseJSON = {}; ", - " tests['response is valid JSON'] = false;", - "}", - "", - "", - "tests['response has PUT data'] = _.has(responseJSON, 'data');", - "tests['response matches the data sent in request'] = (responseJSON.data && responseJSON.data.length === 256);" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "DELETE", - "header": [], - "body": { - "mode": "raw", - "raw": "Donec fermentum, nisi sed cursus eleifend, nulla tortor ultricies tellus, ut vehicula orci arcu ut velit. In volutpat egestas dapibus.\nMorbi condimentum vestibulum sapien. Etiam dignissim diam quis eros lobortis gravida vel lobortis est. Etiam gravida sed." - }, - "url": { - "raw": "{{base_url}}/{{app_url}}/delete", - "host": [ - "{{base_url}}" - ], - "path": [ - "{{app_url}}", - "delete" - ] - }, - "description": "The HTTP `DELETE` method is used to delete resources on a server. The exact\nuse of `DELETE` requests depends on the server implementation. In general, \n`DELETE` requests support both, Query String parameters as well as a Request \nBody.\n\nThis endpoint accepts an HTTP `DELETE` request and provides debug information\nsuch as the HTTP headers, Query String arguments, and the Request Body." - }, - "response": [] - } - ], - "description": "HTTP has multiple request \"verbs\", such as `GET`, `PUT`, `POST`, `DELETE`,\n`PATCH`, `HEAD`, etc. \n\nAn HTTP Method (verb) defines how a request should be interpreted by a server. \nThe endpoints in this section demonstrate various HTTP Verbs. Postman supports \nall the HTTP Verbs, including some rarely used ones, such as `PROPFIND`, `UNLINK`, \netc.\n\nFor details about HTTP Verbs, refer to [RFC 2616](http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9)\n" - }, - { - "name": "Utilities", - "item": [ - { - "name": "Response Status Code", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "var responseJSON;", - "try {", - " responseJSON = JSON.parse(responseBody); ", - " tests[\"Status equals 200\"] = responseJSON.status === 200;", - "}", - "catch (e) { }", - "tests[\"Body contains status\"] = responseBody.has(\"status\");", - "", - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "formdata", - "formdata": [] - }, - "url": { - "raw": "{{base_url}}/{{app_url}}/status/200", - "host": [ - "{{base_url}}" - ], - "path": [ - "{{app_url}}", - "status", - "200" - ] - }, - "description": "This endpoint allows one to instruct the server which status code to respond with.\n\nEvery response is accompanied by a status code. The status code provides a summary of the nature of response sent by the server. For example, a status code of `200` means everything is okay with the response and a code of `404` implies that the requested URL does not exist on server. \nA list of all valid HTTP status code can be found at the [List of Status Codes](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes) wikipedia article. When using Postman, the response status code is described for easy reference.\n\nNote that if an invalid status code is requested to be sent, the server returns a status code of `400 Bad Request`." - }, - "response": [ - { - "name": "200", - "originalRequest": { - "method": "GET", - "header": [], - "body": { - "mode": "formdata", - "formdata": [] - }, - "url": { - "raw": "https://echo.getpostman.com/status/200", - "protocol": "https", - "host": [ - "echo", - "getpostman", - "com" - ], - "path": [ - "status", - "200" - ] - } - }, - "status": "OK", - "code": 200, - "_postman_previewlanguage": "javascript", - "header": [ - { - "key": "Access-Control-Allow-Credentials", - "value": "", - "name": "Access-Control-Allow-Credentials", - "description": "" - }, - { - "key": "Access-Control-Allow-Headers", - "value": "", - "name": "Access-Control-Allow-Headers", - "description": "" - }, - { - "key": "Access-Control-Allow-Methods", - "value": "", - "name": "Access-Control-Allow-Methods", - "description": "" - }, - { - "key": "Access-Control-Allow-Origin", - "value": "", - "name": "Access-Control-Allow-Origin", - "description": "" - }, - { - "key": "Connection", - "value": "keep-alive", - "name": "Connection", - "description": "" - }, - { - "key": "Content-Length", - "value": "14", - "name": "Content-Length", - "description": "" - }, - { - "key": "Content-Type", - "value": "application/json; charset=utf-8", - "name": "Content-Type", - "description": "" - }, - { - "key": "Date", - "value": "Thu, 31 Mar 2016 11:58:47 GMT", - "name": "Date", - "description": "" - }, - { - "key": "ETag", - "value": "W/\"e-1056260003\"", - "name": "ETag", - "description": "" - }, - { - "key": "Server", - "value": "nginx/1.6.2", - "name": "Server", - "description": "" - }, - { - "key": "Vary", - "value": "Accept-Encoding", - "name": "Vary", - "description": "" - }, - { - "key": "X-Powered-By", - "value": "Sails ", - "name": "X-Powered-By", - "description": "" - } - ], - "cookie": [ - { - "expires": "Fri Apr 15 2016 13:14:58 GMT+0200 (heure d’été d’Europe centrale)", - "hostOnly": false, - "httpOnly": false, - "domain": ".getpostman.com", - "path": "/", - "secure": false, - "session": false, - "value": "yes", - "key": "getpostmanlogin" - }, - { - "expires": "Fri Apr 15 2016 13:14:58 GMT+0200 (heure d’été d’Europe centrale)", - "hostOnly": false, - "httpOnly": false, - "domain": ".getpostman.com", - "path": "/", - "secure": false, - "session": false, - "value": "df0c0256028d7ec4d641f766104a9571a8e249685bbc667d7cee030bbf44d3209495c70c03248e31e678a93812591d5e12187a8e99bf6bc5e80c40903f6ff6226938f24e413c0ffa613a7372064ec44a8594e8d3ede6945e34394f369573feeebc4a73a3e24b8c9ac18a53704addb5fd3f71f1ede488ff551feb059e9c1fb208164814e45e0312c4df8ea6e83c26702f42ae634c6afbe82d57c857bbf5598b5527961c1c28688dc2580070a4389f0cf4ec0a179b5b9c11b2ecbaa5460d374065bf5c7a3add9505df0fa89acb9f227f05ed2d4c6b58c39d6d728bd49f6f323ae67d4a75882aa7682f5d6fc5b981ba411d94aa93970bfaefa1953a73e440d50d012b5f288975c888e2345ee7777e746fb5aed3a7b2dbc087c6456621aa78c24a3c17c5f96cf59844933249a352f631e2008cffac6faf06d0e253dcc01cf0067bf56c1fbc5ed61fec1861b60c5accf35ffc2e56154a113004fa1db9d7171c3af8fc063918554092f5", - "key": "postman.sid" - }, - { - "expires": "Sat Mar 31 2018 13:16:21 GMT+0200 (heure d’été d’Europe centrale)", - "hostOnly": false, - "httpOnly": false, - "domain": ".echo.getpostman.com", - "path": "/", - "secure": false, - "session": false, - "value": "GA1.3.1703443399.1459422978", - "key": "_ga" - }, - { - "expires": "Invalid Date", - "hostOnly": true, - "httpOnly": true, - "domain": "echo.getpostman.com", - "path": "/", - "secure": false, - "session": true, - "value": "s%3AvuHU0EKeDbyNjVrEc7U30dMPzVu8CRaD.GOV1H9olcVzXqrwqP%2BC%2B6MVj2UczXivcN00jgPoDYfs", - "key": "sails.sid" - } - ], - "body": "{\"status\":200}" - } - ] - }, - { - "name": "Streamed Response", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "tests[\"response code is 200\"] = responseCode.code === 200;", - "tests[\"response is sent in chunks\"] = (postman.getResponseHeader('Transfer-Encoding') === 'chunked')", - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "formdata", - "formdata": [] - }, - "url": { - "raw": "{{base_url}}/{{app_url}}/stream/10", - "host": [ - "{{base_url}}" - ], - "path": [ - "{{app_url}}", - "stream", - "10" - ] - }, - "description": "This endpoint allows one to recieve streaming http response using [chunked transfer encoding](https://en.wikipedia.org/wiki/Chunked_transfer_encoding) of a configurable length.\n\nA streaming response does not wait for the entire response to be generated on server before flushing it out. This implies that for a fairly large response, parts of it can be streamed to the requestee as and when it is generated on server. The client can then take actions of processing this partially received data." - }, - "response": [] - }, - { - "name": "Delay Response", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "var responseJSON;", - "try { ", - " responseJSON = JSON.parse(responseBody); ", - " tests[\"response body has key delay\"] = 'delay' in responseJSON;", - "}", - "catch (e) { }", - "tests[\"response code is 200\"] = responseCode.code === 200;", - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "formdata", - "formdata": [] - }, - "url": { - "raw": "{{base_url}}/{{app_url}}/delay/3", - "host": [ - "{{base_url}}" - ], - "path": [ - "{{app_url}}", - "delay", - "3" - ] - }, - "description": "Using this endpoint one can configure how long it takes for the server to come back with a response. Appending a number to the URL defines the time (in seconds) the server will wait before responding.\n\nNote that a maximum delay of 10 seconds is accepted by the server." - }, - "response": [ - { - "name": "success-response", - "originalRequest": { - "method": "GET", - "header": [], - "body": { - "mode": "formdata", - "formdata": [] - }, - "url": { - "raw": "https://echo.getpostman.com/delay/3", - "protocol": "https", - "host": [ - "echo", - "getpostman", - "com" - ], - "path": [ - "delay", - "3" - ] - } - }, - "status": "OK", - "code": 200, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Access-Control-Allow-Credentials", - "value": "", - "name": "Access-Control-Allow-Credentials", - "description": "" - }, - { - "key": "Access-Control-Allow-Headers", - "value": "", - "name": "Access-Control-Allow-Headers", - "description": "" - }, - { - "key": "Access-Control-Allow-Methods", - "value": "", - "name": "Access-Control-Allow-Methods", - "description": "" - }, - { - "key": "Access-Control-Allow-Origin", - "value": "", - "name": "Access-Control-Allow-Origin", - "description": "" - }, - { - "key": "Access-Control-Expose-Headers", - "value": "", - "name": "Access-Control-Expose-Headers", - "description": "" - }, - { - "key": "Connection", - "value": "keep-alive", - "name": "Connection", - "description": "" - }, - { - "key": "Content-Length", - "value": "13", - "name": "Content-Length", - "description": "" - }, - { - "key": "Content-Type", - "value": "application/json; charset=utf-8", - "name": "Content-Type", - "description": "" - }, - { - "key": "Date", - "value": "Mon, 02 Jan 2017 09:19:03 GMT", - "name": "Date", - "description": "" - }, - { - "key": "ETag", - "value": "W/\"d-t/L/D5c0SDl+MoXtKdSVOg\"", - "name": "ETag", - "description": "" - }, - { - "key": "Server", - "value": "nginx/1.10.1", - "name": "Server", - "description": "" - }, - { - "key": "Vary", - "value": "Accept-Encoding", - "name": "Vary", - "description": "" - } - ], - "cookie": [ - { - "expires": "Mon Jan 18 2038 22:44:07 GMT+0100 (heure normale d’Europe centrale)", - "httpOnly": true, - "domain": "echo.getpostman.com", - "path": "/", - "secure": false, - "value": "s%3AYjUiFBtGiJVL2a-qzZQZ1DFlAMhgXN9O.WaAjRUV0OteZxwmhbNibuB7VKse068JJIh6PwLQUKmQ", - "key": "sails.sid" - } - ], - "body": "{\"delay\":\"3\"}" - } - ] - }, - { - "name": "Get UTF8 Encoded Response", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "tests[\"response code is 200\"] = responseCode.code === 200;" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "formdata", - "formdata": [] - }, - "url": { - "raw": "{{base_url}}/{{app_url}}/encoding/utf8", - "host": [ - "{{base_url}}" - ], - "path": [ - "{{app_url}}", - "encoding", - "utf8" - ] - }, - "description": "If a response of an endpoint requires to send data beyond the basic English / ASCII character set, the `charset` parameter in the `Content-Type` response header defines the character encoding policy.\n\nThis endpoint returns an `UTF8` character encoded response body with text in various languages such as Greek, Latin, East Asian, etc. Postman can interpret the character encoding and use appropriate methods to display the character set in responses." - }, - "response": [] - }, - { - "name": "GZip Compressed Response", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "try {", - " var data = JSON.parse(responseBody);", - " tests[\"Body contains gzipped\"] = responseBody.has(\"gzipped\");", - " tests[\"Body contains headers\"] = responseBody.has(\"headers\");", - " tests[\"Body contains method\"] = responseBody.has(\"method\");", - "}", - "catch(e) {", - " console.log('Cannot parse response,probably not a JSON');", - "}", - "tests[\"response code is 200\"] = responseCode.code === 200;" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "formdata", - "formdata": [] - }, - "url": { - "raw": "{{base_url}}/{{app_url}}/gzip", - "host": [ - "{{base_url}}" - ], - "path": [ - "{{app_url}}", - "gzip" - ] - }, - "description": "This endpoint returns the response using [gzip compression algoritm](https://en.wikipedia.org/wiki/Gzip).\nThe uncompressed response is a JSON string containing the details of the request sent by the client. For this endpoint to work, one should request with `Accept-encoding` header containing `gzip` as part of its value. Postman supports gzip, deflate and SDCH decoding and automatically sends them as part of the request.\n\nHTTP Compression allows the server to send responses in a compressed format, which is uncompressed by the client before processing. This reduces network bandwidth consumption at the cost of increase in CPU usage.\nTo know more about this, refer the [HTTP Compression](https://en.wikipedia.org/wiki/HTTP_compression) wikipedia article." - }, - "response": [] - }, - { - "name": "Deflate Compressed Response", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "tests[\"response code is 200\"] = responseCode.code === 200;", - "", - "try {", - " var data = JSON.parse(responseBody);", - " tests[\"Body contains deflated\"] = responseBody.has(\"deflated\");", - " tests[\"Body contains headers\"] = responseBody.has(\"headers\");", - " tests[\"Body contains method\"] = responseBody.has(\"method\");", - "}", - "catch(e) {", - " console.log('Cannot parse response,probably not a JSON');", - "}", - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "formdata", - "formdata": [] - }, - "url": { - "raw": "{{base_url}}/{{app_url}}/deflate", - "host": [ - "{{base_url}}" - ], - "path": [ - "{{app_url}}", - "deflate" - ] - }, - "description": "This endpoint returns the response using [deflate compression algoritm](https://en.wikipedia.org/wiki/DEFLATE). \nThe uncompressed response is a JSON string containing the details of the request sent by the client. For this endpoint to work, one should request with `Accept-encoding` header containing `deflate` as part of its value. Postman supports gzip, deflate and SDCH decoding and automatically sends them as part of the request.\n\nHTTP Compression allows the server to send responses in a compressed format, which is uncompressed by the client before processing. This reduces network bandwidth consumption at the cost of increase in CPU usage.\nTo know more about this, refer the [HTTP Compression](https://en.wikipedia.org/wiki/HTTP_compression) wikipedia article." - }, - "response": [] - }, - { - "name": "IP address in JSON format", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "var body = JSON.parse(responseBody);", - "", - "tests[\"Status code is 200\"] = responseCode.code === 200;", - "tests[\"Valid response structure\"] = /^[a-fA-F:\\.0-9]+$/.test(body.ip);" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "formdata", - "formdata": [] - }, - "url": { - "raw": "{{base_url}}/{{app_url}}/ip", - "host": [ - "{{base_url}}" - ], - "path": [ - "{{app_url}}", - "ip" - ] - }, - "description": "A simple `GET` request to return the IP address of the source request in the following `JSON` format:\n\n```json\n{\n ip: \"request-ip-address\"\n}\n```" - }, - "response": [] - } - ] - }, - { - "name": "Utilities / Date and Time", - "item": [ - { - "name": "Current UTC time", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "tests[\"Status code is 200\"] = responseCode.code === 200;", - "tests[\"Time is in a valid format\"] = responseBody === postman.getResponseHeader(\"date\");", - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "formdata", - "formdata": [] - }, - "url": { - "raw": "{{base_url}}/{{app_url}}/time/now", - "host": [ - "{{base_url}}" - ], - "path": [ - "{{app_url}}", - "time", - "now" - ] - }, - "description": "A simple `GET` request to `/time/now` to return the current timestamp as a UTC string.\n\n```\nFri, 04 Nov 2016 09:00:46 GMT\n```" - }, - "response": [ - { - "name": "time as text", - "originalRequest": { - "method": "GET", - "header": [], - "body": { - "mode": "formdata", - "formdata": [] - }, - "url": { - "raw": "https://postman-echo.com/time/now", - "protocol": "https", - "host": [ - "postman-echo", - "com" - ], - "path": [ - "time", - "now" - ] - } - }, - "status": "OK", - "code": 200, - "_postman_previewlanguage": "html", - "header": [ - { - "key": "Access-Control-Allow-Credentials", - "value": "", - "name": "Access-Control-Allow-Credentials", - "description": "" - }, - { - "key": "Access-Control-Allow-Headers", - "value": "", - "name": "Access-Control-Allow-Headers", - "description": "" - }, - { - "key": "Access-Control-Allow-Methods", - "value": "", - "name": "Access-Control-Allow-Methods", - "description": "" - }, - { - "key": "Access-Control-Allow-Origin", - "value": "", - "name": "Access-Control-Allow-Origin", - "description": "" - }, - { - "key": "Access-Control-Expose-Headers", - "value": "", - "name": "Access-Control-Expose-Headers", - "description": "" - }, - { - "key": "Connection", - "value": "keep-alive", - "name": "Connection", - "description": "" - }, - { - "key": "Content-Encoding", - "value": "gzip", - "name": "Content-Encoding", - "description": "" - }, - { - "key": "Content-Length", - "value": "49", - "name": "Content-Length", - "description": "" - }, - { - "key": "Content-Type", - "value": "text/html; charset=utf-8", - "name": "Content-Type", - "description": "" - }, - { - "key": "Date", - "value": "Wed, 11 Jan 2017 10:27:12 GMT", - "name": "Date", - "description": "" - }, - { - "key": "ETag", - "value": "W/\"1d-2jJhkzratfVX9VZ0+raHbw\"", - "name": "ETag", - "description": "" - }, - { - "key": "Server", - "value": "nginx/1.10.1", - "name": "Server", - "description": "" - }, - { - "key": "Vary", - "value": "Accept-Encoding", - "name": "Vary", - "description": "" - }, - { - "key": "set-cookie", - "value": "sails.sid=s%3A2lT3TO7qS1tadeSAp4axl-NcXG9CV6Rf.HGqLY%2FlKEKY4fgCLePaAZs3tCHp%2Bglf7ZOJYlonGeig; Path=/; HttpOnly", - "name": "set-cookie", - "description": "" - } - ], - "cookie": [ - { - "expires": "Mon Jan 18 2038 22:44:07 GMT+0100 (heure normale d’Europe centrale)", - "httpOnly": true, - "domain": "postman-echo.com", - "path": "/", - "secure": false, - "value": "s%3A2lT3TO7qS1tadeSAp4axl-NcXG9CV6Rf.HGqLY%2FlKEKY4fgCLePaAZs3tCHp%2Bglf7ZOJYlonGeig", - "key": "sails.sid" - } - ], - "body": "Wed, 11 Jan 2017 10:27:12 GMT" - } - ] - }, - { - "name": "Timestamp validity", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "var validity = JSON.parse(responseBody).valid;", - "", - "tests[\"Status code is 200\"] = responseCode.code === 200;", - "tests[\"Timestamp is valid\"] = validity === true;", - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "formdata", - "formdata": [] - }, - "url": { - "raw": "{{base_url}}/{{app_url}}/time/valid?timestamp=2016-10-10", - "host": [ - "{{base_url}}" - ], - "path": [ - "{{app_url}}", - "time", - "valid" - ], - "query": [ - { - "key": "timestamp", - "value": "2016-10-10" - } - ] - }, - "description": "A simple `GET` request to `/time/valid` to determine the validity of the timestamp, (current by default).\nThis endpoint accepts `timestamp`, `locale`, `format`, and `strict` query parameters to construct the date time instance to check against.\n\nResponses are provided in JSON format, with a valid key to indicate the result. The response code is `200`.\n\n```\n{\n valid: true/false\n}\n```" - }, - "response": [ - { - "name": "Invalid Timestamp", - "originalRequest": { - "method": "GET", - "header": [], - "body": { - "mode": "formdata", - "formdata": [] - }, - "url": { - "raw": "https://postman-echo.com/time/valid?timestamp=2016-10-10", - "protocol": "https", - "host": [ - "postman-echo", - "com" - ], - "path": [ - "time", - "valid" - ], - "query": [ - { - "key": "timestamp", - "value": "2016-10-10" - } - ] - } - }, - "status": "OK", - "code": 200, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Access-Control-Allow-Credentials", - "value": "", - "name": "Access-Control-Allow-Credentials", - "description": "" - }, - { - "key": "Access-Control-Allow-Headers", - "value": "", - "name": "Access-Control-Allow-Headers", - "description": "" - }, - { - "key": "Access-Control-Allow-Methods", - "value": "", - "name": "Access-Control-Allow-Methods", - "description": "" - }, - { - "key": "Access-Control-Allow-Origin", - "value": "", - "name": "Access-Control-Allow-Origin", - "description": "" - }, - { - "key": "Access-Control-Expose-Headers", - "value": "", - "name": "Access-Control-Expose-Headers", - "description": "" - }, - { - "key": "Connection", - "value": "keep-alive", - "name": "Connection", - "description": "" - }, - { - "key": "Content-Length", - "value": "15", - "name": "Content-Length", - "description": "" - }, - { - "key": "Content-Type", - "value": "application/json; charset=utf-8", - "name": "Content-Type", - "description": "" - }, - { - "key": "Date", - "value": "Wed, 11 Jan 2017 10:27:53 GMT", - "name": "Date", - "description": "" - }, - { - "key": "ETag", - "value": "W/\"f-/i9mO/upK91ZtL0BkKFGtw\"", - "name": "ETag", - "description": "" - }, - { - "key": "Server", - "value": "nginx/1.10.1", - "name": "Server", - "description": "" - }, - { - "key": "Vary", - "value": "Accept-Encoding", - "name": "Vary", - "description": "" - }, - { - "key": "set-cookie", - "value": "sails.sid=s%3ATNJaNxi2QCv4RPBb64sIZxQGN1h6IP3g.9sQVAijlsLsh0r7LgffxXa9k2we6UumPEVv%2Bsk4woLI; Path=/; HttpOnly", - "name": "set-cookie", - "description": "" - } - ], - "cookie": [ - { - "expires": "Mon Jan 18 2038 22:44:07 GMT+0100 (heure normale d’Europe centrale)", - "httpOnly": true, - "domain": "postman-echo.com", - "path": "/", - "secure": false, - "value": "s%3ATNJaNxi2QCv4RPBb64sIZxQGN1h6IP3g.9sQVAijlsLsh0r7LgffxXa9k2we6UumPEVv%2Bsk4woLI", - "key": "sails.sid" - } - ], - "body": "{\"valid\":false}" - }, - { - "name": "Valid Timestamp", - "originalRequest": { - "method": "GET", - "header": [], - "body": { - "mode": "formdata", - "formdata": [] - }, - "url": { - "raw": "https://postman-echo.com/time/valid?timestamp=2016-10-10", - "protocol": "https", - "host": [ - "postman-echo", - "com" - ], - "path": [ - "time", - "valid" - ], - "query": [ - { - "key": "timestamp", - "value": "2016-10-10" - } - ] - } - }, - "status": "OK", - "code": 200, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Access-Control-Allow-Credentials", - "value": "", - "name": "Access-Control-Allow-Credentials", - "description": "" - }, - { - "key": "Access-Control-Allow-Headers", - "value": "", - "name": "Access-Control-Allow-Headers", - "description": "" - }, - { - "key": "Access-Control-Allow-Methods", - "value": "", - "name": "Access-Control-Allow-Methods", - "description": "" - }, - { - "key": "Access-Control-Allow-Origin", - "value": "", - "name": "Access-Control-Allow-Origin", - "description": "" - }, - { - "key": "Access-Control-Expose-Headers", - "value": "", - "name": "Access-Control-Expose-Headers", - "description": "" - }, - { - "key": "Connection", - "value": "keep-alive", - "name": "Connection", - "description": "" - }, - { - "key": "Content-Length", - "value": "14", - "name": "Content-Length", - "description": "" - }, - { - "key": "Content-Type", - "value": "application/json; charset=utf-8", - "name": "Content-Type", - "description": "" - }, - { - "key": "Date", - "value": "Wed, 11 Jan 2017 10:27:33 GMT", - "name": "Date", - "description": "" - }, - { - "key": "ETag", - "value": "W/\"e-OYN7L87J1Ba9oy5mJE2kcA\"", - "name": "ETag", - "description": "" - }, - { - "key": "Server", - "value": "nginx/1.10.1", - "name": "Server", - "description": "" - }, - { - "key": "Vary", - "value": "Accept-Encoding", - "name": "Vary", - "description": "" - }, - { - "key": "set-cookie", - "value": "sails.sid=s%3AdDGZPe1CZw4mAxGVCHr6RfCADCAwquXa.F5MEm5LJad30JHrSwGGoyWLn2OAAGdvUM7kDtzNfdFI; Path=/; HttpOnly", - "name": "set-cookie", - "description": "" - } - ], - "cookie": [ - { - "expires": "Mon Jan 18 2038 22:44:07 GMT+0100 (heure normale d’Europe centrale)", - "httpOnly": true, - "domain": "postman-echo.com", - "path": "/", - "secure": false, - "value": "s%3AdDGZPe1CZw4mAxGVCHr6RfCADCAwquXa.F5MEm5LJad30JHrSwGGoyWLn2OAAGdvUM7kDtzNfdFI", - "key": "sails.sid" - } - ], - "body": "{\"valid\":true}" - } - ] - }, - { - "name": "Format timestamp", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "var format = JSON.parse(responseBody).format;", - "", - "tests[\"Status code is 200\"] = responseCode.code === 200;", - "tests[\"Formatted result is valid\"] = format === \"20\";", - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "formdata", - "formdata": [] - }, - "url": { - "raw": "{{base_url}}/{{app_url}}/time/format?timestamp=2016-10-10&format=mm", - "host": [ - "{{base_url}}" - ], - "path": [ - "{{app_url}}", - "time", - "format" - ], - "query": [ - { - "key": "timestamp", - "value": "2016-10-10" - }, - { - "key": "format", - "value": "mm" - } - ] - }, - "description": "A simple `GET` request to `/time/format` to convert the timestamp to any desired valid format.\n\nThis endpoint accepts `timestamp`, `locale`, `format`, and `strict` query parameters to construct the date time instance to check against.\n\nResponses are provided in JSON format, with a `format` key to indicate the result. The response code is `200` for valid query parameters, and `400` otherwise.\n\n```\n{\n format: \"formatted-timestamp\"\n}\n```" - }, - "response": [] - }, - { - "name": "Extract timestamp unit", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "var unit = JSON.parse(responseBody).unit;", - "", - "tests[\"Status code is 200\"] = responseCode.code === 200;", - "tests[\"Returned unit is valid\"] = unit === 1;", - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "formdata", - "formdata": [] - }, - "url": { - "raw": "{{base_url}}/{{app_url}}/time/unit?timestamp=2016-10-10&unit=day", - "host": [ - "{{base_url}}" - ], - "path": [ - "{{app_url}}", - "time", - "unit" - ], - "query": [ - { - "key": "timestamp", - "value": "2016-10-10" - }, - { - "key": "unit", - "value": "day" - } - ] - }, - "description": "A simple `GET` request to `/time/unit` to extract the specified timestamp unit (as provided in the `unit` query parameter). The default unit returned is the `year`.\n\nThis endpoint accepts `timestamp`, `locale`, `format`, and `strict` query parameters to construct the date time instance to check against.\n\nResponses are provided in JSON format, with a `unit` key to indicate the result. The response code is `200` for valid query parameters, and `400` otherwise.\n\n```\n{\n unit: \"extracted-timestamp-unit\"\n}\n```" - }, - "response": [] - }, - { - "name": "Time addition", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "var sum = JSON.parse(responseBody).sum;", - "", - "tests[\"Status code is 200\"] = responseCode.code === 200;" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "formdata", - "formdata": [] - }, - "url": { - "raw": "{{base_url}}/{{app_url}}/time/add?timestamp=2016-10-10&years=100", - "host": [ - "{{base_url}}" - ], - "path": [ - "{{app_url}}", - "time", - "add" - ], - "query": [ - { - "key": "timestamp", - "value": "2016-10-10" - }, - { - "key": "years", - "value": "100" - } - ] - }, - "description": "A simple `GET` request to `/time/add` to add units of time to the specified / current timestamp (as provided in the `years`, `months`, `days`, `hours`, `minutes`, `seconds`, and `milliseconds` query parameters).\n\nThis endpoint accepts `timestamp`, `locale`, `format`, and `strict` query parameters to construct the date time instance to check against.\n\nResponses are provided in JSON format, with a `sum` key to indicate the result. The response code is `200` for valid query parameters, and `400` otherwise.\n\n```\n{\n sum: \"sum of (provided / current) and provided timestamps\"\n}\n```" - }, - "response": [] - }, - { - "name": "Time subtraction", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "var difference = JSON.parse(responseBody).difference;", - "", - "tests[\"Status code is 200\"] = responseCode.code === 200;" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "formdata", - "formdata": [] - }, - "url": { - "raw": "{{base_url}}/{{app_url}}/time/subtract?timestamp=2016-10-10&years=100", - "host": [ - "{{base_url}}" - ], - "path": [ - "{{app_url}}", - "time", - "subtract" - ], - "query": [ - { - "key": "timestamp", - "value": "2016-10-10" - }, - { - "key": "years", - "value": "100" - } - ] - }, - "description": "A simple `GET` request to `/time/subtract` to subtract units of time from the specified / current timestamp (as provided in the `years`, `months`, `days`, `hours`, `minutes`, `seconds`, and `milliseconds` query parameters).\n\nThis endpoint accepts `timestamp`, `locale`, `format`, and `strict` query parameters to construct the date time instance to check against.\n\nResponses are provided in JSON format, with a `difference` key to indicate the result. The response code is `200` for valid query parameters, and `400` otherwise.\n\n```\n{\n difference: \"difference between (provided / current) and provided timestamps\"\n}\n```" - }, - "response": [] - }, - { - "name": "Start of time", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "var start = JSON.parse(responseBody).start;", - "", - "tests[\"Status code is 200\"] = responseCode.code === 200;" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "formdata", - "formdata": [] - }, - "url": { - "raw": "{{base_url}}/{{app_url}}/time/start?timestamp=2016-10-10&unit=month", - "host": [ - "{{base_url}}" - ], - "path": [ - "{{app_url}}", - "time", - "start" - ], - "query": [ - { - "key": "timestamp", - "value": "2016-10-10" - }, - { - "key": "unit", - "value": "month" - } - ] - }, - "description": "A simple `GET` request to `/time/start` to return a relative timstamp in the past from the specified / current timestamp (as provided in the `unit` query parameter).\n\nFor instance, if the `unit` has been specified as `month`, the returned timestamp would indicate the beginning of the current month. Similar results are returned for other units of time, like: `years`, `months`, `days`, `hours`, `minutes`, `seconds`, and `milliseconds`\n\nThis endpoint accepts `timestamp`, `locale`, `format`, and `strict` query parameters to construct the date time instance to check against.\n\nResponses are provided in JSON format, with a `start` key to indicate the result. The response code is `200` for valid query parameters, and `400` otherwise.\n\n```\n{\n start: \"A timestamp from the past, depending on the `unit` specified\"\n}\n```" - }, - "response": [] - }, - { - "name": "Object representation", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "var object = JSON.parse(responseBody);", - "", - "tests[\"Status code is 200\"] = responseCode.code === 200;" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "formdata", - "formdata": [] - }, - "url": { - "raw": "{{base_url}}/{{app_url}}/time/start?timestamp=2016-10-10&unit=month", - "host": [ - "{{base_url}}" - ], - "path": [ - "{{app_url}}", - "time", - "start" - ], - "query": [ - { - "key": "timestamp", - "value": "2016-10-10" - }, - { - "key": "unit", - "value": "month" - } - ] - }, - "description": "A simple `GET` request to `/time/object` to return the current / provided timestamp as a JSON object.\n\nFor instance, if the `unit` has been specified as `month`, the returned timestamp would indicate the beginning of the current month. Similar results are returned for other units of time, like: `years`, `months`, `days`, `hours`, `minutes`, `seconds`, and `milliseconds`\n\nThis endpoint accepts `timestamp`, `locale`, `format`, and `strict` query parameters to construct the date time instance to check against.\n\nResponses are provided in JSON format. The response code is `200` for valid query parameters, and `400` otherwise.\n\n```\n{\n years: 2016,\n months: 10,\n days: 10,\n hours: 23,\n minutes: 34,\n seconds: 20,\n milliseconds: 980\n}\n```" - }, - "response": [] - }, - { - "name": "Before comparisons", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "var before = JSON.parse(responseBody).before;", - "", - "tests[\"Status code is 200\"] = responseCode.code === 200;", - "tests[\"Comparsion was correct\"] = before === true;" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "formdata", - "formdata": [] - }, - "url": { - "raw": "{{base_url}}/{{app_url}}/time/before?timestamp=2016-10-10&target=2017-10-10", - "host": [ - "{{base_url}}" - ], - "path": [ - "{{app_url}}", - "time", - "before" - ], - "query": [ - { - "key": "timestamp", - "value": "2016-10-10" - }, - { - "key": "target", - "value": "2017-10-10" - } - ] - }, - "description": "A simple `GET` request to `/time/before` to check if the provided timestamps is before a comparison `target` (query parameter).\n\nThis endpoint accepts `timestamp`, `locale`, `format`, `strict`, and `target` query parameters to construct the date time instance to check against.\n\nResponses are provided in JSON format, with a `before` key to indicate the result. The response code is `200` for valid query parameters, and `400` otherwise.\n\n```\n{\n before: true/false\n}\n```" - }, - "response": [] - }, - { - "name": "After comparisons", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "var after = JSON.parse(responseBody).after;", - "", - "tests[\"Status code is 200\"] = responseCode.code === 200;", - "tests[\"Comparsion was correct\"] = after === false;" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "formdata", - "formdata": [] - }, - "url": { - "raw": "{{base_url}}/{{app_url}}/time/after?timestamp=2016-10-10&target=2017-10-10", - "host": [ - "{{base_url}}" - ], - "path": [ - "{{app_url}}", - "time", - "after" - ], - "query": [ - { - "key": "timestamp", - "value": "2016-10-10" - }, - { - "key": "target", - "value": "2017-10-10" - } - ] - }, - "description": "A simple `GET` request to `/time/after` to check if the provided timestamps is after a comparison `target` (query parameter).\n\nThis endpoint accepts `timestamp`, `locale`, `format`, `strict`, and `target` query parameters to construct the date time instance to check against.\n\nResponses are provided in JSON format, with a `after` key to indicate the result. The response code is `200` for valid query parameters, and `400` otherwise.\n\n```\n{\n after: true/false\n}\n```" - }, - "response": [] - }, - { - "name": "Between timestamps", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "var after = JSON.parse(responseBody).after;", - "", - "tests[\"Status code is 200\"] = responseCode.code === 200;", - "tests[\"Comparsion was correct\"] = after === false;" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "formdata", - "formdata": [] - }, - "url": { - "raw": "{{base_url}}/{{app_url}}/time/between?timestamp=2016-10-10&start=2017-10-10&end=2019-10-10", - "host": [ - "{{base_url}}" - ], - "path": [ - "{{app_url}}", - "time", - "between" - ], - "query": [ - { - "key": "timestamp", - "value": "2016-10-10" - }, - { - "key": "start", - "value": "2017-10-10" - }, - { - "key": "end", - "value": "2019-10-10" - } - ] - }, - "description": "A simple `GET` request to `/time/between` to check if the provided timestamp is between a range specified by the `start` and `end` query parameters. A resolution limit can also be specified by the `unit` query parameter.\n\nFor instance, for a resolution `unit` of `month`,\n`2016-10-05` does lie between `2016-11-02` and `2016-09-01`.\n\nThis endpoint also accepts `timestamp`, `locale`, `format`, `strict`, and `target` query parameters to construct the date time instance to check against.\n\nResponses are provided in JSON format, with a `between` key to indicate the result. The response code is `200` for valid query parameters, and `400` otherwise.\n\n```\n{\n between: true/false\n}\n```" - }, - "response": [] - }, - { - "name": "Leap year check", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "tests[\"Status code is 200\"] = responseCode.code === 200;", - "tests[\"Comparsion was correct\"] = JSON.parse(responseBody).leap === true;" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "formdata", - "formdata": [] - }, - "url": { - "raw": "{{base_url}}/{{app_url}}/time/leap?timestamp=2016-10-10", - "host": [ - "{{base_url}}" - ], - "path": [ - "{{app_url}}", - "time", - "leap" - ], - "query": [ - { - "key": "timestamp", - "value": "2016-10-10" - } - ] - }, - "description": "A simple `GET` request to `/time/leap` to check if the provided/current timestamp belongs to a leap year.\n\nThis endpoint also accepts `timestamp`, `locale`, `format`, `strict`, and `target` query parameters to construct the date time instance to check against.\n\nResponses are provided in JSON format, with a `leap` key to indicate the result. The response code is `200` for valid query parameters, and `400` otherwise.\n\n```\n{\n leap: true/false\n}\n```" - }, - "response": [] - } - ], - "description": "A set of `/time/*` mounted requests to perform date-time manipulations, among other operations.\n" - }, - { - "name": "Utilities / Postman Collection", - "item": [ - { - "name": "Transform collection from format v1 to v2", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"id\": \"7875be4b-917d-4aff-8cc4-5606c36bf418\",\n \"name\": \"Sample Postman Collection\",\n \"description\": \"A sample collection to demonstrate collections as a set of related requests\",\n \"order\": [\n \"4d9134be-e8bf-4693-9cd7-1c0fc66ae739\",\n \"141ba274-cc50-4377-a59c-e080066f375e\",\n \"4511ca8b-0bc7-430f-b894-a7ec1036f322\"\n ],\n \"folders\": [],\n \"requests\": [\n {\n \"id\": \"4d9134be-e8bf-4693-9cd7-1c0fc66ae739\",\n \"name\": \"A simple GET request\",\n \"collectionId\": \"877b9dae-a50e-4152-9b89-870c37216f78\",\n \"method\": \"GET\",\n \"headers\": \"\",\n \"data\": [],\n \"rawModeData\": \"\",\n \"tests\": \"tests['response code is 200'] = (responseCode.code === 200);\",\n \"preRequestScript\": \"\",\n \"url\": \"https://postman-echo.com/get?source=newman-sample-github-collection\"\n },\n {\n \"id\": \"141ba274-cc50-4377-a59c-e080066f375e\",\n \"name\": \"A simple POST request\",\n \"collectionId\": \"877b9dae-a50e-4152-9b89-870c37216f78\",\n \"method\": \"POST\",\n \"headers\": \"Content-Type: text/plain\",\n \"dataMode\": \"raw\",\n \"data\": [],\n \"rawModeData\": \"Duis posuere augue vel cursus pharetra. In luctus a ex nec pretium...\",\n \"url\": \"https://postman-echo.com/post\"\n },\n {\n \"id\": \"4511ca8b-0bc7-430f-b894-a7ec1036f322\",\n \"name\": \"A simple POST request with JSON body\",\n \"collectionId\": \"877b9dae-a50e-4152-9b89-870c37216f78\",\n \"method\": \"POST\",\n \"headers\": \"Content-Type: application/json\",\n \"dataMode\": \"raw\",\n \"data\": [],\n \"rawModeData\": \"{\\\"text\\\":\\\"Duis posuere augue vel cursus pharetra. In luctus a ex nec pretium...\\\"}\",\n \"url\": \"https://postman-echo.com/post\"\n }\n ]\n}" - }, - "url": { - "raw": "{{base_url}}/{{app_url}}/transform/collection?from=1&to=2", - "host": [ - "{{base_url}}" - ], - "path": [ - "{{app_url}}", - "transform", - "collection" - ], - "query": [ - { - "key": "from", - "value": "1" - }, - { - "key": "to", - "value": "2" - } - ] - } - }, - "response": [ - { - "name": "Sample v2 Response", - "originalRequest": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json", - "enabled": true, - "description": "The mime type of this content", - "disabled": false - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"id\": \"7875be4b-917d-4aff-8cc4-5606c36bf418\",\n \"name\": \"Sample Postman Collection\",\n \"description\": \"A sample collection to demonstrate collections as a set of related requests\",\n \"order\": [\n \"4d9134be-e8bf-4693-9cd7-1c0fc66ae739\",\n \"141ba274-cc50-4377-a59c-e080066f375e\",\n \"4511ca8b-0bc7-430f-b894-a7ec1036f322\"\n ],\n \"folders\": [],\n \"requests\": [\n {\n \"id\": \"4d9134be-e8bf-4693-9cd7-1c0fc66ae739\",\n \"name\": \"A simple GET request\",\n \"collectionId\": \"877b9dae-a50e-4152-9b89-870c37216f78\",\n \"method\": \"GET\",\n \"headers\": \"\",\n \"data\": [],\n \"rawModeData\": \"\",\n \"tests\": \"tests['response code is 200'] = (responseCode.code === 200);\",\n \"preRequestScript\": \"\",\n \"url\": \"https://postman-echo.com/get?source=newman-sample-github-collection\"\n },\n {\n \"id\": \"141ba274-cc50-4377-a59c-e080066f375e\",\n \"name\": \"A simple POST request\",\n \"collectionId\": \"877b9dae-a50e-4152-9b89-870c37216f78\",\n \"method\": \"POST\",\n \"headers\": \"Content-Type: text/plain\",\n \"dataMode\": \"raw\",\n \"data\": [],\n \"rawModeData\": \"Duis posuere augue vel cursus pharetra. In luctus a ex nec pretium...\",\n \"url\": \"https://postman-echo.com/post\"\n },\n {\n \"id\": \"4511ca8b-0bc7-430f-b894-a7ec1036f322\",\n \"name\": \"A simple POST request with JSON body\",\n \"collectionId\": \"877b9dae-a50e-4152-9b89-870c37216f78\",\n \"method\": \"POST\",\n \"headers\": \"Content-Type: application/json\",\n \"dataMode\": \"raw\",\n \"data\": [],\n \"rawModeData\": \"{\\\"text\\\":\\\"Duis posuere augue vel cursus pharetra. In luctus a ex nec pretium...\\\"}\",\n \"url\": \"https://postman-echo.com/post\"\n }\n ]\n}" - }, - "url": { - "raw": "https://postman-echo.com/transform/collection?from=1&to=2", - "protocol": "https", - "host": [ - "postman-echo", - "com" - ], - "path": [ - "transform", - "collection" - ], - "query": [ - { - "key": "from", - "value": "1" - }, - { - "key": "to", - "value": "2" - } - ] - } - }, - "status": "OK", - "code": 200, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Access-Control-Allow-Credentials", - "value": "", - "name": "Access-Control-Allow-Credentials", - "description": "" - }, - { - "key": "Access-Control-Allow-Headers", - "value": "", - "name": "Access-Control-Allow-Headers", - "description": "" - }, - { - "key": "Access-Control-Allow-Methods", - "value": "", - "name": "Access-Control-Allow-Methods", - "description": "" - }, - { - "key": "Access-Control-Allow-Origin", - "value": "", - "name": "Access-Control-Allow-Origin", - "description": "" - }, - { - "key": "Access-Control-Expose-Headers", - "value": "", - "name": "Access-Control-Expose-Headers", - "description": "" - }, - { - "key": "Connection", - "value": "keep-alive", - "name": "Connection", - "description": "" - }, - { - "key": "Content-Encoding", - "value": "gzip", - "name": "Content-Encoding", - "description": "" - }, - { - "key": "Content-Type", - "value": "application/json; charset=utf-8", - "name": "Content-Type", - "description": "" - }, - { - "key": "Date", - "value": "Wed, 11 Jan 2017 10:41:32 GMT", - "name": "Date", - "description": "" - }, - { - "key": "ETag", - "value": "W/\"4cc-7P727Clhlrl9+b1/vneniw\"", - "name": "ETag", - "description": "" - }, - { - "key": "Server", - "value": "nginx/1.10.1", - "name": "Server", - "description": "" - }, - { - "key": "Vary", - "value": "X-HTTP-Method-Override, Accept-Encoding", - "name": "Vary", - "description": "" - }, - { - "key": "set-cookie", - "value": "sails.sid=s%3AHtnQ1hlPxoj7wZahoNkcjN-aw9nQL0fc.KSyfLbEKhv1Lt3LvH13Ogjv9ENZgsBBSM6V8Y7TqVOU; Path=/; HttpOnly", - "name": "set-cookie", - "description": "" - }, - { - "key": "transfer-encoding", - "value": "chunked", - "name": "transfer-encoding", - "description": "" - } - ], - "cookie": [ - { - "expires": "Mon Jan 18 2038 22:44:07 GMT+0100 (heure normale d’Europe centrale)", - "httpOnly": true, - "domain": "postman-echo.com", - "path": "/", - "secure": false, - "value": "s%3AHtnQ1hlPxoj7wZahoNkcjN-aw9nQL0fc.KSyfLbEKhv1Lt3LvH13Ogjv9ENZgsBBSM6V8Y7TqVOU", - "key": "sails.sid" - } - ], - "body": "{\"variables\":[],\"info\":{\"name\":\"Sample Postman Collection\",\"_postman_id\":\"7875be4b-917d-4aff-8cc4-5606c36bf418\",\"description\":\"A sample collection to demonstrate collections as a set of related requests\",\"schema\":\"https://schema.getpostman.com/json/collection/v2.0.0/collection.json\"},\"item\":[{\"name\":\"A simple GET request\",\"event\":[{\"listen\":\"test\",\"script\":{\"type\":\"text/javascript\",\"exec\":[\"tests['response code is 200'] = (responseCode.code === 200);\"]}}],\"request\":{\"url\":\"https://postman-echo.com/get?source=newman-sample-github-collection\",\"method\":\"GET\",\"header\":[],\"body\":{\"mode\":\"raw\",\"raw\":\"\"}},\"response\":[]},{\"name\":\"A simple POST request\",\"request\":{\"url\":\"https://postman-echo.com/post\",\"method\":\"POST\",\"header\":[{\"key\":\"Content-Type\",\"value\":\"text/plain\",\"description\":\"\"}],\"body\":{\"mode\":\"raw\",\"raw\":\"Duis posuere augue vel cursus pharetra. In luctus a ex nec pretium...\"}},\"response\":[]},{\"name\":\"A simple POST request with JSON body\",\"request\":{\"url\":\"https://postman-echo.com/post\",\"method\":\"POST\",\"header\":[{\"key\":\"Content-Type\",\"value\":\"application/json\",\"description\":\"\"}],\"body\":{\"mode\":\"raw\",\"raw\":\"{\\\"text\\\":\\\"Duis posuere augue vel cursus pharetra. In luctus a ex nec pretium...\\\"}\"}},\"response\":[]}]}" - } - ] - }, - { - "name": "Transform collection from format v2 to v1", - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"info\": {\n \"name\": \"Sample Postman Collection\",\n \"schema\": \"https://schema.getpostman.com/json/collection/v2.0.0/collection.json\",\n \"description\": \"A sample collection to demonstrate collections as a set of related requests\"\n },\n\n \"item\": [{\n \"name\": \"A simple GET request\",\n \"event\": [{\n \"listen\": \"test\",\n \"script\": {\n \"type\": \"text/javascript\",\n \"exec\": [\"tests['response code is 200'] = (responseCode.code === 200);\"]\n }\n }],\n \"request\": {\n \"url\": \"https://postman-echo.com/get?source=newman-sample-github-collection\",\n \"method\": \"GET\"\n }\n }, {\n \"name\": \"A simple POST request\",\n \"request\": {\n \"url\": \"https://postman-echo.com/post\",\n \"method\": \"POST\",\n \"header\": [{\n \"key\": \"Content-Type\",\n \"value\": \"text/plain\"\n }],\n \"body\": {\n \"mode\": \"raw\",\n \"raw\": \"Duis posuere augue vel cursus pharetra. In luctus a ex nec pretium...\"\n }\n }\n }, {\n \"name\": \"A simple POST request with JSON body\",\n \"request\": {\n \"url\": \"https://postman-echo.com/post\",\n \"method\": \"POST\",\n \"header\": [{\n \"key\": \"Content-Type\",\n \"value\": \"application/json\"\n }],\n \"body\": {\n \"mode\": \"raw\",\n \"raw\": \"{\\\"text\\\":\\\"Duis posuere augue vel cursus pharetra. In luctus a ex nec pretium...\\\"}\"\n }\n }\n }]\n}" - }, - "url": { - "raw": "{{base_url}}/{{app_url}}/transform/collection?from=2&to=1", - "host": [ - "{{base_url}}" - ], - "path": [ - "{{app_url}}", - "transform", - "collection" - ], - "query": [ - { - "key": "from", - "value": "2" - }, - { - "key": "to", - "value": "1" - } - ] - } - }, - "response": [ - { - "name": "Sample v1 Response", - "originalRequest": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json", - "enabled": true, - "description": "The mime type of this content", - "disabled": false - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"info\": {\n \"name\": \"Sample Postman Collection\",\n \"schema\": \"https://schema.getpostman.com/json/collection/v2.0.0/collection.json\",\n \"description\": \"A sample collection to demonstrate collections as a set of related requests\"\n },\n\n \"item\": [{\n \"name\": \"A simple GET request\",\n \"event\": [{\n \"listen\": \"test\",\n \"script\": {\n \"type\": \"text/javascript\",\n \"exec\": [\"tests['response code is 200'] = (responseCode.code === 200);\"]\n }\n }],\n \"request\": {\n \"url\": \"https://postman-echo.com/get?source=newman-sample-github-collection\",\n \"method\": \"GET\"\n }\n }, {\n \"name\": \"A simple POST request\",\n \"request\": {\n \"url\": \"https://postman-echo.com/post\",\n \"method\": \"POST\",\n \"header\": [{\n \"key\": \"Content-Type\",\n \"value\": \"text/plain\"\n }],\n \"body\": {\n \"mode\": \"raw\",\n \"raw\": \"Duis posuere augue vel cursus pharetra. In luctus a ex nec pretium...\"\n }\n }\n }, {\n \"name\": \"A simple POST request with JSON body\",\n \"request\": {\n \"url\": \"https://postman-echo.com/post\",\n \"method\": \"POST\",\n \"header\": [{\n \"key\": \"Content-Type\",\n \"value\": \"application/json\"\n }],\n \"body\": {\n \"mode\": \"raw\",\n \"raw\": \"{\\\"text\\\":\\\"Duis posuere augue vel cursus pharetra. In luctus a ex nec pretium...\\\"}\"\n }\n }\n }]\n}" - }, - "url": { - "raw": "https://postman-echo.com/transform/collection?from=2&to=1", - "protocol": "https", - "host": [ - "postman-echo", - "com" - ], - "path": [ - "transform", - "collection" - ], - "query": [ - { - "key": "from", - "value": "2" - }, - { - "key": "to", - "value": "1" - } - ] - } - }, - "status": "OK", - "code": 200, - "_postman_previewlanguage": "json", - "header": [ - { - "key": "Access-Control-Allow-Credentials", - "value": "", - "name": "Access-Control-Allow-Credentials", - "description": "" - }, - { - "key": "Access-Control-Allow-Headers", - "value": "", - "name": "Access-Control-Allow-Headers", - "description": "" - }, - { - "key": "Access-Control-Allow-Methods", - "value": "", - "name": "Access-Control-Allow-Methods", - "description": "" - }, - { - "key": "Access-Control-Allow-Origin", - "value": "", - "name": "Access-Control-Allow-Origin", - "description": "" - }, - { - "key": "Access-Control-Expose-Headers", - "value": "", - "name": "Access-Control-Expose-Headers", - "description": "" - }, - { - "key": "Connection", - "value": "keep-alive", - "name": "Connection", - "description": "" - }, - { - "key": "Content-Encoding", - "value": "gzip", - "name": "Content-Encoding", - "description": "" - }, - { - "key": "Content-Type", - "value": "application/json; charset=utf-8", - "name": "Content-Type", - "description": "" - }, - { - "key": "Date", - "value": "Wed, 11 Jan 2017 10:38:42 GMT", - "name": "Date", - "description": "" - }, - { - "key": "ETag", - "value": "W/\"569-P9uLZEIyoPfMmQ+U0mTO1A\"", - "name": "ETag", - "description": "" - }, - { - "key": "Server", - "value": "nginx/1.10.1", - "name": "Server", - "description": "" - }, - { - "key": "Vary", - "value": "X-HTTP-Method-Override, Accept-Encoding", - "name": "Vary", - "description": "" - }, - { - "key": "set-cookie", - "value": "sails.sid=s%3A55y5Ll7HpTzt_hKuw6N54k4N04ilmMdn.uCPCHttP5DmI%2BdBw2I9NZL55lFFOzz4XxS4qAHv47gI; Path=/; HttpOnly", - "name": "set-cookie", - "description": "" - }, - { - "key": "transfer-encoding", - "value": "chunked", - "name": "transfer-encoding", - "description": "" - } - ], - "cookie": [ - { - "expires": "Mon Jan 18 2038 22:44:07 GMT+0100 (heure normale d’Europe centrale)", - "httpOnly": true, - "domain": "postman-echo.com", - "path": "/", - "secure": false, - "value": "s%3A55y5Ll7HpTzt_hKuw6N54k4N04ilmMdn.uCPCHttP5DmI%2BdBw2I9NZL55lFFOzz4XxS4qAHv47gI", - "key": "sails.sid" - } - ], - "body": "{\"id\":\"0c42230c-c8e4-4ca0-a4aa-d393971de8b8\",\"name\":\"Sample Postman Collection\",\"description\":\"A sample collection to demonstrate collections as a set of related requests\",\"order\":[\"3d04ed83-dc1e-40ec-923c-16aa92509e50\",\"e02f8160-fb41-4633-be80-cc7d701e85b4\",\"77bd6d4d-1060-4927-aa5c-dcdba7f750cf\"],\"folders\":[],\"requests\":[{\"id\":\"3d04ed83-dc1e-40ec-923c-16aa92509e50\",\"name\":\"A simple GET request\",\"collectionId\":\"1dd68aff-a3fa-4f52-904f-5b75053bc9d9\",\"method\":\"GET\",\"headers\":\"\",\"data\":[],\"rawModeData\":\"\",\"tests\":\"tests['response code is 200'] = (responseCode.code === 200);\",\"preRequestScript\":\"\",\"url\":\"https://postman-echo.com/get?source=newman-sample-github-collection\"},{\"id\":\"e02f8160-fb41-4633-be80-cc7d701e85b4\",\"name\":\"A simple POST request\",\"collectionId\":\"1dd68aff-a3fa-4f52-904f-5b75053bc9d9\",\"method\":\"POST\",\"headers\":\"Content-Type: text/plain\",\"dataMode\":\"raw\",\"data\":[],\"rawModeData\":\"Duis posuere augue vel cursus pharetra. In luctus a ex nec pretium...\",\"url\":\"https://postman-echo.com/post\"},{\"id\":\"77bd6d4d-1060-4927-aa5c-dcdba7f750cf\",\"name\":\"A simple POST request with JSON body\",\"collectionId\":\"1dd68aff-a3fa-4f52-904f-5b75053bc9d9\",\"method\":\"POST\",\"headers\":\"Content-Type: application/json\",\"dataMode\":\"raw\",\"data\":[],\"rawModeData\":\"{\\\"text\\\":\\\"Duis posuere augue vel cursus pharetra. In luctus a ex nec pretium...\\\"}\",\"url\":\"https://postman-echo.com/post\"}]}" - } - ] - } - ] - } - ] -} \ No newline at end of file diff --git a/src/test/resources/Postman_Echo.postman_test_run.json b/src/test/resources/Postman_Echo.postman_test_run.json deleted file mode 100644 index 15f419e..0000000 --- a/src/test/resources/Postman_Echo.postman_test_run.json +++ /dev/null @@ -1,1395 +0,0 @@ -{ - "id": "0146e4dc-a3b3-4972-aaec-0968102306cd", - "name": "Postman Echo", - "timestamp": "2024-08-02T21:35:10.922Z", - "collection_id": "5266980-55548714-bfe1-4d87-bc97-cd915e82fcf1", - "folder_id": 0, - "environment_id": "5266980-443be3ff-8a62-4ae4-8dea-10ddc7417e04", - "totalPass": 69, - "delay": 0, - "persist": true, - "status": "finished", - "startedAt": "2024-08-02T21:34:58.651Z", - "totalFail": 15, - "results": [ - { - "id": "ede243e5-78b3-427e-a3c2-be1c552fb55e", - "name": "Basic Auth", - "url": "http://localhost:8082/nexus-backend/api/basic-auth", - "time": 947, - "responseCode": { - "code": 200, - "name": "OK" - }, - "tests": { - "response code is 200": true, - "Body contains authenticated": true - }, - "testPassFailCounts": { - "response code is 200": { - "pass": 1, - "fail": 0 - }, - "Body contains authenticated": { - "pass": 1, - "fail": 0 - } - }, - "times": [ - 947 - ], - "allTests": [ - { - "response code is 200": true, - "Body contains authenticated": true - } - ] - }, - { - "id": "1ae52c41-98de-4c10-808d-9cbea3e0d05d", - "name": "OAuth1.0 Verify Signature", - "url": "http://localhost:8082/nexus-backend/api/oauth1", - "time": 650, - "responseCode": { - "code": 401, - "name": "Unauthorized" - }, - "tests": { - "response code is 200": false, - "Body contains status pass": false - }, - "testPassFailCounts": { - "response code is 200": { - "pass": 0, - "fail": 1 - }, - "Body contains status pass": { - "pass": 0, - "fail": 1 - } - }, - "times": [ - 650 - ], - "allTests": [ - { - "response code is 200": false, - "Body contains status pass": false - } - ] - }, - { - "id": "b50da305-15a9-4d13-8189-d3b8c688b71d", - "name": "Hawk Auth", - "url": "http://localhost:8082/nexus-backend/api/auth/hawk", - "time": 131, - "responseCode": { - "code": 401, - "name": "Unauthorized" - }, - "tests": { - "Status code is 200": false - }, - "testPassFailCounts": { - "Status code is 200": { - "pass": 0, - "fail": 1 - } - }, - "times": [ - 131 - ], - "allTests": [ - { - "Status code is 200": false - } - ] - }, - { - "id": "22a71a75-3abb-4eca-bd99-ed642214e28d", - "name": "Set Cookies", - "url": "http://localhost:8082/nexus-backend/api/cookies/set?foo1=bar1&foo2=bar2", - "time": 249, - "responseCode": { - "code": 200, - "name": "OK" - }, - "tests": { - "Status code is 302 or 200": true, - "Body contains cookies": true, - "Body contains cookie foo1": false, - "Body contains cookie foo2": false, - "foo1 cookie is set": false, - "foo2 cookie is set": false - }, - "testPassFailCounts": { - "Status code is 302 or 200": { - "pass": 1, - "fail": 0 - }, - "Body contains cookies": { - "pass": 1, - "fail": 0 - }, - "Body contains cookie foo1": { - "pass": 0, - "fail": 1 - }, - "Body contains cookie foo2": { - "pass": 0, - "fail": 1 - }, - "foo1 cookie is set": { - "pass": 0, - "fail": 1 - }, - "foo2 cookie is set": { - "pass": 0, - "fail": 1 - } - }, - "times": [ - 249 - ], - "allTests": [ - { - "Status code is 302 or 200": true, - "Body contains cookies": true, - "Body contains cookie foo1": false, - "Body contains cookie foo2": false, - "foo1 cookie is set": false, - "foo2 cookie is set": false - } - ] - }, - { - "id": "4e3a5f83-cbca-45ca-a6df-4ab759f7041a", - "name": "Get Cookies", - "url": "http://localhost:8082/nexus-backend/api/cookies", - "time": 119, - "responseCode": { - "code": 200, - "name": "OK" - }, - "tests": { - "Body contains cookies": true, - "Cookies object is empty": false, - "Status code is 200": true - }, - "testPassFailCounts": { - "Body contains cookies": { - "pass": 1, - "fail": 0 - }, - "Cookies object is empty": { - "pass": 0, - "fail": 1 - }, - "Status code is 200": { - "pass": 1, - "fail": 0 - } - }, - "times": [ - 119 - ], - "allTests": [ - { - "Body contains cookies": true, - "Cookies object is empty": false, - "Status code is 200": true - } - ] - }, - { - "id": "6d4cd094-85ef-47ef-af7d-3c4c9572c72e", - "name": "Delete Cookies", - "url": "http://localhost:8082/nexus-backend/api/cookies/delete?foo1&foo2", - "time": 507, - "responseCode": { - "code": 200, - "name": "OK" - }, - "tests": { - "Status code is 302 or 200": true, - "Body contains cookies": true, - "Body contains cookie foo1": false, - "Body contains cookie foo2": false, - "foo1 cookie is set": true, - "foo2 cookie is set": true - }, - "testPassFailCounts": { - "Status code is 302 or 200": { - "pass": 1, - "fail": 0 - }, - "Body contains cookies": { - "pass": 1, - "fail": 0 - }, - "Body contains cookie foo1": { - "pass": 0, - "fail": 1 - }, - "Body contains cookie foo2": { - "pass": 0, - "fail": 1 - }, - "foo1 cookie is set": { - "pass": 1, - "fail": 0 - }, - "foo2 cookie is set": { - "pass": 1, - "fail": 0 - } - }, - "times": [ - 507 - ], - "allTests": [ - { - "Status code is 302 or 200": true, - "Body contains cookies": true, - "Body contains cookie foo1": false, - "Body contains cookie foo2": false, - "foo1 cookie is set": true, - "foo2 cookie is set": true - } - ] - }, - { - "id": "60f8f9e2-c294-4ae7-b0f7-df5a29d9b14b", - "name": "Request Headers", - "url": "http://localhost:8082/nexus-backend/api/headers", - "time": 210, - "responseCode": { - "code": 200, - "name": "OK" - }, - "tests": { - "Body contains headers": true, - "Header contains host": true, - "Header contains test parameter sent as part of request header": true - }, - "testPassFailCounts": { - "Body contains headers": { - "pass": 1, - "fail": 0 - }, - "Header contains host": { - "pass": 1, - "fail": 0 - }, - "Header contains test parameter sent as part of request header": { - "pass": 1, - "fail": 0 - } - }, - "times": [ - 210 - ], - "allTests": [ - { - "Body contains headers": true, - "Header contains host": true, - "Header contains test parameter sent as part of request header": true - } - ] - }, - { - "id": "e7503622-7ae0-4538-a0cb-a9d71dc2df67", - "name": "Response Headers", - "url": "http://localhost:8082/nexus-backend/api/response-headers?Content-Type=text/html&test=response_headers", - "time": 114, - "responseCode": { - "code": 200, - "name": "OK" - }, - "tests": { - "Body contains Content-Type": true, - "response headers have key sent as part of request": false - }, - "testPassFailCounts": { - "Body contains Content-Type": { - "pass": 1, - "fail": 0 - }, - "response headers have key sent as part of request": { - "pass": 0, - "fail": 1 - } - }, - "times": [ - 114 - ], - "allTests": [ - { - "Body contains Content-Type": true, - "response headers have key sent as part of request": false - } - ] - }, - { - "id": "95eb6f4a-c9ba-4420-9c48-f45b1cbcb95a", - "name": "GET Request", - "url": "http://localhost:8082/nexus-backend/api/get?test=123", - "time": 117, - "responseCode": { - "code": 200, - "name": "OK" - }, - "tests": { - "response is valid JSON": true, - "response json contains headers": true, - "response json contains args": true, - "response json contains url": true, - "args key contains argument passed as url parameter": true, - "args passed via request url params has value \"123\"": true - }, - "testPassFailCounts": { - "response is valid JSON": { - "pass": 1, - "fail": 0 - }, - "response json contains headers": { - "pass": 1, - "fail": 0 - }, - "response json contains args": { - "pass": 1, - "fail": 0 - }, - "response json contains url": { - "pass": 1, - "fail": 0 - }, - "args key contains argument passed as url parameter": { - "pass": 1, - "fail": 0 - }, - "args passed via request url params has value \"123\"": { - "pass": 1, - "fail": 0 - } - }, - "times": [ - 117 - ], - "allTests": [ - { - "response is valid JSON": true, - "response json contains headers": true, - "response json contains args": true, - "response json contains url": true, - "args key contains argument passed as url parameter": true, - "args passed via request url params has value \"123\"": true - } - ] - }, - { - "id": "970d7b83-76f7-4697-8faf-8572501e48c7", - "name": "POST Raw Text", - "url": "http://localhost:8082/nexus-backend/api/post", - "time": 120, - "responseCode": { - "code": 200, - "name": "OK" - }, - "tests": { - "response is valid JSON": true, - "response has post data": true, - "response matches the data posted": true, - "content-type equals text/plain": true - }, - "testPassFailCounts": { - "response is valid JSON": { - "pass": 1, - "fail": 0 - }, - "response has post data": { - "pass": 1, - "fail": 0 - }, - "response matches the data posted": { - "pass": 1, - "fail": 0 - }, - "content-type equals text/plain": { - "pass": 1, - "fail": 0 - } - }, - "times": [ - 120 - ], - "allTests": [ - { - "response is valid JSON": true, - "response has post data": true, - "response matches the data posted": true, - "content-type equals text/plain": true - } - ] - }, - { - "id": "eb1aa446-d144-4dad-b6ce-50d29d150e71", - "name": "POST Form Data", - "url": "http://localhost:8082/nexus-backend/api/post", - "time": 120, - "responseCode": { - "code": 200, - "name": "OK" - }, - "tests": { - "response is valid JSON": true, - "response has post data": true, - "response matches the data posted": true - }, - "testPassFailCounts": { - "response is valid JSON": { - "pass": 1, - "fail": 0 - }, - "response has post data": { - "pass": 1, - "fail": 0 - }, - "response matches the data posted": { - "pass": 1, - "fail": 0 - } - }, - "times": [ - 120 - ], - "allTests": [ - { - "response is valid JSON": true, - "response has post data": true, - "response matches the data posted": true - } - ] - }, - { - "id": "9c6de124-95bf-4a26-a3ed-16c70ae54c86", - "name": "PUT Request", - "url": "http://localhost:8082/nexus-backend/api/put", - "time": 121, - "responseCode": { - "code": 200, - "name": "OK" - }, - "tests": { - "response is valid JSON": true, - "response has PUT data": true, - "response matches the data sent in request": true - }, - "testPassFailCounts": { - "response is valid JSON": { - "pass": 1, - "fail": 0 - }, - "response has PUT data": { - "pass": 1, - "fail": 0 - }, - "response matches the data sent in request": { - "pass": 1, - "fail": 0 - } - }, - "times": [ - 121 - ], - "allTests": [ - { - "response is valid JSON": true, - "response has PUT data": true, - "response matches the data sent in request": true - } - ] - }, - { - "id": "9103ca4e-8334-49b0-a26a-9c8eaa1eef5f", - "name": "PATCH Request", - "url": "http://localhost:8082/nexus-backend/api/patch", - "time": 115, - "responseCode": { - "code": 200, - "name": "OK" - }, - "tests": { - "response is valid JSON": true, - "response has PUT data": true, - "response matches the data sent in request": true - }, - "testPassFailCounts": { - "response is valid JSON": { - "pass": 1, - "fail": 0 - }, - "response has PUT data": { - "pass": 1, - "fail": 0 - }, - "response matches the data sent in request": { - "pass": 1, - "fail": 0 - } - }, - "times": [ - 115 - ], - "allTests": [ - { - "response is valid JSON": true, - "response has PUT data": true, - "response matches the data sent in request": true - } - ] - }, - { - "id": "f4956418-9028-45c9-92ee-d4a6d26839bb", - "name": "DELETE Request", - "url": "http://localhost:8082/nexus-backend/api/delete", - "time": 116, - "responseCode": { - "code": 200, - "name": "OK" - }, - "tests": { - "response is valid JSON": true, - "response has PUT data": true, - "response matches the data sent in request": true - }, - "testPassFailCounts": { - "response is valid JSON": { - "pass": 1, - "fail": 0 - }, - "response has PUT data": { - "pass": 1, - "fail": 0 - }, - "response matches the data sent in request": { - "pass": 1, - "fail": 0 - } - }, - "times": [ - 116 - ], - "allTests": [ - { - "response is valid JSON": true, - "response has PUT data": true, - "response matches the data sent in request": true - } - ] - }, - { - "id": "c19fbecb-faa3-40ab-a765-fdf24784ac7b", - "name": "Response Status Code", - "url": "http://localhost:8082/nexus-backend/api/status/200", - "time": 116, - "responseCode": { - "code": 200, - "name": "OK" - }, - "tests": { - "Status equals 200": true, - "Body contains status": true - }, - "testPassFailCounts": { - "Status equals 200": { - "pass": 1, - "fail": 0 - }, - "Body contains status": { - "pass": 1, - "fail": 0 - } - }, - "times": [ - 116 - ], - "allTests": [ - { - "Status equals 200": true, - "Body contains status": true - } - ] - }, - { - "id": "8219d594-d835-4e41-860d-48a7e4eb756b", - "name": "Streamed Response", - "url": "http://localhost:8082/nexus-backend/api/stream/10", - "time": 116, - "responseCode": { - "code": 200, - "name": "OK" - }, - "tests": { - "response code is 200": true, - "response is sent in chunks": true - }, - "testPassFailCounts": { - "response code is 200": { - "pass": 1, - "fail": 0 - }, - "response is sent in chunks": { - "pass": 1, - "fail": 0 - } - }, - "times": [ - 116 - ], - "allTests": [ - { - "response code is 200": true, - "response is sent in chunks": true - } - ] - }, - { - "id": "720f6ce1-db91-4d7e-9eee-5b8e34cf9eed", - "name": "Delay Response", - "url": "http://localhost:8082/nexus-backend/api/delay/3", - "time": 3118, - "responseCode": { - "code": 200, - "name": "OK" - }, - "tests": { - "response body has key delay": true, - "response code is 200": true - }, - "testPassFailCounts": { - "response body has key delay": { - "pass": 1, - "fail": 0 - }, - "response code is 200": { - "pass": 1, - "fail": 0 - } - }, - "times": [ - 3118 - ], - "allTests": [ - { - "response body has key delay": true, - "response code is 200": true - } - ] - }, - { - "id": "3e77c741-a2c3-41c5-8ec7-78f403d40142", - "name": "Get UTF8 Encoded Response", - "url": "http://localhost:8082/nexus-backend/api/encoding/utf8", - "time": 222, - "responseCode": { - "code": 503, - "name": "Service Unavailable" - }, - "tests": { - "response code is 200": false - }, - "testPassFailCounts": { - "response code is 200": { - "pass": 0, - "fail": 1 - } - }, - "times": [ - 222 - ], - "allTests": [ - { - "response code is 200": false - } - ] - }, - { - "id": "5e68b328-f4fc-460e-9972-cf00026a7d3e", - "name": "GZip Compressed Response", - "url": "http://localhost:8082/nexus-backend/api/gzip", - "time": 122, - "responseCode": { - "code": 200, - "name": "OK" - }, - "tests": { - "Body contains gzipped": true, - "Body contains headers": true, - "Body contains method": true, - "response code is 200": true - }, - "testPassFailCounts": { - "Body contains gzipped": { - "pass": 1, - "fail": 0 - }, - "Body contains headers": { - "pass": 1, - "fail": 0 - }, - "Body contains method": { - "pass": 1, - "fail": 0 - }, - "response code is 200": { - "pass": 1, - "fail": 0 - } - }, - "times": [ - 122 - ], - "allTests": [ - { - "Body contains gzipped": true, - "Body contains headers": true, - "Body contains method": true, - "response code is 200": true - } - ] - }, - { - "id": "4495857c-182d-4fdf-95cd-282ba6a2b204", - "name": "Deflate Compressed Response", - "url": "http://localhost:8082/nexus-backend/api/deflate", - "time": 119, - "responseCode": { - "code": 200, - "name": "OK" - }, - "tests": { - "response code is 200": true, - "Body contains deflated": true, - "Body contains headers": true, - "Body contains method": true - }, - "testPassFailCounts": { - "response code is 200": { - "pass": 1, - "fail": 0 - }, - "Body contains deflated": { - "pass": 1, - "fail": 0 - }, - "Body contains headers": { - "pass": 1, - "fail": 0 - }, - "Body contains method": { - "pass": 1, - "fail": 0 - } - }, - "times": [ - 119 - ], - "allTests": [ - { - "response code is 200": true, - "Body contains deflated": true, - "Body contains headers": true, - "Body contains method": true - } - ] - }, - { - "id": "50aaf38a-9c68-4b57-907a-ab50432c5319", - "name": "IP address in JSON format", - "url": "http://localhost:8082/nexus-backend/api/ip", - "time": 112, - "responseCode": { - "code": 200, - "name": "OK" - }, - "tests": { - "Status code is 200": true, - "Valid response structure": true - }, - "testPassFailCounts": { - "Status code is 200": { - "pass": 1, - "fail": 0 - }, - "Valid response structure": { - "pass": 1, - "fail": 0 - } - }, - "times": [ - 112 - ], - "allTests": [ - { - "Status code is 200": true, - "Valid response structure": true - } - ] - }, - { - "id": "8d2fdd63-ef4d-412e-a61c-177895648460", - "name": "Current UTC time", - "url": "http://localhost:8082/nexus-backend/api/time/now", - "time": 117, - "responseCode": { - "code": 503, - "name": "Service Unavailable" - }, - "tests": { - "Status code is 200": false, - "Time is in a valid format": false - }, - "testPassFailCounts": { - "Status code is 200": { - "pass": 0, - "fail": 1 - }, - "Time is in a valid format": { - "pass": 0, - "fail": 1 - } - }, - "times": [ - 117 - ], - "allTests": [ - { - "Status code is 200": false, - "Time is in a valid format": false - } - ] - }, - { - "id": "cea020b3-f123-47bf-ad2a-6b77871fb909", - "name": "Timestamp validity", - "url": "http://localhost:8082/nexus-backend/api/time/valid?timestamp=2016-10-10", - "time": 120, - "responseCode": { - "code": 200, - "name": "OK" - }, - "tests": { - "Status code is 200": true, - "Timestamp is valid": true - }, - "testPassFailCounts": { - "Status code is 200": { - "pass": 1, - "fail": 0 - }, - "Timestamp is valid": { - "pass": 1, - "fail": 0 - } - }, - "times": [ - 120 - ], - "allTests": [ - { - "Status code is 200": true, - "Timestamp is valid": true - } - ] - }, - { - "id": "53929109-6ed6-4876-a53a-2408f0706d3e", - "name": "Format timestamp", - "url": "http://localhost:8082/nexus-backend/api/time/format?timestamp=2016-10-10&format=mm", - "time": 114, - "responseCode": { - "code": 200, - "name": "OK" - }, - "tests": { - "Status code is 200": true, - "Formatted result is valid": true - }, - "testPassFailCounts": { - "Status code is 200": { - "pass": 1, - "fail": 0 - }, - "Formatted result is valid": { - "pass": 1, - "fail": 0 - } - }, - "times": [ - 114 - ], - "allTests": [ - { - "Status code is 200": true, - "Formatted result is valid": true - } - ] - }, - { - "id": "2a916d1b-1d4d-4141-8752-1784b598fac7", - "name": "Extract timestamp unit", - "url": "http://localhost:8082/nexus-backend/api/time/unit?timestamp=2016-10-10&unit=day", - "time": 118, - "responseCode": { - "code": 200, - "name": "OK" - }, - "tests": { - "Status code is 200": true, - "Returned unit is valid": true - }, - "testPassFailCounts": { - "Status code is 200": { - "pass": 1, - "fail": 0 - }, - "Returned unit is valid": { - "pass": 1, - "fail": 0 - } - }, - "times": [ - 118 - ], - "allTests": [ - { - "Status code is 200": true, - "Returned unit is valid": true - } - ] - }, - { - "id": "57c30b3e-9d8d-4e06-8b6d-6ee5195e3c83", - "name": "Time addition", - "url": "http://localhost:8082/nexus-backend/api/time/add?timestamp=2016-10-10&years=100", - "time": 116, - "responseCode": { - "code": 200, - "name": "OK" - }, - "tests": { - "Status code is 200": true - }, - "testPassFailCounts": { - "Status code is 200": { - "pass": 1, - "fail": 0 - } - }, - "times": [ - 116 - ], - "allTests": [ - { - "Status code is 200": true - } - ] - }, - { - "id": "ac979179-08ae-43bf-ae66-94be4107347d", - "name": "Time subtraction", - "url": "http://localhost:8082/nexus-backend/api/time/subtract?timestamp=2016-10-10&years=100", - "time": 115, - "responseCode": { - "code": 200, - "name": "OK" - }, - "tests": { - "Status code is 200": true - }, - "testPassFailCounts": { - "Status code is 200": { - "pass": 1, - "fail": 0 - } - }, - "times": [ - 115 - ], - "allTests": [ - { - "Status code is 200": true - } - ] - }, - { - "id": "e3531abb-b3ff-48d5-8a23-8b1047a241a4", - "name": "Start of time", - "url": "http://localhost:8082/nexus-backend/api/time/start?timestamp=2016-10-10&unit=month", - "time": 114, - "responseCode": { - "code": 200, - "name": "OK" - }, - "tests": { - "Status code is 200": true - }, - "testPassFailCounts": { - "Status code is 200": { - "pass": 1, - "fail": 0 - } - }, - "times": [ - 114 - ], - "allTests": [ - { - "Status code is 200": true - } - ] - }, - { - "id": "6b5fca75-5fc6-4187-957f-1bdc429e3eea", - "name": "Object representation", - "url": "http://localhost:8082/nexus-backend/api/time/start?timestamp=2016-10-10&unit=month", - "time": 125, - "responseCode": { - "code": 200, - "name": "OK" - }, - "tests": { - "Status code is 200": true - }, - "testPassFailCounts": { - "Status code is 200": { - "pass": 1, - "fail": 0 - } - }, - "times": [ - 125 - ], - "allTests": [ - { - "Status code is 200": true - } - ] - }, - { - "id": "ea211f39-8fc1-4a7b-88de-4c5128549d02", - "name": "Before comparisons", - "url": "http://localhost:8082/nexus-backend/api/time/before?timestamp=2016-10-10&target=2017-10-10", - "time": 114, - "responseCode": { - "code": 200, - "name": "OK" - }, - "tests": { - "Status code is 200": true, - "Comparsion was correct": true - }, - "testPassFailCounts": { - "Status code is 200": { - "pass": 1, - "fail": 0 - }, - "Comparsion was correct": { - "pass": 1, - "fail": 0 - } - }, - "times": [ - 114 - ], - "allTests": [ - { - "Status code is 200": true, - "Comparsion was correct": true - } - ] - }, - { - "id": "46924470-4791-41da-b207-95f56b66fa3f", - "name": "After comparisons", - "url": "http://localhost:8082/nexus-backend/api/time/after?timestamp=2016-10-10&target=2017-10-10", - "time": 114, - "responseCode": { - "code": 200, - "name": "OK" - }, - "tests": { - "Status code is 200": true, - "Comparsion was correct": true - }, - "testPassFailCounts": { - "Status code is 200": { - "pass": 1, - "fail": 0 - }, - "Comparsion was correct": { - "pass": 1, - "fail": 0 - } - }, - "times": [ - 114 - ], - "allTests": [ - { - "Status code is 200": true, - "Comparsion was correct": true - } - ] - }, - { - "id": "100939a6-f006-4921-811c-c9ece72f0bf6", - "name": "Between timestamps", - "url": "http://localhost:8082/nexus-backend/api/time/between?timestamp=2016-10-10&start=2017-10-10&end=2019-10-10", - "time": 117, - "responseCode": { - "code": 200, - "name": "OK" - }, - "tests": { - "Status code is 200": true, - "Comparsion was correct": false - }, - "testPassFailCounts": { - "Status code is 200": { - "pass": 1, - "fail": 0 - }, - "Comparsion was correct": { - "pass": 0, - "fail": 1 - } - }, - "times": [ - 117 - ], - "allTests": [ - { - "Status code is 200": true, - "Comparsion was correct": false - } - ] - }, - { - "id": "4250cac7-9e32-4a88-8011-77a9fb9d0a76", - "name": "Leap year check", - "url": "http://localhost:8082/nexus-backend/api/time/leap?timestamp=2016-10-10", - "time": 113, - "responseCode": { - "code": 200, - "name": "OK" - }, - "tests": { - "Status code is 200": true, - "Comparsion was correct": true - }, - "testPassFailCounts": { - "Status code is 200": { - "pass": 1, - "fail": 0 - }, - "Comparsion was correct": { - "pass": 1, - "fail": 0 - } - }, - "times": [ - 113 - ], - "allTests": [ - { - "Status code is 200": true, - "Comparsion was correct": true - } - ] - }, - { - "id": "2f2581d8-3603-4180-92b2-196425b9f15e", - "name": "Transform collection from format v1 to v2", - "url": "http://localhost:8082/nexus-backend/api/transform/collection?from=1&to=2", - "time": 133, - "responseCode": { - "code": 404, - "name": "Not Found" - }, - "tests": {}, - "testPassFailCounts": {}, - "times": [ - 133 - ], - "allTests": [ - {} - ] - }, - { - "id": "90a5188a-36e7-4fe1-8501-2f5edbb4e865", - "name": "Transform collection from format v2 to v1", - "url": "http://localhost:8082/nexus-backend/api/transform/collection?from=2&to=1", - "time": 120, - "responseCode": { - "code": 404, - "name": "Not Found" - }, - "tests": {}, - "testPassFailCounts": {}, - "times": [ - 120 - ], - "allTests": [ - {} - ] - } - ], - "count": 1, - "totalTime": 9211, - "collection": { - "requests": [ - { - "id": "ede243e5-78b3-427e-a3c2-be1c552fb55e", - "method": "GET" - }, - { - "id": "1ae52c41-98de-4c10-808d-9cbea3e0d05d", - "method": "GET" - }, - { - "id": "b50da305-15a9-4d13-8189-d3b8c688b71d", - "method": "GET" - }, - { - "id": "22a71a75-3abb-4eca-bd99-ed642214e28d", - "method": "GET" - }, - { - "id": "4e3a5f83-cbca-45ca-a6df-4ab759f7041a", - "method": "GET" - }, - { - "id": "6d4cd094-85ef-47ef-af7d-3c4c9572c72e", - "method": "GET" - }, - { - "id": "60f8f9e2-c294-4ae7-b0f7-df5a29d9b14b", - "method": "GET" - }, - { - "id": "e7503622-7ae0-4538-a0cb-a9d71dc2df67", - "method": "GET" - }, - { - "id": "95eb6f4a-c9ba-4420-9c48-f45b1cbcb95a", - "method": "GET" - }, - { - "id": "970d7b83-76f7-4697-8faf-8572501e48c7", - "method": "POST" - }, - { - "id": "eb1aa446-d144-4dad-b6ce-50d29d150e71", - "method": "POST" - }, - { - "id": "9c6de124-95bf-4a26-a3ed-16c70ae54c86", - "method": "PUT" - }, - { - "id": "9103ca4e-8334-49b0-a26a-9c8eaa1eef5f", - "method": "PATCH" - }, - { - "id": "f4956418-9028-45c9-92ee-d4a6d26839bb", - "method": "DELETE" - }, - { - "id": "c19fbecb-faa3-40ab-a765-fdf24784ac7b", - "method": "GET" - }, - { - "id": "8219d594-d835-4e41-860d-48a7e4eb756b", - "method": "GET" - }, - { - "id": "720f6ce1-db91-4d7e-9eee-5b8e34cf9eed", - "method": "GET" - }, - { - "id": "3e77c741-a2c3-41c5-8ec7-78f403d40142", - "method": "GET" - }, - { - "id": "5e68b328-f4fc-460e-9972-cf00026a7d3e", - "method": "GET" - }, - { - "id": "4495857c-182d-4fdf-95cd-282ba6a2b204", - "method": "GET" - }, - { - "id": "50aaf38a-9c68-4b57-907a-ab50432c5319", - "method": "GET" - }, - { - "id": "8d2fdd63-ef4d-412e-a61c-177895648460", - "method": "GET" - }, - { - "id": "cea020b3-f123-47bf-ad2a-6b77871fb909", - "method": "GET" - }, - { - "id": "53929109-6ed6-4876-a53a-2408f0706d3e", - "method": "GET" - }, - { - "id": "2a916d1b-1d4d-4141-8752-1784b598fac7", - "method": "GET" - }, - { - "id": "57c30b3e-9d8d-4e06-8b6d-6ee5195e3c83", - "method": "GET" - }, - { - "id": "ac979179-08ae-43bf-ae66-94be4107347d", - "method": "GET" - }, - { - "id": "e3531abb-b3ff-48d5-8a23-8b1047a241a4", - "method": "GET" - }, - { - "id": "6b5fca75-5fc6-4187-957f-1bdc429e3eea", - "method": "GET" - }, - { - "id": "ea211f39-8fc1-4a7b-88de-4c5128549d02", - "method": "GET" - }, - { - "id": "46924470-4791-41da-b207-95f56b66fa3f", - "method": "GET" - }, - { - "id": "100939a6-f006-4921-811c-c9ece72f0bf6", - "method": "GET" - }, - { - "id": "4250cac7-9e32-4a88-8011-77a9fb9d0a76", - "method": "GET" - }, - { - "id": "2f2581d8-3603-4180-92b2-196425b9f15e", - "method": "POST" - }, - { - "id": "90a5188a-36e7-4fe1-8501-2f5edbb4e865", - "method": "POST" - } - ] - } -} diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml deleted file mode 100644 index ab8ebc8..0000000 --- a/src/test/resources/logback-test.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - %date{HH:mm:ss.SSS} %-5level %logger{36} - %msg %n - - - - - - - - - - - - diff --git a/src/test/resources/mock-api.json b/src/test/resources/mock-api.json deleted file mode 100644 index 4e4c9f2..0000000 --- a/src/test/resources/mock-api.json +++ /dev/null @@ -1,890 +0,0 @@ -{ - "openapi": "3.0.1", - "info": { - "title": "Mock Test API", - "description": "The Mock Test Api Nexus Backend Application\n- 1 Test GET, POST, PUT, PATCH, DELETE\n- 2 Test POST, PUT, PATCH file\n- 3 Test Error 400, 401 and 500\n- 4 Test Security GET and POST XSS data\n", - "contact": { - "name": "Franck Andriano.", - "email": "franck@jservlet.com" - }, - "license": { - "name": "Copyright (c) JServlet.com", - "url": "https://jservlet.com" - }, - "version": "1.0.17" - }, - "servers": [ - { - "url": "http://localhost:8082/nexus-backend" - } - ], - "paths": { - "/mock/v1/datafile": { - "get": { - "tags": [ - "Mock" - ], - "summary": "Get datafile", - "description": "Get datafile", - "operationId": "getFile", - "responses": { - "200": { - "description": "Request executed successfully, returning the requested item(s)", - "content": { - "application/octet-stream": { - "schema": { - "type": "string" - } - } - } - } - } - }, - "put": { - "tags": [ - "Mock" - ], - "summary": "Put datafile", - "description": "Put datafile", - "operationId": "putFile", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "required": [ - "file" - ], - "type": "object", - "properties": { - "file": { - "type": "string", - "format": "binary" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Request executed successfully, returning the requested item(s)", - "content": { - "application/json": { - "schema": { - "type": "string", - "enum": [ - "100 CONTINUE", - "101 SWITCHING_PROTOCOLS", - "102 PROCESSING", - "103 CHECKPOINT", - "200 OK", - "201 CREATED", - "202 ACCEPTED", - "203 NON_AUTHORITATIVE_INFORMATION", - "204 NO_CONTENT", - "205 RESET_CONTENT", - "206 PARTIAL_CONTENT", - "207 MULTI_STATUS", - "208 ALREADY_REPORTED", - "226 IM_USED", - "300 MULTIPLE_CHOICES", - "301 MOVED_PERMANENTLY", - "302 FOUND", - "302 MOVED_TEMPORARILY", - "303 SEE_OTHER", - "304 NOT_MODIFIED", - "305 USE_PROXY", - "307 TEMPORARY_REDIRECT", - "308 PERMANENT_REDIRECT", - "400 BAD_REQUEST", - "401 UNAUTHORIZED", - "402 PAYMENT_REQUIRED", - "403 FORBIDDEN", - "404 NOT_FOUND", - "405 METHOD_NOT_ALLOWED", - "406 NOT_ACCEPTABLE", - "407 PROXY_AUTHENTICATION_REQUIRED", - "408 REQUEST_TIMEOUT", - "409 CONFLICT", - "410 GONE", - "411 LENGTH_REQUIRED", - "412 PRECONDITION_FAILED", - "413 PAYLOAD_TOO_LARGE", - "413 REQUEST_ENTITY_TOO_LARGE", - "414 URI_TOO_LONG", - "414 REQUEST_URI_TOO_LONG", - "415 UNSUPPORTED_MEDIA_TYPE", - "416 REQUESTED_RANGE_NOT_SATISFIABLE", - "417 EXPECTATION_FAILED", - "418 I_AM_A_TEAPOT", - "419 INSUFFICIENT_SPACE_ON_RESOURCE", - "420 METHOD_FAILURE", - "421 DESTINATION_LOCKED", - "422 UNPROCESSABLE_ENTITY", - "423 LOCKED", - "424 FAILED_DEPENDENCY", - "425 TOO_EARLY", - "426 UPGRADE_REQUIRED", - "428 PRECONDITION_REQUIRED", - "429 TOO_MANY_REQUESTS", - "431 REQUEST_HEADER_FIELDS_TOO_LARGE", - "451 UNAVAILABLE_FOR_LEGAL_REASONS", - "500 INTERNAL_SERVER_ERROR", - "501 NOT_IMPLEMENTED", - "502 BAD_GATEWAY", - "503 SERVICE_UNAVAILABLE", - "504 GATEWAY_TIMEOUT", - "505 HTTP_VERSION_NOT_SUPPORTED", - "506 VARIANT_ALSO_NEGOTIATES", - "507 INSUFFICIENT_STORAGE", - "508 LOOP_DETECTED", - "509 BANDWIDTH_LIMIT_EXCEEDED", - "510 NOT_EXTENDED", - "511 NETWORK_AUTHENTICATION_REQUIRED" - ] - } - } - } - } - } - }, - "post": { - "tags": [ - "Mock" - ], - "summary": "Post datafile", - "description": "Post datafile", - "operationId": "postFile", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "required": [ - "file" - ], - "type": "object", - "properties": { - "file": { - "type": "string", - "format": "binary" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Request executed successfully, returning the requested item(s)", - "content": { - "application/json": { - "schema": { - "type": "string", - "enum": [ - "100 CONTINUE", - "101 SWITCHING_PROTOCOLS", - "102 PROCESSING", - "103 CHECKPOINT", - "200 OK", - "201 CREATED", - "202 ACCEPTED", - "203 NON_AUTHORITATIVE_INFORMATION", - "204 NO_CONTENT", - "205 RESET_CONTENT", - "206 PARTIAL_CONTENT", - "207 MULTI_STATUS", - "208 ALREADY_REPORTED", - "226 IM_USED", - "300 MULTIPLE_CHOICES", - "301 MOVED_PERMANENTLY", - "302 FOUND", - "302 MOVED_TEMPORARILY", - "303 SEE_OTHER", - "304 NOT_MODIFIED", - "305 USE_PROXY", - "307 TEMPORARY_REDIRECT", - "308 PERMANENT_REDIRECT", - "400 BAD_REQUEST", - "401 UNAUTHORIZED", - "402 PAYMENT_REQUIRED", - "403 FORBIDDEN", - "404 NOT_FOUND", - "405 METHOD_NOT_ALLOWED", - "406 NOT_ACCEPTABLE", - "407 PROXY_AUTHENTICATION_REQUIRED", - "408 REQUEST_TIMEOUT", - "409 CONFLICT", - "410 GONE", - "411 LENGTH_REQUIRED", - "412 PRECONDITION_FAILED", - "413 PAYLOAD_TOO_LARGE", - "413 REQUEST_ENTITY_TOO_LARGE", - "414 URI_TOO_LONG", - "414 REQUEST_URI_TOO_LONG", - "415 UNSUPPORTED_MEDIA_TYPE", - "416 REQUESTED_RANGE_NOT_SATISFIABLE", - "417 EXPECTATION_FAILED", - "418 I_AM_A_TEAPOT", - "419 INSUFFICIENT_SPACE_ON_RESOURCE", - "420 METHOD_FAILURE", - "421 DESTINATION_LOCKED", - "422 UNPROCESSABLE_ENTITY", - "423 LOCKED", - "424 FAILED_DEPENDENCY", - "425 TOO_EARLY", - "426 UPGRADE_REQUIRED", - "428 PRECONDITION_REQUIRED", - "429 TOO_MANY_REQUESTS", - "431 REQUEST_HEADER_FIELDS_TOO_LARGE", - "451 UNAVAILABLE_FOR_LEGAL_REASONS", - "500 INTERNAL_SERVER_ERROR", - "501 NOT_IMPLEMENTED", - "502 BAD_GATEWAY", - "503 SERVICE_UNAVAILABLE", - "504 GATEWAY_TIMEOUT", - "505 HTTP_VERSION_NOT_SUPPORTED", - "506 VARIANT_ALSO_NEGOTIATES", - "507 INSUFFICIENT_STORAGE", - "508 LOOP_DETECTED", - "509 BANDWIDTH_LIMIT_EXCEEDED", - "510 NOT_EXTENDED", - "511 NETWORK_AUTHENTICATION_REQUIRED" - ] - } - } - } - } - } - }, - "delete": { - "tags": [ - "Mock" - ], - "summary": "Delete datafile", - "description": "Delete datafile", - "operationId": "deleteFile", - "responses": { - "200": { - "description": "Request executed successfully, returning the requested item(s)", - "content": { - "application/json": { - "schema": { - "type": "string", - "enum": [ - "100 CONTINUE", - "101 SWITCHING_PROTOCOLS", - "102 PROCESSING", - "103 CHECKPOINT", - "200 OK", - "201 CREATED", - "202 ACCEPTED", - "203 NON_AUTHORITATIVE_INFORMATION", - "204 NO_CONTENT", - "205 RESET_CONTENT", - "206 PARTIAL_CONTENT", - "207 MULTI_STATUS", - "208 ALREADY_REPORTED", - "226 IM_USED", - "300 MULTIPLE_CHOICES", - "301 MOVED_PERMANENTLY", - "302 FOUND", - "302 MOVED_TEMPORARILY", - "303 SEE_OTHER", - "304 NOT_MODIFIED", - "305 USE_PROXY", - "307 TEMPORARY_REDIRECT", - "308 PERMANENT_REDIRECT", - "400 BAD_REQUEST", - "401 UNAUTHORIZED", - "402 PAYMENT_REQUIRED", - "403 FORBIDDEN", - "404 NOT_FOUND", - "405 METHOD_NOT_ALLOWED", - "406 NOT_ACCEPTABLE", - "407 PROXY_AUTHENTICATION_REQUIRED", - "408 REQUEST_TIMEOUT", - "409 CONFLICT", - "410 GONE", - "411 LENGTH_REQUIRED", - "412 PRECONDITION_FAILED", - "413 PAYLOAD_TOO_LARGE", - "413 REQUEST_ENTITY_TOO_LARGE", - "414 URI_TOO_LONG", - "414 REQUEST_URI_TOO_LONG", - "415 UNSUPPORTED_MEDIA_TYPE", - "416 REQUESTED_RANGE_NOT_SATISFIABLE", - "417 EXPECTATION_FAILED", - "418 I_AM_A_TEAPOT", - "419 INSUFFICIENT_SPACE_ON_RESOURCE", - "420 METHOD_FAILURE", - "421 DESTINATION_LOCKED", - "422 UNPROCESSABLE_ENTITY", - "423 LOCKED", - "424 FAILED_DEPENDENCY", - "425 TOO_EARLY", - "426 UPGRADE_REQUIRED", - "428 PRECONDITION_REQUIRED", - "429 TOO_MANY_REQUESTS", - "431 REQUEST_HEADER_FIELDS_TOO_LARGE", - "451 UNAVAILABLE_FOR_LEGAL_REASONS", - "500 INTERNAL_SERVER_ERROR", - "501 NOT_IMPLEMENTED", - "502 BAD_GATEWAY", - "503 SERVICE_UNAVAILABLE", - "504 GATEWAY_TIMEOUT", - "505 HTTP_VERSION_NOT_SUPPORTED", - "506 VARIANT_ALSO_NEGOTIATES", - "507 INSUFFICIENT_STORAGE", - "508 LOOP_DETECTED", - "509 BANDWIDTH_LIMIT_EXCEEDED", - "510 NOT_EXTENDED", - "511 NETWORK_AUTHENTICATION_REQUIRED" - ] - } - } - } - } - } - }, - "patch": { - "tags": [ - "Mock" - ], - "summary": "Patch datafile", - "description": "Patch datafile", - "operationId": "patchFile", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "required": [ - "file" - ], - "type": "object", - "properties": { - "file": { - "type": "string", - "format": "binary" - } - } - } - } - } - }, - "responses": { - "204": { - "description": "Request executed successfully, returning the requested item(s)", - "content": { - "application/json": { - "schema": { - "type": "string", - "enum": [ - "100 CONTINUE", - "101 SWITCHING_PROTOCOLS", - "102 PROCESSING", - "103 CHECKPOINT", - "200 OK", - "201 CREATED", - "202 ACCEPTED", - "203 NON_AUTHORITATIVE_INFORMATION", - "204 NO_CONTENT", - "205 RESET_CONTENT", - "206 PARTIAL_CONTENT", - "207 MULTI_STATUS", - "208 ALREADY_REPORTED", - "226 IM_USED", - "300 MULTIPLE_CHOICES", - "301 MOVED_PERMANENTLY", - "302 FOUND", - "302 MOVED_TEMPORARILY", - "303 SEE_OTHER", - "304 NOT_MODIFIED", - "305 USE_PROXY", - "307 TEMPORARY_REDIRECT", - "308 PERMANENT_REDIRECT", - "400 BAD_REQUEST", - "401 UNAUTHORIZED", - "402 PAYMENT_REQUIRED", - "403 FORBIDDEN", - "404 NOT_FOUND", - "405 METHOD_NOT_ALLOWED", - "406 NOT_ACCEPTABLE", - "407 PROXY_AUTHENTICATION_REQUIRED", - "408 REQUEST_TIMEOUT", - "409 CONFLICT", - "410 GONE", - "411 LENGTH_REQUIRED", - "412 PRECONDITION_FAILED", - "413 PAYLOAD_TOO_LARGE", - "413 REQUEST_ENTITY_TOO_LARGE", - "414 URI_TOO_LONG", - "414 REQUEST_URI_TOO_LONG", - "415 UNSUPPORTED_MEDIA_TYPE", - "416 REQUESTED_RANGE_NOT_SATISFIABLE", - "417 EXPECTATION_FAILED", - "418 I_AM_A_TEAPOT", - "419 INSUFFICIENT_SPACE_ON_RESOURCE", - "420 METHOD_FAILURE", - "421 DESTINATION_LOCKED", - "422 UNPROCESSABLE_ENTITY", - "423 LOCKED", - "424 FAILED_DEPENDENCY", - "425 TOO_EARLY", - "426 UPGRADE_REQUIRED", - "428 PRECONDITION_REQUIRED", - "429 TOO_MANY_REQUESTS", - "431 REQUEST_HEADER_FIELDS_TOO_LARGE", - "451 UNAVAILABLE_FOR_LEGAL_REASONS", - "500 INTERNAL_SERVER_ERROR", - "501 NOT_IMPLEMENTED", - "502 BAD_GATEWAY", - "503 SERVICE_UNAVAILABLE", - "504 GATEWAY_TIMEOUT", - "505 HTTP_VERSION_NOT_SUPPORTED", - "506 VARIANT_ALSO_NEGOTIATES", - "507 INSUFFICIENT_STORAGE", - "508 LOOP_DETECTED", - "509 BANDWIDTH_LIMIT_EXCEEDED", - "510 NOT_EXTENDED", - "511 NETWORK_AUTHENTICATION_REQUIRED" - ] - } - } - } - } - } - } - }, - "/mock/v1/data": { - "get": { - "tags": [ - "Mock" - ], - "summary": "Get a data", - "description": "Get a data", - "operationId": "get", - "responses": { - "200": { - "description": "Request executed successfully, returning the requested item(s)", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Data" - } - } - } - } - } - }, - "put": { - "tags": [ - "Mock" - ], - "summary": "Put a data", - "description": "Put a data", - "operationId": "put", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Data" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Request executed successfully, returning the requested item(s)", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Data" - } - } - } - } - } - }, - "post": { - "tags": [ - "Mock" - ], - "summary": "Post a data", - "description": "Post a data", - "operationId": "post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Data" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Request executed successfully, returning the requested item(s)", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Data" - } - } - } - } - } - }, - "patch": { - "tags": [ - "Mock" - ], - "summary": "Patch a data", - "description": "Patch a data", - "operationId": "patch", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Data" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Request executed successfully, returning the requested item(s)", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Data" - } - } - } - } - } - } - }, - "/mock/v1/proxy": { - "post": { - "tags": [ - "Mock" - ], - "summary": "Proxy Post data ", - "description": "Proxy Post data", - "operationId": "redirect", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "type": "string" - } - } - } - }, - "responses": { - "200": { - "description": "Request executed successfully, returning the requested item(s)", - "content": { - "*/*": { - "schema": { - "type": "string" - } - } - } - } - } - } - }, - "/mock/v1/dataPostXss": { - "post": { - "tags": [ - "Mock" - ], - "summary": "Post data Xss", - "description": "Post data Xss", - "operationId": "postXss", - "parameters": [ - { - "name": "param1", - "in": "query", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Data" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Request executed successfully, returning the requested item(s)", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Data" - } - } - } - } - } - } - }, - "/mock/v1/dataPostList": { - "post": { - "tags": [ - "Mock" - ], - "summary": "Post data List", - "description": "Post data List", - "operationId": "postDataList", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Data" - } - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Request executed successfully, returning the requested item(s)", - "content": { - "application/json": { - "schema": { - "type": "string" - } - } - } - } - } - } - }, - "/mock/v1/dataList": { - "get": { - "tags": [ - "Mock" - ], - "summary": "Get data List", - "description": "Get data List", - "operationId": "getList", - "responses": { - "200": { - "description": "Request executed successfully, returning the requested item(s)", - "content": { - "application/json": { - "schema": { - "type": "string" - } - } - } - } - } - }, - "post": { - "tags": [ - "Mock" - ], - "summary": "Post and get data List", - "description": "Post and get data List", - "operationId": "postDataList_1", - "responses": { - "200": { - "description": "Request executed successfully, returning the requested item(s)", - "content": { - "application/json": { - "schema": { - "type": "string" - } - } - } - } - } - } - }, - "/mock/v1/dataXss": { - "get": { - "tags": [ - "Mock" - ], - "summary": "Get data Xss", - "description": "Get data Xss", - "operationId": "getXss", - "parameters": [ - { - "name": "param1", - "in": "query", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Request executed successfully, returning the requested item(s)", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Data" - } - } - } - } - } - } - }, - "/mock/v1/dataError500": { - "get": { - "tags": [ - "Mock" - ], - "summary": "Get Error 500", - "description": "Get Error 500", - "operationId": "getError500", - "responses": { - "500": { - "description": "Internal server error, see error code and documentation for more details", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Message" - } - } - } - } - } - } - }, - "/mock/v1/dataError401": { - "get": { - "tags": [ - "Mock" - ], - "summary": "Get Error 401", - "description": "Get Error 401", - "operationId": "getError401", - "responses": { - "401": { - "description": "User not authenticated", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Message" - } - } - } - } - } - } - }, - "/mock/v1/dataError400": { - "get": { - "tags": [ - "Mock" - ], - "summary": "Get Error 400", - "description": "Get Error 400", - "operationId": "getError400", - "responses": { - "400": { - "description": "Request is not formed correctly", - "content": { - "application/json": { - "schema": { - "type": "string" - } - } - } - } - } - } - }, - "/mock/v1/dataBytes": { - "get": { - "tags": [ - "Mock" - ], - "summary": "Get ByteArray data", - "description": "Get ByteArray data", - "operationId": "getBytes", - "responses": { - "200": { - "description": "Request executed successfully, returning the requested item(s)", - "content": { - "*/*": { - "schema": { - "type": "string" - } - } - } - } - } - } - } - }, - "components": { - "schemas": { - "Data": { - "type": "object", - "properties": { - "data1": { - "type": "string" - }, - "data2": { - "type": "string" - }, - "data3": { - "type": "number", - "format": "double" - }, - "data4": { - "type": "string", - "format": "date-time" - } - } - }, - "Message": { - "type": "object", - "properties": { - "code": { - "type": "string" - }, - "level": { - "type": "string" - }, - "source": { - "type": "string" - }, - "message": { - "type": "string" - }, - "cause": { - "type": "string" - }, - "parameters": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - } - } -} diff --git a/src/test/resources/test.html b/src/test/resources/test.html deleted file mode 100644 index 2faae1c..0000000 --- a/src/test/resources/test.html +++ /dev/null @@ -1,43 +0,0 @@ - -
- - -
- - - -
-
-
-    
-
- - -