Monday, August 12, 2019

Proxy connection manager service for Spring Boot

In some special cases, like company security policies, our services are blocked by firewalls, the only chance to use the internet connection for our software is to setup an HTTP/HTTPS/FTP/etc. proxy connection. In this article I'll introduce my idea to solve this problem, when our webservices based on Spring Boot.

With this, we don't have to deal with system properties: (https://docs.oracle.com/javase/6/docs/technotes/guides/net/proxies.html).

First of all, let's create our Maven project with the following pom.xml:

<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>hu.szrnkapeter.proxyservice</groupId>
<artifactId>proxyservice</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>ProxyService</name>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.2.RELEASE</version>
</parent>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
                <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
                <java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

I'd like to explain some parts of this pom.xml to describe what are they for. The following part means we want to use Java 8 as the basic JDK, and UTF-8 as default charset:

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>

We want to use Spring AOP aspects to enhance our validation:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

We want that our code changes should be deployed immediately:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>

We want to test our code:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

And we want to run it as a Spring Boot application:

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>

</build>

Now let's create the Application class, which bootstraps our Spring Boot application:

import java.io.IOException;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

public static void main(final String[] args) throws IOException {
SpringApplication.run(Application.class, args);
}
}

Ok, now we need a Spring Bean (interface + implementation) and a REST controller to manage our Proxy settings:

public interface ProxyService {

public void set(String key, String value);

public String get(ProxyKeyType key);
}

@Service
public class ProxyServiceImpl implements ProxyService {

private static final Logger LOG = LoggerFactory.getLogger(ProxyServiceImpl.class);

@Override
public void set(String key, String value) {
if(value == null) {
LOG.info("Key will be removed: {}", key);
System.clearProperty(key);
return;
}

LOG.info("Key modified: {}", key);
System.setProperty(key, value);
}

@Override
public String get(ProxyKeyType key) {
if(key == null) {
LOG.warn("Key is null!");
throw new IllegalArgumentException("Key is null!");
}

String propertyValue = System.getProperty(key.getKey(), key.getDefaultValue());
LOG.info("Key found: \"{}\"", propertyValue);
return propertyValue;
}
}

@RestController
public class ProxyController {

@Autowired
private ProxyService service;

@PostMapping(path = "/proxy/set", consumes = MimeTypeUtils.APPLICATION_JSON_VALUE)
public ResponseEntity<?> set(@RequestBody SetKeyValueDto dto) {
if(dto == null || dto.getKey() == null) {
return new ResponseEntity<>("Request body or key missing!", HttpStatus.INTERNAL_SERVER_ERROR);
}

service.set(dto.getKey(), dto.getValue());
return new ResponseEntity<>(HttpStatus.OK);
}

@GetMapping(path = "/proxy/get/{key}")
public ResponseEntity<String> get(@PathVariable("key") String key) {
return new ResponseEntity<>(service.get(ProxyKeyType.getByKey(key)), HttpStatus.OK);
}

}

Finally we need a DTO class for transfering data in our HTTP Post service:

public class SetKeyValueDto {

private String key;
private String value;

public SetKeyValueDto() {}

public SetKeyValueDto(String key, String value) {
super();
this.key = key;
this.value = value;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}

Yes, we could use Project Lombok, but there are many articles on the internet why not to use it. :) 

As you can see, I highlighted some parts of my source code. This ProxyKeyType class is an enum, which we use to restrict the possible values only to the Proxy related properties. Without this solution, we can modify all system properties, which would be a possible issue:

