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/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 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 6796519..22ad554 100644 --- a/README.md +++ b/README.md @@ -1,98 +1,1092 @@ [![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 that 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: +### Prerequisites - ```bash - git clone https://github.com/sitmun/sitmun-proxy-middleware.git - ``` +- Java 17 or later +- Docker CE or Docker Desktop +- Git +- Running instance of [sitmun-backend-core](https://github.com/sitmun/sitmun-backend-core) -2. Change to the directory of the repository: +### Local Development - ```bash - cd sitmun-proxy-middleware - ``` +1. **Clone the repository** + ```bash + git clone https://github.com/sitmun/sitmun-proxy-middleware.git + cd sitmun-proxy-middleware + ``` -3. Create a new file named `.env` inside the directory. - Open the `.env` file in a text editor and add in the following format: +2. **Build the application** + ```bash + ./gradlew build -x test + ``` - ```properties - SITMUN_BACKEND_CONFIG_URL=the_location_of_the_sitmun_backend_configuration_endpoint - SITMUN_BACKEND_CONFIG_SECRET=the_shared_secret - ``` +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. Start the SITMUN Middleware proxy: +4. **Verify the service is running** + ```bash + # Check health status + curl http://localhost:8080/actuator/health + + # 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 + ``` - ```bash - docker compose up - ``` +### Docker Deployment - This command will build and start all the services defined in the `docker-compose.yml` file. +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 + ``` -5. Access the SITMUN Middleware Proxy at [http://localhost:9002/actuator/health](http://localhost:9002/actuator/health) and expect: +2. **Start with Docker Compose** + ```bash + cd docker/development + docker-compose up + ``` - ```json - {"status":"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 + +| 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 | + +### Profiles + +#### Development Profile (`dev`) +- Debug logging enabled +- H2 console available +- Detailed error messages +- Development tools enabled + +#### 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 +# 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 +``` + +**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 -- `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. +The project includes several code quality tools: -Additional information is available at . +- **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 -## Uninstalling SITMUN Proxy middleware +### Running Quality Checks -To stop and remove all services, volumes, and networks defined in the `docker-compose.yml` file, use: +```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 -docker compose down -v +# 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 ``` -## Contributing to SITMUN Application Stack +### 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 9256499..9d5ac0c 100644 --- a/build.gradle +++ b/build.gradle @@ -1,68 +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}" - - //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 41d9927..7f93135 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ 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