jruby-http-kit 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +72 -0
- data/Rakefile +1 -0
- data/example/config.ru +9 -0
- data/example/hello_world.rb +17 -0
- data/example/rack.rb +11 -0
- data/example/ring.rb +31 -0
- data/example/sinatra/app.rb +9 -0
- data/example/sinatra/config.ru +8 -0
- data/example/webmachine/Gemfile +4 -0
- data/example/webmachine/Gemfile.lock +16 -0
- data/example/webmachine/app.rb +37 -0
- data/jruby-http-kit.gemspec +25 -0
- data/lib/http_kit/rack_handler.rb +139 -0
- data/lib/http_kit/server.rb +62 -0
- data/lib/http_kit/version.rb +3 -0
- data/lib/http_kit.rb +6 -0
- data/lib/java/clojure-1.5.1.jar +0 -0
- data/lib/java/http-kit.jar +0 -0
- data/lib/rack/handler/http_kit.rb +3 -0
- data/lib/rack/http_kit.rb +3 -0
- data/lib/webmachine/adapters/ring.rb +135 -0
- data/spec/spec_helper.rb +24 -0
- data/spec/support/test_resource.rb +75 -0
- data/spec/webmachine/ring_adapter_spec.rb +127 -0
- data/src/.classpath +7 -0
- data/src/.project +17 -0
- data/src/org/httpkit/BytesInputStream.class +0 -0
- data/src/org/httpkit/BytesInputStream.java +83 -0
- data/src/org/httpkit/DateFormatter.class +0 -0
- data/src/org/httpkit/DynamicBytes.class +0 -0
- data/src/org/httpkit/DynamicBytes.java +76 -0
- data/src/org/httpkit/HTTPException.class +0 -0
- data/src/org/httpkit/HTTPException.java +10 -0
- data/src/org/httpkit/HeaderMap.class +0 -0
- data/src/org/httpkit/HeaderMap.java +98 -0
- data/src/org/httpkit/HttpMethod.class +0 -0
- data/src/org/httpkit/HttpMethod.java +28 -0
- data/src/org/httpkit/HttpStatus.class +0 -0
- data/src/org/httpkit/HttpStatus.java +416 -0
- data/src/org/httpkit/HttpUtils.class +0 -0
- data/src/org/httpkit/HttpUtils.java +484 -0
- data/src/org/httpkit/HttpVersion.class +0 -0
- data/src/org/httpkit/HttpVersion.java +5 -0
- data/src/org/httpkit/LineReader.class +0 -0
- data/src/org/httpkit/LineReader.java +52 -0
- data/src/org/httpkit/LineTooLargeException.class +0 -0
- data/src/org/httpkit/LineTooLargeException.java +13 -0
- data/src/org/httpkit/PrefixThreadFactory.class +0 -0
- data/src/org/httpkit/PrefixThreadFactory.java +20 -0
- data/src/org/httpkit/PriorityQueue.class +0 -0
- data/src/org/httpkit/PriorityQueue.java +235 -0
- data/src/org/httpkit/ProtocolException.class +0 -0
- data/src/org/httpkit/ProtocolException.java +10 -0
- data/src/org/httpkit/RequestTooLargeException.class +0 -0
- data/src/org/httpkit/RequestTooLargeException.java +10 -0
- data/src/org/httpkit/client/AbortException.class +0 -0
- data/src/org/httpkit/client/AbortException.java +13 -0
- data/src/org/httpkit/client/Decoder.class +0 -0
- data/src/org/httpkit/client/Decoder.java +182 -0
- data/src/org/httpkit/client/Handler.class +0 -0
- data/src/org/httpkit/client/HttpClient.class +0 -0
- data/src/org/httpkit/client/HttpClient.java +393 -0
- data/src/org/httpkit/client/HttpsRequest.class +0 -0
- data/src/org/httpkit/client/HttpsRequest.java +141 -0
- data/src/org/httpkit/client/IFilter$1.class +0 -0
- data/src/org/httpkit/client/IFilter$MaxBodyFilter.class +0 -0
- data/src/org/httpkit/client/IFilter.class +0 -0
- data/src/org/httpkit/client/IFilter.java +53 -0
- data/src/org/httpkit/client/IRespListener.class +0 -0
- data/src/org/httpkit/client/IRespListener.java +30 -0
- data/src/org/httpkit/client/IResponseHandler.class +0 -0
- data/src/org/httpkit/client/IResponseHandler.java +18 -0
- data/src/org/httpkit/client/PersistentConn.class +0 -0
- data/src/org/httpkit/client/PersistentConn.java +33 -0
- data/src/org/httpkit/client/Request.class +0 -0
- data/src/org/httpkit/client/Request.java +74 -0
- data/src/org/httpkit/client/RequestConfig.class +0 -0
- data/src/org/httpkit/client/RequestConfig.java +28 -0
- data/src/org/httpkit/client/RespListener.class +0 -0
- data/src/org/httpkit/client/RespListener.java +160 -0
- data/src/org/httpkit/client/SslContextFactory.class +0 -0
- data/src/org/httpkit/client/SslContextFactory.java +79 -0
- data/src/org/httpkit/client/State.class +0 -0
- data/src/org/httpkit/client/TimeoutException.class +0 -0
- data/src/org/httpkit/client/TimeoutException.java +12 -0
- data/src/org/httpkit/client/TrustManagerFactory$1.class +0 -0
- data/src/org/httpkit/client/TrustManagerFactory.class +0 -0
- data/src/org/httpkit/server/AsyncChannel.class +0 -0
- data/src/org/httpkit/server/AsyncChannel.java +286 -0
- data/src/org/httpkit/server/ClojureRing.class +0 -0
- data/src/org/httpkit/server/Frame$BinaryFrame.class +0 -0
- data/src/org/httpkit/server/Frame$CloseFrame.class +0 -0
- data/src/org/httpkit/server/Frame$PingFrame.class +0 -0
- data/src/org/httpkit/server/Frame$TextFrame.class +0 -0
- data/src/org/httpkit/server/Frame.class +0 -0
- data/src/org/httpkit/server/Frame.java +73 -0
- data/src/org/httpkit/server/HttpAtta.class +0 -0
- data/src/org/httpkit/server/HttpAtta.java +10 -0
- data/src/org/httpkit/server/HttpDecoder$State.class +0 -0
- data/src/org/httpkit/server/HttpDecoder.class +0 -0
- data/src/org/httpkit/server/HttpDecoder.java +202 -0
- data/src/org/httpkit/server/HttpHandler.class +0 -0
- data/src/org/httpkit/server/HttpRequest.class +0 -0
- data/src/org/httpkit/server/HttpRequest.java +109 -0
- data/src/org/httpkit/server/HttpServer.class +0 -0
- data/src/org/httpkit/server/HttpServer.java +281 -0
- data/src/org/httpkit/server/IHandler.class +0 -0
- data/src/org/httpkit/server/IHandler.java +12 -0
- data/src/org/httpkit/server/LinkingRunnable.class +0 -0
- data/src/org/httpkit/server/Rack.class +0 -0
- data/src/org/httpkit/server/RackApplication.class +0 -0
- data/src/org/httpkit/server/RackApplication.java +10 -0
- data/src/org/httpkit/server/RackHandler$1.class +0 -0
- data/src/org/httpkit/server/RackHandler.class +0 -0
- data/src/org/httpkit/server/RackHandler.java +259 -0
- data/src/org/httpkit/server/RackHttpHandler.class +0 -0
- data/src/org/httpkit/server/RackHttpHandler.java +20 -0
- data/src/org/httpkit/server/RespCallback.class +0 -0
- data/src/org/httpkit/server/RespCallback.java +19 -0
- data/src/org/httpkit/server/RingHandler$1.class +0 -0
- data/src/org/httpkit/server/RingHandler.class +0 -0
- data/src/org/httpkit/server/RingHandler.java +222 -0
- data/src/org/httpkit/server/ServerAtta.class +0 -0
- data/src/org/httpkit/server/ServerAtta.java +22 -0
- data/src/org/httpkit/server/WSDecoder$State.class +0 -0
- data/src/org/httpkit/server/WSDecoder.class +0 -0
- data/src/org/httpkit/server/WSDecoder.java +181 -0
- data/src/org/httpkit/server/WSHandler.class +0 -0
- data/src/org/httpkit/server/WsAtta.class +0 -0
- data/src/org/httpkit/server/WsAtta.java +11 -0
- data/src/org/httpkit/timer/CancelableFutureTask.class +0 -0
- data/src/org/httpkit/timer/CancelableFutureTask.java +52 -0
- data/src/org/httpkit/timer/TimerService.class +0 -0
- data/src/org/httpkit/timer/TimerService.java +89 -0
- metadata +241 -0
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
package org.httpkit.client;
|
|
2
|
+
|
|
3
|
+
import org.httpkit.*;
|
|
4
|
+
import org.httpkit.ProtocolException;
|
|
5
|
+
|
|
6
|
+
import javax.net.ssl.SSLContext;
|
|
7
|
+
import javax.net.ssl.SSLEngine;
|
|
8
|
+
import javax.net.ssl.SSLException;
|
|
9
|
+
import java.io.IOException;
|
|
10
|
+
import java.net.*;
|
|
11
|
+
import java.nio.ByteBuffer;
|
|
12
|
+
import java.nio.channels.SelectionKey;
|
|
13
|
+
import java.nio.channels.Selector;
|
|
14
|
+
import java.nio.channels.SocketChannel;
|
|
15
|
+
import java.security.NoSuchAlgorithmException;
|
|
16
|
+
import java.util.Iterator;
|
|
17
|
+
import java.util.Queue;
|
|
18
|
+
import java.util.Set;
|
|
19
|
+
import java.util.concurrent.ConcurrentLinkedQueue;
|
|
20
|
+
import java.util.concurrent.atomic.AtomicInteger;
|
|
21
|
+
|
|
22
|
+
import static java.lang.System.currentTimeMillis;
|
|
23
|
+
import static java.nio.channels.SelectionKey.*;
|
|
24
|
+
import static org.httpkit.HttpUtils.SP;
|
|
25
|
+
import static org.httpkit.HttpUtils.getServerAddr;
|
|
26
|
+
import static org.httpkit.client.State.ALL_READ;
|
|
27
|
+
import static org.httpkit.client.State.READ_INITIAL;
|
|
28
|
+
|
|
29
|
+
public final class HttpClient implements Runnable {
|
|
30
|
+
private static final AtomicInteger ID = new AtomicInteger(0);
|
|
31
|
+
|
|
32
|
+
public static final SSLContext DEFAULT_CONTEXT;
|
|
33
|
+
|
|
34
|
+
static {
|
|
35
|
+
try {
|
|
36
|
+
DEFAULT_CONTEXT = SSLContext.getDefault();
|
|
37
|
+
} catch (NoSuchAlgorithmException e) {
|
|
38
|
+
throw new Error("Failed to initialize SSLContext", e);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private final Queue<Request> pending = new ConcurrentLinkedQueue<Request>();
|
|
43
|
+
private final PriorityQueue<Request> requests = new PriorityQueue<Request>();
|
|
44
|
+
private final PriorityQueue<PersistentConn> keepalives = new PriorityQueue<PersistentConn>();
|
|
45
|
+
|
|
46
|
+
private volatile boolean running = true;
|
|
47
|
+
|
|
48
|
+
// shared, single thread
|
|
49
|
+
private final ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 64);
|
|
50
|
+
private final Selector selector;
|
|
51
|
+
|
|
52
|
+
public HttpClient() throws IOException {
|
|
53
|
+
int id = ID.incrementAndGet();
|
|
54
|
+
String name = "client-loop";
|
|
55
|
+
if (id > 1) {
|
|
56
|
+
name = name + "#" + id;
|
|
57
|
+
}
|
|
58
|
+
selector = Selector.open();
|
|
59
|
+
Thread t = new Thread(this, name);
|
|
60
|
+
t.setDaemon(true);
|
|
61
|
+
t.start();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private void clearTimeout(long now) {
|
|
65
|
+
Request r;
|
|
66
|
+
while ((r = requests.peek()) != null) {
|
|
67
|
+
if (r.isTimeout(now)) {
|
|
68
|
+
String msg = "connect timeout: ";
|
|
69
|
+
if (r.isConnected) {
|
|
70
|
+
msg = "read timeout: ";
|
|
71
|
+
}
|
|
72
|
+
// will remove it from queue
|
|
73
|
+
r.finish(new TimeoutException(msg + r.cfg.timeout + "ms"));
|
|
74
|
+
if (r.key != null) {
|
|
75
|
+
closeQuietly(r.key);
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
PersistentConn pc;
|
|
83
|
+
while ((pc = keepalives.peek()) != null) {
|
|
84
|
+
if (pc.isTimeout(now)) {
|
|
85
|
+
closeQuietly(pc.key);
|
|
86
|
+
keepalives.poll();
|
|
87
|
+
} else {
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* http-kit think all connections are keep-alived (since some say it is, but
|
|
95
|
+
* actually is not). but, some are not, http-kit pick them out after the fact
|
|
96
|
+
* <ol>
|
|
97
|
+
* <li>The connection is reused</li>
|
|
98
|
+
* <li>No data received</li>
|
|
99
|
+
* </ol>
|
|
100
|
+
*/
|
|
101
|
+
private boolean cleanAndRetryIfBroken(SelectionKey key, Request req) {
|
|
102
|
+
closeQuietly(key);
|
|
103
|
+
keepalives.remove(key);
|
|
104
|
+
// keep-alived connection, remote server close it without sending byte
|
|
105
|
+
if (req.isReuseConn && req.decoder.state == READ_INITIAL) {
|
|
106
|
+
for (ByteBuffer b : req.request) {
|
|
107
|
+
b.position(0); // reset for retry
|
|
108
|
+
}
|
|
109
|
+
req.isReuseConn = false;
|
|
110
|
+
requests.remove(req); // remove from timeout queue
|
|
111
|
+
pending.offer(req); // queue for retry
|
|
112
|
+
selector.wakeup();
|
|
113
|
+
return true; // retry: re-open a connection to server, sent the request again
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private void doRead(SelectionKey key, long now) {
|
|
119
|
+
Request req = (Request) key.attachment();
|
|
120
|
+
SocketChannel ch = (SocketChannel) key.channel();
|
|
121
|
+
int read = 0;
|
|
122
|
+
try {
|
|
123
|
+
buffer.clear();
|
|
124
|
+
if (req instanceof HttpsRequest) {
|
|
125
|
+
HttpsRequest httpsReq = (HttpsRequest) req;
|
|
126
|
+
if (httpsReq.handshaken) {
|
|
127
|
+
// SSLEngine closed => fine, will return -1 in the next run
|
|
128
|
+
read = httpsReq.unwrapRead(buffer);
|
|
129
|
+
} else {
|
|
130
|
+
read = httpsReq.doHandshake(buffer);
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
read = ch.read(buffer);
|
|
134
|
+
}
|
|
135
|
+
} catch (IOException e) { // The remote forcibly closed the connection
|
|
136
|
+
if (!cleanAndRetryIfBroken(key, req)) {
|
|
137
|
+
req.finish(e); // os X get Connection reset by peer error,
|
|
138
|
+
}
|
|
139
|
+
// java.security.InvalidAlgorithmParameterException: Prime size must be multiple of 64, and can only range from 512 to 1024 (inclusive)
|
|
140
|
+
// java.lang.RuntimeException: Could not generate DH keypair
|
|
141
|
+
} catch (Exception e) {
|
|
142
|
+
req.finish(e);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (read == -1) { // read all, remote closed it cleanly
|
|
146
|
+
if (!cleanAndRetryIfBroken(key, req)) {
|
|
147
|
+
req.finish();
|
|
148
|
+
}
|
|
149
|
+
} else if (read > 0) {
|
|
150
|
+
req.onProgress(now);
|
|
151
|
+
buffer.flip();
|
|
152
|
+
try {
|
|
153
|
+
if (req.decoder.decode(buffer) == ALL_READ) {
|
|
154
|
+
req.finish();
|
|
155
|
+
if (req.cfg.keepAlive > 0) {
|
|
156
|
+
keepalives.offer(new PersistentConn(now + req.cfg.keepAlive, req.addr, key));
|
|
157
|
+
} else {
|
|
158
|
+
closeQuietly(key);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
} catch (HTTPException e) {
|
|
162
|
+
closeQuietly(key);
|
|
163
|
+
req.finish(e);
|
|
164
|
+
} catch (Exception e) {
|
|
165
|
+
closeQuietly(key);
|
|
166
|
+
req.finish(e);
|
|
167
|
+
HttpUtils.printError("should not happen", e); // decoding
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private void closeQuietly(SelectionKey key) {
|
|
173
|
+
try {
|
|
174
|
+
// TODO engine.closeInbound
|
|
175
|
+
key.channel().close();
|
|
176
|
+
} catch (Exception ignore) {
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private void doWrite(SelectionKey key) {
|
|
181
|
+
Request req = (Request) key.attachment();
|
|
182
|
+
SocketChannel ch = (SocketChannel) key.channel();
|
|
183
|
+
try {
|
|
184
|
+
if (req instanceof HttpsRequest) {
|
|
185
|
+
HttpsRequest httpsReq = (HttpsRequest) req;
|
|
186
|
+
if (httpsReq.handshaken) {
|
|
187
|
+
// will flip to OP_READ
|
|
188
|
+
httpsReq.writeWrappedRequest();
|
|
189
|
+
} else {
|
|
190
|
+
buffer.clear();
|
|
191
|
+
if (httpsReq.doHandshake(buffer) < 0) {
|
|
192
|
+
req.finish(); // will be a No status exception
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
ByteBuffer[] buffers = req.request;
|
|
197
|
+
ch.write(buffers);
|
|
198
|
+
if (!buffers[buffers.length - 1].hasRemaining()) {
|
|
199
|
+
key.interestOps(OP_READ);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
} catch (IOException e) {
|
|
203
|
+
if (!cleanAndRetryIfBroken(key, req)) {
|
|
204
|
+
req.finish(e);
|
|
205
|
+
}
|
|
206
|
+
} catch (Exception e) { // rarely happen
|
|
207
|
+
req.finish(e);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
public void exec(String url, RequestConfig cfg, SSLEngine engine, IRespListener cb) {
|
|
212
|
+
URI uri;
|
|
213
|
+
try {
|
|
214
|
+
uri = new URI(url);
|
|
215
|
+
} catch (URISyntaxException e) {
|
|
216
|
+
cb.onThrowable(e);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (uri.getHost() == null) {
|
|
221
|
+
cb.onThrowable(new IllegalArgumentException("host is null: " + url));
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
String scheme = uri.getScheme();
|
|
226
|
+
if (!"http".equals(scheme) && !"https".equals(scheme)) {
|
|
227
|
+
String message = (scheme == null) ? "No protocol specified" : scheme + " is not supported";
|
|
228
|
+
cb.onThrowable(new ProtocolException(message));
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
InetSocketAddress addr;
|
|
233
|
+
try {
|
|
234
|
+
addr = getServerAddr(uri);
|
|
235
|
+
} catch (UnknownHostException e) {
|
|
236
|
+
cb.onThrowable(e);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// copy to modify, normalize header
|
|
241
|
+
HeaderMap headers = HeaderMap.camelCase(cfg.headers);
|
|
242
|
+
headers.put("Host", HttpUtils.getHost(uri));
|
|
243
|
+
|
|
244
|
+
if (!headers.containsKey("Accept")) // allow override
|
|
245
|
+
headers.put("Accept", "*/*");
|
|
246
|
+
if (!headers.containsKey("User-Agent")) // allow override
|
|
247
|
+
headers.put("User-Agent", RequestConfig.DEFAULT_USER_AGENT); // default
|
|
248
|
+
if (!headers.containsKey("Accept-Encoding"))
|
|
249
|
+
headers.put("Accept-Encoding", "gzip, deflate"); // compression is good
|
|
250
|
+
|
|
251
|
+
ByteBuffer request[];
|
|
252
|
+
try {
|
|
253
|
+
request = encode(cfg.method, headers, cfg.body, uri);
|
|
254
|
+
} catch (IOException e) {
|
|
255
|
+
cb.onThrowable(e);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if ("https".equals(scheme)) {
|
|
259
|
+
if (engine == null) {
|
|
260
|
+
engine = DEFAULT_CONTEXT.createSSLEngine();
|
|
261
|
+
}
|
|
262
|
+
engine.setUseClientMode(true);
|
|
263
|
+
pending.offer(new HttpsRequest(addr, request, cb, requests, cfg, engine));
|
|
264
|
+
} else {
|
|
265
|
+
pending.offer(new Request(addr, request, cb, requests, cfg));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// pending.offer(new Request(addr, request, cb, requests, cfg));
|
|
269
|
+
selector.wakeup();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private ByteBuffer[] encode(HttpMethod method, HeaderMap headers, Object body,
|
|
273
|
+
URI uri) throws IOException {
|
|
274
|
+
ByteBuffer bodyBuffer = HttpUtils.bodyBuffer(body);
|
|
275
|
+
|
|
276
|
+
if (body != null) {
|
|
277
|
+
headers.put("Content-Length", Integer.toString(bodyBuffer.remaining()));
|
|
278
|
+
} else {
|
|
279
|
+
headers.put("Content-Length", "0");
|
|
280
|
+
}
|
|
281
|
+
DynamicBytes bytes = new DynamicBytes(196);
|
|
282
|
+
bytes.append(method.toString()).append(SP).append(HttpUtils.getPath(uri));
|
|
283
|
+
bytes.append(" HTTP/1.1\r\n");
|
|
284
|
+
headers.encodeHeaders(bytes);
|
|
285
|
+
ByteBuffer headBuffer = ByteBuffer.wrap(bytes.get(), 0, bytes.length());
|
|
286
|
+
|
|
287
|
+
if (bodyBuffer == null) {
|
|
288
|
+
return new ByteBuffer[]{headBuffer};
|
|
289
|
+
} else {
|
|
290
|
+
return new ByteBuffer[]{headBuffer, bodyBuffer};
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private void finishConnect(SelectionKey key, long now) {
|
|
295
|
+
SocketChannel ch = (SocketChannel) key.channel();
|
|
296
|
+
Request req = (Request) key.attachment();
|
|
297
|
+
try {
|
|
298
|
+
if (ch.finishConnect()) {
|
|
299
|
+
req.isConnected = true;
|
|
300
|
+
req.onProgress(now);
|
|
301
|
+
key.interestOps(OP_WRITE);
|
|
302
|
+
if (req instanceof HttpsRequest) {
|
|
303
|
+
((HttpsRequest) req).engine.beginHandshake();
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
} catch (IOException e) {
|
|
307
|
+
closeQuietly(key); // not added to kee-alive yet;
|
|
308
|
+
req.finish(e);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private void processPending() {
|
|
313
|
+
Request job = null;
|
|
314
|
+
while ((job = pending.poll()) != null) {
|
|
315
|
+
if (job.cfg.keepAlive > 0) {
|
|
316
|
+
PersistentConn con = keepalives.remove(job.addr);
|
|
317
|
+
if (con != null) { // keep alive
|
|
318
|
+
SelectionKey key = con.key;
|
|
319
|
+
if (key.isValid()) {
|
|
320
|
+
job.isReuseConn = true;
|
|
321
|
+
// reuse key, engine
|
|
322
|
+
try {
|
|
323
|
+
job.recycle((Request) key.attachment());
|
|
324
|
+
key.attach(job);
|
|
325
|
+
key.interestOps(OP_WRITE);
|
|
326
|
+
requests.offer(job);
|
|
327
|
+
continue;
|
|
328
|
+
} catch (SSLException e) {
|
|
329
|
+
closeQuietly(key); // https wrap SSLException, start from fresh
|
|
330
|
+
}
|
|
331
|
+
} else {
|
|
332
|
+
// this should not happen often
|
|
333
|
+
closeQuietly(key);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
try {
|
|
338
|
+
SocketChannel ch = SocketChannel.open();
|
|
339
|
+
ch.configureBlocking(false);
|
|
340
|
+
// saved for timeout
|
|
341
|
+
job.key = ch.register(selector, OP_CONNECT, job);
|
|
342
|
+
ch.connect(job.addr);
|
|
343
|
+
requests.offer(job);
|
|
344
|
+
} catch (IOException e) {
|
|
345
|
+
job.finish(e);
|
|
346
|
+
HttpUtils.printError("Try to connect " + job.addr, e);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
public void run() {
|
|
352
|
+
while (running) {
|
|
353
|
+
try {
|
|
354
|
+
long now = currentTimeMillis();
|
|
355
|
+
int select = selector.select(2000);
|
|
356
|
+
if (select > 0) {
|
|
357
|
+
Set<SelectionKey> selectedKeys = selector.selectedKeys();
|
|
358
|
+
Iterator<SelectionKey> ite = selectedKeys.iterator();
|
|
359
|
+
while (ite.hasNext()) {
|
|
360
|
+
SelectionKey key = ite.next();
|
|
361
|
+
if (!key.isValid()) {
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
if (key.isConnectable()) {
|
|
365
|
+
finishConnect(key, now);
|
|
366
|
+
} else if (key.isReadable()) {
|
|
367
|
+
doRead(key, now);
|
|
368
|
+
} else if (key.isWritable()) {
|
|
369
|
+
doWrite(key);
|
|
370
|
+
}
|
|
371
|
+
ite.remove();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
clearTimeout(now);
|
|
375
|
+
processPending();
|
|
376
|
+
} catch (Throwable e) { // catch any exception (including OOM), print it: do not exits the loop
|
|
377
|
+
HttpUtils.printError("select exception, should not happen", e);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
public void stop() throws IOException {
|
|
383
|
+
running = false;
|
|
384
|
+
if (selector != null) {
|
|
385
|
+
selector.close();
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
@Override
|
|
390
|
+
public String toString() {
|
|
391
|
+
return this.getClass().getCanonicalName();
|
|
392
|
+
}
|
|
393
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
package org.httpkit.client;
|
|
2
|
+
|
|
3
|
+
import org.httpkit.PriorityQueue;
|
|
4
|
+
|
|
5
|
+
import javax.net.ssl.SSLEngine;
|
|
6
|
+
import javax.net.ssl.SSLEngineResult;
|
|
7
|
+
import javax.net.ssl.SSLEngineResult.Status;
|
|
8
|
+
import javax.net.ssl.SSLException;
|
|
9
|
+
import java.io.IOException;
|
|
10
|
+
import java.net.InetSocketAddress;
|
|
11
|
+
import java.nio.ByteBuffer;
|
|
12
|
+
import java.nio.channels.SelectionKey;
|
|
13
|
+
import java.nio.channels.SocketChannel;
|
|
14
|
+
|
|
15
|
+
public class HttpsRequest extends Request {
|
|
16
|
+
private static final ByteBuffer EMPTY_BUFFER = ByteBuffer.allocate(0);
|
|
17
|
+
|
|
18
|
+
public HttpsRequest(InetSocketAddress addr, ByteBuffer[] request, IRespListener handler,
|
|
19
|
+
PriorityQueue<Request> clients, RequestConfig config, SSLEngine engine) {
|
|
20
|
+
super(addr, request, handler, clients, config);
|
|
21
|
+
this.engine = engine;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
SSLEngine engine; // package private
|
|
25
|
+
private ByteBuffer myNetData = ByteBuffer.allocate(40 * 1024);
|
|
26
|
+
private ByteBuffer peerNetData = ByteBuffer.allocate(40 * 1024);
|
|
27
|
+
boolean handshaken = false;
|
|
28
|
+
|
|
29
|
+
final int unwrapRead(ByteBuffer peerAppData) throws IOException {
|
|
30
|
+
// TODO, make sure peerNetData has remaining place
|
|
31
|
+
int read = ((SocketChannel) key.channel()).read(peerNetData), unwrapped = 0;
|
|
32
|
+
if (read > 0) {
|
|
33
|
+
peerNetData.flip();
|
|
34
|
+
SSLEngineResult res;
|
|
35
|
+
while ((res = engine.unwrap(peerNetData, peerAppData)).getStatus() == Status.OK) {
|
|
36
|
+
unwrapped += res.bytesProduced();
|
|
37
|
+
if (!peerNetData.hasRemaining())
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
peerNetData.compact();
|
|
41
|
+
switch (res.getStatus()) {
|
|
42
|
+
case OK:
|
|
43
|
+
case BUFFER_UNDERFLOW: // need more data
|
|
44
|
+
return unwrapped;
|
|
45
|
+
case CLOSED:
|
|
46
|
+
return unwrapped > 0 ? unwrapped : -1;
|
|
47
|
+
case BUFFER_OVERFLOW:
|
|
48
|
+
return -1; // can't => peerAppData is 64k
|
|
49
|
+
}
|
|
50
|
+
return unwrapped;
|
|
51
|
+
} else {
|
|
52
|
+
return read;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private void wrapRequest() throws SSLException {
|
|
57
|
+
myNetData.clear();
|
|
58
|
+
SSLEngineResult res = engine.wrap(request, myNetData);
|
|
59
|
+
if (res.getStatus() != Status.OK) {
|
|
60
|
+
// TODO larger buffer, uberflow?
|
|
61
|
+
}
|
|
62
|
+
myNetData.flip();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
final void writeWrappedRequest() throws IOException {
|
|
66
|
+
if (myNetData.hasRemaining()) {
|
|
67
|
+
((SocketChannel) key.channel()).write(myNetData);
|
|
68
|
+
} else if (request[request.length - 1].hasRemaining()) {
|
|
69
|
+
wrapRequest();
|
|
70
|
+
((SocketChannel) key.channel()).write(myNetData);
|
|
71
|
+
}
|
|
72
|
+
if (myNetData.hasRemaining() || request[request.length - 1].hasRemaining()) {
|
|
73
|
+
// need more write
|
|
74
|
+
if ((key.interestOps() & SelectionKey.OP_WRITE) == 0)
|
|
75
|
+
key.interestOps(SelectionKey.OP_WRITE);
|
|
76
|
+
} else {
|
|
77
|
+
// OK, request sent
|
|
78
|
+
key.interestOps(SelectionKey.OP_READ);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
final int doHandshake(ByteBuffer peerAppData) throws IOException {
|
|
83
|
+
SSLEngineResult.HandshakeStatus hs = engine.getHandshakeStatus();
|
|
84
|
+
while (!handshaken) {
|
|
85
|
+
switch (hs) {
|
|
86
|
+
case NEED_TASK:
|
|
87
|
+
Runnable runnable;
|
|
88
|
+
while ((runnable = engine.getDelegatedTask()) != null) {
|
|
89
|
+
runnable.run();
|
|
90
|
+
}
|
|
91
|
+
break;
|
|
92
|
+
case NEED_UNWRAP:
|
|
93
|
+
int read = ((SocketChannel) key.channel()).read(peerNetData);
|
|
94
|
+
if (read < 0) {
|
|
95
|
+
return -1;
|
|
96
|
+
} else {
|
|
97
|
+
peerNetData.flip();
|
|
98
|
+
SSLEngineResult res = engine.unwrap(peerNetData, peerAppData);
|
|
99
|
+
peerNetData.compact();
|
|
100
|
+
switch (res.getStatus()) {
|
|
101
|
+
case BUFFER_OVERFLOW: // Not possible, peerAppData is 64k
|
|
102
|
+
break;
|
|
103
|
+
case CLOSED:
|
|
104
|
+
return -1;
|
|
105
|
+
case BUFFER_UNDERFLOW: // need more data from peer
|
|
106
|
+
return 0;
|
|
107
|
+
}
|
|
108
|
+
// do not flip to write here, since TCP buffer is writable
|
|
109
|
+
}
|
|
110
|
+
break;
|
|
111
|
+
case NEED_WRAP:
|
|
112
|
+
SSLEngineResult res = engine.wrap(EMPTY_BUFFER, myNetData);
|
|
113
|
+
myNetData.flip();
|
|
114
|
+
((SocketChannel) key.channel()).write(myNetData);
|
|
115
|
+
if (myNetData.hasRemaining()) {
|
|
116
|
+
// TODO, make sure data get written
|
|
117
|
+
} else {
|
|
118
|
+
myNetData.clear();
|
|
119
|
+
if (res.getHandshakeStatus() != SSLEngineResult.HandshakeStatus.NEED_WRAP)
|
|
120
|
+
key.interestOps(SelectionKey.OP_READ);
|
|
121
|
+
}
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
hs = engine.getHandshakeStatus();
|
|
125
|
+
handshaken = hs == SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING
|
|
126
|
+
|| hs == SSLEngineResult.HandshakeStatus.FINISHED;
|
|
127
|
+
if (handshaken) {
|
|
128
|
+
wrapRequest();
|
|
129
|
+
writeWrappedRequest(); // TCP buffer maybe empty this time
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
public void recycle(Request old) throws SSLException {
|
|
136
|
+
super.recycle(old);
|
|
137
|
+
this.engine = ((HttpsRequest) old).engine;
|
|
138
|
+
this.handshaken = true;
|
|
139
|
+
wrapRequest(); // prepare for write
|
|
140
|
+
}
|
|
141
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
package org.httpkit.client;
|
|
2
|
+
|
|
3
|
+
import org.httpkit.DynamicBytes;
|
|
4
|
+
|
|
5
|
+
import java.util.Map;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* allow to abort the connection. for example, a crawler may abort the
|
|
9
|
+
* connection if not text
|
|
10
|
+
*
|
|
11
|
+
* @author feng
|
|
12
|
+
*/
|
|
13
|
+
public interface IFilter {
|
|
14
|
+
public final static IFilter ACCEPT_ALL = new IFilter() {
|
|
15
|
+
public boolean accept(DynamicBytes partialBody) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public boolean accept(Map<String, Object> headers) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public String toString() {
|
|
24
|
+
return "Response Filter: ACCEPT all response";
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// if the response is too large, protect OOM
|
|
29
|
+
// For example, HTML expected, but a big mp4 file is returned
|
|
30
|
+
public static class MaxBodyFilter implements IFilter {
|
|
31
|
+
private final int length;
|
|
32
|
+
|
|
33
|
+
public MaxBodyFilter(int maxLength) {
|
|
34
|
+
this.length = maxLength;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public boolean accept(Map<String, Object> headers) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
public String toString() {
|
|
42
|
+
return "Response Filter: ACCEPT when body's length <= " + length;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public boolean accept(DynamicBytes partialBody) {
|
|
46
|
+
return partialBody.length() <= length;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public boolean accept(Map<String, Object> headers);
|
|
51
|
+
|
|
52
|
+
public boolean accept(DynamicBytes partialBody);
|
|
53
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
package org.httpkit.client;
|
|
2
|
+
|
|
3
|
+
import org.httpkit.HttpStatus;
|
|
4
|
+
import org.httpkit.HttpVersion;
|
|
5
|
+
|
|
6
|
+
import java.util.Map;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Interface for response received from server
|
|
10
|
+
* <p/>
|
|
11
|
+
* A low level interface, can be used for very large file download
|
|
12
|
+
*
|
|
13
|
+
* @author feng
|
|
14
|
+
*/
|
|
15
|
+
public interface IRespListener {
|
|
16
|
+
|
|
17
|
+
public void onBodyReceived(byte[] buf, int length) throws AbortException;
|
|
18
|
+
|
|
19
|
+
public void onCompleted();
|
|
20
|
+
|
|
21
|
+
public void onHeadersReceived(Map<String, Object> headers) throws AbortException;
|
|
22
|
+
|
|
23
|
+
public void onInitialLineReceived(HttpVersion version, HttpStatus status)
|
|
24
|
+
throws AbortException;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* protocol error
|
|
28
|
+
*/
|
|
29
|
+
public void onThrowable(Throwable t);
|
|
30
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
package org.httpkit.client;
|
|
2
|
+
|
|
3
|
+
import java.util.Map;
|
|
4
|
+
|
|
5
|
+
public interface IResponseHandler {
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* get called when all the response are fully received from server
|
|
9
|
+
*
|
|
10
|
+
* @param status HTTP status code, like 200
|
|
11
|
+
* @param headers Response HTTP headers
|
|
12
|
+
* @param body Response body, for text is a String, for binary is a
|
|
13
|
+
* InputStream
|
|
14
|
+
*/
|
|
15
|
+
void onSuccess(int status, Map<String, Object> headers, Object body);
|
|
16
|
+
|
|
17
|
+
void onThrowable(Throwable t);
|
|
18
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
package org.httpkit.client;
|
|
2
|
+
|
|
3
|
+
import java.net.InetSocketAddress;
|
|
4
|
+
import java.nio.channels.SelectionKey;
|
|
5
|
+
|
|
6
|
+
public class PersistentConn implements Comparable<PersistentConn> {
|
|
7
|
+
private final long timeoutTs;
|
|
8
|
+
public final InetSocketAddress addr;
|
|
9
|
+
public final SelectionKey key;
|
|
10
|
+
|
|
11
|
+
public PersistentConn(long timeoutTs, InetSocketAddress addr, SelectionKey key) {
|
|
12
|
+
this.timeoutTs = timeoutTs;
|
|
13
|
+
this.addr = addr;
|
|
14
|
+
this.key = key;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
public boolean equals(Object obj) {
|
|
18
|
+
// for PriorityQueue to remove by key and by addr
|
|
19
|
+
return addr.equals(obj) || key.equals(obj);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public int compareTo(PersistentConn o) {
|
|
23
|
+
return (int) (timeoutTs - o.timeoutTs);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public boolean isTimeout(long now) {
|
|
27
|
+
return timeoutTs < now;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public String toString() {
|
|
31
|
+
return addr + "; timeout=" + (timeoutTs - System.currentTimeMillis()) + "ms";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
Binary file
|