Skip to content

Instantly share code, notes, and snippets.

@AdrianoJS
Last active April 18, 2023 05:07
Show Gist options
  • Save AdrianoJS/22a6b68e49a736a7ec5bf5b74941025b to your computer and use it in GitHub Desktop.
Save AdrianoJS/22a6b68e49a736a7ec5bf5b74941025b to your computer and use it in GitHub Desktop.
package no.embriq.api.gateway.filter;
import com.fasterxml.jackson.databind.JsonNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.MediaType;
import org.springframework.http.client.reactive.ClientHttpResponseDecorator;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import java.time.Duration;
import static java.nio.charset.StandardCharsets.UTF_8;
@Component
public class AddAndSubtractCompositionFilter extends AbstractGatewayFilterFactory<Object> {
private static final Logger LOG = LoggerFactory.getLogger(AddAndSubtractCompositionFilter.class);
@Value("${app.dependencies.foo.baseUrl}/api/add?")
private String fooUrl;
@Value("${app.dependencies.bar.baseUrl}/api/subtract?")
private String barUrl;
@Override
public GatewayFilter apply(final Object ignored) {
return ((exchange, chain) -> {
var response = exchange.getResponse();
var compositionResult = executeComposition(exchange.getRequest())
.map(s -> convertToDataBuffer(response, s))
.timeout(Duration.ofSeconds(5))
.doOnSuccess(dataBuffer -> {
printDataBufferContent(dataBuffer);
response.setStatusCode(HttpStatusCode.valueOf(200));
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
});
return response.writeWith(compositionResult);
});
}
@Override
public String name() {
return "CompositionFilter";
}
private DataBuffer convertToDataBuffer(final ServerHttpResponse response, final String s) {
return response.bufferFactory().wrap(s.getBytes(UTF_8));
}
/**
* Dangerous!
* <p>
* Could run out of memory if large object as entire buffer is read into memory.
* Only here for educational purposes.
*
* @param dataBuffer
*/
private void printDataBufferContent(final DataBuffer dataBuffer) {
var bufferAsBytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(bufferAsBytes);
var bufferAsString = new String(bufferAsBytes);
LOG.info("Composition result was '{}'", bufferAsString);
// Necessary as we just read the buffer.
// No result would be returned to the caller otherwise!
// Could also rewrite to the buffer using dataBuffer.readPosition(0).writePosition(0).write(bufferAsBytes)
// This is a possible technique to rewrite the response
// Instead of reading the buffer, you could execute e.g. dataBuffer.readPosition(dataBuffer.writePosition())
// Requires there to be more space in the buffer if not starting from 0
// Useful in combination with ServerHttpResponseDecorator
// If a different response was to be sent, you would have to set the content-length header to the new value.
// e.g. exchange.getResponse().getHeaders().setContentLength(int)
// If content-length is not set, you risk either cutting the response short or hanging indefinitely waiting for the remainder of the response.
dataBuffer.readPosition(0);
}
/**
* All input SHOULD be validated and content should be scanned for security threats,
* e.g. XSS, content bombs (zip, xml etc.), size (DOS by using up available memory) etc.
* <p>
* Skipped for simplicity
*
* @param request
* @return
*/
private Mono<String> executeComposition(final ServerHttpRequest request) {
var client = WebClient.builder().build();
var queryParams = request.getQueryParams();
var additionQueryParams = "a=" + queryParams.get("a").get(0) + "&b=" + queryParams.get("b").get(0);
var subtractionQueryParams = "a=%s&b=" + queryParams.get("c").get(0);
return client.get()
.uri(fooUrl + additionQueryParams) // Should validate query is non-threatening
.retrieve()
.onStatus(HttpStatusCode::isError, ClientResponse::createError)
.bodyToMono(JsonNode.class)
.flatMap(additionResponse -> client.get()
.uri(barUrl + subtractionQueryParams.formatted(additionResponse.get("result").asText()))
.retrieve()
.onStatus(HttpStatusCode::isError, ClientResponse::createError)
.bodyToMono(String.class)
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment