Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,61 +1,5 @@
package co.worklytics.psoxy.gateway.impl;

import java.io.IOException;
import java.net.ConnectException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpHead;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.NameValuePair;
import org.apache.hc.core5.http.message.BasicNameValuePair;
import org.apache.hc.core5.net.WWWFormCodec;
import org.apache.http.HttpStatus;
import org.apache.http.client.utils.URIBuilder;
import com.avaulta.gateway.pseudonyms.PseudonymImplementation;
import com.avaulta.gateway.pseudonyms.impl.UrlSafeTokenPseudonymEncoder;
import com.avaulta.gateway.tokens.ReversibleTokenizationStrategy;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.api.client.http.ByteArrayContent;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpContent;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpRequestFactory;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.HttpTransport;
import com.google.auth.Credentials;
import com.google.auth.http.HttpCredentialsAdapter;
import com.google.auth.http.HttpTransportFactory;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import co.worklytics.psoxy.ControlHeader;
import co.worklytics.psoxy.ErrorCauses;
import co.worklytics.psoxy.ProcessedDataMetadataFields;
Expand All @@ -69,6 +13,7 @@
import co.worklytics.psoxy.gateway.HttpEventRequest;
import co.worklytics.psoxy.gateway.HttpEventResponse;
import co.worklytics.psoxy.gateway.ProcessedContent;
import co.worklytics.psoxy.gateway.ProxyConstants;
import co.worklytics.psoxy.gateway.SecretStore;
import co.worklytics.psoxy.gateway.SourceAuthStrategy;
import co.worklytics.psoxy.gateway.impl.output.OutputUtils;
Expand All @@ -81,6 +26,28 @@
import co.worklytics.psoxy.utils.ComposedHttpRequestInitializer;
import co.worklytics.psoxy.utils.GzipedContentHttpRequestInitializer;
import co.worklytics.psoxy.utils.URLUtils;
import com.avaulta.gateway.pseudonyms.PseudonymImplementation;
import com.avaulta.gateway.pseudonyms.impl.UrlSafeTokenPseudonymEncoder;
import com.avaulta.gateway.tokens.ReversibleTokenizationStrategy;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.api.client.http.ByteArrayContent;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpContent;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpRequestFactory;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.HttpTransport;
import com.google.auth.Credentials;
import com.google.auth.http.HttpCredentialsAdapter;
import com.google.auth.http.HttpTransportFactory;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import dagger.Lazy;
import lombok.AllArgsConstructor;
import lombok.Builder;
Expand All @@ -90,7 +57,32 @@
import lombok.SneakyThrows;
import lombok.Value;
import lombok.extern.java.Log;
import co.worklytics.psoxy.gateway.ProxyConstants;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpHead;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.NameValuePair;
import org.apache.hc.core5.http.message.BasicNameValuePair;
import org.apache.hc.core5.net.WWWFormCodec;
import org.apache.http.HttpStatus;
import org.apache.http.client.utils.URIBuilder;

import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
import java.io.IOException;
import java.net.ConnectException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.*;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

