From 4d15e6b934bf58d2c46457faacb25004a710fae6 Mon Sep 17 00:00:00 2001 From: Francisco J Lopez-Pellicer Date: Wed, 23 Apr 2025 17:06:06 +0200 Subject: [PATCH 1/3] Feat: Add PostgreSQL and Oracle JDBC dependencies to build.gradle --- build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.gradle b/build.gradle index 9256499..3d757ad 100644 --- a/build.gradle +++ b/build.gradle @@ -27,6 +27,9 @@ dependencies { implementation "io.jsonwebtoken:jjwt-jackson:${jjwt_version}" implementation "com.squareup.okhttp3:okhttp:${okhttp3_version}" + implementation 'org.postgresql:postgresql' + implementation 'com.oracle.database.jdbc:ojdbc11-production' + //Jdbc drivers testImplementation 'com.h2database:h2' From 080973f6bc9da4336fc973018d7f692a82df5a0e Mon Sep 17 00:00:00 2001 From: Francisco J Lopez-Pellicer Date: Sun, 3 Aug 2025 19:30:57 +0200 Subject: [PATCH 2/3] refactor: major architectural overhaul and Spring Boot 3.5.4 upgrade - Upgrade from Spring Boot 2.7.18 to Spring Boot 3.5.4 - Reorganize codebase into protocol-based architecture (http, jdbc, wms) - Upgrade to Java 17 and implement Gradle version catalog - Add comprehensive code quality tools (Spotless, enhanced JaCoCo) - Restructure Docker configuration and add environment-specific configs - Completely rewrite documentation with detailed architecture guide - Add Git hooks for automated code quality enforcement - Improve test organization with protocol-specific test classes - Modernize build system with version catalog and quality gates This refactoring improves maintainability, testability, and developer experience while maintaining backward compatibility with existing APIs. --- .editorconfig | 3 - Dockerfile | 11 - README.md | 1119 ++++++++++++++++- build.gradle | 210 +++- config/application-dev.yml | 33 + config/application-prod.yml | 24 + config/application.yml | 27 + docker-compose.yml | 13 - docker/Dockerfile | 46 + docker/development/docker-compose.yml | 40 + gradle.properties | 11 +- gradle/libs.versions.toml | 43 + gradle/wrapper/gradle-wrapper.jar | Bin 59821 -> 63721 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 35 +- gradlew.bat | 1 + lombok.config | 3 - ...ewareApplication.java => Application.java} | 7 +- .../config/ProxyMiddlewareConfiguration.java | 31 +- .../ProxyMiddlewareController.java | 30 +- .../decorator/DecoratedRequest.java | 6 - .../decorator/RequestDecorator.java | 4 +- .../decorator/ResponseDecorator.java | 4 +- .../request/BodyRequestDecorator.java | 28 - .../CapabiltiesResponseDecorator.java | 38 - .../response/PaginationResponseDecorator.java | 21 - .../proxy/middleware/dto/ConfigProxyDto.java | 8 +- ...equest.java => ConfigProxyRequestDto.java} | 10 +- .../middleware/dto/DatasourcePayloadDto.java | 46 - ...ResponseDTO.java => ErrorResponseDto.java} | 5 +- .../proxy/middleware/dto/HttpSecurityDto.java | 2 +- .../middleware/dto/OgcWmsPayloadDto.java | 43 - .../proxy/middleware/dto/PayloadDto.java | 7 +- .../http/HttpClient.java} | 7 +- .../http/HttpClientFactoryService.java | 137 ++ .../http}/HttpContext.java | 3 +- .../http}/HttpContextSecurity.java | 3 +- ...HttpRequestDecoratorAddBasicSecurity.java} | 25 +- .../HttpRequestDecoratorAddEndpoint.java} | 12 +- .../protocols/http/HttpRequestExecutor.java | 127 ++ .../jdbc}/JdbcContext.java | 3 +- .../protocols/jdbc/JdbcPayloadDto.java | 54 + .../JdbcRequestDecoratorAddConnection.java} | 19 +- .../jdbc/JdbcRequestDecoratorAddQuery.java} | 9 +- .../jdbc/JdbcRequestExecutor.java} | 35 +- .../wms/WmsCapabilitiesResponseDecorator.java | 42 + .../protocols/wms/WmsPayloadDto.java | 56 + .../proxy/middleware/request/HttpRequest.java | 115 -- .../middleware/request/RequestFactory.java | 28 - .../middleware/service/HttpClientFactory.java | 85 -- ....java => RequestConfigurationService.java} | 56 +- .../middleware/service/RequestExecutor.java | 7 + .../service/RequestExecutorFactory.java | 29 + .../RequestExecutorResponse.java} | 4 +- .../RequestExecutorResponseImpl.java} | 15 +- ...rvice.java => RequestExecutorService.java} | 31 +- ...itional-spring-configuration-metadata.json | 22 + src/main/resources/application-dev.yml | 33 + src/main/resources/application-prod.yml | 24 + src/main/resources/application.yml | 23 + .../ExecutionRequestExecutorServiceTest.java | 58 + .../http/HttpClientFactoryServiceTest.java | 63 + .../ExecutionRequestExecutorServiceTest.java | 59 + .../ExecutionRequestExecutorServiceTest.java | 97 ++ .../ExecutionRequestExecutorServiceTest.java | 144 +++ .../service/GlobalRequestServiceTest.java | 154 --- .../proxy/middleware/test/TestUtils.java | 38 +- .../UserPasswordAuthenticationRequest.java | 5 +- .../fixtures/AuthorizationProxyFixtures.java | 81 +- .../interceptors/CheckBasicAuthorization.java | 6 +- .../test/interceptors/DoNotRequest.java | 15 +- .../test/interceptors/HostnameCheck.java | 3 +- .../test/interceptors/QueryCheck.java | 3 +- .../test/services/TestClientService.java | 71 -- .../test/services/TestHttpClientFactory.java | 76 -- src/{main => test}/resources/data.sql | 0 76 files changed, 2698 insertions(+), 1092 deletions(-) delete mode 100644 Dockerfile create mode 100644 config/application-dev.yml create mode 100644 config/application-prod.yml create mode 100644 config/application.yml delete mode 100644 docker-compose.yml create mode 100644 docker/Dockerfile create mode 100644 docker/development/docker-compose.yml create mode 100644 gradle/libs.versions.toml delete mode 100644 lombok.config rename src/main/java/org/sitmun/proxy/middleware/{ProxyMiddlewareApplication.java => Application.java} (76%) delete mode 100644 src/main/java/org/sitmun/proxy/middleware/decorator/DecoratedRequest.java delete mode 100644 src/main/java/org/sitmun/proxy/middleware/decorator/request/BodyRequestDecorator.java delete mode 100644 src/main/java/org/sitmun/proxy/middleware/decorator/response/CapabiltiesResponseDecorator.java delete mode 100644 src/main/java/org/sitmun/proxy/middleware/decorator/response/PaginationResponseDecorator.java rename src/main/java/org/sitmun/proxy/middleware/dto/{ConfigProxyRequest.java => ConfigProxyRequestDto.java} (86%) delete mode 100644 src/main/java/org/sitmun/proxy/middleware/dto/DatasourcePayloadDto.java rename src/main/java/org/sitmun/proxy/middleware/dto/{ErrorResponseDTO.java => ErrorResponseDto.java} (91%) delete mode 100644 src/main/java/org/sitmun/proxy/middleware/dto/OgcWmsPayloadDto.java rename src/main/java/org/sitmun/proxy/middleware/{service/ClientService.java => protocols/http/HttpClient.java} (64%) create mode 100644 src/main/java/org/sitmun/proxy/middleware/protocols/http/HttpClientFactoryService.java rename src/main/java/org/sitmun/proxy/middleware/{decorator => protocols/http}/HttpContext.java (61%) rename src/main/java/org/sitmun/proxy/middleware/{decorator => protocols/http}/HttpContextSecurity.java (63%) rename src/main/java/org/sitmun/proxy/middleware/{decorator/request/HttpBasicSecurityRequestDecorator.java => protocols/http/HttpRequestDecoratorAddBasicSecurity.java} (55%) rename src/main/java/org/sitmun/proxy/middleware/{decorator/request/HttpUriDecorator.java => protocols/http/HttpRequestDecoratorAddEndpoint.java} (62%) create mode 100644 src/main/java/org/sitmun/proxy/middleware/protocols/http/HttpRequestExecutor.java rename src/main/java/org/sitmun/proxy/middleware/{decorator => protocols/jdbc}/JdbcContext.java (60%) create mode 100644 src/main/java/org/sitmun/proxy/middleware/protocols/jdbc/JdbcPayloadDto.java rename src/main/java/org/sitmun/proxy/middleware/{decorator/request/JdbcConnectionRequestDecorator.java => protocols/jdbc/JdbcRequestDecoratorAddConnection.java} (71%) rename src/main/java/org/sitmun/proxy/middleware/{decorator/request/JdbcQueryRequestDecorator.java => protocols/jdbc/JdbcRequestDecoratorAddQuery.java} (62%) rename src/main/java/org/sitmun/proxy/middleware/{request/JdbcRequest.java => protocols/jdbc/JdbcRequestExecutor.java} (64%) create mode 100644 src/main/java/org/sitmun/proxy/middleware/protocols/wms/WmsCapabilitiesResponseDecorator.java create mode 100644 src/main/java/org/sitmun/proxy/middleware/protocols/wms/WmsPayloadDto.java delete mode 100644 src/main/java/org/sitmun/proxy/middleware/request/HttpRequest.java delete mode 100644 src/main/java/org/sitmun/proxy/middleware/request/RequestFactory.java delete mode 100644 src/main/java/org/sitmun/proxy/middleware/service/HttpClientFactory.java rename src/main/java/org/sitmun/proxy/middleware/service/{ProxyMiddlewareService.java => RequestConfigurationService.java} (57%) create mode 100644 src/main/java/org/sitmun/proxy/middleware/service/RequestExecutor.java create mode 100644 src/main/java/org/sitmun/proxy/middleware/service/RequestExecutorFactory.java rename src/main/java/org/sitmun/proxy/middleware/{decorator/DecoratedResponse.java => service/RequestExecutorResponse.java} (50%) rename src/main/java/org/sitmun/proxy/middleware/{response/Response.java => service/RequestExecutorResponseImpl.java} (52%) rename src/main/java/org/sitmun/proxy/middleware/service/{GlobalRequestService.java => RequestExecutorService.java} (61%) create mode 100644 src/main/resources/META-INF/additional-spring-configuration-metadata.json create mode 100644 src/main/resources/application-dev.yml create mode 100644 src/main/resources/application-prod.yml create mode 100644 src/test/java/org/sitmun/proxy/middleware/protocols/http/ExecutionRequestExecutorServiceTest.java create mode 100644 src/test/java/org/sitmun/proxy/middleware/protocols/http/HttpClientFactoryServiceTest.java create mode 100644 src/test/java/org/sitmun/proxy/middleware/protocols/jdbc/ExecutionRequestExecutorServiceTest.java create mode 100644 src/test/java/org/sitmun/proxy/middleware/protocols/wms/ExecutionRequestExecutorServiceTest.java create mode 100644 src/test/java/org/sitmun/proxy/middleware/service/ExecutionRequestExecutorServiceTest.java delete mode 100644 src/test/java/org/sitmun/proxy/middleware/service/GlobalRequestServiceTest.java delete mode 100644 src/test/java/org/sitmun/proxy/middleware/test/services/TestClientService.java delete mode 100644 src/test/java/org/sitmun/proxy/middleware/test/services/TestHttpClientFactory.java rename src/{main => test}/resources/data.sql (100%) diff --git a/.editorconfig b/.editorconfig index 1f64e46..1261160 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,8 +6,5 @@ end_of_line = lf indent_style = space indent_size = 2 -[*.sh] -insert_final_newline = true - [gradlew] insert_final_newline = true diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index cece144..0000000 --- a/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -# Stage 0, "build-proxy-middleware", based on OpenJDK 11, to build and compile the frontend -FROM openjdk:11 AS build-proxy-middleware -COPY . /usr/src/sitmun-proxy-middleware -WORKDIR /usr/src/sitmun-proxy-middleware -RUN --mount=type=cache,target=/root/.gradle ./gradlew --no-daemon -i clean build -x test - -# Stage 1, based on OpenJDK, to have only the compiled app -FROM openjdk:11-jre-slim-buster -COPY --from=build-proxy-middleware /usr/src/sitmun-proxy-middleware/build/libs/*.jar /usr/src/proxy.jar -WORKDIR /usr/src -ENTRYPOINT ["java", "-jar", "proxy.jar"] diff --git a/README.md b/README.md index c4812e4..ac26fdc 100644 --- a/README.md +++ b/README.md @@ -1,90 +1,1091 @@ [![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=org.sitmun%3Asitmun-proxy-middleware&metric=alert_status)](https://sonarcloud.io/dashboard?id=org.sitmun%3Asitmun-proxy-middleware) -# SITMUN proxy middleware +# SITMUN Proxy Middleware -The **SITMUN Proxy Middleware** is a reverse proxy and middleware thaqt facilitates the access of the **SITMUN Map Viewer** to protected services and databases. +A Spring Boot microservice that acts as a reverse proxy and middleware to facilitate secure access to protected services and databases for the SITMUN Map Viewer. This service is part of the [SITMUN](https://sitmun.github.io/) geospatial platform ecosystem. -These protected services or databases can have various restrictions or requirements, such as: +## Table of Contents -- They are located on an Intranet, and users outside the Intranet cannot access them directly. -- They require access credentials that should not be disclosed to the users. -- User requests must be modified and validated before being forwarded to the protected service. -- The service’s response needs to be modified before returning it to the client application (e.g., masking part of an image with a map). +- [Overview](#overview) +- [Quick Start](#quick-start) + - [Prerequisites](#prerequisites) + - [Local Development](#local-development) + - [Docker Deployment](#docker-deployment) + - [Troubleshooting](#troubleshooting) + - [Building](#building) +- [Features](#features) + - [Core Functionality](#core-functionality) + - [Security Features](#security-features) + - [Development Features](#development-features) +- [API Reference](#api-reference) + - [Endpoints](#endpoints) + - [Usage Examples](#usage-examples) + - [Request Parameters](#request-parameters) +- [Configuration](#configuration) + - [Environment Variables](#environment-variables) + - [Profiles](#profiles) + - [Configuration Files](#configuration-files) +- [Architecture](#architecture) + - [Technology Stack](#technology-stack) + - [System Architecture](#system-architecture) + - [Key Components](#key-components) + - [Request Processing Flow](#request-processing-flow) + - [Decorator Pattern](#decorator-pattern) + - [Extensibility](#extensibility) + - [Error Handling](#error-handling) +- [Development](#development) + - [Profiles Explained](#profiles-explained) + - [Deployment](#deployment) + - [Project Structure](#project-structure) + - [Build System](#build-system) + - [Code Quality](#code-quality) + - [Running Quality Checks](#running-quality-checks) + - [Version Management](#version-management) + - [Testing](#testing) + - [Development Workflow](#development-workflow) +- [Advanced Features](#advanced-features) + - [Security and Authentication](#security-and-authentication) + - [Monitoring and Observability](#monitoring-and-observability) +- [Contributing](#contributing) + - [Development Guidelines](#development-guidelines) +- [Integration with SITMUN](#integration-with-sitmun) + - [Prerequisites](#prerequisites-1) + - [Configuration Steps](#configuration-steps) + - [Service Types and Configuration](#service-types-and-configuration) + - [Security Configuration](#security-configuration) + - [Monitoring and Health Checks](#monitoring-and-health-checks) + - [Troubleshooting Integration](#troubleshooting-integration) + - [SITMUN Application Stack](#sitmun-application-stack) +- [Support](#support) +- [License](#license) -## Prerequisites +## Overview -Before you begin, ensure you have met the following requirements: +The SITMUN Proxy Middleware provides secure proxy functionality to: -- You have a `Windows/Linux/Mac` machine. -- You have installed the latest version of [Docker CE](https://docs.docker.com/engine/install/) and [Docker Compose](https://docs.docker.com/compose/install/), or [Docker Desktop](https://www.docker.com/products/docker-desktop/). - Docker CE is fully open-source, while Docker Desktop is a commercial product. -- You have installed [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) on your machine. -- You have a basic understanding of Docker, Docker Compose, and Git. -- You have internet access on your machine to pull Docker images and Git repositories. -- You have a running instance of `[sitmun-backend-core](https://github.com/sitmun/sitmun-backend-core)` to make requests (e.g. `http://localhost:9001/api/config/proxy`). -- You know the key to access such instance `SITMUN_BACKEND_CONFIG_SECRET` (e.g. `abcd`) +- **Access Protected Services**: Bridge connections to services located on intranets or requiring special access +- **Credential Management**: Handle authentication without exposing credentials to end users +- **Request Modification**: Transform and validate requests before forwarding to protected services +- **Response Processing**: Modify service responses before returning to client applications +- **Security Layer**: Provide an additional security layer for sensitive geospatial services -## Installing SITMUN Proxy Middleware +This service integrates with the [SITMUN Backend Core](https://github.com/sitmun/sitmun-backend-core) to provide secure proxy capabilities for the SITMUN platform. -To install the SITMUN Proxy Middleware, follow these steps: +## Quick Start -1. Clone the repository: - ```bash - git clone https://github.com/sitmun/sitmun-proxy-middleware.git - ``` +### Prerequisites -2. Change to the directory of the repository: - ```bash - cd sitmun-proxy-middleware - ``` +- Java 17 or later +- Docker CE or Docker Desktop +- Git +- Running instance of [sitmun-backend-core](https://github.com/sitmun/sitmun-backend-core) + +### Local Development + +1. **Clone the repository** + ```bash + git clone https://github.com/sitmun/sitmun-proxy-middleware.git + cd sitmun-proxy-middleware + ``` + +2. **Build the application** + ```bash + ./gradlew build -x test + ``` + +3. **Run the application** + ```bash + # Run with Java directly (recommended) + java -jar build/libs/sitmun-proxy-middleware.jar --spring.profiles.active=prod + + # Or use Gradle bootRun directly + ./gradlew bootRun --args='--spring.profiles.active=prod' + ``` + +4. **Verify the service is running** + ```bash + # Check health status + curl http://localhost:8080/actuator/health -3. Create a new file named `.env` inside the directory. - Open the `.env` file in a text editor and add in the following format: - ```properties - SITMUN_BACKEND_CONFIG_URL=the_location_of_the_sitmun_backend_configuration_endpoint - SITMUN_BACKEND_CONFIG_SECRET=the_shared_secret - ``` -4. Start the SITMUN Middleware proxy: - ```bash - docker compose up - ``` - This command will build and start all the services defined in the `docker-compose.yml` file. - -5. Access the SITMUN Middleware Proxy at [http://localhost:9002/actuator/health](http://localhost:9002/actuator/health) and expect: - ```json - {"status":"UP"} + # Test the proxy endpoint (will return 400 for invalid request, but confirms service is running) + curl -X GET http://localhost:8080/proxy/1/1/test/1 + ``` + +### Docker Deployment + +1. **Create environment configuration** + ```bash + # Create .env file + cat > .env << EOF + SITMUN_BACKEND_CONFIG_URL=http://localhost:9001/api/config/proxy + SITMUN_BACKEND_CONFIG_SECRET=your-secret-key + EOF + ``` + +2. **Start with Docker Compose** + ```bash + cd docker/development + docker-compose up + ``` + +3. **Verify deployment** + ```bash + curl http://localhost:8080/actuator/health ``` -See [SITMUN Application Stack](https://github.com/sitmun/sitmun-application-stack) as an example of how to deploy and run the proxy as parte of the SITMUN stack. +### Troubleshooting + +#### Port Already in Use +```bash +# Use different port +./gradlew bootRun --args='--spring.profiles.active=prod --server.port=8081' +``` + +#### Memory Issues +```bash +# Increase heap size +./gradlew bootRun --args='--spring.profiles.active=prod -Xmx4g -Xms2g' +``` + +#### Docker Issues +```bash +# Clean up Docker resources +cd docker/development +docker-compose down -v +docker system prune -f +``` + +### Building + +```bash +# Build the project (includes Git hooks setup) +./gradlew build + +# Build without tests (faster for development) +./gradlew build -x test + +# Run tests +./gradlew test + +# Create JAR file +./gradlew jar + +# Format code +./gradlew spotlessApply + +# Check code coverage +./gradlew jacocoTestReport +``` + +> **πŸ’‘ Tip**: For development, use `./gradlew build -x test` for faster builds, then run the JAR directly with `java -jar build/libs/sitmun-proxy-middleware.jar --spring.profiles.active=dev` + +## Features + +### Core Functionality + +- **Reverse Proxy**: Route requests to protected services with authentication +- **Request Decorators**: Modify requests using configurable decorator patterns +- **Response Decorators**: Transform responses before returning to clients +- **Authentication Handling**: Manage credentials and tokens securely +- **Multi-Service Support**: Handle different types of services (HTTP, JDBC, etc.) +- **Dynamic Configuration**: Load proxy configuration from SITMUN backend +- **Request Validation**: Validate and sanitize incoming requests +- **Error Handling**: Comprehensive error handling with proper HTTP status codes + +### Security Features + +- **Credential Protection**: Never expose backend credentials to clients +- **Token Management**: Handle JWT and other authentication tokens +- **Request Sanitization**: Clean and validate all incoming requests +- **Access Control**: Enforce service-level access permissions +- **Audit Logging**: Log all proxy requests for security monitoring + +### Development Features + +- **Spring Boot DevTools**: Auto-restart and live reload with intelligent exclusions +- **Profile-based Configuration**: Separate dev and prod configurations +- **Debug Logging**: Detailed logging for development (dev profile only) +- **Automated Quality Checks**: Git hooks for pre-commit validation +- **Conventional Commits**: Enforced commit message format +- **Version Management**: Automated versioning with Axion Release +- **Code Formatting**: Automated code formatting with Spotless +- **Coverage Reporting**: JaCoCo integration for code coverage +- **Comprehensive Testing**: Unit and integration tests with comprehensive coverage + +## API Reference + +### Endpoints + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/proxy/{appId}/{terId}/{type}/{typeId}` | GET | Proxy request to protected service | +| `/actuator/health` | GET | Application health status | + +### Usage Examples + +#### Proxy Request to Protected Service +```bash +curl -X GET "http://localhost:8080/proxy/1/1/wms/123" \ + -H "Authorization: Bearer your-jwt-token" \ + -H "Content-Type: application/json" +``` + +#### Health Check +```bash +curl http://localhost:8080/actuator/health +``` + +Response: +```json +{ + "status": "UP" +} +``` + +### Request Parameters + +- `appId`: Application identifier (Integer) +- `terId`: Territory identifier (Integer) +- `type`: Service type (wms, sql) (String) +- `typeId`: Service instance identifier (Integer) +- `Authorization`: Bearer token (optional, automatically extracts token from "Bearer " prefix) +- Query parameters: Passed through to target service (Map) ## Configuration -The following environment variables are required: +### Environment Variables -- `SITMUN_BACKEND_CONFIG_URL`: The URL to the backend service that provides the configuration for the proxy. -- `SITMUN_BACKEND_CONFIG_SECRET`: The secret key to access the configuration service. It must be the same as the one used in the backend service. +| Variable | Description | Required | Default | +|----------|-------------|----------|---------| +| `SITMUN_BACKEND_CONFIG_URL` | URL to backend configuration service | Yes | - | +| `SITMUN_BACKEND_CONFIG_SECRET` | Secret key for configuration access | Yes | - | +| `SERVER_PORT` | Application port | No | 8080 | +| `SPRING_PROFILES_ACTIVE` | Spring profile to use | No | prod | -Additional information is available at . +### Profiles -## Uninstalling SITMUN Proxy middleware +#### Development Profile (`dev`) +- Debug logging enabled +- H2 console available +- Detailed error messages +- Development tools enabled -To stop and remove all services, volumes, and networks defined in the `docker-compose.yml` file, use: +#### Production Profile (`prod`) +- Minimal logging +- Security optimizations +- Performance tuning +- Production-ready configuration + +### Configuration Files + +#### Base Configuration (`src/main/resources/application.yml`) +```yaml +# Logging Configuration +logging: + level: + ROOT: INFO + org.sitmun.proxy.middleware: INFO + +# Sitmun Proxy Configuration +sitmun: + backend: + config: + url: http://some.url + secret: some-secret + +# Actuator Configuration +management: + endpoints: + web: + exposure: + include: health + base-path: /actuator + endpoint: + health: + show-details: never + show-components: never + health: + defaults: + enabled: true +``` + +#### Development Profile (`src/main/resources/application-dev.yml`) +```yaml +# Development-specific configuration +logging: + level: + org.sitmun.proxy.middleware: DEBUG + org.springframework.web: DEBUG + +# Development tools +spring: + devtools: + restart: + enabled: true + livereload: + enabled: true +``` + +#### Production Profile (`src/main/resources/application-prod.yml`) +```yaml +# Production-specific configuration +logging: + level: + org.sitmun.proxy.middleware: INFO + ROOT: WARN + +# Production optimizations +spring: + devtools: + restart: + enabled: false + livereload: + enabled: false +``` + +#### External Configuration (`config/application.yml`) +The application supports external configuration files mounted in Docker containers: + +```yaml +# External Configuration for SITMUN Proxy Middleware +# This file is mounted from the host system into the container + +# Logging Configuration +logging: + level: + ROOT: INFO + org.sitmun.proxy.middleware: INFO + file: + name: /app/logs/sitmun-proxy-middleware.log + max-size: 100MB + max-history: 30 + +# SITMUN Backend Configuration +sitmun: + backend: + config: + url: http://sitmun-backend:8080 + secret: ${SITMUN_BACKEND_CONFIG_SECRET:your-secret-key-here} + +# Server Configuration +server: + port: 8080 + servlet: + context-path: / + compression: + enabled: true + mime-types: text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json + min-response-size: 1024 + +# HTTP Client Configuration +http: + client: + connect-timeout: 5000 + read-timeout: 10000 + max-connections: 200 + max-connections-per-route: 50 +``` + +#### External Production Configuration (`config/application-prod.yml`) +```yaml +# External Production Configuration +logging: + level: + ROOT: WARN + org.sitmun.proxy.middleware: INFO + file: + name: /app/logs/sitmun-proxy-middleware.log + max-size: 100MB + max-history: 30 + +# Production server configuration +server: + port: 8080 +``` + +## Architecture + +### Technology Stack + +- **Spring Boot 3.5.4**: Application framework with Spring Web, JDBC, and Actuator +- **Spring Web**: REST API support +- **Spring JDBC**: Database connectivity +- **Spring Actuator**: Health checks and monitoring +- **OkHttp 4.12.0**: HTTP client for service communication +- **JJWT 0.12.6**: JWT token handling with API, implementation, and Jackson modules +- **PostgreSQL/Oracle**: Database drivers for JDBC connections +- **H2 2.2.224**: In-memory database for testing +- **JSON 20240303**: JSON processing library +- **Gradle**: Build system with Version Catalogs +- **Docker**: Multi-stage containerization with Amazon Corretto +- **Spotless 7.2.0**: Code formatting with Google Java Format +- **JaCoCo**: Code coverage reporting +- **Axion Release 1.19.0**: Version management with semantic versioning +- **Lombok 8.6**: Reduces boilerplate code + +### System Architecture + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ SITMUN Map │───▢│ Proxy Middleware │───▢│ Protected β”‚ +β”‚ Viewer β”‚ β”‚ β”‚ β”‚ Services β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ SITMUN Backend β”‚ + β”‚ Core β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +**Request Flow:** +1. Client sends request to `/proxy/{appId}/{terId}/{type}/{typeId}` +2. Proxy loads configuration from SITMUN Backend Core +3. Request is decorated and forwarded to target service +4. Response is decorated and returned to client + +### Key Components + +- **`Application.java`**: Main Spring Boot application class +- **`ProxyMiddlewareController`**: Main REST controller handling proxy requests +- **`RequestConfigurationService`**: Orchestrates request processing flow and configuration loading from SITMUN Backend Core +- **`RequestExecutorService`**: Handles request execution logic and protocol routing +- **`RequestExecutorFactory`**: Factory for creating request execution instances based on service type +- **Protocol Implementations**: + - **HTTP**: `HttpRequestExecutor`, `HttpClientFactoryService`, `HttpRequestDecoratorAddBasicSecurity`, `HttpRequestDecoratorAddEndpoint` + - **JDBC**: `JdbcRequestExecutor`, `JdbcRequestDecoratorAddConnection`, `JdbcRequestDecoratorAddQuery` + - **WMS**: `WmsCapabilitiesResponseDecorator` for WMS capabilities processing +- **Decorator Pattern**: Flexible request/response modification through `RequestDecorator` and `ResponseDecorator` interfaces +- **DTO Classes**: Data transfer objects including `ConfigProxyDto`, `ConfigProxyRequestDto`, `ErrorResponseDto`, `HttpSecurityDto`, `PayloadDto` +- **Context Classes**: Protocol-specific contexts (`HttpContext`, `JdbcContext`) for request processing +- **Test Structure**: Comprehensive testing with protocol-specific test classes (`ExecutionRequestExecutorServiceTest` for each protocol) and utilities + +### Request Processing Flow + +1. **Request Reception**: `ProxyMiddlewareController` receives proxy request +2. **Token Extraction**: Extract JWT token from Authorization header +3. **Configuration Loading**: Load service configuration from SITMUN Backend Core using `RequestConfigurationService` +4. **Request Decoration**: Apply decorators to modify request based on service type +5. **Service Execution**: Forward request to target service using `RequestExecutionService` +6. **Response Decoration**: Apply decorators to modify response +7. **Response Return**: Return modified response to client + +### Decorator Pattern + +The service uses the decorator pattern for flexible request/response modification: + +```java +// Request decorators +HttpRequestDecoratorAddBasicSecurity // Adds basic authentication to HTTP requests +HttpRequestDecoratorAddEndpoint // Adds endpoint configuration to HTTP requests +JdbcRequestDecoratorAddConnection // Adds database connection to JDBC requests +JdbcRequestDecoratorAddQuery // Adds query configuration to JDBC requests + +// Response decorators +WmsCapabilitiesResponseDecorator // Modifies WMS capabilities responses +``` + +**Core Interfaces:** +- `RequestDecorator`: Interface for request decorators +- `ResponseDecorator`: Interface for response decorators +- `Decorator`: Base decorator interface +- `Context`: Context interface for decorator operations + +### Extensibility + +- **Custom Decorators**: Implement new request/response decorators +- **Service Types**: Add support for new service types +- **Authentication**: Extend authentication mechanisms +- **Configuration**: Customize configuration loading + +### Error Handling + +- **HTTP Status Codes**: Proper status code mapping +- **Error Response Format**: Consistent error response structure +- **Logging**: Comprehensive error logging + +## Development + +### Profiles Explained + +#### Development Profile +- Enhanced logging for debugging +- Development tools enabled +- H2 console for database management +- Detailed error messages + +#### Production Profile +- Optimized for performance +- Minimal logging +- Security hardening +- Production monitoring + +### Deployment + +#### Docker Deployment ```bash -docker compose down -v +# Build and run with Docker Compose +cd docker/development +docker-compose up --build + +# Run in background +docker-compose up -d + +# View logs +docker-compose logs -f + +# Stop services +docker-compose down ``` -## Contributing to SITMUN Application Stack +**Docker Configuration:** +- **Multi-stage build** using Amazon Corretto 17 (`docker/Dockerfile`) +- **Development environment** with Docker Compose (`docker/development/docker-compose.yml`) +- **Health checks** with curl-based monitoring +- **External configuration** mounting from host +- **JVM optimization** with G1GC and container support +- **Volume mounting** for logs and configuration + +#### Manual Deployment +```bash +# Build JAR +./gradlew build + +# Run with production profile +java -jar build/libs/sitmun-proxy-middleware.jar --spring.profiles.active=prod +``` + +### Project Structure + +``` +sitmun-proxy-middleware/ +β”œβ”€β”€ src/ +β”‚ β”œβ”€β”€ main/ +β”‚ β”‚ β”œβ”€β”€ java/org/sitmun/proxy/middleware/ +β”‚ β”‚ β”‚ β”œβ”€β”€ Application.java # Main application class +β”‚ β”‚ β”‚ β”œβ”€β”€ config/ # Configuration classes +β”‚ β”‚ β”‚ β”œβ”€β”€ controllers/ # REST controllers +β”‚ β”‚ β”‚ β”œβ”€β”€ decorator/ # Request/response decorators +β”‚ β”‚ β”‚ β”œβ”€β”€ dto/ # Data transfer objects +β”‚ β”‚ β”‚ β”œβ”€β”€ protocols/ # Protocol implementations +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ http/ # HTTP protocol support +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ jdbc/ # JDBC protocol support +β”‚ β”‚ β”‚ β”‚ └── wms/ # WMS protocol support (uses HTTP) +β”‚ β”‚ β”‚ β”œβ”€β”€ service/ # Business logic services +β”‚ β”‚ β”‚ └── utils/ # Utility classes +β”‚ β”‚ └── resources/ +β”‚ β”‚ β”œβ”€β”€ application.yml # Base configuration +β”‚ β”‚ β”œβ”€β”€ application-dev.yml # Development profile +β”‚ β”‚ β”œβ”€β”€ application-prod.yml # Production profile +β”‚ β”‚ └── META-INF/ # Spring configuration metadata +β”‚ β”‚ └── additional-spring-configuration-metadata.json +β”‚ └── test/ +β”‚ β”œβ”€β”€ java/org/sitmun/proxy/middleware/ +β”‚ β”‚ β”œβ”€β”€ protocols/ # Protocol-specific tests +β”‚ β”‚ β”‚ β”œβ”€β”€ http/ # HTTP protocol tests +β”‚ β”‚ β”‚ β”œβ”€β”€ jdbc/ # JDBC protocol tests +β”‚ β”‚ β”‚ └── wms/ # WMS protocol tests +β”‚ β”‚ β”œβ”€β”€ service/ # Service layer tests +β”‚ β”‚ β”œβ”€β”€ decorator/ # Decorator tests (empty) +β”‚ β”‚ └── test/ # Test utilities and fixtures +β”‚ β”‚ β”œβ”€β”€ dto/ # Test DTOs +β”‚ β”‚ β”œβ”€β”€ fixtures/ # Test data fixtures +β”‚ β”‚ β”œβ”€β”€ interceptors/ # Test interceptors +β”‚ β”‚ β”œβ”€β”€ service/ # Test service implementations (empty) +β”‚ β”‚ β”œβ”€β”€ TestUtils.java # Test utilities +β”‚ β”‚ └── URIConstants.java # URI constants for tests +β”‚ └── resources/ +β”‚ └── application.yml # Test configuration +β”œβ”€β”€ config/ # External configuration +β”‚ β”œβ”€β”€ application.yml # External base config +β”‚ └── application-prod.yml # External production config +β”œβ”€β”€ docker/ # Docker configuration +β”‚ β”œβ”€β”€ Dockerfile # Multi-stage build with Amazon Corretto +β”‚ └── development/ +β”‚ └── docker-compose.yml # Development environment +β”œβ”€β”€ gradle/ # Gradle configuration +β”‚ β”œβ”€β”€ libs.versions.toml # Version catalog for dependencies +β”‚ └── wrapper/ # Gradle wrapper files +β”œβ”€β”€ build.gradle # Main build configuration +β”œβ”€β”€ settings.gradle # Project settings +β”œβ”€β”€ gradle.properties # Gradle properties +β”œβ”€β”€ gradlew # Gradle wrapper script (Unix) +β”œβ”€β”€ gradlew.bat # Gradle wrapper script (Windows) +``` + +### Key Components + +- **`Application.java`**: Main Spring Boot application class +- **`ProxyMiddlewareController`**: Main REST controller handling proxy requests +- **`RequestConfigurationService`**: Orchestrates request processing flow and configuration loading from SITMUN Backend Core +- **`RequestExecutorService`**: Handles request execution logic and protocol routing +- **`RequestExecutorFactory`**: Factory for creating request execution instances based on service type +- **Protocol Implementations**: + - **HTTP**: `HttpRequestExecutor`, `HttpClientFactoryService`, `HttpRequestDecoratorAddBasicSecurity`, `HttpRequestDecoratorAddEndpoint` + - **JDBC**: `JdbcRequestExecutor`, `JdbcRequestDecoratorAddConnection`, `JdbcRequestDecoratorAddQuery` + - **WMS**: `WmsCapabilitiesResponseDecorator` for WMS capabilities processing +- **Decorator Pattern**: Flexible request/response modification through `RequestDecorator` and `ResponseDecorator` interfaces +- **DTO Classes**: Data transfer objects including `ConfigProxyDto`, `ConfigProxyRequestDto`, `ErrorResponseDto`, `HttpSecurityDto`, `PayloadDto` +- **Context Classes**: Protocol-specific contexts (`HttpContext`, `JdbcContext`) for request processing +- **Test Structure**: Comprehensive testing with protocol-specific test classes (`ExecutionRequestExecutorServiceTest` for each protocol) and utilities + +### Build System + +The project uses Gradle with Version Catalogs for dependency management: + +- **Version Catalog**: `gradle/libs.versions.toml` - Centralized dependency versions +- **Plugins**: Spring Boot 3.5.4, Lombok 8.6, Spotless 7.2.0, Axion Release 1.19.0 +- **Quality Tools**: JaCoCo for coverage, Spotless for formatting +- **Dependencies**: + - Spring Boot Starters (Web, JDBC, Actuator) + - OkHttp 4.12.0 for HTTP client + - JJWT 0.12.6 for JWT handling + - PostgreSQL and Oracle JDBC drivers + - H2 2.2.224 for testing + - JSON 20240303 for JSON processing + +### Code Quality + +The project includes several code quality tools: + +- **Spotless**: Code formatting with Google Java Format +- **JaCoCo**: Code coverage reporting +- **Axion Release**: Version management with semantic versioning +- **Git Hooks**: Automated quality checks and commit validation + +### Running Quality Checks + +```bash +# Format code +./gradlew spotlessApply + +# Check formatting without applying +./gradlew spotlessCheck + +# Check code coverage +./gradlew jacocoTestReport + +# View coverage report +open build/reports/jacoco/test/html/index.html +``` + +### Version Management + +The project uses Axion Release for automated version management: + +```bash +# Check current version +./gradlew currentVersion + +# Create a new release +./gradlew release + +# Create a new patch version +./gradlew patch +``` + +#### Creating a Release + +**Prerequisites:** +1. **Clean Git State**: Ensure all changes are committed +2. **Working Directory**: No uncommitted changes +3. **Git Repository**: Must be a valid Git repository + +**Step-by-Step Release Process:** +```bash +# 1. Check current Git status +git status + +# 2. Add and commit any pending changes +git add . +git commit -m "docs: update documentation for release" + +# 3. Verify the repository is clean +git status + +# 4. Check current version +./gradlew currentVersion + +# 5. Create a new release +./gradlew release + +# 6. Push the release tag +git push --tags +``` + +**Release Types:** +- `./gradlew release`: Creates a new patch version (e.g., 1.0.0 β†’ 1.0.1) +- `./gradlew release -Prelease.scope=minor`: Creates a new minor version (e.g., 1.0.0 β†’ 1.1.0) +- `./gradlew release -Prelease.scope=major`: Creates a new major version (e.g., 1.0.0 β†’ 2.0.0) + +### Testing + +The project includes comprehensive testing: + +```bash +# Run all tests +./gradlew test + +# Run specific test class +./gradlew test --tests ProxyMiddlewareControllerTest + +# Run protocol-specific tests +./gradlew test --tests *HttpExecutionRequestExecutorServiceTest +./gradlew test --tests *JdbcExecutionRequestExecutorServiceTest +./gradlew test --tests *WmsExecutionRequestExecutorServiceTest + +# Run service tests +./gradlew test --tests *ServiceExecutionRequestExecutorServiceTest + +# Run tests with coverage +./gradlew test jacocoTestReport +``` + +#### Test Coverage + +- **Protocol Tests**: Each protocol (HTTP, JDBC, WMS) has dedicated test classes + - `ExecutionRequestExecutorServiceTest` for each protocol + - `HttpClientFactoryServiceTest` for HTTP client factory +- **Service Tests**: `ExecutionRequestExecutorServiceTest` for service layer testing +- **Test Utilities**: + - `TestUtils` for common test functionality + - `URIConstants` for test URI constants + - `AuthorizationProxyFixtures` for test data fixtures + - Test interceptors for request/response simulation +- **Test DTOs**: `AuthenticationResponse` and `UserPasswordAuthenticationRequest` for testing +- **Edge Cases**: Boundary conditions and error handling through comprehensive test scenarios + +### Development Workflow + +#### Git Hooks + +The project includes automated Git hooks that run on every commit: + +**Pre-commit checks:** +- Code formatting validation (Spotless) +- Unit and integration tests +- Code coverage verification + +**Commit message validation:** +- Conventional commit format enforcement +- SITMUN-specific scope support `(proxy)` + +#### Commit Message Format + +Follow the conventional commit format: +``` +(): + +[optional body] + +[optional footer] +``` + +**Types:** +- `feat`: New feature +- `fix`: Bug fix +- `docs`: Documentation changes +- `style`: Code style changes +- `refactor`: Code refactoring +- `test`: Test changes +- `chore`: Maintenance tasks +- `perf`: Performance improvements +- `ci`: CI/CD changes +- `build`: Build system changes + +**Examples:** +```bash +git commit -m "feat(proxy): add request decorator functionality" +git commit -m "fix(proxy): resolve authentication token handling" +git commit -m "docs: update README with proxy configuration info" +git commit -m "test: add integration tests for proxy requests" +git commit -m "style: format code with Google Java Format" +``` + +#### Managing Git Hooks + +```bash +# Install Git hooks (automatic with build) +./gradlew setupGitHooks + +# Remove Git hooks +./gradlew removeGitHooks +``` + +## Advanced Features + +### Security and Authentication + +The service includes comprehensive security features: + +- **JWT Token Handling**: Secure token validation and processing +- **Credential Protection**: Never expose backend credentials +- **Request Sanitization**: Clean and validate all incoming requests +- **Access Control**: Enforce service-level permissions +- **Audit Logging**: Comprehensive request logging + +### Monitoring and Observability + +- **Spring Boot Actuator**: Health checks, metrics, and application monitoring +- **Custom Health Indicators**: Proxy service health monitoring +- **Request Tracking**: Real-time request monitoring +- **Error Handling**: Comprehensive error handling and logging +- **Performance Metrics**: Request timing and performance monitoring + +#### Actuator Endpoints + +| Endpoint | Description | Access | +|----------|-------------|--------| +| `/actuator/health` | Application health status | Public | + +**Health Check Response:** +```json +{ + "status": "UP" +} +``` + +## Contributing + +1. Fork the repository +2. Create a feature branch +3. Make your changes following the conventional commit format +4. Add tests for new functionality +5. Ensure all tests pass and code is formatted +6. Submit a pull request + +### Development Guidelines + +- Follow the conventional commit format +- Write tests for new functionality +- Ensure code coverage remains high +- Run quality checks before committing +- Update documentation as needed + +## Integration with SITMUN + +This service is designed to provide secure proxy capabilities for the [SITMUN](https://github.com/sitmun/) platform. It can be deployed as a microservice alongside other SITMUN components. + +### Prerequisites + +Before integrating the Proxy Middleware with SITMUN, ensure you have: + +- **SITMUN Backend Core** running and accessible +- **SITMUN Map Viewer** configured to use the proxy +- **Network connectivity** between all SITMUN components +- **Shared secret key** for secure communication + +### Configuration Steps + +#### 1. SITMUN Backend Core Configuration + +The Proxy Middleware requires configuration from the SITMUN Backend Core. Ensure your backend is configured to provide proxy configuration: + +```yaml +# SITMUN Backend Core configuration +sitmun: + backend: + proxy: + enabled: true + secret: ${SITMUN_PROXY_SECRET:your-shared-secret} + endpoints: + - /api/config/proxy +``` + +#### 2. Proxy Middleware Configuration + +Configure the Proxy Middleware to connect to your SITMUN Backend Core: + +```bash +# Environment variables for Docker deployment +SITMUN_BACKEND_CONFIG_URL=http://sitmun-backend:8080/api/config/proxy +SITMUN_BACKEND_CONFIG_SECRET=your-shared-secret +``` + +Or in `application.yml`: + +```yaml +sitmun: + backend: + config: + url: http://sitmun-backend:8080/api/config/proxy + secret: your-shared-secret +``` + +#### 3. SITMUN Map Viewer Configuration + +Configure the SITMUN Map Viewer to use the Proxy Middleware for protected services: + +```javascript +// Map Viewer configuration +const mapViewerConfig = { + proxy: { + enabled: true, + baseUrl: 'http://localhost:8080/proxy', + authentication: { + type: 'bearer', + token: 'your-jwt-token' + } + }, + services: { + wms: { + useProxy: true, + proxyPath: '/{appId}/{terId}/wms/{serviceId}' + }, + jdbc: { + useProxy: true, + proxyPath: '/{appId}/{terId}/jdbc/{serviceId}' + } + } +}; +``` + +#### 4. Network Configuration + +Ensure proper network connectivity between components: + +```yaml +# Docker Compose network configuration +services: + sitmun-backend: + # ... backend configuration + networks: + - sitmun-network + + sitmun-proxy-middleware: + # ... proxy configuration + networks: + - sitmun-network + environment: + - SITMUN_BACKEND_CONFIG_URL=http://sitmun-backend:8080/api/config/proxy + - SITMUN_BACKEND_CONFIG_SECRET=your-shared-secret + +networks: + sitmun-network: + driver: bridge +``` + +### Service Types and Configuration + +The Proxy Middleware supports different service types that can be configured in the SITMUN Backend Core: + +#### WMS Services +```json +{ + "type": "wms", + "url": "http://protected-wms-service/wms", + "layers": ["layer1", "layer2"], + "authentication": { + "type": "basic", + "username": "protected_user", + "password": "protected_pass" + } +} +``` + + + +#### JDBC Services +```json +{ + "type": "jdbc", + "url": "jdbc:postgresql://protected-db:5432/database", + "username": "db_user", + "password": "db_password", + "query": "SELECT * FROM spatial_data WHERE territory_id = ?" +} +``` + +### Security Configuration + +The service includes basic security features for proxy authentication and request handling. + +### Monitoring and Health Checks + +#### Health Check Configuration +```yaml +# Health check configuration for SITMUN integration +management: + endpoints: + web: + exposure: + include: health,info,metrics + endpoint: + health: + show-details: when-authorized + show-components: always + health: + defaults: + enabled: true + indicators: + sitmun-backend: + enabled: true +``` + +#### Logging Configuration +```yaml +# Logging configuration for SITMUN integration +logging: + level: + org.sitmun.proxy.middleware: INFO + org.sitmun: DEBUG + pattern: + console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" + file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" +``` + +### Troubleshooting Integration + +#### Common Issues + +1. **Connection Refused to SITMUN Backend** + ```bash + # Check if backend is running + curl http://sitmun-backend:8080/actuator/health + + # Verify network connectivity + docker exec sitmun-proxy-middleware ping sitmun-backend + ``` + +2. **Authentication Failures** + ```bash + # Check shared secret configuration + echo $SITMUN_BACKEND_CONFIG_SECRET + + # Verify JWT token format + curl -H "Authorization: Bearer your-token" http://localhost:8080/proxy/1/1/test/1 + ``` + +3. **Service Configuration Not Found** + ```bash + # Check backend configuration endpoint + curl http://sitmun-backend:8080/api/config/proxy + + # Verify service configuration in backend + curl -H "Authorization: Bearer your-token" http://sitmun-backend:8080/api/services + ``` + +#### Debug Mode +```bash +# Enable debug logging for integration issues +export LOGGING_LEVEL_ORG_SITMUN_PROXY_MIDDLEWARE=DEBUG +export LOGGING_LEVEL_ORG_SITMUN=DEBUG + +# Restart the proxy middleware +docker-compose restart sitmun-proxy-middleware +``` + +### SITMUN Application Stack + +See [SITMUN Application Stack](https://github.com/sitmun/sitmun-application-stack) as an example of how to deploy and run the proxy as part of the SITMUN stack. -To contribute to SITMUN Application Stack, follow these steps: +## Support -1. **Fork this repository** on GitHub. -2. **Clone your forked repository** to your local machine. -3. **Create a new branch** for your changes. -4. **Make your changes** and commit them. -5. **Push your changes** to your forked repository. -6. **Create the pull request** from your branch on GitHub. +For questions and support: -Alternatively, see the GitHub documentation on [creating a pull request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request). +- Open an issue on GitHub +- Check the [SITMUN documentation](https://sitmun.github.io/) +- Join the SITMUN community discussions ## License diff --git a/build.gradle b/build.gradle index 3d757ad..9d5ac0c 100644 --- a/build.gradle +++ b/build.gradle @@ -1,71 +1,203 @@ plugins { id 'java' - id 'org.springframework.boot' version '2.7.18' - id 'io.spring.dependency-management' version '1.1.5' - id 'io.freefair.lombok' version '8.6' id 'jacoco' - id 'org.sonarqube' version '3.2.0' + alias(libs.plugins.spring.boot) + alias(libs.plugins.spring.dependency.management) + alias(libs.plugins.lombok) + alias(libs.plugins.spotless) + alias(libs.plugins.axion.release) } group = 'org.sitmun' -sourceCompatibility = JavaVersion.VERSION_11 - +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} repositories { mavenCentral() } dependencies { - implementation 'org.springframework.boot:spring-boot-starter-data-rest' - implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.boot:spring-boot-starter-jdbc' - implementation 'org.springframework.boot:spring-boot-starter-actuator' - implementation "org.json:json:${json_version}" - implementation "io.jsonwebtoken:jjwt-api:${jjwt_version}" - implementation "io.jsonwebtoken:jjwt-impl:${jjwt_version}" - implementation "io.jsonwebtoken:jjwt-jackson:${jjwt_version}" - implementation "com.squareup.okhttp3:okhttp:${okhttp3_version}" - - implementation 'org.postgresql:postgresql' - implementation 'com.oracle.database.jdbc:ojdbc11-production' - - //Jdbc drivers - testImplementation 'com.h2database:h2' - - testImplementation('org.springframework.boot:spring-boot-starter-test') { + // Spring Boot Starters + implementation libs.spring.boot.starter.web + implementation libs.spring.boot.starter.jdbc + implementation libs.spring.boot.starter.actuator + + // JSON and JWT + implementation libs.json + implementation libs.jjwt.api + implementation libs.jjwt.impl + implementation libs.jjwt.jackson + + // HTTP Client + implementation libs.okhttp3 + + // Database Drivers + implementation libs.postgresql + implementation libs.oracle.jdbc + + // Development tools (excluded from production builds) + developmentOnly libs.spring.boot.devtools + + // Test Dependencies + testImplementation(libs.spring.boot.starter.test) { exclude group: 'com.vaadin.external.google', module: 'android-json' } + + testImplementation libs.h2 } -tasks.named('test') { +tasks.named('test', Test) { useJUnitPlatform() } -test { - ignoreFailures = true +// Spotless configuration +spotless { + java { + googleJavaFormat() + } } +// JaCoCo configuration jacocoTestReport { reports { - xml.enabled true + xml.required = true + } +} + +// Axion Release Plugin Configuration +scmVersion { + ignoreUncommittedChanges.set(true) + tag { + prefix = 'sitmun-proxy-middleware' + versionSeparator = '/' + initialVersion({config, position -> '1.0.0'}) } + versionIncrementer('incrementPatch') } -test.finalizedBy jacocoTestReport +project.version = scmVersion.version + +// Git hooks setup +tasks.register('setupGitHooks') { + group = 'git hooks' + description = 'Install Git hooks for commit message validation and pre-commit checks' + def hookDir = new File(project.rootDir, '.git/hooks') + def commitMsgHook = new File(hookDir, 'commit-msg') + def preCommitHook = new File(hookDir, 'pre-commit') + + doLast { + hookDir.mkdirs() + + // Pre-commit hook + preCommitHook.text = '''#!/bin/sh + echo "πŸ” Running pre-commit checks..." + + # Code formatting check + echo " πŸ“ Checking code formatting..." + ./gradlew spotlessCheck + if [ $? -ne 0 ]; then + echo "❌ Code formatting check failed. Run './gradlew spotlessApply' to fix." + exit 1 + fi + + # Tests + echo " πŸ§ͺ Running tests..." + ./gradlew test + if [ $? -ne 0 ]; then + echo "❌ Tests failed. Please fix the failing tests." + exit 1 + fi + + # Code coverage check + echo " πŸ“Š Checking code coverage..." + ./gradlew jacocoTestReport + if [ $? -ne 0 ]; then + echo "❌ Code coverage check failed." + exit 1 + fi + + echo "βœ… All pre-commit checks passed!" + '''.stripIndent() + preCommitHook.setExecutable(true) + + // Commit message hook + commitMsgHook.text = '''#!/bin/sh + commit_msg=$(cat $1) + + # Conventional commit pattern for SITMUN Proxy Middleware + pattern="^(feat|fix|docs|style|refactor|test|chore|perf|ci|build)(\\(proxy\\))?: .+" + + if ! echo "$commit_msg" | grep -qE "$pattern"; then + echo "❌ Invalid commit message format." + echo "" + echo "Expected: (proxy): " + echo "" + echo "Types:" + echo " feat - New feature" + echo " fix - Bug fix" + echo " docs - Documentation changes" + echo " style - Code style changes" + echo " refactor - Code refactoring" + echo " test - Test changes" + echo " chore - Maintenance tasks" + echo " perf - Performance improvements" + echo " ci - CI/CD changes" + echo " build - Build system changes" + echo "" + echo "Examples:" + echo " feat(proxy): add request decorator functionality" + echo " fix(proxy): resolve authentication token handling" + echo " docs: update README with proxy configuration info" + echo " test: add integration tests for proxy requests" + echo " style: format code with Google Java Format" + echo " refactor: improve request processing flow" + echo " chore: update dependencies to latest versions" + echo " perf: optimize proxy performance" + echo " ci: add GitHub Actions workflow" + echo " build: update Gradle configuration" + exit 1 + fi + + echo "βœ… Commit message format is valid!" + '''.stripIndent() + commitMsgHook.setExecutable(true) -tasks.named('sonarqube').configure { - dependsOn test + + println "βœ… Git hooks installed successfully!" + println "πŸ“‹ Pre-commit hook: Runs tests, formatting, and security checks" + println "πŸ“ Commit-msg hook: Validates conventional commit message format" + println "" + println "πŸ’‘ Usage examples:" + println " git commit -m \"feat(proxy): add request decorator functionality\"" + println " git commit -m \"fix(proxy): resolve authentication token handling\"" + println " git commit -m \"docs: update README with proxy configuration\"" + println " git commit -m \"test: add integration tests for proxy requests\"" + } } -sonarqube { - properties { - property 'sonar.host.url', 'https://sonarcloud.io' - property 'sonar.organization', 'sitmun' - property 'sonar.projectKey', 'org.sitmun:sitmun-proxy-middleware' - property 'sonar.links.homepage', 'https://github.com/sitmun/sitmun-proxy-middleware' - property 'sonar.links.scm', 'https://github.com/sitmun/sitmun-proxy-middleware' - property 'sonar.links.issue', 'https://github.com/sitmun/sitmun-proxy-middleware/issues' - property 'sonar.verbose', 'true' +// Task to remove Git hooks +tasks.register('removeGitHooks') { + group = 'git hooks' + description = 'Remove Git hooks' + def hookDir = new File(project.rootDir, '.git/hooks') + + doLast { + ['pre-commit', 'commit-msg'].each { hookName -> + def hookFile = new File(hookDir, hookName) + if (hookFile.exists()) { + hookFile.delete() + println "πŸ—‘οΈ Removed ${hookName} hook" + } + } + println "βœ… Git hooks removed successfully!" } } + +// Make setupGitHooks available as a dependency for build +tasks.named('build') { + dependsOn 'setupGitHooks' +} diff --git a/config/application-dev.yml b/config/application-dev.yml new file mode 100644 index 0000000..082dbf5 --- /dev/null +++ b/config/application-dev.yml @@ -0,0 +1,33 @@ +# Development Profile Configuration +spring: + # DevTools Configuration (only for development) + devtools: + restart: + enabled: true + poll-interval: 2s # Faster polling for better responsiveness + quiet-period: 1s + exclude: + - "**/batch/**" # Exclude batch jobs to prevent restarts during processing + - "**/tilesources/**" # Exclude tile source processing + - "**/io/**" # Exclude MBTiles I/O operations + - "**/config/**" # Exclude configuration classes + - "**/service/**" # Exclude service layer + - "**/utils/**" # Exclude utility classes + - "**/dto/**" # Exclude DTOs + - "**/controllers/**" # Exclude controllers (optional) + livereload: + enabled: true + port: 35729 + + # H2 Console (only for development) + h2: + console: + enabled: true + path: /h2-console + +# Development-specific logging +logging: + level: + org.sitmun.proxy.middleware: DEBUG + org.hibernate.SQL: DEBUG + org.hibernate.type.descriptor.sql.BasicBinder: TRACE \ No newline at end of file diff --git a/config/application-prod.yml b/config/application-prod.yml new file mode 100644 index 0000000..98311dc --- /dev/null +++ b/config/application-prod.yml @@ -0,0 +1,24 @@ +# Development Profile Configuration +spring: + # H2 Console disabled for production + h2: + console: + enabled: false + +# Production-specific logging +logging: + level: + org.sitmun.mbtiles: INFO + root: WARN + pattern: + console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" + +# Production-specific actuator configuration +management: + endpoint: + health: + show-details: never # Hide detailed health info in production + endpoints: + web: + exposure: + include: health # Minimal endpoints for production \ No newline at end of file diff --git a/config/application.yml b/config/application.yml new file mode 100644 index 0000000..792bba0 --- /dev/null +++ b/config/application.yml @@ -0,0 +1,27 @@ +# Logging Configuration +logging: + level: + ROOT: INFO + org.sitmun.proxy.middleware: INFO + +# Sitmun Proxy Configuration +sitmun: + backend: + config: + url: http://some.url + secret: some-secret + +# Actuator Configuration +management: + endpoints: + web: + exposure: + include: health + base-path: /actuator + endpoint: + health: + show-details: never + show-components: never + health: + defaults: + enabled: true \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index a4568dc..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,13 +0,0 @@ -services: - admin: - image: sitmun-proxy-middleware - build: - context: . - args: - PROXY_VERSION: 0.1.0-SNAPSHOT - environment: - SITMUN_BACKEND_CONFIG_URL: ${SITMUN_BACKEND_CONFIG_URL} - SITMUN_BACKEND_CONFIG_SECRET: ${SITMUN_BACKEND_CONFIG_SECRET} - - ports: - - "9002:8080" diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..a9ce40b --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,46 @@ +# Stage 0, "build-proxy-middleware", based on OpenJDK 17, to build and compile the application +ARG JDK_VERSION=17 +ARG JDK_TAG=0.16 +ARG ALPINE_TAG=3.22 + +# Use OpenJDK 17 as base image +FROM amazoncorretto:${JDK_VERSION}.${JDK_TAG} AS build-proxy-middleware +COPY . /usr/src/sitmun-proxy-middleware +WORKDIR /usr/src/sitmun-proxy-middleware +RUN --mount=type=cache,target=/root/.gradle ./gradlew --no-daemon -i clean build -x test -x setupGitHooks +ARG JDK_VERSION=17 +ARG JDK_TAG=0.16 +ARG ALPINE_TAG=3.22 + +# Stage 1, based on OpenJDK 17, to have only the compiled app + +# Use OpenJDK 17 as base image +FROM amazoncorretto:${JDK_VERSION}.${JDK_TAG}-alpine${ALPINE_TAG} + +# Create application directories +RUN mkdir -p /app/config + +# Copy the built JAR file +COPY --from=build-proxy-middleware /usr/src/sitmun-proxy-middleware/build/libs/*.jar /app/sitmun-proxy-middleware.jar + +# Set working directory +WORKDIR /app + +# Expose port +EXPOSE 8080 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8080/actuator/health || exit 1 + +# Default JVM options +ENV JAVA_OPTS="-Xms512m -Xmx1024m -XX:+UseG1GC -XX:+UseContainerSupport" + +# Default Spring profile +ENV SPRING_PROFILES_ACTIVE="prod" + +# Volume for external configuration +VOLUME ["/app/config"] + +# Entry point with support for external configuration +ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar sitmun-proxy-middleware.jar --spring.config.additional-location=file:/app/config/"] diff --git a/docker/development/docker-compose.yml b/docker/development/docker-compose.yml new file mode 100644 index 0000000..8c6e94b --- /dev/null +++ b/docker/development/docker-compose.yml @@ -0,0 +1,40 @@ +services: + sitmun-proxy-middleware: + build: + context: ../.. + dockerfile: docker/Dockerfile + container_name: sitmun-proxy-middleware + ports: + - "8080:8080" + environment: + # Spring Boot Configuration + - SPRING_PROFILES_ACTIVE=prod + - SERVER_PORT=8080 + + # JVM Configuration + - JAVA_OPTS=-Xms512m -Xmx1024m -XX:+UseG1GC -XX:+UseContainerSupport + + # Logging Configuration + - LOGGING_LEVEL_ROOT=INFO + - LOGGING_LEVEL_ORG_SITMUN_PROXY_MIDDLEWARE=INFO + + # SITMUN Backend Configuration + - SITMUN_BACKEND_CONFIG_URL=http://sitmun-backend:8080 + - SITMUN_BACKEND_CONFIG_SECRET=your-secret-key-here + + # Actuator Configuration + - MANAGEMENT_ENDPOINTS_WEB_EXPOSURE_INCLUDE=health,info,metrics + - MANAGEMENT_ENDPOINT_HEALTH_SHOW_DETAILS=never + + volumes: + # External configuration directory + - ../../config:/app/config:ro + + restart: unless-stopped + + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 4e4b3cf..ac1a35f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,10 +1 @@ -# Project version -version=0.2.0-SNAPSHOT - -# Dependencies -jjwt_version=0.12.6 -json_version=20240303 -okhttp3_version=4.12.0 - -# Spring version -spring_boot_version=2.7.18 \ No newline at end of file +org.gradle.configuration-cache=true \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..5999e4b --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,43 @@ +[versions] +# Spring Boot and related versions +spring-boot = "3.5.4" +spring-dependency-management = "1.1.7" + +# Plugin versions +lombok = "8.6" +spotless = "7.2.0" +axion-release = "1.19.0" + +# Dependency versions +h2 = "2.2.224" +json = "20240303" +jjwt = "0.12.6" +okhttp3 = "4.12.0" + +[libraries] +spring-boot-starter-web = { group = "org.springframework.boot", name = "spring-boot-starter-web" } +spring-boot-starter-jdbc = { group = "org.springframework.boot", name = "spring-boot-starter-jdbc" } +spring-boot-starter-actuator = { group = "org.springframework.boot", name = "spring-boot-starter-actuator" } +spring-boot-starter-test = { group = "org.springframework.boot", name = "spring-boot-starter-test" } +spring-boot-devtools = { group = "org.springframework.boot", name = "spring-boot-devtools" } +h2 = { group = "com.h2database", name = "h2", version.ref = "h2" } + +# JSON and JWT +json = { group = "org.json", name = "json", version.ref = "json" } +jjwt-api = { group = "io.jsonwebtoken", name = "jjwt-api", version.ref = "jjwt" } +jjwt-impl = { group = "io.jsonwebtoken", name = "jjwt-impl", version.ref = "jjwt" } +jjwt-jackson = { group = "io.jsonwebtoken", name = "jjwt-jackson", version.ref = "jjwt" } + +# HTTP Client +okhttp3 = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp3" } + +# Database Drivers +postgresql = { group = "org.postgresql", name = "postgresql" } +oracle-jdbc = { group = "com.oracle.database.jdbc", name = "ojdbc11-production" } + +[plugins] +spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot" } +spring-dependency-management = { id = "io.spring.dependency-management", version.ref = "spring-dependency-management" } +lombok = { id = "io.freefair.lombok", version.ref = "lombok" } +spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } +axion-release = { id = "pl.allegro.tech.build.axion-release", version.ref = "axion-release" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 41d9927a4d4fb3f96a785543079b8df6723c946b..7f93135c49b765f8051ef9d0a6055ff8e46073d8 100644 GIT binary patch delta 44451 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 W0}GkPe@wIjfE;Yd41Mt9+y4Mm#rGos delta 40585 zcmY(qV|1XwvIQDzV%xTD+qP}{i!<@Wwryu(OzdQ0+n8vAH}9Ui&b#kl_v-prwQBFG zz5DDAymk>BQdtfh0xlU89wQMK4HPbs(FOE>Zb%RyARtbzR!ra^|6jx!#(y4@AwUVR z;6OlNVEzS(fTU6^mUV)HfZ%|GfG{PiWMC%G+@mK;yI}&45b1DxL>H4=px z=~X>~f6HHzWKQ5n9l*Y$<8cxzL0!Wg)-xDmO{EO$Q7%o^sALa?h>3-qITfe6>rBz3RujVGntj4oJ38{lrCHF zEBHDVdgrNZkMk+MF>bns_%U8|S(|y-R-P_T&zs56ZX*{O zLA*NGyX+CH2M>V#`yaQi3h6UA@V*2Q$sG9dZjk*`}u#RvOqk*8r5k+AZP!<#H zow+P6u9@jLA?5CBUW#llsN;0-pRpRd@EaOihs~)Ar0plI5~0*4%M*5^7}F~wesQN> zX&z09>I~f(jO)4s!0uUhTU*oM)g#ZO>jdJ@DeJCG5B80{CvLF<`Z{>Rm6(}%RPTa7UU zz19{mD$}J!=FVip3^hD$(N6IS1Jt2-s2xU`4$OC)YkECMAVNDdXgVD}&uf7&f!vFW z%F&@TuSdFiO>L-LrZ>hlwNIe9w!;+F-IcR3>x3@eG1CIJy@FS+OGmjiI@9R54bKQD zqVT94SB_$s+Smx@_jhFXoqhRifg&q~6 zi_NAb%f+V~LpsA@n0M!Ry5MtgSWCB6%65gNxuy>Pt) z#svDcxa=|N@Sul!kUM+9AvbqHWtSL{_PgIhh;GS-=ICgd9|h|HoBK9xgriHFTuEJW zxio_X>n0`2yxir{yFcfePV>i6+sl$#HNin<3{wh;JK{|)*5#ww7rYwD>iLQ4;df~h zOMGO2>Y<`%?d7fNgReBX9+kVDz>f)w>0`=~>>8)0uJRM>C~n+)&OUp$IMmJ5`rm`f z9|8-9E5&fP7T4zM^o`p4kk`EMU8`-sjk}lH*6c}JeD7v^N>cI^?lxc*y^^1C0d9iuHdk76iXH5uV?obA%=&-yPN+2@ui^(Ps#pDtb zmk{J{(ANoqgKcYKUj<%hz52gzzQBWXoY?=u3?N7Yj-)AMs!}sv6>y!=il)>W?hXZj z-_#e(M)4=DZ_(#%Jnf)aur+Z{atRrak+)V`^p@Fx^$#CRsndYj<%%z+$2zh2H$W^! zBUiF>J1(_oHr|A3+83M{?36?Mew+o-RXNI5L z0W&0SKW+Tnv{c*7JW`R_p#sGO9=9UUlYMxgkW&!$Q$sFwj8CSArAWNt3igd+IB!29 z>_^ylaZkn044mz!jFqnytU!aO{jeh z{0y8AJcHtdk3M1CKLi242)jx#Y`&vlD|6?nS5hzA{tzyQePRKl6^&2cB<^ydhq9vK zcRhiEOCl-*K|;d@<%f8J7=z&+H{a~#RyqSW-@oxv3;+6Gth~9$N%r?0|347v)1ueU z{tr@rL;nNJe-KHS?CZw@gj=AgN>iij3UA?}P;??}Fm`vW z34cWhzAUBpo+dA18S#>ddXxn$W`2@=`p)Cz&bvqoJfjA>!*LTNk9TrCZ@Tr~be~*q zQ31dQOh4VrFL5v@_9Bd-g;?oij`19aejc=wJ4>{r4q`3Ftd1~Xo-)>CQ+TFdkFv;{ z&((zR+>A5<)`k%;gWY+gA6;ztjuvB&%^j1!FRAjS=kohh)ogltcs>oaRa#*oJ-*6b zurYV*>L`oTvIoCm@3|j_k$N>`H!h4;IHBRwNc7cm)rMb~5vpT<|9u#+;(4{pKXkMN z3X%7%$tjJ5|2!3lvU3~o97$bY2)VU-2@#`XfUFDM(Pu320{S96T} zrralMNGzkn29VJq5bk)MM{ll&FQOxTUPMXos zRFelLEi!{ho?5a=bno?LanZx{U@_0ksj#PN4_VDpkY#~(%2!td7gbk}cG&^CqSGU8 z*d?`WZHLmVo8*(Vtnpn1?~TdB@=>SsVOL{6#zyP%WQJw@^CYW5bwgS&wOz=~wIZCI z!j4vR%%8X;hxX&FU8z>eV7jM+8s8ka?F$JAFyjjr2Wj9@t3|+Xv@;~<2VrwsPCI>; z`(wlHGB^NcDuJ7Y#%q!6z3;jnr>AU-99#|Q!)_$0+|LthCnyFAx~0aOMNy<>OAj2yRXmwO6~9-BI~Roxio~mpqmByC$>NH<@ep-TwKb^ruF8-EF3@=lj2`TTeg_q_9}c;UHdVXaSJtT zx%alqLVv@jA8qDpYrII@Y>#se86bDz5twT%?jNnc$_iP?|3-{5zjiyPT(Z2i3t!;I zMLqIrcL4~6kNvnUEo~9E;26saZ3w(;H#ASs?E6x~w`e3E^4oDnbL9dfBm$K?Lo%kAis$)z6^A)XT*jzZFHw=! za3+koOK0de#XjO+Q1h>d0R)X>Na!!fC@tr#hQi=$05X{rR`Ia~349|=gbub&nOP2w z>;w)e7TG-xY89E>P3xk!4hCgA15>#;3rBJhJ4F@f`WywgCfZpMUQwN0miqXqc#n9^ zpY5~W1ByBSi(~}+b>T^@jnaf)_EVeXOIGLBusvmS-#erI37D$5m<$A|e~3O9{r?$S zUP$dmVe&2@0&KadNuYmSDV!=ea zgmT8E0GDFHE!}Gg7VN2BVpHjfd`^7~4n}fW>Bk=R0CL!nHG-%Gqj!6 z3y=p&;wPbeokxVhi0GX1Pdlm(aK+O595s@CB7%!9Qr#?+QtUG~ZpBMUe{Pm^@BOZ$ zjzhc6Inw_0i5O6qGWU%l(RY?hDlE6=aqW6K%6h~c&QhR@Dzby~lkrYlz>6`#0K1y@ zz5Y2J1!VOx66~;s-}u76R~<;#8^_pcpi=IfogNxOqk925Z_Vbtd%mS*0+Sxtie(EU z-TQWlZYB8}MB;>|7)M@KdK4(Xe-F}}90$z0PC!H>|MF5;wfqkCGE!CYV)oF>+?Rox zN~}eY&#HeqWi$pa1@7DG=fe@pn!^n;Z9LV@0nDao(T}*>ThUds?s_AP@x&Jz$wEOF-k|B4_-Au(oj~!!J|kz{^JAq=y!I14r_}Gj+bW*Q;mM(_E^k$ucE9rZ)wm` z0ZQ7=Dv!%-dC1p7O7_W(pvIi7JsponS%)gq|7@UBsQl#CDk(j#unt%5(Xl$!#Ik6Q z^7Fc_bqMSMms1B!vYK=i;*TpunxT8pHm>b@Ely4YE3hO<@Lk!G*8Lv0t#ND1uk&uM z*)?iLXf)!INcXb{W@@%l%Xan~8pZKn07nx8Z?rrr&NCC)QZ2Zp$G>6Cs?tm*KeqP- zeXDi&>l{&JNUYCQRjB$D*Krouv*M=akmgwRU;^Z_B0zDwVZixE@!0Q$Ii10r=J7Ri zw{CBlY)rE3GI zNAf#D`;7OU)Nhr3$VY$~N-%9xRwEZW?GYqluf>u{B$1Y^NOLHBNfii0Ru)_ReJ1k8 zAKp_A4VGbrq3LB6(=-12z||3FEyAs_z^oApxd787g5er$LFgpvgQRja`b)9c`HYFM z@b-;4pz-TJqxOI2s|07IDH>D|kP1o=5R(6w5&(>5_9kv_BRa6&ddsPTW4hbTkN6w1 z`{FV%U`^bHGBB{ejlUB>)7>R$dxoHidN<}dP&AR3l+&kBq?Gw-!?n$eNDSD zp#t-?pDHxV^MLoM&{-e<=|{VDVCCGG_b&+|?^Bnz)bv1wzPkl8_TUH^lc%I~Iezm7 zvBw-k-U&wdGw@%g?bhh?6>#ak)`)(?{oe*^D)lKltSa^EoWmnAT}vYa_&3Dr@9^iR zFzhvoGEWK-^XeOn_8h)n8HV&(ZW@T*rwD>ddk-VeVDt1S)2j3I z4|CZ)n?uVzqU;=St5y0Zh0FRkq<8fp1pqjg@2N+x@=+?G;RsfgL^Y&E>mU|g�Kz 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 41dfb87..3fa8f86 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index a69d9cb..1aa94a4 100755 --- a/gradlew +++ b/gradlew @@ -55,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -80,13 +80,11 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,22 +131,29 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -193,11 +198,15 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/gradlew.bat b/gradlew.bat index 53a6b23..6689b85 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -26,6 +26,7 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% diff --git a/lombok.config b/lombok.config deleted file mode 100644 index 189c0be..0000000 --- a/lombok.config +++ /dev/null @@ -1,3 +0,0 @@ -# This file is generated by the 'io.freefair.lombok' Gradle plugin -config.stopBubbling = true -lombok.addLombokGeneratedAnnotation = true diff --git a/src/main/java/org/sitmun/proxy/middleware/ProxyMiddlewareApplication.java b/src/main/java/org/sitmun/proxy/middleware/Application.java similarity index 76% rename from src/main/java/org/sitmun/proxy/middleware/ProxyMiddlewareApplication.java rename to src/main/java/org/sitmun/proxy/middleware/Application.java index 28ab61e..66dc614 100644 --- a/src/main/java/org/sitmun/proxy/middleware/ProxyMiddlewareApplication.java +++ b/src/main/java/org/sitmun/proxy/middleware/Application.java @@ -5,10 +5,9 @@ import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) -public class ProxyMiddlewareApplication { +public class Application { public static void main(String[] args) { - SpringApplication.run(ProxyMiddlewareApplication.class, args); + SpringApplication.run(Application.class, args); } - -} \ No newline at end of file +} diff --git a/src/main/java/org/sitmun/proxy/middleware/config/ProxyMiddlewareConfiguration.java b/src/main/java/org/sitmun/proxy/middleware/config/ProxyMiddlewareConfiguration.java index e830950..89c7895 100644 --- a/src/main/java/org/sitmun/proxy/middleware/config/ProxyMiddlewareConfiguration.java +++ b/src/main/java/org/sitmun/proxy/middleware/config/ProxyMiddlewareConfiguration.java @@ -1,9 +1,14 @@ package org.sitmun.proxy.middleware.config; +import java.time.Duration; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.web.client.RestTemplate; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.ForwardedHeaderFilter; @Configuration @@ -11,11 +16,35 @@ public class ProxyMiddlewareConfiguration { @Bean public RestTemplate restTemplate(RestTemplateBuilder builder) { - return builder.build(); + return builder + .connectTimeout(Duration.ofSeconds(10)) + .readTimeout(Duration.ofSeconds(30)) + .build(); + } + + @Bean + public SimpleClientHttpRequestFactory httpRequestFactory() { + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + factory.setConnectTimeout(10000); // 10 seconds + factory.setReadTimeout(30000); // 30 seconds + return factory; } @Bean public ForwardedHeaderFilter forwardedHeaderFilter() { return new ForwardedHeaderFilter(); } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.addAllowedOriginPattern("*"); + configuration.addAllowedMethod("*"); + configuration.addAllowedHeader("*"); + configuration.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } } diff --git a/src/main/java/org/sitmun/proxy/middleware/controllers/ProxyMiddlewareController.java b/src/main/java/org/sitmun/proxy/middleware/controllers/ProxyMiddlewareController.java index 106f3ff..04aee63 100644 --- a/src/main/java/org/sitmun/proxy/middleware/controllers/ProxyMiddlewareController.java +++ b/src/main/java/org/sitmun/proxy/middleware/controllers/ProxyMiddlewareController.java @@ -1,33 +1,33 @@ package org.sitmun.proxy.middleware.controllers; -import org.sitmun.proxy.middleware.service.ProxyMiddlewareService; +import jakarta.servlet.http.HttpServletRequest; +import java.util.Map; +import org.sitmun.proxy.middleware.service.RequestConfigurationService; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import javax.servlet.http.HttpServletRequest; -import java.util.Map; - @RestController @RequestMapping("/proxy") public class ProxyMiddlewareController { - private final ProxyMiddlewareService proxyMiddlewareService; + private final RequestConfigurationService requestConfigurationService; - public ProxyMiddlewareController(ProxyMiddlewareService proxyMiddlewareService) { - this.proxyMiddlewareService = proxyMiddlewareService; + public ProxyMiddlewareController(RequestConfigurationService requestConfigurationService) { + this.requestConfigurationService = requestConfigurationService; } @GetMapping("/{appId}/{terId}/{type}/{typeId}") - public ResponseEntity getService(@PathVariable("appId") Integer appId, - @PathVariable("terId") Integer terId, - @PathVariable("type") String type, - @PathVariable("typeId") Integer typeId, - @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = false) String authorization, - @RequestParam(required = false) Map params, - HttpServletRequest request) { + public ResponseEntity getService( + @PathVariable("appId") Integer appId, + @PathVariable("terId") Integer terId, + @PathVariable("type") String type, + @PathVariable("typeId") Integer typeId, + @RequestHeader(value = HttpHeaders.AUTHORIZATION, required = false) String authorization, + @RequestParam(required = false) Map params, + HttpServletRequest request) { String token = authorization != null ? authorization.substring(7) : null; String url = request.getRequestURL().toString(); - return proxyMiddlewareService.doRequest(appId, terId, type, typeId, token, params, url); + return requestConfigurationService.doRequest(appId, terId, type, typeId, token, params, url); } } diff --git a/src/main/java/org/sitmun/proxy/middleware/decorator/DecoratedRequest.java b/src/main/java/org/sitmun/proxy/middleware/decorator/DecoratedRequest.java deleted file mode 100644 index 986bbac..0000000 --- a/src/main/java/org/sitmun/proxy/middleware/decorator/DecoratedRequest.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.sitmun.proxy.middleware.decorator; - -public interface DecoratedRequest { - DecoratedResponse execute(); - String describe(); -} diff --git a/src/main/java/org/sitmun/proxy/middleware/decorator/RequestDecorator.java b/src/main/java/org/sitmun/proxy/middleware/decorator/RequestDecorator.java index d4eb175..14a23d3 100644 --- a/src/main/java/org/sitmun/proxy/middleware/decorator/RequestDecorator.java +++ b/src/main/java/org/sitmun/proxy/middleware/decorator/RequestDecorator.java @@ -1,5 +1,3 @@ package org.sitmun.proxy.middleware.decorator; -public interface RequestDecorator extends Decorator { -} - +public interface RequestDecorator extends Decorator {} diff --git a/src/main/java/org/sitmun/proxy/middleware/decorator/ResponseDecorator.java b/src/main/java/org/sitmun/proxy/middleware/decorator/ResponseDecorator.java index d1db43e..1503526 100644 --- a/src/main/java/org/sitmun/proxy/middleware/decorator/ResponseDecorator.java +++ b/src/main/java/org/sitmun/proxy/middleware/decorator/ResponseDecorator.java @@ -1,5 +1,3 @@ package org.sitmun.proxy.middleware.decorator; -public interface ResponseDecorator extends Decorator { -} - +public interface ResponseDecorator extends Decorator {} diff --git a/src/main/java/org/sitmun/proxy/middleware/decorator/request/BodyRequestDecorator.java b/src/main/java/org/sitmun/proxy/middleware/decorator/request/BodyRequestDecorator.java deleted file mode 100644 index 076e599..0000000 --- a/src/main/java/org/sitmun/proxy/middleware/decorator/request/BodyRequestDecorator.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.sitmun.proxy.middleware.decorator.request; - -import org.sitmun.proxy.middleware.decorator.Context; -import org.sitmun.proxy.middleware.decorator.HttpContext; -import org.sitmun.proxy.middleware.decorator.RequestDecorator; -import org.springframework.stereotype.Component; - -@Component -public class BodyRequestDecorator implements RequestDecorator { - - @Override - public boolean accept(Object target, Context context) { - // TODO complete implementation - // OgcWmsPayloadDto ogcPayload = (OgcWmsPayloadDto) payload; - // result = "POST".equalsIgnoreCase(ogcPayload.getMethod()) && ogcPayload.getRequestBody() != null; - return context instanceof HttpContext; - } - - @Override - public void addBehavior(Object target, Context context) { - // TODO Valid implementation - // HttpRequest request = (HttpRequest) target; - // HttpContext httpContext = (HttpContext) context; - // Example - // OgcWmsPayloadDto ogcPayload = (OgcWmsPayloadDto) payload; - // globalRequest.getCustomHttpRequest().getRequestBuilder().post(ogcPayload.getRequestBody()); - } -} diff --git a/src/main/java/org/sitmun/proxy/middleware/decorator/response/CapabiltiesResponseDecorator.java b/src/main/java/org/sitmun/proxy/middleware/decorator/response/CapabiltiesResponseDecorator.java deleted file mode 100644 index 1346e17..0000000 --- a/src/main/java/org/sitmun/proxy/middleware/decorator/response/CapabiltiesResponseDecorator.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.sitmun.proxy.middleware.decorator.response; - -import lombok.extern.slf4j.Slf4j; -import org.sitmun.proxy.middleware.decorator.Context; -import org.sitmun.proxy.middleware.decorator.ResponseDecorator; -import org.sitmun.proxy.middleware.dto.OgcWmsPayloadDto; -import org.sitmun.proxy.middleware.response.Response; -import org.springframework.stereotype.Component; - -import java.nio.charset.StandardCharsets; - -@Component -@Slf4j -public class CapabiltiesResponseDecorator implements ResponseDecorator { - - @Override - public boolean accept(Object target, Context context) { - if (context instanceof OgcWmsPayloadDto) { - OgcWmsPayloadDto ogcWmsPayloadDto = (OgcWmsPayloadDto) context; - return ogcWmsPayloadDto.getParameters().getOrDefault("REQUEST", "").equalsIgnoreCase("GetCapabilities"); - } - return false; - } - - @Override - public void addBehavior(Object response, Context context) { - log.info("Adding behavior to response {} of {}", response, context); - if (context instanceof OgcWmsPayloadDto) { - OgcWmsPayloadDto ogcWmsPayloadDto = (OgcWmsPayloadDto) context; - //noinspection unchecked - Response response1 = (Response) response; - String s = new String(response1.getBody(), StandardCharsets.UTF_8); - String output = s.replaceAll(ogcWmsPayloadDto.getUri(), response1.getBaseUrl()); - log.info("Replacement of {} by {} in GetCapabilities response", ogcWmsPayloadDto.getUri(), response1.getBaseUrl()); - response1.setBody(output.getBytes(StandardCharsets.UTF_8)); - } - } -} diff --git a/src/main/java/org/sitmun/proxy/middleware/decorator/response/PaginationResponseDecorator.java b/src/main/java/org/sitmun/proxy/middleware/decorator/response/PaginationResponseDecorator.java deleted file mode 100644 index c1bc11d..0000000 --- a/src/main/java/org/sitmun/proxy/middleware/decorator/response/PaginationResponseDecorator.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.sitmun.proxy.middleware.decorator.response; - -import org.sitmun.proxy.middleware.decorator.Context; -import org.sitmun.proxy.middleware.decorator.ResponseDecorator; -import org.springframework.stereotype.Component; - -@Component -public class PaginationResponseDecorator implements ResponseDecorator { - - @Override - public boolean accept(Object target, Context response) { - // TODO return MediaType.APPLICATION_JSON.equals(response.getHeaders().getContentType()); - return false; - } - - @Override - public void addBehavior(Object response, Context context) { - //TODO implementation if necessary - } - -} diff --git a/src/main/java/org/sitmun/proxy/middleware/dto/ConfigProxyDto.java b/src/main/java/org/sitmun/proxy/middleware/dto/ConfigProxyDto.java index 6c49564..9a6442c 100644 --- a/src/main/java/org/sitmun/proxy/middleware/dto/ConfigProxyDto.java +++ b/src/main/java/org/sitmun/proxy/middleware/dto/ConfigProxyDto.java @@ -3,6 +3,8 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import lombok.*; +import org.sitmun.proxy.middleware.protocols.jdbc.JdbcPayloadDto; +import org.sitmun.proxy.middleware.protocols.wms.WmsPayloadDto; @Getter @Setter @@ -16,7 +18,9 @@ public class ConfigProxyDto { private long exp; @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") - @JsonSubTypes({@JsonSubTypes.Type(value = OgcWmsPayloadDto.class), @JsonSubTypes.Type(value = DatasourcePayloadDto.class)}) + @JsonSubTypes({ + @JsonSubTypes.Type(value = WmsPayloadDto.class), + @JsonSubTypes.Type(value = JdbcPayloadDto.class) + }) private PayloadDto payload; - } diff --git a/src/main/java/org/sitmun/proxy/middleware/dto/ConfigProxyRequest.java b/src/main/java/org/sitmun/proxy/middleware/dto/ConfigProxyRequestDto.java similarity index 86% rename from src/main/java/org/sitmun/proxy/middleware/dto/ConfigProxyRequest.java rename to src/main/java/org/sitmun/proxy/middleware/dto/ConfigProxyRequestDto.java index 3f4c9a3..42c46d6 100644 --- a/src/main/java/org/sitmun/proxy/middleware/dto/ConfigProxyRequest.java +++ b/src/main/java/org/sitmun/proxy/middleware/dto/ConfigProxyRequestDto.java @@ -1,19 +1,17 @@ package org.sitmun.proxy.middleware.dto; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Map; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.springframework.beans.factory.annotation.Value; - -import java.util.Map; @Getter @Setter @NoArgsConstructor @AllArgsConstructor -public class ConfigProxyRequest { +public class ConfigProxyRequestDto { @JsonProperty("appId") private int appId; @@ -28,7 +26,6 @@ public class ConfigProxyRequest { private int typeId; @JsonProperty("method") - @Value("GET") private String method; @JsonProperty("parameters") @@ -39,5 +36,4 @@ public class ConfigProxyRequest { @JsonProperty("id_token") private String token; - -} \ No newline at end of file +} diff --git a/src/main/java/org/sitmun/proxy/middleware/dto/DatasourcePayloadDto.java b/src/main/java/org/sitmun/proxy/middleware/dto/DatasourcePayloadDto.java deleted file mode 100644 index ccbd579..0000000 --- a/src/main/java/org/sitmun/proxy/middleware/dto/DatasourcePayloadDto.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.sitmun.proxy.middleware.dto; - -import com.fasterxml.jackson.annotation.JsonTypeName; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import org.sitmun.proxy.middleware.decorator.JdbcContext; - -import java.util.List; - -@Getter -@Setter -@JsonTypeName("DatasourcePayload") -@NoArgsConstructor -public class DatasourcePayloadDto extends PayloadDto implements JdbcContext { - - private String uri; - private String user; - private String password; - private String driver; - private String sql; - - - @Builder - public DatasourcePayloadDto(List vary, String uri, String user, String password, String driver, String sql) { - super(vary); - this.uri = uri; - this.user = user; - this.password = password; - this.driver = driver; - this.sql = sql; - } - - @Override - public String describe() { - return "DatasourcePayloadDto{" + - "vary=" + getVary() + - ", uri='" + uri + '\'' + - ", user='" + user + '\'' + - ", password='****'" + - ", driver='" + driver + '\'' + - ", sql='" + sql + '\'' + - '}'; - } -} diff --git a/src/main/java/org/sitmun/proxy/middleware/dto/ErrorResponseDTO.java b/src/main/java/org/sitmun/proxy/middleware/dto/ErrorResponseDto.java similarity index 91% rename from src/main/java/org/sitmun/proxy/middleware/dto/ErrorResponseDTO.java rename to src/main/java/org/sitmun/proxy/middleware/dto/ErrorResponseDto.java index 9cd9ebf..be54cd3 100644 --- a/src/main/java/org/sitmun/proxy/middleware/dto/ErrorResponseDTO.java +++ b/src/main/java/org/sitmun/proxy/middleware/dto/ErrorResponseDto.java @@ -1,17 +1,16 @@ package org.sitmun.proxy.middleware.dto; +import java.util.Date; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import java.util.Date; - @Getter @Setter @NoArgsConstructor @AllArgsConstructor -public class ErrorResponseDTO { +public class ErrorResponseDto { private int status; private String error; diff --git a/src/main/java/org/sitmun/proxy/middleware/dto/HttpSecurityDto.java b/src/main/java/org/sitmun/proxy/middleware/dto/HttpSecurityDto.java index c1f0223..74fa737 100644 --- a/src/main/java/org/sitmun/proxy/middleware/dto/HttpSecurityDto.java +++ b/src/main/java/org/sitmun/proxy/middleware/dto/HttpSecurityDto.java @@ -4,7 +4,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.sitmun.proxy.middleware.decorator.HttpContextSecurity; +import org.sitmun.proxy.middleware.protocols.http.HttpContextSecurity; @Getter @Setter diff --git a/src/main/java/org/sitmun/proxy/middleware/dto/OgcWmsPayloadDto.java b/src/main/java/org/sitmun/proxy/middleware/dto/OgcWmsPayloadDto.java deleted file mode 100644 index d58ab41..0000000 --- a/src/main/java/org/sitmun/proxy/middleware/dto/OgcWmsPayloadDto.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.sitmun.proxy.middleware.dto; - -import com.fasterxml.jackson.annotation.JsonTypeName; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import org.sitmun.proxy.middleware.decorator.HttpContext; - -import java.util.List; -import java.util.Map; - -@Getter -@Setter -@JsonTypeName("OgcWmsPayload") -@NoArgsConstructor -public class OgcWmsPayloadDto extends PayloadDto implements HttpContext { - - private String uri; - private String method; - private Map parameters; - private HttpSecurityDto security; - - @Builder - public OgcWmsPayloadDto(List vary, String uri, String method, Map parameters, HttpSecurityDto security) { - super(vary); - this.uri = uri; - this.method = method; - this.parameters = parameters; - this.security = security; - } - - @Override - public String describe() { - return "OgcWmsPayloadDto{" + - "vary=" + getVary() + - ", uri='" + uri + '\'' + - ", method='" + method + '\'' + - ", parameters=" + parameters + - ", security=" + security + - '}'; - } -} diff --git a/src/main/java/org/sitmun/proxy/middleware/dto/PayloadDto.java b/src/main/java/org/sitmun/proxy/middleware/dto/PayloadDto.java index d610f01..8005061 100644 --- a/src/main/java/org/sitmun/proxy/middleware/dto/PayloadDto.java +++ b/src/main/java/org/sitmun/proxy/middleware/dto/PayloadDto.java @@ -1,13 +1,12 @@ package org.sitmun.proxy.middleware.dto; +import java.util.List; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import org.sitmun.proxy.middleware.decorator.Context; -import java.util.List; - @Getter @Setter @AllArgsConstructor @@ -18,8 +17,6 @@ public class PayloadDto implements Context { @Override public String describe() { - return "PayloadDto{" + - "vary=" + vary + - "}"; + return "PayloadDto{" + "vary=" + vary + "}"; } } diff --git a/src/main/java/org/sitmun/proxy/middleware/service/ClientService.java b/src/main/java/org/sitmun/proxy/middleware/protocols/http/HttpClient.java similarity index 64% rename from src/main/java/org/sitmun/proxy/middleware/service/ClientService.java rename to src/main/java/org/sitmun/proxy/middleware/protocols/http/HttpClient.java index 6169b3b..cdc52e7 100644 --- a/src/main/java/org/sitmun/proxy/middleware/service/ClientService.java +++ b/src/main/java/org/sitmun/proxy/middleware/protocols/http/HttpClient.java @@ -1,11 +1,10 @@ -package org.sitmun.proxy.middleware.service; +package org.sitmun.proxy.middleware.protocols.http; +import java.io.IOException; import okhttp3.Request; import okhttp3.Response; -import java.io.IOException; - -public interface ClientService { +public interface HttpClient { Response executeRequest(Request httpRequest) throws IOException; } diff --git a/src/main/java/org/sitmun/proxy/middleware/protocols/http/HttpClientFactoryService.java b/src/main/java/org/sitmun/proxy/middleware/protocols/http/HttpClientFactoryService.java new file mode 100644 index 0000000..b372267 --- /dev/null +++ b/src/main/java/org/sitmun/proxy/middleware/protocols/http/HttpClientFactoryService.java @@ -0,0 +1,137 @@ +package org.sitmun.proxy.middleware.protocols.http; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import lombok.extern.slf4j.Slf4j; +import okhttp3.Interceptor; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Service +@Slf4j +public class HttpClientFactoryService implements HttpClient { + + private final List unsafeAllowedHosts; + private OkHttpClient safeClient; + private OkHttpClient unsafeClient; + private final List interceptors = new ArrayList<>(); + + public HttpClientFactoryService( + @Value("${sitmun.client.unsafe-allowed-hosts:*}") List unsafeAllowedHosts) { + this.unsafeAllowedHosts = unsafeAllowedHosts; + safeClient = createSafeClient(); + unsafeClient = createUnsafeClient(); + } + + private OkHttpClient createUnsafeClient() { + OkHttpClient.Builder builder = new OkHttpClient.Builder(); + interceptors.forEach(builder::addInterceptor); + return configureToIgnoreCertificate(builder).build(); + } + + private OkHttpClient createSafeClient() { + OkHttpClient.Builder builder = new OkHttpClient.Builder(); + interceptors.forEach(builder::addInterceptor); + return builder.build(); + } + + @Override + public Response executeRequest(Request httpRequest) throws IOException { + return getClient(httpRequest.url().host()).newCall(httpRequest).execute(); + } + + public OkHttpClient getClient(String host) { + try { + if (unsafeAllowedHosts.contains("*") || unsafeAllowedHosts.contains(host)) { + log.warn("Using Unsafe Client"); + return unsafeClient; + } else { + return safeClient; + } + } catch (Exception e) { + log.warn("Exception while creating client: {}", e.getMessage(), e); + return safeClient; + } + } + + private static OkHttpClient.Builder configureToIgnoreCertificate(OkHttpClient.Builder builder) { + log.warn("Ignore SSL Certificate"); + try { + + // Create a trust manager that does not validate certificate chains + final TrustManager[] trustAllCerts = + new TrustManager[] { + new X509TrustManager() { + @Override + public void checkClientTrusted( + java.security.cert.X509Certificate[] chain, String authType) { + // No implementation needed for ignoring SSL certificate validation + } + + @Override + public void checkServerTrusted( + java.security.cert.X509Certificate[] chain, String authType) { + // No implementation needed for ignoring SSL certificate validation + } + + @Override + public java.security.cert.X509Certificate[] getAcceptedIssuers() { + return new java.security.cert.X509Certificate[] {}; + } + } + }; + + // Install the all-trusting trust manager + final SSLContext sslContext = SSLContext.getInstance("SSL"); + sslContext.init(null, trustAllCerts, new java.security.SecureRandom()); + // Create a ssl socket factory with our all-trusting manager + final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory(); + + builder.sslSocketFactory(sslSocketFactory, (X509TrustManager) trustAllCerts[0]); + builder.hostnameVerifier((hostname, session) -> true); + } catch (Exception e) { + log.warn("Exception while configuring IgnoreSslCertificate: {}", e.getMessage(), e); + } + return builder; + } + + public void removeAllInterceptors() { + interceptors.clear(); + safeClient = createSafeClient(); + unsafeClient = createUnsafeClient(); + } + + public void addInterceptor(Interceptor interceptor) { + if (!interceptors.contains(interceptor)) { + interceptors.add(interceptor); + safeClient = createSafeClient(); + unsafeClient = createUnsafeClient(); + } + } + + public void addInterceptors(Interceptor... interceptor) { + for (Interceptor i : interceptor) { + if (!interceptors.contains(i)) { + interceptors.add(i); + } + } + safeClient = createSafeClient(); + unsafeClient = createUnsafeClient(); + } + + public void removeInterceptor(Interceptor interceptor) { + if (interceptors.contains(interceptor)) { + interceptors.remove(interceptor); + safeClient = createSafeClient(); + unsafeClient = createUnsafeClient(); + } + } +} diff --git a/src/main/java/org/sitmun/proxy/middleware/decorator/HttpContext.java b/src/main/java/org/sitmun/proxy/middleware/protocols/http/HttpContext.java similarity index 61% rename from src/main/java/org/sitmun/proxy/middleware/decorator/HttpContext.java rename to src/main/java/org/sitmun/proxy/middleware/protocols/http/HttpContext.java index 66a6fc4..0d148d1 100644 --- a/src/main/java/org/sitmun/proxy/middleware/decorator/HttpContext.java +++ b/src/main/java/org/sitmun/proxy/middleware/protocols/http/HttpContext.java @@ -1,6 +1,7 @@ -package org.sitmun.proxy.middleware.decorator; +package org.sitmun.proxy.middleware.protocols.http; import java.util.Map; +import org.sitmun.proxy.middleware.decorator.Context; public interface HttpContext extends Context { HttpContextSecurity getSecurity(); diff --git a/src/main/java/org/sitmun/proxy/middleware/decorator/HttpContextSecurity.java b/src/main/java/org/sitmun/proxy/middleware/protocols/http/HttpContextSecurity.java similarity index 63% rename from src/main/java/org/sitmun/proxy/middleware/decorator/HttpContextSecurity.java rename to src/main/java/org/sitmun/proxy/middleware/protocols/http/HttpContextSecurity.java index 2fdbf65..1459dd7 100644 --- a/src/main/java/org/sitmun/proxy/middleware/decorator/HttpContextSecurity.java +++ b/src/main/java/org/sitmun/proxy/middleware/protocols/http/HttpContextSecurity.java @@ -1,8 +1,7 @@ -package org.sitmun.proxy.middleware.decorator; +package org.sitmun.proxy.middleware.protocols.http; public interface HttpContextSecurity { String getUsername(); String getPassword(); - } diff --git a/src/main/java/org/sitmun/proxy/middleware/decorator/request/HttpBasicSecurityRequestDecorator.java b/src/main/java/org/sitmun/proxy/middleware/protocols/http/HttpRequestDecoratorAddBasicSecurity.java similarity index 55% rename from src/main/java/org/sitmun/proxy/middleware/decorator/request/HttpBasicSecurityRequestDecorator.java rename to src/main/java/org/sitmun/proxy/middleware/protocols/http/HttpRequestDecoratorAddBasicSecurity.java index 4e1b9fe..6282025 100644 --- a/src/main/java/org/sitmun/proxy/middleware/decorator/request/HttpBasicSecurityRequestDecorator.java +++ b/src/main/java/org/sitmun/proxy/middleware/protocols/http/HttpRequestDecoratorAddBasicSecurity.java @@ -1,24 +1,20 @@ -package org.sitmun.proxy.middleware.decorator.request; +package org.sitmun.proxy.middleware.protocols.http; +import java.util.Base64; import org.sitmun.proxy.middleware.decorator.Context; -import org.sitmun.proxy.middleware.decorator.HttpContext; import org.sitmun.proxy.middleware.decorator.RequestDecorator; -import org.sitmun.proxy.middleware.request.HttpRequest; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; -import java.util.Base64; - @Component -public class HttpBasicSecurityRequestDecorator implements RequestDecorator { +public class HttpRequestDecoratorAddBasicSecurity implements RequestDecorator { @Override public boolean accept(Object target, Context context) { - if (context instanceof HttpContext) { - HttpContext ctx = (HttpContext) context; + if (context instanceof HttpContext ctx) { return ctx.getSecurity() != null - && StringUtils.hasText(ctx.getSecurity().getUsername()) - && StringUtils.hasText(ctx.getSecurity().getPassword()); + && StringUtils.hasText(ctx.getSecurity().getUsername()) + && StringUtils.hasText(ctx.getSecurity().getPassword()); } else { return false; } @@ -26,9 +22,14 @@ public boolean accept(Object target, Context context) { @Override public void addBehavior(Object target, Context context) { - HttpRequest request = (HttpRequest) target; + HttpRequestExecutor request = (HttpRequestExecutor) target; HttpContext httpContext = (HttpContext) context; - String authString = httpContext.getSecurity().getUsername().concat(":").concat(httpContext.getSecurity().getPassword()); + String authString = + httpContext + .getSecurity() + .getUsername() + .concat(":") + .concat(httpContext.getSecurity().getPassword()); String authEncode = encodeAuthorization(authString); request.setHeader("Authorization", "Basic ".concat(authEncode)); } diff --git a/src/main/java/org/sitmun/proxy/middleware/decorator/request/HttpUriDecorator.java b/src/main/java/org/sitmun/proxy/middleware/protocols/http/HttpRequestDecoratorAddEndpoint.java similarity index 62% rename from src/main/java/org/sitmun/proxy/middleware/decorator/request/HttpUriDecorator.java rename to src/main/java/org/sitmun/proxy/middleware/protocols/http/HttpRequestDecoratorAddEndpoint.java index fc3e648..8b4112d 100644 --- a/src/main/java/org/sitmun/proxy/middleware/decorator/request/HttpUriDecorator.java +++ b/src/main/java/org/sitmun/proxy/middleware/protocols/http/HttpRequestDecoratorAddEndpoint.java @@ -1,18 +1,15 @@ -package org.sitmun.proxy.middleware.decorator.request; +package org.sitmun.proxy.middleware.protocols.http; import org.sitmun.proxy.middleware.decorator.Context; -import org.sitmun.proxy.middleware.decorator.HttpContext; import org.sitmun.proxy.middleware.decorator.RequestDecorator; -import org.sitmun.proxy.middleware.request.HttpRequest; import org.springframework.stereotype.Component; @Component -public class HttpUriDecorator implements RequestDecorator { +public class HttpRequestDecoratorAddEndpoint implements RequestDecorator { @Override public boolean accept(Object target, Context context) { - if (context instanceof HttpContext) { - HttpContext httpContext = (HttpContext) context; + if (context instanceof HttpContext httpContext) { return httpContext.getParameters() != null && !httpContext.getParameters().isEmpty(); } else { return false; @@ -21,10 +18,9 @@ public boolean accept(Object target, Context context) { @Override public void addBehavior(Object target, Context context) { - HttpRequest request = (HttpRequest) target; + HttpRequestExecutor request = (HttpRequestExecutor) target; HttpContext httpContext = (HttpContext) context; request.setUrl(httpContext.getUri()); request.setParameters(httpContext.getParameters()); } - } diff --git a/src/main/java/org/sitmun/proxy/middleware/protocols/http/HttpRequestExecutor.java b/src/main/java/org/sitmun/proxy/middleware/protocols/http/HttpRequestExecutor.java new file mode 100644 index 0000000..42f8cff --- /dev/null +++ b/src/main/java/org/sitmun/proxy/middleware/protocols/http/HttpRequestExecutor.java @@ -0,0 +1,127 @@ +package org.sitmun.proxy.middleware.protocols.http; + +import java.io.IOException; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import okhttp3.ResponseBody; +import org.sitmun.proxy.middleware.dto.ErrorResponseDto; +import org.sitmun.proxy.middleware.service.RequestExecutor; +import org.sitmun.proxy.middleware.service.RequestExecutorResponse; +import org.sitmun.proxy.middleware.service.RequestExecutorResponseImpl; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; +import org.springframework.web.util.*; + +@Slf4j +public class HttpRequestExecutor implements RequestExecutor { + + private final Map headers = new HashMap<>(); + private final Map parameters = new HashMap<>(); + private final HttpClient httpClient; + private final String baseUrl; + @Setter private String url; + + public HttpRequestExecutor(String baseUrl, HttpClient httpClient) { + this.baseUrl = baseUrl; + this.httpClient = httpClient; + } + + public void setHeader(String header, String value) { + headers.put(header, value); + } + + public void setParameters(Map parameters) { + this.parameters.putAll(parameters); + } + + @SuppressWarnings("unchecked") + @Override + public RequestExecutorResponse execute() { + if (!StringUtils.hasText(url)) { + throw new IllegalStateException("Url is not set"); + } + + okhttp3.Request.Builder builder = new okhttp3.Request.Builder(); + + builder.url(getUrl()); + headers.keySet().forEach(k -> builder.addHeader(k, headers.get(k))); + + okhttp3.Request httpRequest = builder.build(); + + log.info("Executing request to: {}", httpRequest.url()); + log.info("Method: {}", httpRequest.method()); + log.info("Headers: {}", httpRequest.headers()); + log.info("Base URL: {}", baseUrl); + + try (okhttp3.Response r = httpClient.executeRequest(httpRequest)) { + ResponseBody body = r.body(); + if (body == null) + return new RequestExecutorResponseImpl<>(baseUrl, r.code(), r.header("content-type"), null); + return new RequestExecutorResponseImpl<>( + baseUrl, r.code(), r.header("content-type"), body.bytes()); + } catch (IOException e) { + log.error("Error getting response: {}", e.getMessage(), e); + return new RequestExecutorResponseImpl<>( + baseUrl, + 500, + "application/json", + new ErrorResponseDto( + 500, "ServiceError", "Error with the request to final service", "", new Date())); + } + } + + public String getUrl() { + if (!parameters.isEmpty()) { + UriComponents components = UriComponentsBuilder.fromUriString(url).build(); + + MultiValueMap queryParams = + new LinkedMultiValueMap<>(components.getQueryParams().size()); + + components.getQueryParams().forEach((k, v) -> queryParams.put(k.toUpperCase(), v)); + parameters.forEach( + (k, v) -> { + String upperKey = k.toUpperCase(); + List existingValues = queryParams.get(upperKey); + if (existingValues == null || !existingValues.contains(v)) { + queryParams.add(upperKey, v); + } + }); + + String path = components.getPath() != null ? components.getPath() : ""; + + UriComponentsBuilder builder = + UriComponentsBuilder.newInstance() + .scheme(components.getScheme()) + .host(components.getHost()) + .port(components.getPort()) + .path(path) + .queryParams(queryParams); + + log.info("path: {}", components.getPath()); + log.info("query: {}", queryParams); + + return builder.toUriString(); + } else { + return url; + } + } + + public String describe() { + return "HttpRequest{" + + "url='" + + url + + '\'' + + ", headers=" + + headers + + ", parameters=" + + parameters + + ", baseUrl=" + + baseUrl + + '}'; + } +} diff --git a/src/main/java/org/sitmun/proxy/middleware/decorator/JdbcContext.java b/src/main/java/org/sitmun/proxy/middleware/protocols/jdbc/JdbcContext.java similarity index 60% rename from src/main/java/org/sitmun/proxy/middleware/decorator/JdbcContext.java rename to src/main/java/org/sitmun/proxy/middleware/protocols/jdbc/JdbcContext.java index 70d2d3b..2bf85d6 100644 --- a/src/main/java/org/sitmun/proxy/middleware/decorator/JdbcContext.java +++ b/src/main/java/org/sitmun/proxy/middleware/protocols/jdbc/JdbcContext.java @@ -1,5 +1,6 @@ -package org.sitmun.proxy.middleware.decorator; +package org.sitmun.proxy.middleware.protocols.jdbc; +import org.sitmun.proxy.middleware.decorator.Context; public interface JdbcContext extends Context { String getDriver(); diff --git a/src/main/java/org/sitmun/proxy/middleware/protocols/jdbc/JdbcPayloadDto.java b/src/main/java/org/sitmun/proxy/middleware/protocols/jdbc/JdbcPayloadDto.java new file mode 100644 index 0000000..1608a3d --- /dev/null +++ b/src/main/java/org/sitmun/proxy/middleware/protocols/jdbc/JdbcPayloadDto.java @@ -0,0 +1,54 @@ +package org.sitmun.proxy.middleware.protocols.jdbc; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import java.util.List; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.sitmun.proxy.middleware.dto.PayloadDto; + +@Getter +@Setter +@JsonTypeName("DatasourcePayload") +@NoArgsConstructor +public class JdbcPayloadDto extends PayloadDto implements JdbcContext { + + private String uri; + private String user; + private String password; + private String driver; + private String sql; + + @Builder + public JdbcPayloadDto( + List vary, String uri, String user, String password, String driver, String sql) { + super(vary); + this.uri = uri; + this.user = user; + this.password = password; + this.driver = driver; + this.sql = sql; + } + + @Override + public String describe() { + return "DatasourcePayloadDto{" + + "vary=" + + getVary() + + ", uri='" + + uri + + '\'' + + ", user='" + + user + + '\'' + + ", password='****'" + + ", driver='" + + driver + + '\'' + + ", sql='" + + sql + + '\'' + + '}'; + } +} diff --git a/src/main/java/org/sitmun/proxy/middleware/decorator/request/JdbcConnectionRequestDecorator.java b/src/main/java/org/sitmun/proxy/middleware/protocols/jdbc/JdbcRequestDecoratorAddConnection.java similarity index 71% rename from src/main/java/org/sitmun/proxy/middleware/decorator/request/JdbcConnectionRequestDecorator.java rename to src/main/java/org/sitmun/proxy/middleware/protocols/jdbc/JdbcRequestDecoratorAddConnection.java index 36b150a..c22aa2b 100644 --- a/src/main/java/org/sitmun/proxy/middleware/decorator/request/JdbcConnectionRequestDecorator.java +++ b/src/main/java/org/sitmun/proxy/middleware/protocols/jdbc/JdbcRequestDecoratorAddConnection.java @@ -1,20 +1,16 @@ -package org.sitmun.proxy.middleware.decorator.request; +package org.sitmun.proxy.middleware.protocols.jdbc; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; import lombok.extern.slf4j.Slf4j; import org.sitmun.proxy.middleware.decorator.Context; -import org.sitmun.proxy.middleware.decorator.JdbcContext; import org.sitmun.proxy.middleware.decorator.RequestDecorator; -import org.sitmun.proxy.middleware.request.JdbcRequest; import org.springframework.stereotype.Component; -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.SQLException; - - @Slf4j @Component -public class JdbcConnectionRequestDecorator implements RequestDecorator { +public class JdbcRequestDecoratorAddConnection implements RequestDecorator { @Override public boolean accept(Object target, Context context) { @@ -23,7 +19,7 @@ public boolean accept(Object target, Context context) { @Override public void addBehavior(Object target, Context context) { - JdbcRequest request = (JdbcRequest) target; + JdbcRequestExecutor request = (JdbcRequestExecutor) target; JdbcContext jdbcContext = (JdbcContext) context; request.setConnection(getConnection(jdbcContext)); } @@ -33,7 +29,8 @@ private Connection getConnection(JdbcContext context) { Connection connection = null; try { Class.forName(context.getDriver()); - connection = DriverManager.getConnection(context.getUri(), context.getUser(), context.getPassword()); + connection = + DriverManager.getConnection(context.getUri(), context.getUser(), context.getPassword()); } catch (SQLException | ClassNotFoundException e) { log.error("Error getting connection: {}", e.getMessage(), e); } diff --git a/src/main/java/org/sitmun/proxy/middleware/decorator/request/JdbcQueryRequestDecorator.java b/src/main/java/org/sitmun/proxy/middleware/protocols/jdbc/JdbcRequestDecoratorAddQuery.java similarity index 62% rename from src/main/java/org/sitmun/proxy/middleware/decorator/request/JdbcQueryRequestDecorator.java rename to src/main/java/org/sitmun/proxy/middleware/protocols/jdbc/JdbcRequestDecoratorAddQuery.java index 03e1825..bc9565f 100644 --- a/src/main/java/org/sitmun/proxy/middleware/decorator/request/JdbcQueryRequestDecorator.java +++ b/src/main/java/org/sitmun/proxy/middleware/protocols/jdbc/JdbcRequestDecoratorAddQuery.java @@ -1,13 +1,11 @@ -package org.sitmun.proxy.middleware.decorator.request; +package org.sitmun.proxy.middleware.protocols.jdbc; import org.sitmun.proxy.middleware.decorator.Context; -import org.sitmun.proxy.middleware.decorator.JdbcContext; import org.sitmun.proxy.middleware.decorator.RequestDecorator; -import org.sitmun.proxy.middleware.request.JdbcRequest; import org.springframework.stereotype.Component; @Component -public class JdbcQueryRequestDecorator implements RequestDecorator { +public class JdbcRequestDecoratorAddQuery implements RequestDecorator { @Override public boolean accept(Object target, Context context) { @@ -16,9 +14,8 @@ public boolean accept(Object target, Context context) { @Override public void addBehavior(Object target, Context context) { - JdbcRequest request = (JdbcRequest) target; + JdbcRequestExecutor request = (JdbcRequestExecutor) target; JdbcContext jdbcContext = (JdbcContext) context; request.setSql(jdbcContext.getSql()); } - } diff --git a/src/main/java/org/sitmun/proxy/middleware/request/JdbcRequest.java b/src/main/java/org/sitmun/proxy/middleware/protocols/jdbc/JdbcRequestExecutor.java similarity index 64% rename from src/main/java/org/sitmun/proxy/middleware/request/JdbcRequest.java rename to src/main/java/org/sitmun/proxy/middleware/protocols/jdbc/JdbcRequestExecutor.java index 301a169..f76c315 100644 --- a/src/main/java/org/sitmun/proxy/middleware/request/JdbcRequest.java +++ b/src/main/java/org/sitmun/proxy/middleware/protocols/jdbc/JdbcRequestExecutor.java @@ -1,35 +1,37 @@ -package org.sitmun.proxy.middleware.request; - - -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; -import org.sitmun.proxy.middleware.decorator.DecoratedRequest; -import org.sitmun.proxy.middleware.decorator.DecoratedResponse; -import org.sitmun.proxy.middleware.dto.ErrorResponseDTO; -import org.sitmun.proxy.middleware.response.Response; +package org.sitmun.proxy.middleware.protocols.jdbc; import java.sql.*; -import java.util.Date; import java.util.*; +import java.util.Date; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.sitmun.proxy.middleware.dto.ErrorResponseDto; +import org.sitmun.proxy.middleware.service.RequestExecutor; +import org.sitmun.proxy.middleware.service.RequestExecutorResponse; +import org.sitmun.proxy.middleware.service.RequestExecutorResponseImpl; @Setter @Slf4j -public class JdbcRequest implements DecoratedRequest { +public class JdbcRequestExecutor implements RequestExecutor { private Connection connection; private String sql; @SuppressWarnings("unchecked") @Override - public DecoratedResponse execute() { + public RequestExecutorResponse execute() { List> result = new ArrayList<>(); try (Connection connectionUsed = connection) { executeStatement(connectionUsed, result); } catch (SQLException e) { log.error("Error getting response: {}", e.getMessage(), e); - return new Response<>(null, 500, "application/json", new ErrorResponseDTO(500, "SQLError", e.getMessage(), "", new Date())); + return new RequestExecutorResponseImpl<>( + null, + 500, + "application/json", + new ErrorResponseDto(500, "SQLError", e.getMessage(), "", new Date())); } - return new Response<>(null, 200, "application/json", result); + return new RequestExecutorResponseImpl<>(null, 200, "application/json", result); } private void executeStatement(Connection connection, List> result) { @@ -57,9 +59,6 @@ private void retrieveResultSetMetadata(Statement stmt, List> } public String describe() { - return "JdbcRequest{" + - "connection=" + connection + - ", sql='" + sql + '\'' + - '}'; + return "JdbcRequest{" + "connection=" + connection + ", sql='" + sql + '\'' + '}'; } } diff --git a/src/main/java/org/sitmun/proxy/middleware/protocols/wms/WmsCapabilitiesResponseDecorator.java b/src/main/java/org/sitmun/proxy/middleware/protocols/wms/WmsCapabilitiesResponseDecorator.java new file mode 100644 index 0000000..bc1e221 --- /dev/null +++ b/src/main/java/org/sitmun/proxy/middleware/protocols/wms/WmsCapabilitiesResponseDecorator.java @@ -0,0 +1,42 @@ +package org.sitmun.proxy.middleware.protocols.wms; + +import java.nio.charset.StandardCharsets; +import lombok.extern.slf4j.Slf4j; +import org.sitmun.proxy.middleware.decorator.Context; +import org.sitmun.proxy.middleware.decorator.ResponseDecorator; +import org.sitmun.proxy.middleware.service.RequestExecutorResponseImpl; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +public class WmsCapabilitiesResponseDecorator implements ResponseDecorator { + + @Override + public boolean accept(Object target, Context context) { + if (context instanceof WmsPayloadDto wmsPayloadDto) { + return wmsPayloadDto + .getParameters() + .getOrDefault("REQUEST", "") + .equalsIgnoreCase("GetCapabilities"); + } + return false; + } + + @Override + public void addBehavior(Object response, Context context) { + log.info("Adding behavior to response {} of {}", response, context); + if (context instanceof WmsPayloadDto wmsPayloadDto) { + //noinspection unchecked + RequestExecutorResponseImpl requestExecutionResponseImpl1 = + (RequestExecutorResponseImpl) response; + String s = new String(requestExecutionResponseImpl1.getBody(), StandardCharsets.UTF_8); + String output = + s.replaceAll(wmsPayloadDto.getUri(), requestExecutionResponseImpl1.getBaseUrl()); + log.info( + "Replacement of {} by {} in GetCapabilities response", + wmsPayloadDto.getUri(), + requestExecutionResponseImpl1.getBaseUrl()); + requestExecutionResponseImpl1.setBody(output.getBytes(StandardCharsets.UTF_8)); + } + } +} diff --git a/src/main/java/org/sitmun/proxy/middleware/protocols/wms/WmsPayloadDto.java b/src/main/java/org/sitmun/proxy/middleware/protocols/wms/WmsPayloadDto.java new file mode 100644 index 0000000..bb1ded0 --- /dev/null +++ b/src/main/java/org/sitmun/proxy/middleware/protocols/wms/WmsPayloadDto.java @@ -0,0 +1,56 @@ +package org.sitmun.proxy.middleware.protocols.wms; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import java.util.List; +import java.util.Map; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.sitmun.proxy.middleware.dto.HttpSecurityDto; +import org.sitmun.proxy.middleware.dto.PayloadDto; +import org.sitmun.proxy.middleware.protocols.http.HttpContext; + +@Getter +@Setter +@JsonTypeName("OgcWmsPayload") +@NoArgsConstructor +public class WmsPayloadDto extends PayloadDto implements HttpContext { + + private String uri; + private String method; + private Map parameters; + private HttpSecurityDto security; + + @Builder + public WmsPayloadDto( + List vary, + String uri, + String method, + Map parameters, + HttpSecurityDto security) { + super(vary); + this.uri = uri; + this.method = method; + this.parameters = parameters; + this.security = security; + } + + @Override + public String describe() { + return "OgcWmsPayloadDto{" + + "vary=" + + getVary() + + ", uri='" + + uri + + '\'' + + ", method='" + + method + + '\'' + + ", parameters=" + + parameters + + ", security=" + + security + + '}'; + } +} diff --git a/src/main/java/org/sitmun/proxy/middleware/request/HttpRequest.java b/src/main/java/org/sitmun/proxy/middleware/request/HttpRequest.java deleted file mode 100644 index 4140ac6..0000000 --- a/src/main/java/org/sitmun/proxy/middleware/request/HttpRequest.java +++ /dev/null @@ -1,115 +0,0 @@ -package org.sitmun.proxy.middleware.request; - -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; -import okhttp3.ResponseBody; -import org.sitmun.proxy.middleware.decorator.DecoratedRequest; -import org.sitmun.proxy.middleware.decorator.DecoratedResponse; -import org.sitmun.proxy.middleware.dto.ErrorResponseDTO; -import org.sitmun.proxy.middleware.response.Response; -import org.sitmun.proxy.middleware.service.ClientService; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; -import org.springframework.util.StringUtils; -import org.springframework.web.util.*; - -import java.io.IOException; -import java.util.Date; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; - -@Slf4j -public class HttpRequest implements DecoratedRequest { - - private final Map headers = new HashMap<>(); - private final Map parameters = new HashMap<>(); - private final ClientService clientService; - private final String baseUrl; - @Setter - private String url; - - public HttpRequest(String baseUrl, ClientService clientService) { - this.baseUrl = baseUrl; - this.clientService = clientService; - } - - public void setHeader(String header, String value) { - headers.put(header, value); - } - - public void setParameters(Map parameters) { - this.parameters.putAll(parameters); - } - - @SuppressWarnings("unchecked") - @Override - public DecoratedResponse execute() { - if (!StringUtils.hasText(url)) { - throw new IllegalStateException("Url is not set"); - } - - okhttp3.Request.Builder builder = new okhttp3.Request.Builder(); - - builder.url(getUrl()); - headers.keySet().forEach(k -> builder.addHeader(k, headers.get(k))); - - okhttp3.Request httpRequest = builder.build(); - - log.info("Executing request to: {}", httpRequest.url()); - log.info("Method: {}", httpRequest.method()); - log.info("Headers: {}", httpRequest.headers()); - log.info("Base URL: {}", baseUrl); - - try (okhttp3.Response r = clientService.executeRequest(httpRequest)) { - ResponseBody body = r.body(); - if (body == null) return new Response<>(baseUrl, r.code(), r.header("content-type"), null); - return new Response<>(baseUrl, r.code(), r.header("content-type"), body.bytes()); - } catch (IOException e) { - log.error("Error getting response: {}", e.getMessage(), e); - return new Response<>(baseUrl, 500, "application/json", new ErrorResponseDTO(500, "ServiceError", "Error with the request to final service", "", new Date())); - } - } - - public String getUrl() { - if (!parameters.isEmpty()) { - UriComponents components = UriComponentsBuilder.fromUriString(url).build(); - - MultiValueMap queryParams = new LinkedMultiValueMap<>(components.getQueryParams().size()); - - components.getQueryParams().forEach((k, v) -> queryParams.put(k.toUpperCase(), v)); - parameters.forEach((k, v) -> { - String upperKey = k.toUpperCase(); - if ( - !queryParams.containsKey(upperKey) || - queryParams.get(upperKey) == null || - !queryParams.get(upperKey).contains(v)) { - queryParams.add(upperKey, v); - } - }); - - UriComponentsBuilder builder = UriComponentsBuilder.newInstance() - .scheme(components.getScheme()) - .host(components.getHost()) - .port(components.getPort()) - .path(components.getPath()) - .queryParams(queryParams); - - log.info("path: {}", components.getPath()); - log.info("query: {}", queryParams); - - return builder.toUriString(); - } else { - return url; - } - } - - public String describe() { - return "HttpRequest{" + - "url='" + url + '\'' + - ", headers=" + headers + - ", parameters=" + parameters + - ", baseUrl=" + baseUrl + - '}'; - } -} diff --git a/src/main/java/org/sitmun/proxy/middleware/request/RequestFactory.java b/src/main/java/org/sitmun/proxy/middleware/request/RequestFactory.java deleted file mode 100644 index f86bf9c..0000000 --- a/src/main/java/org/sitmun/proxy/middleware/request/RequestFactory.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.sitmun.proxy.middleware.request; - -import org.sitmun.proxy.middleware.decorator.Context; -import org.sitmun.proxy.middleware.decorator.DecoratedRequest; -import org.sitmun.proxy.middleware.decorator.HttpContext; -import org.sitmun.proxy.middleware.decorator.JdbcContext; -import org.sitmun.proxy.middleware.service.ClientService; -import org.springframework.stereotype.Component; - -@Component -public class RequestFactory { - - private final ClientService clientService; - - public RequestFactory(ClientService clientService) { - this.clientService = clientService; - } - - public DecoratedRequest create(String baseUrl, Context context) { - if (context instanceof HttpContext) { - return new HttpRequest(baseUrl, clientService); - } else if (context instanceof JdbcContext) { - return new JdbcRequest(); - } else { - throw new IllegalArgumentException("Payload type not supported"); - } - } -} diff --git a/src/main/java/org/sitmun/proxy/middleware/service/HttpClientFactory.java b/src/main/java/org/sitmun/proxy/middleware/service/HttpClientFactory.java deleted file mode 100644 index 8dfda01..0000000 --- a/src/main/java/org/sitmun/proxy/middleware/service/HttpClientFactory.java +++ /dev/null @@ -1,85 +0,0 @@ -package org.sitmun.proxy.middleware.service; - -import lombok.extern.slf4j.Slf4j; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; - -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.TrustManager; -import javax.net.ssl.X509TrustManager; -import java.io.IOException; -import java.util.List; - -@Service -@Slf4j -public class HttpClientFactory implements ClientService { - - private final List unsafeAllowedHosts; - private final OkHttpClient safeClient; - private final OkHttpClient unsafeClient; - - public HttpClientFactory(@Value("${sitmun.client.unsafe-allowed-hosts:*}") List unsafeAllowedHosts) { - this.unsafeAllowedHosts = unsafeAllowedHosts; - safeClient = new OkHttpClient.Builder().build(); - unsafeClient = configureToIgnoreCertificate(new OkHttpClient.Builder()).build(); - } - - @Override - public Response executeRequest(Request httpRequest) throws IOException { - return getClient(httpRequest.url().host()).newCall(httpRequest).execute(); - } - - public OkHttpClient getClient(String host) { - try { - if (unsafeAllowedHosts.contains("*") || unsafeAllowedHosts.contains(host)) { - log.warn("Using Unsafe Client"); - return unsafeClient; - } else { - return safeClient; - } - } catch (Exception e) { - log.warn("Exception while creating client: "+e.getMessage(), e); - return safeClient; - } - } - - private static OkHttpClient.Builder configureToIgnoreCertificate(OkHttpClient.Builder builder) { - log.warn("Ignore SSL Certificate"); - try { - - // Create a trust manager that does not validate certificate chains - final TrustManager[] trustAllCerts = new TrustManager[] { - new X509TrustManager() { - @Override - public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) { - } - - @Override - public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) { - } - - @Override - public java.security.cert.X509Certificate[] getAcceptedIssuers() { - return new java.security.cert.X509Certificate[]{}; - } - } - }; - - // Install the all-trusting trust manager - final SSLContext sslContext = SSLContext.getInstance("SSL"); - sslContext.init(null, trustAllCerts, new java.security.SecureRandom()); - // Create a ssl socket factory with our all-trusting manager - final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory(); - - builder.sslSocketFactory(sslSocketFactory, (X509TrustManager)trustAllCerts[0]); - builder.hostnameVerifier((hostname, session) -> true); - } catch (Exception e) { - log.warn("Exception while configuring IgnoreSslCertificate: "+e.getMessage(), e); - } - return builder; - } -} diff --git a/src/main/java/org/sitmun/proxy/middleware/service/ProxyMiddlewareService.java b/src/main/java/org/sitmun/proxy/middleware/service/RequestConfigurationService.java similarity index 57% rename from src/main/java/org/sitmun/proxy/middleware/service/ProxyMiddlewareService.java rename to src/main/java/org/sitmun/proxy/middleware/service/RequestConfigurationService.java index 8d5fea9..11c35a8 100644 --- a/src/main/java/org/sitmun/proxy/middleware/service/ProxyMiddlewareService.java +++ b/src/main/java/org/sitmun/proxy/middleware/service/RequestConfigurationService.java @@ -1,9 +1,13 @@ package org.sitmun.proxy.middleware.service; +import static org.sitmun.proxy.middleware.utils.LoggerUtils.logAsPrettyJson; + +import java.util.Date; +import java.util.Map; import lombok.extern.slf4j.Slf4j; import org.sitmun.proxy.middleware.dto.ConfigProxyDto; -import org.sitmun.proxy.middleware.dto.ConfigProxyRequest; -import org.sitmun.proxy.middleware.dto.ErrorResponseDTO; +import org.sitmun.proxy.middleware.dto.ConfigProxyRequestDto; +import org.sitmun.proxy.middleware.dto.ErrorResponseDto; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; @@ -13,44 +17,49 @@ import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; -import java.util.Date; -import java.util.Map; - -import static org.sitmun.proxy.middleware.utils.LoggerUtils.logAsPrettyJson; - @Service @Slf4j -public class ProxyMiddlewareService { +public class RequestConfigurationService { private final RestTemplate restTemplate; - private final GlobalRequestService globalRequestService; + private final RequestExecutorService requestExecutorService; + @Value("${sitmun.backend.config.url}") private String configUrl; + @Value("${sitmun.backend.config.secret}") private String secret; - public ProxyMiddlewareService(RestTemplate restTemplate, GlobalRequestService globalRequestService) { + public RequestConfigurationService( + RestTemplate restTemplate, RequestExecutorService requestExecutorService) { this.restTemplate = restTemplate; - this.globalRequestService = globalRequestService; + this.requestExecutorService = requestExecutorService; } - public ResponseEntity doRequest(Integer appId, Integer terId, String type, - Integer typeId, String token, Map params, - String url) { - ConfigProxyRequest configProxyRequest = new ConfigProxyRequest(appId, terId, type, typeId, "GET", params, null, token); + public ResponseEntity doRequest( + Integer appId, + Integer terId, + String type, + Integer typeId, + String token, + Map params, + String url) { + ConfigProxyRequestDto configProxyRequest = + new ConfigProxyRequestDto(appId, terId, type, typeId, "GET", params, null, token); logAsPrettyJson(log, "Request to the API:\n{}", configProxyRequest); ResponseEntity response = configRequest(configProxyRequest); - if (response.getStatusCodeValue() == 200) { + if (response.getStatusCode().value() == 200) { ConfigProxyDto configProxyDto = (ConfigProxyDto) response.getBody(); logAsPrettyJson(log, "Response from the API:\n{}", configProxyDto); if (configProxyDto != null) { log.info("Requesting data from the final service"); - return globalRequestService.executeRequest(url, configProxyDto.getPayload()); + return requestExecutorService.executeRequest(url, configProxyDto.getPayload()); } else { - ErrorResponseDTO errorResponse = new ErrorResponseDTO(401, "Bad Request", "Request not valid", configUrl, new Date()); + ErrorResponseDto errorResponse = + new ErrorResponseDto(401, "Bad Request", "Request not valid", configUrl, new Date()); return ResponseEntity.status(401).body(errorResponse); } } else { @@ -58,19 +67,22 @@ public ResponseEntity doRequest(Integer appId, Integer terId, String type, } } - private ResponseEntity configRequest(ConfigProxyRequest configRequest) { + private ResponseEntity configRequest(ConfigProxyRequestDto configRequest) { HttpHeaders requestHeaders = new HttpHeaders(); requestHeaders.add("X-SITMUN-Proxy-Key", this.secret); - HttpEntity httpEntity = new HttpEntity<>(configRequest, requestHeaders); + HttpEntity httpEntity = new HttpEntity<>(configRequest, requestHeaders); try { return restTemplate.exchange(configUrl, HttpMethod.POST, httpEntity, ConfigProxyDto.class); } catch (HttpClientErrorException e) { log.error("Error getting response: {}", e.getMessage(), e); - ErrorResponseDTO errorResponse = new ErrorResponseDTO(e.getRawStatusCode(), "", e.getMessage(), configUrl, new Date()); + ErrorResponseDto errorResponse = + new ErrorResponseDto( + e.getStatusCode().value(), "", e.getMessage(), configUrl, new Date()); return ResponseEntity.status(e.getStatusCode()).body(errorResponse); } catch (Exception e) { log.error("Error getting response: {}", e.getMessage(), e); - ErrorResponseDTO errorResponse = new ErrorResponseDTO(500, "", e.getMessage(), configUrl, new Date()); + ErrorResponseDto errorResponse = + new ErrorResponseDto(500, "", e.getMessage(), configUrl, new Date()); return ResponseEntity.status(500).body(errorResponse); } } diff --git a/src/main/java/org/sitmun/proxy/middleware/service/RequestExecutor.java b/src/main/java/org/sitmun/proxy/middleware/service/RequestExecutor.java new file mode 100644 index 0000000..902f07b --- /dev/null +++ b/src/main/java/org/sitmun/proxy/middleware/service/RequestExecutor.java @@ -0,0 +1,7 @@ +package org.sitmun.proxy.middleware.service; + +public interface RequestExecutor { + RequestExecutorResponse execute(); + + String describe(); +} diff --git a/src/main/java/org/sitmun/proxy/middleware/service/RequestExecutorFactory.java b/src/main/java/org/sitmun/proxy/middleware/service/RequestExecutorFactory.java new file mode 100644 index 0000000..392bb9f --- /dev/null +++ b/src/main/java/org/sitmun/proxy/middleware/service/RequestExecutorFactory.java @@ -0,0 +1,29 @@ +package org.sitmun.proxy.middleware.service; + +import org.sitmun.proxy.middleware.decorator.Context; +import org.sitmun.proxy.middleware.protocols.http.HttpClient; +import org.sitmun.proxy.middleware.protocols.http.HttpContext; +import org.sitmun.proxy.middleware.protocols.http.HttpRequestExecutor; +import org.sitmun.proxy.middleware.protocols.jdbc.JdbcContext; +import org.sitmun.proxy.middleware.protocols.jdbc.JdbcRequestExecutor; +import org.springframework.stereotype.Component; + +@Component +public class RequestExecutorFactory { + + private final HttpClient httpClient; + + public RequestExecutorFactory(HttpClient httpClient) { + this.httpClient = httpClient; + } + + public RequestExecutor create(String baseUrl, Context context) { + if (context instanceof HttpContext) { + return new HttpRequestExecutor(baseUrl, httpClient); + } else if (context instanceof JdbcContext) { + return new JdbcRequestExecutor(); + } else { + throw new IllegalArgumentException("Payload type not supported"); + } + } +} diff --git a/src/main/java/org/sitmun/proxy/middleware/decorator/DecoratedResponse.java b/src/main/java/org/sitmun/proxy/middleware/service/RequestExecutorResponse.java similarity index 50% rename from src/main/java/org/sitmun/proxy/middleware/decorator/DecoratedResponse.java rename to src/main/java/org/sitmun/proxy/middleware/service/RequestExecutorResponse.java index cd9e921..15791f7 100644 --- a/src/main/java/org/sitmun/proxy/middleware/decorator/DecoratedResponse.java +++ b/src/main/java/org/sitmun/proxy/middleware/service/RequestExecutorResponse.java @@ -1,7 +1,7 @@ -package org.sitmun.proxy.middleware.decorator; +package org.sitmun.proxy.middleware.service; import org.springframework.http.ResponseEntity; -public interface DecoratedResponse { +public interface RequestExecutorResponse { ResponseEntity asResponseEntity(); } diff --git a/src/main/java/org/sitmun/proxy/middleware/response/Response.java b/src/main/java/org/sitmun/proxy/middleware/service/RequestExecutorResponseImpl.java similarity index 52% rename from src/main/java/org/sitmun/proxy/middleware/response/Response.java rename to src/main/java/org/sitmun/proxy/middleware/service/RequestExecutorResponseImpl.java index 6fdb314..1c1eb29 100644 --- a/src/main/java/org/sitmun/proxy/middleware/response/Response.java +++ b/src/main/java/org/sitmun/proxy/middleware/service/RequestExecutorResponseImpl.java @@ -1,19 +1,18 @@ -package org.sitmun.proxy.middleware.response; +package org.sitmun.proxy.middleware.service; import lombok.Data; -import org.sitmun.proxy.middleware.decorator.DecoratedResponse; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @Data -public class Response implements DecoratedResponse { +public class RequestExecutorResponseImpl implements RequestExecutorResponse { private String baseUrl; private int statusCode; private String contentType; private T body; - public Response(String baseUrl, int statusCode, String contentType, T body) { + public RequestExecutorResponseImpl(String baseUrl, int statusCode, String contentType, T body) { this.baseUrl = baseUrl; this.statusCode = statusCode; this.contentType = contentType; @@ -22,10 +21,8 @@ public Response(String baseUrl, int statusCode, String contentType, T body) { @Override public ResponseEntity asResponseEntity() { - return ResponseEntity - .status(statusCode) - .contentType(MediaType.parseMediaType(contentType)) - .body(body); + return ResponseEntity.status(statusCode) + .contentType(MediaType.parseMediaType(contentType)) + .body(body); } } - diff --git a/src/main/java/org/sitmun/proxy/middleware/service/GlobalRequestService.java b/src/main/java/org/sitmun/proxy/middleware/service/RequestExecutorService.java similarity index 61% rename from src/main/java/org/sitmun/proxy/middleware/service/GlobalRequestService.java rename to src/main/java/org/sitmun/proxy/middleware/service/RequestExecutorService.java index f91b834..31a6be0 100644 --- a/src/main/java/org/sitmun/proxy/middleware/service/GlobalRequestService.java +++ b/src/main/java/org/sitmun/proxy/middleware/service/RequestExecutorService.java @@ -1,31 +1,30 @@ package org.sitmun.proxy.middleware.service; +import java.util.List; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.sitmun.proxy.middleware.decorator.*; -import org.sitmun.proxy.middleware.request.RequestFactory; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; -import java.util.List; - @Service @Slf4j -public class GlobalRequestService { +public class RequestExecutorService { - private final RequestFactory requestFactory; + private final RequestExecutorFactory requestExecutorFactory; private final List requestDecorators; private final List responseDecorators; - @Getter - private DecoratedRequest lastRequest; - @Getter - private DecoratedResponse lastResponse; - @Getter - private Context lastContext; - public GlobalRequestService(RequestFactory requestFactory, List requestDecorators, List responseDecorators) { - this.requestFactory = requestFactory; + @Getter private RequestExecutor lastRequestExecutor; + @Getter private RequestExecutorResponse lastResponse; + @Getter private Context lastContext; + + public RequestExecutorService( + RequestExecutorFactory requestExecutorFactory, + List requestDecorators, + List responseDecorators) { + this.requestExecutorFactory = requestExecutorFactory; this.requestDecorators = requestDecorators; this.responseDecorators = responseDecorators; } @@ -34,16 +33,16 @@ public ResponseEntity executeRequest(String baseUrl, Context context) { lastContext = context; log.info("Executing request with context: {}", context.describe()); - DecoratedRequest request = requestFactory.create(baseUrl, context); + RequestExecutor request = requestExecutorFactory.create(baseUrl, context); log.info("Default request: {}", request.describe()); requestDecorators.forEach(d -> d.apply(request, context)); log.info("Final request: {}", request.describe()); - lastRequest = request; + lastRequestExecutor = request; log.info("Executing request after applying context: {}", context.describe()); - DecoratedResponse response = request.execute(); + RequestExecutorResponse response = request.execute(); responseDecorators.forEach(d -> d.apply(response, context)); lastResponse = response; return response.asResponseEntity(); diff --git a/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 0000000..a4ad686 --- /dev/null +++ b/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -0,0 +1,22 @@ +{ + "properties": [ + { + "name": "sitmun.client.unsafe-allowed-hosts", + "type": "java.util.List", + "description": "List of hostnames that are allowed to use unsafe SSL connections. Use '*' to allow all hosts.", + "defaultValue": ["*"] + }, + { + "name": "sitmun.backend.config.url", + "type": "java.lang.String", + "description": "URL for the SITMUN backend configuration API endpoint. This is the URL where the proxy middleware will request configuration data.", + "sourceType": "org.sitmun.proxy.middleware.service.RequestConfigurationService" + }, + { + "name": "sitmun.backend.config.secret", + "type": "java.lang.String", + "description": "Secret key for authenticating with the SITMUN backend configuration API. This secret is used in the X-SITMUN-Proxy-Key header.", + "sourceType": "org.sitmun.proxy.middleware.service.RequestConfigurationService" + } + ] +} \ No newline at end of file diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml new file mode 100644 index 0000000..082dbf5 --- /dev/null +++ b/src/main/resources/application-dev.yml @@ -0,0 +1,33 @@ +# Development Profile Configuration +spring: + # DevTools Configuration (only for development) + devtools: + restart: + enabled: true + poll-interval: 2s # Faster polling for better responsiveness + quiet-period: 1s + exclude: + - "**/batch/**" # Exclude batch jobs to prevent restarts during processing + - "**/tilesources/**" # Exclude tile source processing + - "**/io/**" # Exclude MBTiles I/O operations + - "**/config/**" # Exclude configuration classes + - "**/service/**" # Exclude service layer + - "**/utils/**" # Exclude utility classes + - "**/dto/**" # Exclude DTOs + - "**/controllers/**" # Exclude controllers (optional) + livereload: + enabled: true + port: 35729 + + # H2 Console (only for development) + h2: + console: + enabled: true + path: /h2-console + +# Development-specific logging +logging: + level: + org.sitmun.proxy.middleware: DEBUG + org.hibernate.SQL: DEBUG + org.hibernate.type.descriptor.sql.BasicBinder: TRACE \ No newline at end of file diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml new file mode 100644 index 0000000..98311dc --- /dev/null +++ b/src/main/resources/application-prod.yml @@ -0,0 +1,24 @@ +# Development Profile Configuration +spring: + # H2 Console disabled for production + h2: + console: + enabled: false + +# Production-specific logging +logging: + level: + org.sitmun.mbtiles: INFO + root: WARN + pattern: + console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" + +# Production-specific actuator configuration +management: + endpoint: + health: + show-details: never # Hide detailed health info in production + endpoints: + web: + exposure: + include: health # Minimal endpoints for production \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 55b7bcc..792bba0 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,4 +1,27 @@ +# Logging Configuration logging: level: ROOT: INFO org.sitmun.proxy.middleware: INFO + +# Sitmun Proxy Configuration +sitmun: + backend: + config: + url: http://some.url + secret: some-secret + +# Actuator Configuration +management: + endpoints: + web: + exposure: + include: health + base-path: /actuator + endpoint: + health: + show-details: never + show-components: never + health: + defaults: + enabled: true \ No newline at end of file diff --git a/src/test/java/org/sitmun/proxy/middleware/protocols/http/ExecutionRequestExecutorServiceTest.java b/src/test/java/org/sitmun/proxy/middleware/protocols/http/ExecutionRequestExecutorServiceTest.java new file mode 100644 index 0000000..53e8487 --- /dev/null +++ b/src/test/java/org/sitmun/proxy/middleware/protocols/http/ExecutionRequestExecutorServiceTest.java @@ -0,0 +1,58 @@ +package org.sitmun.proxy.middleware.protocols.http; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.sitmun.proxy.middleware.test.fixtures.AuthorizationProxyFixtures.*; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.nio.charset.StandardCharsets; +import java.util.Objects; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.sitmun.proxy.middleware.service.RequestExecutorService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.json.JacksonTester; +import org.springframework.http.ResponseEntity; + +@SpringBootTest +@AutoConfigureTestDatabase +@TestInstance(Lifecycle.PER_CLASS) +@DisplayName("Generic http requests tests") +class ExecutionRequestExecutorServiceTest { + + @Autowired private RequestExecutorService requestExecutorService; + + @Autowired private HttpClientFactoryService httpClientFactoryService; + + private JacksonTester jsonTester; + + @BeforeAll + void setup() { + ObjectMapper objectMapper = new ObjectMapper(); + JacksonTester.initFields(this, objectMapper); + } + + @AfterEach + void clearInterceptors() { + httpClientFactoryService.removeAllInterceptors(); + } + + /** + * Public user access to the public WFS service. + * + * @throws Exception for unexpected failures + */ + @Test + @DisplayName("Request to a public WFS service") + void publicWfs() throws Exception { + ResponseEntity response = requestExecutorService.executeRequest("", wfsService(false)); + assertThat(response.getStatusCode().value()).isEqualTo(200); + assertThat(Objects.requireNonNull(response.getHeaders().get("Content-Type")).get(0)) + .isEqualTo("application/json;charset=UTF-8"); + Object body = response.getBody(); + assertThat(body).isNotNull().isInstanceOf(byte[].class); + String text = new String((byte[]) body, StandardCharsets.UTF_8); + assertThat(jsonTester.parse(text)).extracting("totalFeatures").isEqualTo(273); + } +} diff --git a/src/test/java/org/sitmun/proxy/middleware/protocols/http/HttpClientFactoryServiceTest.java b/src/test/java/org/sitmun/proxy/middleware/protocols/http/HttpClientFactoryServiceTest.java new file mode 100644 index 0000000..87a0f51 --- /dev/null +++ b/src/test/java/org/sitmun/proxy/middleware/protocols/http/HttpClientFactoryServiceTest.java @@ -0,0 +1,63 @@ +package org.sitmun.proxy.middleware.protocols.http; + +import static org.assertj.core.api.Fail.fail; + +import java.io.IOException; +import java.util.List; +import okhttp3.Request; +import okhttp3.Response; +import org.assertj.core.util.Lists; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("HttpClientFactory tests") +class HttpClientFactoryServiceTest { + + @Test + @DisplayName("Fail with SSLHandshakeException") + void failWithASSLHandhakeException() { + String url = "https://self-signed.badssl.com "; + List unsafeAllowedHosts = Lists.list(); + HttpClient client = new HttpClientFactoryService(unsafeAllowedHosts); + + Request request = new Request.Builder().url(url).header("Accept", "*/*").build(); + + try (Response response = client.executeRequest(request)) { + fail("Unexpected exception"); + } catch (IOException e) { + } + } + + @Test + @DisplayName("Any request use the unsafe client") + void anyRequestUseTheUnsafeClient() { + String url = "https://self-signed.badssl.com"; + List unsafeAllowedHosts = Lists.list("*"); + + HttpClient client = new HttpClientFactoryService(unsafeAllowedHosts); + + Request request = new Request.Builder().url(url).header("Accept", "*/*").build(); + try (Response response = client.executeRequest(request)) { + // Do nothing + } catch (IOException e) { + fail("Unexpected exception"); + } + } + + @Test + @DisplayName("Use unsafe client when domain matches") + void useUnsafeClientWhenDomainMatches() { + String url = "https://self-signed.badssl.com"; + List unsafeAllowedHosts = Lists.list("self-signed.badssl.com"); + + HttpClient client = new HttpClientFactoryService(unsafeAllowedHosts); + + Request request = new Request.Builder().url(url).header("Accept", "*/*").build(); + + try (Response response = client.executeRequest(request)) { + // Do nothing + } catch (IOException e) { + fail("Unexpected exception"); + } + } +} diff --git a/src/test/java/org/sitmun/proxy/middleware/protocols/jdbc/ExecutionRequestExecutorServiceTest.java b/src/test/java/org/sitmun/proxy/middleware/protocols/jdbc/ExecutionRequestExecutorServiceTest.java new file mode 100644 index 0000000..f653dd2 --- /dev/null +++ b/src/test/java/org/sitmun/proxy/middleware/protocols/jdbc/ExecutionRequestExecutorServiceTest.java @@ -0,0 +1,59 @@ +package org.sitmun.proxy.middleware.protocols.jdbc; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.sitmun.proxy.middleware.test.fixtures.AuthorizationProxyFixtures.*; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Objects; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.sitmun.proxy.middleware.service.RequestExecutorService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.json.JacksonTester; +import org.springframework.http.ResponseEntity; + +@SpringBootTest +@AutoConfigureTestDatabase +@TestInstance(Lifecycle.PER_CLASS) +@DisplayName("Relational requests tests") +class ExecutionRequestExecutorServiceTest { + + @Autowired private RequestExecutorService requestExecutorService; + + @BeforeAll + void setup() { + ObjectMapper objectMapper = new ObjectMapper(); + JacksonTester.initFields(this, objectMapper); + } + + /** Public user access to a relational service. */ + @Test + @DisplayName("Request to a JDBC service") + void jdbcAccess() { + ResponseEntity response = + requestExecutorService.executeRequest("", inMemoryH2Database(false)); + assertThat(response.getStatusCode().value()).isEqualTo(200); + assertThat(Objects.requireNonNull(response.getHeaders().get("Content-Type")).get(0)) + .isEqualTo("application/json"); + Object body = response.getBody(); + assertThat(body).isNotNull().asInstanceOf(InstanceOfAssertFactories.LIST).hasSize(35); + } + + /** Public user access to a relational service filtered. */ + @Test + @DisplayName("Request to a JDBC service with filters") + @Disabled( + "Redundant test: the test is identical to jdbcAccess because the SQL query is built on the Configuration and Authorization API") + void jdbcAccessWithFilters() { + ResponseEntity response = + requestExecutorService.executeRequest("", inMemoryH2Database(false)); + assertThat(response.getStatusCode().value()).isEqualTo(200); + assertThat(Objects.requireNonNull(response.getHeaders().get("Content-Type")).get(0)) + .isEqualTo("application/json"); + Object body = response.getBody(); + assertThat(body).isNotNull().asInstanceOf(InstanceOfAssertFactories.LIST).hasSize(35); + } +} diff --git a/src/test/java/org/sitmun/proxy/middleware/protocols/wms/ExecutionRequestExecutorServiceTest.java b/src/test/java/org/sitmun/proxy/middleware/protocols/wms/ExecutionRequestExecutorServiceTest.java new file mode 100644 index 0000000..7913f43 --- /dev/null +++ b/src/test/java/org/sitmun/proxy/middleware/protocols/wms/ExecutionRequestExecutorServiceTest.java @@ -0,0 +1,97 @@ +package org.sitmun.proxy.middleware.protocols.wms; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.sitmun.proxy.middleware.test.fixtures.AuthorizationProxyFixtures.*; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Objects; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.sitmun.proxy.middleware.protocols.http.HttpClientFactoryService; +import org.sitmun.proxy.middleware.service.RequestExecutorService; +import org.sitmun.proxy.middleware.test.interceptors.CheckBasicAuthorization; +import org.sitmun.proxy.middleware.test.interceptors.DoNotRequest; +import org.sitmun.proxy.middleware.test.interceptors.HostnameCheck; +import org.sitmun.proxy.middleware.test.interceptors.QueryCheck; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.json.JacksonTester; +import org.springframework.http.ResponseEntity; + +@SpringBootTest +@AutoConfigureTestDatabase +@TestInstance(Lifecycle.PER_CLASS) +@DisplayName("WMS request tests") +class ExecutionRequestExecutorServiceTest { + + @Autowired private RequestExecutorService requestExecutorService; + + @Autowired private HttpClientFactoryService httpClientFactoryService; + + private JacksonTester jsonTester; + + @BeforeAll + void setup() { + ObjectMapper objectMapper = new ObjectMapper(); + JacksonTester.initFields(this, objectMapper); + } + + @AfterEach + void clearInterceptors() { + httpClientFactoryService.removeAllInterceptors(); + } + + /** Public user access to the public WMS service. */ + @Test + @DisplayName("Request to a public WMS service") + void publicWms() { + ResponseEntity response = requestExecutorService.executeRequest("", wmsService(false)); + assertThat(response.getStatusCode().value()).isEqualTo(200); + assertThat(Objects.requireNonNull(response.getHeaders().get("Content-Type")).get(0)) + .isEqualTo("image/png"); + } + + /** Public user access to the public WMS service. */ + @Test + @DisplayName("Request to a public WMS service with parameters in the URI") + void publicWmsWithURI() { + ResponseEntity response = + requestExecutorService.executeRequest("", wmsServiceWithURIWithParameters()); + assertThat(response.getStatusCode().value()).isEqualTo(200); + assertThat(Objects.requireNonNull(response.getHeaders().get("Content-Type")).get(0)) + .isEqualTo("image/png"); + } + + /** Public user access to a private WMS service with basic authentication. */ + @Test + @DisplayName("Basic Authentication added to service") + void privateWmsBasicAuthentication() { + CheckBasicAuthorization interceptor = new CheckBasicAuthorization(); + httpClientFactoryService.addInterceptors(interceptor, DoNotRequest.INSTANCE); + requestExecutorService.executeRequest("", wmsService(true)); + assertThat(interceptor.getExpectation()).isEqualTo("userServ:passwordServ"); + } + + /** Public user access to a private WMS service with an IP on a private network. */ + @Test + @DisplayName("Request to service with an IP instead of a Hostname") + void privateWmsIpPrivateRed() { + HostnameCheck interceptor = new HostnameCheck(); + httpClientFactoryService.addInterceptors(interceptor, DoNotRequest.INSTANCE); + requestExecutorService.executeRequest("", wmsServiceWithIPasHostname()); + assertThat(interceptor.getExpectation()).isEqualTo("154.58.18.33"); + } + + /** Public user access to a private WMS service, adding a filter to the request. */ + @Test + @DisplayName("Request to service with filters") + void privateWfsWithFilter() { + QueryCheck interceptor = new QueryCheck(); + httpClientFactoryService.addInterceptors(interceptor, DoNotRequest.INSTANCE); + requestExecutorService.executeRequest("", wfsService(true)); + assertThat(interceptor.getExpectation()) + .isEqualToIgnoringCase( + "REQUEST=GetFeature&VERSION=2.0.0&outputformat=application/json&SERVICE=WFS&CQL_FILTER=tr_05=5&typename=grid:gridp_250"); + } +} diff --git a/src/test/java/org/sitmun/proxy/middleware/service/ExecutionRequestExecutorServiceTest.java b/src/test/java/org/sitmun/proxy/middleware/service/ExecutionRequestExecutorServiceTest.java new file mode 100644 index 0000000..12e7e53 --- /dev/null +++ b/src/test/java/org/sitmun/proxy/middleware/service/ExecutionRequestExecutorServiceTest.java @@ -0,0 +1,144 @@ +package org.sitmun.proxy.middleware.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.sitmun.proxy.middleware.test.fixtures.AuthorizationProxyFixtures.*; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.nio.charset.StandardCharsets; +import java.util.Objects; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.sitmun.proxy.middleware.protocols.http.HttpClientFactoryService; +import org.sitmun.proxy.middleware.test.interceptors.CheckBasicAuthorization; +import org.sitmun.proxy.middleware.test.interceptors.DoNotRequest; +import org.sitmun.proxy.middleware.test.interceptors.HostnameCheck; +import org.sitmun.proxy.middleware.test.interceptors.QueryCheck; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.json.JacksonTester; +import org.springframework.http.ResponseEntity; + +@SpringBootTest +@AutoConfigureTestDatabase +@TestInstance(Lifecycle.PER_CLASS) +@DisplayName("GlobalRequestService tests") +class ExecutionRequestExecutorServiceTest { + + @Autowired private RequestExecutorService requestExecutorService; + + @Autowired private HttpClientFactoryService httpClientFactoryService; + + private JacksonTester jsonTester; + + @BeforeAll + void setup() { + ObjectMapper objectMapper = new ObjectMapper(); + JacksonTester.initFields(this, objectMapper); + } + + @AfterEach + void clearInterceptors() { + httpClientFactoryService.removeAllInterceptors(); + } + + /** Public user access to the public WMS service. */ + @Test + @DisplayName("Request to a public WMS service") + void publicWms() { + ResponseEntity response = requestExecutorService.executeRequest("", wmsService(false)); + assertThat(response.getStatusCode().value()).isEqualTo(200); + assertThat(Objects.requireNonNull(response.getHeaders().get("Content-Type")).get(0)) + .isEqualTo("image/png"); + } + + /** Public user access to the public WMS service. */ + @Test + @DisplayName("Request to a public WMS service with parameters in the URI") + void publicWmsWithURI() { + ResponseEntity response = + requestExecutorService.executeRequest("", wmsServiceWithURIWithParameters()); + assertThat(response.getStatusCode().value()).isEqualTo(200); + assertThat(Objects.requireNonNull(response.getHeaders().get("Content-Type")).get(0)) + .isEqualTo("image/png"); + } + + /** + * Public user access to the public WFS service. + * + * @throws Exception for unexpected failures + */ + @Test + @DisplayName("Request to a public WFS service") + void publicWfs() throws Exception { + ResponseEntity response = requestExecutorService.executeRequest("", wfsService(false)); + assertThat(response.getStatusCode().value()).isEqualTo(200); + assertThat(Objects.requireNonNull(response.getHeaders().get("Content-Type")).get(0)) + .isEqualTo("application/json;charset=UTF-8"); + Object body = response.getBody(); + assertThat(body).isNotNull().isInstanceOf(byte[].class); + String text = new String((byte[]) body, StandardCharsets.UTF_8); + assertThat(jsonTester.parse(text)).extracting("totalFeatures").isEqualTo(273); + } + + /** Public user access to a private WMS service with basic authentication. */ + @Test + @DisplayName("Basic Authentication added to service") + void privateWmsBasicAuthentication() { + CheckBasicAuthorization interceptor = new CheckBasicAuthorization(); + httpClientFactoryService.addInterceptors(interceptor, DoNotRequest.INSTANCE); + requestExecutorService.executeRequest("", wmsService(true)); + assertThat(interceptor.getExpectation()).isEqualTo("userServ:passwordServ"); + } + + /** Public user access to a private WMS service with an IP on a private network. */ + @Test + @DisplayName("Request to service with an IP instead of a Hostname") + void privateWmsIpPrivateRed() { + HostnameCheck interceptor = new HostnameCheck(); + httpClientFactoryService.addInterceptors(interceptor, DoNotRequest.INSTANCE); + requestExecutorService.executeRequest("", wmsServiceWithIPasHostname()); + assertThat(interceptor.getExpectation()).isEqualTo("154.58.18.33"); + } + + /** Public user access to a private WMS service, adding a filter to the request. */ + @Test + @DisplayName("Request to service with filters") + void privateWfsWithFilter() { + QueryCheck interceptor = new QueryCheck(); + httpClientFactoryService.addInterceptors(interceptor, DoNotRequest.INSTANCE); + requestExecutorService.executeRequest("", wfsService(true)); + assertThat(interceptor.getExpectation()) + .isEqualToIgnoringCase( + "REQUEST=GetFeature&VERSION=2.0.0&outputformat=application/json&SERVICE=WFS&CQL_FILTER=tr_05=5&typename=grid:gridp_250"); + } + + /** Public user access to a relational service. */ + @Test + @DisplayName("Request to a JDBC service") + void jdbcAccess() { + ResponseEntity response = + requestExecutorService.executeRequest("", inMemoryH2Database(false)); + assertThat(response.getStatusCode().value()).isEqualTo(200); + assertThat(Objects.requireNonNull(response.getHeaders().get("Content-Type")).get(0)) + .isEqualTo("application/json"); + Object body = response.getBody(); + assertThat(body).isNotNull().asInstanceOf(InstanceOfAssertFactories.LIST).hasSize(35); + } + + /** Public user access to a relational service filtered. */ + @Test + @DisplayName("Request to a JDBC service with filters") + @Disabled( + "Redundant test: the test is identical to jdbcAccess because the SQL query is built on the Configuration and Authorization API") + void jdbcAccessWithFilters() { + ResponseEntity response = + requestExecutorService.executeRequest("", inMemoryH2Database(false)); + assertThat(response.getStatusCode().value()).isEqualTo(200); + assertThat(Objects.requireNonNull(response.getHeaders().get("Content-Type")).get(0)) + .isEqualTo("application/json"); + Object body = response.getBody(); + assertThat(body).isNotNull().asInstanceOf(InstanceOfAssertFactories.LIST).hasSize(35); + } +} diff --git a/src/test/java/org/sitmun/proxy/middleware/service/GlobalRequestServiceTest.java b/src/test/java/org/sitmun/proxy/middleware/service/GlobalRequestServiceTest.java deleted file mode 100644 index 699cf53..0000000 --- a/src/test/java/org/sitmun/proxy/middleware/service/GlobalRequestServiceTest.java +++ /dev/null @@ -1,154 +0,0 @@ -package org.sitmun.proxy.middleware.service; - -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.jupiter.api.*; -import org.junit.jupiter.api.TestInstance.Lifecycle; -import org.sitmun.proxy.middleware.test.interceptors.CheckBasicAuthorization; -import org.sitmun.proxy.middleware.test.interceptors.DoNotRequest; -import org.sitmun.proxy.middleware.test.interceptors.HostnameCheck; -import org.sitmun.proxy.middleware.test.interceptors.QueryCheck; -import org.sitmun.proxy.middleware.test.services.TestClientService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.json.JacksonTester; -import org.springframework.http.ResponseEntity; - -import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.Objects; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.sitmun.proxy.middleware.test.fixtures.AuthorizationProxyFixtures.*; - - -@SpringBootTest -@AutoConfigureTestDatabase -@TestInstance(Lifecycle.PER_CLASS) -@DisplayName("GlobalRequestService tests") -class GlobalRequestServiceTest { - - @Autowired - private GlobalRequestService globalRequestService; - - @Autowired - private TestClientService testClientService; - - private JacksonTester jsonTester; - - @BeforeAll - public void setup() { - ObjectMapper objectMapper = new ObjectMapper(); - JacksonTester.initFields(this, objectMapper); - } - - @AfterEach - public void clearInterceptors() { - testClientService.removeAllInterceptors(); - } - - /** - * Public user access to the public WMS service. - */ - @Test - @DisplayName("Request to a public WMS service") - void publicWms() { - ResponseEntity response = globalRequestService.executeRequest("", wmsService(false)); - assertThat(response.getStatusCodeValue()).isEqualTo(200); - assertThat(Objects.requireNonNull(response.getHeaders().get("Content-Type")).get(0)).isEqualTo("image/png"); - } - - /** - * Public user access to the public WMS service. - */ - @Test - @DisplayName("Request to a public WMS service with parameters in the URI") - void publicWmsWithURI() { - ResponseEntity response = globalRequestService.executeRequest("", wmsServiceWithURIWithParameters()); - assertThat(response.getStatusCodeValue()).isEqualTo(200); - assertThat(Objects.requireNonNull(response.getHeaders().get("Content-Type")).get(0)).isEqualTo("image/png"); - } - - /** - * Public user access to the public WFS service. - * - * @throws Exception for unexpected failures - */ - @Test - @DisplayName("Request to a public WFS service") - void publicWfs() throws Exception { - ResponseEntity response = globalRequestService.executeRequest("", wfsService(false)); - assertThat(response.getStatusCodeValue()).isEqualTo(200); - assertThat(Objects.requireNonNull(response.getHeaders().get("Content-Type")).get(0)).isEqualTo("application/json;charset=UTF-8"); - Object body = response.getBody(); - assertThat(body).isNotNull().isInstanceOf(byte[].class); - String text = new String((byte[]) body, StandardCharsets.UTF_8); - assertThat(jsonTester.parse(text)) - .extracting("totalFeatures").isEqualTo(268); - } - - /** - * Public user access to a private WMS service with basic authentication. - */ - @Test - @DisplayName("Basic Authentication added to service") - void privateWmsBasicAuthentication() { - CheckBasicAuthorization interceptor = new CheckBasicAuthorization(); - testClientService.addInterceptors(interceptor, DoNotRequest.INSTANCE); - globalRequestService.executeRequest("", wmsService(true)); - assertThat(interceptor.getExpectation()).isEqualTo("userServ:passwordServ"); - } - - /** - * Public user access to a private WMS service with an IP on a private network. - */ - @Test - @DisplayName("Request to service with an IP instead of a Hostname") - void privateWmsIpPrivateRed() { - HostnameCheck interceptor = new HostnameCheck(); - testClientService.addInterceptors(interceptor, DoNotRequest.INSTANCE); - globalRequestService.executeRequest("", wmsServiceWithIPasHostname()); - assertThat(interceptor.getExpectation()).isEqualTo("154.58.18.33"); - } - - /** - * Public user access to a private WMS service, adding a filter to the - * request. - */ - @Test - @DisplayName("Request to service with filters") - void privateWfsWithFilter() { - QueryCheck interceptor = new QueryCheck(); - testClientService.addInterceptors(interceptor, DoNotRequest.INSTANCE); - globalRequestService.executeRequest("", wfsService(true)); - assertThat(interceptor.getExpectation()).isEqualTo("REQUEST=GetFeature&VERSION=2.0.0&outputformat=application/json&SERVICE=WFS&CQL_FILTER=tr_05=5&typename=grid:gridp_250"); - } - - /** - * Public user access to a relational service. - */ - @Test - @DisplayName("Request to a JDBC service") - void jdbcAccess() { - ResponseEntity response = globalRequestService.executeRequest("", inMemoryH2Database(false)); - assertThat(response.getStatusCodeValue()).isEqualTo(200); - assertThat(Objects.requireNonNull(response.getHeaders().get("Content-Type")).get(0)).isEqualTo("application/json"); - Object body = response.getBody(); - assertThat(body).isNotNull().isInstanceOf(List.class).asList().hasSize(35); - } - - /** - * Public user access to a relational service filtered. - */ - @Test - @DisplayName("Request to a JDBC service with filters") - @Disabled("Redundant test: the test is identical to jdbcAccess because the SQL query is built on the Configuration and Authorization API") - void jdbcAccessWithFilters() { - ResponseEntity response = globalRequestService.executeRequest("", inMemoryH2Database(false)); - assertThat(response.getStatusCodeValue()).isEqualTo(200); - assertThat(Objects.requireNonNull(response.getHeaders().get("Content-Type")).get(0)).isEqualTo("application/json"); - Object body = response.getBody(); - assertThat(body).isNotNull().isInstanceOf(List.class).asList().hasSize(35); - } - -} diff --git a/src/test/java/org/sitmun/proxy/middleware/test/TestUtils.java b/src/test/java/org/sitmun/proxy/middleware/test/TestUtils.java index 7a301e5..0965904 100644 --- a/src/test/java/org/sitmun/proxy/middleware/test/TestUtils.java +++ b/src/test/java/org/sitmun/proxy/middleware/test/TestUtils.java @@ -1,5 +1,7 @@ package org.sitmun.proxy.middleware.test; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; + import com.fasterxml.jackson.databind.ObjectMapper; import org.assertj.core.api.Assertions; import org.json.JSONObject; @@ -10,24 +12,27 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.web.client.RestTemplate; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; - public class TestUtils { private static final String ADMIN_USERNAME = "admin"; private static final String ADMIN_PASSWORD = "admin"; public static String requestAuthorization(MockMvc mvc) throws Exception { - UserPasswordAuthenticationRequest login = UserPasswordAuthenticationRequest.builder() - .username(ADMIN_USERNAME) - .password(ADMIN_PASSWORD) - .build(); + UserPasswordAuthenticationRequest login = + UserPasswordAuthenticationRequest.builder() + .username(ADMIN_USERNAME) + .password(ADMIN_PASSWORD) + .build(); ObjectMapper mapper = new ObjectMapper(); String result = ""; - String authorization = mvc.perform(post(URIConstants.AUTHORIZATION_URI) - .contentType(MediaType.APPLICATION_JSON) - .content(mapper.writeValueAsString(login))) - .andReturn().getResponse().getContentAsString(); + String authorization = + mvc.perform( + post(URIConstants.AUTHORIZATION_URI) + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(login))) + .andReturn() + .getResponse() + .getContentAsString(); if (authorization.contains("id_token")) { result = (String) new JSONObject(authorization).get("id_token"); @@ -36,13 +41,14 @@ public static String requestAuthorization(MockMvc mvc) throws Exception { } public static String requestAuthorization(RestTemplate restTemplate) { - UserPasswordAuthenticationRequest login = UserPasswordAuthenticationRequest.builder() - .username(ADMIN_USERNAME) - .password(ADMIN_PASSWORD) - .build(); + UserPasswordAuthenticationRequest login = + UserPasswordAuthenticationRequest.builder() + .username(ADMIN_USERNAME) + .password(ADMIN_PASSWORD) + .build(); ResponseEntity loginResponse = - restTemplate - .postForEntity(URIConstants.AUTHORIZATION_URI, login, AuthenticationResponse.class); + restTemplate.postForEntity( + URIConstants.AUTHORIZATION_URI, login, AuthenticationResponse.class); Assertions.assertThat(loginResponse.getBody()).isNotNull(); return "Bearer " + loginResponse.getBody().getIdToken(); } diff --git a/src/test/java/org/sitmun/proxy/middleware/test/dto/UserPasswordAuthenticationRequest.java b/src/test/java/org/sitmun/proxy/middleware/test/dto/UserPasswordAuthenticationRequest.java index ddf1a90..c1d8927 100644 --- a/src/test/java/org/sitmun/proxy/middleware/test/dto/UserPasswordAuthenticationRequest.java +++ b/src/test/java/org/sitmun/proxy/middleware/test/dto/UserPasswordAuthenticationRequest.java @@ -1,12 +1,9 @@ package org.sitmun.proxy.middleware.test.dto; - import lombok.Builder; import lombok.Data; -/** - * DTO object for storing a user's credentials. - */ +/** DTO object for storing a user's credentials. */ @Data @Builder public class UserPasswordAuthenticationRequest { diff --git a/src/test/java/org/sitmun/proxy/middleware/test/fixtures/AuthorizationProxyFixtures.java b/src/test/java/org/sitmun/proxy/middleware/test/fixtures/AuthorizationProxyFixtures.java index 5c10aba..5833c25 100644 --- a/src/test/java/org/sitmun/proxy/middleware/test/fixtures/AuthorizationProxyFixtures.java +++ b/src/test/java/org/sitmun/proxy/middleware/test/fixtures/AuthorizationProxyFixtures.java @@ -1,55 +1,60 @@ package org.sitmun.proxy.middleware.test.fixtures; -import org.sitmun.proxy.middleware.dto.DatasourcePayloadDto; -import org.sitmun.proxy.middleware.dto.HttpSecurityDto; -import org.sitmun.proxy.middleware.dto.OgcWmsPayloadDto; - import java.util.HashMap; +import org.sitmun.proxy.middleware.dto.HttpSecurityDto; +import org.sitmun.proxy.middleware.protocols.jdbc.JdbcPayloadDto; +import org.sitmun.proxy.middleware.protocols.wms.WmsPayloadDto; public class AuthorizationProxyFixtures { - public static OgcWmsPayloadDto wmsService(boolean basicAuthentication) { - return OgcWmsPayloadDto.builder() - .method("GET") - .parameters(getWmsParameters()) - .security(basicAuthentication ? new HttpSecurityDto("basic", "http", "userServ", "passwordServ") : null) - .uri("https://sitmun.diba.cat/arcgis/services/PUBLIC/DTE50/MapServer/WmsServer") - .build(); + public static WmsPayloadDto wmsService(boolean basicAuthentication) { + return WmsPayloadDto.builder() + .method("GET") + .parameters(getWmsParameters()) + .security( + basicAuthentication + ? new HttpSecurityDto("basic", "http", "userServ", "passwordServ") + : null) + .uri("https://sitmun.diba.cat/arcgis/services/PUBLIC/DTE50/MapServer/WmsServer") + .build(); } - public static OgcWmsPayloadDto wmsServiceWithIPasHostname() { - return OgcWmsPayloadDto.builder() - .method("GET") - .parameters(getWmsParameters()) - .uri("http://154.58.18.33/arcgis/services/PUBLIC/DTE50/MapServer/WmsServer") - .build(); + public static WmsPayloadDto wmsServiceWithIPasHostname() { + return WmsPayloadDto.builder() + .method("GET") + .parameters(getWmsParameters()) + .uri("http://154.58.18.33/arcgis/services/PUBLIC/DTE50/MapServer/WmsServer") + .build(); } - public static OgcWmsPayloadDto wmsServiceWithURIWithParameters() { - return OgcWmsPayloadDto.builder() - .method("GET") - .parameters(getWmsParameters()) - .uri("https://sitmun.diba.cat/arcgis/services/PUBLIC/DTE50/MapServer/WmsServer?service=WMS&") - .build(); + public static WmsPayloadDto wmsServiceWithURIWithParameters() { + return WmsPayloadDto.builder() + .method("GET") + .parameters(getWmsParameters()) + .uri( + "https://sitmun.diba.cat/arcgis/services/PUBLIC/DTE50/MapServer/WmsServer?service=WMS&") + .build(); } - public static OgcWmsPayloadDto wfsService(boolean filtered) { - OgcWmsPayloadDto payload = new OgcWmsPayloadDto(); + public static WmsPayloadDto wfsService(boolean filtered) { + WmsPayloadDto payload = new WmsPayloadDto(); payload.setMethod("GET"); payload.setParameters(getWfsParameters(filtered)); payload.setSecurity(null); - payload.setUri("https://www.juntadeandalucia.es/institutodeestadisticaycartografia/geoserver-ieca/grid/wfs"); + payload.setUri( + "https://www.juntadeandalucia.es/institutodeestadisticaycartografia/geoserver-ieca/grid/wfs"); payload.setVary(null); return payload; } - private static HashMap getWmsParameters() { + private static HashMap getWmsParameters() { HashMap parameters = new HashMap<>(); parameters.put("REQUEST", "GetMap"); parameters.put("VERSION", "1.3.0"); parameters.put("SERVICE", "WMS"); parameters.put("LAYERS", "DTE50_MUN,DTE50_PROV"); - parameters.put("BBOX", "2.1358108520507812,41.37616450732182,2.1797561645507812,41.39986165460519"); + parameters.put( + "BBOX", "2.1358108520507812,41.37616450732182,2.1797561645507812,41.39986165460519"); parameters.put("CRS", "EPSG:4326"); parameters.put("STYLES", ""); parameters.put("FORMAT", "image/png"); @@ -70,7 +75,8 @@ private static HashMap getWfsParameters(boolean filtered) { if (filtered) { parameters.put("CQL_FILTER", "tr_05=5"); } else { - parameters.put("BBOX", "243818.6189194798,4133819.057198299,255162.31367381044,4141774.181994748"); + parameters.put( + "BBOX", "243818.6189194798,4133819.057198299,255162.31367381044,4141774.181994748"); } return parameters; @@ -84,14 +90,13 @@ private static String getSql(boolean filtered) { return sql; } - public static DatasourcePayloadDto inMemoryH2Database(boolean filtered) { - return DatasourcePayloadDto.builder() - .driver("org.h2.Driver") - .uri("jdbc:h2:mem:testdb") - .user("admin") - .password("admin") - .sql(getSql(filtered)) - .build(); + public static JdbcPayloadDto inMemoryH2Database(boolean filtered) { + return JdbcPayloadDto.builder() + .driver("org.h2.Driver") + .uri("jdbc:h2:mem:testdb") + .user("admin") + .password("admin") + .sql(getSql(filtered)) + .build(); } - } diff --git a/src/test/java/org/sitmun/proxy/middleware/test/interceptors/CheckBasicAuthorization.java b/src/test/java/org/sitmun/proxy/middleware/test/interceptors/CheckBasicAuthorization.java index e8da58f..7eed945 100644 --- a/src/test/java/org/sitmun/proxy/middleware/test/interceptors/CheckBasicAuthorization.java +++ b/src/test/java/org/sitmun/proxy/middleware/test/interceptors/CheckBasicAuthorization.java @@ -1,14 +1,12 @@ package org.sitmun.proxy.middleware.test.interceptors; +import java.io.IOException; +import java.util.Base64; import lombok.Getter; import okhttp3.Interceptor; import okhttp3.Response; import org.jetbrains.annotations.NotNull; -import java.io.IOException; -import java.util.Base64; - - @Getter public class CheckBasicAuthorization implements Interceptor { private String expectation; diff --git a/src/test/java/org/sitmun/proxy/middleware/test/interceptors/DoNotRequest.java b/src/test/java/org/sitmun/proxy/middleware/test/interceptors/DoNotRequest.java index 74d0b48..e6dcea0 100644 --- a/src/test/java/org/sitmun/proxy/middleware/test/interceptors/DoNotRequest.java +++ b/src/test/java/org/sitmun/proxy/middleware/test/interceptors/DoNotRequest.java @@ -10,11 +10,12 @@ public class DoNotRequest implements Interceptor { @Override public Response intercept(@NotNull Chain chain) { return new Response.Builder() - .code(418) // Whatever code - .body(ResponseBody.create("", MediaType.parse("plain/text"))) // Whatever body - .addHeader("Content-Type", "plain/text") - .protocol(Protocol.HTTP_2) - .message("Dummy response") - .request(chain.request()) - .build(); } + .code(418) // Whatever code + .body(ResponseBody.create("", MediaType.parse("plain/text"))) // Whatever body + .addHeader("Content-Type", "plain/text") + .protocol(Protocol.HTTP_2) + .message("Dummy response") + .request(chain.request()) + .build(); + } } diff --git a/src/test/java/org/sitmun/proxy/middleware/test/interceptors/HostnameCheck.java b/src/test/java/org/sitmun/proxy/middleware/test/interceptors/HostnameCheck.java index 14bb836..e1e7df2 100644 --- a/src/test/java/org/sitmun/proxy/middleware/test/interceptors/HostnameCheck.java +++ b/src/test/java/org/sitmun/proxy/middleware/test/interceptors/HostnameCheck.java @@ -1,12 +1,11 @@ package org.sitmun.proxy.middleware.test.interceptors; +import java.io.IOException; import lombok.Getter; import okhttp3.*; import org.jetbrains.annotations.NotNull; import org.springframework.stereotype.Component; -import java.io.IOException; - @Getter @Component public class HostnameCheck implements Interceptor { diff --git a/src/test/java/org/sitmun/proxy/middleware/test/interceptors/QueryCheck.java b/src/test/java/org/sitmun/proxy/middleware/test/interceptors/QueryCheck.java index 41c03be..23dd743 100644 --- a/src/test/java/org/sitmun/proxy/middleware/test/interceptors/QueryCheck.java +++ b/src/test/java/org/sitmun/proxy/middleware/test/interceptors/QueryCheck.java @@ -1,5 +1,6 @@ package org.sitmun.proxy.middleware.test.interceptors; +import java.io.IOException; import lombok.Getter; import okhttp3.Interceptor; import okhttp3.Request; @@ -7,8 +8,6 @@ import org.jetbrains.annotations.NotNull; import org.springframework.stereotype.Component; -import java.io.IOException; - @Getter @Component public class QueryCheck implements Interceptor { diff --git a/src/test/java/org/sitmun/proxy/middleware/test/services/TestClientService.java b/src/test/java/org/sitmun/proxy/middleware/test/services/TestClientService.java deleted file mode 100644 index 70804ab..0000000 --- a/src/test/java/org/sitmun/proxy/middleware/test/services/TestClientService.java +++ /dev/null @@ -1,71 +0,0 @@ -package org.sitmun.proxy.middleware.test.services; - -import okhttp3.Interceptor; -import okhttp3.OkHttpClient; -import okhttp3.OkHttpClient.Builder; -import okhttp3.Request; -import okhttp3.Response; -import org.sitmun.proxy.middleware.service.ClientService; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Service; - -import javax.annotation.Priority; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -@Profile("test") -@Priority(1) -@Service -public class TestClientService implements ClientService { - - private final List interceptors = new ArrayList<>(); - - private OkHttpClient httpClient; - - public TestClientService(List interceptors) { - this.interceptors.addAll(interceptors); - httpClient = build(); - } - - private OkHttpClient build() { - Builder builder = new Builder(); - for (Interceptor ti : interceptors) { - builder.addInterceptor(ti); - } - return builder.build(); - } - - @Override - public Response executeRequest(Request httpRequest) throws IOException { - return httpClient.newCall(httpRequest).execute(); - } - - public void addInterceptor(Interceptor interceptor) { - if (!interceptors.contains(interceptor)) { - interceptors.add(interceptor); - httpClient = build(); - } - } - - public void addInterceptors(Interceptor... interceptor) { - for (Interceptor i : interceptor) { - if (!interceptors.contains(i)) { - interceptors.add(i); - } - } - httpClient = build(); - } - - public void removeInterceptor(Interceptor interceptor) { - if (interceptors.contains(interceptor)) { - interceptors.remove(interceptor); - httpClient = build(); - } - } - - public void removeAllInterceptors() { - interceptors.clear(); - httpClient = build(); - } -} diff --git a/src/test/java/org/sitmun/proxy/middleware/test/services/TestHttpClientFactory.java b/src/test/java/org/sitmun/proxy/middleware/test/services/TestHttpClientFactory.java deleted file mode 100644 index e37243c..0000000 --- a/src/test/java/org/sitmun/proxy/middleware/test/services/TestHttpClientFactory.java +++ /dev/null @@ -1,76 +0,0 @@ -package org.sitmun.proxy.middleware.test.services; - -import okhttp3.Request; -import okhttp3.Response; -import org.assertj.core.util.Lists; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.sitmun.proxy.middleware.service.ClientService; -import org.sitmun.proxy.middleware.service.HttpClientFactory; - -import javax.net.ssl.SSLHandshakeException; -import java.io.IOException; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertThrows; - -@DisplayName("HttpClientFactory tests") -public class TestHttpClientFactory { - - @Test - @DisplayName("Fail with SSLHandshakeException") - public void failWithASSLHandhakeException() { - String url = "https://ovc.catastro.meh.es/Cartografia/WMS/ServidorWMS.aspx"; - List unsafeAllowedHosts = Lists.list(); - ClientService client = new HttpClientFactory(unsafeAllowedHosts); - - Request request = new Request.Builder() - .url(url) - .header("Accept", "*/*") - .build(); - assertThrows(SSLHandshakeException.class, () -> { - try (Response response = client.executeRequest(request)) { - // Do nothing - } - }); - } - - @Test - @DisplayName("Any request use the unsafe client") - public void anyRequestUseTheUnsafeClient() { - String url = "https://ovc.catastro.meh.es/Cartografia/WMS/ServidorWMS.aspx"; - List unsafeAllowedHosts = Lists.list("*"); - - ClientService client = new HttpClientFactory(unsafeAllowedHosts); - - Request request = new Request.Builder() - .url(url) - .header("Accept", "*/*") - .build(); - try(Response response = client.executeRequest(request)) { - // Do nothing - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Test - @DisplayName("Use unsafe client when domain matches") - public void useUnsafeClientWhenDomainMatches() { - String url = "https://ovc.catastro.meh.es/Cartografia/WMS/ServidorWMS.aspx"; - List unsafeAllowedHosts = Lists.list("ovc.catastro.meh.es"); - - ClientService client = new HttpClientFactory(unsafeAllowedHosts); - - Request request = new Request.Builder() - .url(url) - .header("Accept", "*/*") - .build(); - - try(Response response = client.executeRequest(request)) { - // Do nothing - } catch (IOException e) { - throw new RuntimeException(e); - } - } - } diff --git a/src/main/resources/data.sql b/src/test/resources/data.sql similarity index 100% rename from src/main/resources/data.sql rename to src/test/resources/data.sql From 2139fd54dd170f1bb975d50be95fac7320f9fa9e Mon Sep 17 00:00:00 2001 From: Francisco J Lopez-Pellicer Date: Sun, 17 Aug 2025 13:10:44 +0200 Subject: [PATCH 3/3] chore: add CHANGELOG.md to document project updates and versioning --- CHANGELOG.md | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b2e3612 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,68 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [1.1.0] - 2025-08-03 + +### Added + +- Protocol-specific request executors for HTTP, JDBC, and WMS services +- Comprehensive code quality tools with Spotless and enhanced JaCoCo +- Git hooks for automated code quality enforcement and conventional commit validation +- Docker configuration with multi-stage builds using Amazon Corretto 17 +- External configuration mounting for containerized deployments (replaced basic docker-compose.yml) +- Spring configuration metadata for better IDE support +- Gradle Version Catalog for centralized dependency management +- Axion Release plugin for automated version management + +### Changed + +- Migrated to Spring Boot 3.5.4 & Java 17 +- Migrated dependencies to Version Catalog +- Reorganized codebase into protocol-based architecture (http, jdbc, wms) +- Completely rewrote documentation with detailed architecture guide +- Improved test organization with protocol-specific test classes +- Restructured Docker configuration with environment-specific configs + +### Fixed + +- Modernized SITMUN proxy middleware configuration and deployment structure +- Improved request processing and error handling +- Enhanced backward compatibility with existing APIs +- Build system with quality gates and automated checks + +## [1.0.0] - 2024-11-12 + +### Added + +- Initial stable release of SITMUN Proxy Middleware +- Spring Boot 2.7.18 application with proxy functionality +- Decorator pattern implementation for flexible request/response modification +- JWT token handling and authentication support (JJWT 0.12.6) +- REST API with proxy endpoint `/proxy/{appId}/{terId}/{type}/{typeId}` +- Spring Boot Actuator for health monitoring and application metrics +- Docker support with basic containerization (docker-compose.yml) +- OkHttp 4.12.0-based HTTP client +- Request sanitization and access control +- Error handling with proper HTTP status codes +- Comprehensive test suite with H2 database for testing +- Decorator-based architecture with HTTP and JDBC context support + +### Changed + +- Modernized from legacy proxy implementations +- Implemented proper dependency management +- Enhanced code quality and maintainability + +### Fixed + +- Various bug fixes and improvements from development phase + +[unreleased]: https://github.com/sitmun/sitmun-proxy-middleware/compare/sitmun-proxy-middleware/1.1.0...HEAD +[1.1.0]: https://github.com/sitmun/sitmun-proxy-middleware/compare/sitmun-proxy-middleware/1.0.0...sitmun-proxy-middleware/1.1.0 +[1.0.0]: https://github.com/sitmun/sitmun-proxy-middleware/releases/tag/sitmun-proxy-middleware/1.0.0