Skip to content

Commit

Permalink
Merge pull request #154 from groldan/ows_through_proxy
Browse files Browse the repository at this point in the history
Cascaded OWS Stores with HTTP proxy externalized configuration
  • Loading branch information
groldan authored Jan 13, 2022
2 parents b54e73e + bdd2beb commit e20506a
Show file tree
Hide file tree
Showing 10 changed files with 725 additions and 1 deletion.
2 changes: 1 addition & 1 deletion config
91 changes: 91 additions & 0 deletions docs/configuration/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Cloud Native GeoServer externalized configuration guide

## Contents
{:.no_toc}

* Will be replaced with the ToC, excluding the "Contents" header
{:toc}

## Introduction

TBD

## GeoServer configuration properties

## HTTP proxy for cascaded OWS (WMS/WMTS/WFS) Stores

Cascaded OWS stores make use of a SPI (Service Provider Interface)
extension point to configure the appropriate GeoTools `HTTPClientFactory`.

We provide a Spring Boot Auto-configuration that can be configured
through regular spring-boot externalized properties and only affects
GeoTools HTTP clients instead of the whole JVM.

The usual way to set an http proxy is through the `http.proxyHost`, `http.proxyPort`,
`http.proxyUser`, `http.proxyPassword` Java System Properties.

In the context of Cloud Native GeoServer containerized applications,
this presents a number of drawbacks:

