001/** 002 * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). 003 * <p> 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * <p> 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * <p> 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package io.jboot.components.gateway; 017 018import com.jfinal.kit.LogKit; 019import com.jfinal.log.Log; 020import io.jboot.exception.JbootException; 021import io.jboot.utils.StrUtil; 022 023import javax.net.ssl.*; 024import javax.servlet.http.HttpServletRequest; 025import javax.servlet.http.HttpServletResponse; 026import java.io.*; 027import java.net.HttpURLConnection; 028import java.net.ProtocolException; 029import java.net.URL; 030import java.security.cert.X509Certificate; 031import java.util.*; 032import java.util.zip.GZIPInputStream; 033 034public class GatewayHttpProxy { 035 036 private static final Log LOG = Log.getLog(GatewayHttpProxy.class); 037 038 private int readTimeOut = 10000; //10s 039 private int connectTimeOut = 5000; //5s 040 private int retries = 2; 041 private String contentType = JbootGatewayConfig.DEFAULT_PROXY_CONTENT_TYPE; 042 043 044 private boolean instanceFollowRedirects = false; 045 private boolean useCaches = false; 046 047 private Map<String, String> headers; 048 049 private Exception exception; 050 051 052 public GatewayHttpProxy() { 053 } 054 055 056 public GatewayHttpProxy(JbootGatewayConfig config) { 057 this.readTimeOut = config.getProxyReadTimeout(); 058 this.connectTimeOut = config.getProxyConnectTimeout(); 059 this.retries = config.getProxyRetries(); 060 this.contentType = config.getProxyContentType(); 061 } 062 063 064 public void sendRequest(String url, HttpServletRequest req, HttpServletResponse resp) { 065 int triesCount = Math.max(retries, 0); 066 Exception exception = null; 067 068 do { 069 try { 070 exception = null; 071 doSendRequest(url, req, resp); 072 } catch (Exception ex) { 073 exception = ex; 074 } 075 } while (exception != null && triesCount-- > 0); 076 077 if (exception != null) { 078 this.exception = exception; 079 LOG.error(exception.toString(), exception); 080 } 081 } 082 083 084 protected void doSendRequest(String url, HttpServletRequest req, HttpServletResponse resp) throws Exception { 085 086 HttpURLConnection conn = null; 087 try { 088 conn = getConnection(url); 089 090 /** 091 * 配置 HttpURLConnection 的 http 请求头 092 */ 093 configConnection(conn, req); 094 095 096 // get 请求 097 if ("get".equalsIgnoreCase(req.getMethod())) { 098 conn.connect(); 099 } 100 // post 请求 101 else { 102 conn.setDoOutput(true); 103 conn.setDoInput(true); 104 copyRequestStreamToConnection(req, conn); 105 } 106 107 108 /** 109 * 配置 HttpServletResponse 的 http 响应头 110 */ 111 configResponse(resp, conn); 112 113 /** 114 * 复制链接的 inputStream 流到 Response 115 */ 116 copyConnStreamToResponse(conn, resp); 117 118 } finally { 119 if (conn != null) { 120 conn.disconnect(); 121 } 122 } 123 } 124 125 126 protected void copyRequestStreamToConnection(HttpServletRequest req, HttpURLConnection conn) throws IOException { 127 OutputStream outStream = null; 128 InputStream inStream = null; 129 try { 130 outStream = conn.getOutputStream(); 131 inStream = req.getInputStream(); 132 int len; 133 byte[] buffer = new byte[1024]; 134 while ((len = inStream.read(buffer)) != -1) { 135 outStream.write(buffer, 0, len); 136 } 137 } finally { 138 quetlyClose(outStream, inStream); 139 } 140 } 141 142 143 protected void copyConnStreamToResponse(HttpURLConnection conn, HttpServletResponse resp) throws IOException { 144 if (resp.isCommitted()) { 145 return; 146 } 147 148 InputStream inStream = null; 149 OutputStream outStream = null; 150 try { 151 inStream = getInputStream(conn); 152 outStream = resp.getOutputStream(); 153 byte[] buffer = new byte[1024]; 154 for (int len; (len = inStream.read(buffer)) != -1; ) { 155 outStream.write(buffer, 0, len); 156 } 157// outStream.flush(); 158 } finally { 159 quetlyClose(inStream); 160 } 161 } 162 163 164 protected void quetlyClose(Closeable... closeables) { 165 for (Closeable closeable : closeables) { 166 if (closeable != null) { 167 try { 168 closeable.close(); 169 } catch (IOException e) { 170 LogKit.logNothing(e); 171 } 172 } 173 } 174 } 175 176 177 protected void configResponse(HttpServletResponse resp, HttpURLConnection conn) throws IOException { 178 179 if (resp.isCommitted()) { 180 return; 181 } 182 183 resp.setStatus(conn.getResponseCode()); 184 185 //conn 是否已经指定了 contentType,如果指定了,就用 conn 的,否则就用自己配置的 186 boolean isContentTypeSetted = false; 187 188 Map<String, List<String>> headerFields = conn.getHeaderFields(); 189 if (headerFields != null && !headerFields.isEmpty()) { 190 Set<String> headerNames = headerFields.keySet(); 191 for (String headerName : headerNames) { 192 //需要排除 Content-Encoding,因为 Server 可能已经使用 gzip 压缩,但是此代理已经对 gzip 内容进行解压了 193 if (StrUtil.isBlank(headerName) || "Content-Encoding".equalsIgnoreCase(headerName)) { 194 continue; 195 } 196 197 String headerFieldValue = conn.getHeaderField(headerName); 198 if (StrUtil.isNotBlank(headerFieldValue)) { 199 resp.setHeader(headerName, headerFieldValue); 200 if ("Content-Type".equalsIgnoreCase(headerName)) { 201 isContentTypeSetted = true; 202 } 203 } 204 } 205 } 206 207 //conn 没有 Content-Type,需要设置为手动配置的内容 208 if (!isContentTypeSetted) { 209 resp.setContentType(contentType); 210 } 211 } 212 213 protected InputStream getInputStream(HttpURLConnection conn) throws IOException { 214 InputStream stream = conn.getResponseCode() >= 400 215 ? conn.getErrorStream() 216 : conn.getInputStream(); 217 218 if ("gzip".equalsIgnoreCase(conn.getContentEncoding())) { 219 return new GZIPInputStream(stream); 220 } else { 221 return stream; 222 } 223 } 224 225 226 protected void configConnection(HttpURLConnection conn, HttpServletRequest req) throws ProtocolException { 227 228 conn.setReadTimeout(readTimeOut); 229 conn.setConnectTimeout(connectTimeOut); 230 conn.setInstanceFollowRedirects(instanceFollowRedirects); 231 conn.setUseCaches(useCaches); 232 233 conn.setRequestMethod(req.getMethod()); 234 235 Enumeration<String> headerNames = req.getHeaderNames(); 236 while (headerNames.hasMoreElements()) { 237 String headerName = headerNames.nextElement(); 238 if (StrUtil.isNotBlank(headerName)) { 239 String headerFieldValue = req.getHeader(headerName); 240 if (StrUtil.isNotBlank(headerFieldValue)) { 241 conn.setRequestProperty(headerName, headerFieldValue); 242 } 243 } 244 } 245 246 if (this.headers != null) { 247 for (Map.Entry<String, String> entry : this.headers.entrySet()) { 248 conn.setRequestProperty(entry.getKey(), entry.getValue()); 249 } 250 } 251 } 252 253 protected HttpURLConnection getConnection(String urlString) { 254 try { 255 if (urlString.toLowerCase().startsWith("https")) { 256 return getHttpsConnection(urlString); 257 } else { 258 return getHttpConnection(urlString); 259 } 260 } catch (Throwable ex) { 261 throw new JbootException(ex); 262 } 263 } 264 265 protected HttpURLConnection getHttpConnection(String urlString) throws Exception { 266 URL url = new URL(urlString); 267 return (HttpURLConnection) url.openConnection(); 268 } 269 270 protected HttpsURLConnection getHttpsConnection(String urlString) throws Exception { 271 272 SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE"); 273 TrustManager[] tm = {trustAnyTrustManager}; 274 sslContext.init(null, tm, null); 275 SSLSocketFactory ssf = sslContext.getSocketFactory(); 276 277 URL url = new URL(urlString); 278 HttpsURLConnection conn = (HttpsURLConnection) url.openConnection(); 279 conn.setHostnameVerifier(hnv); 280 conn.setSSLSocketFactory(ssf); 281 return conn; 282 } 283 284 protected static X509TrustManager trustAnyTrustManager = new X509TrustManager() { 285 @Override 286 public void checkClientTrusted(X509Certificate[] chain, String authType) { 287 } 288 289 @Override 290 public void checkServerTrusted(X509Certificate[] chain, String authType) { 291 } 292 293 @Override 294 public X509Certificate[] getAcceptedIssuers() { 295 return null; 296 } 297 }; 298 299 protected static HostnameVerifier hnv = (hostname, session) -> true; 300 301 302 public Exception getException() { 303 return exception; 304 } 305 306 public void setException(Exception exception) { 307 this.exception = exception; 308 } 309 310 public int getReadTimeOut() { 311 return readTimeOut; 312 } 313 314 public void setReadTimeOut(int readTimeOut) { 315 this.readTimeOut = readTimeOut; 316 } 317 318 public int getConnectTimeOut() { 319 return connectTimeOut; 320 } 321 322 public void setConnectTimeOut(int connectTimeOut) { 323 this.connectTimeOut = connectTimeOut; 324 } 325 326 public int getRetries() { 327 return retries; 328 } 329 330 public void setRetries(int retries) { 331 this.retries = retries; 332 } 333 334 public String getContentType() { 335 return contentType; 336 } 337 338 public void setContentType(String contentType) { 339 this.contentType = contentType; 340 } 341 342 public boolean isInstanceFollowRedirects() { 343 return instanceFollowRedirects; 344 } 345 346 public void setInstanceFollowRedirects(boolean instanceFollowRedirects) { 347 this.instanceFollowRedirects = instanceFollowRedirects; 348 } 349 350 public boolean isUseCaches() { 351 return useCaches; 352 } 353 354 public void setUseCaches(boolean useCaches) { 355 this.useCaches = useCaches; 356 } 357 358 public Map<String, String> getHeaders() { 359 return headers; 360 } 361 362 public void setHeaders(Map<String, String> headers) { 363 this.headers = headers; 364 } 365 366 public GatewayHttpProxy addHeader(String key, String value) { 367 if (this.headers == null) { 368 this.headers = new HashMap<>(); 369 } 370 this.headers.put(key, value); 371 return this; 372 } 373 374 public GatewayHttpProxy addHeaders(Map<String, String> headers) { 375 if (this.headers == null) { 376 this.headers = new HashMap<>(); 377 } 378 this.headers.putAll(headers); 379 return this; 380 } 381 382 383}