public enum ProxyKeyType {

HTTP_HOST("http.proxyHost", ""), //
HTTPS_HOST("https.proxyHost", ""), //
HTTP_PORT("http.proxyPort", ""), //
HTTPS_PORT("https.proxyPort", ""), //
HTTP_USERNAME("http.proxyUser", ""), //
HTTPS_USERNAME("https.proxyUser", ""), //
HTTP_PASSWORD("http.proxyPassword", ""), //
HTTPS_PASSWORD("https.proxyPassword", ""),
FTP_HOST("ftp.proxyHost", ""), //
FTP_PORT("ftp.proxyPort", ""), //
FTP_NON_HOST("ftp.nonProxyHosts", ""), //
SOCKS_HOST("socksProxyHost", ""), //
SOCKS_PORT("socksProxyPort ", "");

private String key;
private String defaultValue;
private static Map<String, ProxyKeyType> keyMap;

static {
keyMap = new HashMap<>();
for(ProxyKeyType type : values()) {
keyMap.put(type.key, type);
}
}

private ProxyKeyType(String k, String v) {
key = k;
defaultValue = v;
}
public String getKey() {
return key;
}
public String getDefaultValue() {
return defaultValue;
}
public static ProxyKeyType getByKey(String key) {
return keyMap.get(key);
}
}

As you can see, this is not a normal, simple enum. I enhanced it with some extra functionality. When the controller receives the DTO, we got String, and with getByKey(String key), we can get the proper ProxyKeyType by the system property name. And we can define a default value as well.

Now the last function we should implement is a security filter to avoid unappropriate system property updates. We achieve this by creating an AOP aspect. We only have to secure the set() service:

@Aspect
@Component
public class CheckPropertyAspect {

private static final Logger LOG = LoggerFactory.getLogger(CheckPropertyAspect.class);

@Around("execution(* hu.szrnkapeter.proxy.service.ProxyService.set(..))")
    public Object check(ProceedingJoinPoint joinPoint) throws Throwable {
    String key = (String) joinPoint.getArgs()[0];
    ProxyKeyType keyType = ProxyKeyType.getByKey(key);
    if(keyType == null) {
    LOG.warn("Invalid key type: {}", key);
    return null;
    }

    return joinPoint.proceed();
    }
}

We get the key from the request, and we check it if the given property is enabled by the ProxyKeyType. If not, we break the process.

Ok, we have everything now, let's test it with a unittest!

@RunWith(SpringRunner.class)
@SpringBootTest
public class ProxyServiceSpringBootTest {

private static final String ASSERT_WRONG_HOST = "Wrong host in response!";
private static final String LOCALHOST = "127.0.0.1";

@Autowired
private ProxyService service;

@Test
public void test001_validKey() {
service.set(ProxyKeyType.HTTP_HOST.getKey(), LOCALHOST);
String response = service.get(ProxyKeyType.HTTP_HOST);
Assert.assertEquals(ASSERT_WRONG_HOST, System.getProperty(ProxyKeyType.HTTP_HOST.getKey()), response);
}

@Test
public void test002_invvalidKey() {
service.set("invalid_key", LOCALHOST);
String response = service.get(ProxyKeyType.HTTP_HOST);
Assert.assertEquals(ASSERT_WRONG_HOST, "", response);
}
}

If you check the results, you can find this:
2019-08-12 07:30:14.390  INFO 1012 --- [           main] h.s.proxy.ProxyServiceSpringBootTest     : Started ProxyServiceSpringBootTest in 4.616 seconds (JVM running for 6.77)
2019-08-12 07:30:14.827  WARN 1012 --- [           main] h.szrnkapeter.proxy.CheckPropertyAspect  : Invalid key type: invalid_key
2019-08-12 07:30:14.839  INFO 1012 --- [           main] h.s.proxy.service.ProxyServiceImpl       : Key found: ""
2019-08-12 07:30:14.847  INFO 1012 --- [           main] h.s.proxy.service.ProxyServiceImpl       : Key modified: http.proxyHost
2019-08-12 07:30:14.847  INFO 1012 --- [           main] h.s.proxy.service.ProxyServiceImpl       : Key found: "127.0.0.1"

The whole project is available on Github:

Configure and use VSCode for Java web development

Embarking on Java web development often starts with choosing the right tools that streamline the coding process while enhancing productivity...