├── .gitignore ├── LICENSE ├── README.md ├── ToDoList.md ├── pom.xml └── src ├── main └── java │ └── me │ └── gv7 │ └── woodpecker │ └── requests │ ├── AbstractResponse.java │ ├── BasicAuth.java │ ├── Cookie.java │ ├── DefaultSettings.java │ ├── Header.java │ ├── Headers.java │ ├── HttpConnInputStream.java │ ├── HttpHeaders.java │ ├── Interceptor.java │ ├── InterceptorChain.java │ ├── KeyStores.java │ ├── Methods.java │ ├── Parameter.java │ ├── Proxies.java │ ├── RawResponse.java │ ├── Request.java │ ├── RequestBuilder.java │ ├── Requests.java │ ├── Response.java │ ├── ResponseHandler.java │ ├── Session.java │ ├── StatusCodes.java │ ├── body │ ├── BytesRequestBody.java │ ├── ContentTypes.java │ ├── FileRequestBody.java │ ├── FormRequestBody.java │ ├── InputStreamRequestBody.java │ ├── InputStreamSupplier.java │ ├── InputStreamSupplierRequestBody.java │ ├── JsonRequestBody.java │ ├── MultiPartRequestBody.java │ ├── Part.java │ ├── RequestBody.java │ ├── StringRequestBody.java │ └── package-info.java │ ├── config │ ├── CustomHttpHeaderConfig.java │ ├── HttpConfigManager.java │ ├── ProxyConfig.java │ ├── TimeoutConfig.java │ └── UserAgentConfig.java │ ├── exception │ ├── IllegalStatusException.java │ ├── RequestsException.java │ ├── TooManyRedirectsException.java │ └── TrustManagerLoadFailedException.java │ ├── executor │ ├── CookieJar.java │ ├── DefaultCookieJar.java │ ├── HttpExecutor.java │ ├── NopCookieJar.java │ ├── RequestExecutorFactory.java │ ├── SessionContext.java │ ├── URLConnectionExecutor.java │ ├── URLConnectionExecutorFactory.java │ └── package-info.java │ ├── json │ ├── FastJsonProcessor.java │ ├── GsonProcessor.java │ ├── JacksonProcessor.java │ ├── JsonLookup.java │ ├── JsonProcessor.java │ ├── JsonProcessorNotFoundException.java │ ├── TypeInfer.java │ └── package-info.java │ ├── package-info.java │ └── utils │ ├── CookieDateUtil.java │ ├── Cookies.java │ ├── NopHostnameVerifier.java │ ├── SSLSocketFactories.java │ ├── URLUtils.java │ └── package-info.java └── test ├── java └── me │ └── gv7 │ └── woodpecker │ └── requests │ ├── ChunkedHeaderTest.java │ ├── CookieTest.java │ ├── HeadersTest.java │ ├── IgnoreHttpConfigTest.java │ ├── MultiPartTest.java │ ├── RequestsProxyTest.java │ ├── RequestsTest.java │ ├── SessionTest.java │ ├── body │ └── RequestBodyTest.java │ ├── executor │ └── DefaultCookieJarTest.java │ ├── json │ ├── FastJsonProcessorTest.java │ └── JsonLookupTest.java │ ├── mock │ ├── EchoBodyServlet.java │ ├── EchoHeaderServlet.java │ ├── MockBasicAuthenticationServlet.java │ ├── MockGetServlet.java │ ├── MockMultiPartServlet.java │ ├── MockPostServlet.java │ └── MockServer.java │ └── util │ ├── CookiesTest.java │ └── URLUtilsTest.java └── resources ├── jetty-logging.properties └── keystore /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | *.iml 3 | .idea/ 4 | out/ 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Liu Dong 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |

woodpecker-requests

3 |

4 | 5 |

6 | 7 | 8 | 9 |