* Standard Java proxy parameters only work with System properties,
not OS environment variables, and setting system properties is more
cumbersome than env variables (you have to modify the container run command).
* `http.proxyUser/Password` are not standard properties, though commonly used, it's kind of
JDK implementation dependent.
* Setting `-Dhttp.proxy* System properties affects all HTTP clients in the container, meaning
requests to the config-service, discovery-service, etc., will also try to go through the proxy,
or you need to go through the extra burden of figuring out how to ignore them.
* If the proxy is secured, and since the http client used may not respect the
http.proxyUser/Password parameters, the apps won't start since they'll get
HTTP 407 "Proxy Authentication Required".

The following externalized configuration properties apply, with these suggested default values:

```yaml
# GeoTools HTTP Client proxy configuration, allows configuring cascaded WMS/WMTS/WFS stores
# that need to go through an HTTP proxy without affecting all the http clients at the JVM level
# These are default settings. The enabled property can be set to false to disable the custom
# HTTPClientFactory altogether.
# The following OS environment variables can be set for easier configuration:
# HTTP(S)_PROXYHOST, HTTP(S)_PROXYPORT, HTTP(S)_PROXYUSER, HTTP(S)_PROXYPASSWORD, HTTP(S)_NONPROXYHOSTS
geotools:
httpclient:
proxy:
enabled: true
http:
host: ${http.proxyHost:}
port: ${http.proxyPort:}
user: ${http.proxyUser:}
password: ${http.proxyPassword:}
nonProxyHosts: ${http.nonProxyHosts:localhost.*}
# comma separated list of Java regular expressions, e.g.: nonProxyHosts: localhost, example.*
https:
host: ${https.proxyHost:${geotools.httpclient.proxy.http.host}}
port: ${https.proxyPort:${geotools.httpclient.proxy.http.port}}
user: ${https.proxyUser:${geotools.httpclient.proxy.http.user}}
password: ${https.proxyPassword:${geotools.httpclient.proxy.http.password}}
nonProxyHosts: ${https.nonProxyHosts:${geotools.httpclient.proxy.http.nonProxyHosts}}
```
### Configure HTTP proxy with environment variables in docker-compose.yml
As mentioned above, regular JVM proxy configuration works with Java System properties
but not with Operating System environment variables.
The above `geotools.httpclient.proxy` config properties though allow to do so
easily as in the following `docker-compose.yml` snippet:

```yaml
version: "3.8"
...
services:
...
wms:
image: geoservercloud/geoserver-cloud-wms:<version>
environment:
HTTP_PROXYHOST: 192.168.86.26
HTTP_PROXYPORT: 80
HTTP_PROXYUSER: jack
HTTP_PROXYPASSWORD: insecure
...
```
4 changes: 4 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,3 +213,7 @@ Things to mention
** Gateway: custom filter to adhere to OWS case-insensitive parameter names
-->

# Configuration guide

Check out the [Externalized configuration guide](configuration/index.md) to learn about fine tuning GeoServer-Cloud applications.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package org.geoserver.cloud.autoconfigure.catalog;

import org.geoserver.cloud.config.catalog.CoreBackendConfiguration;
import org.geotools.autoconfigure.httpclient.GeoToolsHttpClientAutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
Expand All @@ -19,6 +20,7 @@
* (provided their configs are included in this class' {@code @Import} list)
*/
@Configuration(proxyBeanMethods = false)
@AutoConfigureAfter(GeoToolsHttpClientAutoConfiguration.class)
@Import({ //
CoreBackendConfiguration.class, //
DataDirectoryAutoConfiguration.class, //
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* (c) 2021 Open Source Geospatial Foundation - all rights reserved This code is licensed under the
* GPL 2.0 license, available at the root application directory.
*/
package org.geotools.autoconfigure.httpclient;

import lombok.extern.slf4j.Slf4j;
import org.geotools.autoconfigure.httpclient.ProxyConfig.ProxyHostConfig;
import org.geotools.http.HTTPClientFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* {@link EnableAutoConfiguration @EnableAutoConfiguration} auto configuration for a GeoTools {@link
* HTTPClientFactory} that can be configured through spring-boot externalized properties and only
* affects GeoTools http clients instead of the whole JVM.
*
* <p>The usual way to set an http proxy is through the {@literal http.proxyHost}, {@literal
* http.proxyPort}, {@literal http.proxyUser}, {@literal http.proxyPassword} Java System Properties.
*
* <p>In the context of Cloud Native GeoServer containerized applications, this has a number of
* drawbacks:
*
* <ul>
* <li>Standard java proxy parameters only work with System properties, not env variables (at
* least with the apache http client), and setting system properties is more cumbersome than
* env variables (you have to modify the container run command)
* <li>{@literal http.proxyUser/Password} are not standard properties, though commonly used, it's
* kind of JDK implementation dependent.
* <li>Setting {@literal -Dhtt.proxy*} System properties affects all HTTP clients in the
* container, meaning requests to the {@literal config-service}, {@literal discovery-service},
* etc., will also try to go through the proxy, or you need to go through the extra burden of
* figuring out how to ignore them.
* <li>If the proxy is secured, and since the http client used may not respect the {@literal
* http.proxyUser/Password} parameters, the apps won't start since they'll get HTTP 407 "Proxy
* Authentication Required".
* </ul>
*
* <p>The following externalized configuration properties apply:
*
* <pre>
* <code>
* geotools:
* httpclient:
* proxy:
* # defaults to true, false disables the autoconfiguration and falls back to standard GeoServer behavior
* enabled: true
* http:
* host:
* port:
* user:
* password:
* nonProxyHosts:
* # comma separated list of Java regular expressions, e.g.: nonProxyHosts: localhost, example.*
* https:
* host:
* port:
* user:
* password:
* nonProxyHosts:
* </code>
* </pre>
*/
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties
@ConditionalOnProperty(
prefix = "geotools.httpclient.proxy",
name = "enabled",
havingValue = "true",
matchIfMissing = true
)
@Slf4j(topic = "org.geotools.autoconfigure.httpclient")
public class GeoToolsHttpClientAutoConfiguration {

@ConfigurationProperties(prefix = "geotools.httpclient.proxy")
public @Bean ProxyConfig geoToolsHttpProxyConfiguration() {
System.setProperty(
"HTTP_CLIENT_FACTORY",
SpringEnvironmentAwareGeoToolsHttpClientFactory.class.getCanonicalName());
return new ProxyConfig();
}

public @Bean SpringEnvironmentAwareGeoToolsHttpClientFactory
springEnvironmentAwareGeoToolsHttpClientFactory(@Autowired ProxyConfig proxyConfig) {

log.info("Using spring environment aware GeoTools HTTPClientFactory");
log(proxyConfig.getHttp(), "HTTP");
log(proxyConfig.getHttps(), "HTTPS");
SpringEnvironmentAwareGeoToolsHttpClientFactory.setProxyConfig(proxyConfig);

return new SpringEnvironmentAwareGeoToolsHttpClientFactory();
}

private void log(ProxyHostConfig config, String protocol) {
config.host()
.ifPresentOrElse(
host ->
log.info(
"{} proxy configured for GeoTools cascaded OWS stores: {}:{}, secured: {}",
protocol,
host,
config.port(),
config.isSecured()),
() ->
log.info(
"No {} proxy configured for GeoTools cascaded OWS stores",
protocol));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* (c) 2021 Open Source Geospatial Foundation - all rights reserved This code is licensed under the
* GPL 2.0 license, available at the root application directory.
*/
package org.geotools.autoconfigure.httpclient;

import java.util.List;
import java.util.Optional;
import java.util.regex.Pattern;
import lombok.Data;
import lombok.NonNull;
import org.springframework.util.StringUtils;

/** */
public @Data class ProxyConfig {

private boolean enabled = true;
private ProxyHostConfig http = new ProxyHostConfig();
private ProxyHostConfig https = new ProxyHostConfig();

public static @Data class ProxyHostConfig {
private String host;
private Integer port;
private String user;
private String password;
private List<Pattern> nonProxyHosts;

public Optional<ProxyHostConfig> forHost(@NonNull String targetHostname) {
if (host().isEmpty()) {
return Optional.empty();
}
for (Pattern p : nonProxyHosts()) {
if (p.matcher(targetHostname).matches()) {
return Optional.empty();
}
}
return Optional.of(this);
}

public List<Pattern> nonProxyHosts() {
return this.nonProxyHosts == null ? List.of() : this.nonProxyHosts;
}

public Optional<String> host() {
return StringUtils.hasLength(this.host) ? Optional.of(this.host) : Optional.empty();
}

public int port() {
return port == null ? 80 : port.intValue();
}

public boolean isSecured() {
return StringUtils.hasLength(host)
&& StringUtils.hasLength(user)
&& StringUtils.hasLength(password);
}
}

public ProxyHostConfig ofProtocol(@NonNull String protocol) {
if ("http".equals(protocol)) return http == null ? new ProxyHostConfig() : http;
if ("https".equals(protocol)) return https == null ? new ProxyHostConfig() : https;
throw new IllegalArgumentException("Uknown protocol " + protocol + ". Expected http(s)");
}
}
Loading

0 comments on commit e20506a

Please sign in to comment.