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}