diff --git a/core/pom.xml b/core/pom.xml index 63dcce4c7..51db5664e 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -299,7 +299,6 @@ org.apache.httpcomponents httpclient - @@ -432,4 +431,4 @@ - \ No newline at end of file + diff --git a/core/src/main/java/com/predic8/membrane/core/azure/AzureDns.java b/core/src/main/java/com/predic8/membrane/core/azure/AzureDns.java new file mode 100644 index 000000000..f26310f81 --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/azure/AzureDns.java @@ -0,0 +1,73 @@ +package com.predic8.membrane.core.azure; + +import com.predic8.membrane.annot.MCAttribute; +import com.predic8.membrane.annot.MCElement; +import com.predic8.membrane.core.config.security.acme.AcmeValidation; + +@MCElement(topLevel = false, name = "azureDns") +public class AzureDns extends AcmeValidation { + + private String dnsZoneName; + private String subscriptionId; + private String tenantId; + private String resourceGroup; + private String resource = "https://management.azure.com"; + private AzureIdentity identity; + + public String getDnsZoneName() { + return dnsZoneName; + } + + @MCAttribute + public void setDnsZoneName(String dnsZoneName) { + this.dnsZoneName = dnsZoneName; + } + + public String getSubscriptionId() { + return subscriptionId; + } + + @MCAttribute + public void setSubscriptionId(String subscriptionId) { + this.subscriptionId = subscriptionId; + } + + public String getTenantId() { + if (identity != null) { + return identity.getTenantId(); + } + return tenantId; + } + + @MCAttribute + public void setTenantId(String tenantId) { + this.tenantId = tenantId; + } + + public String getResourceGroup() { + return resourceGroup; + } + + @MCAttribute + public void setResourceGroup(String resourceGroup) { + this.resourceGroup = resourceGroup; + } + + public String getResource() { + return resource; + } + + @MCAttribute + public void setResource(String resource) { + this.resource = resource; + } + + public AzureIdentity getIdentity() { + return identity; + } + + @MCAttribute + public void setIdentity(AzureIdentity identity) { + this.identity = identity; + } +} diff --git a/core/src/main/java/com/predic8/membrane/core/azure/AzureIdentity.java b/core/src/main/java/com/predic8/membrane/core/azure/AzureIdentity.java new file mode 100644 index 000000000..8a1fe5ad5 --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/azure/AzureIdentity.java @@ -0,0 +1,59 @@ +package com.predic8.membrane.core.azure; + +import com.predic8.membrane.annot.MCAttribute; +import com.predic8.membrane.annot.MCElement; + +@MCElement(name = "azureIdentity") +public class AzureIdentity { + + private String grantType = "client_credentials"; + private String clientId; + private String clientSecret; + private String resource = "https://management.azure.com"; + private String tenantId; + + public String getGrantType() { + return grantType; + } + + @MCAttribute + public void setGrantType(String grantType) { + this.grantType = grantType; + } + + public String getClientId() { + return clientId; + } + + @MCAttribute + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getClientSecret() { + return clientSecret; + } + + @MCAttribute + public void setClientSecret(String clientSecret) { + this.clientSecret = clientSecret; + } + + public String getResource() { + return resource; + } + + @MCAttribute + public void setResource(String resource) { + this.resource = resource; + } + + public String getTenantId() { + return tenantId; + } + + @MCAttribute + public void setTenantId(String tenantId) { + this.tenantId = tenantId; + } +} diff --git a/core/src/main/java/com/predic8/membrane/core/azure/AzureTableStorage.java b/core/src/main/java/com/predic8/membrane/core/azure/AzureTableStorage.java new file mode 100644 index 000000000..31dff3173 --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/azure/AzureTableStorage.java @@ -0,0 +1,72 @@ +package com.predic8.membrane.core.azure; + +import com.predic8.membrane.annot.MCAttribute; +import com.predic8.membrane.annot.MCChildElement; +import com.predic8.membrane.annot.MCElement; +import com.predic8.membrane.core.config.security.acme.AcmeSynchronizedStorage; +import com.predic8.membrane.core.transport.http.client.HttpClientConfiguration; + +@MCElement(name = "azureTableStorage", topLevel = false) +public class AzureTableStorage implements AcmeSynchronizedStorage { + + private String storageAccountName; + private String storageAccountKey; + private String tableName = "membrane"; + private String partitionKey = "acme"; + private HttpClientConfiguration httpClientConfiguration; + + private String customHost; + + public String getCustomHost() { + return customHost; + } + + public void setCustomHost(String customHost) { + this.customHost = customHost; + } + + public String getStorageAccountName() { + return storageAccountName; + } + + @MCAttribute + public void setStorageAccountName(String storageAccountName) { + this.storageAccountName = storageAccountName; + } + + public String getStorageAccountKey() { + return storageAccountKey; + } + + @MCAttribute + public void setStorageAccountKey(String storageAccountKey) { + this.storageAccountKey = storageAccountKey; + } + + public String getTableName() { + return tableName; + } + + @MCAttribute + public void setTableName(String tableName) { + this.tableName = tableName; + } + + public String getPartitionKey() { + return partitionKey; + } + + @MCAttribute + public void setPartitionKey(String partitionKey) { + this.partitionKey = partitionKey; + } + + public HttpClientConfiguration getHttpClientConfiguration() { + return httpClientConfiguration; + } + + @MCChildElement + public void setHttpClientConfiguration(HttpClientConfiguration httpClientConfiguration) { + this.httpClientConfiguration = httpClientConfiguration; + } +} diff --git a/core/src/main/java/com/predic8/membrane/core/azure/api/AzureApiClient.java b/core/src/main/java/com/predic8/membrane/core/azure/api/AzureApiClient.java new file mode 100644 index 000000000..1f1ac85c6 --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/azure/api/AzureApiClient.java @@ -0,0 +1,56 @@ +package com.predic8.membrane.core.azure.api; + +import com.predic8.membrane.core.azure.AzureDns; +import com.predic8.membrane.core.azure.AzureIdentity; +import com.predic8.membrane.core.azure.AzureTableStorage; +import com.predic8.membrane.core.azure.api.auth.AuthenticationApi; +import com.predic8.membrane.core.azure.api.dns.DnsRecordApi; +import com.predic8.membrane.core.azure.api.tablestorage.TableStorageApi; +import com.predic8.membrane.core.transport.http.HttpClient; +import com.predic8.membrane.core.transport.http.HttpClientFactory; +import com.predic8.membrane.core.util.TimerManager; + +import javax.annotation.Nullable; + +public class AzureApiClient implements AutoCloseable { + + private final HttpClient httpClient; + private final AuthenticationApi authApi; + private final TableStorageApi tableStorageApi; + + + public AzureApiClient( + @Nullable AzureIdentity identityConfig, + AzureTableStorage tableStorage, + HttpClientFactory httpClientFactory + ) { + if (httpClientFactory == null) { + httpClientFactory = new HttpClientFactory(new TimerManager()); + } + this.httpClient = httpClientFactory.createClient(tableStorage.getHttpClientConfiguration()); + + authApi = new AuthenticationApi(httpClient, identityConfig); + tableStorageApi = new TableStorageApi(this, tableStorage); + } + + public DnsRecordApi dnsRecords(AzureDns dnsOperator) { + return new DnsRecordApi(this, dnsOperator); + } + + public TableStorageApi tableStorage() { + return tableStorageApi; + } + + public AuthenticationApi auth() { + return authApi; + } + + public HttpClient httpClient() { + return httpClient; + } + + @Override + public void close() throws Exception { + this.httpClient.close(); + } +} diff --git a/core/src/main/java/com/predic8/membrane/core/azure/api/HttpClientConfigurable.java b/core/src/main/java/com/predic8/membrane/core/azure/api/HttpClientConfigurable.java new file mode 100644 index 000000000..91ccd7c1e --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/azure/api/HttpClientConfigurable.java @@ -0,0 +1,8 @@ +package com.predic8.membrane.core.azure.api; + +import com.predic8.membrane.core.transport.http.HttpClient; + +public interface HttpClientConfigurable { + HttpClient http(); + T config(); +} diff --git a/core/src/main/java/com/predic8/membrane/core/azure/api/auth/AuthenticationApi.java b/core/src/main/java/com/predic8/membrane/core/azure/api/auth/AuthenticationApi.java new file mode 100644 index 000000000..9998f9b7c --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/azure/api/auth/AuthenticationApi.java @@ -0,0 +1,54 @@ +package com.predic8.membrane.core.azure.api.auth; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.predic8.membrane.core.azure.AzureIdentity; +import com.predic8.membrane.core.exchange.Exchange; +import com.predic8.membrane.core.http.Request; +import com.predic8.membrane.core.transport.http.HttpClient; + +import javax.annotation.Nullable; +import java.net.URISyntaxException; +import java.util.Map; +import java.util.stream.Collectors; + +public class AuthenticationApi { + + private final HttpClient http; + private final AzureIdentity config; + private final Map tokenPayload; + + public AuthenticationApi(HttpClient http, @Nullable AzureIdentity config) { + this.http = http; + this.config = config; + + if (config == null) { + tokenPayload = Map.of(); + return; + } + + tokenPayload = Map.of( + "grant_type", config.getGrantType(), + "client_id", config.getClientId(), + "client_secret", config.getClientSecret(), + "resource", config.getResource() + ); + } + + public String accessToken() throws Exception { + var response = http.call(tokenExchange()).getResponse(); + return new ObjectMapper() + .readTree(response.getBodyAsStringDecoded()) + .get("access_token") + .asText(); + } + + private Exchange tokenExchange() throws URISyntaxException { + var tenantId = config.getTenantId(); + return new Request.Builder() + .post("https://login.microsoftonline.com/" + tenantId + "/oauth2/token") + .body(tokenPayload.entrySet().stream() + .map(entry -> entry.getKey() + "=" + entry.getValue()) + .collect(Collectors.joining("&"))) + .buildExchange(); + } +} diff --git a/core/src/main/java/com/predic8/membrane/core/azure/api/dns/DnsProvisionable.java b/core/src/main/java/com/predic8/membrane/core/azure/api/dns/DnsProvisionable.java new file mode 100644 index 000000000..f561d7344 --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/azure/api/dns/DnsProvisionable.java @@ -0,0 +1,5 @@ +package com.predic8.membrane.core.azure.api.dns; + +public interface DnsProvisionable { + void provisionDns(String domain, String record); +} diff --git a/core/src/main/java/com/predic8/membrane/core/azure/api/dns/DnsRecordApi.java b/core/src/main/java/com/predic8/membrane/core/azure/api/dns/DnsRecordApi.java new file mode 100644 index 000000000..dc4c00ebd --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/azure/api/dns/DnsRecordApi.java @@ -0,0 +1,37 @@ +package com.predic8.membrane.core.azure.api.dns; + +import com.predic8.membrane.core.azure.AzureDns; +import com.predic8.membrane.core.azure.api.AzureApiClient; +import com.predic8.membrane.core.azure.api.HttpClientConfigurable; +import com.predic8.membrane.core.http.Request; +import com.predic8.membrane.core.transport.http.HttpClient; + +public class DnsRecordApi implements HttpClientConfigurable { + + private final AzureApiClient apiClient; + private final AzureDns config; + + public DnsRecordApi(AzureApiClient apiClient, AzureDns config) { + this.apiClient = apiClient; + this.config = config; + } + + public DnsRecordCommandExecutor txt(String name) { + return new DnsRecordCommandExecutor(this, name, DnsRecordType.TXT); + } + + protected Request.Builder requestBuilder() throws Exception { + return new Request.Builder() + .header("Authorization", "Bearer " + apiClient.auth().accessToken()); + } + + @Override + public HttpClient http() { + return apiClient.httpClient(); + } + + @Override + public AzureDns config() { + return config; + } +} diff --git a/core/src/main/java/com/predic8/membrane/core/azure/api/dns/DnsRecordCommandExecutor.java b/core/src/main/java/com/predic8/membrane/core/azure/api/dns/DnsRecordCommandExecutor.java new file mode 100644 index 000000000..96c4848e8 --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/azure/api/dns/DnsRecordCommandExecutor.java @@ -0,0 +1,74 @@ +package com.predic8.membrane.core.azure.api.dns; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class DnsRecordCommandExecutor { + + private final DnsRecordApi api; + private final String basePath; + private int ttl = 3600; + + private final List records = new ArrayList<>(); + + public DnsRecordCommandExecutor(DnsRecordApi api, String recordSetName, DnsRecordType type) { + this.api = api; + + var resourceBasePath = String.format("%s/subscriptions/%s/resourceGroups/%s/providers", + api.config().getResource(), + api.config().getSubscriptionId(), + api.config().getResourceGroup() + ); + + basePath = String.format("%s/Microsoft.Network/dnsZones/%s/%s/%s?api-version=2018-05-01", + resourceBasePath, + api.config().getDnsZoneName(), + type.toString(), + recordSetName + ); + } + + public DnsRecordCommandExecutor ttl(int ttl) { + this.ttl = ttl; + return this; + } + + public JsonNode create() throws Exception { + Map properties = new HashMap<>(Map.of("TTL", ttl)); + + records.forEach(record -> properties.putAll(record.payload())); + + var payload = Map.of("properties", properties); + + var response = api.http().call( + api.requestBuilder() + .put(basePath) + .contentType("application/json") + .body(new ObjectMapper().writeValueAsString(payload)) + .buildExchange() + ) + .getResponse(); + + return new ObjectMapper().readTree(response.getBodyAsStringDecoded()); + } + + public TxtRecordBuilder addRecord() { + var builder = new TxtRecordBuilder(this); + records.add(builder); + return builder; + } + + public void delete() throws Exception { + api.http().call( + api.requestBuilder() + .delete(basePath) + .header("Accept", "application/json") + .buildExchange() + ); + } +} diff --git a/core/src/main/java/com/predic8/membrane/core/azure/api/dns/DnsRecordType.java b/core/src/main/java/com/predic8/membrane/core/azure/api/dns/DnsRecordType.java new file mode 100644 index 000000000..eab82df1d --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/azure/api/dns/DnsRecordType.java @@ -0,0 +1,21 @@ +package com.predic8.membrane.core.azure.api.dns; + +public enum DnsRecordType { + TXT("txt") + ; + + private final String value; + + DnsRecordType(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + @Override + public String toString() { + return getValue(); + } +} diff --git a/core/src/main/java/com/predic8/membrane/core/azure/api/dns/SupportedDnsRecordType.java b/core/src/main/java/com/predic8/membrane/core/azure/api/dns/SupportedDnsRecordType.java new file mode 100644 index 000000000..b8332fc36 --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/azure/api/dns/SupportedDnsRecordType.java @@ -0,0 +1,8 @@ +package com.predic8.membrane.core.azure.api.dns; + +import java.util.List; +import java.util.Map; + +public interface SupportedDnsRecordType { + Map>>> payload(); +} diff --git a/core/src/main/java/com/predic8/membrane/core/azure/api/dns/TxtRecordBuilder.java b/core/src/main/java/com/predic8/membrane/core/azure/api/dns/TxtRecordBuilder.java new file mode 100644 index 000000000..906caf44a --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/azure/api/dns/TxtRecordBuilder.java @@ -0,0 +1,30 @@ +package com.predic8.membrane.core.azure.api.dns; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class TxtRecordBuilder implements SupportedDnsRecordType { + + private final DnsRecordCommandExecutor parent; + private final List values = new ArrayList<>(); + + public TxtRecordBuilder(DnsRecordCommandExecutor parent) { + this.parent = parent; + } + + public DnsRecordCommandExecutor withValue(String... values) { + Collections.addAll(this.values, values); + return parent; + } + + @Override + public Map>>> payload() { + return Map.of( + "TXTRecords", List.of( + Collections.singletonMap("value", List.of(String.join("", values))) + ) + ); + } +} diff --git a/core/src/main/java/com/predic8/membrane/core/azure/api/tablestorage/TableEntityCommandExecutor.java b/core/src/main/java/com/predic8/membrane/core/azure/api/tablestorage/TableEntityCommandExecutor.java new file mode 100644 index 000000000..177f5bdce --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/azure/api/tablestorage/TableEntityCommandExecutor.java @@ -0,0 +1,75 @@ +package com.predic8.membrane.core.azure.api.tablestorage; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.NoSuchElementException; + +public class TableEntityCommandExecutor { + + private final TableStorageApi api; + private final String path; + + public TableEntityCommandExecutor(TableStorageApi api, String rowKey) { + this.api = api; + + path = api.config().getCustomHost() == null + ? String.format("https://%s.table.core.windows.net/%s%s", + api.config().getStorageAccountName(), + api.config().getTableName(), + URLEncoder.encode("(PartitionKey='" + api.config().getPartitionKey() + "',RowKey='" + rowKey + "')", StandardCharsets.UTF_8)) + : String.format("%s/%s%s", + api.config().getCustomHost(), + api.config().getTableName(), + URLEncoder.encode("(PartitionKey='" + api.config().getPartitionKey() + "',RowKey='" + rowKey + "')", StandardCharsets.UTF_8)); + } + + public JsonNode get() throws Exception { + var res = api.http().call( + api.requestBuilder(path) + .get(path) + .buildExchange() + ).getResponse(); + + var response = new ObjectMapper().readTree(res.getBodyAsStringDecoded()); + + if (response.has("odata.error")) { + throw new NoSuchElementException(res.getBodyAsStringDecoded()); + } + + return response; + } + + public void insertOrReplace(String data) throws Exception { + var payload = Map.of("data", data); + + var exc = api.http().call( + api.requestBuilder(path) + .put(path) + .body(new ObjectMapper().writeValueAsString(payload)) + .buildExchange() + ); + + if (exc.getResponse().getStatusCode() != 204) { + throw new RuntimeException(exc.getResponse().toString()); + } + } + + public void delete() throws Exception { + var exc = api.http().call( + api.requestBuilder(path) + .delete(path) + .header("If-Match", "*") + .buildExchange() + ); + + var response = exc.getResponse(); + + if (response.getStatusCode() != 204) { + throw new RuntimeException(response.toString()); + } + } +} diff --git a/core/src/main/java/com/predic8/membrane/core/azure/api/tablestorage/TableStorageApi.java b/core/src/main/java/com/predic8/membrane/core/azure/api/tablestorage/TableStorageApi.java new file mode 100644 index 000000000..fff7093b8 --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/azure/api/tablestorage/TableStorageApi.java @@ -0,0 +1,96 @@ +package com.predic8.membrane.core.azure.api.tablestorage; + +import com.predic8.membrane.core.azure.AzureTableStorage; +import com.predic8.membrane.core.azure.api.AzureApiClient; +import com.predic8.membrane.core.azure.api.HttpClientConfigurable; +import com.predic8.membrane.core.http.Request; +import com.predic8.membrane.core.transport.http.HttpClient; + +import javax.annotation.Nullable; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.Base64; +import java.util.Locale; + +public class TableStorageApi implements HttpClientConfigurable { + + private final AzureApiClient apiClient; + private final AzureTableStorage config; + + public TableStorageApi(AzureApiClient apiClient, AzureTableStorage config) { + this.apiClient = apiClient; + this.config = config; + } + + public TableStorageCommandExecutor table() { + return new TableStorageCommandExecutor(this); + } + + public TableEntityCommandExecutor entity(String rowKey) { + return new TableEntityCommandExecutor(this, rowKey); + } + + @Override + public HttpClient http() { + return apiClient.httpClient(); + } + + @Override + public AzureTableStorage config() { + return config; + } + + protected Request.Builder requestBuilder(@Nullable String announceUrl) throws Exception { + var date = now(); + + var stringToSign = buildStringToSign(date, announceUrl); + var storageAccountKey = config.getStorageAccountKey(); + + var signature = sign(storageAccountKey, stringToSign); + + var storageAccountName = config.getStorageAccountName(); + + return new Request.Builder() + .contentType("application/json") + .header("Date", date) + .header("x-ms-version", "2020-12-06") + .header("Accept", "application/json") + .header("DataServiceVersion", "3.0;NetFx") + .header("MaxDataServiceVersion", "3.0;NetFx") + .header("Authorization", + String.format("SharedKeyLite %s:%s", storageAccountName, signature)); + } + + private String now() { + return DateTimeFormatter + .ofPattern("EEE, dd MMM yyyy HH:mm:ss z", Locale.ENGLISH) + .withZone(ZoneId.of("GMT")) + .format(Instant.now()); + } + + private String buildStringToSign(String date, @Nullable String url) { + var storageAccount = config.getStorageAccountName(); + var base = String.format("%s\n/%s", date, storageAccount); + + if (url == null) { + return base + "/Tables"; + } + + return base + url.substring(url.lastIndexOf("/" + config.getTableName())); + } + + private String sign(String base64Key, String stringToSign) throws Exception { + var algo = "HmacSHA256"; + + byte[] key = java.util.Base64.getDecoder().decode(base64Key); + Mac hmacSHA256 = Mac.getInstance(algo); + hmacSHA256.init(new SecretKeySpec(key, algo)); + byte[] utf8Bytes = stringToSign.getBytes(StandardCharsets.UTF_8); + + return Base64.getEncoder().encodeToString(hmacSHA256.doFinal(utf8Bytes)); + } +} diff --git a/core/src/main/java/com/predic8/membrane/core/azure/api/tablestorage/TableStorageCommandExecutor.java b/core/src/main/java/com/predic8/membrane/core/azure/api/tablestorage/TableStorageCommandExecutor.java new file mode 100644 index 000000000..fcbcdfb18 --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/azure/api/tablestorage/TableStorageCommandExecutor.java @@ -0,0 +1,41 @@ +package com.predic8.membrane.core.azure.api.tablestorage; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.util.Map; + +public class TableStorageCommandExecutor { + + private final TableStorageApi api; + private final String path; + + public TableStorageCommandExecutor(TableStorageApi api) { + this.api = api; + + path = api.config().getCustomHost() == null + ? String.format("https://%s.table.core.windows.net/Tables", api.config().getStorageAccountName()) + : api.config().getCustomHost() + "/Tables"; + } + + public void create() throws Exception { + var payload = Map.of( + "TableName", api.config().getTableName() + ); + + var exc = api.http().call( + api.requestBuilder(null) + .post(path) + .body(new ObjectMapper().writeValueAsString(payload)) + .buildExchange() + ); + + var response = exc.getResponse(); + + var isCreated = response.getStatusCode() == 201; + var isExisting = response.getStatusCode() == 409; + + if (!(isCreated || isExisting)) { + throw new RuntimeException(exc.getResponse().toString()); + } + } +} diff --git a/core/src/main/java/com/predic8/membrane/core/config/security/acme/AcmeValidation.java b/core/src/main/java/com/predic8/membrane/core/config/security/acme/AcmeValidation.java index 30e54a5a7..b94e19c88 100644 --- a/core/src/main/java/com/predic8/membrane/core/config/security/acme/AcmeValidation.java +++ b/core/src/main/java/com/predic8/membrane/core/config/security/acme/AcmeValidation.java @@ -13,5 +13,10 @@ limitations under the License. */ package com.predic8.membrane.core.config.security.acme; -public class AcmeValidation { +public abstract class AcmeValidation { + + public boolean useDnsValidation() { + return true; + } + } diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/json/JsonProtectionException.java b/core/src/main/java/com/predic8/membrane/core/interceptor/json/JsonProtectionException.java new file mode 100644 index 000000000..f0586fbfa --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/json/JsonProtectionException.java @@ -0,0 +1,26 @@ +package com.predic8.membrane.core.interceptor.json; + +public class JsonProtectionException extends Exception{ + private final String message; + private final int line; + private final int col; + + public JsonProtectionException(String msg, int line, int col) { + this.message = msg; + this.line = line; + this.col = col; + } + + @Override + public String getMessage() { + return this.message; + } + + public int getLine() { + return line; + } + + public int getCol() { + return col; + } +} diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/json/JsonProtectionInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/json/JsonProtectionInterceptor.java index 54941cd64..fe29f1ace 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/json/JsonProtectionInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/json/JsonProtectionInterceptor.java @@ -29,12 +29,13 @@ import static com.fasterxml.jackson.core.JsonParser.Feature.*; import static com.fasterxml.jackson.core.JsonTokenId.*; import static com.fasterxml.jackson.databind.DeserializationFeature.*; +import static com.predic8.membrane.core.exceptions.ProblemDetails.createProblemDetails; import static com.predic8.membrane.core.interceptor.Interceptor.Flow.*; import static com.predic8.membrane.core.interceptor.Outcome.*; import static java.util.EnumSet.*; /** - * Enforces JSON restrictions. + * Enforces JSON restrictions in requests */ @MCElement(name = "jsonProtection") public class JsonProtectionInterceptor extends AbstractInterceptor { @@ -45,6 +46,7 @@ public class JsonProtectionInterceptor extends AbstractInterceptor { .configure(FAIL_ON_READING_DUP_TREE_KEY, true) .configure(STRICT_DUPLICATE_DETECTION, true); + private Boolean reportError; private int maxTokens = 10000; private int maxSize = 50 * 1024 * 1024; private int maxDepth = 50; @@ -65,22 +67,33 @@ public void init() throws Exception { maxKeyLength = maxStringLength; } + private boolean shouldProvideDetails() { + if (reportError != null) { + return reportError; + } + return !router.isProduction(); + } + private abstract static class Context { - public abstract void check(JsonToken jsonToken, JsonParser parser) throws IOException; + public abstract void check(JsonToken jsonToken, JsonParser parser) throws IOException, JsonProtectionException; } private class ObjContext extends Context { int n; - @Override - public void check(JsonToken jsonToken, JsonParser parser) throws IOException { + public void check(JsonToken jsonToken, JsonParser parser) throws JsonProtectionException, IOException { if (jsonToken.id() == ID_END_OBJECT) return; n++; if (n > maxObjectSize) - throw new JsonParseException(parser, "Exceeded maxObjectSize (" + maxObjectSize + ")."); - if (parser.getCurrentName().length() > maxKeyLength) - throw new JsonParseException(parser, "Exceeded maxKeyLength (" + maxKeyLength + ")."); + throw new JsonProtectionException("Exceeded maxObjectSize.", + parser.currentLocation().getLineNr(), + parser.currentLocation().getColumnNr()); + if (parser.getCurrentName().length() > maxKeyLength) { + throw new JsonProtectionException("Exceeded maxKeyLength.", + parser.currentLocation().getLineNr(), + parser.currentLocation().getColumnNr()); + } } } @@ -88,12 +101,14 @@ private class ArrContext extends Context { int n; @Override - public void check(JsonToken jsonToken, JsonParser parser) throws JsonParseException { + public void check(JsonToken jsonToken, JsonParser parser) throws JsonProtectionException { if (jsonToken.id() == ID_END_ARRAY) return; n++; if (n > maxArraySize) - throw new JsonParseException(parser, "Exceeded maxArraySize (" + maxArraySize + ")."); + throw new JsonProtectionException("Exceeded maxArraySize.", + parser.currentLocation().getLineNr(), + parser.currentLocation().getColumnNr()); } } @@ -103,16 +118,35 @@ public Outcome handleRequest(Exchange exc) throws Exception { return CONTINUE; try { parseJson(new CountingInputStream(exc.getRequest().getBodyAsStreamDecoded())); + } catch (JsonProtectionException e) { + LOG.debug(e.getMessage()); + exc.setResponse(createErrorResponse(e.getMessage(), e.getLine(), e.getCol())); + return RETURN; } catch (JsonParseException e) { - LOG.error(e.getMessage()); - exc.setResponse(Response.badRequest().build()); + LOG.debug(e.getMessage()); + exc.setResponse(createErrorResponse(e.getMessage(), e.getLocation().getLineNr(), e.getLocation().getColumnNr())); + return RETURN; + } catch (Throwable e) { + LOG.debug(e.getMessage()); + exc.setResponse(createErrorResponse(e.getMessage(), null, null)); return RETURN; } - return CONTINUE; } - private void parseJson(CountingInputStream cis) throws IOException { + private Response createErrorResponse(String msg, Integer line, Integer col) { + if (shouldProvideDetails()) { + Map details = new HashMap<>() {{ + put("message", msg); + if (line != null) put("line", line); + if (col != null) put("column", col); + }}; + return createProblemDetails(400, "/security/json-validation", "JSON Protection Violation", details); + } + return Response.badRequest().build(); + } + + private void parseJson(CountingInputStream cis) throws IOException, JsonProtectionException { JsonParser parser = om.createParser(cis); int tokenCount = 0; int depth = 0; @@ -124,35 +158,47 @@ private void parseJson(CountingInputStream cis) throws IOException { break; tokenCount++; if (tokenCount > maxTokens) - throw new JsonParseException(parser, "Exceeded maxTokens (" + maxTokens + ")."); + throw new JsonProtectionException("Exceeded maxTokens.", + parser.currentLocation().getLineNr(), + parser.currentLocation().getColumnNr()); if (cis.getCount() > maxSize) - throw new JsonParseException(parser, "Exceeded maxSize (" + maxSize + ")."); + throw new JsonProtectionException("Exceeded maxSize.", + parser.currentLocation().getLineNr(), + parser.currentLocation().getColumnNr()); if (currentContext != null) currentContext.check(jsonToken, parser); switch (jsonToken.id()) { case ID_START_OBJECT: depth++; if (depth > maxDepth) - throw new JsonParseException(parser, "Exceeded maxDepth (" + maxDepth + ")."); + throw new JsonProtectionException("Exceeded maxDepth.", + parser.currentLocation().getLineNr(), + parser.currentLocation().getColumnNr()); contexts.add(currentContext = new ObjContext()); break; case ID_START_ARRAY: depth++; if (depth > maxArraySize) - throw new JsonParseException(parser, "Exceeded maxArraySize (" + maxArraySize + ")."); + throw new JsonProtectionException("Exceeded maxArraySize.", + parser.currentLocation().getLineNr(), + parser.currentLocation().getColumnNr()); contexts.add(currentContext = new ArrContext()); break; case ID_END_OBJECT: case ID_END_ARRAY: depth--; if (depth < 0) - throw new JsonParseException(parser, "invalid"); + throw new JsonProtectionException("Invalid JSON Document.", + parser.currentLocation().getLineNr(), + parser.currentLocation().getColumnNr()); contexts.remove(contexts.size() - 1); currentContext = contexts.size() == 0 ? null : contexts.get(contexts.size() - 1); break; case ID_STRING: if (parser.getValueAsString().length() > maxStringLength) - throw new JsonParseException(parser, "Exceeded maxStringLength (" + maxStringLength + ")."); + throw new JsonProtectionException("Exceeded maxStringLength.", + parser.currentLocation().getLineNr(), + parser.currentLocation().getColumnNr()); break; case ID_NUMBER_INT: case ID_NUMBER_FLOAT: @@ -164,19 +210,40 @@ private void parseJson(CountingInputStream cis) throws IOException { case ID_NO_TOKEN: case ID_FIELD_NAME: case ID_EMBEDDED_OBJECT: - throw new RuntimeException("not handled."); + throw new JsonProtectionException("Not handled.", + parser.currentLocation().getLineNr(), + parser.currentLocation().getColumnNr()); default: - throw new RuntimeException("not handled (" + jsonToken.id() + ")"); + throw new JsonProtectionException("Not handled (\" + jsonToken.id() + \")", + parser.currentLocation().getLineNr(), + parser.currentLocation().getColumnNr()); } } if (cis.getCount() > maxSize) - throw new JsonParseException(parser, "Exceeded maxSize (" + maxSize + ")."); + throw new JsonProtectionException("Exceeded maxSize.", + parser.currentLocation().getLineNr(), + parser.currentLocation().getColumnNr()); } public int getMaxTokens() { return maxTokens; } + /** + * @description Overwrites default error reporting behaviour. If set to true, errors will provide ProblemDetails body, + * if set to false, errors will throw standard exceptions. + * @default null + * @param reportError + */ + @MCAttribute + public void setReportError(boolean reportError) { + this.reportError = reportError; + } + + public boolean getReportError() { + return reportError; + } + /** * @description Maximum number of tokens a JSON document may consist of. For example, {"a":"b"} counts * as 3. diff --git a/core/src/main/java/com/predic8/membrane/core/rules/AbstractProxy.java b/core/src/main/java/com/predic8/membrane/core/rules/AbstractProxy.java index 73e0b73ff..99ecfa516 100644 --- a/core/src/main/java/com/predic8/membrane/core/rules/AbstractProxy.java +++ b/core/src/main/java/com/predic8/membrane/core/rules/AbstractProxy.java @@ -15,8 +15,6 @@ import java.util.ArrayList; import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import com.predic8.membrane.core.stats.RuleStatisticCollector; import com.predic8.membrane.core.transport.ssl.*; @@ -27,7 +25,6 @@ import com.predic8.membrane.annot.MCAttribute; import com.predic8.membrane.annot.MCChildElement; import com.predic8.membrane.core.Router; -import com.predic8.membrane.core.exchange.Exchange; import com.predic8.membrane.core.interceptor.Interceptor; public abstract class AbstractProxy implements Rule { @@ -189,7 +186,7 @@ public void init() throws Exception { if (acmeCtx == null) acmeCtx = new AcmeSSLContext(sslInboundParser, host, router.getHttpClientFactory(), router.getTimerManager()); setSslInboundContext(acmeCtx); - acmeCtx.init(router.getKubernetesClientFactory()); + acmeCtx.init(router.getKubernetesClientFactory(), router.getHttpClientFactory()); return; } diff --git a/core/src/main/java/com/predic8/membrane/core/transport/ssl/AcmeSSLContext.java b/core/src/main/java/com/predic8/membrane/core/transport/ssl/AcmeSSLContext.java index c667dec14..b57204ba0 100644 --- a/core/src/main/java/com/predic8/membrane/core/transport/ssl/AcmeSSLContext.java +++ b/core/src/main/java/com/predic8/membrane/core/transport/ssl/AcmeSSLContext.java @@ -61,8 +61,8 @@ public AcmeSSLContext(SSLParser parser, this.timerManager = timerManager != null ? timerManager : new TimerManager(); } - public void init(@Nullable KubernetesClientFactory kubernetesClientFactory) { - client.init(kubernetesClientFactory); + public void init(@Nullable KubernetesClientFactory kubernetesClientFactory, @Nullable HttpClientFactory httpClientFactory) { + client.init(kubernetesClientFactory, httpClientFactory); initAndSchedule(); } diff --git a/core/src/main/java/com/predic8/membrane/core/transport/ssl/acme/AcmeAzureTableApiStorageEngine.java b/core/src/main/java/com/predic8/membrane/core/transport/ssl/acme/AcmeAzureTableApiStorageEngine.java new file mode 100644 index 000000000..e39dcfa03 --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/transport/ssl/acme/AcmeAzureTableApiStorageEngine.java @@ -0,0 +1,252 @@ +package com.predic8.membrane.core.transport.ssl.acme; + +import com.fasterxml.jackson.databind.JsonNode; +import com.predic8.membrane.core.azure.AzureDns; +import com.predic8.membrane.core.azure.AzureTableStorage; +import com.predic8.membrane.core.azure.api.dns.DnsProvisionable; +import com.predic8.membrane.core.azure.api.AzureApiClient; +import com.predic8.membrane.core.transport.http.HttpClientFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; +import java.util.Arrays; + +public class AcmeAzureTableApiStorageEngine implements AcmeSynchronizedStorageEngine, DnsProvisionable { + + private static final Logger log = LoggerFactory.getLogger(AcmeAzureTableApiStorageEngine.class); + + private static final String CURRENT = "current"; + private static final String CURRENT_ERROR = "current-error"; + private static final String CURRENT_KEY = "current-key"; + + private final AzureApiClient apiClient; + private final AzureDns azureDns; + + public AcmeAzureTableApiStorageEngine( + AzureTableStorage tableStorage, + @Nullable AzureDns azureDns, + @Nullable HttpClientFactory httpClientFactory + ) { + this.azureDns = azureDns; + apiClient = new AzureApiClient(azureDns == null ? null : azureDns.getIdentity(), tableStorage, httpClientFactory); + + try { + apiClient.tableStorage().table().create(); + } catch (Exception e) { + // ignore if table exists already + log.debug("Ignore table already exists exception"); + } + + log.debug("Loaded {}", this.getClass().getSimpleName()); + } + + private JsonNode getEntity(String rowKey) { + try { + log.debug("Get entity for {}", rowKey); + return apiClient.tableStorage().entity(rowKey).get(); + } catch (Exception e) { + log.debug("Entity {} does not exist, returning null", rowKey); + return null; + } + } + + private String getDataPropertyOfEntity(String rowKey) { + var entity = getEntity(rowKey); + + return entity != null + ? entity.get("data").asText() + : null; + } + + private void upsertDataEntity(String rowKey, String data) { + try { + log.debug("Upserting key {}", rowKey); + apiClient.tableStorage().entity(rowKey).insertOrReplace(data); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private String id(String[] hosts) { + int i = Arrays.hashCode(hosts); + if (i < 0) i = Integer.MAX_VALUE + i + 1; + return hosts[0] + "-" + i; + } + + private String getPublicKeyRowKey(String[] hosts) { + return String.format("key-%s-pub.pem", id(hosts)); + } + + private String getPrivateKeyRowKey(String[] hosts) { + return String.format("key-%s.pem", id(hosts)); + } + + private String getCertChainRowKey(String[] hosts) { + return String.format("cert-%s.pem", id(hosts)); + } + + private String getTokenRowKey(String host) { + return String.format("token-%s", host); + } + + private String getOALRowKey(String[] hosts, String postfix) { + return String.format("oal-%s-%s.json", id(hosts), postfix); + } + + @Override + public String getAccountKey() { + return getDataPropertyOfEntity("account"); + } + + @Override + public void setAccountKey(String key) { + upsertDataEntity("account", key); + } + + @Override + public void setKeyPair(String[] hosts, AcmeKeyPair key) { + upsertDataEntity(getPublicKeyRowKey(hosts), key.getPublicKey()); + upsertDataEntity(getPrivateKeyRowKey(hosts), key.getPrivateKey()); + } + + @Override + public String getPublicKey(String[] hosts) { + return getDataPropertyOfEntity(getPublicKeyRowKey(hosts)); + } + + @Override + public String getPrivateKey(String[] hosts) { + return getDataPropertyOfEntity(getPrivateKeyRowKey(hosts)); + } + + @Override + public void setCertChain(String[] hosts, String caChain) { + upsertDataEntity(getCertChainRowKey(hosts), caChain); + } + + @Override + public String getCertChain(String[] hosts) { + return getDataPropertyOfEntity(getCertChainRowKey(hosts)); + } + + @Override + public void setToken(String host, String token) { + upsertDataEntity(getTokenRowKey(host), token); + } + + @Override + public String getToken(String host) { + return getDataPropertyOfEntity(getTokenRowKey(host)); + } + + @Override + public String getOAL(String[] hosts) { + return getDataPropertyOfEntity(getOALRowKey(hosts, CURRENT)); + } + + @Override + public void setOAL(String[] hosts, String oal) { + upsertDataEntity(getOALRowKey(hosts, CURRENT), oal); + } + + @Override + public String getAccountURL() { + return getDataPropertyOfEntity("account-url"); + } + + @Override + public void setAccountURL(String url) { + upsertDataEntity("account-url", url); + } + + @Override + public String getAccountContacts() { + return getDataPropertyOfEntity("account-contacts"); + } + + @Override + public void setAccountContacts(String contacts) { + upsertDataEntity("account-contacts", contacts); + } + + @Override + public String getOALError(String[] hosts) { + return getDataPropertyOfEntity(getOALRowKey(hosts, CURRENT_ERROR)); + } + + @Override + public void setOALError(String[] hosts, String oalError) { + upsertDataEntity(getOALRowKey(hosts, CURRENT_ERROR), oalError); + } + + @Override + public String getOALKey(String[] hosts) { + return getDataPropertyOfEntity(getOALRowKey(hosts, CURRENT_KEY)); + } + + @Override + public void setOALKey(String[] hosts, String oalKey) { + upsertDataEntity(getOALRowKey(hosts, CURRENT_KEY), oalKey); + } + + @Override + public void archiveOAL(String[] hosts) { + long now = System.currentTimeMillis(); + attemptRename(getOALRowKey(hosts, CURRENT), getOALRowKey(hosts, String.valueOf(now))); + attemptRename(getOALRowKey(hosts, CURRENT_ERROR), getOALRowKey(hosts, now + "-error")); + attemptRename(getOALRowKey(hosts, CURRENT_KEY), getOALRowKey(hosts, now + "-key")); + } + + private void attemptRename(String f1, String f2) { + log.debug("Attempt rename {} to {}", f1, f2); + var first = getDataPropertyOfEntity(f1); + + if (first != null) { + try { + log.debug("creating {}", f2); + apiClient.tableStorage().entity(f2).insertOrReplace(first); + log.debug("removing {}", f1); + apiClient.tableStorage().entity(f1).delete(); + } catch (Exception e) { + throw new RuntimeException(e); + } + return; + } + + log.debug("Attempt rename, but there was nothing to rename"); + } + + @Override + public void provisionDns(String domain, String record) { + try { + apiClient.dnsRecords(azureDns).txt("_acme-challenge") + .ttl(300) + .addRecord() + .withValue(record) + .create(); + + // https://learn.microsoft.com/en-us/azure/dns/dns-faq#how-long-does-it-take-for-dns-changes-to-take-effect- + Thread.sleep(60_000); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public boolean acquireLease(long durationMillis) { + // Single instance + return true; + } + + @Override + public boolean prolongLease(long durationMillis) { + // Single instance + return true; + } + + @Override + public void releaseLease() { + // Single instance + } +} diff --git a/core/src/main/java/com/predic8/membrane/core/transport/ssl/acme/AcmeClient.java b/core/src/main/java/com/predic8/membrane/core/transport/ssl/acme/AcmeClient.java index 98cd546eb..e1ea86b26 100644 --- a/core/src/main/java/com/predic8/membrane/core/transport/ssl/acme/AcmeClient.java +++ b/core/src/main/java/com/predic8/membrane/core/transport/ssl/acme/AcmeClient.java @@ -18,6 +18,9 @@ import com.fasterxml.jackson.datatype.joda.*; import com.google.common.collect.*; import com.predic8.membrane.core.*; +import com.predic8.membrane.core.azure.AzureDns; +import com.predic8.membrane.core.azure.AzureTableStorage; +import com.predic8.membrane.core.azure.api.dns.DnsProvisionable; import com.predic8.membrane.core.config.security.acme.*; import com.predic8.membrane.core.exchange.*; import com.predic8.membrane.core.http.*; @@ -91,6 +94,7 @@ public class AcmeClient { private final String algorithm = AlgorithmIdentifiers.ECDSA_USING_P256_CURVE_AND_SHA256; private final Duration validity; private AcmeSynchronizedStorageEngine asse; + private AcmeValidation acmeValidation; public AcmeClient(Acme acme, @Nullable HttpClientFactory httpClientFactory) { directoryUrl = acme.getDirectoryUrl(); @@ -101,7 +105,8 @@ public AcmeClient(Acme acme, @Nullable HttpClientFactory httpClientFactory) { httpClientFactory = new HttpClientFactory(null); hc = httpClientFactory.createClient(acme.getHttpClientConfiguration()); validity = acme.getValidityDuration(); - challengeType = acme.getValidationMethod() != null && acme.getValidationMethod() instanceof DnsOperatorAcmeValidation ? TYPE_DNS_01 : TYPE_HTTP_01; + this.acmeValidation = acme.getValidationMethod(); + challengeType = acme.getValidationMethod() != null && acme.getValidationMethod().useDnsValidation() ? TYPE_DNS_01 : TYPE_HTTP_01; om.registerModule(new JodaModule()); @@ -110,7 +115,7 @@ public AcmeClient(Acme acme, @Nullable HttpClientFactory httpClientFactory) { throw new RuntimeException("The ACME client is still experimental, please set to acknowledge."); } - public void init(@Nullable KubernetesClientFactory kubernetesClientFactory) { + public void init(@Nullable KubernetesClientFactory kubernetesClientFactory, @Nullable HttpClientFactory httpClientFactory) { if (ass == null) { throw new RuntimeException(" is used, but to storage is configured."); } else if (ass instanceof FileStorage) { @@ -119,9 +124,15 @@ public void init(@Nullable KubernetesClientFactory kubernetesClientFactory) { asse = new AcmeKubernetesStorageEngine((KubernetesStorage) ass, kubernetesClientFactory); } else if (ass instanceof MemoryStorage) { asse = new AcmeMemoryStorageEngine(); + } else if (ass instanceof AzureTableStorage) { + asse = new AcmeAzureTableApiStorageEngine((AzureTableStorage) ass, (AzureDns) acmeValidation, httpClientFactory); } else { throw new RuntimeException("Unsupported: Storage type " + ass.getClass().getName()); } + + if (challengeType.equals(TYPE_DNS_01) && !(asse instanceof DnsProvisionable)) { + throw new RuntimeException("A"); + } } public void loadDirectory() throws Exception { @@ -284,7 +295,7 @@ private void provisionDns(Authorization auth, Challenge challenge) throws JoseEx MessageDigest digest = MessageDigest.getInstance("SHA-256"); String record = java.util.Base64.getUrlEncoder().withoutPadding().encodeToString(digest.digest(keyAuth.getBytes(UTF_8))); - ((AcmeKubernetesStorageEngine)asse).provisionDns(auth.getIdentifier().getValue(), record); + ((DnsProvisionable)asse).provisionDns(auth.getIdentifier().getValue(), record); } private void provisionHttp(Authorization auth, Challenge challenge) { diff --git a/core/src/main/java/com/predic8/membrane/core/transport/ssl/acme/AcmeKubernetesStorageEngine.java b/core/src/main/java/com/predic8/membrane/core/transport/ssl/acme/AcmeKubernetesStorageEngine.java index 27e57da2c..c226fc471 100644 --- a/core/src/main/java/com/predic8/membrane/core/transport/ssl/acme/AcmeKubernetesStorageEngine.java +++ b/core/src/main/java/com/predic8/membrane/core/transport/ssl/acme/AcmeKubernetesStorageEngine.java @@ -13,6 +13,7 @@ limitations under the License. */ package com.predic8.membrane.core.transport.ssl.acme; +import com.predic8.membrane.core.azure.api.dns.DnsProvisionable; import com.predic8.membrane.core.config.security.acme.KubernetesStorage; import com.predic8.membrane.core.kubernetes.client.KubernetesApiException; import com.predic8.membrane.core.kubernetes.client.KubernetesClient; @@ -30,7 +31,7 @@ import static com.google.common.collect.Lists.newArrayList; import static java.nio.charset.StandardCharsets.UTF_8; -public class AcmeKubernetesStorageEngine implements AcmeSynchronizedStorageEngine { +public class AcmeKubernetesStorageEngine implements AcmeSynchronizedStorageEngine, DnsProvisionable { private static final Logger LOG = LoggerFactory.getLogger(AcmeKubernetesStorageEngine.class); @@ -370,6 +371,7 @@ public LeaseException(Throwable cause) { } } + @Override public void provisionDns(String domain, String record) { Map wantedRecord = of("type", "TXT", "timeout", 300, diff --git a/core/src/main/java/com/predic8/membrane/core/transport/ssl/acme/AcmeRenewal.java b/core/src/main/java/com/predic8/membrane/core/transport/ssl/acme/AcmeRenewal.java index 3b1fe39ac..774efd0cd 100644 --- a/core/src/main/java/com/predic8/membrane/core/transport/ssl/acme/AcmeRenewal.java +++ b/core/src/main/java/com/predic8/membrane/core/transport/ssl/acme/AcmeRenewal.java @@ -201,7 +201,7 @@ private void waitFor(String what, Supplier condition, Runnable job) thr private Challenge getChallenge(Authorization auth) throws JsonProcessingException, FatalAcmeException { Optional challenge = auth.getChallenges().stream().filter(c -> client.getChallengeType().equals(c.getType())).findAny(); if (challenge.isEmpty()) - throw new FatalAcmeException("Could not find challenge of type http01: " + om.writeValueAsString(auth)); + throw new FatalAcmeException("Could not find challenge of type "+client.getChallengeType()+": " + om.writeValueAsString(auth)); return challenge.get(); } diff --git a/core/src/test/java/com/predic8/membrane/core/azure/AcmeAzureTableApiStorageEngineTest.java b/core/src/test/java/com/predic8/membrane/core/azure/AcmeAzureTableApiStorageEngineTest.java new file mode 100644 index 000000000..36aec117d --- /dev/null +++ b/core/src/test/java/com/predic8/membrane/core/azure/AcmeAzureTableApiStorageEngineTest.java @@ -0,0 +1,39 @@ +package com.predic8.membrane.core.azure; + +import com.predic8.membrane.core.transport.ssl.acme.AcmeAzureTableApiStorageEngine; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class AcmeAzureTableApiStorageEngineTest { + + private AcmeAzureTableApiStorageEngine engine; + + @BeforeAll + void setup() { + var tableStorage = new AzureTableStorage(); + tableStorage.setStorageAccountName(""); + tableStorage.setStorageAccountKey(""); + + + engine = new AcmeAzureTableApiStorageEngine( + tableStorage, + null, + null + ); + } + + + + @Test + void createTable() { + + } + + @Test + void ignoreExistingTable() { + + } + + +} + diff --git a/core/src/test/java/com/predic8/membrane/core/azure/AzureApiClientTest.java b/core/src/test/java/com/predic8/membrane/core/azure/AzureApiClientTest.java new file mode 100644 index 000000000..9b406fe1d --- /dev/null +++ b/core/src/test/java/com/predic8/membrane/core/azure/AzureApiClientTest.java @@ -0,0 +1,121 @@ +package com.predic8.membrane.core.azure; + +import com.predic8.membrane.core.azure.api.AzureApiClient; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.NoSuchElementException; + +import static org.junit.jupiter.api.Assertions.*; + +class AzureApiClientTest { + + AzureDnsApiSimulator simulator; + AzureApiClient apiClient; + + @BeforeEach + void setup() throws IOException { + var tableStorage = new AzureTableStorage(); + tableStorage.setStorageAccountName("hello"); + tableStorage.setStorageAccountKey("thisisasecretandshouldhaveenoughbits"); + + int port = 3050; + + tableStorage.setCustomHost("http://localhost:" + port); + + simulator = new AzureDnsApiSimulator(port); + simulator.start(); + + apiClient = new AzureApiClient(null, tableStorage, null); + } + + @AfterEach + void tearDown() { + simulator.stop(); + } + + @Test + void createNonExistingTable() { + assertDoesNotThrow(() -> apiClient.tableStorage().table().create()); + } + + @Test + void ignoreExistingTable() { + assertDoesNotThrow(() -> { + apiClient.tableStorage().table().create(); + apiClient.tableStorage().table().create(); + }); + } + + @Test + void insertWithoutBaseTable() { + assertThrows(RuntimeException.class, () -> apiClient + .tableStorage() + .entity("account") + .insertOrReplace("initial")); + } + + @Test + void createTableEntry() throws Exception { + assertDoesNotThrow(() -> { + apiClient.tableStorage().table().create(); + + apiClient + .tableStorage() + .entity("account") + .insertOrReplace("initial"); + }); + + var data = apiClient.tableStorage().entity("account").get() + .get("data").asText(); + + assertEquals("initial", data); + } + + @Test + void getNonExistingEntity() { + assertDoesNotThrow(() -> apiClient.tableStorage().table().create()); + assertThrows(NoSuchElementException.class, () -> apiClient.tableStorage().entity("foo").get()); + } + + @Test + void updateTableEntry() throws Exception { + assertDoesNotThrow(() -> { + apiClient.tableStorage().table().create(); + + apiClient + .tableStorage() + .entity("account") + .insertOrReplace("initial"); + + apiClient + .tableStorage() + .entity("account") + .insertOrReplace("updated"); + }); + + var data = apiClient.tableStorage().entity("account").get() + .get("data").asText(); + + assertEquals("updated", data); + } + + @Test + void deleteExistingEntity() { + assertDoesNotThrow(() -> { + apiClient.tableStorage().table().create(); + apiClient.tableStorage().entity("account") + .insertOrReplace("initial"); + }); + assertDoesNotThrow(() -> apiClient.tableStorage().entity("account").get()); + assertDoesNotThrow(() -> apiClient.tableStorage().entity("account").delete()); + } + + @Test + void deleteNonExisting() { + assertDoesNotThrow(() -> apiClient.tableStorage().table().create()); + assertDoesNotThrow(() -> apiClient.tableStorage().entity("account").delete()); + } +} diff --git a/core/src/test/java/com/predic8/membrane/core/azure/AzureDnsApiSimulator.java b/core/src/test/java/com/predic8/membrane/core/azure/AzureDnsApiSimulator.java new file mode 100644 index 000000000..30962a9ce --- /dev/null +++ b/core/src/test/java/com/predic8/membrane/core/azure/AzureDnsApiSimulator.java @@ -0,0 +1,215 @@ +package com.predic8.membrane.core.azure; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.predic8.membrane.core.HttpRouter; +import com.predic8.membrane.core.exchange.Exchange; +import com.predic8.membrane.core.http.Response; +import com.predic8.membrane.core.interceptor.AbstractInterceptor; +import com.predic8.membrane.core.interceptor.Outcome; +import com.predic8.membrane.core.rules.ServiceProxy; +import com.predic8.membrane.core.rules.ServiceProxyKey; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.*; +import java.util.regex.Pattern; + +public class AzureDnsApiSimulator { + + private static final Logger log = LoggerFactory.getLogger(AzureDnsApiSimulator.class); + + private final int port; + private HttpRouter router; + + private Map>> tableStorage = new HashMap<>(); + + public AzureDnsApiSimulator(int port) { + this.port = port; + } + + public void start() throws IOException { + router = new HttpRouter(); + router.setHotDeploy(false); + + var sp = new ServiceProxy(new ServiceProxyKey(port), "localhost", port); + + sp.getInterceptors().add(new AbstractInterceptor() { + @Override + public Outcome handleRequest(Exchange exc) throws Exception { + log.info("got request {}" + exc.getRequestURI()); + + if (missingHeaders(exc)) { + exc.setResponse(Response.badRequest().build()); + return Outcome.RETURN; + } + + if (exc.getRequestURI().equals("/Tables")) { + return createTableStorageTable(exc); + } + + if (exc.getRequestURI().startsWith("/membrane")) { + return switch (exc.getRequest().getMethod()) { + case "PUT" -> insertOrReplaceTableStorageEntity(exc); + case "GET" -> getEntityFromTableStorage(exc); + case "DELETE" -> deleteEntityFromTableStorage(exc); + default -> Outcome.RETURN; + }; + } + + exc.setResponse(Response.notFound().build()); + return Outcome.RETURN; + } + }); + + router.add(sp); + router.start(); + } + + private boolean missingHeaders(Exchange exc) { + var hasNeededHeaders = Arrays.stream(exc.getRequest().getHeader().getAllHeaderFields()).allMatch(headerField -> + List.of("Date", "x-ms-version", "DataServiceVersion", "MaxDataServiceVersion", "Authorization") + .contains(headerField.getHeaderName()) + ); + + if (!hasNeededHeaders) { + return false; + } + + return true; + } + + private Outcome deleteEntityFromTableStorage(Exchange exc) { + var uriPayload = extractValuesFromUri(exc.getRequestURI()); + + if (uriPayload == null) { + exc.setResponse(Response.badRequest().build()); + return Outcome.RETURN; + } + + var tableName = uriPayload.get("tableName"); + var rowKey = uriPayload.get("rowKey"); + + tableStorage.put(tableName, + tableStorage.get(tableName).stream() + .filter(map -> !map.get("RowKey").equals(rowKey)) + .toList() + ); + + exc.setResponse(Response.statusCode(204).build()); + return Outcome.RETURN; + } + + private Outcome insertOrReplaceTableStorageEntity(Exchange exc) throws JsonProcessingException { + var data = new ObjectMapper() + .readTree(exc.getRequest().getBodyAsStringDecoded()) + .get("data") + .asText(); + + if (data == null) { + exc.setResponse(Response.badRequest().build()); + return Outcome.RETURN; + } + + var uriPayload = extractValuesFromUri(exc.getRequestURI()); + + if (uriPayload == null) { + exc.setResponse(Response.badRequest().build()); + return Outcome.RETURN; + } + + var tableName = uriPayload.get("tableName"); + var partitionKey = uriPayload.get("partitionKey"); + var rowKey = uriPayload.get("rowKey"); + + if (!tableStorage.containsKey(tableName)) { + exc.setResponse(Response.notFound().build()); + return Outcome.RETURN; + } + + var table = tableStorage.get(tableName); + var row = table.stream().filter(t -> t.get("RowKey").equals(rowKey)).findFirst(); + + if (row.isPresent()) { + row.get().put("data", data); + } else { + table.add(new HashMap<>(Map.of( + "PartitionKey", partitionKey, + "RowKey", rowKey, + "data", data + ))); + } + + exc.setResponse(Response.statusCode(204).build()); + return Outcome.RETURN; + } + + private Outcome getEntityFromTableStorage(Exchange exc) throws JsonProcessingException { + var uriPayload = extractValuesFromUri(exc.getRequestURI()); + + if (uriPayload == null) { + exc.setResponse(Response.badRequest().build()); + return Outcome.RETURN; + } + + var tableName = uriPayload.get("tableName"); + var rowKey = uriPayload.get("rowKey"); + + var row = tableStorage.get(tableName).stream() + .filter(e -> e.get("RowKey").equals(rowKey)) + .findFirst(); + + if (row.isEmpty()) { + exc.setResponse(Response.statusCode(400) + .body(new ObjectMapper().writeValueAsString(Map.of("odata.error", "foo"))) + .build()); + return Outcome.RETURN; + } + + exc.setResponse(Response.statusCode(200) + .body(new ObjectMapper().writeValueAsString(row.get())) + .build()); + return Outcome.RETURN; + } + + private Map extractValuesFromUri(String uri) { + var pattern = Pattern.compile("/(\\w+)%28PartitionKey%3D%27(\\w+)%27%2CRowKey%3D%27(\\w+)%27%29"); + var matcher = pattern.matcher(uri); + + if (matcher.matches()) { + return Map.of( + "tableName", matcher.group(1), + "partitionKey", matcher.group(2), + "rowKey", matcher.group(3) + ); + } + + return null; + } + + private Outcome createTableStorageTable(Exchange exc) throws JsonProcessingException { + var tableName = new ObjectMapper() + .readTree(exc.getRequest().getBodyAsStringDecoded()) + .get("TableName") + .asText(); + + if (tableName == null || tableName.isBlank() || tableName.isEmpty()) { + exc.setResponse(Response.badRequest().build()); + return Outcome.RETURN; + } + + if (tableStorage.containsKey(tableName)) { + exc.setResponse(Response.statusCode(409).build()); + } else { + tableStorage.put(tableName, new ArrayList<>()); + exc.setResponse(Response.statusCode(201).build()); + } + + return Outcome.RETURN; + } + + public void stop() { + router.stop(); + } +} diff --git a/core/src/test/java/com/predic8/membrane/core/interceptor/json/JsonProtectionInterceptorTest.java b/core/src/test/java/com/predic8/membrane/core/interceptor/json/JsonProtectionInterceptorTest.java index 9a4484eed..72085c246 100644 --- a/core/src/test/java/com/predic8/membrane/core/interceptor/json/JsonProtectionInterceptorTest.java +++ b/core/src/test/java/com/predic8/membrane/core/interceptor/json/JsonProtectionInterceptorTest.java @@ -14,21 +14,31 @@ package com.predic8.membrane.core.interceptor.json; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.predic8.membrane.core.Router; +import com.predic8.membrane.core.exceptions.ProblemDetails; +import com.predic8.membrane.core.exchange.Exchange; import com.predic8.membrane.core.http.*; import com.predic8.membrane.core.interceptor.*; import org.junit.jupiter.api.*; +import java.util.Arrays; + import static com.google.common.base.Strings.*; import static com.predic8.membrane.core.http.MimeType.APPLICATION_JSON; import static com.predic8.membrane.core.interceptor.Outcome.*; import static org.junit.jupiter.api.Assertions.*; public class JsonProtectionInterceptorTest { - static JsonProtectionInterceptor jpi; + static JsonProtectionInterceptor jpiProd; + static JsonProtectionInterceptor jpiDev; + + private static JsonProtectionInterceptor buildJPI(boolean prod) throws Exception { + Router router = new Router(); + router.setProduction(prod); + JsonProtectionInterceptor jpi = new JsonProtectionInterceptor(); - @BeforeAll - public static void init() { - jpi = new JsonProtectionInterceptor(); jpi.setMaxTokens(4096); jpi.setMaxSize(10240); jpi.setMaxDepth(10); @@ -36,6 +46,15 @@ public static void init() { jpi.setMaxKeyLength(10); jpi.setMaxObjectSize(10); jpi.setMaxArraySize(2048); + + jpi.init(router); + return jpi; + } + + @BeforeAll + public static void init() throws Exception { + jpiProd = buildJPI(true); + jpiDev = buildJPI(false); } @Test @@ -53,17 +72,26 @@ public void ok2() throws Exception { } @Test - public void duplicateKey() throws Exception { + void duplicateKey() throws Exception { send(""" {"a":1,"a":2}""", - RETURN); + RETURN, + 1, + 11, + "Duplicate field 'a'\n" + + " at [Source: (com.google.common.io.CountingInputStream); line: 1, column: 11]"); } @Test public void malformed() throws Exception { send(""" {""", - RETURN); + RETURN, + 1, + 2, + "Unexpected end-of-input: expected close marker for Object" + + " (start marker at [Source: (com.google.common.io.CountingInputStream); line: 1, column: 1])\n" + + " at [Source: (com.google.common.io.CountingInputStream); line: 1, column: 2]"); } @Test @@ -74,7 +102,10 @@ public void empty() throws Exception { @Test public void tooLong() throws Exception { send("[" + repeat("\"0123456\",", 1024) + "\"x\"]", - RETURN); + RETURN, + 1, + 8003, + "Exceeded maxSize."); } @Test @@ -86,7 +117,10 @@ public void justNotTooLong() throws Exception { @Test public void tooDeep() throws Exception { send(repeat("{\"a\":", 11) + "1" + repeat("}", 11), - RETURN); + RETURN, + 1, + 52, + "Exceeded maxDepth."); } @Test @@ -98,7 +132,10 @@ public void justNotTooDeep() throws Exception { @Test public void stringTooLong() throws Exception { send("[\"" + repeat("1", 21) + "\"]", - RETURN); + RETURN, + 1, + 25, + "Exceeded maxStringLength."); } @Test @@ -110,19 +147,28 @@ public void stringJustNotTooLong() throws Exception { @Test public void keyTooLong() throws Exception { send("{\"01234567890\": \"" + repeat("1", 20) + "\"}", - RETURN); + RETURN, + 1, + 18, + "Exceeded maxKeyLength."); } @Test public void keyTooLong2() throws Exception { send("{\"0123456789\": { \"01234567890\": \"" + repeat("1", 20) + "\"} }", - RETURN); + RETURN, + 1, + 34, + "Exceeded maxKeyLength."); } @Test public void keyTooLong3() throws Exception { send("{\"0123456789\": [ { \"01234567890\": \"" + repeat("1", 20) + "\"} ] }", - RETURN); + RETURN, + 1, + 36, + "Exceeded maxKeyLength."); } @Test @@ -141,7 +187,10 @@ public void objectTooLarge() throws Exception { } sb.append("}"); send(sb.toString(), - RETURN); + RETURN, + 1, + 79, + "Exceeded maxObjectSize."); } @Test @@ -160,7 +209,10 @@ public void objectJustNotTooLarge() throws Exception { @Test public void arrayTooLarge() throws Exception { send("[" + repeat("1,", 2048) + "1]", - RETURN); + RETURN, + 1, + 4099, + "Exceeded maxArraySize."); } @Test @@ -172,7 +224,10 @@ public void arrayJustNotTooLarge() throws Exception { @Test public void tooManyTokens() throws Exception { send("[" + repeat("1,", 2047) + "[" + repeat("1,", 2047) + "1]" + "]", - RETURN); + RETURN, + 1, + 8192, + "Exceeded maxTokens."); } @Test @@ -181,9 +236,30 @@ public void justNotTooManyTokens() throws Exception { CONTINUE); } + private void send(String body, Outcome expectOut, Object ...parameters) throws Exception { + ObjectMapper om = new ObjectMapper(); + Exchange e = new Request.Builder() + .post("/") + .contentType(APPLICATION_JSON) + .body(body) + .buildExchange(); + + if (expectOut == CONTINUE) { + assertEquals(expectOut, jpiProd.handleRequest(e)); + assertNull(e.getResponse()); - private void send(String body, Outcome expectedOutcome) throws Exception { - var e = new Request.Builder().post("/").contentType(APPLICATION_JSON).body(body).buildExchange(); - assertEquals(expectedOutcome, jpi.handleRequest(e)); + assertEquals(expectOut, jpiDev.handleRequest(e)); + assertNull(e.getResponse()); + } else { + assertEquals(expectOut, jpiProd.handleRequest(e)); + assertEquals("", e.getResponse().getBodyAsStringDecoded()); + + assertEquals(expectOut, jpiDev.handleRequest(e)); + JsonNode jn = om.readTree(e.getResponse().getBodyAsStringDecoded()); + assertEquals(parameters[2], jn.get("details").get("message").asText()); + assertEquals("JSON Protection Violation", jn.get("title").asText()); + assertEquals(parameters[0],jn.get("details").get("line").asInt()); + assertEquals(parameters[1], jn.get("details").get("column").asInt()); + } } } diff --git a/core/src/test/java/com/predic8/membrane/core/transport/ssl/acme/AcmeStepTest.java b/core/src/test/java/com/predic8/membrane/core/transport/ssl/acme/AcmeStepTest.java index de3000d0c..8853e9e33 100644 --- a/core/src/test/java/com/predic8/membrane/core/transport/ssl/acme/AcmeStepTest.java +++ b/core/src/test/java/com/predic8/membrane/core/transport/ssl/acme/AcmeStepTest.java @@ -45,7 +45,6 @@ import static com.predic8.membrane.core.transport.ssl.acme.Order.ORDER_STATUS_PROCESSING; import static com.predic8.membrane.core.transport.ssl.acme.Order.ORDER_STATUS_READY; import static org.junit.jupiter.api.Assertions.*; -import static org.junit.jupiter.api.Assertions.*; public class AcmeStepTest { diff --git a/distribution/examples/validation/form/README.md b/distribution/examples/validation/form/README.md new file mode 100644 index 000000000..c0ebc851f --- /dev/null +++ b/distribution/examples/validation/form/README.md @@ -0,0 +1,43 @@ +# Validation - Forms + +This sample explains how to set up and use the `formValidation` plugin. + + +## Running the Example + +1. Navigate to the `/examples/validation/form` directory. + + +2. Execute the `service-proxy.sh` script, or its batch file equivalent. + + +3. Use the following `curl` command in a terminal to send valid form data to Membrane: + `curl -o /dev/null -s -w "%{http_code}\n" -X POST -d "name=JohnSmith" http://localhost:2000` + The command returns the status code 200 "Ok", indicating that the request was successful. + + +4. Now send some invalid data to see how the system responds. Run the following curl command, which includes digits in the form data (making it invalid as per the rules defined in the `proxies.xml` file): + `curl -o /dev/null -s -w "%{http_code}\n" -X POST -d "name=JohnSmith1234" http://localhost:2000` + This time, we receive the status code 400, denoting a "Bad Request". + +## How it is done + +Let's examine the `proxies.xml` file. + +```xml + + + + + + + + +``` + +We define an `` component on port 2000 that uses the `formValidation` plugin. +By adding child elements to the plugin, we establish the necessary validation rules for the form. Each field specified is associated with a regex pattern that acts as the validation rule. + +--- +See: +- []() reference \ No newline at end of file diff --git a/distribution/examples/validation/json-schema/README.md b/distribution/examples/validation/json-schema/README.md index bead2e950..5d9015f52 100644 --- a/distribution/examples/validation/json-schema/README.md +++ b/distribution/examples/validation/json-schema/README.md @@ -1,11 +1,11 @@ -### JSON Schema Validation +# Validation - JSON Schema -To run this example you should install Curl from http://curl.haxx.se/download.html , if -you have not done so already. +This sample explains how to set up and use the `validator` plugin, utilizing JSON schemas for validation. -To run this example execute the following steps: -1. Go to the directory `examples/validation/json-schema`. +## Running the Example + +1. Go to the directory `/examples/validation/json-schema`. 2. Start `service-proxy.bat` or `service-proxy.sh`. @@ -15,8 +15,6 @@ To run this example execute the following steps: 5. Run `curl -d @bad2000.json http://localhost:2000/`. Observe that you get a validation error response. - - Keeping the router running, you can try a more complex schema. 1. Have a look at `schema2001.json`, `good2001.json` and `bad2001.json`. @@ -25,14 +23,39 @@ Keeping the router running, you can try a more complex schema. 3. Run `curl -d @bad2001.json http://localhost:2001/`. Observe that you get a validation error response. - - -Resources: - http://tools.ietf.org/html/draft-zyp-json-schema-03 - -(The file schema2001.json is loosely based on chapter 3 of -http://tools.ietf.org/html/draft-zyp-json-schema-03 .) +## How it is done + +Let's examine the `proxies.xml` file. + +```xml + + + + + + + + + + + + + + + + + + Response.ok("<response>good request</response>").build() + + + +``` + +We define three `` components, running on ports 2000-2002. +The first two validate all requests using the JSON schema defined in the `` component's `jsonSchema` attribute. +The successfully validated requests then get sent to the third `` component, where we simply return a 200 "Ok" response. --- See: +- [JSON Schema](https://json-schema.org/) documentation - [validator](https://membrane-soa.org/api-gateway-doc/current/configuration/reference/validator.htm) reference \ No newline at end of file diff --git a/distribution/examples/validation/json-schema/proxies.xml b/distribution/examples/validation/json-schema/proxies.xml index 2f3a1c070..e762e0d76 100644 --- a/distribution/examples/validation/json-schema/proxies.xml +++ b/distribution/examples/validation/json-schema/proxies.xml @@ -6,25 +6,25 @@ - + - + - + - + - + Response.ok("<response>good request</response>").build() - + diff --git a/distribution/examples/validation/soap-Proxy/README.md b/distribution/examples/validation/soap-Proxy/README.md index 53b0bf9ec..56d0a22f0 100644 --- a/distribution/examples/validation/soap-Proxy/README.md +++ b/distribution/examples/validation/soap-Proxy/README.md @@ -1,36 +1,37 @@ -### SOAP Message Validation +# Validation - SOAP -To run this example execute the following steps +This sample explains how to set up and use the `validator` plugin within a `soapProxy` component. -#### PREREQUISITES: -- Install Curl from http://curl.haxx.se/download.html , if you have not done so already. Let us assume it is in your PATH. +## Running the Example -As the URL of a WSDL is specified in proxies.xml, the Service Proxy retrieves all corresponding schemas and tries to validate the message body using them. +1. Go to `/examples/validation/soap-Proxy` -As a SOAP-Proxy is a specialized version of `the ServiceProxy`, a SOAP-Proxy has a certain advantage if it is used for a SOAP Service. A SOAP-Proxy needs the WSDL's URL just once. Adding a Validator then needs less code, as you can see in the example below: -``` - - - -``` +2. Start `service-proxy.bat` or `service-proxy.sh` -Execute the following steps: -1. Go to `examples/validation/soap-Proxy` +3. Run the following command, observe a successful response: + `curl --header "Content-Type: application/soap+xml" -d @blz-soap.xml http://localhost:2000/axis2/services/BLZService/getBankResponse` -2. Start `service-proxy.bat` or `service-proxy.sh` -3. Navigate into the `soap-Proxy` folder and run the following command on the console. Observe a successful response. +4. Run this next command and observe that verification fails: + `curl --header "Content-Type: application/soap+xml" -d @invalid-blz-soap.xml http://localhost:2000/axis2/services/BLZService/getBankResponse` +## How it is done + +Let's examine the `proxies.xml` file. + +```xml + + + + + ``` -curl --header "Content-Type: application/soap+xml" -d @blz-soap.xml http://localhost:2000/axis2/services/BLZService/getBankResponse -``` -4. Run the following command in the same directory and observe that verification fails. -``` -curl --header "Content-Type: application/soap+xml" -d @invalid-blz-soap.xml http://localhost:2000/axis2/services/BLZService/getBankResponse -``` + +We define a `` component running on port 2000, which is an expanded version of the basic `` and `` components, capable of storing a WSDL manifest. +Now we simply put a `` component withing the ``, without any attributes, as it inherits the WSDL from the proxy component. --- See: diff --git a/distribution/examples/validation/xml/README.md b/distribution/examples/validation/xml/README.md index 0aaf6ff16..4cfc9dfd4 100644 --- a/distribution/examples/validation/xml/README.md +++ b/distribution/examples/validation/xml/README.md @@ -1,18 +1,49 @@ -### XML Schema Validation +### Validation - XML -To run this example you should install Curl from http://curl.haxx.se/download.html , if -you have not done so already. Let us assume it is in your PATH. +This sample explains how to set up and use the `validator` plugin, utilizing XML schemas for validation. -Execute the following steps: -1. Go to the directory `examples/validation/xml`. +## Running the Example + +1. Go to the directory `/examples/validation/xml`. + 2. Start `service-proxy.bat` or `service-proxy.sh`. + 3. Run `curl -d @year.xml http://localhost:2000/`. Observe that you get a successful response. + 4. Run `curl -d @invalid-year.xml http://localhost:2000/`. Observe that you get a validation error response. +## How it is done + +Let's examine the `proxies.xml` file. + +```xml + + + + + + + + + + + + + + Response.ok("<amount>100</amount>").build() + + + +``` + +We have two `` components in action, operating on ports `2000` and `2001`. +The initial one employs the XML schema in the `` component's schema attribute for validating requests. Upon successful validation, these requests are forwarded to the second `` component. +Here, an XML document is generated and redirected back to the first `` component for a secondary round of validation, this time using a different schema for response validation. + --- See: - [validator](https://membrane-soa.org/api-gateway-doc/current/configuration/reference/validator.htm) reference \ No newline at end of file diff --git a/distribution/examples/validation/xml/proxies.xml b/distribution/examples/validation/xml/proxies.xml index e854904b0..1b1880502 100644 --- a/distribution/examples/validation/xml/proxies.xml +++ b/distribution/examples/validation/xml/proxies.xml @@ -6,7 +6,7 @@ - + @@ -14,13 +14,13 @@ - + - + Response.ok("<amount>100</amount>").build() - +