From 99fcb47f55c9312ebb95cb3a8edf3485130f75ab Mon Sep 17 00:00:00 2001 From: ztzh_xieyun Date: Thu, 25 Apr 2024 13:54:14 +0800 Subject: [PATCH] =?UTF-8?q?=E6=BA=90=E7=A0=81=E6=94=B9=E9=80=A0,=20?= =?UTF-8?q?=E5=8F=8D=E5=90=91=E4=BB=A3=E7=90=86=E6=94=AF=E6=8C=81=E5=8A=A0?= =?UTF-8?q?=E8=A7=A3=E5=AF=86=E3=80=81=E9=9D=9E=E5=8A=A0=E5=AF=86=E4=BC=A0?= =?UTF-8?q?=E8=BE=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sf-vertx/pom.xml | 4 - .../com/sf/vertx/handle/BodyHandlerImpl.java | 1 + .../com/sf/vertx/handle/ProxyHandlerImpl.java | 33 +- .../com/sf/vertx/init/DynamicBuildServer.java | 4 +- .../sf/vertx/strategy/security/AesUtils.java | 84 ----- .../vertx/strategy/security/Base64Utils.java | 31 -- .../sf/vertx/strategy/security/HexUtils.java | 53 ---- .../vertx/strategy/security/MainSecurity.java | 78 ----- .../strategy/security/MessageDigest.java | 47 --- .../sf/vertx/strategy/security/RSA2Utils.java | 282 ----------------- .../vertx/strategy/security/StringUtils.java | 53 ---- .../main/java/examples/HttpProxyExamples.java | 182 +++++++++++ .../src/main/java/examples/package-info.java | 6 + .../main/java/io/vertx/httpproxy/Body.java | 91 ++++++ .../java/io/vertx/httpproxy/HttpProxy.java | 122 +++++++ .../java/io/vertx/httpproxy/ProxyContext.java | 48 +++ .../io/vertx/httpproxy/ProxyInterceptor.java | 31 ++ .../java/io/vertx/httpproxy/ProxyOptions.java | 79 +++++ .../httpproxy/ProxyOptionsConverter.java | 47 +++ .../java/io/vertx/httpproxy/ProxyRequest.java | 181 +++++++++++ .../io/vertx/httpproxy/ProxyResponse.java | 133 ++++++++ .../vertx/httpproxy/cache/CacheOptions.java | 61 ++++ .../cache/CacheOptionsConverter.java | 39 +++ .../httpproxy/impl/BufferedReadStream.java | 82 +++++ .../httpproxy/impl/BufferingReadStream.java | 76 +++++ .../httpproxy/impl/BufferingWriteStream.java | 69 ++++ .../io/vertx/httpproxy/impl/CacheControl.java | 50 +++ .../io/vertx/httpproxy/impl/CacheImpl.java | 23 ++ .../vertx/httpproxy/impl/CachingFilter.java | 150 +++++++++ .../io/vertx/httpproxy/impl/HttpUtils.java | 55 ++++ .../io/vertx/httpproxy/impl/ParseUtils.java | 81 +++++ .../vertx/httpproxy/impl/ProxiedRequest.java | 230 ++++++++++++++ .../vertx/httpproxy/impl/ProxiedResponse.java | 271 ++++++++++++++++ .../io/vertx/httpproxy/impl/Resource.java | 51 +++ .../io/vertx/httpproxy/impl/ReverseProxy.java | 299 ++++++++++++++++++ .../java/io/vertx/httpproxy/package-info.java | 18 ++ .../io/vertx/httpproxy/spi/cache/Cache.java | 9 + 37 files changed, 2491 insertions(+), 663 deletions(-) delete mode 100644 sf-vertx/src/main/java/com/sf/vertx/strategy/security/AesUtils.java delete mode 100644 sf-vertx/src/main/java/com/sf/vertx/strategy/security/Base64Utils.java delete mode 100644 sf-vertx/src/main/java/com/sf/vertx/strategy/security/HexUtils.java delete mode 100644 sf-vertx/src/main/java/com/sf/vertx/strategy/security/MainSecurity.java delete mode 100644 sf-vertx/src/main/java/com/sf/vertx/strategy/security/MessageDigest.java delete mode 100644 sf-vertx/src/main/java/com/sf/vertx/strategy/security/RSA2Utils.java delete mode 100644 sf-vertx/src/main/java/com/sf/vertx/strategy/security/StringUtils.java create mode 100644 sf-vertx/src/main/java/examples/HttpProxyExamples.java create mode 100644 sf-vertx/src/main/java/examples/package-info.java create mode 100644 sf-vertx/src/main/java/io/vertx/httpproxy/Body.java create mode 100644 sf-vertx/src/main/java/io/vertx/httpproxy/HttpProxy.java create mode 100644 sf-vertx/src/main/java/io/vertx/httpproxy/ProxyContext.java create mode 100644 sf-vertx/src/main/java/io/vertx/httpproxy/ProxyInterceptor.java create mode 100644 sf-vertx/src/main/java/io/vertx/httpproxy/ProxyOptions.java create mode 100644 sf-vertx/src/main/java/io/vertx/httpproxy/ProxyOptionsConverter.java create mode 100644 sf-vertx/src/main/java/io/vertx/httpproxy/ProxyRequest.java create mode 100644 sf-vertx/src/main/java/io/vertx/httpproxy/ProxyResponse.java create mode 100644 sf-vertx/src/main/java/io/vertx/httpproxy/cache/CacheOptions.java create mode 100644 sf-vertx/src/main/java/io/vertx/httpproxy/cache/CacheOptionsConverter.java create mode 100644 sf-vertx/src/main/java/io/vertx/httpproxy/impl/BufferedReadStream.java create mode 100644 sf-vertx/src/main/java/io/vertx/httpproxy/impl/BufferingReadStream.java create mode 100644 sf-vertx/src/main/java/io/vertx/httpproxy/impl/BufferingWriteStream.java create mode 100644 sf-vertx/src/main/java/io/vertx/httpproxy/impl/CacheControl.java create mode 100644 sf-vertx/src/main/java/io/vertx/httpproxy/impl/CacheImpl.java create mode 100644 sf-vertx/src/main/java/io/vertx/httpproxy/impl/CachingFilter.java create mode 100644 sf-vertx/src/main/java/io/vertx/httpproxy/impl/HttpUtils.java create mode 100644 sf-vertx/src/main/java/io/vertx/httpproxy/impl/ParseUtils.java create mode 100644 sf-vertx/src/main/java/io/vertx/httpproxy/impl/ProxiedRequest.java create mode 100644 sf-vertx/src/main/java/io/vertx/httpproxy/impl/ProxiedResponse.java create mode 100644 sf-vertx/src/main/java/io/vertx/httpproxy/impl/Resource.java create mode 100644 sf-vertx/src/main/java/io/vertx/httpproxy/impl/ReverseProxy.java create mode 100644 sf-vertx/src/main/java/io/vertx/httpproxy/package-info.java create mode 100644 sf-vertx/src/main/java/io/vertx/httpproxy/spi/cache/Cache.java diff --git a/sf-vertx/pom.xml b/sf-vertx/pom.xml index 4ea5e87..82e434b 100644 --- a/sf-vertx/pom.xml +++ b/sf-vertx/pom.xml @@ -96,10 +96,6 @@ io.vertx vertx-core - - io.vertx - vertx-http-proxy - io.vertx vertx-web diff --git a/sf-vertx/src/main/java/com/sf/vertx/handle/BodyHandlerImpl.java b/sf-vertx/src/main/java/com/sf/vertx/handle/BodyHandlerImpl.java index 6c15a7f..e670969 100644 --- a/sf-vertx/src/main/java/com/sf/vertx/handle/BodyHandlerImpl.java +++ b/sf-vertx/src/main/java/com/sf/vertx/handle/BodyHandlerImpl.java @@ -77,6 +77,7 @@ public class BodyHandlerImpl implements BodyHandler { @Override public void handle(RoutingContext context) { + // TODO 改造了这个地方 final HttpServerRequest request = context.request(); final HttpServerResponse response = context.response(); String sacAppHeaderKey = request.getHeader(DynamicBuildServer.SAC_APP_HEADER_KEY); diff --git a/sf-vertx/src/main/java/com/sf/vertx/handle/ProxyHandlerImpl.java b/sf-vertx/src/main/java/com/sf/vertx/handle/ProxyHandlerImpl.java index 5802c12..34a168f 100644 --- a/sf-vertx/src/main/java/com/sf/vertx/handle/ProxyHandlerImpl.java +++ b/sf-vertx/src/main/java/com/sf/vertx/handle/ProxyHandlerImpl.java @@ -1,25 +1,12 @@ package com.sf.vertx.handle; -import com.sf.vertx.security.MainSecurity; - -import io.vertx.core.Future; -import io.vertx.core.buffer.Buffer; -import io.vertx.core.http.HttpClient; -import io.vertx.core.http.PoolOptions; -import io.vertx.core.json.JsonObject; -import io.vertx.ext.web.Router; import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.client.WebClient; -import io.vertx.httpproxy.Body; import io.vertx.httpproxy.HttpProxy; -import io.vertx.httpproxy.ProxyRequest; -import io.vertx.httpproxy.ProxyResponse; -import lombok.extern.slf4j.Slf4j; /** * @author Emad Alblueshi */ -@Slf4j public class ProxyHandlerImpl implements ProxyHandler { private final HttpProxy httpProxy; @@ -40,23 +27,9 @@ public class ProxyHandlerImpl implements ProxyHandler { @Override public void handle(RoutingContext ctx) { - // 创建一个响应并设置参数 -// ctx.request().response().setStatusCode(200) -// .putHeader("content-type", "text/plain").end(body); - // 发起一个请求 - -// this.mainWebClient.post(9198, "127.0.0.1", "/vertx/body").sendJson(ctx.getBodyAsString(), h -> { -// if (h.succeeded()) { -// JsonObject responseData = h.result().bodyAsJsonObject(); -// log.info("responseData:{}", responseData); -// // 加密 -// String dataStr = MainSecurity.aesEncrypt(responseData.toString(), "dadddsdfadfadsfa33323223"); -// log.info("dataStr:{}", dataStr); -// ctx.request().response().setStatusCode(200).putHeader("content-type", "application/json").end(dataStr); -// } -// }); - + // TODO 改造了这个地方 + httpProxy.handle(ctx, mainWebClient); // 原始代码只有如下一句 - httpProxy.handle(ctx.request()); + // httpProxy.handle(ctx.request()); } } \ No newline at end of file diff --git a/sf-vertx/src/main/java/com/sf/vertx/init/DynamicBuildServer.java b/sf-vertx/src/main/java/com/sf/vertx/init/DynamicBuildServer.java index 6604dbd..724f622 100644 --- a/sf-vertx/src/main/java/com/sf/vertx/init/DynamicBuildServer.java +++ b/sf-vertx/src/main/java/com/sf/vertx/init/DynamicBuildServer.java @@ -119,8 +119,9 @@ public class DynamicBuildServer implements ApplicationRunner { return context.sendRequest(); } }); - // mainHttpRouter.route().handler(ProxyHandler.create(proxy)); WebClient mainWebClient = WebClient.create(VERTX); + //mainHttpRouter.route().handler(ProxyHandler.create(proxy)); + mainHttpRouter.route().handler(BodyHandler.create()).handler(ProxyHandler.create(mainWebClient,proxy)); } @@ -136,6 +137,7 @@ public class DynamicBuildServer implements ApplicationRunner { SacLoadBalancing sacLoadBalancing = null; // 2、是否存在server服务配置 if (appConfig.getService() != null && appConfig.getService().size() > 0) { + // header传递服务名, 这样就不需要遍历,提高性能 for (SacService service : appConfig.getService()) { // uri是否匹配 boolean match = false; diff --git a/sf-vertx/src/main/java/com/sf/vertx/strategy/security/AesUtils.java b/sf-vertx/src/main/java/com/sf/vertx/strategy/security/AesUtils.java deleted file mode 100644 index 691524b..0000000 --- a/sf-vertx/src/main/java/com/sf/vertx/strategy/security/AesUtils.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.sf.vertx.strategy.security; - -import javax.crypto.BadPaddingException; -import javax.crypto.Cipher; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.SecretKeySpec; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; - -/** - * 本类采用对称加密 加密算法:{@link #PADDINIG_MODE} - * {@link #RESULT_TYPE} 密文结果:1=base64 2=hex - */ -public class AesUtils { - /** - * 整平方法 - */ - private static final int RESULT_TYPE = 2; - - /** - * 加密方法 - */ - private static final String AES_ALGORITHM = "AES"; - - /** - * 填充方法 - */ - private static final String PADDINIG_MODE = "AES/CBC/PKCS5Padding"; - - /** - * 偏移量 - */ - private static final byte[] IV = "0000000000000000".getBytes(); - - public static String encrypt(String s, String k) throws Exception { - SecretKeySpec key = new SecretKeySpec(StringUtils.getBytes(k), AES_ALGORITHM); - byte[] data = encrypt(StringUtils.getBytes(s), key); - String result; - switch (RESULT_TYPE) { - case 1: - result = Base64Utils.encode(data); - break; - case 2: - result = HexUtils.bytes2Hex(data); - break; - default: - throw new Exception("Unsupport Result Type"); - } - return result; - } - - public static String decrypt(String s, String k) throws Exception { - SecretKeySpec key = new SecretKeySpec(StringUtils.getBytes(k), AES_ALGORITHM); - byte[] data; - switch (RESULT_TYPE) { - case 1: - data = Base64Utils.decode(s); - break; - case 2: - data = HexUtils.hex2Bytes(s); - break; - default: - throw new Exception("Unsupport Result Type"); - } - return StringUtils.bytes2String(decrypt(data, key)); - } - - private static byte[] encrypt(byte[] data, SecretKeySpec keySpec) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException { - IvParameterSpec ivspec = new IvParameterSpec(IV); - Cipher cipher = Cipher.getInstance(PADDINIG_MODE); - cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivspec); - return cipher.doFinal(data); - } - - private static byte[] decrypt(byte[] data, SecretKeySpec keySpec) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException { - IvParameterSpec ivspec = new IvParameterSpec(IV); - Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); - cipher.init(Cipher.DECRYPT_MODE, keySpec, ivspec); - return cipher.doFinal(data); - } -} diff --git a/sf-vertx/src/main/java/com/sf/vertx/strategy/security/Base64Utils.java b/sf-vertx/src/main/java/com/sf/vertx/strategy/security/Base64Utils.java deleted file mode 100644 index 06c9ff1..0000000 --- a/sf-vertx/src/main/java/com/sf/vertx/strategy/security/Base64Utils.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.sf.vertx.strategy.security; - -import java.io.UnsupportedEncodingException; -import java.util.Base64; - -/** - * 将Base64字符串和字节数组互转 - */ -public class Base64Utils { - - /** - * 将字节数组编码为base64字符串 - * - * @param b 字节数组 - * @return String 字符串 - */ - public static String encode(byte[] b) { - return Base64.getEncoder().encodeToString(b); - } - - /** - * 将字符串转base64为字节数组 - * - * @param s 字符串 - * @return byte[] 字节数组 - */ - public static byte[] decode(String s) { - return Base64.getDecoder().decode(s); - } - -} diff --git a/sf-vertx/src/main/java/com/sf/vertx/strategy/security/HexUtils.java b/sf-vertx/src/main/java/com/sf/vertx/strategy/security/HexUtils.java deleted file mode 100644 index c274b71..0000000 --- a/sf-vertx/src/main/java/com/sf/vertx/strategy/security/HexUtils.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.sf.vertx.strategy.security; - -/** - * 将字节数组和HEX字符串进行互转 - */ -public class HexUtils { - private static final char[] HEX = new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; - - /** - * 字节数组转字符串 - * - * @param byteArr 字节数组 - * @return 字符串 - */ - public static String bytes2Hex(byte[] byteArr) { - StringBuilder sb = new StringBuilder(byteArr.length); - for (byte b : byteArr) { - String sTemp = Integer.toHexString(255 & b); - if (sTemp.length() < 2) { - sb.append(0); - } - sb.append(sTemp.toUpperCase()); - } - return sb.toString(); - } - - /** - * HEX字符串转字节数组 - * - * @param str 字符串 - * @return 字节数组 - */ - public static byte[] hex2Bytes(String str) { - if (str == null) { - return null; - } else { - char[] hex = str.toCharArray(); - int length = hex.length / 2; - byte[] raw = new byte[length]; - - for (int i = 0; i < length; ++i) { - int high = Character.digit(hex[i * 2], 16); - int low = Character.digit(hex[i * 2 + 1], 16); - int value = high << 4 | low; - if (value > 127) { - value -= 256; - } - raw[i] = (byte) value; - } - return raw; - } - } -} diff --git a/sf-vertx/src/main/java/com/sf/vertx/strategy/security/MainSecurity.java b/sf-vertx/src/main/java/com/sf/vertx/strategy/security/MainSecurity.java deleted file mode 100644 index a58e1cb..0000000 --- a/sf-vertx/src/main/java/com/sf/vertx/strategy/security/MainSecurity.java +++ /dev/null @@ -1,78 +0,0 @@ -package com.sf.vertx.strategy.security; - -import java.util.logging.Logger; - -/** - * 本项目中真正对外提供服务的工具类 - */ -public class MainSecurity { - private static final Logger LOGGER = Logger.getLogger(MainSecurity.class.getName()); - - /** - * 加密,当失败的时候,返回空字符串 - * - * @param content - * @param pubKey - * @return - */ - public static String rsaEncrypt(String content, String pubKey) { - try { - return RSA2Utils.encrypt(content, pubKey); - } catch (Exception e) { - LOGGER.info("RSA加密失败"); - e.printStackTrace(); - return null; - } - } - - public static String rsaDecrypt(String content, String priKey) { - try { - return RSA2Utils.decrypt(content, priKey); - } catch (Exception e) { - LOGGER.info("RSA解密失败"); - e.printStackTrace(); - return null; - } - } - - public static String aesEncrypt(String content, String key) { - try { - return AesUtils.encrypt(content, key); - } catch (Exception e) { - e.printStackTrace(); - LOGGER.info("AES加密失败"); - return null; - } - } - - public static String aesDecrypt(String content, String key) { - try { - return AesUtils.decrypt(content, key); - } catch (Exception e) { - e.printStackTrace(); - LOGGER.info("AES解密失败"); - return null; - } - } - - public static String sign(String content) { - try { - return MessageDigest.md5(content); - } catch (Exception e) { - e.printStackTrace(); - LOGGER.info("MD5加密失败"); - return null; - } - } - - public static void main(String[] args) { - System.out.println(aesEncrypt("{\n" - + " \"errorCode\": \"test\",\n" - + " \"result\": 0,\n" - + " \"data\": {\n" - + " \"username\" : \"test\"\n" - + " }\n" - + "}", "dadddsdfadfadsfa33323223")); - System.out.println(aesDecrypt("59A69B6BBCF046C3CF9953C5CC078CC68ABBED261030F3C119F65B9A3EB306423F32560751B22616500070BCE57153BE2B6E47CAC380C7D226862558256325826FECC43E957FCACB74E22BA48125FABC01FC77B127E384F8F62D9881741931A8C1ABF0A391F06606A060499EB53EF217", "dadddsdfadfadsfa33323223")); - } -} diff --git a/sf-vertx/src/main/java/com/sf/vertx/strategy/security/MessageDigest.java b/sf-vertx/src/main/java/com/sf/vertx/strategy/security/MessageDigest.java deleted file mode 100644 index b91b43b..0000000 --- a/sf-vertx/src/main/java/com/sf/vertx/strategy/security/MessageDigest.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.sf.vertx.strategy.security; - -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; -import java.security.NoSuchAlgorithmException; - -public class MessageDigest { - - public static String md5(String text) throws NoSuchAlgorithmException { - java.security.MessageDigest digest = java.security.MessageDigest.getInstance("md5"); - byte[] buffer = digest.digest(text.getBytes()); - StringBuilder sb = new StringBuilder(); - for (byte b : buffer) { - int a = b & 0xff; - String hex = Integer.toHexString(a); - - if (hex.length() == 1) { - hex = 0 + hex; - } - sb.append(hex); - } - return sb.toString(); - } - - private static String hmacSha1(String data, String key, int type) throws Exception { - //根据给定的字节数组构造一个密钥,第二参数指定一个密钥算法的名称 - SecretKeySpec signinKey = new SecretKeySpec(key.getBytes(), "HmacSHA1"); - //生成一个指定 Mac 算法 的 Mac 对象 - Mac mac = Mac.getInstance("HmacSHA1"); - //用给定密钥初始化 Mac 对象 - mac.init(signinKey); - //完成 Mac 操作 - byte[] rawHmac = mac.doFinal(StringUtils.getBytes(data)); - String result; - switch (type) { - case 1: - result = Base64Utils.encode(rawHmac); - break; - case 2: - result = HexUtils.bytes2Hex(rawHmac); - break; - default: - throw new Exception("Unsupport Type"); - } - return result; - } -} \ No newline at end of file diff --git a/sf-vertx/src/main/java/com/sf/vertx/strategy/security/RSA2Utils.java b/sf-vertx/src/main/java/com/sf/vertx/strategy/security/RSA2Utils.java deleted file mode 100644 index 70db5b2..0000000 --- a/sf-vertx/src/main/java/com/sf/vertx/strategy/security/RSA2Utils.java +++ /dev/null @@ -1,282 +0,0 @@ -package com.sf.vertx.strategy.security; - - -import javax.crypto.BadPaddingException; -import javax.crypto.Cipher; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.security.*; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.PKCS8EncodedKeySpec; -import java.security.spec.X509EncodedKeySpec; -import java.util.ArrayList; -import java.util.List; - -/** - * 本类进行非对称加密,不推荐使用非对称加密对长字符串进行加密或者解密,徒增资源消耗,另外由于长度限制,过长的字符串的加密和解密会使用循环,对数据分段加密;本类采用的 - * 密钥字符串均为Base64加密后的 - * 另外所有异常都会抛出 - * 下面将会列举几个可以自定义或者暴露出去的接口和参数 - * {@link #IS_LONG_TEXT} 是否否对长文本处理 - * {@link #RESULT_TYPE} 密文结果:1=base64 2=hex - * {@link #RSA_ALGORITHM} RSA算法 - * {@link #encrypt(String, String)} 加密方法 - * {@link #decrypt(String, String)} 解密方法 - * {@link #getKeyPair} 解密方法 - */ -public class RSA2Utils { - /** - * 是否对长文本加密;请参照{@link #MAX_DECRYPT_BLOCK}和{@link #MAX_ENCRYPT_BLOCK} - */ - private static final boolean IS_LONG_TEXT = true; - /** - * 结果类型 - */ - private static final int RESULT_TYPE = 2; - /** - * RSA 算法 - */ - private static final String RSA_ALGORITHM = "RSA"; - /** - * 长文本解密块大小 - */ - private static final int MAX_DECRYPT_BLOCK = 128; - /** - * 长文本加密块大小 - */ - private static final int MAX_ENCRYPT_BLOCK = 117; - /** - * KyeSize - */ - private static final int KEY_SIZE = 2048; - - /** - * 加密 - * - * @param content 待加密的字符串 - * @param pubKey 公钥字符串 - * @return 加密后的文本 - * @throws Exception 异常 - */ - public static String encrypt(String content, String pubKey) throws Exception { - byte[] data = StringUtils.getBytes(content); - PublicKey publicKey = string2PubKey(pubKey); - byte[] resultArr; - if (IS_LONG_TEXT) { - resultArr = encryptLongStr(data, publicKey); - } else { - resultArr = encrypt(data, publicKey); - } - String result; - switch (RESULT_TYPE) { - case 1: - result = Base64Utils.encode(resultArr); - break; - case 2: - result = HexUtils.bytes2Hex(resultArr); - break; - default: - throw new Exception("Unsupport result type"); - } - return result; - } - - /** - * @param content 密文内容 - * @param priKey 私钥 - * @return 解密后的字符串 - * @throws Exception 异常 - */ - public static String decrypt(String content, String priKey) throws Exception { - byte[] data; - switch (RESULT_TYPE) { - case 1: - data = Base64Utils.decode(content); - break; - case 2: - data = HexUtils.hex2Bytes(content); - break; - default: - throw new Exception("Unsupport result type"); - } - PrivateKey privateKey = string2PrivateKey(priKey); - byte[] result; - if (IS_LONG_TEXT) { - result = decryptLongStr(data, privateKey); - } else { - result = decrypt(privateKey, data); - } - return StringUtils.bytes2String(result); - } - - /** - * 响应公私钥对 - * - * @return 0号 公钥 1号 私钥 - * @throws NoSuchAlgorithmException 异常 - */ - public static List getKeyPair() throws NoSuchAlgorithmException { - KeyPair keyPairObj = getKeyPairObj(); - List data = new ArrayList<>(); - data.add(Base64Utils.encode(keyPairObj.getPublic().getEncoded())); - data.add(Base64Utils.encode(keyPairObj.getPrivate().getEncoded())); - return data; - // jdk 17 - //return List.of(Base64Utils.encode(keyPairObj.getPublic().getEncoded()), Base64Utils.encode(keyPairObj.getPrivate().getEncoded())); - } - - /** - * 将公钥字符串转化为对象 - * - * @param s base64字符串 - * @return 公钥 - * @throws NoSuchAlgorithmException 异常 - * @throws UnsupportedEncodingException 异常 - * @throws InvalidKeySpecException 异常 - */ - private static PublicKey string2PubKey(String s) throws NoSuchAlgorithmException, UnsupportedEncodingException, InvalidKeySpecException { - KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM); - X509EncodedKeySpec keySpec = new X509EncodedKeySpec(Base64Utils.decode(s)); - return keyFactory.generatePublic(keySpec); - } - - /** - * 对段字符串进行加密 - * - * @param bytes 字节数组 - * @param publicKey 公钥 - * @return 加密后的数组 - * @throws InvalidKeyException 异常 - * @throws BadPaddingException 异常 - * @throws IllegalBlockSizeException 异常 - * @throws NoSuchPaddingException 异常 - * @throws NoSuchAlgorithmException 异常 - */ - private static byte[] encrypt(byte[] bytes, PublicKey publicKey) throws InvalidKeyException, BadPaddingException, IllegalBlockSizeException, NoSuchPaddingException, NoSuchAlgorithmException { - Cipher cipher = Cipher.getInstance(RSA_ALGORITHM); - cipher.init(Cipher.ENCRYPT_MODE, publicKey); - return cipher.doFinal(bytes); - } - - /** - * 对长字符串进行加密 - * - * @param bytes 字节数组 - * @param publicKey 公钥 - * @return 加密后的数组 - * @throws NoSuchPaddingException 异常 - * @throws NoSuchAlgorithmException 异常 - * @throws InvalidKeyException 异常 - */ - private static byte[] encryptLongStr(byte[] bytes, PublicKey publicKey) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException { - Cipher cipher = Cipher.getInstance(RSA_ALGORITHM); - cipher.init(Cipher.ENCRYPT_MODE, publicKey); - int inputLen = bytes.length; - byte[] encryptedData = new byte[0]; - try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { - int offSet = 0; - byte[] cache; - int i = 0; - // 对数据分段加密 - while (inputLen - offSet > 0) { - if (inputLen - offSet > MAX_ENCRYPT_BLOCK) { - cache = cipher.doFinal(bytes, offSet, MAX_ENCRYPT_BLOCK); - } else { - cache = cipher.doFinal(bytes, offSet, inputLen - offSet); - } - out.write(cache, 0, cache.length); - i++; - offSet = i * MAX_ENCRYPT_BLOCK; - } - encryptedData = out.toByteArray(); - } catch (IOException | BadPaddingException | IllegalBlockSizeException e) { - e.printStackTrace(); - } - return encryptedData; - } - - /** - * 私钥字符串转为私钥对象 - * - * @param priStr 私钥字符串 - * @return 私钥对象 - * @throws NoSuchAlgorithmException 异常 - * @throws InvalidKeySpecException 异常 - */ - private static PrivateKey string2PrivateKey(String priStr) throws NoSuchAlgorithmException, InvalidKeySpecException { - PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Base64Utils.decode(priStr)); - KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM); - return keyFactory.generatePrivate(keySpec); - } - - /** - * 解密 - * - * @param privateKey 私钥 - * @param bytes 字节数组 - * @return 解密后的字节数组 - * @throws NoSuchPaddingException 异常 - * @throws NoSuchAlgorithmException 异常 - * @throws BadPaddingException 异常 - * @throws IllegalBlockSizeException 异常 - * @throws InvalidKeyException 异常 - */ - public static byte[] decrypt(PrivateKey privateKey, byte[] bytes) throws NoSuchPaddingException, NoSuchAlgorithmException, BadPaddingException, IllegalBlockSizeException, InvalidKeyException { - Cipher cipher = Cipher.getInstance(privateKey.getAlgorithm()); - cipher.init(Cipher.DECRYPT_MODE, privateKey); - return cipher.doFinal(bytes); - } - - /** - * 解密 - * - * @param data 解密前的字节数组 - * @param privateKey 私钥 - * @return 解密后的字节数组 - * @throws InvalidKeyException 异常 - * @throws NoSuchPaddingException 异常 - * @throws NoSuchAlgorithmException 异常 - */ - public static byte[] decryptLongStr(byte[] data, PrivateKey privateKey) throws InvalidKeyException, NoSuchPaddingException, NoSuchAlgorithmException { - Cipher cipher = Cipher.getInstance(privateKey.getAlgorithm()); - cipher.init(Cipher.DECRYPT_MODE, privateKey); - int inputLen = data.length; - byte[] result = new byte[0]; - try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { - int offSet = 0; - byte[] cache; - int i = 0; - // 对数据分段解密 - while (inputLen - offSet > 0) { - if (inputLen - offSet > MAX_DECRYPT_BLOCK) { - cache = cipher.doFinal(data, offSet, MAX_DECRYPT_BLOCK); - } else { - cache = cipher.doFinal(data, offSet, inputLen - offSet); - } - out.write(cache, 0, cache.length); - i++; - offSet = i * MAX_DECRYPT_BLOCK; - } - result = out.toByteArray(); - } catch (BadPaddingException | IllegalBlockSizeException | IOException e) { - e.printStackTrace(); - } - return result; - } - - /** - * 获得一堆公私钥 - * - * @return KeyPair对象 - * @throws NoSuchAlgorithmException 异常 - */ - private static KeyPair getKeyPairObj() throws NoSuchAlgorithmException { - KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(RSA_ALGORITHM); - SecureRandom secureRandom = new SecureRandom(StringUtils.getBytes(String.valueOf(System.currentTimeMillis()))); - keyPairGenerator.initialize(KEY_SIZE, secureRandom); - return keyPairGenerator.genKeyPair(); - } -} diff --git a/sf-vertx/src/main/java/com/sf/vertx/strategy/security/StringUtils.java b/sf-vertx/src/main/java/com/sf/vertx/strategy/security/StringUtils.java deleted file mode 100644 index c36668f..0000000 --- a/sf-vertx/src/main/java/com/sf/vertx/strategy/security/StringUtils.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.sf.vertx.strategy.security; - -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; - -public class StringUtils { - /** - * 编码方法 - */ - private static final Charset CHARSET = StandardCharsets.UTF_8; - - /** - * 根据本类的常量对字符串进行获取字节数组的操作 - * - * @param s String:待处理的字符串 - * @return byte[] 形成的字节数组 - */ - public static byte[] getBytes(String s) { - return s.getBytes(CHARSET); - } - - /** - * 将字节数组转为文本 - * - * @param data 字节数组 - * @return 文本 - */ - public static String bytes2String(byte[] data) { - return new String(data, CHARSET); - } - - - /** - * 字符串是不是空的 - * - * @param str 字符串 - * @return 结果 - */ - public static boolean isBlank(String str) { - int strLen; - if (str != null && (strLen = str.length()) != 0) { - for (int i = 0; i < strLen; ++i) { - if (!Character.isWhitespace(str.charAt(i))) { - return false; - } - } - return true; - } else { - return true; - } - } - -} diff --git a/sf-vertx/src/main/java/examples/HttpProxyExamples.java b/sf-vertx/src/main/java/examples/HttpProxyExamples.java new file mode 100644 index 0000000..184ec62 --- /dev/null +++ b/sf-vertx/src/main/java/examples/HttpProxyExamples.java @@ -0,0 +1,182 @@ +package examples; + +import io.vertx.core.Future; +import io.vertx.core.MultiMap; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpClient; +import io.vertx.core.http.HttpServer; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.http.RequestOptions; +import io.vertx.core.net.HostAndPort; +import io.vertx.core.net.SocketAddress; +import io.vertx.httpproxy.Body; +import io.vertx.httpproxy.HttpProxy; +import io.vertx.httpproxy.ProxyContext; +import io.vertx.httpproxy.ProxyInterceptor; +import io.vertx.httpproxy.ProxyOptions; +import io.vertx.httpproxy.ProxyRequest; +import io.vertx.httpproxy.ProxyResponse; +import io.vertx.httpproxy.cache.CacheOptions; + +/** + * @author Emad Alblueshi + */ + +public class HttpProxyExamples { + + public void origin(Vertx vertx) { + HttpServer originServer = vertx.createHttpServer(); + + originServer.requestHandler(req -> { + req.response() + .putHeader("content-type", "text/html") + .end("

I'm the target resource!

"); + }).listen(7070); + } + + public void proxy(Vertx vertx) { + HttpClient proxyClient = vertx.createHttpClient(); + + HttpProxy proxy = HttpProxy.reverseProxy(proxyClient); + proxy.origin(7070, "origin"); + + HttpServer proxyServer = vertx.createHttpServer(); + + proxyServer.requestHandler(proxy).listen(8080); + } + + private SocketAddress resolveOriginAddress(HttpServerRequest request) { + return null; + } + + public void originSelector(HttpProxy proxy) { + proxy.originSelector(request -> Future.succeededFuture(resolveOriginAddress(request))); + } + + private RequestOptions resolveOriginOptions(HttpServerRequest request) { + return null; + } + + public void originRequestProvider(HttpProxy proxy) { + proxy.originRequestProvider((request, client) -> client.request(resolveOriginOptions(request))); + } + + public void inboundInterceptor(HttpProxy proxy) { + proxy.addInterceptor(new ProxyInterceptor() { + @Override + public Future handleProxyRequest(ProxyContext context) { + ProxyRequest proxyRequest = context.request(); + + filter(proxyRequest.headers()); + + // Continue the interception chain + return context.sendRequest(); + } + }); + } + + public void outboundInterceptor(HttpProxy proxy) { + proxy.addInterceptor(new ProxyInterceptor() { + @Override + public Future handleProxyResponse(ProxyContext context) { + ProxyResponse proxyResponse = context.response(); + + filter(proxyResponse.headers()); + + // Continue the interception chain + return context.sendResponse(); + } + }); + } + + public void bodyFilter(HttpProxy proxy) { + proxy.addInterceptor(new ProxyInterceptor() { + @Override + public Future handleProxyResponse(ProxyContext context) { + ProxyResponse proxyResponse = context.response(); + + // Create a filtered body + Body filteredBody = filter(proxyResponse.getBody()); + + // And then let the response use it + proxyResponse.setBody(filteredBody); + + // Continue the interception chain + return context.sendResponse(); + } + }); + } + + public void immediateResponse(HttpProxy proxy) { + proxy.addInterceptor(new ProxyInterceptor() { + @Override + public Future handleProxyRequest(ProxyContext context) { + + ProxyRequest proxyRequest = context.request(); + + // Release the underlying resources + proxyRequest.release(); + + // Create a response and populate it + ProxyResponse proxyResponse = proxyRequest.response() + .setStatusCode(200) + .putHeader("content-type", "text/plain") + .setBody(Body.body(Buffer.buffer("Hello World"))); + + return Future.succeededFuture(proxyResponse); + } + }); + } + + private void filter(MultiMap headers) { + // + } + + private Body filter(Body body) { + return body; + } + + public void more(Vertx vertx, HttpClient proxyClient) { + HttpProxy proxy = HttpProxy.reverseProxy(proxyClient).originSelector( + address -> Future.succeededFuture(SocketAddress.inetSocketAddress(7070, "origin")) + ); + } + + public void lowLevel(Vertx vertx, HttpServer proxyServer, HttpClient proxyClient) { + + proxyServer.requestHandler(request -> { + ProxyRequest proxyRequest = ProxyRequest.reverseProxy(request); + + proxyClient.request(proxyRequest.getMethod(), 8080, "origin", proxyRequest.getURI()) + .compose(proxyRequest::send) + .onSuccess(proxyResponse -> { + // Send the proxy response + proxyResponse.send(); + }) + .onFailure(err -> { + // Release the request + proxyRequest.release(); + + // Send error + request.response().setStatusCode(500) + .send(); + }); + }); + } + + public void overrideAuthority(HttpProxy proxy) { + proxy.addInterceptor(new ProxyInterceptor() { + @Override + public Future handleProxyRequest(ProxyContext context) { + ProxyRequest proxyRequest = context.request(); + proxyRequest.setAuthority(HostAndPort.create("example.com", 80)); + return ProxyInterceptor.super.handleProxyRequest(context); + } + }); + } + + public void cacheConfig(Vertx vertx, HttpClient proxyClient) { + HttpProxy proxy = HttpProxy.reverseProxy(new ProxyOptions().setCacheOptions(new CacheOptions()), proxyClient); + } +} diff --git a/sf-vertx/src/main/java/examples/package-info.java b/sf-vertx/src/main/java/examples/package-info.java new file mode 100644 index 0000000..fd465db --- /dev/null +++ b/sf-vertx/src/main/java/examples/package-info.java @@ -0,0 +1,6 @@ +/** + * @author Emad Alblueshi + */ +@Source +package examples; +import io.vertx.docgen.Source; diff --git a/sf-vertx/src/main/java/io/vertx/httpproxy/Body.java b/sf-vertx/src/main/java/io/vertx/httpproxy/Body.java new file mode 100644 index 0000000..18cd882 --- /dev/null +++ b/sf-vertx/src/main/java/io/vertx/httpproxy/Body.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2011-2020 Contributors to the Eclipse Foundation + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + */ +package io.vertx.httpproxy; + +import io.vertx.codegen.annotations.VertxGen; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.streams.ReadStream; +import io.vertx.httpproxy.impl.BufferedReadStream; + +/** + * Handles the HTTP proxy body. + *

+ * @author Julien Viet + */ +@VertxGen +public interface Body { + + /** + * Create a new {@code Body} instance. + * + * @param stream the {@code ReadStream} of the body + * @param len the determined length of the body + * @return a reference to this, so the API can be used fluently + */ + static Body body(ReadStream stream, long len) { + return new Body() { + @Override + public long length() { + return len; + } + @Override + public ReadStream stream() { + return stream; + } + }; + } + + /** + * Create a new {@code Body} instance. + * + * @param stream the {@link ReadStream} of the body + * @return a reference to this, so the API can be used fluently + */ + static Body body(ReadStream stream) { + return body(stream, -1L); + } + + /** + * Create a new {@code Body} instance. + * + * @param buffer the {@link Buffer} of the body + * @return a reference to this, so the API can be used fluently + */ + static Body body(Buffer buffer) { + return new Body() { + @Override + public long length() { + return buffer.length(); + } + @Override + public ReadStream stream() { + return new BufferedReadStream(buffer); + } + }; + } + + /** + * + * Get length of the {@code Body}. + * + * @return the body length or {@code -1} if that can't be determined + */ + long length(); + + /** + * + * Get stream of the {@code Body}. + * + * @return the body stream + */ + ReadStream stream(); + +} diff --git a/sf-vertx/src/main/java/io/vertx/httpproxy/HttpProxy.java b/sf-vertx/src/main/java/io/vertx/httpproxy/HttpProxy.java new file mode 100644 index 0000000..1c1b2a7 --- /dev/null +++ b/sf-vertx/src/main/java/io/vertx/httpproxy/HttpProxy.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2011-2020 Contributors to the Eclipse Foundation + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + */ +package io.vertx.httpproxy; + +import io.vertx.codegen.annotations.Fluent; +import io.vertx.codegen.annotations.GenIgnore; +import io.vertx.codegen.annotations.VertxGen; +import io.vertx.core.Future; +import io.vertx.core.Handler; +import io.vertx.core.http.HttpClient; +import io.vertx.core.http.HttpClientRequest; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.http.RequestOptions; +import io.vertx.core.net.SocketAddress; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.client.WebClient; +import io.vertx.httpproxy.impl.ReverseProxy; + +import java.util.function.BiFunction; +import java.util.function.Function; + +/** + * Handles the HTTP reverse proxy logic between the user agent and the origin. + *

+ * @author Julien Viet + */ +@VertxGen +public interface HttpProxy extends Handler { + + /** + * Create a new {@code HttpProxy} instance. + * + * @param client the {@code HttpClient} that forwards outbound requests to the origin. + * @return a reference to this, so the API can be used fluently. + */ + static HttpProxy reverseProxy(HttpClient client) { + return new ReverseProxy(new ProxyOptions(), client); + } + + /** + * Create a new {@code HttpProxy} instance. + * + * @param client the {@code HttpClient} that forwards outbound requests to the origin. + * @return a reference to this, so the API can be used fluently. + */ + static HttpProxy reverseProxy(ProxyOptions options, HttpClient client) { + return new ReverseProxy(options, client); + } + + /** + * Set the {@code SocketAddress} of the origin. + * + * @param address the {@code SocketAddress} of the origin + * @return a reference to this, so the API can be used fluently + */ + @Fluent + default HttpProxy origin(SocketAddress address) { + return originSelector(req -> Future.succeededFuture(address)); + } + + /** + * Set the host name and port number of the origin. + * + * @param port the port number of the origin server + * @param host the host name of the origin server + * @return a reference to this, so the API can be used fluently + */ + @Fluent + default HttpProxy origin(int port, String host) { + return origin(SocketAddress.inetSocketAddress(port, host)); + } + + /** + * Set a selector that resolves the origin address based on the incoming HTTP request. + * + * @param selector the selector + * @return a reference to this, so the API can be used fluently + */ + @Fluent + default HttpProxy originSelector(Function> selector) { + return originRequestProvider((req, client) -> selector + .apply(req) + .flatMap(server -> client.request(new RequestOptions().setServer(server)))); + } + + /** + * Set a provider that creates the request to the origin server based the incoming HTTP request. + * Setting a provider overrides any origin selector previously set. + * + * @param provider the provider + * @return a reference to this, so the API can be used fluently + */ + @GenIgnore() + @Fluent + HttpProxy originRequestProvider(BiFunction> provider); + + /** + * Add an interceptor to the interceptor chain. + * + * @param interceptor + * @return a reference to this, so the API can be used fluently + */ + @Fluent + HttpProxy addInterceptor(ProxyInterceptor interceptor); + + /** + * Handle the outbound {@code HttpServerRequest}. + * + * @param request the outbound {@code HttpServerRequest} + */ + void handle(HttpServerRequest request); + + void handle(RoutingContext ctx, WebClient mainWebClient); +} diff --git a/sf-vertx/src/main/java/io/vertx/httpproxy/ProxyContext.java b/sf-vertx/src/main/java/io/vertx/httpproxy/ProxyContext.java new file mode 100644 index 0000000..8853325 --- /dev/null +++ b/sf-vertx/src/main/java/io/vertx/httpproxy/ProxyContext.java @@ -0,0 +1,48 @@ +package io.vertx.httpproxy; + +import io.vertx.codegen.annotations.VertxGen; +import io.vertx.core.Future; + +/** + * A controller for proxy interception. + */ +@VertxGen +public interface ProxyContext { + + /** + * @return the proxy request + */ + ProxyRequest request(); + + /** + * @return the proxy response, it might be {@code null} if the response has not been sent + */ + ProxyResponse response(); + + /** + * + */ + Future sendRequest(); + + /** + * + */ + Future sendResponse(); + + /** + * Attach a payload to the context + * + * @param name the payload name + * @param value any payload value + */ + void set(String name, Object value); + + /** + * Get a payload attached to this context + * + * @param name the payload name + * @param type the expected payload type + * @return the attached payload + */ + T get(String name, Class type); +} diff --git a/sf-vertx/src/main/java/io/vertx/httpproxy/ProxyInterceptor.java b/sf-vertx/src/main/java/io/vertx/httpproxy/ProxyInterceptor.java new file mode 100644 index 0000000..18ad8a3 --- /dev/null +++ b/sf-vertx/src/main/java/io/vertx/httpproxy/ProxyInterceptor.java @@ -0,0 +1,31 @@ +package io.vertx.httpproxy; + +import io.vertx.codegen.annotations.VertxGen; +import io.vertx.core.Future; + +/** + * A {@link HttpProxy} interceptor. + */ +@VertxGen(concrete = false) +public interface ProxyInterceptor { + + /** + * Handle the proxy request at the stage of this interceptor. + * + * @param context the proxy context + * @return when the request has actually been sent to the origin + */ + default Future handleProxyRequest(ProxyContext context) { + return context.sendRequest(); + } + + /** + * Handle the proxy response at the stage of this interceptor. + * + * @param context the proxy context + * @return when the response has actually been sent to the user-agent + */ + default Future handleProxyResponse(ProxyContext context) { + return context.sendResponse(); + } +} diff --git a/sf-vertx/src/main/java/io/vertx/httpproxy/ProxyOptions.java b/sf-vertx/src/main/java/io/vertx/httpproxy/ProxyOptions.java new file mode 100644 index 0000000..bae6bed --- /dev/null +++ b/sf-vertx/src/main/java/io/vertx/httpproxy/ProxyOptions.java @@ -0,0 +1,79 @@ +package io.vertx.httpproxy; + +import io.vertx.codegen.annotations.DataObject; +import io.vertx.codegen.json.annotations.JsonGen; +import io.vertx.core.json.JsonObject; +import io.vertx.httpproxy.cache.CacheOptions; + +/** + * Proxy options. + */ +@DataObject +@JsonGen(publicConverter = false) +public class ProxyOptions { + + /** + * Enable WebSocket support : {@code true} + */ + public static final boolean DEFAULT_SUPPORT_WEBSOCKET = true; + + private CacheOptions cacheOptions; + private boolean supportWebSocket; + + public ProxyOptions(JsonObject json) { + ProxyOptionsConverter.fromJson(json, this); + } + + public ProxyOptions() { + supportWebSocket = DEFAULT_SUPPORT_WEBSOCKET; + } + + /** + * @return the cache options + */ + public CacheOptions getCacheOptions() { + return cacheOptions; + } + + /** + * Set the cache options that configures the proxy. + * + * {@code null} cache options disables caching, by default cache is disabled. + * + * @param cacheOptions the cache options + * @return a reference to this, so the API can be used fluently + */ + public ProxyOptions setCacheOptions(CacheOptions cacheOptions) { + this.cacheOptions = cacheOptions; + return this; + } + + /** + * @return whether WebSocket are supported + */ + public boolean getSupportWebSocket() { + return supportWebSocket; + } + + /** + * Set whether WebSocket are supported. + * + * @param supportWebSocket {@code true} to enable WebSocket support, {@code false} otherwise + * @return a reference to this, so the API can be used fluently + */ + public ProxyOptions setSupportWebSocket(boolean supportWebSocket) { + this.supportWebSocket = supportWebSocket; + return this; + } + + @Override + public String toString() { + return toJson().toString(); + } + + public JsonObject toJson() { + JsonObject json = new JsonObject(); + ProxyOptionsConverter.toJson(this, json); + return json; + } +} diff --git a/sf-vertx/src/main/java/io/vertx/httpproxy/ProxyOptionsConverter.java b/sf-vertx/src/main/java/io/vertx/httpproxy/ProxyOptionsConverter.java new file mode 100644 index 0000000..79e9c79 --- /dev/null +++ b/sf-vertx/src/main/java/io/vertx/httpproxy/ProxyOptionsConverter.java @@ -0,0 +1,47 @@ +package io.vertx.httpproxy; + +import io.vertx.core.json.JsonObject; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.impl.JsonUtil; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.util.Base64; + +/** + * Converter and mapper for {@link io.vertx.httpproxy.ProxyOptions}. + * NOTE: This class has been automatically generated from the {@link io.vertx.httpproxy.ProxyOptions} original class using Vert.x codegen. + */ +public class ProxyOptionsConverter { + + + private static final Base64.Decoder BASE64_DECODER = JsonUtil.BASE64_DECODER; + private static final Base64.Encoder BASE64_ENCODER = JsonUtil.BASE64_ENCODER; + + static void fromJson(Iterable> json, ProxyOptions obj) { + for (java.util.Map.Entry member : json) { + switch (member.getKey()) { + case "cacheOptions": + if (member.getValue() instanceof JsonObject) { + obj.setCacheOptions(new io.vertx.httpproxy.cache.CacheOptions((io.vertx.core.json.JsonObject)member.getValue())); + } + break; + case "supportWebSocket": + if (member.getValue() instanceof Boolean) { + obj.setSupportWebSocket((Boolean)member.getValue()); + } + break; + } + } + } + + static void toJson(ProxyOptions obj, JsonObject json) { + toJson(obj, json.getMap()); + } + + static void toJson(ProxyOptions obj, java.util.Map json) { + if (obj.getCacheOptions() != null) { + json.put("cacheOptions", obj.getCacheOptions().toJson()); + } + json.put("supportWebSocket", obj.getSupportWebSocket()); + } +} diff --git a/sf-vertx/src/main/java/io/vertx/httpproxy/ProxyRequest.java b/sf-vertx/src/main/java/io/vertx/httpproxy/ProxyRequest.java new file mode 100644 index 0000000..5f7bda9 --- /dev/null +++ b/sf-vertx/src/main/java/io/vertx/httpproxy/ProxyRequest.java @@ -0,0 +1,181 @@ +/* + * Copyright (c) 2011-2020 Contributors to the Eclipse Foundation + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + */ +package io.vertx.httpproxy; + +import io.vertx.codegen.annotations.Fluent; +import io.vertx.codegen.annotations.GenIgnore; +import io.vertx.codegen.annotations.VertxGen; +import io.vertx.core.Future; +import io.vertx.core.MultiMap; +import io.vertx.core.http.HttpClientRequest; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.http.HttpVersion; +import io.vertx.core.net.HostAndPort; +import io.vertx.httpproxy.impl.ProxiedRequest; + +/** + * + * Handles the interoperability of the request between the user agent and the origin. + * + * @author Julien Viet + */ +@VertxGen +public interface ProxyRequest { + + /** + * Create a new {@code ProxyRequest} instance, the proxied request will be paused. + * + * @param proxiedRequest the {@code HttpServerRequest} that is proxied + * @return a reference to this, so the API can be used fluently + */ + static ProxyRequest reverseProxy(HttpServerRequest proxiedRequest) { + proxiedRequest.pause(); + return new ProxiedRequest(proxiedRequest); + } + + /** + * @return the HTTP version of the proxied request + */ + HttpVersion version(); + + /** + * @return the absolute URI of the proxied request + */ + String absoluteURI(); + + /** + * @return the HTTP method to be sent to the origin server. + */ + HttpMethod getMethod(); + + /** + * Set the HTTP method to be sent to the origin server. + * + *

The initial HTTP method value is the proxied request HTTP method. + * + * @param method the new HTTP method + * @return a reference to this, so the API can be used fluently + */ + @Fluent + ProxyRequest setMethod(HttpMethod method); + + /** + * @return the request URI to be sent to the origin server. + */ + String getURI(); + + /** + * Set the request URI to be sent to the origin server. + * + *

The initial request URI value is the proxied request URI. + * + * @param uri the new URI + * @return a reference to this, so the API can be used fluently + */ + @Fluent + ProxyRequest setURI(String uri); + + /** + * @return the request body to be sent to the origin server. + */ + Body getBody(); + + /** + * Set the request body to be sent to the origin server. + * + *

The initial request body value is the proxied request body. + * + * @param body the new body + * @return a reference to this, so the API can be used fluently + */ + @Fluent + ProxyRequest setBody(Body body); + + /** + * Set the request authority + * + *

    + *
  • for HTTP/1 the {@literal Host} header
  • + *
  • for HTTP/2 the {@literal :authority} pseudo header
  • + *
+ * + * The value must follow the {@literal :} syntax. + * + * @param authority the authority + * @return a reference to this, so the API can be used fluently + */ + @Fluent + ProxyRequest setAuthority(HostAndPort authority); + + /** + * @return the request authority, for HTTP2 the {@literal :authority} pseudo header otherwise the {@literal Host} header + */ + HostAndPort getAuthority(); + + /** + * @return the headers that will be sent to the origin server, the returned headers can be modified. The headers + * map is populated with the proxied request headers + */ + MultiMap headers(); + + /** + * Put an HTTP header + * + * @param name The header name + * @param value The header value + * @return a reference to this, so the API can be used fluently + */ + @GenIgnore + @Fluent + ProxyRequest putHeader(CharSequence name, CharSequence value); + + /** + * Proxy this request to the origin server using the specified {@code request} and then send the proxy response. + * + * @param request the request connected to the origin server + */ + default Future proxy(HttpClientRequest request) { + return send(request).flatMap(resp -> resp.send()); + } + + /** + * Send this request to the origin server using the specified {@code request}. + * + *

The returned future will be completed with the proxy response returned by the origin. + * + * @param request the request connected to the origin server + */ + Future send(HttpClientRequest request); + + /** + * Release the proxy request and its associated resources + * + *

The HTTP server request is resumed, no HTTP server response is sent. + * + * @return a reference to this, so the API can be used fluently + */ + @Fluent + ProxyRequest release(); + + /** + * @return the proxied HTTP server request + */ + HttpServerRequest proxiedRequest(); + + /** + * Create and return the proxy response. + * + * @return the proxy response + */ + ProxyResponse response(); + +} diff --git a/sf-vertx/src/main/java/io/vertx/httpproxy/ProxyResponse.java b/sf-vertx/src/main/java/io/vertx/httpproxy/ProxyResponse.java new file mode 100644 index 0000000..80b3e0e --- /dev/null +++ b/sf-vertx/src/main/java/io/vertx/httpproxy/ProxyResponse.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2011-2020 Contributors to the Eclipse Foundation + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + */ +package io.vertx.httpproxy; + +import io.vertx.codegen.annotations.Fluent; +import io.vertx.codegen.annotations.GenIgnore; +import io.vertx.codegen.annotations.VertxGen; +import io.vertx.core.Future; +import io.vertx.core.MultiMap; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.streams.ReadStream; + +import java.util.function.Function; + +/** + * + * Handles the interoperability of the response between the origin and the user agent. + * + * @author Julien Viet + */ +@VertxGen +public interface ProxyResponse { + + /** + * + * Return the corresponding {@code ProxyRequest}. + * + * @return the proxy request + */ + ProxyRequest request(); + + /** + * Get the status code. + * + * @return the status code to be sent to the user agent + */ + int getStatusCode(); + + /** + * Set the status code to be sent to the user agent. + * + *

The initial value is the proxied response status code. + * + * @param sc the status code + * @return a reference to this, so the API can be used fluently + */ + @Fluent + ProxyResponse setStatusCode(int sc); + + /** + * Get the status message. + * + * @return the status message to be sent to the user agent + */ + String getStatusMessage(); + + /** + * Set the status message to be sent to the user agent. + * + *

The initial value is the proxied response status message. + * + * @param statusMessage the status message + * @return a reference to this, so the API can be used fluently + */ + @Fluent + ProxyResponse setStatusMessage(String statusMessage); + + /** + * @return the headers that will be sent to the user agent, the returned headers can be modified. The headers + * map is populated with the proxied response headers + */ + MultiMap headers(); + + /** + * Put an HTTP header. + * + * @param name The header name + * @param value The header value + * @return a reference to this, so the API can be used fluently + */ + @GenIgnore + @Fluent + ProxyResponse putHeader(CharSequence name, CharSequence value); + + /** + * Get the body of the response. + * + * @return the response body to be sent to the user agent + */ + Body getBody(); + + /** + * Set the request body to be sent to the user agent. + * + *

The initial request body value is the proxied response body. + * + * @param body the new body + * @return a reference to this, so the API can be used fluently + */ + @Fluent + ProxyResponse setBody(Body body); + + boolean publicCacheControl(); + + long maxAge(); + + /** + * @return the {@code etag} sent by the origin response + */ + String etag(); + + /** + * Send the proxies response to the user agent. + */ + Future send(); + + /** + * Release the proxy response. + * + *

The proxied response is resumed, no HTTP response is sent to the user-agent + */ + @Fluent + ProxyResponse release(); + +} diff --git a/sf-vertx/src/main/java/io/vertx/httpproxy/cache/CacheOptions.java b/sf-vertx/src/main/java/io/vertx/httpproxy/cache/CacheOptions.java new file mode 100644 index 0000000..69af307 --- /dev/null +++ b/sf-vertx/src/main/java/io/vertx/httpproxy/cache/CacheOptions.java @@ -0,0 +1,61 @@ +package io.vertx.httpproxy.cache; + +import io.vertx.codegen.annotations.DataObject; +import io.vertx.codegen.json.annotations.JsonGen; +import io.vertx.core.impl.Arguments; +import io.vertx.core.json.JsonObject; +import io.vertx.httpproxy.impl.CacheImpl; +import io.vertx.httpproxy.spi.cache.Cache; + +/** + * Cache options. + */ +@DataObject +@JsonGen(publicConverter = false) +public class CacheOptions { + + public static final int DEFAULT_MAX_SIZE = 1000; + + private int maxSize = DEFAULT_MAX_SIZE; + + public CacheOptions() { + } + + public CacheOptions(JsonObject json) { + CacheOptionsConverter.fromJson(json, this); + } + + /** + * @return the max number of entries the cache can hold + */ + public int getMaxSize() { + return maxSize; + } + + /** + * Set the max number of entries the cache can hold. + * + * @param maxSize the max size + * @return a reference to this, so the API can be used fluently + */ + public CacheOptions setMaxSize(int maxSize) { + Arguments.require(maxSize > 0, "Max size must be > 0"); + this.maxSize = maxSize; + return this; + } + + public Cache newCache() { + return new CacheImpl<>(this); + } + + @Override + public String toString() { + return toJson().toString(); + } + + public JsonObject toJson() { + JsonObject json = new JsonObject(); + CacheOptionsConverter.toJson(this, json); + return json; + } +} diff --git a/sf-vertx/src/main/java/io/vertx/httpproxy/cache/CacheOptionsConverter.java b/sf-vertx/src/main/java/io/vertx/httpproxy/cache/CacheOptionsConverter.java new file mode 100644 index 0000000..6a637b5 --- /dev/null +++ b/sf-vertx/src/main/java/io/vertx/httpproxy/cache/CacheOptionsConverter.java @@ -0,0 +1,39 @@ +package io.vertx.httpproxy.cache; + +import io.vertx.core.json.JsonObject; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.impl.JsonUtil; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.util.Base64; + +/** + * Converter and mapper for {@link io.vertx.httpproxy.cache.CacheOptions}. + * NOTE: This class has been automatically generated from the {@link io.vertx.httpproxy.cache.CacheOptions} original class using Vert.x codegen. + */ +public class CacheOptionsConverter { + + + private static final Base64.Decoder BASE64_DECODER = JsonUtil.BASE64_DECODER; + private static final Base64.Encoder BASE64_ENCODER = JsonUtil.BASE64_ENCODER; + + static void fromJson(Iterable> json, CacheOptions obj) { + for (java.util.Map.Entry member : json) { + switch (member.getKey()) { + case "maxSize": + if (member.getValue() instanceof Number) { + obj.setMaxSize(((Number)member.getValue()).intValue()); + } + break; + } + } + } + + static void toJson(CacheOptions obj, JsonObject json) { + toJson(obj, json.getMap()); + } + + static void toJson(CacheOptions obj, java.util.Map json) { + json.put("maxSize", obj.getMaxSize()); + } +} diff --git a/sf-vertx/src/main/java/io/vertx/httpproxy/impl/BufferedReadStream.java b/sf-vertx/src/main/java/io/vertx/httpproxy/impl/BufferedReadStream.java new file mode 100644 index 0000000..e4f040d --- /dev/null +++ b/sf-vertx/src/main/java/io/vertx/httpproxy/impl/BufferedReadStream.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2011-2020 Contributors to the Eclipse Foundation + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + */ +package io.vertx.httpproxy.impl; + +import io.vertx.core.Handler; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.streams.ReadStream; + +public class BufferedReadStream implements ReadStream { + + private long demand = 0L; + private Handler endHandler; + private Handler handler; + private boolean ended = false; + private final Buffer content; + + public BufferedReadStream() { + this.content = Buffer.buffer(); + } + + public BufferedReadStream(Buffer content) { + this.content = content; + } + + @Override + public ReadStream exceptionHandler(Handler handler) { + return this; + } + + @Override + public ReadStream handler(Handler handler) { + this.handler = handler; + return this; + } + + @Override + public ReadStream pause() { + demand = 0L; + return this; + } + + @Override + public ReadStream resume() { + fetch(Long.MAX_VALUE); + return this; + } + + @Override + public ReadStream fetch(long amount) { + if (!ended && amount > 0) { + ended = true; + demand += amount; + if (demand < 0L) { + demand = Long.MAX_VALUE; + } + if (demand != Long.MAX_VALUE) { + demand--; + } + if (handler != null && content.length() > 0) { + handler.handle(content); + } + if (endHandler != null) { + endHandler.handle(null); + } + } + return this; + } + + @Override + public ReadStream endHandler(Handler endHandler) { + this.endHandler = endHandler; + return this; + } +} diff --git a/sf-vertx/src/main/java/io/vertx/httpproxy/impl/BufferingReadStream.java b/sf-vertx/src/main/java/io/vertx/httpproxy/impl/BufferingReadStream.java new file mode 100644 index 0000000..8d6185d --- /dev/null +++ b/sf-vertx/src/main/java/io/vertx/httpproxy/impl/BufferingReadStream.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2011-2020 Contributors to the Eclipse Foundation + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + */ +package io.vertx.httpproxy.impl; + +import io.vertx.core.Handler; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.streams.ReadStream; + +class BufferingReadStream implements ReadStream { + + private final ReadStream stream; + private final Buffer content; + private Handler endHandler; + + public BufferingReadStream(ReadStream stream, Buffer content) { + this.stream = stream; + this.content = content; + } + + @Override + public ReadStream exceptionHandler(Handler handler) { + stream.exceptionHandler(handler); + return this; + } + + @Override + public ReadStream handler(Handler handler) { + if (handler != null) { + stream.handler(buff -> { + content.appendBuffer(buff); + handler.handle(buff); + }); + } else { + stream.handler(null); + } + return this; + } + + @Override + public ReadStream pause() { + stream.pause(); + return this; + } + + @Override + public ReadStream resume() { + stream.resume(); + return this; + } + + @Override + public ReadStream fetch(long amount) { + stream.fetch(amount); + return this; + } + + @Override + public ReadStream endHandler(Handler endHandler) { + if (endHandler != null) { + stream.endHandler(v -> { + endHandler.handle(null); + }); + } else { + stream.endHandler(null); + } + return this; + } +} diff --git a/sf-vertx/src/main/java/io/vertx/httpproxy/impl/BufferingWriteStream.java b/sf-vertx/src/main/java/io/vertx/httpproxy/impl/BufferingWriteStream.java new file mode 100644 index 0000000..c0aa8fb --- /dev/null +++ b/sf-vertx/src/main/java/io/vertx/httpproxy/impl/BufferingWriteStream.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2011-2020 Contributors to the Eclipse Foundation + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + */ +package io.vertx.httpproxy.impl; + +import io.vertx.codegen.annotations.Nullable; +import io.vertx.core.AsyncResult; +import io.vertx.core.Future; +import io.vertx.core.Handler; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.streams.ReadStream; +import io.vertx.core.streams.WriteStream; + +class BufferingWriteStream implements WriteStream { + + private final Buffer content; + + public BufferingWriteStream() { + this.content = Buffer.buffer(); + } + + public Buffer content() { + return content; + } + + @Override + public WriteStream exceptionHandler(Handler handler) { + return this; + } + + @Override + public Future write(Buffer data) { + content.appendBuffer(data); + return Future.succeededFuture(); + } + + @Override + public void write(Buffer data, Handler> handler) { + content.appendBuffer(data); + handler.handle(Future.succeededFuture()); + } + + @Override + public void end(Handler> handler) { + handler.handle(Future.succeededFuture()); + } + + @Override + public WriteStream setWriteQueueMaxSize(int maxSize) { + return this; + } + + @Override + public boolean writeQueueFull() { + return false; + } + + @Override + public WriteStream drainHandler(@Nullable Handler handler) { + return this; + } +} diff --git a/sf-vertx/src/main/java/io/vertx/httpproxy/impl/CacheControl.java b/sf-vertx/src/main/java/io/vertx/httpproxy/impl/CacheControl.java new file mode 100644 index 0000000..a89b94c --- /dev/null +++ b/sf-vertx/src/main/java/io/vertx/httpproxy/impl/CacheControl.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2011-2020 Contributors to the Eclipse Foundation + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + */ +package io.vertx.httpproxy.impl; + +/** + * @author Julien Viet + */ +public class CacheControl { + + private int maxAge; + private boolean _public; + + public CacheControl parse(String header) { + maxAge = -1; + _public = false; + String[] parts = header.split(","); // No regex + for (String part : parts) { + part = part.trim().toLowerCase(); + switch (part) { + case "public": + _public = true; + break; + default: + if (part.startsWith("max-age=")) { + maxAge = Integer.parseInt(part.substring(8)); + + } + break; + } + } + return this; + } + + public int maxAge() { + return maxAge; + } + + public boolean isPublic() { + return _public; + } + +} diff --git a/sf-vertx/src/main/java/io/vertx/httpproxy/impl/CacheImpl.java b/sf-vertx/src/main/java/io/vertx/httpproxy/impl/CacheImpl.java new file mode 100644 index 0000000..4614707 --- /dev/null +++ b/sf-vertx/src/main/java/io/vertx/httpproxy/impl/CacheImpl.java @@ -0,0 +1,23 @@ +package io.vertx.httpproxy.impl; + +import io.vertx.httpproxy.cache.CacheOptions; +import io.vertx.httpproxy.spi.cache.Cache; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Simplistic implementation. + */ +public class CacheImpl extends LinkedHashMap implements Cache { + + private final int maxSize; + + public CacheImpl(CacheOptions options) { + this.maxSize = options.getMaxSize(); + } + + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > maxSize; + } +} diff --git a/sf-vertx/src/main/java/io/vertx/httpproxy/impl/CachingFilter.java b/sf-vertx/src/main/java/io/vertx/httpproxy/impl/CachingFilter.java new file mode 100644 index 0000000..5d1ed52 --- /dev/null +++ b/sf-vertx/src/main/java/io/vertx/httpproxy/impl/CachingFilter.java @@ -0,0 +1,150 @@ +package io.vertx.httpproxy.impl; + +import io.vertx.core.Future; +import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.httpproxy.Body; +import io.vertx.httpproxy.ProxyContext; +import io.vertx.httpproxy.ProxyInterceptor; +import io.vertx.httpproxy.ProxyRequest; +import io.vertx.httpproxy.ProxyResponse; +import io.vertx.httpproxy.spi.cache.Cache; + +import java.time.Instant; +import java.util.function.BiFunction; + +class CachingFilter implements ProxyInterceptor { + + private static final BiFunction CACHE_GET_AND_VALIDATE = (key, resource) -> { + long now = System.currentTimeMillis(); + long val = resource.timestamp + resource.maxAge; + return val < now ? null : resource; + }; + + private final Cache cache; + + public CachingFilter(Cache cache) { + this.cache = cache; + } + + @Override + public Future handleProxyRequest(ProxyContext context) { + Future future = tryHandleProxyRequestFromCache(context); + if (future != null) { + return future; + } + return context.sendRequest(); + } + + @Override + public Future handleProxyResponse(ProxyContext context) { + return sendAndTryCacheProxyResponse(context); + } + + private Future sendAndTryCacheProxyResponse(ProxyContext context) { + + ProxyResponse response = context.response(); + Resource cached = context.get("cached_resource", Resource.class); + + if (cached != null && response.getStatusCode() == 304) { + // Warning: this relies on the fact that HttpServerRequest will not send a body for HEAD + response.release(); + cached.init(response); + return context.sendResponse(); + } + + ProxyRequest request = response.request(); + if (response.publicCacheControl() && response.maxAge() > 0) { + if (request.getMethod() == HttpMethod.GET) { + String absoluteUri = request.absoluteURI(); + Resource res = new Resource( + absoluteUri, + response.getStatusCode(), + response.getStatusMessage(), + response.headers(), + System.currentTimeMillis(), + response.maxAge()); + Body body = response.getBody(); + response.setBody(Body.body(new BufferingReadStream(body.stream(), res.content), body.length())); + Future fut = context.sendResponse(); + fut.onSuccess(v -> { + cache.put(absoluteUri, res); + }); + return fut; + } else { + if (request.getMethod() == HttpMethod.HEAD) { + Resource resource = cache.get(request.absoluteURI()); + if (resource != null) { + if (!revalidateResource(response, resource)) { + // Invalidate cache + cache.remove(request.absoluteURI()); + } + } + } + return context.sendResponse(); + } + } else { + return context.sendResponse(); + } + } + + private static boolean revalidateResource(ProxyResponse response, Resource resource) { + if (resource.etag != null && response.etag() != null) { + return resource.etag.equals(response.etag()); + } + return true; + } + + private Future tryHandleProxyRequestFromCache(ProxyContext context) { + + ProxyRequest proxyRequest = context.request(); + + HttpServerRequest response = proxyRequest.proxiedRequest(); + + Resource resource; + HttpMethod method = response.method(); + if (method == HttpMethod.GET || method == HttpMethod.HEAD) { + String cacheKey = proxyRequest.absoluteURI(); + resource = cache.computeIfPresent(cacheKey, CACHE_GET_AND_VALIDATE); + if (resource == null) { + return null; + } + } else { + return null; + } + + String cacheControlHeader = response.getHeader(HttpHeaders.CACHE_CONTROL); + if (cacheControlHeader != null) { + CacheControl cacheControl = new CacheControl().parse(cacheControlHeader); + if (cacheControl.maxAge() >= 0) { + long now = System.currentTimeMillis(); + long currentAge = now - resource.timestamp; + if (currentAge > cacheControl.maxAge() * 1000) { + String etag = resource.headers.get(HttpHeaders.ETAG); + if (etag != null) { + proxyRequest.headers().set(HttpHeaders.IF_NONE_MATCH, resource.etag); + context.set("cached_resource", resource); + return context.sendRequest(); + } else { + return null; + } + } + } + } + + // + String ifModifiedSinceHeader = response.getHeader(HttpHeaders.IF_MODIFIED_SINCE); + if ((response.method() == HttpMethod.GET || response.method() == HttpMethod.HEAD) && ifModifiedSinceHeader != null && resource.lastModified != null) { + Instant ifModifiedSince = ParseUtils.parseHeaderDate(ifModifiedSinceHeader); + if (!ifModifiedSince.isAfter(resource.lastModified)) { + response.response().setStatusCode(304).end(); + return Future.succeededFuture(); + } + } + proxyRequest.release(); + ProxyResponse proxyResponse = proxyRequest.response(); + resource.init(proxyResponse); + return Future.succeededFuture(proxyResponse); + } +} diff --git a/sf-vertx/src/main/java/io/vertx/httpproxy/impl/HttpUtils.java b/sf-vertx/src/main/java/io/vertx/httpproxy/impl/HttpUtils.java new file mode 100644 index 0000000..4aa83f4 --- /dev/null +++ b/sf-vertx/src/main/java/io/vertx/httpproxy/impl/HttpUtils.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2011-2020 Contributors to the Eclipse Foundation + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + */ +package io.vertx.httpproxy.impl; + +import io.vertx.core.MultiMap; +import io.vertx.core.http.HttpHeaders; + +import java.time.Instant; +import java.util.List; + +class HttpUtils { + + static Boolean isChunked(MultiMap headers) { + List te = headers.getAll("transfer-encoding"); + if (te != null) { + boolean chunked = false; + for (String val : te) { + if (val.equals("chunked")) { + chunked = true; + } else { + return null; + } + } + return chunked; + } else { + return false; + } + } + + static Instant dateHeader(MultiMap headers) { + String dateHeader = headers.get(HttpHeaders.DATE); + if (dateHeader == null) { + List warningHeaders = headers.getAll("warning"); + if (warningHeaders.size() > 0) { + for (String warningHeader : warningHeaders) { + Instant date = ParseUtils.parseWarningHeaderDate(warningHeader); + if (date != null) { + return date; + } + } + } + return null; + } else { + return ParseUtils.parseHeaderDate(dateHeader); + } + } +} diff --git a/sf-vertx/src/main/java/io/vertx/httpproxy/impl/ParseUtils.java b/sf-vertx/src/main/java/io/vertx/httpproxy/impl/ParseUtils.java new file mode 100644 index 0000000..b22a224 --- /dev/null +++ b/sf-vertx/src/main/java/io/vertx/httpproxy/impl/ParseUtils.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2011-2020 Contributors to the Eclipse Foundation + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + */ +package io.vertx.httpproxy.impl; + +import java.time.*; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.util.*; + +/** + * @author Julien Viet + */ +public class ParseUtils { + + public static final DateTimeFormatter RFC_850_DATE_TIME = new DateTimeFormatterBuilder() + .appendPattern("EEEE, dd-MMM-yy HH:mm:ss") + .parseLenient() + .appendLiteral(" GMT") + .toFormatter(Locale.US) + .withZone(ZoneId.of("UTC")); + + public static final DateTimeFormatter ASC_TIME = new DateTimeFormatterBuilder() + .appendPattern("EEE MMM d HH:mm:ss yyyy") + .parseLenient() + .toFormatter(Locale.US) + .withZone(ZoneId.of("UTC")); + + public static Instant parseHeaderDate(String value) { + try { + return parseHttpDate(value); + } catch (Exception e) { + return null; + } + } + + public static Instant parseWarningHeaderDate(String value) { + // warn-code + int index = value.indexOf(' '); + if (index > 0) { + // warn-agent + index = value.indexOf(' ', index + 1); + if (index > 0) { + // warn-text + index = value.indexOf(' ', index + 1); + if (index > 0) { + // warn-date + int len = value.length(); + if (index + 2 < len && value.charAt(index + 1) == '"' && value.charAt(len - 1) == '"') { + // Space for 2 double quotes + return parseHeaderDate(value.substring(index + 2, len - 1)); + } + } + } + } + return null; + } + + public static String formatHttpDate(Instant date) { + return DateTimeFormatter.RFC_1123_DATE_TIME.format(OffsetDateTime.ofInstant(date, ZoneOffset.UTC)); + } + + // https://www.rfc-editor.org/rfc/rfc9110#http.date + public static Instant parseHttpDate(String value) throws Exception { + int pos = value.indexOf(','); + if (pos == 3) { // e.g. Sun, 06 Nov 1994 08:49:37 GMT + return DateTimeFormatter.RFC_1123_DATE_TIME.parse(value, Instant::from); + } + if (pos == -1) { // e.g. Sun Nov 6 08:49:37 1994 + return ASC_TIME.parse(value, Instant::from); + } + return RFC_850_DATE_TIME.parse(value, Instant::from); // e.g. Sunday, 06-Nov-94 08:49:37 GMT + } +} diff --git a/sf-vertx/src/main/java/io/vertx/httpproxy/impl/ProxiedRequest.java b/sf-vertx/src/main/java/io/vertx/httpproxy/impl/ProxiedRequest.java new file mode 100644 index 0000000..b3da6fe --- /dev/null +++ b/sf-vertx/src/main/java/io/vertx/httpproxy/impl/ProxiedRequest.java @@ -0,0 +1,230 @@ +/* + * Copyright (c) 2011-2020 Contributors to the Eclipse Foundation + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + */ +package io.vertx.httpproxy.impl; + +import io.vertx.core.AsyncResult; +import io.vertx.core.Future; +import io.vertx.core.Handler; +import io.vertx.core.MultiMap; +import io.vertx.core.Promise; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpClientRequest; +import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.http.HttpVersion; +import io.vertx.core.http.impl.HttpServerRequestInternal; +import io.vertx.core.impl.ContextInternal; +import io.vertx.core.net.HostAndPort; +import io.vertx.core.streams.Pipe; +import io.vertx.httpproxy.Body; +import io.vertx.httpproxy.ProxyRequest; +import io.vertx.httpproxy.ProxyResponse; + +import java.util.Map; +import java.util.Objects; + +public class ProxiedRequest implements ProxyRequest { + + private static final CharSequence X_FORWARDED_HOST = HttpHeaders.createOptimized("x-forwarded-host"); + + private static final MultiMap HOP_BY_HOP_HEADERS = MultiMap.caseInsensitiveMultiMap() + .add(HttpHeaders.CONNECTION, "whatever") + .add(HttpHeaders.KEEP_ALIVE, "whatever") + .add(HttpHeaders.PROXY_AUTHENTICATE, "whatever") + .add(HttpHeaders.PROXY_AUTHORIZATION, "whatever") + .add("te", "whatever") + .add("trailer", "whatever") + .add(HttpHeaders.TRANSFER_ENCODING, "whatever") + .add(HttpHeaders.UPGRADE, "whatever"); + + final ContextInternal context; + private HttpMethod method; + private final HttpVersion version; + private String uri; + private final String absoluteURI; + private Body body; + private HostAndPort authority; + private final MultiMap headers; + HttpClientRequest request; + private final HttpServerRequest proxiedRequest; + + public ProxiedRequest(HttpServerRequest proxiedRequest) { + + // Determine content length + long contentLength = -1L; + String contentLengthHeader = proxiedRequest.getHeader(HttpHeaders.CONTENT_LENGTH); + if (contentLengthHeader != null) { + try { + contentLength = Long.parseLong(contentLengthHeader); + } catch (NumberFormatException e) { + // Ignore ??? + } + } + + this.method = proxiedRequest.method(); + this.version = proxiedRequest.version(); + this.body = Body.body(proxiedRequest, contentLength); + this.uri = proxiedRequest.uri(); + this.headers = MultiMap.caseInsensitiveMultiMap().addAll(proxiedRequest.headers()); + this.absoluteURI = proxiedRequest.absoluteURI(); + this.proxiedRequest = proxiedRequest; + this.context = (ContextInternal) ((HttpServerRequestInternal) proxiedRequest).context(); + this.authority = proxiedRequest.authority(); + } + + @Override + public HttpVersion version() { + return version; + } + + @Override + public String getURI() { + return uri; + } + + @Override + public ProxyRequest setURI(String uri) { + this.uri = uri; + return this; + } + + @Override + public Body getBody() { + return body; + } + + @Override + public ProxyRequest setBody(Body body) { + this.body = body; + return this; + } + + @Override + public ProxyRequest setAuthority(HostAndPort authority) { + Objects.requireNonNull(authority); + this.authority= authority; + return this; + } + + @Override + public HostAndPort getAuthority() { + return authority; + } + + @Override + public String absoluteURI() { + return absoluteURI; + } + + @Override + public HttpMethod getMethod() { + return method; + } + + @Override + public ProxyRequest setMethod(HttpMethod method) { + this.method = method; + return this; + } + + @Override + public HttpServerRequest proxiedRequest() { + return proxiedRequest; + } + + @Override + public ProxyRequest release() { + body.stream().resume(); + headers.clear(); + body = null; + return this; + } + + @Override + public ProxyResponse response() { + return new ProxiedResponse(this, proxiedRequest.response()); + } + + void sendRequest(Handler> responseHandler) { + + request.response().map(r -> { + r.pause(); // Pause it + return new ProxiedResponse(this, proxiedRequest.response(), r); + }).onComplete(responseHandler); + + + request.setMethod(method); + request.setURI(uri); + + // Add all headers + for (Map.Entry header : headers) { + String name = header.getKey(); + String value = header.getValue(); + if (!HOP_BY_HOP_HEADERS.contains(name) && !name.equals("host")) { + request.headers().add(name, value); + } + } + + // + if (authority != null) { + request.authority(authority); + HostAndPort proxiedAuthority = proxiedRequest.authority(); + if (!equals(authority, proxiedAuthority)) { + // Should cope with existing forwarded host headers + request.putHeader(X_FORWARDED_HOST, proxiedAuthority.toString()); + } + } + + long len = body.length(); + if (len >= 0) { + request.putHeader(HttpHeaders.CONTENT_LENGTH, Long.toString(len)); + } else { + Boolean isChunked = HttpUtils.isChunked(proxiedRequest.headers()); + request.setChunked(len == -1 && Boolean.TRUE == isChunked); + } + + Pipe pipe = body.stream().pipe(); + pipe.endOnComplete(true); + pipe.endOnFailure(false); + pipe.to(request, ar -> { + if (ar.failed()) { + request.reset(); + } + }); + } + + private static boolean equals(HostAndPort hp1, HostAndPort hp2) { + if (hp1 == null || hp2 == null) { + return false; + } + return hp1.host().equals(hp2.host()) && hp1.port() == hp2.port(); + } + + @Override + public ProxyRequest putHeader(CharSequence name, CharSequence value) { + headers.set(name, value); + return this; + } + + @Override + public MultiMap headers() { + return headers; + } + + @Override + public Future send(HttpClientRequest request) { + Promise promise = context.promise(); + this.request = request; + sendRequest(promise); + return promise.future(); + } +} diff --git a/sf-vertx/src/main/java/io/vertx/httpproxy/impl/ProxiedResponse.java b/sf-vertx/src/main/java/io/vertx/httpproxy/impl/ProxiedResponse.java new file mode 100644 index 0000000..5080734 --- /dev/null +++ b/sf-vertx/src/main/java/io/vertx/httpproxy/impl/ProxiedResponse.java @@ -0,0 +1,271 @@ +/* + * Copyright (c) 2011-2020 Contributors to the Eclipse Foundation + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + */ +package io.vertx.httpproxy.impl; + +import io.vertx.core.AsyncResult; +import io.vertx.core.Future; +import io.vertx.core.Handler; +import io.vertx.core.MultiMap; +import io.vertx.core.Promise; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpClientResponse; +import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.HttpServerResponse; +import io.vertx.core.http.HttpVersion; +import io.vertx.core.streams.Pipe; +import io.vertx.core.streams.ReadStream; +import io.vertx.httpproxy.Body; +import io.vertx.httpproxy.ProxyRequest; +import io.vertx.httpproxy.ProxyResponse; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +class ProxiedResponse implements ProxyResponse { + + private final ProxiedRequest request; + private final HttpServerResponse proxiedResponse; + private int statusCode; + private String statusMessage; + private Body body; + private final MultiMap headers; + private HttpClientResponse response; + private long maxAge; + private String etag; + private boolean publicCacheControl; + + ProxiedResponse(ProxiedRequest request, HttpServerResponse proxiedResponse) { + this.response = null; + this.statusCode = 200; + this.headers = MultiMap.caseInsensitiveMultiMap(); + this.request = request; + this.proxiedResponse = proxiedResponse; + } + + ProxiedResponse(ProxiedRequest request, HttpServerResponse proxiedResponse, HttpClientResponse response) { + + // Determine content length + long contentLength = -1L; + String contentLengthHeader = response.getHeader(HttpHeaders.CONTENT_LENGTH); + if (contentLengthHeader != null) { + try { + contentLength = Long.parseLong(contentLengthHeader); + } catch (NumberFormatException e) { + // Ignore ??? + } + } + + this.request = request; + this.response = response; + this.proxiedResponse = proxiedResponse; + this.statusCode = response.statusCode(); + this.statusMessage = response.statusMessage(); + this.body = Body.body(response, contentLength); + + long maxAge = -1; + boolean publicCacheControl = false; + String cacheControlHeader = response.getHeader(HttpHeaders.CACHE_CONTROL); + if (cacheControlHeader != null) { + CacheControl cacheControl = new CacheControl().parse(cacheControlHeader); + if (cacheControl.isPublic()) { + publicCacheControl = true; + if (cacheControl.maxAge() > 0) { + maxAge = (long)cacheControl.maxAge() * 1000; + } else { + String dateHeader = response.getHeader(HttpHeaders.DATE); + String expiresHeader = response.getHeader(HttpHeaders.EXPIRES); + if (dateHeader != null && expiresHeader != null) { + maxAge = ParseUtils.parseHeaderDate(expiresHeader).toEpochMilli() - ParseUtils.parseHeaderDate(dateHeader).toEpochMilli(); + } + } + } + } + this.maxAge = maxAge; + this.publicCacheControl = publicCacheControl; + this.etag = response.getHeader(HttpHeaders.ETAG); + this.headers = MultiMap.caseInsensitiveMultiMap().addAll(response.headers()); + } + + @Override + public ProxyRequest request() { + return request; + } + + @Override + public int getStatusCode() { + return statusCode; + } + + @Override + public ProxyResponse setStatusCode(int sc) { + statusCode = sc; + return this; + } + + @Override + public String getStatusMessage() { + return statusMessage; + } + + @Override + public ProxyResponse setStatusMessage(String statusMessage) { + this.statusMessage = statusMessage; + return this; + } + + @Override + public Body getBody() { + return body; + } + + @Override + public ProxyResponse setBody(Body body) { + this.body = body; + return this; + } + + @Override + public boolean publicCacheControl() { + return publicCacheControl; + } + + @Override + public long maxAge() { + return maxAge; + } + + @Override + public String etag() { + return etag; + } + + @Override + public MultiMap headers() { + return headers; + } + + @Override + public ProxyResponse putHeader(CharSequence name, CharSequence value) { + headers.set(name, value); + return this; + } + + @Override + public Future send() { + Promise promise = request.context.promise(); + send(promise); + return promise.future(); + } + + public void send(Handler> completionHandler) { + // Set stuff + proxiedResponse.setStatusCode(statusCode); + + if(statusMessage != null) { + proxiedResponse.setStatusMessage(statusMessage); + } + + // Date header + Instant date = HttpUtils.dateHeader(headers); + if (date == null) { + date = Instant.now(); + } + try { + proxiedResponse.putHeader("date", ParseUtils.formatHttpDate(date)); + } catch (Exception e) { + e.printStackTrace(); + } + + // Warning header + List warningHeaders = headers.getAll("warning"); + if (warningHeaders.size() > 0) { + warningHeaders = new ArrayList<>(warningHeaders); + String dateHeader = headers.get("date"); + Instant dateInstant = dateHeader != null ? ParseUtils.parseHeaderDate(dateHeader) : null; + Iterator i = warningHeaders.iterator(); + // Suppress incorrect warning header + while (i.hasNext()) { + String warningHeader = i.next(); + Instant warningInstant = ParseUtils.parseWarningHeaderDate(warningHeader); + if (warningInstant != null && dateInstant != null && !warningInstant.equals(dateInstant)) { + i.remove(); + } + } + } + proxiedResponse.putHeader("warning", warningHeaders); + + // Handle other headers + headers.forEach(header -> { + String name = header.getKey(); + String value = header.getValue(); + if (name.equalsIgnoreCase("date") || name.equalsIgnoreCase("warning") || name.equalsIgnoreCase("transfer-encoding")) { + // Skip + } else { + proxiedResponse.headers().add(name, value); + } + }); + + // + if (body == null) { + proxiedResponse.end(); + return; + } + + long len = body.length(); + if (len >= 0) { + proxiedResponse.putHeader(HttpHeaders.CONTENT_LENGTH, Long.toString(len)); + } else { + if (request.proxiedRequest().version() == HttpVersion.HTTP_1_0) { + // Special handling for HTTP 1.0 clients that cannot handle chunked encoding + // we need to buffer the content + BufferingWriteStream buffer = new BufferingWriteStream(); + body.stream().pipeTo(buffer, ar -> { + if (ar.succeeded()) { + Buffer content = buffer.content(); + proxiedResponse.end(content, completionHandler); + } else { + System.out.println("Not implemented"); + } + }); + return; + } + proxiedResponse.setChunked(true); + } + ReadStream bodyStream = body.stream(); + sendResponse(bodyStream, completionHandler); + } + + @Override + public ProxyResponse release() { + if (response != null) { + response.resume(); + response = null; + body = null; + headers.clear(); + } + return this; + } + + private void sendResponse(ReadStream body, Handler> completionHandler) { + Pipe pipe = body.pipe(); + pipe.endOnSuccess(true); + pipe.endOnFailure(false); + pipe.to(proxiedResponse, ar -> { + if (ar.failed()) { + request.request.reset(); + proxiedResponse.reset(); + } + completionHandler.handle(ar); + }); + } +} diff --git a/sf-vertx/src/main/java/io/vertx/httpproxy/impl/Resource.java b/sf-vertx/src/main/java/io/vertx/httpproxy/impl/Resource.java new file mode 100644 index 0000000..2b7768b --- /dev/null +++ b/sf-vertx/src/main/java/io/vertx/httpproxy/impl/Resource.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2011-2020 Contributors to the Eclipse Foundation + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + */ +package io.vertx.httpproxy.impl; + +import io.vertx.core.MultiMap; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpHeaders; +import io.vertx.httpproxy.Body; +import io.vertx.httpproxy.ProxyResponse; + +import java.time.Instant; + +class Resource { + + final String absoluteUri; + final int statusCode; + final String statusMessage; + final MultiMap headers; + final long timestamp; + final long maxAge; + final Instant lastModified; + final String etag; + final Buffer content = Buffer.buffer(); + + Resource(String absoluteUri, int statusCode, String statusMessage, MultiMap headers, long timestamp, long maxAge) { + String lastModifiedHeader = headers.get(HttpHeaders.LAST_MODIFIED); + this.absoluteUri = absoluteUri; + this.statusCode = statusCode; + this.statusMessage = statusMessage; + this.headers = headers; + this.timestamp = timestamp; + this.maxAge = maxAge; + this.lastModified = lastModifiedHeader != null ? ParseUtils.parseHeaderDate(lastModifiedHeader) : null; + this.etag = headers.get(HttpHeaders.ETAG); + } + + void init(ProxyResponse proxyResponse) { + proxyResponse.setStatusCode(200); + proxyResponse.setStatusMessage(statusMessage); + proxyResponse.headers().addAll(headers); + proxyResponse.setBody(Body.body(content)); + } +} diff --git a/sf-vertx/src/main/java/io/vertx/httpproxy/impl/ReverseProxy.java b/sf-vertx/src/main/java/io/vertx/httpproxy/impl/ReverseProxy.java new file mode 100644 index 0000000..b39373b --- /dev/null +++ b/sf-vertx/src/main/java/io/vertx/httpproxy/impl/ReverseProxy.java @@ -0,0 +1,299 @@ +/* + * Copyright (c) 2011-2020 Contributors to the Eclipse Foundation + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + */ +package io.vertx.httpproxy.impl; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.function.BiFunction; + +import org.apache.commons.lang3.StringUtils; + +import com.sf.vertx.init.DynamicBuildServer; +import com.sf.vertx.security.MainSecurity; +import com.sf.vertx.service.impl.AppConfigServiceImpl; + +import io.vertx.core.Future; +import io.vertx.core.Promise; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpClient; +import io.vertx.core.http.HttpClientRequest; +import io.vertx.core.http.HttpClientResponse; +import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.http.HttpServerResponse; +import io.vertx.core.json.JsonObject; +import io.vertx.core.net.NetSocket; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.client.WebClient; +import io.vertx.httpproxy.Body; +import io.vertx.httpproxy.HttpProxy; +import io.vertx.httpproxy.ProxyContext; +import io.vertx.httpproxy.ProxyInterceptor; +import io.vertx.httpproxy.ProxyOptions; +import io.vertx.httpproxy.ProxyRequest; +import io.vertx.httpproxy.ProxyResponse; +import io.vertx.httpproxy.cache.CacheOptions; +import io.vertx.httpproxy.spi.cache.Cache; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class ReverseProxy implements HttpProxy { + + private final HttpClient client; + private final boolean supportWebSocket; + private BiFunction> selector = (req, client) -> Future + .failedFuture("No origin available"); + private final List interceptors = new ArrayList<>(); + private RoutingContext ctx; + private WebClient mainWebClient; + + public ReverseProxy(ProxyOptions options, HttpClient client) { + CacheOptions cacheOptions = options.getCacheOptions(); + if (cacheOptions != null) { + Cache cache = cacheOptions.newCache(); + addInterceptor(new CachingFilter(cache)); + } + this.client = client; + this.supportWebSocket = options.getSupportWebSocket(); + } + + @Override + public HttpProxy originRequestProvider( + BiFunction> provider) { + selector = provider; + return this; + } + + @Override + public HttpProxy addInterceptor(ProxyInterceptor interceptor) { + interceptors.add(interceptor); + return this; + } + + @Override + public void handle(RoutingContext ctx, WebClient mainWebClient) { + // TODO 改造了这个地方 + this.ctx = ctx; + this.mainWebClient = mainWebClient; + handle(ctx.request()); + } + + @Override + public void handle(HttpServerRequest request) { + ProxyRequest proxyRequest = ProxyRequest.reverseProxy(request); + + // Encoding sanity check + Boolean chunked = HttpUtils.isChunked(request.headers()); + if (chunked == null) { + end(proxyRequest, 400); + return; + } + + // WebSocket upgrade tunneling + if (supportWebSocket && io.vertx.core.http.impl.HttpUtils.canUpgradeToWebSocket(request)) { + handleWebSocketUpgrade(proxyRequest); + return; + } + + Proxy proxy = new Proxy(proxyRequest); + proxy.filters = interceptors.listIterator(); + proxy.sendRequest().compose(proxy::sendProxyResponse); + } + + private void handleWebSocketUpgrade(ProxyRequest proxyRequest) { + HttpServerRequest proxiedRequest = proxyRequest.proxiedRequest(); + resolveOrigin(proxiedRequest).onComplete(ar -> { + if (ar.succeeded()) { + HttpClientRequest request = ar.result(); + request.setMethod(HttpMethod.GET); + request.setURI(proxiedRequest.uri()); + request.headers().addAll(proxiedRequest.headers()); + Future fut2 = request.connect(); + proxiedRequest.handler(request::write); + proxiedRequest.endHandler(v -> request.end()); + proxiedRequest.resume(); + fut2.onComplete(ar2 -> { + if (ar2.succeeded()) { + HttpClientResponse proxiedResponse = ar2.result(); + if (proxiedResponse.statusCode() == 101) { + HttpServerResponse response = proxiedRequest.response(); + response.setStatusCode(101); + response.headers().addAll(proxiedResponse.headers()); + Future otherso = proxiedRequest.toNetSocket(); + otherso.onComplete(ar3 -> { + if (ar3.succeeded()) { + NetSocket responseSocket = ar3.result(); + NetSocket proxyResponseSocket = proxiedResponse.netSocket(); + responseSocket.handler(proxyResponseSocket::write); + proxyResponseSocket.handler(responseSocket::write); + responseSocket.closeHandler(v -> proxyResponseSocket.close()); + proxyResponseSocket.closeHandler(v -> responseSocket.close()); + } else { + // Find reproducer + System.err.println("Handle this case"); + ar3.cause().printStackTrace(); + } + }); + } else { + // Rejection + proxiedRequest.resume(); + end(proxyRequest, proxiedResponse.statusCode()); + } + } else { + proxiedRequest.resume(); + end(proxyRequest, 502); + } + }); + } else { + proxiedRequest.resume(); + end(proxyRequest, 502); + } + }); + } + + private void end(ProxyRequest proxyRequest, int sc) { + proxyRequest.response().release().setStatusCode(sc).putHeader(HttpHeaders.CONTENT_LENGTH, "0").setBody(null) + .send(); + } + + private Future resolveOrigin(HttpServerRequest proxiedRequest) { + return selector.apply(proxiedRequest, client); + } + + private class Proxy implements ProxyContext { + + private final ProxyRequest request; + private ProxyResponse response; + private final Map attachments = new HashMap<>(); + private ListIterator filters; + + private Proxy(ProxyRequest request) { + this.request = request; + } + + @Override + public void set(String name, Object value) { + attachments.put(name, value); + } + + @Override + public T get(String name, Class type) { + Object o = attachments.get(name); + return type.isInstance(o) ? type.cast(o) : null; + } + + @Override + public ProxyRequest request() { + return request; + } + + @Override + public Future sendRequest() { + if (filters.hasNext()) { + ProxyInterceptor next = filters.next(); + return next.handleProxyRequest(this); + } else { + return sendProxyRequest(request); + } + } + + @Override + public ProxyResponse response() { + return response; + } + + @Override + public Future sendResponse() { + if (filters.hasPrevious()) { + ProxyInterceptor filter = filters.previous(); + return filter.handleProxyResponse(this); + } else { + return response.send(); + } + } + + private Future sendProxyRequest(ProxyRequest proxyRequest) { + // TODO 改造了这个地方 + // 创建一个响应并设置参数 +// ctx.request().response().setStatusCode(200) +// .putHeader("content-type", "text/plain").end(body); + // 发起一个请求 + String sacAppHeaderKey = proxyRequest.headers().get(DynamicBuildServer.SAC_APP_HEADER_KEY); + if (StringUtils.isNotBlank(sacAppHeaderKey) && AppConfigServiceImpl.appDataSecurity(sacAppHeaderKey)) { + String body = ctx.getBodyAsString(); + String bodyData = MainSecurity.aesDecrypt(body, "dadddsdfadfadsfa33323223"); + return Future.future(p -> { + mainWebClient.post(9198, "127.0.0.1", "/vertx/body").sendJson(bodyData, h -> { + if (h.succeeded()) { + // 释放资源 + proxyRequest.release(); + JsonObject responseData = h.result().bodyAsJsonObject(); + log.info("responseData:{}", responseData); + // 加密 + String dataStr = MainSecurity.aesEncrypt(responseData.toString(), "dadddsdfadfadsfa33323223"); + log.info("aesEncrypt dataStr:{}", dataStr); + ctx.request().response().setStatusCode(200).putHeader("content-type", "application/json") + .end(dataStr); + Buffer buffer = Buffer.buffer(dataStr); + ProxyResponse proxyResponse = proxyRequest.response().setStatusCode(200) + .putHeader("content-type", "application/json").setBody(Body.body(buffer)); + p.complete(proxyResponse); + } else { + p.fail(h.cause()); + } + }); + }); + } else { + Future f = resolveOrigin(proxyRequest.proxiedRequest()); + f.onFailure(err -> { + // Should this be done here ? I don't think so + HttpServerRequest proxiedRequest = proxyRequest.proxiedRequest(); + proxiedRequest.resume(); + Promise promise = Promise.promise(); + proxiedRequest.exceptionHandler(promise::tryFail); + proxiedRequest.endHandler(promise::tryComplete); + promise.future().onComplete(ar2 -> { + end(proxyRequest, 502); + }); + }); + return f.compose(a -> sendProxyRequest(proxyRequest, a)); + } + + } + + private Future sendProxyRequest(ProxyRequest proxyRequest, HttpClientRequest request) { + Future fut = proxyRequest.send(request); + fut.onFailure(err -> { + proxyRequest.proxiedRequest().response().setStatusCode(502).end(); + }); + return fut; + } + + private Future sendProxyResponse(ProxyResponse response) { + + this.response = response; + + // Check validity + Boolean chunked = HttpUtils.isChunked(response.headers()); + if (chunked == null) { + // response.request().release(); // Is it needed ??? + end(response.request(), 501); + return Future.succeededFuture(); // should use END future here ??? + } + + return sendResponse(); + } + } +} diff --git a/sf-vertx/src/main/java/io/vertx/httpproxy/package-info.java b/sf-vertx/src/main/java/io/vertx/httpproxy/package-info.java new file mode 100644 index 0000000..d364f8e --- /dev/null +++ b/sf-vertx/src/main/java/io/vertx/httpproxy/package-info.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2011-2020 Contributors to the Eclipse Foundation + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + */ + +/** + * @author Julien Viet + */ +@ModuleGen(name = "vertx-http-proxy", groupPackage = "io.vertx", useFutures = true) +package io.vertx.httpproxy; + +import io.vertx.codegen.annotations.ModuleGen; diff --git a/sf-vertx/src/main/java/io/vertx/httpproxy/spi/cache/Cache.java b/sf-vertx/src/main/java/io/vertx/httpproxy/spi/cache/Cache.java new file mode 100644 index 0000000..c930719 --- /dev/null +++ b/sf-vertx/src/main/java/io/vertx/httpproxy/spi/cache/Cache.java @@ -0,0 +1,9 @@ +package io.vertx.httpproxy.spi.cache; + +import java.util.Map; + +/** + * Cache SPI. + */ +public interface Cache extends Map { +}