@NoArgsConstructor(onConstructor_ = @Inject)
@Log
Expand Down Expand Up @@ -241,6 +233,15 @@ public HttpEventResponse handle(HttpEventRequest requestToProxy,
// our canonicalization code for this to go wrong
log.log(Level.WARNING, "Error parsing / building request URL", e);

// InvalidTokenException extends RuntimeException
if (e instanceof ReversibleTokenizationStrategy.InvalidTokenException ite) {
return HttpEventResponse.builder()
.statusCode(HttpStatus.SC_UNPROCESSABLE_ENTITY)
.header(ProcessedDataMetadataFields.ERROR.getHttpHeader(),
ite.getErrorCode().name())
.build();
}
Comment on lines +236 to +243

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The newly added behavior that surfaces InvalidTokenException details during URL building (and the associated status/header/body mapping) doesn’t appear to be covered by tests. Please add/extend ApiDataRequestHandlerTest to assert the response status and X-Psoxy-Error header (and any new detail header/body) for this failure mode, so future refactors don’t silently change the contract.

Copilot uses AI. Check for mistakes.

return HttpEventResponse.builder()
.statusCode(HttpStatus.SC_INTERNAL_SERVER_ERROR)
.header(ProcessedDataMetadataFields.ERROR.getHttpHeader(),
Expand Down Expand Up @@ -375,7 +376,7 @@ public HttpEventResponse handle(HttpEventRequest requestToProxy,
if (isSocketTimeoutException(e)) {
return buildNetworkTimeoutErrorResponse(builder, e);
}

builder.statusCode(HttpStatus.SC_INTERNAL_SERVER_ERROR);
builder.body("Failed to parse request; review logs");
builder.header(ProcessedDataMetadataFields.ERROR.getHttpHeader(),
Expand All @@ -394,9 +395,10 @@ public HttpEventResponse handle(HttpEventRequest requestToProxy,
log.log(Level.WARNING, e.getMessage(), e);
return builder.build();
} catch (ReversibleTokenizationStrategy.InvalidTokenException e) {
builder.statusCode(HttpStatus.SC_CONFLICT);
builder.statusCode(HttpStatus.SC_UNPROCESSABLE_ENTITY);
builder.header(ProcessedDataMetadataFields.ERROR.getHttpHeader(),
ErrorCauses.TOKENIZED_REQUEST_PARAMETER_INVALID.name());
e.getErrorCode().name());

log.log(Level.WARNING, e.getMessage(), e);
return builder.build();
}
Expand Down Expand Up @@ -1045,7 +1047,7 @@ private boolean isSocketTimeoutException(Throwable throwable) {
*/
private HttpEventResponse buildNetworkTimeoutErrorResponse(
HttpEventResponse.HttpEventResponseBuilder builder, Throwable e) {

builder.statusCode(HttpStatus.SC_BAD_GATEWAY);
builder.body("Network timeout: unable to connect to target API. " +
"This could indicate: " +
Expand All @@ -1054,12 +1056,12 @@ private HttpEventResponse buildNetworkTimeoutErrorResponse(
"If using VPC connector, verify: VPC connector is active, CIDR range is correct, firewall allows egress, Cloud NAT is configured.");
builder.header(ProcessedDataMetadataFields.ERROR.getHttpHeader(),
ErrorCauses.NETWORK_EGRESS_BLOCKED.name());

log.log(Level.SEVERE, "SocketTimeoutException: Network timeout connecting to target API", e);
log.log(Level.SEVERE, "Possible causes: " +
"1) Proxy network egress blocked - check VPC connector configuration, firewall rules, Cloud NAT; " +
"2) Target API unreachable or slow - check API status, DNS resolution");

return builder.build();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.avaulta.gateway.tokens;

import lombok.Getter;

import java.util.function.Function;

/**
Expand Down Expand Up @@ -40,12 +42,31 @@ default byte[] getReversibleToken(String originalDatum) {


/**
* Indicates that the token could not be reversed, likely because invalid
*
* Indicates that the token could not be reversed, likely because it is invalid.
*/
class InvalidTokenException extends RuntimeException {
public InvalidTokenException(String message, Throwable cause) {
super(message, cause);

@Getter
public enum ErrorCode {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe not the best approach, but for certain cases (rules exceptions, this, and access errors) could be helpful to exactly know what's going on rather than a generic error

ALGORITHM_PARAMETER_ERROR("Failed to decrypt token; some algorithm parameter, such as iv, is wrong"),
BAD_PADDING("Failed to decrypt token; token appears to be corrupted or invalid, due to mismatch between token's padding and the padding expected by the cipher mode"),
ILLEGAL_BLOCK_SIZE("Failed to decrypt token; token appears to be corrupted or invalid, as block size seems to differ from expected"),
DECRYPTION_FAILED("Failed to decrypt token; most likely because encryption key has been rotated");

private final String message;

ErrorCode(String message) {
this.message = message;
}

}

@Getter
private final ErrorCode errorCode;

public InvalidTokenException(ErrorCode errorCode, Throwable cause) {
super(errorCode.name() + ": " + errorCode.getMessage(), cause);
this.errorCode = errorCode;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package com.avaulta.gateway.tokens.impl;

import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.KeySpec;
import java.util.Arrays;
import java.util.function.Function;
import com.avaulta.gateway.tokens.DeterministicTokenizationStrategy;
import com.avaulta.gateway.tokens.ReversibleTokenizationStrategy;
import lombok.Builder;
import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.Value;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
Expand All @@ -16,14 +18,13 @@
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import com.avaulta.gateway.tokens.DeterministicTokenizationStrategy;
import com.avaulta.gateway.tokens.ReversibleTokenizationStrategy;
import lombok.Builder;
import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.Value;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.KeySpec;
import java.util.Arrays;
import java.util.function.Function;

@Builder
@RequiredArgsConstructor
Expand Down Expand Up @@ -130,13 +131,13 @@ public String getOriginalDatum(@NonNull byte[] reversibleToken) throws Reversibl
} catch (InvalidKeyException e) {
throw new RuntimeException("Invalid encryption key configuration", e);
} catch (InvalidAlgorithmParameterException e) {
throw new InvalidTokenException("Failed to decrypt token; some algorithm parameter, such as iv, is wrong", e);
throw new InvalidTokenException(InvalidTokenException.ErrorCode.ALGORITHM_PARAMETER_ERROR, e);
} catch (BadPaddingException e) {
throw new InvalidTokenException("Failed to decrypt token; token appears to be corrupted or invalid, due to mismatch between token's padding and the padding expected by the cipher mode", e);
throw new InvalidTokenException(InvalidTokenException.ErrorCode.BAD_PADDING, e);
} catch (IllegalBlockSizeException e) {
throw new InvalidTokenException("Failed to decrypt token; token appears to be corrupted or invalid, as block size seems to differ from expected", e);
throw new InvalidTokenException(InvalidTokenException.ErrorCode.ILLEGAL_BLOCK_SIZE, e);
} catch (RuntimeException e) {
throw new InvalidTokenException("Failed to decrypt token; most likely because encryption key has been rotated", e);
throw new InvalidTokenException(InvalidTokenException.ErrorCode.DECRYPTION_FAILED, e);
}
}

Expand Down
Loading