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.http.jboot;
017
018import com.jfinal.log.Log;
019import io.jboot.components.http.HttpMimeTypes;
020import io.jboot.components.http.JbootHttp;
021import io.jboot.components.http.JbootHttpRequest;
022import io.jboot.components.http.JbootHttpResponse;
023import io.jboot.exception.JbootException;
024import io.jboot.utils.ArrayUtil;
025import io.jboot.utils.QuietlyUtil;
026import io.jboot.utils.StrUtil;
027
028import javax.net.ssl.*;
029import java.io.*;
030import java.net.HttpCookie;
031import java.net.HttpURLConnection;
032import java.net.ProtocolException;
033import java.net.URL;
034import java.security.KeyStore;
035import java.security.SecureRandom;
036import java.security.cert.X509Certificate;
037import java.util.List;
038import java.util.Map;
039import java.util.zip.GZIPInputStream;
040
041
042public class JbootHttpImpl implements JbootHttp {
043
044    private static final Log LOG = Log.getLog(JbootHttpImpl.class);
045
046
047    @Override
048    public JbootHttpResponse handle(JbootHttpRequest request) {
049        JbootHttpResponse response = new JbootHttpResponse(request);
050        doProcess(request, response);
051        return response;
052    }
053
054
055    private void doProcess(JbootHttpRequest request, JbootHttpResponse response) {
056        HttpURLConnection connection = null;
057        InputStream inStream = null;
058        try {
059
060            //获取 http 链接
061            connection = getConnection(request);
062
063            //配置 http 链接
064            configConnection(connection, request);
065
066
067            //post 或者 put 请求
068            if (request.isPostRequest() || request.isPutRequest()) {
069
070                connection.setDoOutput(true);
071
072                //处理文件上传的post提交
073                if (request.isMultipartFormData()) {
074                    if (ArrayUtil.isNotEmpty(request.getParams())) {
075                        uploadByMultipart(request, connection);
076                    }
077                }
078
079                //处理正常的post提交
080                else {
081                    String uploadBodyString = request.getUploadBodyString();
082                    if (StrUtil.isNotEmpty(uploadBodyString)) {
083                        byte[] bytes = uploadBodyString.getBytes(request.getCharset());
084
085                        if (StrUtil.isBlank(request.getHeader("Content-Length"))) {
086                            connection.setRequestProperty("Content-Length", String.valueOf(bytes.length));
087                        }
088
089                        try (OutputStream outStream = connection.getOutputStream();) {
090                            outStream.write(bytes);
091                            outStream.flush();
092                        }
093                    }
094                }
095            }
096
097            //get 请求
098            else {
099                connection.connect();
100            }
101
102            int responseCode = connection.getResponseCode();
103
104            //自动重定向
105            if (responseCode >= 300 && responseCode < 400 && request.isAutoRedirect()) {
106                processRedirect(request, response, connection);
107                return;
108            }
109
110
111            inStream = getInputStream(connection, responseCode);
112
113            response.setContentType(connection.getContentType());
114            response.setResponseCode(connection.getResponseCode());
115            response.setHeaders(connection.getHeaderFields());
116
117            //是否要读取 body 数据
118            if (request.isReadBody()) {
119                response.copyStream(inStream);
120            }
121
122        } catch (Throwable ex) {
123            response.setError(ex);
124            LOG.error(ex.toString(), ex);
125        } finally {
126
127            if (connection != null) {
128                connection.disconnect();
129            }
130
131            QuietlyUtil.closeQuietly(inStream, response);
132        }
133    }
134
135
136    /**
137     * 手动重定向
138     *
139     * @param request
140     * @param response
141     * @param connection
142     */
143    private void processRedirect(JbootHttpRequest request, JbootHttpResponse response, HttpURLConnection connection) throws IOException {
144        if (request.getCurrentRedirectCount() > request.getMaxRedirectCount()) {
145            throw new IOException("Exceeded redirect count.");
146        }
147
148
149        String location = connection.getHeaderField("Location");
150        request.setCurrentRedirectCount(request.getCurrentRedirectCount() + 1);
151
152        //绝对路径
153        if (location.startsWith("/")) {
154            int firstSlash = request.getRequestUrl().indexOf("/", 8); // 8  == "https://".length()
155            location = request.getRequestUrl().substring(0, firstSlash) + location;
156        }
157
158        //相对路径
159        else if (!location.toLowerCase().startsWith("http")) {
160            int lastSlash = request.getRequestUrl().lastIndexOf("/");
161            location = request.getRequestUrl().substring(0, lastSlash + 1) + location;
162        }
163
164        //携带 cookie
165        String responseCookieString = connection.getHeaderField("Set-Cookie");
166        if (StrUtil.isNotBlank(responseCookieString)) {
167            List<HttpCookie> cookies = HttpCookie.parse(responseCookieString);
168            StringBuilder cookie = new StringBuilder(StrUtil.obtainDefault(request.getHeader("Cookie"), ""));
169            for (HttpCookie httpCookie : cookies) {
170                cookie.append(httpCookie.getName()).append("=").append(httpCookie.getValue()).append("; ");
171            }
172            request.addHeader("Cookie", cookie.toString());
173        }
174
175        request.setRequestUrl(location);
176        request.setMethod(JbootHttpRequest.METHOD_GET);
177
178        doProcess(request, response);
179    }
180
181
182    private InputStream getInputStream(HttpURLConnection connection, int responseCode) throws IOException {
183        InputStream stream = responseCode >= 400 ? connection.getErrorStream() : connection.getInputStream();
184        if ("gzip".equalsIgnoreCase(connection.getContentEncoding())) {
185            return new GZIPInputStream(stream);
186        } else {
187            return stream;
188        }
189
190    }
191
192
193    private void uploadByMultipart(JbootHttpRequest request, HttpURLConnection connection) throws IOException {
194        String endFlag = "\r\n";
195        String startFlag = "--";
196        String boundary = "------" + StrUtil.uuid();
197        connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
198        DataOutputStream dos = new DataOutputStream(connection.getOutputStream());
199        for (Map.Entry entry : request.getParams().entrySet()) {
200            if (entry.getValue() instanceof File) {
201                File file = (File) entry.getValue();
202                checkFileNormal(file);
203                writeString(dos, request, startFlag + boundary + endFlag);
204                writeString(dos, request, "Content-Disposition: form-data; name=\"" + entry.getKey() + "\"; filename=\"" + file.getName() + "\"");
205                writeString(dos, request, endFlag + "Content-Type: " + HttpMimeTypes.getMimeType(file.getName()));
206                writeString(dos, request, endFlag + endFlag);
207
208                writeFile(dos, file);
209
210                writeString(dos, request, endFlag);
211            } else {
212                writeString(dos, request, startFlag + boundary + endFlag);
213                writeString(dos, request, "Content-Disposition: form-data; name=\"" + entry.getKey() + "\"");
214                writeString(dos, request, endFlag + endFlag);
215                writeString(dos, request, String.valueOf(entry.getValue()));
216                writeString(dos, request, endFlag);
217            }
218        }
219
220        writeString(dos, request, startFlag + boundary + startFlag + endFlag);
221        dos.flush();
222    }
223
224    private void writeString(DataOutputStream dos, JbootHttpRequest request, String s) throws IOException {
225        dos.write(s.getBytes(request.getCharset()));
226    }
227
228    private void writeFile(DataOutputStream dos, File file) throws IOException {
229        try (FileInputStream fStream = new FileInputStream(file)) {
230            byte[] buffer = new byte[2028];
231            for (int len = 0; (len = fStream.read(buffer)) > 0; ) {
232                dos.write(buffer, 0, len);
233            }
234        }
235    }
236
237    private static void checkFileNormal(File file) {
238        if (!file.exists()) {
239            throw new JbootException("file not exists!!!!" + file);
240        }
241        if (file.isDirectory()) {
242            throw new JbootException("cannot upload directory!!!!" + file);
243        }
244        if (!file.canRead()) {
245            throw new JbootException("cannnot read file!!!" + file);
246        }
247    }
248
249
250    private static void configConnection(HttpURLConnection connection, JbootHttpRequest request) throws ProtocolException {
251        if (connection == null) {
252            return;
253        }
254        connection.setReadTimeout(request.getReadTimeOut());
255        connection.setConnectTimeout(request.getConnectTimeOut());
256        connection.setRequestMethod(request.getMethod());
257        connection.setInstanceFollowRedirects(request.isInstanceFollowRedirects());
258
259        //如果 reqeust 的 header 不配置 content-Type, 使用默认的
260        connection.setRequestProperty("Content-Type", request.getContentType());
261
262        if (request.getHeaders() != null && request.getHeaders().size() > 0) {
263            for (Map.Entry<String, String> entry : request.getHeaders().entrySet()) {
264                connection.setRequestProperty(entry.getKey(), entry.getValue());
265            }
266        }
267    }
268
269    private static HttpURLConnection getConnection(JbootHttpRequest request) throws Exception {
270
271        //get 请求 或者 带有body内容的 post 请求,需要在 url 追加参数
272        if (!request.isPostOrPutRequest() || request.getBodyContent() != null) {
273            request.appendParasToUrl();
274        }
275
276        return request.isHttps() ? getHttpsConnection(request) : getHttpConnection(request);
277    }
278
279    private static HttpURLConnection getHttpConnection(JbootHttpRequest request) throws Exception {
280        URL url = new URL(request.getRequestUrl());
281        HttpURLConnection conn = (HttpURLConnection) (request.getProxy() != null
282                ? url.openConnection(request.getProxy()) : url.openConnection());
283        return conn;
284    }
285
286    private static HttpsURLConnection getHttpsConnection(JbootHttpRequest request) throws Exception {
287        URL url = new URL(request.getRequestUrl());
288        HttpsURLConnection conn = (HttpsURLConnection) (request.getProxy() != null
289                ? url.openConnection(request.getProxy()) : url.openConnection());
290
291        //自定义 sslContext
292        if (request.getSslContext() != null) {
293            SSLSocketFactory ssf = request.getSslContext().getSocketFactory();
294            conn.setSSLSocketFactory(ssf);
295        }
296        //配置证书的路径和密码
297        else if (request.getCertPath() != null && request.getCertPass() != null) {
298
299            KeyStore clientStore = KeyStore.getInstance("PKCS12");
300            clientStore.load(request.getCertInputStream(), request.getCertPass().toCharArray());
301
302            KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
303            keyManagerFactory.init(clientStore, request.getCertPass().toCharArray());
304            KeyManager[] keyManagers = keyManagerFactory.getKeyManagers();
305
306
307            TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
308            trustManagerFactory.init(clientStore);
309
310            SSLContext sslContext = SSLContext.getInstance("TLS");
311            sslContext.init(keyManagers, trustManagerFactory.getTrustManagers(), new SecureRandom());
312
313            conn.setSSLSocketFactory(sslContext.getSocketFactory());
314
315        }
316        // 默认的 sslContext
317        else {
318            conn.setHostnameVerifier(hnv);
319            SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");
320            if (sslContext != null) {
321                TrustManager[] tm = {trustAnyTrustManager};
322                sslContext.init(null, tm, null);
323                SSLSocketFactory ssf = sslContext.getSocketFactory();
324                conn.setSSLSocketFactory(ssf);
325            }
326        }
327        return conn;
328    }
329
330    private static X509TrustManager trustAnyTrustManager = new X509TrustManager() {
331        @Override
332        public void checkClientTrusted(X509Certificate[] chain, String authType) {
333        }
334
335        @Override
336        public void checkServerTrusted(X509Certificate[] chain, String authType) {
337        }
338
339        @Override
340        public X509Certificate[] getAcceptedIssuers() {
341            return null;
342        }
343    };
344
345    private static HostnameVerifier hnv = (hostname, session) -> true;
346
347
348}