10 | 11 | 12 | `woodpecker-requests`是基于 [requests](https://github.com/hsiafan/requests) 为woodpecker框架定制开发的httpclient库,目的是编写插件时能拥有像python requests一样的便利。特点为可以全局设置代理、全局设置UA等 13 | 14 | 15 | ---- 16 | 17 | Requests is a http request lib with fluent api for java, inspired by the python request module. 18 | Requests requires JDK 1.8+, the last version support Java7 is 4.18.* . 19 | 20 | Table of Contents 21 | ================= 22 | 23 | * [Maven Setting](#maven-setting) 24 | * [Usage](#usage) 25 | * [Simple Case](#simple-case) 26 | * [Charset](#charset) 27 | * [Passing Parameters](#passing-parameters) 28 | * [Set Headers](#set-headers) 29 | * [Cookies](#cookies) 30 | * [Request with data](#request-with-data) 31 | * [Json support](#json-support) 32 | * [Basic Auth](#basic-auth) 33 | * [Redirection](#redirection) 34 | * [Timeout](#timeout) 35 | * [Response compress](#response-compress-encoding) 36 | * [Https Verification](#https-verification) 37 | * [Proxy](#proxy) 38 | * [Session](#session) 39 | 40 | # Maven Setting 41 | 42 | Requests is now in maven central repo. 43 | 44 | 45 | https://mvnrepository.com/artifact/me.gv7.woodpecker/woodpecker-requests 46 | 47 | ```xml 48 | 49 | me.gv7.woodpecker 50 | woodpecker-requests 51 | 0.1.0 52 | 53 | ``` 54 | 55 | # Global Config 56 | 57 | ### global proxy 58 | 59 | ```java 60 | public static boolean enable = false; // open proxy or close 61 | public static String protocol = "http"; // http or socks 62 | public static String host = "127.0.0.1"; 63 | public static int port = 8080; 64 | public static String username; // socks proxy user,can set null 65 | public static String password; // socks proxy pass,can set null 66 | HttpConfigManager.setProxyConfig(enable 67 | ,protocol 68 | ,host 69 | ,port 70 | ,username 71 | ,password); 72 | } 73 | ``` 74 | ### Global proxies are not used 75 | 76 | ```java 77 | Requests.method("GET","http://wwww.baidu.com/").proxy(Proxy.NO_PROXY).send(); 78 | ``` 79 | 80 | ### global user-agent 81 | 82 | ```java 83 | LinkedList uaList = new LinkedList<>(); 84 | // set greater than 2 will random choise. 85 | uaList.add("Mozilla/5.0 (Android; Mobile; rv:14.0) Gecko/14.0 Firefox/14.0"); 86 | uaList.add("Mozilla/5.0 (Android; Tablet; rv:14.0) Gecko/14.0 Firefox/14.0"); 87 | HttpConfigManager.setUserAgentConfig(uaList); 88 | ``` 89 | 90 | ### global time out 91 | 92 | ```java 93 | int time = 5000; // !!!attention!!! millisecond 94 | boolean enableMandatoryTimeout = false; // ignored user set 95 | int mandatoryTimeout = 1; 96 | HttpConfigManager.setTimeoutConfig(time 97 | ,enableMandatoryTimeout 98 | ,mandatoryTimeout); 99 | ``` 100 | **enableMandatoryTimeout** will ignore user set,such us: 101 | ```java 102 | Requests.get("http://woodpecker.gv7.me/").timeout(10000).send() // timeout will be replaced by mandatoryTimeout 103 | ``` 104 | # Usage 105 | 106 | ## Simple Case 107 | One simple http request example that do http get request and read response as string: 108 | 109 | ```java 110 | String url = ...; 111 | String resp = Requests.get(url).send().readToText(); 112 | // or 113 | Response resp = Requests.get(url).send().toTextResponse(); 114 | ``` 115 | 116 | Post and other method: 117 | 118 | ```java 119 | resp = Requests.post(url).send().readToText(); 120 | resp = Requests.head(url).send().readToText(); 121 | ... 122 | ``` 123 | 124 | The response object have several common http response fields can be used: 125 | 126 | ```java 127 | RawResponse resp = Requests.get(url).send(); 128 | int statusCode = resp.statusCode(); 129 | String contentLen = resp.getHeader("Content-Length"); 130 | Cookie cookie = resp.getCookie("_bd_name"); 131 | String body = resp.readToText(); 132 | ``` 133 | Make sure call readToText or other methods to consume resp, or call close method to close resp. 134 | 135 | The readToText() method here trans http response body as String, more other methods provided: 136 | 137 | ```java 138 | // get response as string, use encoding get from response header 139 | String resp = Requests.get(url).send().readToText(); 140 | // get response as bytes 141 | byte[] resp1 = Requests.get(url).send().readToBytes(); 142 | // save response as file 143 | boolean result = Requests.get(url).send().writeToFile("/path/to/save/file"); 144 | ``` 145 | 146 | ## Charset 147 | 148 | Requests default use UTF-8 to encode parameters, post forms or request string body, you can set other charset by: 149 | 150 | ```java 151 | String resp = Requests.get(url).charset(StandardCharsets.ISO_8859_1).send().readToText(); 152 | ``` 153 | 154 | When read response to text-based result, use charset get from http response header, or UTF-8 if not found. 155 | You can force use specified charset by: 156 | 157 | ```java 158 | String resp = Requests.get(url).send().charset(StandardCharsets.ISO_8859_1).readToText(); 159 | ``` 160 | 161 | ## Passing Parameters 162 | 163 | Pass parameters in urls using params method: 164 | 165 | ```java 166 | // set params by map 167 | Map params = new HashMap<>(); 168 | params.put("k1", "v1"); 169 | params.put("k2", "v2"); 170 | String resp = Requests.get(url).params(params).send().readToText(); 171 | // set multi params 172 | String resp = Requests.get(url) 173 | .params(Parameter.of("k1", "v1"), Parameter.of("k2", "v2")) 174 | .send().readToText(); 175 | ``` 176 | 177 | If you want to send post www-form-encoded parameters, use body() methods: 178 | 179 | ```java 180 | // set params by map 181 | Map params = new HashMap<>(); 182 | params.put("k1", "v1"); 183 | params.put("k2", "v2"); 184 | String resp = Requests.post(url).body(params).send().readToText(); 185 | // set multi params 186 | String resp = Requests.post(url) 187 | .body(Parameter.of("k1", "v1"), Parameter.of("k2", "v2")) 188 | .send().readToText(); 189 | ``` 190 | The forms parameter should only works with post method. 191 | 192 | ## Set Headers 193 | 194 | Http request headers can be set by headers method: 195 | 196 | ```java 197 | // set headers by map 198 | Map headers = new HashMap<>(); 199 | headers.put("k1", "v1"); 200 | headers.put("k2", "v2"); 201 | String resp = Requests.get(url).headers(headers).send().readToText(); 202 | // set multi headers 203 | String resp = Requests.get(url) 204 | .headers(new Header("k1", "v1"), new Header("k2", "v2")) 205 | .send().readToText(); 206 | ``` 207 | 208 | ## Cookies 209 | 210 | Cookies can be add by: 211 | 212 | ```java 213 | Map cookies = new HashMap<>(); 214 | cookies.put("k1", "v1"); 215 | cookies.put("k2", "v2"); 216 | // set cookies by map 217 | String resp = Requests.get(url).cookies(cookies).send().readToText(); 218 | // set cookies 219 | String resp = Requests.get(url) 220 | .cookies(Parameter.of("k1", "v1"), Parameter.of("k2", "v2")) 221 | .send().readToText(); 222 | ``` 223 | 224 | ## Request with data 225 | 226 | Http Post, Put, Patch method can send request body. Take Post for example: 227 | 228 | ```java 229 | // set post form data 230 | String resp = Requests.post(url).body(Parameter.of("k1", "v1"), Parameter.of("k2", "v2")) 231 | .send().readToText(); 232 | // set post form data by map 233 | Map formData = new HashMap<>(); 234 | formData.put("k1", "v1"); 235 | formData.put("k2", "v2"); 236 | String resp = Requests.post(url).body(formData).send().readToText(); 237 | // send byte array data as body 238 | byte[] data = ...; 239 | resp = Requests.post(url).body(data).send().readToText(); 240 | // send string data as body 241 | String str = ...; 242 | resp = Requests.post(url).body(str).send().readToText(); 243 | // send data from inputStream 244 | InputStreamSupplier supplier = ...; 245 | resp = Requests.post(url).body(supplier).send().readToText(); 246 | ``` 247 | 248 | One more complicate situation is multiPart post request, this can be done via multiPart method, 249 | one simplified multi part request example which send files and param data: 250 | 251 | ```java 252 | // send form-encoded data 253 | InputStreamSupplier supplier = ...; 254 | byte[] bytes = ...; 255 | String resp = Requests.post(url) 256 | .multiPartBody( 257 | Part.file("file1", new File(...)), 258 | Part.file("file2", "second_file.dat", supplier), 259 | Part.text("input", "on") 260 | ).send().readToText(); 261 | ``` 262 | 263 | ## Json support 264 | 265 | Requests can handle json encoder(for request body)/decoder(for response body), if having Json Binding, Jackson, Gson, or Fastjson lib in classpath. 266 | 267 | ```java 268 | // send json body, content-type is set to application/json 269 | RawResponse response = Requests.post("http://.../update_person") 270 | .jsonBody(value) 271 | .send(); 272 | // response body as json, to value 273 | Person person = Requests.post("http://.../get_person") 274 | .params(Parameter.of("id", 101)) 275 | .send().readToJson(Person.class); 276 | // json body decoder to generic type 277 | List persons = Requests.post("http://.../get_person_list") 278 | .send().readToJson(new TypeInfer>() {}); 279 | 280 | ``` 281 | 282 | You may set your own json processor by: 283 | 284 | ```java 285 | JsonProcessor jsonProcessor = ...; 286 | JsonLookup.getInstance().register(jsonProcessor); 287 | 288 | ``` 289 | 290 | ## Basic Auth 291 | 292 | Set http basic auth param by auth method: 293 | 294 | ```java 295 | String resp = Requests.get(url).basicAuth("user", "passwd").send().readToText(); 296 | ``` 297 | 298 | ## Redirection 299 | 300 | Requests will handle 30x http redirect automatically, you can disable it by: 301 | 302 | ```java 303 | Requests.get(url).followRedirect(false).send(); 304 | ``` 305 | 306 | ## Timeout 307 | 308 | You can set connection connect timeout, and socket read/write timeout value, as blow: 309 | 310 | ```java 311 | // set connect timeout and socket timeout 312 | Requests.get(url).socksTimeout(20_000).connectTimeout(30_000).send(); 313 | ``` 314 | 315 | ## Response compress encoding 316 | Requests send Accept-Encoding: gzip, deflate, and auto handle response decompress in default. You can disable this by: 317 | 318 | ```java 319 | // do not send Accept-Encoding: gzip, deflate header 320 | String resp = Requests.get(url).acceptCompress(false).send().readToText(); 321 | // do not decompress response body 322 | String resp2 = Requests.get(url).send().decompress(false).readToText(); 323 | ``` 324 | 325 | ## Https Verification 326 | 327 | Some https sites do not have trusted http certificate, Exception will be thrown when request. 328 | You can disable https certificate verify by: 329 | 330 | ```java 331 | Requests.get(url).verify(false).send(); 332 | ``` 333 | 334 | ## Proxy 335 | 336 | Set proxy by proxy method: 337 | 338 | ```java 339 | Requests.get(url).proxy(Proxies.httpProxy("127.0.0.1", 8081)).send(); // http proxy 340 | Requests.get(url).proxy(Proxies.socksProxy("127.0.0.1", 1080)).send(); // socks proxy proxy 341 | ``` 342 | 343 | # Session 344 | 345 | Session maintains cookies, basic auth and maybe other http context for you, useful when need login or other situations. 346 | Session have the same usage as Requests. 347 | 348 | ```java 349 | Session session = Requests.session(); 350 | String resp1 = session.get(url1).send().readToText(); 351 | String resp2 = session.get(url2).send().readToText(); 352 | ``` 353 | -------------------------------------------------------------------------------- /ToDoList.md: -------------------------------------------------------------------------------- 1 | - [ ] 支持分块传输 -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 4.0.0 7 | me.gv7.woodpecker 8 | woodpecker-requests 9 | jar 10 | 0.2.1 11 | https://github.com/woodpecker-framework/woodpecker-requests 12 | 13 | 14 | org.sonatype.oss 15 | oss-parent 16 | 7 17 | 18 | 19 | woodpecker-requests are secondary development based on https://github.com/hsiafan/requests, as the woodpecker framework tailored httpclient 20 | 21 | 22 | 23 | 24 | MIT License 25 | http://www.opensource.org/licenses/mit-license.php 26 | 27 | 28 | 29 | 30 | 31 | scm:git:git://github.com/woodpecker-framework/woodpecker-requests.git 32 | scm:git:ssh://github.com:woodpecker-framework/woodpecker-requests.git 33 | http://github.com/woodpecker-framework/woodpecker-requests/tree/master 34 | 35 | 36 | 37 | 38 | 39 | c0ny1 40 | guishuox@gmail.com 41 | woodpecker-framework 42 | http://woodpecker.gv7.me 43 | 44 | 45 | 46 | 47 | 1.8 48 | 1.8 49 | 2.10.0 50 | 2.8.5 51 | 1.2.51 52 | 53 | 54 | 55 | 56 | org.checkerframework 57 | checker-qual 58 | 2.6.0 59 | provided 60 | 61 | 62 | net.dongliu 63 | commons 64 | 8.1.0 65 | 66 | 67 | com.google.code.gson 68 | gson 69 | ${gson.version} 70 | true 71 | 72 | 73 | com.fasterxml.jackson.core 74 | jackson-core 75 | ${jackson.version} 76 | true 77 | 78 | 79 | com.fasterxml.jackson.core 80 | jackson-databind 81 | ${jackson.version} 82 | true 83 | 84 | 85 | com.fasterxml.jackson.core 86 | jackson-annotations 87 | ${jackson.version} 88 | true 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | junit 99 | junit 100 | 4.12 101 | test 102 | 103 | 104 | org.eclipse.jetty 105 | jetty-servlet 106 | 9.3.13.v20161014 107 | test 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | org.apache.maven.plugins 130 | maven-source-plugin 131 | 3.1.0 132 | 133 | 134 | attach-sources 135 | 136 | jar-no-fork 137 | 138 | 139 | 140 | 141 | 142 | 143 | org.apache.maven.plugins 144 | maven-compiler-plugin 145 | 3.3 146 | 147 | 1.8 148 | 1.8 149 | UTF-8 150 | true 151 | 152 | 153 | 154 | 155 | org.apache.maven.plugins 156 | maven-gpg-plugin 157 | 1.6 158 | 159 | 160 | sign-artifacts 161 | verify 162 | 163 | sign 164 | 165 | 166 | 167 | 168 | 169 | 170 | org.sonatype.plugins 171 | nexus-staging-maven-plugin 172 | 1.6.7 173 | true 174 | 175 | woodpecker 176 | https://oss.sonatype.org/ 177 | true 178 | 179 | 180 | 181 | 182 | org.apache.maven.plugins 183 | maven-release-plugin 184 | 2.5.3 185 | 186 | 187 | 188 | org.apache.maven.plugins 189 | maven-javadoc-plugin 190 | 3.2.0 191 | 192 | 193 | attach-javadocs 194 | 195 | jar 196 | 197 | 198 | -Xdoclint:none 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | woodpecker 210 | https://oss.sonatype.org/content/repositories/snapshots 211 | 212 | 213 | woodpecker 214 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 215 | 216 | 217 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/AbstractResponse.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests; 2 | 3 | import org.checkerframework.checker.nullness.qual.Nullable; 4 | 5 | import java.util.Collections; 6 | import java.util.List; 7 | 8 | /** 9 | * Common parent for RawResponse and Response 10 | */ 11 | class AbstractResponse { 12 | protected final String url; 13 | protected final int statusCode; 14 | protected final List cookies; 15 | protected final Headers headers; 16 | 17 | protected AbstractResponse(String url, int statusCode, List cookies, Headers headers) { 18 | this.url = url; 19 | this.statusCode = statusCode; 20 | this.cookies = Collections.unmodifiableList(cookies); 21 | this.headers = headers; 22 | } 23 | 24 | /** 25 | * Get actual url (redirected) 26 | * @deprecated using {@link #url} 27 | */ 28 | @Deprecated 29 | public String getURL() { 30 | return url; 31 | } 32 | 33 | /** 34 | * return actual url (redirected) 35 | */ 36 | public String url() { 37 | return url; 38 | } 39 | 40 | /** 41 | * return response status code 42 | * @return status code 43 | * @deprecated using {@link #statusCode} 44 | */ 45 | @Deprecated 46 | public int getStatusCode() { 47 | return statusCode; 48 | } 49 | 50 | /** 51 | * return response status code 52 | * @return status code 53 | */ 54 | public int statusCode() { 55 | return statusCode; 56 | } 57 | 58 | /** 59 | * Get all cookies returned by this response 60 | * @deprecated using {@link #cookies} 61 | */ 62 | @Deprecated 63 | public List getCookies() { 64 | return cookies; 65 | } 66 | 67 | /** 68 | * Get all cookies returned by this response 69 | */ 70 | 71 | public List cookies() { 72 | return cookies; 73 | } 74 | 75 | /** 76 | * Get all response headers 77 | * @deprecated using {@link #headers} 78 | */ 79 | @Deprecated 80 | public List
getHeaders() { 81 | return headers.getHeaders(); 82 | } 83 | 84 | /** 85 | * Return all response headers 86 | */ 87 | public List
headers() { 88 | return headers.getHeaders(); 89 | } 90 | 91 | /** 92 | * Get first cookie match the name returned by this response, return null if not found 93 | * 94 | * @deprecated using {{@link #getCookie(String)}} instead 95 | */ 96 | @Deprecated 97 | @Nullable 98 | public Cookie getFirstCookie(String name) { 99 | return getCookie(name); 100 | } 101 | 102 | /** 103 | * Get first cookie match the name returned by this response, return null if not found 104 | */ 105 | @Nullable 106 | public Cookie getCookie(String name) { 107 | for (Cookie cookie : cookies) { 108 | if (cookie.name().equals(name)) { 109 | return cookie; 110 | } 111 | } 112 | return null; 113 | } 114 | 115 | /** 116 | * Get first header value match the name, return null if not exists 117 | * 118 | * @deprecated using {@link #getHeader(String)} instead 119 | */ 120 | @Deprecated 121 | @Nullable 122 | public String getFirstHeader(String name) { 123 | return headers.getFirstHeader(name); 124 | } 125 | 126 | /** 127 | * Get first header value match the name, return null if not exists 128 | */ 129 | @Nullable 130 | public String getHeader(String name) { 131 | return headers.getHeader(name); 132 | } 133 | 134 | /** 135 | * Get all headers values with name. If not exists, return empty list 136 | */ 137 | public List getHeaders(String name) { 138 | return this.headers.getHeaders(name); 139 | } 140 | 141 | } 142 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/BasicAuth.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests; 2 | 3 | 4 | import java.io.Serializable; 5 | import java.util.Base64; 6 | import java.util.Objects; 7 | 8 | import static java.nio.charset.StandardCharsets.UTF_8; 9 | 10 | /** 11 | * Http Basic Authentication 12 | * 13 | * @author Liu Dong 14 | */ 15 | public class BasicAuth implements Serializable { 16 | private static final long serialVersionUID = 7453526434365174929L; 17 | private final String user; 18 | private final String password; 19 | 20 | public BasicAuth(String user, String password) { 21 | this.user = Objects.requireNonNull(user); 22 | this.password = Objects.requireNonNull(password); 23 | } 24 | 25 | /** 26 | * @deprecated use {@link #user()} 27 | */ 28 | @Deprecated 29 | public String getUser() { 30 | return user; 31 | } 32 | 33 | /** 34 | * @deprecated use {@link #password()} 35 | */ 36 | @Deprecated 37 | public String getPassword() { 38 | return password; 39 | } 40 | 41 | public String user() { 42 | return user; 43 | } 44 | 45 | public String password() { 46 | return password; 47 | } 48 | 49 | /** 50 | * Encode to http header 51 | */ 52 | public String encode() { 53 | return "Basic " + Base64.getEncoder().encodeToString((user + ":" + password).getBytes(UTF_8)); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/Cookie.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests; 2 | 3 | 4 | import java.io.Serializable; 5 | import java.util.Map; 6 | 7 | import static java.util.Objects.requireNonNull; 8 | 9 | public class Cookie extends Parameter implements Map.Entry, Serializable { 10 | private static final long serialVersionUID = -287880603936079757L; 11 | /** 12 | * The cookie domain set by attribute or from url 13 | */ 14 | private final String domain; 15 | /** 16 | * The cookie path set by attribute or from url 17 | */ 18 | private final String path; 19 | /** 20 | * The cookie expire timestamp, zero means no expiry is set 21 | */ 22 | private final long expiry; 23 | /** 24 | * If secure attribute is set 25 | */ 26 | private final boolean secure; 27 | 28 | /** 29 | * If true, the cookie did not set domain attribute 30 | */ 31 | private final boolean hostOnly; 32 | 33 | public Cookie(String domain, String path, String name, String value, long expiry, boolean secure, 34 | boolean hostOnly) { 35 | super(name, value); 36 | this.domain = requireNonNull(domain); 37 | this.path = requireNonNull(path); 38 | this.expiry = expiry; 39 | this.secure = secure; 40 | this.hostOnly = hostOnly; 41 | } 42 | 43 | /** 44 | * If cookie is expired 45 | */ 46 | public boolean expired(long now) { 47 | return expiry != 0 && expiry < now; 48 | } 49 | 50 | /** 51 | * @deprecated use {@link #domain()} 52 | */ 53 | @Deprecated 54 | public String getDomain() { 55 | return domain; 56 | } 57 | 58 | /** 59 | * @deprecated use {@link #secure()} 60 | */ 61 | @Deprecated 62 | public boolean isSecure() { 63 | return secure; 64 | } 65 | 66 | /** 67 | * @deprecated use {@link #expiry()} 68 | */ 69 | @Deprecated 70 | public long getExpiry() { 71 | return expiry; 72 | } 73 | 74 | /** 75 | * @deprecated use {@link #path()} 76 | */ 77 | @Deprecated 78 | public String getPath() { 79 | return path; 80 | } 81 | 82 | /** 83 | * @deprecated use {@link #hostOnly()} 84 | */ 85 | @Deprecated 86 | public boolean isHostOnly() { 87 | return hostOnly; 88 | } 89 | 90 | public String domain() { 91 | return domain; 92 | } 93 | 94 | public boolean secure() { 95 | return secure; 96 | } 97 | 98 | public long expiry() { 99 | return expiry; 100 | } 101 | 102 | public String path() { 103 | return path; 104 | } 105 | 106 | public boolean hostOnly() { 107 | return hostOnly; 108 | } 109 | 110 | @Override 111 | public boolean equals(Object o) { 112 | if (this == o) return true; 113 | if (o == null || getClass() != o.getClass()) return false; 114 | 115 | Cookie cookie = (Cookie) o; 116 | 117 | if (expiry != cookie.expiry) return false; 118 | if (secure != cookie.secure) return false; 119 | if (hostOnly != cookie.hostOnly) return false; 120 | if (!domain.equals(cookie.domain)) return false; 121 | if (!path.equals(cookie.path)) return false; 122 | if (!name.equals(cookie.name)) return false; 123 | return value.equals(cookie.value); 124 | } 125 | 126 | @Override 127 | public int hashCode() { 128 | int result = domain.hashCode(); 129 | result = 31 * result + path.hashCode(); 130 | result = 31 * result + name.hashCode(); 131 | result = 31 * result + value.hashCode(); 132 | result = 31 * result + (int) (expiry ^ (expiry >>> 32)); 133 | result = 31 * result + (secure ? 1 : 0); 134 | result = 31 * result + (hostOnly ? 1 : 0); 135 | return result; 136 | } 137 | 138 | @Override 139 | public String toString() { 140 | return "Cookie{" + 141 | "domain='" + domain + '\'' + 142 | ", path='" + path + '\'' + 143 | ", name='" + name + '\'' + 144 | ", value='" + value + '\'' + 145 | ", expiry=" + expiry + 146 | ", secure=" + secure + 147 | ", hostOnly=" + hostOnly + 148 | '}'; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/DefaultSettings.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests; 2 | 3 | /** 4 | * Default setting value for request client 5 | */ 6 | public class DefaultSettings { 7 | /** 8 | * Default user agent 9 | */ 10 | public static final String USER_AGENT = "Requests 5.0.3, Java " + System.getProperty("java.version"); 11 | 12 | /** 13 | * Default connect timeout for http connection 14 | */ 15 | public static final int CONNECT_TIMEOUT = 3000; 16 | /** 17 | * Default socks timeout for http connection 18 | */ 19 | public static final int SOCKS_TIMEOUT = 5000; 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/Header.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests; 2 | 3 | import static java.util.Objects.requireNonNull; 4 | 5 | /** 6 | * Http header 7 | */ 8 | public class Header extends Parameter { 9 | private static final long serialVersionUID = 4314480501179865952L; 10 | 11 | /** 12 | * Constructor. 13 | * 14 | * use {@link #of(String, String)} instead 15 | */ 16 | public Header(String key, String value) { 17 | super(key, value); 18 | } 19 | 20 | /** 21 | * Create new header 22 | * 23 | * use {@link #of(String, Object)} instead. 24 | */ 25 | public Header(String name, Object value) { 26 | this(name, requireNonNull(value).toString()); 27 | } 28 | 29 | /** 30 | * Create new header. 31 | * @param name header name 32 | * @param value header value 33 | * @return header 34 | */ 35 | public static Header of(String name, String value) { 36 | return new Header(requireNonNull(name), requireNonNull(value)); 37 | } 38 | 39 | /** 40 | * Create new header. 41 | * @param name header name 42 | * @param value header value 43 | * @return header 44 | */ 45 | public static Header of(String name, Object value) { 46 | return new Header(requireNonNull(name), String.valueOf(requireNonNull(value))); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/Headers.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests; 2 | 3 | 4 | import net.dongliu.commons.Lazy; 5 | import net.dongliu.commons.collection.Lists; 6 | import org.checkerframework.checker.nullness.qual.Nullable; 7 | 8 | import java.io.Serializable; 9 | import java.nio.charset.Charset; 10 | import java.nio.charset.IllegalCharsetNameException; 11 | import java.nio.charset.UnsupportedCharsetException; 12 | import java.util.*; 13 | 14 | import static java.util.Objects.requireNonNull; 15 | 16 | /** 17 | * Wrap to deal with response headers. 18 | * This class is thread-safe. 19 | * 20 | * @author Liu Dong 21 | */ 22 | public class Headers implements Serializable { 23 | private static final long serialVersionUID = -1283402589869346874L; 24 | private final List
headers; 25 | private transient final Lazy>> lazyMap; 26 | 27 | public Headers(List
headers) { 28 | this.headers = Lists.copy(requireNonNull(headers)); 29 | this.lazyMap = Lazy.of(() -> toMap(headers)); 30 | } 31 | 32 | private static Map> toMap(List
headers) { 33 | Map> map = new HashMap<>(); 34 | for (Map.Entry header : headers) { 35 | String key = header.getKey().toLowerCase(); 36 | String value = header.getValue(); 37 | List list = map.get(key); 38 | if (list == null) { 39 | list = new ArrayList<>(4); 40 | list.add(value); 41 | map.put(key, list); 42 | } else { 43 | list.add(value); 44 | } 45 | } 46 | return map; 47 | } 48 | 49 | /** 50 | * Get headers by name. If not exists, return empty list 51 | */ 52 | public List getHeaders(String name) { 53 | requireNonNull(name); 54 | List values = lazyMap.get().get(name.toLowerCase()); 55 | if (values == null) { 56 | return Lists.of(); 57 | } 58 | return Collections.unmodifiableList(values); 59 | } 60 | 61 | /** 62 | * Get the first header value matched name. If not exists, return null. 63 | * 64 | * @deprecated using {@link #getHeader(String)} instead 65 | */ 66 | @Deprecated 67 | @Nullable 68 | public String getFirstHeader(String name) { 69 | requireNonNull(name); 70 | return getHeader(name); 71 | } 72 | 73 | /** 74 | * Get the first header value matched name. If not exists, return null. 75 | */ 76 | @Nullable 77 | public String getHeader(String name) { 78 | requireNonNull(name); 79 | List values = lazyMap.get().get(name.toLowerCase()); 80 | if (values == null) { 81 | return null; 82 | } 83 | return values.get(0); 84 | } 85 | 86 | /** 87 | * Get header value as long. If not exists, return defaultValue 88 | */ 89 | public long getLongHeader(String name, long defaultValue) { 90 | String firstHeader = getHeader(name); 91 | if (firstHeader == null) { 92 | return defaultValue; 93 | } 94 | try { 95 | return Long.parseLong(firstHeader.trim()); 96 | } catch (NumberFormatException e) { 97 | return defaultValue; 98 | } 99 | } 100 | 101 | public List
getHeaders() { 102 | return headers; 103 | } 104 | 105 | /** 106 | * Get charset set in content type header. 107 | * 108 | * @return the charset, or defaultCharset if no charset is set. 109 | */ 110 | public Charset getCharset(Charset defaultCharset) { 111 | String contentType = getHeader(HttpHeaders.NAME_CONTENT_TYPE); 112 | if (contentType == null) { 113 | return defaultCharset; 114 | } 115 | String[] items = contentType.split(";"); 116 | for (String item : items) { 117 | item = item.trim(); 118 | if (item.isEmpty()) { 119 | continue; 120 | } 121 | int idx = item.indexOf('='); 122 | if (idx < 0) { 123 | continue; 124 | } 125 | String key = item.substring(0, idx).trim(); 126 | if (key.equalsIgnoreCase("charset")) { 127 | try { 128 | return Charset.forName(item.substring(idx + 1).trim()); 129 | } catch (IllegalCharsetNameException | UnsupportedCharsetException e) { 130 | return defaultCharset; 131 | } 132 | } 133 | } 134 | return defaultCharset; 135 | } 136 | 137 | /** 138 | * Get charset set in content type header. 139 | * 140 | * @return null if no charset is set. 141 | * 142 | * @deprecated using {@link #getCharset(Charset)} instead 143 | */ 144 | @Nullable 145 | @Deprecated 146 | public Charset getCharset() { 147 | return getCharset(null); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/HttpConnInputStream.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests; 2 | 3 | import java.io.FilterInputStream; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.net.HttpURLConnection; 7 | 8 | /** 9 | * A InputStream delegate all method to http response input stream, and release http connection(to connection pool or close) When this input stream closed. 10 | */ 11 | class HttpConnInputStream extends FilterInputStream { 12 | 13 | private final HttpURLConnection conn; 14 | 15 | protected HttpConnInputStream(InputStream in, HttpURLConnection conn) { 16 | super(in); 17 | this.conn = conn; 18 | } 19 | 20 | @Override 21 | public void close() throws IOException { 22 | try { 23 | super.close(); 24 | } finally { 25 | conn.disconnect(); 26 | } 27 | 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/HttpHeaders.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests; 2 | 3 | /** 4 | * Utils for header 5 | * 6 | * @author Liu Dong 7 | */ 8 | public class HttpHeaders { 9 | public static final String NAME_AUTHORIZATION = "Authorization"; 10 | public static final String NAME_PROXY_AUTHORIZATION = "Proxy-Authorization"; 11 | public static final String NAME_USER_AGENT = "User-Agent"; 12 | public static final String NAME_ACCEPT_ENCODING = "Accept-Encoding"; 13 | public static final String NAME_CONTENT_TYPE = "Content-Type"; 14 | public static final String NAME_COOKIE = "Cookie"; 15 | public static final String NAME_SET_COOKIE = "Set-Cookie"; 16 | public static final String NAME_CONTENT_ENCODING = "Content-Encoding"; 17 | public static final String NAME_LOCATION = "Location"; 18 | public static final String NAME_REFERER = "Referer"; 19 | 20 | public static final String CONTENT_TYPE_FORM_ENCODED = "application/x-www-form-urlencoded"; 21 | public static final String CONTENT_TYPE_JSON = "application/json"; 22 | public static final String CONTENT_TYPE_TEXT = "plain/text"; 23 | public static final String CONTENT_TYPE_BINARY = "application/octet-stream"; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/Interceptor.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests; 2 | 3 | 4 | import org.checkerframework.checker.nullness.qual.NonNull; 5 | 6 | /** 7 | * Http request Interceptor 8 | * 9 | * @author Liu Dong 10 | */ 11 | public interface Interceptor { 12 | 13 | /** 14 | * Intercept http request process. 15 | * 16 | * @param target used to proceed request with remains interceptors and url executor 17 | * @param request the http request 18 | * @return http response from target invoke, or replaced / wrapped by implementation 19 | */ 20 | @NonNull 21 | RawResponse intercept(InvocationTarget target, Request request); 22 | 23 | 24 | interface InvocationTarget { 25 | /** 26 | * Process the request, and return response 27 | */ 28 | @NonNull 29 | RawResponse proceed(Request request); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/InterceptorChain.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests; 2 | 3 | import me.gv7.woodpecker.requests.executor.HttpExecutor; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * @author Liu Dong 9 | */ 10 | class InterceptorChain implements Interceptor.InvocationTarget { 11 | private final List interceptorList; 12 | private final HttpExecutor httpExecutor; 13 | 14 | public InterceptorChain(List interceptorList, HttpExecutor httpExecutor) { 15 | this.interceptorList = interceptorList; 16 | this.httpExecutor = httpExecutor; 17 | } 18 | 19 | @Override 20 | public RawResponse proceed(Request request) { 21 | if (interceptorList.isEmpty()) { 22 | return httpExecutor.proceed(request); 23 | } 24 | Interceptor interceptor = interceptorList.get(0); 25 | InterceptorChain chain = new InterceptorChain(interceptorList.subList(1, interceptorList.size()), httpExecutor); 26 | return interceptor.intercept(chain, request); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/KeyStores.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests; 2 | 3 | import me.gv7.woodpecker.requests.exception.TrustManagerLoadFailedException; 4 | import net.dongliu.commons.io.Closeables; 5 | 6 | import java.io.FileInputStream; 7 | import java.io.FileNotFoundException; 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.security.KeyStore; 11 | import java.security.KeyStoreException; 12 | import java.security.NoSuchAlgorithmException; 13 | import java.security.cert.CertificateException; 14 | 15 | public class KeyStores { 16 | 17 | /** 18 | * Load keystore from file. 19 | */ 20 | public static KeyStore load(String path, char[] password) { 21 | try { 22 | return load(new FileInputStream(path), password); 23 | } catch (FileNotFoundException e) { 24 | throw new TrustManagerLoadFailedException(e); 25 | } 26 | } 27 | 28 | /** 29 | * Load keystore from InputStream, close the stream after load succeed or failed. 30 | */ 31 | public static KeyStore load(InputStream in, char[] password) { 32 | 33 | try { 34 | KeyStore myTrustStore = KeyStore.getInstance(KeyStore.getDefaultType()); 35 | myTrustStore.load(in, password); 36 | return myTrustStore; 37 | } catch (CertificateException | NoSuchAlgorithmException | KeyStoreException | IOException e) { 38 | throw new TrustManagerLoadFailedException(e); 39 | } finally { 40 | Closeables.closeQuietly(in); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/Methods.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests; 2 | 3 | public class Methods { 4 | 5 | public static final String GET = "GET"; 6 | public static final String POST = "POST"; 7 | public static final String PUT = "PUT"; 8 | public static final String HEAD = "HEAD"; 9 | public static final String DELETE = "DELETE"; 10 | public static final String PATCH = "PATCH"; 11 | 12 | public static final String TRACE = "TRACE"; 13 | public static final String OPTIONS = "OPTIONS"; 14 | public static final String CONNECT = "CONNECT"; 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/Parameter.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests; 2 | 3 | import java.io.Serializable; 4 | import java.util.Map; 5 | import java.util.Objects; 6 | 7 | /** 8 | * Immutable parameter entry, the key and value cannot be null 9 | * 10 | * @author Liu Dong 11 | */ 12 | public class Parameter implements Map.Entry, Serializable { 13 | private static final long serialVersionUID = -6525353427059094141L; 14 | 15 | protected final String name; 16 | protected final T value; 17 | 18 | public Parameter(String key, T value) { 19 | this.name = Objects.requireNonNull(key); 20 | this.value = Objects.requireNonNull(value); 21 | } 22 | 23 | public static Parameter of(String key, V value) { 24 | return new Parameter<>(key, value); 25 | } 26 | 27 | /** 28 | * @deprecated using {@link #name()} 29 | */ 30 | @Deprecated 31 | public String getKey() { 32 | return name; 33 | } 34 | 35 | /** 36 | * the name of param 37 | * 38 | * @deprecated using {@link #name()} 39 | */ 40 | @Deprecated 41 | public String getName() { 42 | return name; 43 | } 44 | 45 | public String name() { 46 | return name; 47 | } 48 | 49 | /** 50 | * @deprecated using {@link #value()} 51 | */ 52 | @Deprecated 53 | public T getValue() { 54 | return value; 55 | } 56 | 57 | public T value() { 58 | return value; 59 | } 60 | 61 | /** 62 | * @deprecated not implemented 63 | */ 64 | @Deprecated 65 | @Override 66 | public T setValue(T value) { 67 | throw new UnsupportedOperationException("Pair is read only"); 68 | } 69 | 70 | @Override 71 | public boolean equals(Object o) { 72 | if (this == o) return true; 73 | if (o == null || getClass() != o.getClass()) return false; 74 | 75 | Parameter parameter = (Parameter) o; 76 | 77 | if (!name.equals(parameter.name)) return false; 78 | return value.equals(parameter.value); 79 | } 80 | 81 | @Override 82 | public int hashCode() { 83 | int result = name.hashCode(); 84 | result = 31 * result + value.hashCode(); 85 | return result; 86 | } 87 | 88 | @Override 89 | public String toString() { 90 | return "(" + name + " = " + value + ")"; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/Proxies.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests; 2 | 3 | import java.net.InetSocketAddress; 4 | import java.net.Proxy; 5 | import java.util.Objects; 6 | 7 | /** 8 | * Utils class for create Proxy 9 | * 10 | * @author Liu Dong 11 | */ 12 | public class Proxies { 13 | /** 14 | * Create http proxy 15 | */ 16 | public static Proxy httpProxy(String host, int port) { 17 | return new Proxy(Proxy.Type.HTTP, new InetSocketAddress(Objects.requireNonNull(host), port)); 18 | } 19 | 20 | /** 21 | * Create http proxy, with authentication 22 | */ 23 | // public static Proxy httpProxy(String host, int port, String user, String password) { 24 | // Objects.requireNonNull(user); 25 | // Objects.requireNonNull(password); 26 | // return new AuthenticationHttpProxy(new InetSocketAddress(Objects.requireNonNull(host), port), 27 | // new BasicAuth(user, password)); 28 | // } 29 | 30 | /** 31 | * Create socks5 proxy 32 | */ 33 | public static Proxy socksProxy(String host, int port) { 34 | return new Proxy(Proxy.Type.SOCKS, new InetSocketAddress(Objects.requireNonNull(host), port)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/RawResponse.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests; 2 | 3 | import me.gv7.woodpecker.requests.exception.RequestsException; 4 | import net.dongliu.commons.Lazy; 5 | import net.dongliu.commons.io.Closeables; 6 | import net.dongliu.commons.io.InputStreams; 7 | import net.dongliu.commons.io.Readers; 8 | import me.gv7.woodpecker.requests.json.JsonLookup; 9 | import me.gv7.woodpecker.requests.json.TypeInfer; 10 | import org.checkerframework.checker.nullness.qual.Nullable; 11 | 12 | import java.io.*; 13 | import java.lang.reflect.Type; 14 | import java.net.HttpURLConnection; 15 | import java.nio.charset.Charset; 16 | import java.nio.file.Files; 17 | import java.nio.file.Path; 18 | import java.util.List; 19 | import java.util.zip.GZIPInputStream; 20 | import java.util.zip.Inflater; 21 | import java.util.zip.InflaterInputStream; 22 | 23 | import static java.nio.charset.StandardCharsets.UTF_8; 24 | import static java.util.Objects.requireNonNull; 25 | 26 | /** 27 | * Raw http response. 28 | * It you do not consume http response body, with readToText, readToBytes, writeToFile, toTextResponse, 29 | * toJsonResponse, etc.., you need to close this raw response manually 30 | * 31 | * @author Liu Dong 32 | */ 33 | public class RawResponse extends AbstractResponse implements AutoCloseable { 34 | private final String method; 35 | private final String statusLine; 36 | private final InputStream body; 37 | @Nullable 38 | private final Charset charset; 39 | private final boolean decompress; 40 | 41 | public RawResponse(String method, String url, int statusCode, String statusLine, List cookies, 42 | Headers headers, InputStream body, Charset charset, boolean decompress) { 43 | super(url, statusCode, cookies, headers); 44 | this.method = method; 45 | this.statusLine = statusLine; 46 | this.body = body; 47 | this.charset = charset; 48 | this.decompress = decompress; 49 | } 50 | 51 | // Only for internal use. Do not call this method. 52 | public RawResponse(String method, String url, int statusCode, String statusLine, List cookies, Headers headers, 53 | InputStream input, HttpURLConnection conn) { 54 | super(url, statusCode, cookies, headers); 55 | this.method = method; 56 | this.statusLine = statusLine; 57 | this.body = new HttpConnInputStream(input, conn); 58 | this.charset = null; 59 | this.decompress = true; 60 | } 61 | 62 | 63 | @Override 64 | public void close() { 65 | try { 66 | body.close(); 67 | } catch (IOException e) { 68 | throw new UncheckedIOException(e); 69 | } 70 | } 71 | 72 | /** 73 | * Return a new RawResponse instance with response body charset set. 74 | * If charset is not set(which is default), will try to get charset from response headers; If failed, use UTF-8. 75 | * 76 | * @deprecated use {{@link #charset(Charset)}} instead 77 | */ 78 | @Deprecated 79 | public RawResponse withCharset(Charset charset) { 80 | return charset(charset); 81 | } 82 | 83 | /** 84 | * Set response read charset. 85 | * If not set, would get charset from response headers. If not found, would use UTF-8. 86 | */ 87 | public RawResponse charset(Charset charset) { 88 | return new RawResponse(method, url, statusCode, statusLine, cookies, headers, body, charset, decompress); 89 | } 90 | 91 | /** 92 | * Set response read charset. 93 | * If not set, would get charset from response headers. If not found, would use UTF-8. 94 | */ 95 | public RawResponse charset(String charset) { 96 | return charset(Charset.forName(requireNonNull(charset))); 97 | } 98 | 99 | /** 100 | * If decompress http response body. Default is true. 101 | */ 102 | public RawResponse decompress(boolean decompress) { 103 | return new RawResponse(method, url, statusCode, statusLine, cookies, headers, body, charset, decompress); 104 | } 105 | 106 | /** 107 | * Read response body to string. return empty string if response has no body 108 | */ 109 | public String readToText() { 110 | Charset charset = getCharset(); 111 | try (InputStream in = body(); 112 | Reader reader = new InputStreamReader(in, charset)) { 113 | return Readers.readAll(reader); 114 | } catch (IOException e) { 115 | throw new RequestsException(e); 116 | } finally { 117 | close(); 118 | } 119 | } 120 | 121 | /** 122 | * Convert to response, with body as text. The origin raw response will be closed 123 | */ 124 | public Response toTextResponse() { 125 | return new Response<>(this.url, this.statusCode, this.cookies, this.headers, readToText()); 126 | } 127 | 128 | /** 129 | * Read response body to byte array. return empty byte array if response has no body 130 | */ 131 | public byte[] readToBytes() { 132 | try { 133 | try (InputStream in = body()) { 134 | return InputStreams.readAll(in); 135 | } 136 | } catch (IOException e) { 137 | throw new RequestsException(e); 138 | } finally { 139 | close(); 140 | } 141 | } 142 | 143 | /** 144 | * Handle response body with handler, return a new response with content as handler result. 145 | * The response is closed whether this call succeed or failed with exception. 146 | */ 147 | public Response toResponse(ResponseHandler handler) { 148 | ResponseHandler.ResponseInfo responseInfo = new ResponseHandler.ResponseInfo(this.url, this.statusCode, this.headers, body()); 149 | try { 150 | T result = handler.handle(responseInfo); 151 | return new Response<>(this.url, this.statusCode, this.cookies, this.headers, result); 152 | } catch (IOException e) { 153 | throw new RequestsException(e); 154 | } finally { 155 | close(); 156 | } 157 | } 158 | 159 | /** 160 | * Convert to response, with body as byte array 161 | */ 162 | public Response toBytesResponse() { 163 | return new Response<>(this.url, this.statusCode, this.cookies, this.headers, readToBytes()); 164 | } 165 | 166 | /** 167 | * Deserialize response content as json 168 | * 169 | * @return null if json value is null or empty 170 | */ 171 | public T readToJson(Type type) { 172 | try { 173 | return JsonLookup.getInstance().lookup().unmarshal(body(), getCharset(), type); 174 | } catch (IOException e) { 175 | throw new RequestsException(e); 176 | } finally { 177 | close(); 178 | } 179 | } 180 | 181 | /** 182 | * Deserialize response content as json 183 | * 184 | * @return null if json value is null or empty 185 | */ 186 | public T readToJson(TypeInfer typeInfer) { 187 | return readToJson(typeInfer.getType()); 188 | } 189 | 190 | /** 191 | * Deserialize response content as json 192 | * 193 | * @return null if json value is null or empty 194 | */ 195 | public T readToJson(Class cls) { 196 | return readToJson((Type) cls); 197 | } 198 | 199 | /** 200 | * Convert http response body to json result 201 | */ 202 | public Response toJsonResponse(TypeInfer typeInfer) { 203 | return new Response<>(this.url, this.statusCode, this.cookies, this.headers, readToJson(typeInfer)); 204 | } 205 | 206 | /** 207 | * Convert http response body to json result 208 | */ 209 | public Response toJsonResponse(Class cls) { 210 | return new Response<>(this.url, this.statusCode, this.cookies, this.headers, readToJson(cls)); 211 | } 212 | 213 | /** 214 | * Write response body to file 215 | */ 216 | public void writeToFile(File file) { 217 | try { 218 | try (OutputStream os = new FileOutputStream(file)) { 219 | InputStreams.transferTo(body(), os); 220 | } 221 | } catch (IOException e) { 222 | throw new RequestsException(e); 223 | } finally { 224 | close(); 225 | } 226 | } 227 | 228 | /** 229 | * Write response body to file 230 | */ 231 | public void writeToFile(Path path) { 232 | try { 233 | try (OutputStream os = Files.newOutputStream(path)) { 234 | InputStreams.transferTo(body(), os); 235 | } 236 | } catch (IOException e) { 237 | throw new RequestsException(e); 238 | } finally { 239 | close(); 240 | } 241 | } 242 | 243 | 244 | /** 245 | * Write response body to file 246 | */ 247 | public void writeToFile(String path) { 248 | try { 249 | try (OutputStream os = new FileOutputStream(path)) { 250 | InputStreams.transferTo(body(), os); 251 | } 252 | } catch (IOException e) { 253 | throw new RequestsException(e); 254 | } finally { 255 | close(); 256 | } 257 | } 258 | 259 | /** 260 | * Write response body to file, and return response contains the file. 261 | */ 262 | public Response toFileResponse(Path path) { 263 | File file = path.toFile(); 264 | this.writeToFile(file); 265 | return new Response<>(this.url, this.statusCode, this.cookies, this.headers, file); 266 | } 267 | 268 | /** 269 | * Write response body to OutputStream. OutputStream will not be closed. 270 | */ 271 | public void writeTo(OutputStream out) { 272 | try { 273 | InputStreams.transferTo(body(), out); 274 | } catch (IOException e) { 275 | throw new RequestsException(e); 276 | } finally { 277 | close(); 278 | } 279 | } 280 | 281 | /** 282 | * Write response body to Writer, charset can be set using {@link #charset(Charset)}, 283 | * or will use charset detected from response header if not set. 284 | * Writer will not be closed. 285 | */ 286 | public void writeTo(Writer writer) { 287 | try { 288 | try (InputStream in = body(); 289 | Reader reader = new InputStreamReader(in, getCharset())) { 290 | Readers.transferTo(reader, writer); 291 | } 292 | } catch (IOException e) { 293 | throw new RequestsException(e); 294 | } finally { 295 | close(); 296 | } 297 | } 298 | 299 | /** 300 | * Consume and discard this response body. 301 | */ 302 | public void discardBody() { 303 | try (InputStream in = body) { 304 | InputStreams.discardAll(in); 305 | } catch (IOException e) { 306 | throw new RequestsException(e); 307 | } finally { 308 | close(); 309 | } 310 | } 311 | 312 | /** 313 | * Get the status line 314 | * 315 | * @deprecated use {@link #statusLine()} 316 | */ 317 | @Deprecated 318 | public String getStatusLine() { 319 | return statusLine; 320 | } 321 | 322 | /** 323 | * Get the status line 324 | */ 325 | public String statusLine() { 326 | return statusLine; 327 | } 328 | 329 | private Lazy decompressedBody = Lazy.of(this::decompressBody); 330 | 331 | /** 332 | * The response body input stream 333 | * 334 | * @deprecated use {@link #body()} 335 | */ 336 | @Deprecated 337 | public InputStream getInput() { 338 | return decompressedBody.get(); 339 | } 340 | 341 | public String method() { 342 | return method; 343 | } 344 | 345 | /** 346 | * The response body input stream 347 | */ 348 | public InputStream body() { 349 | return decompressedBody.get(); 350 | } 351 | 352 | @Nullable 353 | public Charset charset() { 354 | return charset; 355 | } 356 | 357 | public boolean decompress() { 358 | return decompress; 359 | } 360 | 361 | private Charset getCharset() { 362 | if (this.charset != null) { 363 | return this.charset; 364 | } 365 | return headers.getCharset(UTF_8); 366 | } 367 | 368 | 369 | /** 370 | * Wrap response input stream if it is compressed, return input its self if not use compress 371 | */ 372 | private InputStream decompressBody() { 373 | if (!decompress) { 374 | return body; 375 | } 376 | // if has no body, some server still set content-encoding header, 377 | // GZIPInputStream wrap empty input stream will cause exception. we should check this 378 | if (method.equals(Methods.HEAD) 379 | || (statusCode >= 100 && statusCode < 200) || statusCode == StatusCodes.NOT_MODIFIED || statusCode == StatusCodes.NO_CONTENT) { 380 | return body; 381 | } 382 | 383 | String contentEncoding = headers.getHeader(HttpHeaders.NAME_CONTENT_ENCODING); 384 | if (contentEncoding == null) { 385 | return body; 386 | } 387 | 388 | //we should remove the content-encoding header here? 389 | switch (contentEncoding) { 390 | case "gzip": 391 | try { 392 | return new GZIPInputStream(body); 393 | } catch (IOException e) { 394 | Closeables.closeQuietly(body); 395 | throw new RequestsException(e); 396 | } 397 | case "deflate": 398 | // Note: deflate implements may or may not wrap in zlib due to rfc confusing. 399 | // here deal with deflate without zlib header 400 | return new InflaterInputStream(body, new Inflater(true)); 401 | case "identity": 402 | case "compress": //historic; deprecated in most applications and replaced by gzip or deflate 403 | default: 404 | return body; 405 | } 406 | } 407 | } 408 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/Request.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests; 2 | 3 | import me.gv7.woodpecker.requests.executor.SessionContext; 4 | import me.gv7.woodpecker.requests.body.RequestBody; 5 | import org.checkerframework.checker.nullness.qual.Nullable; 6 | 7 | import java.io.Serializable; 8 | import java.net.Proxy; 9 | import java.net.URL; 10 | import java.nio.charset.Charset; 11 | import java.security.KeyStore; 12 | import java.util.Collection; 13 | import java.util.Map; 14 | import java.util.Map.Entry; 15 | 16 | /** 17 | * Http request 18 | * 19 | * @author Liu Dong 20 | */ 21 | public class Request implements Serializable { 22 | private static final long serialVersionUID = -2585065451136206831L; 23 | private final String method; 24 | private final Collection> headers; 25 | private final Collection> cookies; 26 | private final Collection> params; 27 | 28 | private final String userAgent; 29 | private final Charset charset; 30 | @Nullable 31 | private final RequestBody body; 32 | private final int socksTimeout; 33 | private final int connectTimeout; 34 | @Nullable 35 | private final Proxy proxy; 36 | private final boolean followRedirect; 37 | private final int maxRedirectCount; 38 | private final boolean acceptCompress; 39 | private final boolean verify; 40 | @Nullable 41 | private final KeyStore keyStore; 42 | @Nullable 43 | private final BasicAuth basicAuth; 44 | @Nullable 45 | private final SessionContext sessionContext; 46 | private final URL url; 47 | private final boolean keepAlive; 48 | 49 | Request(RequestBuilder builder) { 50 | method = builder.method; 51 | headers = builder.headers; 52 | cookies = builder.cookies; 53 | userAgent = builder.userAgent; 54 | charset = builder.charset; 55 | body = builder.body; 56 | socksTimeout = builder.socksTimeout; 57 | connectTimeout = builder.connectTimeout; 58 | proxy = builder.proxy; 59 | followRedirect = builder.followRedirect; 60 | maxRedirectCount = builder.maxRedirectCount; 61 | acceptCompress = builder.acceptCompress; 62 | verify = builder.verify; 63 | keyStore = builder.keyStore; 64 | basicAuth = builder.basicAuth; 65 | sessionContext = builder.sessionContext; 66 | keepAlive = builder.keepAlive; 67 | this.url = builder.url; 68 | this.params = builder.params; 69 | } 70 | 71 | /** 72 | * Create and copy fields to mutable builder instance. 73 | */ 74 | public RequestBuilder toBuilder() { 75 | return new RequestBuilder(this); 76 | } 77 | 78 | /** 79 | * @deprecated use {@link #method()} 80 | */ 81 | @Deprecated 82 | public String getMethod() { 83 | return method; 84 | } 85 | 86 | /** 87 | * @deprecated use {@link #headers()} 88 | */ 89 | @Deprecated 90 | public Collection> getHeaders() { 91 | return headers; 92 | } 93 | 94 | /** 95 | * @deprecated use {@link #cookies()} 96 | */ 97 | @Deprecated 98 | public Collection> getCookies() { 99 | return cookies; 100 | } 101 | 102 | /** 103 | * @deprecated use {@link #userAgent()} 104 | */ 105 | @Deprecated 106 | public String getUserAgent() { 107 | return userAgent; 108 | } 109 | 110 | /** 111 | * @deprecated use {@link #charset()} 112 | */ 113 | @Deprecated 114 | public Charset getCharset() { 115 | return charset; 116 | } 117 | 118 | /** 119 | * @deprecated use {@link #body()} 120 | */ 121 | @Deprecated 122 | @Nullable 123 | public RequestBody getBody() { 124 | return body; 125 | } 126 | 127 | /** 128 | * @deprecated use {@link #socksTimeout()} 129 | */ 130 | @Deprecated 131 | public int getSocksTimeout() { 132 | return socksTimeout; 133 | } 134 | 135 | /** 136 | * @deprecated use {@link #connectTimeout()} 137 | */ 138 | @Deprecated 139 | public int getConnectTimeout() { 140 | return connectTimeout; 141 | } 142 | 143 | /** 144 | * @deprecated use {@link #proxy()} 145 | */ 146 | @Deprecated 147 | @Nullable 148 | public Proxy getProxy() { 149 | return proxy; 150 | } 151 | 152 | /** 153 | * @deprecated use {@link #followRedirect()} 154 | */ 155 | @Deprecated 156 | public boolean isFollowRedirect() { 157 | return followRedirect; 158 | } 159 | 160 | /** 161 | * @deprecated use {@link #maxRedirectCount()} 162 | */ 163 | @Deprecated 164 | public int getMaxRedirectCount() { 165 | return maxRedirectCount; 166 | } 167 | 168 | /** 169 | * @deprecated use {@link #acceptCompress()} 170 | */ 171 | @Deprecated 172 | public boolean isCompress() { 173 | return acceptCompress; 174 | } 175 | 176 | /** 177 | * @deprecated use {@link #verify()} 178 | */ 179 | @Deprecated 180 | public boolean isVerify() { 181 | return verify; 182 | } 183 | 184 | /** 185 | * @deprecated use {@link #keyStore()} 186 | */ 187 | @Deprecated 188 | @Nullable 189 | public KeyStore getKeyStore() { 190 | return keyStore; 191 | } 192 | 193 | /** 194 | * @deprecated use {@link #basicAuth()} 195 | */ 196 | @Deprecated 197 | public BasicAuth getBasicAuth() { 198 | return basicAuth; 199 | } 200 | 201 | /** 202 | * @deprecated use {@link #sessionContext()} 203 | */ 204 | @Deprecated 205 | @Nullable 206 | public SessionContext getSessionContext() { 207 | return sessionContext; 208 | } 209 | 210 | /** 211 | * @deprecated use {@link #url()} 212 | */ 213 | @Deprecated 214 | public URL getUrl() { 215 | return url; 216 | } 217 | 218 | 219 | /** 220 | * Parameter to append to url. 221 | * @deprecated use {@link #params()} 222 | */ 223 | @Deprecated 224 | public Collection> getParams() { 225 | return params; 226 | } 227 | 228 | /** 229 | * @deprecated use {@link #keepAlive()} 230 | */ 231 | @Deprecated 232 | public boolean isKeepAlive() { 233 | return keepAlive; 234 | } 235 | 236 | 237 | public String method() { 238 | return method; 239 | } 240 | 241 | public Collection> headers() { 242 | return headers; 243 | } 244 | 245 | public Collection> cookies() { 246 | return cookies; 247 | } 248 | 249 | public Collection> params() { 250 | return params; 251 | } 252 | 253 | public String userAgent() { 254 | return userAgent; 255 | } 256 | 257 | public Charset charset() { 258 | return charset; 259 | } 260 | 261 | @Nullable 262 | public RequestBody body() { 263 | return body; 264 | } 265 | 266 | public int socksTimeout() { 267 | return socksTimeout; 268 | } 269 | 270 | public int connectTimeout() { 271 | return connectTimeout; 272 | } 273 | 274 | @Nullable 275 | public Proxy proxy() { 276 | return proxy; 277 | } 278 | 279 | public boolean followRedirect() { 280 | return followRedirect; 281 | } 282 | 283 | public int maxRedirectCount() { 284 | return maxRedirectCount; 285 | } 286 | 287 | public boolean acceptCompress() { 288 | return acceptCompress; 289 | } 290 | 291 | public boolean verify() { 292 | return verify; 293 | } 294 | 295 | public KeyStore keyStore() { 296 | return keyStore; 297 | } 298 | 299 | public BasicAuth basicAuth() { 300 | return basicAuth; 301 | } 302 | 303 | public SessionContext sessionContext() { 304 | return sessionContext; 305 | } 306 | 307 | public URL url() { 308 | return url; 309 | } 310 | 311 | public boolean keepAlive() { 312 | return keepAlive; 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/Requests.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests; 2 | 3 | 4 | import me.gv7.woodpecker.requests.executor.RequestExecutorFactory; 5 | 6 | import java.net.URL; 7 | 8 | /** 9 | * Http request utils methods. 10 | * 11 | * @author Liu Dong 12 | */ 13 | public class Requests { 14 | 15 | /** 16 | * Start a GET request 17 | */ 18 | public static RequestBuilder get(URL url) { 19 | return newRequest(Methods.GET, url); 20 | } 21 | 22 | /** 23 | * Start a POST request 24 | */ 25 | public static RequestBuilder post(URL url) { 26 | return newRequest(Methods.POST, url); 27 | } 28 | 29 | /** 30 | * Start a PUT request 31 | */ 32 | public static RequestBuilder put(URL url) { 33 | return newRequest(Methods.PUT, url); 34 | } 35 | 36 | /** 37 | * Start a DELETE request 38 | */ 39 | public static RequestBuilder delete(URL url) { 40 | return newRequest(Methods.DELETE, url); 41 | } 42 | 43 | /** 44 | * Start a HEAD request 45 | */ 46 | public static RequestBuilder head(URL url) { 47 | return newRequest(Methods.HEAD, url); 48 | } 49 | 50 | /** 51 | * Start a PATCH request 52 | */ 53 | public static RequestBuilder patch(URL url) { 54 | return newRequest(Methods.PATCH, url); 55 | } 56 | 57 | /** 58 | * Create new request with method and url 59 | */ 60 | public static RequestBuilder newRequest(String method, URL url) { 61 | return new RequestBuilder().method(method).url(url); 62 | } 63 | 64 | public static RequestBuilder get(String url) { 65 | return newRequest(Methods.GET, url); 66 | } 67 | 68 | public static RequestBuilder post(String url) { 69 | return newRequest(Methods.POST, url); 70 | } 71 | 72 | public static RequestBuilder put(String url) { 73 | return newRequest(Methods.PUT, url); 74 | } 75 | 76 | public static RequestBuilder delete(String url) { 77 | return newRequest(Methods.DELETE, url); 78 | } 79 | 80 | public static RequestBuilder head(String url) { 81 | return newRequest(Methods.HEAD, url); 82 | } 83 | 84 | public static RequestBuilder patch(String url) { 85 | return newRequest(Methods.PATCH, url); 86 | } 87 | 88 | public static RequestBuilder method(String method,String url) throws Exception { 89 | if(method.equals("GET")){ 90 | return newRequest(Methods.GET, url); 91 | }else if(method.equals("POST")){ 92 | return newRequest(Methods.POST, url); 93 | }else if(method.equals("PUT")){ 94 | return newRequest(Methods.PUT, url); 95 | }else if(method.equals("DELETE")){ 96 | return newRequest(Methods.DELETE, url); 97 | }else if(method.equals("HEAD")){ 98 | return newRequest(Methods.HEAD, url); 99 | }else if(method.equals("PATCH")){ 100 | return newRequest(Methods.PATCH, url); 101 | }else{ 102 | throw new Exception(String.format("%s is an unknown method",method)); 103 | } 104 | } 105 | 106 | /** 107 | * Create new request with method and url 108 | */ 109 | public static RequestBuilder newRequest(String method, String url) { 110 | return new RequestBuilder().method(method).url(url); 111 | } 112 | 113 | /** 114 | * Create new session. 115 | */ 116 | public static Session session() { 117 | RequestExecutorFactory factory = RequestExecutorFactory.getInstance(); 118 | return new Session(factory.newSessionContext()); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/Response.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests; 2 | 3 | 4 | import java.io.Serializable; 5 | import java.util.List; 6 | 7 | 8 | /** 9 | * Response with transformed result 10 | * 11 | * @author Liu Dong 12 | */ 13 | public class Response extends AbstractResponse implements Serializable { 14 | private static final long serialVersionUID = 5956373495731090956L; 15 | private final T body; 16 | 17 | /** 18 | * Create new response instance. 19 | * @param url the final url(redirected) 20 | * @param statusCode the status code 21 | * @param cookies the cookies 22 | * @param headers the headers 23 | * @param body the body 24 | */ 25 | Response(String url, int statusCode, List cookies, Headers headers, T body) { 26 | super(url, statusCode, cookies, headers); 27 | this.body = body; 28 | } 29 | 30 | /** 31 | * Return the body part. 32 | * @return the body 33 | * @deprecated use {@link #body()} 34 | */ 35 | @Deprecated 36 | public T getBody() { 37 | return body; 38 | } 39 | 40 | /** 41 | * Return the body part. 42 | * @return the body 43 | */ 44 | public T body() { 45 | return body; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/ResponseHandler.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | 6 | /** 7 | * Handler raw response body, convert to result T 8 | */ 9 | public interface ResponseHandler { 10 | 11 | /** 12 | * Handler raw response body, convert to result T 13 | */ 14 | T handle(ResponseInfo responseInfo) throws IOException; 15 | 16 | /** 17 | * Response info 18 | */ 19 | class ResponseInfo { 20 | private String url; 21 | private int statusCode; 22 | private Headers headers; 23 | private InputStream body; 24 | 25 | ResponseInfo(String url, int statusCode, Headers headers, InputStream body) { 26 | this.url = url; 27 | this.statusCode = statusCode; 28 | this.headers = headers; 29 | this.body = body; 30 | } 31 | 32 | /** 33 | * The url after redirect. if no redirect during request, this url is the origin url send. 34 | * @deprecated use {@link #url()} 35 | */ 36 | @Deprecated 37 | public String getUrl() { 38 | return url; 39 | } 40 | 41 | /** 42 | * The response status code 43 | * @deprecated use {@link #statusCode()} 44 | */ 45 | @Deprecated 46 | public int getStatusCode() { 47 | return statusCode; 48 | } 49 | 50 | /** 51 | * The response headers 52 | * @deprecated use {@link #headers()} 53 | */ 54 | @Deprecated 55 | public Headers getHeaders() { 56 | return headers; 57 | } 58 | 59 | /** 60 | * The response input stream 61 | * @deprecated use {@link #body()} 62 | */ 63 | @Deprecated 64 | public InputStream getIn() { 65 | return body; 66 | } 67 | 68 | /** 69 | * The url after redirect. if no redirect during request, this url is the origin url send. 70 | */ 71 | public String url() { 72 | return url; 73 | } 74 | 75 | /** 76 | * The response status code 77 | */ 78 | public int statusCode() { 79 | return statusCode; 80 | } 81 | 82 | /** 83 | * The response headers 84 | */ 85 | public Headers headers() { 86 | return headers; 87 | } 88 | 89 | /** 90 | * The response body as input stream 91 | */ 92 | public InputStream body() { 93 | return body; 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/Session.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests; 2 | 3 | import me.gv7.woodpecker.requests.executor.SessionContext; 4 | 5 | import java.net.URL; 6 | import java.util.List; 7 | 8 | /** 9 | * Http request share cookies etc. 10 | * This class is Thread-Safe. 11 | */ 12 | public class Session { 13 | 14 | private final SessionContext context; 15 | 16 | Session(SessionContext context) { 17 | this.context = context; 18 | } 19 | 20 | public RequestBuilder get(String url) { 21 | return newRequest(Methods.GET, url); 22 | } 23 | 24 | public RequestBuilder post(String url) { 25 | return newRequest(Methods.POST, url); 26 | } 27 | 28 | public RequestBuilder put(String url) { 29 | return newRequest(Methods.PUT, url); 30 | } 31 | 32 | public RequestBuilder head(String url) { 33 | return newRequest(Methods.HEAD, url); 34 | } 35 | 36 | public RequestBuilder delete(String url) { 37 | return newRequest(Methods.DELETE, url); 38 | } 39 | 40 | public RequestBuilder patch(String url) { 41 | return newRequest(Methods.PATCH, url); 42 | } 43 | 44 | public RequestBuilder newRequest(String method, String url) { 45 | return new RequestBuilder().sessionContext(context).url(url).method(method); 46 | } 47 | 48 | public RequestBuilder get(URL url) { 49 | return newRequest(Methods.GET, url); 50 | } 51 | 52 | public RequestBuilder post(URL url) { 53 | return newRequest(Methods.POST, url); 54 | } 55 | 56 | public RequestBuilder put(URL url) { 57 | return newRequest(Methods.PUT, url); 58 | } 59 | 60 | public RequestBuilder head(URL url) { 61 | return newRequest(Methods.HEAD, url); 62 | } 63 | 64 | public RequestBuilder delete(URL url) { 65 | return newRequest(Methods.DELETE, url); 66 | } 67 | 68 | public RequestBuilder patch(URL url) { 69 | return newRequest(Methods.PATCH, url); 70 | } 71 | 72 | 73 | /** 74 | * Return all cookies this session current hold. 75 | */ 76 | public List currentCookies() { 77 | return context.cookieJar().getCookies(); 78 | } 79 | 80 | /** 81 | * Create new request with method and url 82 | */ 83 | public RequestBuilder newRequest(String method, URL url) { 84 | return new RequestBuilder().sessionContext(context).url(url).method(method); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/StatusCodes.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests; 2 | 3 | /** 4 | * Status code for http response 5 | */ 6 | public class StatusCodes { 7 | public static final int CONTINUE = 100; 8 | public static final int SWITCHING_PROTOCOLS = 101; 9 | 10 | public static final int OK = 200; 11 | public static final int CREATED = 201; 12 | public static final int ACCEPTED = 202; 13 | public static final int NON_AUTHORITATIVE_INFORMATION = 203; 14 | public static final int NO_CONTENT = 204; 15 | public static final int RESET_CONTENT = 205; 16 | public static final int PARTIAL_CONTENT = 205; 17 | 18 | public static final int MULTIPLE_CHOICES = 300; 19 | public static final int MOVED_PERMANENTLY = 301; 20 | public static final int FOUND = 302; 21 | public static final int SEE_OTHER = 303; 22 | public static final int NOT_MODIFIED = 304; 23 | public static final int USE_PROXY = 305; 24 | public static final int TEMPORARY_REDIRECT = 307; 25 | public static final int PERMANENT_REDIRECT = 308; 26 | 27 | public static final int BAD_REQUEST = 400; 28 | public static final int UNAUTHORIZED = 401; 29 | public static final int PAYMENT_REQUIRED = 402; 30 | public static final int FORBIDDEN = 403; 31 | public static final int NOT_FOUND = 404; 32 | public static final int METHOD_NOT_ALLOWED = 405; 33 | public static final int NOT_ACCEPTABLE = 406; 34 | public static final int PROXY_AUTHENTICATION_REQUIRED = 407; 35 | public static final int REQUEST_TIMEOUT = 408; 36 | public static final int CONFLICT = 409; 37 | public static final int GONE = 410; 38 | public static final int LENGTH_REQUIRED = 411; 39 | public static final int PRECONDITION_FAILED = 412; 40 | public static final int REQUEST_ENTITY_TOO_LARGE = 413; 41 | public static final int REQUEST_URI_TOO_LONG = 414; 42 | public static final int UNSUPPORTED_MEDIA_TYPE = 415; 43 | public static final int REQUESTED_RANGE_NOT_SATISFIABLE = 416; 44 | public static final int EXPECTATION_FAILED = 417; 45 | 46 | 47 | public static final int INTERNAL_SERVER_ERROR = 500; 48 | public static final int NOT_IMPLEMENTED = 501; 49 | public static final int BAD_GATEWAY = 502; 50 | public static final int SERVICE_UNAVAILABLE = 503; 51 | public static final int GATEWAY_TIMEOUT = 504; 52 | public static final int HTTP_VERSION_NOT_SUPPORTED = 505; 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/body/BytesRequestBody.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.body; 2 | 3 | import me.gv7.woodpecker.requests.HttpHeaders; 4 | 5 | import java.io.IOException; 6 | import java.io.OutputStream; 7 | import java.nio.charset.Charset; 8 | 9 | /** 10 | * @author Liu Dong 11 | */ 12 | class BytesRequestBody extends RequestBody { 13 | private static final long serialVersionUID = 5476648279904264255L; 14 | 15 | BytesRequestBody(byte[] body) { 16 | super(body, HttpHeaders.CONTENT_TYPE_BINARY, false); 17 | } 18 | 19 | @Override public void writeBody(OutputStream out, Charset charset) throws IOException { 20 | out.write(body()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/body/ContentTypes.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.body; 2 | 3 | import me.gv7.woodpecker.requests.HttpHeaders; 4 | 5 | import java.io.File; 6 | import java.io.IOException; 7 | import java.nio.file.Files; 8 | 9 | class ContentTypes { 10 | static String probeContentType(File file) { 11 | String contentType; 12 | try { 13 | contentType = Files.probeContentType(file.toPath()); 14 | } catch (IOException e) { 15 | contentType = null; 16 | } 17 | if (contentType == null) { 18 | contentType = HttpHeaders.CONTENT_TYPE_BINARY; 19 | } 20 | return contentType; 21 | } 22 | 23 | /** 24 | * If content type looks like a text content. 25 | */ 26 | static boolean isText(String contentType) { 27 | return contentType.contains("text") || contentType.contains("json") 28 | || contentType.contains("xml") || contentType.contains("html"); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/body/FileRequestBody.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.body; 2 | 3 | import net.dongliu.commons.io.InputStreams; 4 | 5 | import java.io.File; 6 | import java.io.FileInputStream; 7 | import java.io.IOException; 8 | import java.io.OutputStream; 9 | import java.nio.charset.Charset; 10 | 11 | /** 12 | * Request body, which get data from file. 13 | * 14 | * @author Liu Dong 15 | */ 16 | class FileRequestBody extends RequestBody { 17 | private static final long serialVersionUID = -1902920038280221251L; 18 | 19 | FileRequestBody(File body) { 20 | super(body, ContentTypes.probeContentType(body), false); 21 | } 22 | 23 | @Override 24 | public void writeBody(OutputStream out, Charset charset) throws IOException { 25 | try (FileInputStream in = new FileInputStream(body())) { 26 | InputStreams.transferTo(in, out); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/body/FormRequestBody.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.body; 2 | 3 | import me.gv7.woodpecker.requests.HttpHeaders; 4 | import me.gv7.woodpecker.requests.utils.URLUtils; 5 | 6 | import java.io.IOException; 7 | import java.io.OutputStream; 8 | import java.io.OutputStreamWriter; 9 | import java.io.Writer; 10 | import java.nio.charset.Charset; 11 | import java.util.Collection; 12 | import java.util.Map; 13 | 14 | /** 15 | * @author Liu Dong 16 | */ 17 | class FormRequestBody extends RequestBody>> { 18 | private static final long serialVersionUID = 6322052512305107136L; 19 | 20 | FormRequestBody(Collection> body) { 21 | super(body, HttpHeaders.CONTENT_TYPE_FORM_ENCODED, true); 22 | } 23 | 24 | @Override 25 | public void writeBody(OutputStream out, Charset charset) throws IOException { 26 | String content = URLUtils.encodeForms(URLUtils.toStringParameters(body()), charset); 27 | try (Writer writer = new OutputStreamWriter(out, charset)) { 28 | writer.write(content); 29 | } 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/body/InputStreamRequestBody.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.body; 2 | 3 | import me.gv7.woodpecker.requests.HttpHeaders; 4 | import net.dongliu.commons.io.InputStreams; 5 | 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.io.OutputStream; 9 | import java.nio.charset.Charset; 10 | 11 | /** 12 | * @author Liu Dong 13 | * @deprecated use {@link InputStreamSupplierRequestBody} instead 14 | */ 15 | @Deprecated 16 | class InputStreamRequestBody extends RequestBody { 17 | private static final long serialVersionUID = -2463504960044237751L; 18 | 19 | InputStreamRequestBody(InputStream body) { 20 | super(body, HttpHeaders.CONTENT_TYPE_BINARY, false); 21 | } 22 | 23 | @Override 24 | public void writeBody(OutputStream out, Charset charset) throws IOException { 25 | try (InputStream in = body()) { 26 | InputStreams.transferTo(in, out); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/body/InputStreamSupplier.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.body; 2 | 3 | import java.io.InputStream; 4 | import java.util.function.Supplier; 5 | 6 | /** 7 | * Which can provider a input stream. 8 | */ 9 | public interface InputStreamSupplier extends Supplier { 10 | /** 11 | * Return a InputStream. 12 | * Every call to this method, should return a new InputStream, all InputStreams returned should contains the same data. 13 | * 14 | * @return A InputStream 15 | */ 16 | InputStream get(); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/body/InputStreamSupplierRequestBody.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.body; 2 | 3 | import me.gv7.woodpecker.requests.HttpHeaders; 4 | import net.dongliu.commons.io.InputStreams; 5 | 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.io.OutputStream; 9 | import java.nio.charset.Charset; 10 | 11 | /** 12 | * @author Liu Dong 13 | */ 14 | class InputStreamSupplierRequestBody extends RequestBody { 15 | private static final long serialVersionUID = -2463504912342237751L; 16 | 17 | InputStreamSupplierRequestBody(InputStreamSupplier body) { 18 | super(body, HttpHeaders.CONTENT_TYPE_BINARY, false); 19 | } 20 | 21 | @Override 22 | public void writeBody(OutputStream out, Charset charset) throws IOException { 23 | try (InputStream in = body().get()) { 24 | InputStreams.transferTo(in, out); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/body/JsonRequestBody.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.body; 2 | 3 | import me.gv7.woodpecker.requests.HttpHeaders; 4 | import me.gv7.woodpecker.requests.json.JsonLookup; 5 | 6 | import java.io.IOException; 7 | import java.io.OutputStream; 8 | import java.io.OutputStreamWriter; 9 | import java.io.Writer; 10 | import java.nio.charset.Charset; 11 | 12 | /** 13 | * @author Liu Dong 14 | */ 15 | class JsonRequestBody extends RequestBody { 16 | 17 | private static final long serialVersionUID = 890531624817102489L; 18 | 19 | JsonRequestBody(T body) { 20 | super(body, HttpHeaders.CONTENT_TYPE_JSON, true); 21 | } 22 | 23 | @Override 24 | public void writeBody(OutputStream out, Charset charset) throws IOException { 25 | try (Writer writer = new OutputStreamWriter(out, charset)) { 26 | JsonLookup.getInstance().lookup().marshal(writer, body()); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/body/MultiPartRequestBody.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.body; 2 | 3 | import java.io.IOException; 4 | import java.io.OutputStream; 5 | import java.io.OutputStreamWriter; 6 | import java.io.Writer; 7 | import java.nio.charset.Charset; 8 | import java.util.Collection; 9 | 10 | /** 11 | * MultiPart request body 12 | * 13 | * @author Liu Dong 14 | */ 15 | class MultiPartRequestBody extends RequestBody>> { 16 | private static final String BOUNDARY = "********************" + System.currentTimeMillis(); 17 | private static final String LINE_END = "\r\n"; 18 | private static final long serialVersionUID = -2150328570818986957L; 19 | 20 | public MultiPartRequestBody(Collection> body) { 21 | super(body, "multipart/form-data; boundary=" + BOUNDARY, false); 22 | } 23 | 24 | @Override 25 | public void writeBody(OutputStream out, Charset charset) throws IOException { 26 | Writer writer = new OutputStreamWriter(out); 27 | for (Part part : body()) { 28 | String contentType = part.contentType(); 29 | String name = part.name(); 30 | String fileName = part.fileName(); 31 | 32 | writeBoundary(writer); 33 | 34 | writer.write("Content-Disposition: form-data; name=\"" + name + "\""); 35 | if (fileName != null && !fileName.isEmpty()) { 36 | writer.write("; filename=\"" + fileName + '"'); 37 | } 38 | writer.write(LINE_END); 39 | 40 | if (contentType != null && !contentType.isEmpty()) { 41 | writer.write("Content-Type: " + contentType); 42 | Charset partCharset = part.charset(); 43 | if (partCharset != null) { 44 | writer.write("; charset=" + partCharset.name().toLowerCase()); 45 | } 46 | writer.write(LINE_END); 47 | } 48 | writer.write(LINE_END); 49 | writer.flush(); 50 | 51 | part.writeTo(out); 52 | out.flush(); 53 | writer.write(LINE_END); 54 | writer.flush(); 55 | out.flush(); 56 | } 57 | writer.write("--"); 58 | writer.write(BOUNDARY); 59 | writer.write("--"); 60 | writer.write(LINE_END); 61 | writer.flush(); 62 | } 63 | 64 | private void writeBoundary(Writer writer) throws IOException { 65 | writer.write("--"); 66 | writer.write(BOUNDARY); 67 | writer.write(LINE_END); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/body/Part.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.body; 2 | 3 | import me.gv7.woodpecker.requests.HttpHeaders; 4 | import net.dongliu.commons.Objects2; 5 | import net.dongliu.commons.io.InputStreams; 6 | import org.checkerframework.checker.nullness.qual.Nullable; 7 | 8 | import java.io.*; 9 | import java.nio.charset.Charset; 10 | 11 | import static java.nio.charset.StandardCharsets.ISO_8859_1; 12 | import static java.util.Objects.requireNonNull; 13 | 14 | 15 | /** 16 | * This class represent one part(field) of http multipart request body. 17 | * 18 | * @author Liu Dong 19 | */ 20 | public class Part implements Serializable { 21 | private static final long serialVersionUID = -8628605676399143491L; 22 | 23 | /** 24 | * Filed name 25 | */ 26 | private final String name; 27 | /** 28 | * The file name, for http form's file input. 29 | * If is text input, this field is null. 30 | */ 31 | @Nullable 32 | private final String fileName; 33 | /** 34 | * The part content 35 | */ 36 | private final T body; 37 | 38 | /** 39 | * The content type of this Part. 40 | */ 41 | @Nullable 42 | private final String contentType; 43 | 44 | /** 45 | * The charset of this part content. 46 | */ 47 | @Nullable 48 | private final Charset charset; 49 | 50 | private final PartWriter partWriter; 51 | 52 | private Part(String name, @Nullable String fileName, T body, @Nullable String contentType, 53 | @Nullable Charset charset, PartWriter partWriter) { 54 | this.name = requireNonNull(name); 55 | this.fileName = fileName; 56 | this.body = requireNonNull(body); 57 | this.contentType = contentType; 58 | this.charset = charset; 59 | this.partWriter = partWriter; 60 | } 61 | 62 | /** 63 | * Set content type for this part. 64 | */ 65 | public Part contentType(String contentType) { 66 | requireNonNull(contentType); 67 | return new Part<>(name, fileName, body, contentType, charset, partWriter); 68 | } 69 | 70 | /** 71 | * The charset of this part's content. Each part of MultiPart body can has it's own charset set. 72 | * Default not set. 73 | * 74 | * @param charset the charset 75 | * @return self 76 | */ 77 | public Part charset(Charset charset) { 78 | requireNonNull(charset); 79 | return new Part<>(name, fileName, body, contentType, charset, partWriter); 80 | } 81 | 82 | /** 83 | * Create a file multi-part field, from file. 84 | * This return a part equivalent to <input type="file" /> field in multi part form. 85 | */ 86 | public static Part file(String name, File file) { 87 | return file(name, file.getName(), file); 88 | } 89 | 90 | /** 91 | * Create a file multi-part field, from file. 92 | * This return a part equivalent to <input type="file" /> field in multi part form. 93 | */ 94 | public static Part file(String name, String fileName, File file) { 95 | return new Part<>(name, fileName, file, ContentTypes.probeContentType(file), null, (body, out, charset) -> { 96 | try (InputStream in = new FileInputStream(body)) { 97 | InputStreams.transferTo(in, out); 98 | } 99 | }); 100 | } 101 | 102 | 103 | /** 104 | * Create a file multi-part field, from InputStream. 105 | * This return a part equivalent to <input type="file" /> field in multi part form. 106 | * 107 | * @deprecated Http body may be send multi times(because of redirect or other reasons), use {@link Part#file(String, String, InputStreamSupplier)} instead. 108 | */ 109 | @Deprecated 110 | public static Part file(String name, String fileName, InputStream in) { 111 | return new Part<>(name, fileName, in, HttpHeaders.CONTENT_TYPE_BINARY, null, (body, out, charset) -> { 112 | try (InputStream bin = body) { 113 | InputStreams.transferTo(bin, out); 114 | } 115 | }); 116 | } 117 | 118 | /** 119 | * Create a file multi-part field, from InputStream. 120 | * This return a part equivalent to <input type="file" /> field in multi part form. 121 | */ 122 | public static Part file(String name, String fileName, InputStreamSupplier supplier) { 123 | return new Part<>(name, fileName, supplier, HttpHeaders.CONTENT_TYPE_BINARY, null, (body, out, charset) -> { 124 | try (InputStream bin = body.get()) { 125 | InputStreams.transferTo(bin, out); 126 | } 127 | }); 128 | } 129 | 130 | /** 131 | * Create a file multi-part field, from byte array data. 132 | * This return a part equivalent to <input type="file" /> field in multi part form. 133 | */ 134 | public static Part file(String name, String fileName, byte[] bytes) { 135 | return new Part<>(name, fileName, bytes, HttpHeaders.CONTENT_TYPE_BINARY, null, (body, out, charset) -> out.write(body)); 136 | } 137 | 138 | /** 139 | * Create a text multi-part field. 140 | * This return a part equivalent to <input type="text" /> field in multi part form. 141 | */ 142 | public static Part text(String name, String value) { 143 | // the text part do not set content type 144 | return new Part<>(name, null, value, null, null, (body, out, charset) -> { 145 | OutputStreamWriter writer = new OutputStreamWriter(out, Objects2.elvis(charset, ISO_8859_1)); 146 | writer.write(body); 147 | writer.flush(); 148 | }); 149 | } 150 | 151 | /** 152 | * Create a (name, value) text multi-part field. 153 | * This return a part equivalent to <input type="text" /> field in multi part form. 154 | * 155 | * @deprecated use {@link #text(String, String)} instead. 156 | */ 157 | @Deprecated 158 | public static Part param(String name, String value) { 159 | return text(name, value); 160 | } 161 | 162 | /** 163 | * @deprecated use {@link #name()} 164 | */ 165 | @Deprecated 166 | public String getName() { 167 | return name; 168 | } 169 | 170 | /** 171 | * @deprecated use {@link #fileName()} 172 | */ 173 | @Deprecated 174 | @Nullable 175 | public String getFileName() { 176 | return fileName; 177 | } 178 | 179 | /** 180 | * @deprecated use {@link #body()} 181 | */ 182 | @Deprecated 183 | public T getBody() { 184 | return body; 185 | } 186 | 187 | /** 188 | * @deprecated use {@link #contentType()} 189 | */ 190 | @Deprecated 191 | @Nullable 192 | public String getContentType() { 193 | return contentType; 194 | } 195 | 196 | /** 197 | * @deprecated use {@link #charset()} 198 | */ 199 | @Deprecated 200 | @Nullable 201 | public Charset getCharset() { 202 | return charset; 203 | } 204 | 205 | /** 206 | * The part field name 207 | * 208 | * @return the name 209 | */ 210 | public String name() { 211 | return name; 212 | } 213 | 214 | /** 215 | * The filename of th part. may be null if not exists 216 | */ 217 | @Nullable 218 | public String fileName() { 219 | return fileName; 220 | } 221 | 222 | /** 223 | * The part body 224 | */ 225 | public T body() { 226 | return body; 227 | } 228 | 229 | /** 230 | * The content type of the part. For text part, the contentType is always null. 231 | */ 232 | @Nullable 233 | public String contentType() { 234 | return contentType; 235 | } 236 | 237 | 238 | /** 239 | * The charset of this part's content. Each part of MultiPart body can has it's own charset set. 240 | * For text part, the charset is always null. 241 | * 242 | * @return the charset of this part content. if not set, return null. 243 | */ 244 | @Nullable 245 | public Charset charset() { 246 | return charset; 247 | } 248 | 249 | /** 250 | * Write part content to output stream. 251 | */ 252 | public void writeTo(OutputStream out) throws IOException { 253 | partWriter.writeTo(body, out, charset); 254 | } 255 | 256 | 257 | private interface PartWriter { 258 | void writeTo(T body, OutputStream out, @Nullable Charset charset) throws IOException; 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/body/RequestBody.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.body; 2 | 3 | import net.dongliu.commons.collection.Lists; 4 | 5 | import java.io.*; 6 | import java.nio.charset.Charset; 7 | import java.util.Collection; 8 | import java.util.Map; 9 | 10 | import static java.util.Objects.requireNonNull; 11 | 12 | /** 13 | * Request body parent class 14 | * 15 | * @author Liu Dong 16 | */ 17 | public abstract class RequestBody implements Serializable { 18 | private static final long serialVersionUID = 1332594280620699388L; 19 | private final T body; 20 | private String contentType; 21 | /** 22 | * If write charset to contentType header value 23 | */ 24 | private final boolean includeCharset; 25 | 26 | protected RequestBody(T body, String contentType, boolean includeCharset) { 27 | this.body = body; 28 | this.contentType = contentType; 29 | this.includeCharset = includeCharset; 30 | } 31 | 32 | /** 33 | * The request body 34 | * 35 | * @deprecated use {@link #body()} 36 | */ 37 | @Deprecated 38 | public T getBody() { 39 | return body; 40 | } 41 | 42 | /** 43 | * The request body 44 | */ 45 | public T body() { 46 | return body; 47 | } 48 | 49 | /** 50 | * Set content-type value for this request body 51 | * 52 | * @deprecated use {@link RequestBody#contentType(String)} 53 | */ 54 | @Deprecated 55 | public RequestBody setContentType(String contentType) { 56 | this.contentType = requireNonNull(contentType); 57 | return this; 58 | } 59 | 60 | /** 61 | * Set content-type value for this request body 62 | */ 63 | public RequestBody contentType(String contentType) { 64 | this.contentType = requireNonNull(contentType); 65 | return this; 66 | } 67 | 68 | /** 69 | * the content type 70 | * 71 | * @return may be null if no content type is set 72 | * @deprecated use {@link #contentType()} 73 | */ 74 | @Deprecated 75 | public String getContentType() { 76 | return contentType; 77 | } 78 | 79 | /** 80 | * the content type 81 | * 82 | * @return may be null if no content type is set 83 | */ 84 | public String contentType() { 85 | return contentType; 86 | } 87 | 88 | /** 89 | * If write charset to contentType 90 | * 91 | * @deprecated use {@link #includeCharset()} 92 | */ 93 | @Deprecated 94 | public boolean isIncludeCharset() { 95 | return includeCharset; 96 | } 97 | 98 | /** 99 | * If write charset to contentType 100 | */ 101 | public boolean includeCharset() { 102 | return includeCharset; 103 | } 104 | 105 | /** 106 | * Write the request body to output stream. 107 | * 108 | * @param out the output stream to writeTo to 109 | * @param charset the charset to use 110 | */ 111 | public void writeTo(OutputStream out, Charset charset) throws IOException { 112 | writeBody(out, charset); 113 | } 114 | 115 | /** 116 | * Write Request body. 117 | * Note: os should not be closed when this method finished. 118 | */ 119 | public abstract void writeBody(OutputStream out, Charset charset) throws IOException; 120 | 121 | /** 122 | * Create request body send json data 123 | */ 124 | public static RequestBody json(T value) { 125 | return new JsonRequestBody<>(value); 126 | } 127 | 128 | /** 129 | * Create request body send string data 130 | */ 131 | public static RequestBody text(String value) { 132 | return new StringRequestBody(requireNonNull(value)); 133 | } 134 | 135 | /** 136 | * Create request body send x-www-form-encoded data 137 | */ 138 | public static RequestBody>> 139 | form(Collection> params) { 140 | return new FormRequestBody(requireNonNull(params)); 141 | } 142 | 143 | /** 144 | * Create request body send x-www-form-encoded data 145 | */ 146 | public static RequestBody>> form(Map.Entry... params) { 147 | return form(Lists.of(params)); 148 | } 149 | 150 | /** 151 | * Create request body send byte array data 152 | */ 153 | public static RequestBody bytes(byte[] value) { 154 | return new BytesRequestBody(requireNonNull(value)); 155 | } 156 | 157 | /** 158 | * Create request body from input stream. 159 | * 160 | * @deprecated Http body may be send multi times(because of redirect or other reasons), use {@link RequestBody#inputStream(InputStreamSupplier)} )} instead. 161 | */ 162 | @Deprecated 163 | public static RequestBody inputStream(InputStream in) { 164 | return new InputStreamRequestBody(requireNonNull(in)); 165 | } 166 | 167 | /** 168 | * Create request body from input stream. 169 | */ 170 | public static RequestBody inputStream(InputStreamSupplier supplier) { 171 | return new InputStreamSupplierRequestBody(requireNonNull(supplier)); 172 | } 173 | 174 | /** 175 | * Create request body from file 176 | */ 177 | public static RequestBody file(File file) { 178 | return new FileRequestBody(requireNonNull(file)); 179 | } 180 | 181 | /** 182 | * Create multi-part post request body 183 | */ 184 | public static RequestBody>> multiPart(Collection> parts) { 185 | return new MultiPartRequestBody(requireNonNull(parts)); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/body/StringRequestBody.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.body; 2 | 3 | import me.gv7.woodpecker.requests.HttpHeaders; 4 | 5 | import java.io.IOException; 6 | import java.io.OutputStream; 7 | import java.io.OutputStreamWriter; 8 | import java.io.Writer; 9 | import java.nio.charset.Charset; 10 | 11 | /** 12 | * @author Liu Dong 13 | */ 14 | class StringRequestBody extends RequestBody { 15 | private static final long serialVersionUID = -1542159158991437897L; 16 | 17 | StringRequestBody(String body) { 18 | super(body, HttpHeaders.CONTENT_TYPE_TEXT, true); 19 | } 20 | 21 | @Override 22 | public void writeBody(OutputStream out, Charset charset) throws IOException { 23 | try (Writer writer = new OutputStreamWriter(out, charset)) { 24 | writer.write(body()); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/body/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Liu Dong 3 | */ 4 | package me.gv7.woodpecker.requests.body; -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/config/CustomHttpHeaderConfig.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.config; 2 | 3 | import java.util.LinkedHashMap; 4 | 5 | public class CustomHttpHeaderConfig { 6 | private LinkedHashMap customHttpHeaders = new LinkedHashMap(); 7 | private boolean overwriteHttpHeader = false; 8 | 9 | public LinkedHashMap getCustomHttpHeaders() { 10 | return customHttpHeaders; 11 | } 12 | 13 | public void setCustomHttpHeaders(LinkedHashMap customHttpHeaders) { 14 | this.customHttpHeaders = customHttpHeaders; 15 | } 16 | 17 | public boolean isOverwriteHttpHeader() { 18 | return overwriteHttpHeader; 19 | } 20 | 21 | public void setOverwriteHttpHeader(boolean overwriteHttpHeader) { 22 | this.overwriteHttpHeader = overwriteHttpHeader; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/config/HttpConfigManager.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.config; 2 | 3 | import java.util.LinkedHashMap; 4 | import java.util.LinkedList; 5 | import java.util.Random; 6 | 7 | public class HttpConfigManager { 8 | private static ProxyConfig proxyConfig = new ProxyConfig(); 9 | private static TimeoutConfig timeoutConfig = new TimeoutConfig(); 10 | private static UserAgentConfig userAgentConfig = new UserAgentConfig(); 11 | private static CustomHttpHeaderConfig customHttpHeaderConfig = new CustomHttpHeaderConfig(); 12 | 13 | 14 | public static void setProxyConfig(boolean enable,String protocol,String host,int port,String username,String password){ 15 | proxyConfig.setEnable(enable); 16 | proxyConfig.setProtocol(protocol); 17 | proxyConfig.setHost(host); 18 | proxyConfig.setPort(port); 19 | proxyConfig.setUsername(username); 20 | proxyConfig.setPassword(password); 21 | } 22 | 23 | 24 | public static ProxyConfig getProxyConfig() { 25 | return proxyConfig; 26 | } 27 | 28 | 29 | public static void setTimeoutConfig(int defaultTimeout,boolean enableMandatoryTimeout,int mandatoryTimeout){ 30 | timeoutConfig.setDefaultTimeout(defaultTimeout); 31 | timeoutConfig.setEnableMandatoryTimeout(enableMandatoryTimeout); 32 | timeoutConfig.setMandatoryTimeout(mandatoryTimeout); 33 | } 34 | 35 | 36 | public static TimeoutConfig getTimeoutConfig() { 37 | return timeoutConfig; 38 | } 39 | 40 | public static void setUserAgentConfig(LinkedList userAgentList){ 41 | userAgentConfig.setUserAgentList(userAgentList); 42 | } 43 | 44 | 45 | public static UserAgentConfig getUserAgentConfig(){ 46 | return userAgentConfig; 47 | } 48 | 49 | 50 | public static String getUserAgent(){ 51 | LinkedList userAgentList = userAgentConfig.getUserAgentList(); 52 | if(userAgentList.size() == 0){ 53 | return null; 54 | } 55 | int n = new Random().nextInt(userAgentList.size()); 56 | return userAgentList.get(n); 57 | } 58 | 59 | 60 | public static void setCustomHttpHeaderConfig(LinkedHashMap customHttpHeaders,boolean overwriteHttpHeader){ 61 | customHttpHeaderConfig.setCustomHttpHeaders(customHttpHeaders); 62 | customHttpHeaderConfig.setOverwriteHttpHeader(overwriteHttpHeader); 63 | } 64 | 65 | 66 | public static CustomHttpHeaderConfig getCustomHttpHeaderConfig() { 67 | return customHttpHeaderConfig; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/config/ProxyConfig.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.config; 2 | 3 | public class ProxyConfig { 4 | private boolean enable; 5 | private String protocol; 6 | private String host; 7 | private int port; 8 | private String username; 9 | private String password; 10 | 11 | public boolean isEnable() { 12 | return enable; 13 | } 14 | 15 | public void setEnable(boolean enable) { 16 | this.enable = enable; 17 | } 18 | 19 | public String getProtocol() { 20 | return protocol; 21 | } 22 | 23 | public void setProtocol(String protocol) { 24 | this.protocol = protocol; 25 | } 26 | 27 | public String getHost() { 28 | return host; 29 | } 30 | 31 | public void setHost(String host) { 32 | this.host = host; 33 | } 34 | 35 | public int getPort() { 36 | return port; 37 | } 38 | 39 | public void setPort(int port) { 40 | this.port = port; 41 | } 42 | 43 | public String getUsername() { 44 | return username; 45 | } 46 | 47 | public void setUsername(String username) { 48 | this.username = username; 49 | } 50 | 51 | public String getPassword() { 52 | return password; 53 | } 54 | 55 | public void setPassword(String password) { 56 | this.password = password; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/config/TimeoutConfig.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.config; 2 | 3 | public class TimeoutConfig { 4 | private int defaultTimeout; 5 | private boolean enableMandatoryTimeout; 6 | private int mandatoryTimeout; 7 | 8 | public int getDefaultTimeout() { 9 | return defaultTimeout; 10 | } 11 | 12 | public void setDefaultTimeout(int defaultTimeout) { 13 | this.defaultTimeout = defaultTimeout; 14 | } 15 | 16 | public boolean isEnableMandatoryTimeout() { 17 | return enableMandatoryTimeout; 18 | } 19 | 20 | public void setEnableMandatoryTimeout(boolean enableMandatoryTimeout) { 21 | this.enableMandatoryTimeout = enableMandatoryTimeout; 22 | } 23 | 24 | public int getMandatoryTimeout() { 25 | return mandatoryTimeout; 26 | } 27 | 28 | public void setMandatoryTimeout(int mandatoryTimeout) { 29 | this.mandatoryTimeout = mandatoryTimeout; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/config/UserAgentConfig.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.config; 2 | 3 | import java.util.LinkedList; 4 | import java.util.List; 5 | 6 | public class UserAgentConfig { 7 | private LinkedList userAgentList = new LinkedList(); 8 | public LinkedList getUserAgentList() { 9 | return userAgentList; 10 | } 11 | 12 | public void setUserAgentList(LinkedList userAgentList) { 13 | this.userAgentList = userAgentList; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/exception/IllegalStatusException.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.exception; 2 | 3 | /** 4 | * Thrown when status code is not 2xx 5 | */ 6 | public class IllegalStatusException extends RequestsException { 7 | private static final long serialVersionUID = 7652853590053144388L; 8 | 9 | public IllegalStatusException(int statusCode) { 10 | super("Illegal http status code: " + statusCode); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/exception/RequestsException.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.exception; 2 | 3 | /** 4 | * Thrown when request failed. 5 | * 6 | * @author Liu Dong 7 | */ 8 | public class RequestsException extends RuntimeException { 9 | private static final long serialVersionUID = -932950698709129457L; 10 | 11 | public RequestsException(String msg) { 12 | super(msg); 13 | } 14 | 15 | public RequestsException(String msg, Exception e) { 16 | super(msg, e); 17 | } 18 | 19 | public RequestsException(Exception e) { 20 | super(e); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/exception/TooManyRedirectsException.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.exception; 2 | 3 | /** 4 | * Thrown when redirect too many times. 5 | */ 6 | public class TooManyRedirectsException extends RequestsException { 7 | private static final long serialVersionUID = 6781138499271333117L; 8 | 9 | public TooManyRedirectsException(int count) { 10 | super("Redirect too many times: " + count); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/exception/TrustManagerLoadFailedException.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.exception; 2 | 3 | /** 4 | * Thrown when something wrong occurred when load certificate, construct trust manager, etc. 5 | */ 6 | public class TrustManagerLoadFailedException extends RequestsException { 7 | 8 | public TrustManagerLoadFailedException(Exception e) { 9 | super(e); 10 | } 11 | 12 | public TrustManagerLoadFailedException(String message) { 13 | super(message); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/executor/CookieJar.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.executor; 2 | 3 | import me.gv7.woodpecker.requests.Cookie; 4 | 5 | import java.net.URL; 6 | import java.util.Collection; 7 | import java.util.List; 8 | 9 | /** 10 | * Interface for storing cookies 11 | */ 12 | public interface CookieJar { 13 | 14 | /** 15 | * Add multi cookies to cookie jar. 16 | */ 17 | void storeCookies(Collection cookies); 18 | 19 | /** 20 | * Get cookies match the given url. 21 | * 22 | * @return the cookie match url, return empty collection if no match cookie 23 | */ 24 | List getCookies(URL url); 25 | 26 | /** 27 | * Get all cookies in this store 28 | */ 29 | List getCookies(); 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/executor/DefaultCookieJar.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.executor; 2 | 3 | import me.gv7.woodpecker.requests.Cookie; 4 | import me.gv7.woodpecker.requests.utils.Cookies; 5 | 6 | import java.io.Serializable; 7 | import java.net.URL; 8 | import java.util.*; 9 | 10 | /** 11 | * CookieJar that store cookie in memory, maintaining cookies following RFC 6265 12 | */ 13 | class DefaultCookieJar implements CookieJar, Serializable { 14 | 15 | private static final long serialVersionUID = 8372575235144209124L; 16 | private Map cookieMap = new HashMap<>(); 17 | 18 | @Override 19 | public synchronized void storeCookies(Collection cookies) { 20 | for (Cookie cookie : cookies) { 21 | CookieKey key = new CookieKey(cookie.domain(), cookie.path(), cookie.name()); 22 | cookieMap.put(key, cookie); 23 | } 24 | removeExpiredCookies(); 25 | } 26 | 27 | private void removeExpiredCookies() { 28 | long now = System.currentTimeMillis(); 29 | cookieMap.entrySet().removeIf(entry -> entry.getValue().expired(now)); 30 | } 31 | 32 | @Override 33 | public synchronized List getCookies(URL url) { 34 | long now = System.currentTimeMillis(); 35 | List matched = new ArrayList<>(); 36 | for (Cookie cookie : cookieMap.values()) { 37 | if (!Cookies.match(cookie, url.getProtocol(), url.getHost().toLowerCase(), url.getPath())) { 38 | continue; 39 | } 40 | if (cookie.expired(now)) { 41 | continue; 42 | } 43 | matched.add(cookie); 44 | } 45 | // we did not sort using create time here 46 | matched.sort((cookie1, cookie2) -> cookie2.path().length() - cookie1.path().length()); 47 | return matched; 48 | } 49 | 50 | @Override 51 | public synchronized List getCookies() { 52 | return new ArrayList<>(cookieMap.values()); 53 | } 54 | 55 | private static class CookieKey { 56 | private final String domain; 57 | private final String path; 58 | private final String name; 59 | 60 | public CookieKey(String domain, String path, String name) { 61 | this.domain = domain; 62 | this.path = path; 63 | this.name = name; 64 | } 65 | 66 | @Override 67 | public boolean equals(Object o) { 68 | if (this == o) return true; 69 | if (o == null || getClass() != o.getClass()) return false; 70 | 71 | CookieKey cookieKey = (CookieKey) o; 72 | 73 | if (!domain.equals(cookieKey.domain)) return false; 74 | if (!path.equals(cookieKey.path)) return false; 75 | return name.equals(cookieKey.name); 76 | } 77 | 78 | @Override 79 | public int hashCode() { 80 | int result = domain.hashCode(); 81 | result = 31 * result + path.hashCode(); 82 | result = 31 * result + name.hashCode(); 83 | return result; 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/executor/HttpExecutor.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.executor; 2 | 3 | import me.gv7.woodpecker.requests.Interceptor; 4 | import me.gv7.woodpecker.requests.RawResponse; 5 | import me.gv7.woodpecker.requests.Request; 6 | import org.checkerframework.checker.nullness.qual.NonNull; 7 | 8 | /** 9 | * Http executor 10 | * 11 | * @author Liu Dong 12 | */ 13 | public interface HttpExecutor extends Interceptor.InvocationTarget { 14 | /** 15 | * Process the request, and return response 16 | */ 17 | @NonNull 18 | RawResponse proceed(Request request); 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/executor/NopCookieJar.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.executor; 2 | 3 | import net.dongliu.commons.collection.Lists; 4 | import me.gv7.woodpecker.requests.Cookie; 5 | 6 | import java.net.URL; 7 | import java.util.Collection; 8 | import java.util.List; 9 | 10 | /** 11 | * Cookie jar that do nothing. Used for plain request. 12 | */ 13 | class NopCookieJar implements CookieJar { 14 | 15 | static final NopCookieJar instance = new NopCookieJar(); 16 | 17 | private NopCookieJar() { 18 | } 19 | 20 | @Override 21 | public void storeCookies(Collection cookies) { 22 | 23 | } 24 | 25 | @Override 26 | public List getCookies(URL url) { 27 | return Lists.of(); 28 | } 29 | 30 | @Override 31 | public List getCookies() { 32 | return Lists.of(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/executor/RequestExecutorFactory.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.executor; 2 | 3 | /** 4 | * Request Client interface 5 | */ 6 | public abstract class RequestExecutorFactory { 7 | 8 | public static RequestExecutorFactory getInstance() { 9 | return URLConnectionExecutorFactory.instance; 10 | } 11 | 12 | /** 13 | * Create new session context 14 | */ 15 | public abstract SessionContext newSessionContext(); 16 | 17 | public abstract HttpExecutor getHttpExecutor(); 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/executor/SessionContext.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.executor; 2 | 3 | import java.io.Serializable; 4 | 5 | import static java.util.Objects.requireNonNull; 6 | 7 | /** 8 | * Maintain session. 9 | */ 10 | public class SessionContext implements Serializable { 11 | private static final long serialVersionUID = -2357887929783737274L; 12 | private final CookieJar cookieJar; 13 | 14 | public SessionContext(CookieJar cookieJar) { 15 | this.cookieJar = requireNonNull(cookieJar); 16 | } 17 | 18 | /** 19 | * @deprecated use {@link #cookieJar()} 20 | */ 21 | @Deprecated 22 | public CookieJar getCookieJar() { 23 | return cookieJar; 24 | } 25 | 26 | public CookieJar cookieJar() { 27 | return cookieJar; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/executor/URLConnectionExecutor.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.executor; 2 | 3 | import me.gv7.woodpecker.requests.*; 4 | import net.dongliu.commons.io.InputStreams; 5 | import me.gv7.woodpecker.requests.body.RequestBody; 6 | import me.gv7.woodpecker.requests.exception.RequestsException; 7 | import me.gv7.woodpecker.requests.exception.TooManyRedirectsException; 8 | import me.gv7.woodpecker.requests.utils.Cookies; 9 | import me.gv7.woodpecker.requests.utils.NopHostnameVerifier; 10 | import me.gv7.woodpecker.requests.utils.SSLSocketFactories; 11 | import me.gv7.woodpecker.requests.utils.URLUtils; 12 | import org.checkerframework.checker.nullness.qual.Nullable; 13 | 14 | import javax.net.ssl.HttpsURLConnection; 15 | import javax.net.ssl.SSLSocketFactory; 16 | import java.io.IOException; 17 | import java.io.InputStream; 18 | import java.io.OutputStream; 19 | import java.net.*; 20 | import java.nio.charset.Charset; 21 | import java.util.ArrayList; 22 | import java.util.Collection; 23 | import java.util.List; 24 | import java.util.Map; 25 | 26 | import static me.gv7.woodpecker.requests.HttpHeaders.*; 27 | import static me.gv7.woodpecker.requests.StatusCodes.*; 28 | 29 | /** 30 | * Execute http request with url connection 31 | * 32 | * @author Liu Dong 33 | */ 34 | class URLConnectionExecutor implements HttpExecutor { 35 | 36 | static { 37 | // we can modify Host, and other restricted headers 38 | System.setProperty("sun.net.http.allowRestrictedHeaders", "true"); 39 | System.setProperty("http.keepAlive", "true"); 40 | // default is 5 41 | System.setProperty("http.maxConnections", "100"); 42 | } 43 | 44 | @Override 45 | public RawResponse proceed(Request request) { 46 | RawResponse response = doRequest(request); 47 | 48 | int statusCode = response.statusCode(); 49 | if (!request.followRedirect() || !isRedirect(statusCode)) { 50 | return response; 51 | } 52 | 53 | // handle redirect 54 | response.discardBody(); 55 | int redirectTimes = 0; 56 | final int maxRedirectTimes = request.maxRedirectCount(); 57 | URL redirectUrl = request.url(); 58 | while (redirectTimes++ < maxRedirectTimes) { 59 | String location = response.getHeader(NAME_LOCATION); 60 | if (location == null) { 61 | throw new RequestsException("Redirect location not found"); 62 | } 63 | try { 64 | redirectUrl = new URL(redirectUrl, location); 65 | } catch (MalformedURLException e) { 66 | throw new RequestsException("Resolve redirect url error, location: " + location, e); 67 | } 68 | String method = request.method(); 69 | RequestBody body = request.body(); 70 | if (statusCode == MOVED_PERMANENTLY || statusCode == FOUND || statusCode == SEE_OTHER) { 71 | // 301/302 change method to get, due to historical reason. 72 | method = Methods.GET; 73 | body = null; 74 | } 75 | 76 | RequestBuilder builder = request.toBuilder().method(method).url(redirectUrl) 77 | .followRedirect(false).body(body); 78 | response = builder.send(); 79 | if (!isRedirect(response.statusCode())) { 80 | return response; 81 | } 82 | response.discardBody(); 83 | } 84 | throw new TooManyRedirectsException(maxRedirectTimes); 85 | } 86 | 87 | private static boolean isRedirect(int status) { 88 | return status == MULTIPLE_CHOICES || status == MOVED_PERMANENTLY || status == FOUND || status == SEE_OTHER 89 | || status == TEMPORARY_REDIRECT || status == PERMANENT_REDIRECT; 90 | } 91 | 92 | 93 | private RawResponse doRequest(Request request) { 94 | Charset charset = request.charset(); 95 | URL url = URLUtils.joinUrl(request.url(), URLUtils.toStringParameters(request.params()), charset); 96 | @Nullable RequestBody body = request.body(); 97 | CookieJar cookieJar; 98 | if (request.sessionContext() != null) { 99 | cookieJar = request.sessionContext().cookieJar(); 100 | } else { 101 | cookieJar = NopCookieJar.instance; 102 | } 103 | 104 | @Nullable Proxy proxy = request.proxy(); 105 | if (proxy == null) { 106 | proxy = Proxy.NO_PROXY; 107 | } 108 | HttpURLConnection conn; 109 | try { 110 | conn = (HttpURLConnection) url.openConnection(proxy); 111 | } catch (IOException e) { 112 | throw new RequestsException(e); 113 | } 114 | 115 | // disable cache 116 | conn.setUseCaches(false); 117 | 118 | // deal with https 119 | if (conn instanceof HttpsURLConnection) { 120 | HttpsURLConnection httpsConn = (HttpsURLConnection) conn; 121 | if (!request.verify()) { 122 | SSLSocketFactory ssf = SSLSocketFactories.getTrustAllSSLSocketFactory(); 123 | httpsConn.setSSLSocketFactory(ssf); 124 | // do not verify host of certificate 125 | httpsConn.setHostnameVerifier(NopHostnameVerifier.getInstance()); 126 | } else if (request.keyStore() != null) { 127 | SSLSocketFactory ssf = SSLSocketFactories.getCustomTrustSSLSocketFactory(request.keyStore()); 128 | httpsConn.setSSLSocketFactory(ssf); 129 | } 130 | } 131 | 132 | try { 133 | conn.setRequestMethod(request.method()); 134 | } catch (ProtocolException e) { 135 | throw new RequestsException(e); 136 | } 137 | conn.setReadTimeout(request.socksTimeout()); 138 | conn.setConnectTimeout(request.connectTimeout()); 139 | // Url connection did not deal with cookie when handle redirect. Disable it and handle it manually 140 | conn.setInstanceFollowRedirects(false); 141 | if (body != null) { 142 | conn.setDoOutput(true); 143 | String contentType = body.contentType(); 144 | if (contentType != null) { 145 | if (body.includeCharset()) { 146 | contentType += "; charset=" + request.charset().name().toLowerCase(); 147 | } 148 | conn.setRequestProperty(NAME_CONTENT_TYPE, contentType); 149 | } 150 | } 151 | 152 | // headers 153 | if (!request.userAgent().isEmpty()) { 154 | conn.setRequestProperty(NAME_USER_AGENT, request.userAgent()); 155 | } 156 | if (request.acceptCompress()) { 157 | conn.setRequestProperty(NAME_ACCEPT_ENCODING, "gzip, deflate"); 158 | } 159 | 160 | if (request.basicAuth() != null) { 161 | conn.setRequestProperty(NAME_AUTHORIZATION, request.basicAuth().encode()); 162 | } 163 | 164 | // set cookies 165 | Collection sessionCookies = cookieJar.getCookies(url); 166 | if (!request.cookies().isEmpty() || !sessionCookies.isEmpty()) { 167 | StringBuilder sb = new StringBuilder(); 168 | for (Map.Entry entry : request.cookies()) { 169 | sb.append(entry.getKey()).append("=").append(String.valueOf(entry.getValue())).append("; "); 170 | } 171 | for (Cookie cookie : sessionCookies) { 172 | sb.append(cookie.name()).append("=").append(cookie.value()).append("; "); 173 | } 174 | if (sb.length() > 2) { 175 | sb.setLength(sb.length() - 2); 176 | String cookieStr = sb.toString(); 177 | conn.setRequestProperty(NAME_COOKIE, cookieStr); 178 | } 179 | } 180 | 181 | // set user custom headers 182 | for (Map.Entry header : request.headers()) { 183 | conn.setRequestProperty(header.getKey(), String.valueOf(header.getValue())); 184 | } 185 | 186 | // disable keep alive 187 | if (!request.keepAlive()) { 188 | conn.setRequestProperty("Connection", "close"); 189 | } 190 | 191 | try { 192 | conn.connect(); 193 | } catch (IOException e) { 194 | throw new RequestsException(e); 195 | } 196 | 197 | try { 198 | // send body 199 | if (body != null) { 200 | sendBody(body, conn, charset); 201 | } 202 | return getResponse(url, conn, cookieJar, request.method()); 203 | } catch (IOException e) { 204 | conn.disconnect(); 205 | throw new RequestsException(e); 206 | } catch (Throwable e) { 207 | conn.disconnect(); 208 | throw e; 209 | } 210 | } 211 | 212 | /** 213 | * Wrap response, deal with headers and cookies 214 | */ 215 | private RawResponse getResponse(URL url, HttpURLConnection conn, CookieJar cookieJar, String method) 216 | throws IOException { 217 | // read result 218 | int status = conn.getResponseCode(); 219 | String host = url.getHost().toLowerCase(); 220 | 221 | String statusLine = null; 222 | // headers and cookies 223 | List
headerList = new ArrayList<>(); 224 | List cookies = new ArrayList<>(); 225 | int index = 0; 226 | while (true) { 227 | String key = conn.getHeaderFieldKey(index); 228 | String value = conn.getHeaderField(index); 229 | if (value == null) { 230 | break; 231 | } 232 | index++; 233 | //status line 234 | if (key == null) { 235 | statusLine = value; 236 | continue; 237 | } 238 | headerList.add(new Header(key, value)); 239 | if (key.equalsIgnoreCase(NAME_SET_COOKIE)) { 240 | Cookie c = Cookies.parseCookie(value, host, Cookies.calculatePath(url.getPath())); 241 | if (c != null) { 242 | cookies.add(c); 243 | } 244 | } 245 | } 246 | Headers headers = new Headers(headerList); 247 | 248 | InputStream input; 249 | try { 250 | input = conn.getInputStream(); 251 | } catch (IOException e) { 252 | input = conn.getErrorStream(); 253 | } 254 | if (input == null) { 255 | input = InputStreams.empty(); 256 | } 257 | 258 | // update session 259 | cookieJar.storeCookies(cookies); 260 | return new RawResponse(method, url.toExternalForm(), status, statusLine == null ? "" : statusLine, 261 | cookies, headers, input, conn); 262 | } 263 | 264 | 265 | private void sendBody(RequestBody body, HttpURLConnection conn, Charset requestCharset) { 266 | try (OutputStream os = conn.getOutputStream()) { 267 | body.writeBody(os, requestCharset); 268 | } catch (IOException e) { 269 | throw new RequestsException(e); 270 | } 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/executor/URLConnectionExecutorFactory.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.executor; 2 | 3 | /** 4 | * Only for internal use 5 | */ 6 | class URLConnectionExecutorFactory extends RequestExecutorFactory { 7 | static final RequestExecutorFactory instance = new URLConnectionExecutorFactory(); 8 | 9 | @Override 10 | public SessionContext newSessionContext() { 11 | return new SessionContext(new DefaultCookieJar()); 12 | } 13 | 14 | @Override 15 | public HttpExecutor getHttpExecutor() { 16 | return new URLConnectionExecutor(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/executor/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This package is only for internal use. 3 | * Provide HttpExecutor by JDK HttpUrlConnection. 4 | */ 5 | package me.gv7.woodpecker.requests.executor; -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/json/FastJsonProcessor.java: -------------------------------------------------------------------------------- 1 | //package me.gv7.woodpecker.requests.json; 2 | // 3 | //import com.alibaba.fastjson.JSON; 4 | // 5 | //import java.io.IOException; 6 | //import java.io.InputStream; 7 | //import java.io.Writer; 8 | //import java.lang.reflect.Type; 9 | //import java.nio.charset.Charset; 10 | // 11 | ///** 12 | // * @author Liu Dong 13 | // */ 14 | //public class FastJsonProcessor implements JsonProcessor { 15 | // @Override 16 | // public void marshal(Writer writer, Object value) { 17 | // JSON.writeJSONString(writer, value); 18 | // } 19 | // 20 | // @Override 21 | // public T unmarshal(InputStream inputStream, Charset charset, Type type) throws IOException { 22 | // return JSON.parseObject(inputStream, charset, type); 23 | // } 24 | //} 25 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/json/GsonProcessor.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.json; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.GsonBuilder; 5 | import com.google.gson.TypeAdapterFactory; 6 | 7 | import java.io.*; 8 | import java.lang.reflect.Type; 9 | import java.nio.charset.Charset; 10 | import java.util.ServiceLoader; 11 | import java.util.logging.Level; 12 | import java.util.logging.Logger; 13 | 14 | /** 15 | * Provider json ability with gson 16 | * 17 | * @author Liu Dong 18 | */ 19 | public class GsonProcessor implements JsonProcessor { 20 | private static final Logger logger = Logger.getLogger(GsonProcessor.class.getName()); 21 | private final Gson gson; 22 | 23 | public GsonProcessor() { 24 | this(getDefaultGson()); 25 | } 26 | 27 | private static Gson getDefaultGson() { 28 | GsonBuilder gsonBuilder = new GsonBuilder().disableHtmlEscaping(); 29 | registerAllTypeFactories(gsonBuilder); 30 | return gsonBuilder.create(); 31 | } 32 | 33 | /** 34 | * Find and register all gson type factory using spi 35 | */ 36 | private static void registerAllTypeFactories(GsonBuilder gsonBuilder) { 37 | ServiceLoader loader = ServiceLoader.load(TypeAdapterFactory.class); 38 | for (TypeAdapterFactory typeFactory : loader) { 39 | if (logger.isLoggable(Level.FINE)) { 40 | logger.fine("Add gson type factory: " + typeFactory.getClass().getName()); 41 | } 42 | gsonBuilder.registerTypeAdapterFactory(typeFactory); 43 | } 44 | } 45 | 46 | public GsonProcessor(Gson gson) { 47 | this.gson = gson; 48 | } 49 | 50 | @Override 51 | public void marshal(Writer writer, Object value) { 52 | gson.toJson(value, writer); 53 | } 54 | 55 | @Override 56 | public T unmarshal(InputStream inputStream, Charset charset, Type type) throws IOException { 57 | try (Reader reader = new InputStreamReader(inputStream, charset)) { 58 | return gson.fromJson(reader, type); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/json/JacksonProcessor.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.json; 2 | 3 | import com.fasterxml.jackson.databind.JavaType; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import org.checkerframework.checker.nullness.qual.Nullable; 6 | 7 | import java.io.*; 8 | import java.lang.reflect.Type; 9 | import java.nio.charset.Charset; 10 | 11 | /** 12 | * Provider json ability via jackson 13 | * 14 | * @author Liu Dong 15 | */ 16 | public class JacksonProcessor implements JsonProcessor { 17 | 18 | private final ObjectMapper objectMapper; 19 | 20 | public JacksonProcessor() { 21 | this(createDefault()); 22 | } 23 | 24 | private static ObjectMapper createDefault() { 25 | return new ObjectMapper().findAndRegisterModules(); 26 | } 27 | 28 | public JacksonProcessor(ObjectMapper objectMapper) { 29 | this.objectMapper = objectMapper; 30 | } 31 | 32 | @Override 33 | public void marshal(Writer writer, @Nullable Object value) throws IOException { 34 | objectMapper.writeValue(writer, value); 35 | } 36 | 37 | @Override 38 | public T unmarshal(InputStream inputStream, Charset charset, Type type) throws IOException { 39 | try (Reader reader = new InputStreamReader(inputStream, charset)) { 40 | JavaType javaType = objectMapper.getTypeFactory().constructType(type); 41 | return objectMapper.readValue(reader, javaType); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/json/JsonLookup.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.json; 2 | 3 | 4 | 5 | import org.checkerframework.checker.nullness.qual.NonNull; 6 | import org.checkerframework.checker.nullness.qual.Nullable; 7 | 8 | import java.util.Objects; 9 | 10 | /** 11 | * Lookup json, from classpath 12 | * 13 | * @author Liu Dong 14 | */ 15 | public class JsonLookup { 16 | private static JsonLookup instance = new JsonLookup(); 17 | @Nullable 18 | private volatile JsonProcessor registeredJsonProcessor; 19 | 20 | private JsonLookup() { 21 | } 22 | 23 | public static JsonLookup getInstance() { 24 | return instance; 25 | } 26 | 27 | /** 28 | * Set json provider for using. 29 | * 30 | * @see JsonProcessor 31 | * @see JacksonProcessor 32 | * @see GsonProcessor 33 | */ 34 | public void register(JsonProcessor jsonProcessor) { 35 | this.registeredJsonProcessor = Objects.requireNonNull(jsonProcessor); 36 | } 37 | 38 | /** 39 | * If classpath has gson 40 | */ 41 | boolean hasGson() { 42 | try { 43 | Class.forName("com.google.gson.Gson"); 44 | return true; 45 | } catch (ClassNotFoundException e) { 46 | return false; 47 | } 48 | } 49 | 50 | /** 51 | * Create Gson Provider 52 | */ 53 | JsonProcessor gsonProvider() { 54 | return new GsonProcessor(); 55 | } 56 | 57 | /** 58 | * if jackson in classpath 59 | */ 60 | boolean hasJackson() { 61 | try { 62 | Class.forName("com.fasterxml.jackson.databind.ObjectMapper"); 63 | return true; 64 | } catch (ClassNotFoundException e) { 65 | return false; 66 | } 67 | } 68 | 69 | boolean hasFastJson() { 70 | try { 71 | Class.forName("com.alibaba.fastjson.JSON"); 72 | return true; 73 | } catch (ClassNotFoundException e) { 74 | return false; 75 | } 76 | } 77 | 78 | /** 79 | * Create Jackson Provider 80 | */ 81 | JsonProcessor jacksonProvider() { 82 | return new JacksonProcessor(); 83 | } 84 | 85 | // JsonProcessor fastJsonProvider() { 86 | // return new FastJsonProcessor(); 87 | // } 88 | 89 | /** 90 | * Find one json provider. 91 | * 92 | * @throws JsonProcessorNotFoundException if no json provider found 93 | */ 94 | @NonNull 95 | public JsonProcessor lookup() { 96 | JsonProcessor registeredJsonProcessor = this.registeredJsonProcessor; 97 | if (registeredJsonProcessor != null) { 98 | return registeredJsonProcessor; 99 | } 100 | 101 | if (!init) { 102 | synchronized (this) { 103 | if (!init) { 104 | lookedJsonProcessor = lookupInClasspath(); 105 | init = true; 106 | } 107 | } 108 | } 109 | 110 | if (lookedJsonProcessor != null) { 111 | return lookedJsonProcessor; 112 | } 113 | throw new JsonProcessorNotFoundException("Json Provider not found"); 114 | } 115 | 116 | @Nullable 117 | private JsonProcessor lookedJsonProcessor; 118 | private boolean init; 119 | 120 | @Nullable 121 | private JsonProcessor lookupInClasspath() { 122 | if (hasJackson()) { 123 | return jacksonProvider(); 124 | } 125 | if (hasGson()) { 126 | return gsonProvider(); 127 | } 128 | // if (hasFastJson()) { 129 | // return fastJsonProvider(); 130 | // } 131 | return null; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/json/JsonProcessor.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.json; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.io.Writer; 6 | import java.lang.reflect.Type; 7 | import java.nio.charset.Charset; 8 | 9 | /** 10 | * Json provider 11 | * 12 | * @author Liu Dong 13 | */ 14 | public interface JsonProcessor { 15 | 16 | /** 17 | * Serialize value to json, and writeTo to writer 18 | */ 19 | void marshal(Writer writer, Object value) throws IOException; 20 | 21 | /** 22 | * Deserialize json from input stream, with charset and type. 23 | */ 24 | T unmarshal(InputStream inputStream, Charset charset, Type type) throws IOException; 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/json/JsonProcessorNotFoundException.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.json; 2 | 3 | /** 4 | * @author Liu Dong 5 | */ 6 | public class JsonProcessorNotFoundException extends RuntimeException { 7 | private static final long serialVersionUID = -5416319717741876095L; 8 | 9 | public JsonProcessorNotFoundException() { 10 | } 11 | 12 | public JsonProcessorNotFoundException(String message) { 13 | super(message); 14 | } 15 | 16 | public JsonProcessorNotFoundException(String message, Throwable cause) { 17 | super(message, cause); 18 | } 19 | 20 | public JsonProcessorNotFoundException(Throwable cause) { 21 | super(cause); 22 | } 23 | 24 | public JsonProcessorNotFoundException(String message, Throwable cause, boolean enableSuppression, 25 | boolean writableStackTrace) { 26 | super(message, cause, enableSuppression, writableStackTrace); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/json/TypeInfer.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.json; 2 | 3 | import java.lang.reflect.ParameterizedType; 4 | import java.lang.reflect.Type; 5 | 6 | /** 7 | * Simple generic type infer class 8 | * 9 | * @param 10 | */ 11 | public abstract class TypeInfer { 12 | final Type type; 13 | 14 | protected TypeInfer() { 15 | this.type = getSuperclassTypeParameter(getClass()); 16 | } 17 | 18 | private static Type getSuperclassTypeParameter(Class subclass) { 19 | Type superclass = subclass.getGenericSuperclass(); 20 | if (superclass instanceof Class) { 21 | throw new RuntimeException("Missing type parameter."); 22 | } 23 | ParameterizedType parameterized = (ParameterizedType) superclass; 24 | return parameterized.getActualTypeArguments()[0]; 25 | } 26 | 27 | /** 28 | * Gets underlying {@code Type} instance. 29 | */ 30 | public final Type getType() { 31 | return type; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/json/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Json adapter, wrapping jackson, gson, for maybe upcoming json-B 3 | * 4 | * @author Liu Dong 5 | */ 6 | package me.gv7.woodpecker.requests.json; -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * The request package 3 | * @author Liu Dong 4 | */ 5 | package me.gv7.woodpecker.requests; -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/utils/CookieDateUtil.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.utils; 2 | 3 | import net.dongliu.commons.collection.Lists; 4 | import org.checkerframework.checker.nullness.qual.Nullable; 5 | 6 | import java.text.ParseException; 7 | import java.text.SimpleDateFormat; 8 | import java.util.*; 9 | 10 | /** 11 | * @author Liu Dong 12 | */ 13 | final class CookieDateUtil { 14 | 15 | /** 16 | * Date format pattern used to parse HTTP date headers in RFC 1123 format. 17 | */ 18 | public static final String PATTERN_RFC1123 = "EEE, dd MMM yyyy HH:mm:ss zzz"; 19 | 20 | /** 21 | * Date format pattern used to parse HTTP date headers in RFC 1036 format. 22 | */ 23 | public static final String PATTERN_RFC1036 = "EEEE, dd-MMM-yy HH:mm:ss zzz"; 24 | 25 | /** 26 | * Date format pattern used to parse HTTP date headers in ANSI C 27 | * asctime() format. 28 | */ 29 | public static final String PATTERN_ASCTIME = "EEE MMM d HH:mm:ss yyyy"; 30 | 31 | private static final Collection DEFAULT_PATTERNS = Lists.of( 32 | PATTERN_ASCTIME, PATTERN_RFC1036, PATTERN_RFC1123); 33 | 34 | private static final Date DEFAULT_TWO_DIGIT_YEAR_START; 35 | 36 | static { 37 | Calendar calendar = Calendar.getInstance(); 38 | calendar.set(2000, Calendar.JANUARY, 1, 0, 0); 39 | DEFAULT_TWO_DIGIT_YEAR_START = calendar.getTime(); 40 | } 41 | 42 | private static final TimeZone GMT = TimeZone.getTimeZone("GMT"); 43 | 44 | /** 45 | * This class should not be instantiated. 46 | */ 47 | private CookieDateUtil() { 48 | } 49 | 50 | 51 | /** 52 | * Parses a date value. The formats used for parsing the date value are retrieved from 53 | * the default http params. 54 | * 55 | * @param dateValue the date value to parse 56 | * @return the parsed date 57 | */ 58 | @Nullable 59 | public static Date parseDate(String dateValue) { 60 | return parseDate(dateValue, DEFAULT_PATTERNS); 61 | } 62 | 63 | /** 64 | * Parses the date value using the given date formats. 65 | * 66 | * @param dateValue the date value to parse 67 | * @param dateFormats the date formats to use 68 | * @return the parsed date 69 | */ 70 | @Nullable 71 | public static Date parseDate(String dateValue, Collection dateFormats) { 72 | return parseDate(dateValue, dateFormats, DEFAULT_TWO_DIGIT_YEAR_START); 73 | } 74 | 75 | /** 76 | * Parses the date value using the given date formats. 77 | * 78 | * @param dateValue the date value to parse 79 | * @param dateFormats the date formats to use 80 | * @param startDate During parsing, two digit years will be placed in the range 81 | * startDate to startDate + 100 years. This value may 82 | * be null. When null is given as a parameter, year 83 | * 2000 will be used. 84 | * @return the parsed date 85 | */ 86 | @Nullable 87 | public static Date parseDate(String dateValue, Collection dateFormats, Date startDate) { 88 | 89 | // trim single quotes around date if present 90 | // see issue #5279 91 | if (dateValue.length() > 1 && dateValue.startsWith("'") && dateValue.endsWith("'")) { 92 | dateValue = dateValue.substring(1, dateValue.length() - 1); 93 | } 94 | 95 | SimpleDateFormat dateParser = null; 96 | for (String format : dateFormats) { 97 | if (dateParser == null) { 98 | dateParser = new SimpleDateFormat(format, Locale.US); 99 | dateParser.setTimeZone(TimeZone.getTimeZone("GMT")); 100 | dateParser.set2DigitYearStart(startDate); 101 | } else { 102 | dateParser.applyPattern(format); 103 | } 104 | try { 105 | return dateParser.parse(dateValue); 106 | } catch (ParseException pe) { 107 | // ignore this exception, we will try the next format 108 | } 109 | } 110 | 111 | // we were unable to parse the date 112 | return null; 113 | } 114 | 115 | /** 116 | * Formats the given date according to the RFC 1123 pattern. 117 | * 118 | * @param date The date to format. 119 | * @return An RFC 1123 formatted date string. 120 | * @see #PATTERN_RFC1123 121 | */ 122 | public static String formatDate(Date date) { 123 | return formatDate(date, PATTERN_RFC1123); 124 | } 125 | 126 | /** 127 | * Formats the given date according to the specified pattern. The pattern 128 | * must conform to that used by the {@link SimpleDateFormat simple date 129 | * format} class. 130 | * 131 | * @param date The date to format. 132 | * @param pattern The pattern to use for formatting the date. 133 | * @return A formatted date string. 134 | * @throws IllegalArgumentException If the given date pattern is invalid. 135 | * @see SimpleDateFormat 136 | */ 137 | public static String formatDate(Date date, String pattern) { 138 | SimpleDateFormat formatter = new SimpleDateFormat(pattern, Locale.US); 139 | formatter.setTimeZone(GMT); 140 | return formatter.format(date); 141 | } 142 | 143 | } -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/utils/Cookies.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.utils; 2 | 3 | import me.gv7.woodpecker.requests.Cookie; 4 | import me.gv7.woodpecker.requests.Parameter; 5 | import org.checkerframework.checker.nullness.qual.Nullable; 6 | 7 | import java.util.Date; 8 | 9 | /** 10 | * Only for internal use. 11 | * Http response set cookie header. 12 | *

13 | * RFC 6265: 14 | * The origin domain of a cookie is the domain of the originating request. 15 | * If the origin domain is an IP, the cookie's domain attribute must not be set. 16 | * If a cookie's domain attribute is not set, the cookie is only applicable to its origin domain. 17 | * If a cookie's domain attribute is set, 18 | * -- the cookie is applicable to that domain and all its subdomains; 19 | * -- the cookie's domain must be the same as, or a parent of, the origin domain 20 | * -- the cookie's domain must not be a TLD, a public suffix, or a parent of a public suffix. 21 | */ 22 | public class Cookies { 23 | 24 | /** 25 | * Get cookie default path from url path 26 | */ 27 | public static String calculatePath(String uri) { 28 | if (!uri.startsWith("/")) { 29 | return "/"; 30 | } 31 | int idx = uri.lastIndexOf('/'); 32 | return uri.substring(0, idx + 1); 33 | } 34 | 35 | /** 36 | * A simple, non-accurate method(we just need to distinguish ip and domain) to judge if host is a ipv4/ipv6 address. 37 | */ 38 | public static boolean isIP(String host) { 39 | return isIPV4(host) || isIPV6(host); 40 | } 41 | 42 | 43 | private static boolean isIPV4(String host) { 44 | int dotCount = 0; 45 | for (int i = 0; i < host.length(); i++) { 46 | char c = host.charAt(i); 47 | if (c >= '0' && c <= '9') { 48 | // 49 | } else if (c == '.') { 50 | if (++dotCount > 3) { 51 | return false; 52 | } 53 | } else { 54 | return false; 55 | } 56 | } 57 | return true; 58 | } 59 | 60 | private static boolean isIPV6(String host) { 61 | int colonCount = 0; 62 | int dotCount = 0; 63 | for (int i = 0; i < host.length(); i++) { 64 | char c = host.charAt(i); 65 | if (c >= '0' && c <= '9') { 66 | // 67 | } else if (c >= 'a' && c <= 'f' || c >= 'A' && c <= 'F') { 68 | if (dotCount > 0) { 69 | return false; 70 | } 71 | } else if (c == ':') { 72 | colonCount++; 73 | if (colonCount + dotCount > 7) { 74 | return false; 75 | } 76 | } else if (c == '.') { 77 | dotCount++; 78 | if (colonCount < 2 || dotCount > 3) { 79 | return false; 80 | } 81 | } else { 82 | return false; 83 | } 84 | } 85 | return true; 86 | } 87 | 88 | 89 | /** 90 | * If domainSuffix is suffix of domain 91 | * 92 | * @param domain start with "." 93 | * @param domainSuffix not start with "." 94 | */ 95 | public static boolean isDomainSuffix(String domain, String domainSuffix) { 96 | if (domain.length() < domainSuffix.length()) { 97 | return false; 98 | } 99 | if (domain.length() == domainSuffix.length()) { 100 | return domain.equals(domainSuffix); 101 | } 102 | 103 | return domain.endsWith(domainSuffix) && domain.charAt(domain.length() - domainSuffix.length() - 1) == '.'; 104 | } 105 | 106 | 107 | /** 108 | * If cookie match the given scheme, host, and path. 109 | * 110 | * @param host should be lower case 111 | */ 112 | public static boolean match(Cookie cookie, String protocol, String host, String path) { 113 | if (cookie.secure() && !protocol.equalsIgnoreCase("https")) { 114 | return false; 115 | } 116 | 117 | // check domain 118 | if (isIP(host) || cookie.hostOnly()) { 119 | if (!host.equals(cookie.domain())) { 120 | return false; 121 | } 122 | } else { 123 | if (!Cookies.isDomainSuffix(host, cookie.domain())) { 124 | return false; 125 | } 126 | } 127 | 128 | String cookiePath = cookie.path(); 129 | // check path 130 | if (cookiePath.length() > path.length()) { 131 | return false; 132 | } 133 | if (cookiePath.length() == path.length()) { 134 | return cookiePath.equals(path); 135 | } 136 | if (!path.startsWith(cookiePath)) { 137 | return false; 138 | } 139 | if (cookiePath.charAt(cookiePath.length() - 1) != '/' && path.charAt(cookiePath.length()) != '/') { 140 | return false; 141 | } 142 | return true; 143 | } 144 | 145 | /** 146 | * Parse one cookie header value, return the cookie. 147 | * 148 | * @param host should be lower case 149 | * @return return null means is not a valid cookie str. 150 | */ 151 | @Nullable 152 | public static Cookie parseCookie(String cookieStr, String host, String defaultPath) { 153 | String[] items = cookieStr.split(";"); 154 | Parameter param = parseCookieNameValue(items[0]); 155 | if (param == null) { 156 | return null; 157 | } 158 | 159 | String domain = ""; 160 | String path = ""; 161 | long expiry = 0; 162 | boolean secure = false; 163 | for (String item : items) { 164 | item = item.trim(); 165 | if (item.isEmpty()) { 166 | continue; 167 | } 168 | Parameter attribute = parseCookieAttribute(item); 169 | switch (attribute.name().toLowerCase()) { 170 | case "domain": 171 | domain = normalizeDomain(attribute.value()); 172 | break; 173 | case "path": 174 | path = normalizePath(attribute.value()); 175 | break; 176 | case "expires": 177 | // If a cookie has both the Max-Age and the Expires attribute, the Max-Age attribute has precedence 178 | // and controls the expiration date of the cookie. 179 | if (expiry == 0) { 180 | Date date = CookieDateUtil.parseDate(attribute.value()); 181 | if (date != null) { 182 | expiry = date.getTime(); 183 | if (expiry == 0) { 184 | expiry = 1; 185 | } 186 | } 187 | } 188 | break; 189 | case "max-age": 190 | try { 191 | int seconds = Integer.parseInt(attribute.value()); 192 | expiry = System.currentTimeMillis() + seconds * 1000; 193 | if (expiry == 0) { 194 | expiry = 1; 195 | } 196 | } catch (NumberFormatException ignore) { 197 | } 198 | break; 199 | case "secure": 200 | secure = true; 201 | break; 202 | case "httponly": 203 | // ignore http only now 204 | break; 205 | default: 206 | } 207 | } 208 | 209 | if (path.isEmpty()) { 210 | path = defaultPath; 211 | } 212 | boolean hostOnly; 213 | if (domain.isEmpty()) { 214 | domain = host; 215 | hostOnly = true; 216 | } else { 217 | if (isIP(host)) { 218 | // should not set 219 | return null; 220 | } 221 | if (!isDomainSuffix(host, domain)) { 222 | return null; 223 | } 224 | hostOnly = false; 225 | } 226 | 227 | return new Cookie(domain, path, param.name(), param.value(), expiry, secure, hostOnly); 228 | } 229 | 230 | 231 | @Nullable 232 | private static Parameter parseCookieNameValue(String str) { 233 | // Browsers always split the name and value on the first = symbol in the string 234 | int idx = str.indexOf("="); 235 | if (idx <= 0) { 236 | // If there is no = symbol in the string at all, RFC 6265 ignore this cookie. 237 | // if cookie name is empty, RFC 6265 ignore this cookie. 238 | return null; 239 | } 240 | return Parameter.of(str.substring(0, idx).trim(), str.substring(idx + 1).trim()); 241 | } 242 | 243 | private static Parameter parseCookieAttribute(String str) { 244 | int idx = str.indexOf("="); 245 | if (idx < 0) { 246 | return Parameter.of(str, ""); 247 | } else { 248 | return Parameter.of(str.substring(0, idx), str.substring(idx + 1)); 249 | } 250 | } 251 | 252 | private static String normalizePath(String str) { 253 | if (!str.startsWith("/")) { 254 | // use defaultPath 255 | return ""; 256 | } 257 | return str; 258 | } 259 | 260 | /** 261 | * Parse cookie domain. 262 | * In RFC 6265, the leading dot will be ignored, and cookie always available in sub domains. 263 | * 264 | * @return the final domain 265 | */ 266 | private static String normalizeDomain(String value) { 267 | if (value.startsWith(".")) { 268 | return value.substring(1); 269 | } 270 | return value.toLowerCase(); 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/utils/NopHostnameVerifier.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.utils; 2 | 3 | import javax.net.ssl.HostnameVerifier; 4 | import javax.net.ssl.SSLSession; 5 | 6 | /** 7 | * @author Liu Dong 8 | */ 9 | public class NopHostnameVerifier implements HostnameVerifier { 10 | 11 | private static class Holder { 12 | private static NopHostnameVerifier instance = new NopHostnameVerifier(); 13 | } 14 | 15 | public static HostnameVerifier getInstance() { 16 | return Holder.instance; 17 | } 18 | 19 | @Override 20 | public boolean verify(String s, SSLSession sslSession) { 21 | return true; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/utils/SSLSocketFactories.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.utils; 2 | 3 | import me.gv7.woodpecker.requests.exception.TrustManagerLoadFailedException; 4 | import me.gv7.woodpecker.requests.exception.RequestsException; 5 | 6 | import javax.net.ssl.*; 7 | import java.security.*; 8 | import java.security.cert.CertificateException; 9 | import java.security.cert.X509Certificate; 10 | import java.util.concurrent.ConcurrentHashMap; 11 | import java.util.concurrent.ConcurrentMap; 12 | 13 | /** 14 | * Utils method for ssl socket factory 15 | * 16 | * @author Liu Dong 17 | */ 18 | public class SSLSocketFactories { 19 | 20 | // To reuse the connection, settings on the underlying socket must use the exact same objects. 21 | 22 | private static final SSLSocketFactory sslSocketFactoryLazy = _getTrustAllSSLSocketFactory(); 23 | 24 | public static SSLSocketFactory _getTrustAllSSLSocketFactory() { 25 | TrustManager trustManager = new TrustAllTrustManager(); 26 | SSLContext sslContext; 27 | try { 28 | sslContext = SSLContext.getInstance("SSL"); 29 | sslContext.init(null, new TrustManager[]{trustManager}, new SecureRandom()); 30 | } catch (NoSuchAlgorithmException | KeyManagementException e) { 31 | throw new RequestsException(e); 32 | } 33 | 34 | return sslContext.getSocketFactory(); 35 | } 36 | 37 | 38 | public static SSLSocketFactory getTrustAllSSLSocketFactory() { 39 | return sslSocketFactoryLazy; 40 | } 41 | 42 | private static final ConcurrentMap map = new ConcurrentHashMap<>(); 43 | 44 | private static SSLSocketFactory _getCustomSSLSocketFactory(KeyStore keyStore) { 45 | TrustManager trustManager = new CustomCertTrustManager(keyStore); 46 | SSLContext sslContext; 47 | try { 48 | sslContext = SSLContext.getInstance("SSL"); 49 | sslContext.init(null, new TrustManager[]{trustManager}, new SecureRandom()); 50 | } catch (NoSuchAlgorithmException | KeyManagementException e) { 51 | throw new RequestsException(e); 52 | } 53 | 54 | return sslContext.getSocketFactory(); 55 | } 56 | 57 | public static SSLSocketFactory getCustomTrustSSLSocketFactory(KeyStore keyStore) { 58 | if (!map.containsKey(keyStore)) { 59 | map.put(keyStore, _getCustomSSLSocketFactory(keyStore)); 60 | } 61 | return map.get(keyStore); 62 | } 63 | 64 | static class TrustAllTrustManager implements X509TrustManager { 65 | @Override 66 | public void checkClientTrusted(X509Certificate[] x509Certificates, String s) { 67 | } 68 | 69 | @Override 70 | public void checkServerTrusted(X509Certificate[] x509Certificates, String s) { 71 | } 72 | 73 | @Override 74 | public X509Certificate[] getAcceptedIssuers() { 75 | return null; 76 | } 77 | } 78 | 79 | /** 80 | * Trust Manager that trust additional x509 certificates provided by user 81 | */ 82 | static class CustomCertTrustManager implements X509TrustManager { 83 | 84 | private final KeyStore keyStore; 85 | private final X509TrustManager defaultTrustManager; 86 | private final X509TrustManager trustManager; 87 | 88 | public CustomCertTrustManager(KeyStore keyStore) { 89 | this.keyStore = keyStore; 90 | // get the default trust manager 91 | TrustManagerFactory defaultTrustManagerFactory; 92 | try { 93 | defaultTrustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); 94 | defaultTrustManagerFactory.init((KeyStore) null); 95 | } catch (NoSuchAlgorithmException | KeyStoreException e) { 96 | throw new TrustManagerLoadFailedException(e); 97 | } 98 | X509TrustManager defaultTrustManager = null; 99 | for (TrustManager tm : defaultTrustManagerFactory.getTrustManagers()) { 100 | if (tm instanceof X509TrustManager) { 101 | defaultTrustManager = (X509TrustManager) tm; 102 | break; 103 | } 104 | } 105 | if (defaultTrustManager == null) { 106 | throw new TrustManagerLoadFailedException("Default X509TrustManager not found"); 107 | } 108 | this.defaultTrustManager = defaultTrustManager; 109 | 110 | TrustManagerFactory trustManagerFactory; 111 | try { 112 | trustManagerFactory = TrustManagerFactory.getInstance("SunX509", "SunJSSE"); 113 | trustManagerFactory.init(keyStore); 114 | } catch (NoSuchAlgorithmException | NoSuchProviderException | KeyStoreException e) { 115 | throw new TrustManagerLoadFailedException(e); 116 | } 117 | 118 | X509TrustManager trustManager = null; 119 | for (TrustManager tm : trustManagerFactory.getTrustManagers()) { 120 | if (tm instanceof X509TrustManager) { 121 | trustManager = (X509TrustManager) tm; 122 | break; 123 | } 124 | } 125 | if (trustManager == null) { 126 | throw new TrustManagerLoadFailedException("X509TrustManager for user keystore not found"); 127 | } 128 | this.trustManager = trustManager; 129 | } 130 | 131 | @Override 132 | public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { 133 | try { 134 | trustManager.checkClientTrusted(chain, authType); 135 | } catch (CertificateException e) { 136 | defaultTrustManager.checkClientTrusted(chain, authType); 137 | } 138 | } 139 | 140 | @Override 141 | public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { 142 | try { 143 | trustManager.checkServerTrusted(chain, authType); 144 | } catch (CertificateException e) { 145 | defaultTrustManager.checkServerTrusted(chain, authType); 146 | } 147 | } 148 | 149 | @Override 150 | public X509Certificate[] getAcceptedIssuers() { 151 | X509Certificate[] defaultAcceptedIssuers = defaultTrustManager.getAcceptedIssuers(); 152 | X509Certificate[] acceptedIssuers = trustManager.getAcceptedIssuers(); 153 | X509Certificate[] result = new X509Certificate[defaultAcceptedIssuers.length + acceptedIssuers.length]; 154 | System.arraycopy(defaultAcceptedIssuers, 0, result, 0, defaultAcceptedIssuers.length); 155 | System.arraycopy(acceptedIssuers, 0, result, defaultAcceptedIssuers.length, acceptedIssuers.length); 156 | return result; 157 | } 158 | } 159 | 160 | } 161 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/utils/URLUtils.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.utils; 2 | 3 | import me.gv7.woodpecker.requests.Parameter; 4 | import me.gv7.woodpecker.requests.exception.RequestsException; 5 | 6 | import java.io.UnsupportedEncodingException; 7 | import java.net.MalformedURLException; 8 | import java.net.URL; 9 | import java.net.URLDecoder; 10 | import java.net.URLEncoder; 11 | import java.nio.charset.Charset; 12 | import java.util.ArrayList; 13 | import java.util.Collection; 14 | import java.util.List; 15 | import java.util.Map; 16 | 17 | /** 18 | * Util methods for encode / decode uri. 19 | */ 20 | public class URLUtils { 21 | 22 | /** 23 | * Encode key-value form parameter 24 | */ 25 | public static String encodeForm(Parameter query, Charset charset) { 26 | try { 27 | return URLEncoder.encode(query.name(), charset.name()) + "=" + URLEncoder.encode(query.value(), 28 | charset.name()); 29 | } catch (UnsupportedEncodingException e) { 30 | // should not happen 31 | throw new RequestsException(e); 32 | } 33 | } 34 | 35 | /** 36 | * Encode multi form parameters 37 | */ 38 | public static String encodeForms(Collection> queries, Charset charset) { 39 | StringBuilder sb = new StringBuilder(); 40 | try { 41 | for (Parameter query : queries) { 42 | sb.append(URLEncoder.encode(query.name(), charset.name())); 43 | sb.append('='); 44 | sb.append(URLEncoder.encode(query.value(), charset.name())); 45 | sb.append('&'); 46 | } 47 | } catch (UnsupportedEncodingException e) { 48 | // should not happen 49 | throw new RequestsException(e); 50 | } 51 | if (sb.length() > 0) { 52 | sb.deleteCharAt(sb.length() - 1); 53 | } 54 | return sb.toString(); 55 | } 56 | 57 | /** 58 | * Decode key-value query parameter 59 | */ 60 | public static Parameter decodeForm(String s, Charset charset) { 61 | int idx = s.indexOf("="); 62 | try { 63 | if (idx < 0) { 64 | return Parameter.of("", URLDecoder.decode(s, charset.name())); 65 | } 66 | return Parameter.of(URLDecoder.decode(s.substring(0, idx), charset.name()), 67 | URLDecoder.decode(s.substring(idx + 1), charset.name())); 68 | } catch (UnsupportedEncodingException e) { 69 | // should not happen 70 | throw new RequestsException(e); 71 | } 72 | } 73 | 74 | /** 75 | * Parse query params 76 | */ 77 | public static List> decodeForms(String queryStr, Charset charset) { 78 | String[] queries = queryStr.split("&"); 79 | List> list = new ArrayList<>(queries.length); 80 | for (String query : queries) { 81 | list.add(decodeForm(query, charset)); 82 | } 83 | 84 | return list; 85 | } 86 | 87 | public static List> toStringParameters(Collection> params) { 88 | List> parameters = new ArrayList<>(params.size()); 89 | for (Map.Entry entry : params) { 90 | parameters.add(Parameter.of(entry.getKey(), String.valueOf(entry.getValue()))); 91 | } 92 | return parameters; 93 | } 94 | 95 | public static URL joinUrl(URL url, Collection> params, Charset charset) { 96 | if (params.isEmpty()) { 97 | return url; 98 | } 99 | 100 | StringBuilder sb = new StringBuilder(); 101 | sb.append(url.getProtocol()).append(':'); 102 | if (url.getAuthority() != null && !url.getAuthority().isEmpty()) { 103 | sb.append("//").append(url.getAuthority()); 104 | } 105 | if (url.getPath() != null) { 106 | sb.append(url.getPath()); 107 | } 108 | 109 | String query = url.getQuery(); 110 | String newQuery = encodeForms(params, charset); 111 | if (query == null || query.isEmpty()) { 112 | sb.append('?').append(newQuery); 113 | } else { 114 | sb.append('?').append(query).append('&').append(newQuery); 115 | } 116 | 117 | if (url.getRef() != null) { 118 | sb.append('#').append(url.getRef()); 119 | } 120 | 121 | URL fullURL; 122 | try { 123 | fullURL = new URL(sb.toString()); 124 | } catch (MalformedURLException e) { 125 | throw new RequestsException(e); 126 | 127 | } 128 | return fullURL; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/main/java/me/gv7/woodpecker/requests/utils/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Utils classes, Only for internal use! 3 | * 4 | * @author Liu Dong 5 | */ 6 | package me.gv7.woodpecker.requests.utils; -------------------------------------------------------------------------------- /src/test/java/me/gv7/woodpecker/requests/ChunkedHeaderTest.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests; 2 | 3 | import org.junit.Test; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | /** 9 | * @author c0ny1 10 | * @date 2023/04/11 16:32:50 11 | **/ 12 | public class ChunkedHeaderTest { 13 | /** 14 | * 问题:无法发送Request header Transfer-Encoding: chunked 15 | * 原因:经过测试会在分块传输头会在sun.net.www.protocol.http.HttpURLConnection#writeRequests()处被删除。 16 | * 注意: 17 | * 1.不要用burp抓包测试,burp似乎对分块进行合并,请使用wireshark。 18 | * 2.在me.gv7.woodpecker.requests.executor.URLConnectionExecutor#doRequest(me.gv7.woodpecker.requests.Request)处 19 | * 加入conn.setChunkedStreamingMode(1);可以发送Transfer-Encoding: chunked,但是没发完全控制body。要支持分块传输还需要深入深入研究。 20 | * 21 | */ 22 | @Test 23 | public void sendChunkedHeader(){ 24 | Map header = new HashMap(); 25 | header.put("Transfer-Encoding","chunked"); 26 | header.put("aaa","bbb"); 27 | Requests.post("http://127.0.0.1:1664/").headers(header).body("2\r\nxxs3\r\ndwe").proxy(Proxies.httpProxy("127.0.0.1",1664)).send(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/test/java/me/gv7/woodpecker/requests/CookieTest.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests; 2 | 3 | public class CookieTest { 4 | 5 | } -------------------------------------------------------------------------------- /src/test/java/me/gv7/woodpecker/requests/HeadersTest.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests; 2 | 3 | import me.gv7.woodpecker.requests.config.HttpConfigManager; 4 | import net.dongliu.commons.collection.Lists; 5 | import org.junit.Test; 6 | 7 | import java.net.InetSocketAddress; 8 | import java.net.Proxy; 9 | import java.util.Arrays; 10 | import java.util.HashMap; 11 | import java.util.LinkedHashMap; 12 | 13 | import static org.junit.Assert.assertEquals; 14 | 15 | /** 16 | * @author Liu Dong 17 | */ 18 | public class HeadersTest { 19 | @Test 20 | public void getHeaders() { 21 | Headers headers = new Headers(Arrays.asList( 22 | new Header("Location", "www"), 23 | new Header("Location", "www2"), 24 | new Header("Content-Length", "100") 25 | )); 26 | assertEquals(Lists.of("www", "www2"), headers.getHeaders("Location")); 27 | } 28 | 29 | @Test 30 | public void getHeader() { 31 | Headers headers = new Headers(Lists.of( 32 | new Header("Location", "www"), 33 | new Header("Location", "www2"), 34 | new Header("Content-Length", "100") 35 | )); 36 | assertEquals("www", headers.getHeader("Location")); 37 | assertEquals("www", headers.getHeader("location")); 38 | } 39 | 40 | @Test 41 | public void getLongHeader() { 42 | Headers headers = new Headers(Lists.of( 43 | new Header("Location", "www"), 44 | new Header("Location", "www2"), 45 | new Header("Content-Length", "100") 46 | )); 47 | assertEquals(100, headers.getLongHeader("Content-Length", -1)); 48 | } 49 | 50 | /** 51 | * 测试不覆盖模式下,Host以及用户设置的头部是否保持原样 52 | */ 53 | @Test 54 | public void setNoOverwriteHeader(){ 55 | LinkedHashMap newHeaders = new LinkedHashMap<>(); 56 | newHeaders.put("Host","overwrite.com"); 57 | newHeaders.put("aaa","overwrite"); 58 | newHeaders.put("bbb","bbb"); 59 | HttpConfigManager.setCustomHttpHeaderConfig(newHeaders,false); 60 | LinkedHashMap headers = new LinkedHashMap<>(); 61 | headers.put("aaa","aaa"); 62 | headers.put("ccc","ccc"); 63 | RequestBuilder requestBuilder = Requests.get("http://woodpecker.gv7.me/index").headers(headers); 64 | requestBuilder.build(); 65 | 66 | assertEquals(null,requestBuilder.getHeader("Host")); 67 | assertEquals("aaa",requestBuilder.getHeader("aaa")); 68 | assertEquals("bbb",requestBuilder.getHeader("bbb")); 69 | assertEquals("ccc",requestBuilder.getHeader("ccc")); 70 | } 71 | 72 | /** 73 | * 测试覆盖模式下是否覆盖成功 74 | */ 75 | @Test 76 | public void setOverwriteHeader(){ 77 | LinkedHashMap newHeaders = new LinkedHashMap<>(); 78 | newHeaders.put("Host","overwrite.com"); 79 | newHeaders.put("aaa","overwrite"); 80 | newHeaders.put("bbb","bbb"); 81 | HttpConfigManager.setCustomHttpHeaderConfig(newHeaders,true); 82 | LinkedHashMap headers = new LinkedHashMap<>(); 83 | headers.put("aaa","aaa"); 84 | headers.put("ccc","ccc"); 85 | RequestBuilder requestBuilder = Requests.get("http://woodpecker.gv7.me/index").headers(headers); 86 | requestBuilder.build(); 87 | 88 | assertEquals("overwrite.com",requestBuilder.getHeader("Host")); 89 | assertEquals("overwrite",requestBuilder.getHeader("aaa")); 90 | assertEquals("bbb",requestBuilder.getHeader("bbb")); 91 | assertEquals("ccc",requestBuilder.getHeader("ccc")); // 测试是否用户未被覆盖的header否否保持 92 | } 93 | 94 | /** 95 | * 测试新增多个header是否会覆盖 96 | */ 97 | @Test 98 | public void setManyHeader(){ 99 | HashMap h = new HashMap<>(); 100 | h.put("header1","header1"); 101 | HashMap h2 = new HashMap(); 102 | h2.put("header2","header2"); 103 | HashMap h3 = new HashMap(); 104 | h3.put("header3","header3"); 105 | String url = "http://www.baidu.com"; 106 | RequestBuilder requestBuilder = Requests.get(url) 107 | .headers(h) // header1 108 | .headers(h2) // header2 109 | .headers(h3.entrySet()) // header3 110 | .headers(Lists.of(new Header("header4","header4"))); // header4 111 | assertEquals("header1",requestBuilder.getHeader("header1")); 112 | assertEquals("header2",requestBuilder.getHeader("header2")); 113 | assertEquals("header3",requestBuilder.getHeader("header3")); 114 | assertEquals("header4",requestBuilder.getHeader("header4")); 115 | } 116 | 117 | } -------------------------------------------------------------------------------- /src/test/java/me/gv7/woodpecker/requests/IgnoreHttpConfigTest.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests; 2 | 3 | import me.gv7.woodpecker.requests.config.HttpConfigManager; 4 | import org.junit.Test; 5 | 6 | import java.util.LinkedHashMap; 7 | 8 | import static org.junit.Assert.assertEquals; 9 | 10 | public class IgnoreHttpConfigTest { 11 | /** 12 | * 设置开启忽略Http配置模式 13 | */ 14 | @Test 15 | public void ignoreHttpConfigTest(){ 16 | LinkedHashMap newHeaders = new LinkedHashMap<>(); 17 | newHeaders.put("aaa","overwrite"); 18 | newHeaders.put("bbb","bbb"); 19 | HttpConfigManager.setCustomHttpHeaderConfig(newHeaders,true); 20 | 21 | LinkedHashMap headers = new LinkedHashMap<>(); 22 | headers.put("aaa","aaa"); 23 | headers.put("ccc","ccc"); 24 | RequestBuilder requestBuilder = Requests.get("http://woodpecker.gv7.me/index").headers(headers).ignoreHttpConfig(true); 25 | requestBuilder.build(); 26 | 27 | assertEquals("aaa",requestBuilder.getHeader("aaa")); 28 | assertEquals(null,requestBuilder.getHeader("bbb")); 29 | assertEquals("ccc",requestBuilder.getHeader("ccc")); // 测试是否用户未被覆盖的header否否保持 30 | } 31 | 32 | /** 33 | * 设置关闭忽略Http配置模式 34 | */ 35 | @Test 36 | public void notIgnoreHttpConfigTest(){ 37 | LinkedHashMap newHeaders = new LinkedHashMap<>(); 38 | newHeaders.put("aaa","overwrite"); 39 | newHeaders.put("bbb","bbb"); 40 | HttpConfigManager.setCustomHttpHeaderConfig(newHeaders,true); 41 | 42 | LinkedHashMap headers = new LinkedHashMap<>(); 43 | headers.put("aaa","aaa"); 44 | headers.put("ccc","ccc"); 45 | RequestBuilder requestBuilder = Requests.get("http://woodpecker.gv7.me/index").headers(headers).ignoreHttpConfig(false); 46 | requestBuilder.build(); 47 | 48 | assertEquals("overwrite",requestBuilder.getHeader("aaa")); 49 | assertEquals("bbb",requestBuilder.getHeader("bbb")); 50 | assertEquals("ccc",requestBuilder.getHeader("ccc")); // 测试是否用户未被覆盖的header否否保持 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/test/java/me/gv7/woodpecker/requests/MultiPartTest.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests; 2 | 3 | import me.gv7.woodpecker.requests.body.Part; 4 | import org.junit.Test; 5 | 6 | import java.io.File; 7 | 8 | public class MultiPartTest { 9 | 10 | @Test 11 | public void testOf() throws Exception { 12 | Part multiPart = Part.file("writeTo", new File("MultiPartTest.java")); 13 | } 14 | } -------------------------------------------------------------------------------- /src/test/java/me/gv7/woodpecker/requests/RequestsProxyTest.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests; 2 | 3 | import me.gv7.woodpecker.requests.config.HttpConfigManager; 4 | import me.gv7.woodpecker.requests.mock.MockServer; 5 | import org.junit.AfterClass; 6 | import org.junit.BeforeClass; 7 | import org.junit.Ignore; 8 | import org.junit.Test; 9 | 10 | import java.net.Proxy; 11 | 12 | import static org.junit.Assert.assertEquals; 13 | 14 | /** 15 | * @author Liu Dong 16 | */ 17 | @Ignore 18 | public class RequestsProxyTest { 19 | 20 | private static MockServer server = new MockServer(); 21 | 22 | @BeforeClass 23 | public static void init() { 24 | server.start(); 25 | } 26 | 27 | @AfterClass 28 | public static void destroy() { 29 | server.stop(); 30 | } 31 | 32 | @Test 33 | public void testHttpProxy() { 34 | // http proxy with redirect 35 | RawResponse response = Requests 36 | .get("https://www.google.com") 37 | .proxy(Proxies.httpProxy("127.0.0.1", 1081)) 38 | .send(); 39 | response.close(); 40 | assertEquals(200, response.statusCode()); 41 | } 42 | 43 | @Test 44 | public void testSocksProxy() { 45 | // socks proxy with redirect 46 | RawResponse response = Requests 47 | .get("https://www.google.com") 48 | .proxy(Proxies.socksProxy("127.0.0.1", 1080)) 49 | .send(); 50 | response.close(); 51 | assertEquals(200, response.statusCode()); 52 | } 53 | 54 | @Test 55 | public void testHttpProxy1() throws Exception { 56 | HttpConfigManager.setProxyConfig(true 57 | ,"http" 58 | ,"127.0.0.1" 59 | ,8080 60 | ,null 61 | ,null); 62 | RawResponse response = Requests.method("GET","http://wwww.baidu.com/xxx").proxy(Proxy.NO_PROXY).send(); 63 | System.out.println(response.readToText()); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/test/java/me/gv7/woodpecker/requests/RequestsTest.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests; 2 | 3 | import net.dongliu.commons.collection.Lists; 4 | import me.gv7.woodpecker.requests.body.InputStreamSupplier; 5 | import me.gv7.woodpecker.requests.body.Part; 6 | import me.gv7.woodpecker.requests.json.TypeInfer; 7 | import me.gv7.woodpecker.requests.mock.MockServer; 8 | import org.junit.AfterClass; 9 | import org.junit.BeforeClass; 10 | import org.junit.Test; 11 | 12 | import java.io.InputStream; 13 | import java.nio.charset.StandardCharsets; 14 | import java.security.KeyStore; 15 | import java.util.HashMap; 16 | import java.util.List; 17 | import java.util.Map; 18 | 19 | import static org.junit.Assert.*; 20 | 21 | public class RequestsTest { 22 | 23 | private static MockServer server = new MockServer(); 24 | 25 | @BeforeClass 26 | public static void init() { 27 | server.start(); 28 | } 29 | 30 | @AfterClass 31 | public static void destroy() { 32 | server.stop(); 33 | } 34 | 35 | @Test 36 | public void testGet() { 37 | String resp = Requests.get("http://127.0.0.1:8080") 38 | .requestCharset(StandardCharsets.UTF_8).send().readToText(); 39 | assertFalse(resp.isEmpty()); 40 | 41 | resp = Requests.get("http://127.0.0.1:8080").send().readToText(); 42 | assertFalse(resp.isEmpty()); 43 | 44 | // get with params 45 | Map map = new HashMap<>(); 46 | map.put("wd", "test"); 47 | resp = Requests.get("http://127.0.0.1:8080").params(map).send().readToText(); 48 | assertFalse(resp.isEmpty()); 49 | assertTrue(resp.contains("wd=test")); 50 | } 51 | 52 | @Test 53 | public void testHead() { 54 | RawResponse resp = Requests.head("http://127.0.0.1:8080") 55 | .requestCharset(StandardCharsets.UTF_8).send(); 56 | assertEquals(200, resp.statusCode()); 57 | String statusLine = resp.statusLine(); 58 | assertEquals("HTTP/1.1 200 OK", statusLine); 59 | String text = resp.readToText(); 60 | assertTrue(text.isEmpty()); 61 | } 62 | 63 | @Test 64 | public void testPost() { 65 | // form encoded post 66 | String text = Requests.post("http://127.0.0.1:8080/post") 67 | .body(Parameter.of("wd", "test")) 68 | .send().readToText(); 69 | assertTrue(text.contains("wd=test")); 70 | } 71 | 72 | @Test 73 | public void testCookie() { 74 | Response response = Requests.get("http://127.0.0.1:8080/cookie") 75 | .cookies(Parameter.of("test", "value")).send().toTextResponse(); 76 | boolean flag = false; 77 | for (Cookie cookie : response.cookies()) { 78 | if (cookie.name().equals("test")) { 79 | flag = true; 80 | break; 81 | } 82 | } 83 | assertTrue(flag); 84 | } 85 | 86 | @Test 87 | public void testBasicAuth() { 88 | Response response = Requests.get("http://127.0.0.1:8080/basicAuth") 89 | .basicAuth("test", "password") 90 | .send().toTextResponse(); 91 | assertEquals(200, response.statusCode()); 92 | } 93 | 94 | @Test 95 | public void testRedirect() { 96 | Response resp = Requests.get("http://127.0.0.1:8080/redirect").userAgent("my-user-agent") 97 | .send().toTextResponse(); 98 | assertEquals(200, resp.statusCode()); 99 | assertTrue(resp.body().contains("/redirected")); 100 | assertTrue(resp.body().contains("my-user-agent")); 101 | } 102 | 103 | @Test 104 | public void testMultiPart() { 105 | String body = Requests.post("http://127.0.0.1:8080/multi_part") 106 | .multiPartBody(Part.file("writeTo", "keystore", new InputStreamSupplier() { 107 | @Override 108 | public InputStream get() { 109 | return this.getClass().getResourceAsStream("/keystore"); 110 | } 111 | }).contentType("application/octem-stream")) 112 | .send().readToText(); 113 | assertTrue(body.contains("writeTo")); 114 | assertTrue(body.contains("application/octem-stream")); 115 | } 116 | 117 | 118 | @Test 119 | public void testMultiPartText() { 120 | String body = Requests.post("http://127.0.0.1:8080/multi_part") 121 | .multiPartBody(Part.text("test", "this is test value")) 122 | .send().readToText(); 123 | assertTrue(body.contains("this is test value")); 124 | assertTrue(!body.contains("plain/text")); 125 | } 126 | 127 | @Test 128 | public void sendJson() { 129 | String text = Requests.post("http://127.0.0.1:8080/echo_body").jsonBody(Lists.of(1, 2, 3)) 130 | .send().readToText(); 131 | assertTrue(text.startsWith("[")); 132 | assertTrue(text.endsWith("]")); 133 | } 134 | 135 | @Test 136 | public void receiveJson() { 137 | List list = Requests.post("http://127.0.0.1:8080/echo_body").jsonBody(Lists.of(1, 2, 3)) 138 | .send().readToJson(new TypeInfer>() { 139 | }); 140 | assertEquals(3, list.size()); 141 | } 142 | 143 | @Test 144 | public void sendHeaders() { 145 | RequestBuilder.DEBUG = true; 146 | String text = Requests.get("http://gv7.me/echo_header") 147 | .headers(new Header("Host", "www.test.com"), new Header("TestHeader", 1)) 148 | .proxy(Proxies.httpProxy("127.0.0.1",8080)) 149 | .send().readToText(); 150 | assertTrue(text.contains("Host: www.test.com")); 151 | assertTrue(text.contains("TestHeader: 1")); 152 | } 153 | 154 | @Test 155 | public void sendHeaders2() { 156 | RequestBuilder.DEBUG = true; 157 | Map headers = new HashMap(); 158 | headers.put("TestHeader","1"); 159 | String text = Requests.get("http://gv7.me/echo_header") 160 | .headers(headers) 161 | .proxy(Proxies.httpProxy("127.0.0.1",8080)) 162 | .send().readToText(); 163 | assertTrue(text.contains("Host: www.test.com")); 164 | assertTrue(text.contains("TestHeader: 1")); 165 | } 166 | 167 | @Test 168 | public void testHttps() { 169 | Response response = Requests.get("https://127.0.0.1:8443/https") 170 | .verify(false).send().toTextResponse(); 171 | assertEquals(200, response.statusCode()); 172 | 173 | 174 | KeyStore keyStore = KeyStores.load(this.getClass().getResourceAsStream("/keystore"), "123456".toCharArray()); 175 | response = Requests.get("https://127.0.0.1:8443/https") 176 | .keyStore(keyStore) 177 | .send().toTextResponse(); 178 | assertEquals(200, response.statusCode()); 179 | } 180 | 181 | @Test 182 | public void testInterceptor() { 183 | final long[] statusCode = {0}; 184 | Interceptor interceptor = (target, request) -> { 185 | RawResponse response = target.proceed(request); 186 | statusCode[0] = response.statusCode(); 187 | return response; 188 | }; 189 | 190 | String text = Requests.get("http://127.0.0.1:8080/echo_header") 191 | .interceptors(interceptor) 192 | .send().readToText(); 193 | assertFalse(text.isEmpty()); 194 | assertTrue(statusCode[0] > 0); 195 | } 196 | } -------------------------------------------------------------------------------- /src/test/java/me/gv7/woodpecker/requests/SessionTest.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests; 2 | 3 | import me.gv7.woodpecker.requests.mock.MockServer; 4 | import org.junit.AfterClass; 5 | import org.junit.BeforeClass; 6 | import org.junit.Test; 7 | 8 | import static org.junit.Assert.assertEquals; 9 | import static org.junit.Assert.assertTrue; 10 | 11 | public class SessionTest { 12 | 13 | private static MockServer server = new MockServer(); 14 | 15 | @BeforeClass 16 | public static void init() { 17 | server.start(); 18 | } 19 | 20 | @AfterClass 21 | public static void destroy() { 22 | server.stop(); 23 | } 24 | 25 | @Test 26 | public void testSession() { 27 | Session session = Requests.session(); 28 | String response = session.get("http://127.0.0.1:8080").send().readToText(); 29 | assertTrue(!response.isEmpty()); 30 | } 31 | } -------------------------------------------------------------------------------- /src/test/java/me/gv7/woodpecker/requests/body/RequestBodyTest.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.body; 2 | 3 | import net.dongliu.commons.collection.Lists; 4 | import org.junit.Test; 5 | 6 | import java.io.ByteArrayOutputStream; 7 | import java.nio.charset.StandardCharsets; 8 | import java.util.List; 9 | 10 | import static org.junit.Assert.assertEquals; 11 | 12 | public class RequestBodyTest { 13 | @Test 14 | public void json() throws Exception { 15 | RequestBody> body = RequestBody.json(Lists.of("1", "2", "3")); 16 | assertEquals("application/json", body.contentType()); 17 | 18 | ByteArrayOutputStream bos = new ByteArrayOutputStream(); 19 | body.writeBody(bos, StandardCharsets.UTF_8); 20 | String str = bos.toString("UTF-8"); 21 | assertEquals("[\"1\",\"2\",\"3\"]", str); 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /src/test/java/me/gv7/woodpecker/requests/executor/DefaultCookieJarTest.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.executor; 2 | 3 | import net.dongliu.commons.collection.Lists; 4 | import net.dongliu.commons.collection.Sets; 5 | import me.gv7.woodpecker.requests.Cookie; 6 | import org.junit.Test; 7 | 8 | import java.net.URL; 9 | import java.util.HashSet; 10 | import java.util.List; 11 | import java.util.concurrent.TimeUnit; 12 | 13 | import static org.junit.Assert.assertEquals; 14 | 15 | public class DefaultCookieJarTest { 16 | @Test 17 | public void storeCookies() { 18 | long now = System.currentTimeMillis(); 19 | long oneHour = TimeUnit.HOURS.toMillis(1); 20 | CookieJar cookieJar = new DefaultCookieJar(); 21 | List cookies = Lists.of( 22 | new Cookie("test.com", "/", "test", "value", 0, false, false), 23 | new Cookie("test.com", "/test/", "test", "value1", 0, false, false), 24 | new Cookie("test.com", "/test/", "test1", "value1", 0, false, false), 25 | new Cookie("test1.com", "/", "test2", "value2", 0, false, false) 26 | ); 27 | cookieJar.storeCookies(cookies); 28 | assertEquals(new HashSet<>(cookies), new HashSet<>(cookieJar.getCookies())); 29 | 30 | List cookies1 = Lists.of( 31 | new Cookie("test.com", "/test/", "test1", "value1", now - oneHour, false, false), 32 | new Cookie("test1.com", "/", "test2", "value2", now + oneHour, false, false) 33 | ); 34 | cookieJar.storeCookies(cookies1); 35 | assertEquals(Sets.of( 36 | new Cookie("test.com", "/", "test", "value", 0, false, false), 37 | new Cookie("test.com", "/test/", "test", "value1", 0, false, false), 38 | new Cookie("test1.com", "/", "test2", "value2", now + oneHour, false, false) 39 | ), new HashSet<>(cookieJar.getCookies())); 40 | } 41 | 42 | @Test 43 | public void getCookies() throws Exception { 44 | CookieJar cookieJar = new DefaultCookieJar(); 45 | List cookies = Lists.of( 46 | new Cookie("test.com", "/", "test", "value", 0, false, false), 47 | new Cookie("test.com", "/test/", "test", "value1", 0, false, false), 48 | new Cookie("test.com", "/test/", "test1", "value1", 0, false, false), 49 | new Cookie("test1.com", "/", "test2", "value2", 0, false, false) 50 | ); 51 | cookieJar.storeCookies(cookies); 52 | 53 | URL url = new URL("http://www.test.com/test/"); 54 | List matched = cookieJar.getCookies(url); 55 | assertEquals(3, matched.size()); 56 | assertEquals(Sets.of( 57 | new Cookie("test.com", "/test/", "test", "value1", 0, false, false), 58 | new Cookie("test.com", "/test/", "test1", "value1", 0, false, false) 59 | ), new HashSet<>(matched.subList(0, 2))); 60 | assertEquals(new Cookie("test.com", "/", "test", "value", 0, false, false), matched.get(2)); 61 | } 62 | } -------------------------------------------------------------------------------- /src/test/java/me/gv7/woodpecker/requests/json/FastJsonProcessorTest.java: -------------------------------------------------------------------------------- 1 | //package me.gv7.woodpecker.requests.json; 2 | // 3 | //import org.junit.Test; 4 | // 5 | //import java.io.ByteArrayInputStream; 6 | //import java.io.StringWriter; 7 | //import java.nio.charset.StandardCharsets; 8 | // 9 | //import static org.junit.Assert.assertEquals; 10 | // 11 | ///** 12 | // * @author Liu Dong 13 | // */ 14 | //public class FastJsonProcessorTest { 15 | // @Test 16 | // public void marshal() throws Exception { 17 | // FastJsonProcessor jsonProvider = new FastJsonProcessor(); 18 | // try (StringWriter writer = new StringWriter()) { 19 | // jsonProvider.marshal(writer, "test"); 20 | // String s = writer.toString(); 21 | // assertEquals("\"test\"", s); 22 | // } 23 | // } 24 | // 25 | // @Test 26 | // public void unmarshal() throws Exception { 27 | // FastJsonProcessor jsonProvider = new FastJsonProcessor(); 28 | // String str = jsonProvider.unmarshal(new ByteArrayInputStream("\"test\"".getBytes()), StandardCharsets.UTF_8, 29 | // String.class); 30 | // assertEquals("test", str); 31 | // } 32 | // 33 | //} -------------------------------------------------------------------------------- /src/test/java/me/gv7/woodpecker/requests/json/JsonLookupTest.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.json; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * @author Liu Dong 9 | */ 10 | public class JsonLookupTest { 11 | 12 | @Test 13 | public void test() { 14 | JsonLookup lookup = JsonLookup.getInstance(); 15 | assertTrue(lookup.hasFastJson()); 16 | assertTrue(lookup.hasGson()); 17 | assertTrue(lookup.hasJackson()); 18 | } 19 | } -------------------------------------------------------------------------------- /src/test/java/me/gv7/woodpecker/requests/mock/EchoBodyServlet.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.mock; 2 | 3 | 4 | import net.dongliu.commons.io.Readers; 5 | 6 | import javax.servlet.ServletException; 7 | import javax.servlet.http.HttpServlet; 8 | import javax.servlet.http.HttpServletRequest; 9 | import javax.servlet.http.HttpServletResponse; 10 | import java.io.IOException; 11 | import java.io.PrintWriter; 12 | 13 | /** 14 | * @author Liu Dong {@literal } 15 | */ 16 | public class EchoBodyServlet extends HttpServlet { 17 | 18 | @Override 19 | protected void doPost(HttpServletRequest request, HttpServletResponse response) 20 | throws ServletException, IOException { 21 | String body = Readers.readAll(request.getReader()); 22 | 23 | response.setContentType("text/plain"); 24 | response.setCharacterEncoding(request.getCharacterEncoding()); 25 | PrintWriter out = response.getWriter(); 26 | out.write(body); 27 | out.flush(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/test/java/me/gv7/woodpecker/requests/mock/EchoHeaderServlet.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.mock; 2 | 3 | import javax.servlet.ServletException; 4 | import javax.servlet.http.HttpServlet; 5 | import javax.servlet.http.HttpServletRequest; 6 | import javax.servlet.http.HttpServletResponse; 7 | import java.io.IOException; 8 | import java.io.PrintWriter; 9 | import java.util.Enumeration; 10 | 11 | /** 12 | * @author Liu Dong {@literal } 13 | */ 14 | public class EchoHeaderServlet extends HttpServlet { 15 | 16 | @Override 17 | protected void doGet(HttpServletRequest request, HttpServletResponse response) 18 | throws ServletException, IOException { 19 | StringBuilder sb = new StringBuilder(); 20 | Enumeration headerNames = request.getHeaderNames(); 21 | while (headerNames.hasMoreElements()) { 22 | String headerName = headerNames.nextElement(); 23 | String header = request.getHeader(headerName); 24 | sb.append(headerName).append(": ").append(header).append("\n"); 25 | } 26 | String headers = sb.toString(); 27 | 28 | response.setContentType("text/plain"); 29 | response.setCharacterEncoding("UTF-8"); 30 | PrintWriter out = response.getWriter(); 31 | out.write(headers); 32 | out.flush(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/test/java/me/gv7/woodpecker/requests/mock/MockBasicAuthenticationServlet.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.mock; 2 | 3 | import javax.servlet.ServletException; 4 | import javax.servlet.http.HttpServlet; 5 | import javax.servlet.http.HttpServletRequest; 6 | import javax.servlet.http.HttpServletResponse; 7 | import java.io.IOException; 8 | import java.io.PrintWriter; 9 | import java.util.Base64; 10 | 11 | /** 12 | * @author Liu Dong {@literal } 13 | */ 14 | public class MockBasicAuthenticationServlet extends HttpServlet { 15 | 16 | private static final long serialVersionUID = 1567638593606667604L; 17 | 18 | public void doGet(HttpServletRequest req, HttpServletResponse res) 19 | throws ServletException, IOException { 20 | 21 | PrintWriter out = res.getWriter(); 22 | String auth = req.getHeader("Authorization"); 23 | if (!allowUser(auth)) { 24 | // Not allowed, so report he's unauthorized 25 | res.setHeader("WWW-Authenticate", "BASIC realm=\"test basic auth\""); 26 | res.sendError(HttpServletResponse.SC_UNAUTHORIZED); 27 | // Could offer to add him to the allowed user list 28 | } else { 29 | // Allowed, so show him the secret stuff 30 | out.println(req.getRequestURI()); 31 | } 32 | } 33 | 34 | private boolean allowUser(String auth) { 35 | 36 | if (auth == null) { 37 | return false; 38 | } 39 | if (!auth.toUpperCase().startsWith("BASIC ")) { 40 | return false; 41 | } 42 | String encodedToken = auth.substring(6); 43 | String token = new String(Base64.getDecoder().decode(encodedToken)); 44 | 45 | return "test:password".equals(token); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/test/java/me/gv7/woodpecker/requests/mock/MockGetServlet.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.mock; 2 | 3 | import javax.servlet.ServletException; 4 | import javax.servlet.http.Cookie; 5 | import javax.servlet.http.HttpServlet; 6 | import javax.servlet.http.HttpServletRequest; 7 | import javax.servlet.http.HttpServletResponse; 8 | import java.io.IOException; 9 | import java.io.PrintWriter; 10 | import java.util.Enumeration; 11 | 12 | /** 13 | * @author Liu Dong {@literal } 14 | */ 15 | public class MockGetServlet extends HttpServlet { 16 | 17 | @Override 18 | protected void doGet(HttpServletRequest request, HttpServletResponse response) 19 | throws ServletException, IOException { 20 | String uri = request.getRequestURI(); 21 | String queryStr = request.getQueryString(); 22 | 23 | StringBuilder sb = new StringBuilder(); 24 | Enumeration headerNames = request.getHeaderNames(); 25 | while (headerNames.hasMoreElements()) { 26 | String headerName = headerNames.nextElement(); 27 | sb.append(headerName).append('='); 28 | Enumeration headerValues = request.getHeaders(headerName); 29 | while (headerValues.hasMoreElements()) { 30 | String headerValue = headerValues.nextElement(); 31 | sb.append(headerValue).append(';'); 32 | } 33 | sb.deleteCharAt(sb.length() - 1); 34 | sb.append('\n'); 35 | } 36 | sb.deleteCharAt(sb.length() - 1); 37 | String headers = sb.toString(); 38 | 39 | response.setContentType("text/plain"); 40 | response.setCharacterEncoding("UTF-8"); 41 | // cookie 42 | Cookie[] cookies = request.getCookies(); 43 | if (cookies != null) { 44 | for (Cookie cookie : cookies) { 45 | response.addCookie(cookie); 46 | } 47 | } 48 | 49 | PrintWriter out = response.getWriter(); 50 | 51 | switch (uri) { 52 | case "/redirect": 53 | response.sendRedirect("/redirected"); 54 | break; 55 | default: 56 | out.println(uri); 57 | out.println(queryStr); 58 | out.println(sb.toString()); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/test/java/me/gv7/woodpecker/requests/mock/MockMultiPartServlet.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.mock; 2 | 3 | import net.dongliu.commons.io.InputStreams; 4 | import org.eclipse.jetty.server.Request; 5 | 6 | import javax.servlet.MultipartConfigElement; 7 | import javax.servlet.ServletException; 8 | import javax.servlet.http.HttpServlet; 9 | import javax.servlet.http.HttpServletRequest; 10 | import javax.servlet.http.HttpServletResponse; 11 | import javax.servlet.http.Part; 12 | import java.io.IOException; 13 | import java.io.InputStream; 14 | import java.io.OutputStream; 15 | import java.nio.charset.StandardCharsets; 16 | import java.util.Collection; 17 | 18 | /** 19 | * @author Liu Dong {@literal } 20 | */ 21 | public class MockMultiPartServlet extends HttpServlet { 22 | 23 | private static final MultipartConfigElement MULTI_PART_CONFIG = new MultipartConfigElement(System.getProperty 24 | ("java.io.tmpdir")); 25 | 26 | @Override 27 | protected void doPost(HttpServletRequest request, HttpServletResponse response) 28 | throws ServletException, IOException { 29 | request.setAttribute(Request.__MULTIPART_CONFIG_ELEMENT, MULTI_PART_CONFIG); 30 | 31 | Collection parts = request.getParts(); 32 | 33 | response.setContentType("text/plain"); 34 | response.setCharacterEncoding("UTF-8"); 35 | response.setStatus(HttpServletResponse.SC_OK); 36 | OutputStream out = response.getOutputStream(); 37 | for (Part part : parts) { 38 | out.write(part.getName().getBytes(StandardCharsets.UTF_8)); 39 | out.write('\n'); 40 | if (part.getContentType() != null) { 41 | out.write(part.getContentType().getBytes(StandardCharsets.UTF_8)); 42 | out.write('\n'); 43 | } 44 | try (InputStream in = part.getInputStream()) { 45 | InputStreams.transferTo(in, out); 46 | } 47 | out.write('\n'); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/test/java/me/gv7/woodpecker/requests/mock/MockPostServlet.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.mock; 2 | 3 | import javax.servlet.ServletException; 4 | import javax.servlet.http.HttpServlet; 5 | import javax.servlet.http.HttpServletRequest; 6 | import javax.servlet.http.HttpServletResponse; 7 | import java.io.IOException; 8 | import java.io.PrintWriter; 9 | import java.util.Map; 10 | 11 | /** 12 | * @author Liu Dong {@literal } 13 | */ 14 | public class MockPostServlet extends HttpServlet { 15 | 16 | @Override 17 | protected void doPost(HttpServletRequest request, HttpServletResponse response) 18 | throws ServletException, IOException { 19 | String uri = request.getRequestURI(); 20 | Map params = request.getParameterMap(); 21 | 22 | response.setContentType("text/plain"); 23 | response.setCharacterEncoding("UTF-8"); 24 | response.setStatus(HttpServletResponse.SC_OK); 25 | PrintWriter out = response.getWriter(); 26 | out.println(uri); 27 | for (Map.Entry entry : params.entrySet()) { 28 | out.println(entry.getKey() + "=" + entry.getValue()[0]); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/test/java/me/gv7/woodpecker/requests/mock/MockServer.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.mock; 2 | 3 | import org.eclipse.jetty.http.HttpVersion; 4 | import org.eclipse.jetty.server.*; 5 | import org.eclipse.jetty.servlet.ServletHandler; 6 | import org.eclipse.jetty.util.ssl.SslContextFactory; 7 | 8 | /** 9 | * @author Liu Dong {@literal } 10 | */ 11 | public class MockServer { 12 | 13 | private Server server; 14 | 15 | public void start() { 16 | server = new Server(); 17 | 18 | HttpConfiguration http_config = new HttpConfiguration(); 19 | http_config.setSecureScheme("https"); 20 | http_config.setSecurePort(8443); 21 | http_config.setOutputBufferSize(32768); 22 | 23 | ServerConnector http = new ServerConnector(server, new HttpConnectionFactory(http_config)); 24 | http.setPort(8080); 25 | http.setIdleTimeout(30000); 26 | 27 | SslContextFactory sslContextFactory = new SslContextFactory(); 28 | sslContextFactory.setKeyStorePath(this.getClass().getResource("/keystore").toExternalForm()); 29 | sslContextFactory.setKeyStorePassword("123456"); 30 | sslContextFactory.setKeyManagerPassword("123456"); 31 | 32 | HttpConfiguration httpsConfig = new HttpConfiguration(http_config); 33 | httpsConfig.addCustomizer(new SecureRequestCustomizer()); 34 | 35 | ServerConnector https = new ServerConnector(server, 36 | new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString()), 37 | new HttpConnectionFactory(httpsConfig)); 38 | https.setPort(8443); 39 | https.setIdleTimeout(500000); 40 | 41 | server.setConnectors(new Connector[]{http, https}); 42 | 43 | ServletHandler handler = new ServletHandler(); 44 | server.setHandler(handler); 45 | handler.addServletWithMapping(MockGetServlet.class, "/*"); 46 | handler.addServletWithMapping(MockPostServlet.class, "/post"); 47 | handler.addServletWithMapping(MockBasicAuthenticationServlet.class, "/basicAuth"); 48 | handler.addServletWithMapping(MockMultiPartServlet.class, "/multi_part"); 49 | handler.addServletWithMapping(EchoBodyServlet.class, "/echo_body"); 50 | handler.addServletWithMapping(EchoHeaderServlet.class, "/echo_header"); 51 | 52 | // Start things up! 53 | try { 54 | server.start(); 55 | } catch (Exception e) { 56 | throw new RuntimeException(e); 57 | } 58 | 59 | } 60 | 61 | public void join() { 62 | try { 63 | server.join(); 64 | } catch (InterruptedException ignore) { 65 | } 66 | } 67 | 68 | public void stop() { 69 | try { 70 | server.stop(); 71 | } catch (Exception ignore) { 72 | } 73 | } 74 | 75 | public static void main(String[] args) { 76 | MockServer server = new MockServer(); 77 | server.start(); 78 | server.join(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/test/java/me/gv7/woodpecker/requests/util/CookiesTest.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.util; 2 | 3 | import me.gv7.woodpecker.requests.Cookie; 4 | import me.gv7.woodpecker.requests.utils.Cookies; 5 | import org.junit.Test; 6 | 7 | import static org.junit.Assert.*; 8 | 9 | /** 10 | * @author Liu Dong 11 | */ 12 | public class CookiesTest { 13 | @Test 14 | public void isSubDomain() throws Exception { 15 | assertTrue(Cookies.isDomainSuffix("www.baidu.com", "baidu.com")); 16 | assertTrue(Cookies.isDomainSuffix("baidu.com", "baidu.com")); 17 | assertFalse(Cookies.isDomainSuffix("a.com", "baidu.com")); 18 | assertFalse(Cookies.isDomainSuffix("ww.a.com", "baidu.com")); 19 | } 20 | 21 | @Test 22 | public void effectivePath() { 23 | assertEquals("/test/", Cookies.calculatePath("/test/123")); 24 | assertEquals("/", Cookies.calculatePath("/")); 25 | assertEquals("/", Cookies.calculatePath("")); 26 | } 27 | 28 | @Test 29 | public void isIP() { 30 | assertTrue(Cookies.isIP("202.38.64.10")); 31 | assertTrue(Cookies.isIP("2001:0DB8:0000:0023:0008:0800:200C:417A")); 32 | assertTrue(Cookies.isIP("2001:DB8:0:23:8:800:200C:417A")); 33 | assertTrue(Cookies.isIP("FF01:0:0:0:0:0:0:1101")); 34 | assertTrue(Cookies.isIP("::1")); 35 | assertTrue(Cookies.isIP("::")); 36 | assertTrue(Cookies.isIP("::192.168.0.1")); 37 | assertTrue(Cookies.isIP("::FFFF:192.168.0.1")); 38 | assertFalse(Cookies.isIP("202.test.com")); 39 | assertFalse(Cookies.isIP("163.com")); 40 | assertFalse(Cookies.isIP("163.ac")); 41 | } 42 | 43 | @Test 44 | public void match() throws Exception { 45 | Cookie cookie = new Cookie("test.com", "/", "test", "value", 0, false, false); 46 | assertTrue(Cookies.match(cookie, "http", "test.com", "/test/")); 47 | assertTrue(Cookies.match(cookie, "http", "www.test.com", "/test/")); 48 | 49 | // https 50 | cookie = new Cookie("test.com", "/", "test", "value", 0, true, false); 51 | assertFalse(Cookies.match(cookie, "http", "test.com", "/test/")); 52 | assertTrue(Cookies.match(cookie, "https", "test.com", "/test/")); 53 | 54 | cookie = new Cookie("test.com", "/", "test", "value", 0, false, true); 55 | assertTrue(Cookies.match(cookie, "http", "test.com", "/test/")); 56 | assertFalse(Cookies.match(cookie, "http", "www.test.com", "/test/")); 57 | 58 | cookie = new Cookie("test.com", "/test/", "test", "value", 0, false, false); 59 | assertFalse(Cookies.match(cookie, "http", "test.com", "/")); 60 | assertTrue(Cookies.match(cookie, "http", "www.test.com", "/test/")); 61 | 62 | cookie = new Cookie("202.38.64.10", "/", "test", "value", 0, false, false); 63 | assertFalse(Cookies.match(cookie, "http", "38.64.10", "/")); 64 | assertTrue(Cookies.match(cookie, "http", "202.38.64.10", "/")); 65 | } 66 | 67 | @Test 68 | public void parseCookie() throws Exception { 69 | String cookieStr = "__bsi=11937048251853133038_00_0_I_R_181_0303_C02F_N_I_I_0;" + 70 | " expires=Thu, 16-Mar-17 03:39:29 GMT; domain=www.baidu.com; path=/"; 71 | Cookie cookie = Cookies.parseCookie(cookieStr, "www.baidu.com", "/"); 72 | assertNotNull(cookie); 73 | assertFalse(cookie.hostOnly()); 74 | assertEquals("www.baidu.com", cookie.domain()); 75 | assertEquals(1489635569000L, cookie.expiry()); 76 | assertEquals("11937048251853133038_00_0_I_R_181_0303_C02F_N_I_I_0", cookie.value()); 77 | assertEquals("/", cookie.path()); 78 | assertEquals("__bsi", cookie.name()); 79 | 80 | 81 | cookieStr = "V2EX_TAB=\"2|1:0|10:1489639030|8:V2EX_TAB|4:YWxs|94149dfee574a182c7a43cbcb752230e9e09ca44173293ca6ab446e9e1754598\";" + 82 | " expires=Thu, 30 Mar 2017 04:37:10 GMT; httponly; Path=/"; 83 | cookie = Cookies.parseCookie(cookieStr, "www.v2ex.com", "/"); 84 | assertNotNull(cookie); 85 | assertEquals("www.v2ex.com", cookie.domain()); 86 | assertTrue(cookie.hostOnly()); 87 | assertEquals(1490848630000L, cookie.expiry()); 88 | assertEquals("/", cookie.path()); 89 | assertEquals("V2EX_TAB", cookie.name()); 90 | 91 | cookieStr = "YF-V5-G0=a2489c19ecf98bbe86a7bf6f0edcb071;Path=/"; 92 | cookie = Cookies.parseCookie(cookieStr, "weibo.com", "/"); 93 | assertNotNull(cookie); 94 | assertTrue(cookie.hostOnly()); 95 | assertEquals("weibo.com", cookie.domain()); 96 | assertEquals(0, cookie.expiry()); 97 | 98 | cookieStr = "ALF=1521175171; expires=Friday, 16-Mar-2018 04:39:31 GMT; path=/; domain=.sina.com.cn"; 99 | cookie = Cookies.parseCookie(cookieStr, "login.sina.com.cn", "/sso/"); 100 | assertNotNull(cookie); 101 | assertEquals("sina.com.cn", cookie.domain()); 102 | assertEquals(1521175171000L, cookie.expiry()); 103 | assertEquals("/", cookie.path()); 104 | 105 | 106 | cookie = Cookies.parseCookie("skey=@k4bPcIye6; PATH=/; DOMAIN=qq.com;", "ssl.ptlogin2.qq.com", "/"); 107 | assertNotNull(cookie); 108 | assertEquals("qq.com", cookie.domain()); 109 | } 110 | } -------------------------------------------------------------------------------- /src/test/java/me/gv7/woodpecker/requests/util/URLUtilsTest.java: -------------------------------------------------------------------------------- 1 | package me.gv7.woodpecker.requests.util; 2 | 3 | import net.dongliu.commons.collection.Lists; 4 | import me.gv7.woodpecker.requests.Parameter; 5 | import me.gv7.woodpecker.requests.utils.URLUtils; 6 | import org.junit.Test; 7 | 8 | import java.net.URL; 9 | import java.util.List; 10 | 11 | import static java.nio.charset.StandardCharsets.UTF_8; 12 | import static org.junit.Assert.assertEquals; 13 | 14 | public class URLUtilsTest { 15 | @Test 16 | public void joinUrl() throws Exception { 17 | List> empty = Lists.of(); 18 | assertEquals("http://www.test.com/", URLUtils.joinUrl(new URL("http://www.test.com/"), 19 | empty, UTF_8).toExternalForm()); 20 | assertEquals("http://www.test.com/path", URLUtils.joinUrl(new URL("http://www.test.com/path"), 21 | empty, UTF_8).toExternalForm()); 22 | 23 | assertEquals("http://www.test.com/path?t=v", URLUtils.joinUrl(new URL("http://www.test.com/path"), 24 | Lists.of(Parameter.of("t", "v")), UTF_8).toExternalForm()); 25 | assertEquals("http://www.test.com/path?s=t&t=v", URLUtils.joinUrl(new URL("http://www.test.com/path?s=t"), 26 | Lists.of(Parameter.of("t", "v")), UTF_8).toExternalForm()); 27 | assertEquals("http://www.test.com/path?t=v", URLUtils.joinUrl(new URL("http://www.test.com/path?"), 28 | Lists.of(Parameter.of("t", "v")), UTF_8).toExternalForm()); 29 | assertEquals("http://www.test.com/path?t=v#seg", URLUtils.joinUrl(new URL("http://www.test.com/path#seg"), 30 | Lists.of(Parameter.of("t", "v")), UTF_8).toExternalForm()); 31 | assertEquals("http://www.test.com/path?t=v#", URLUtils.joinUrl(new URL("http://www.test.com/path#"), 32 | Lists.of(Parameter.of("t", "v")), UTF_8).toExternalForm()); 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /src/test/resources/jetty-logging.properties: -------------------------------------------------------------------------------- 1 | org.eclipse.jetty.LEVEL=WARN -------------------------------------------------------------------------------- /src/test/resources/keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woodpecker-framework/woodpecker-requests/802997f1e68edb4d9eb9ea8ec29c8966d8cca77c/src/test/resources/keystore --------------------------------------------------------------------------------