jruby-http-kit 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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,74 @@
|
|
1
|
+
package org.httpkit.client;
|
2
|
+
|
3
|
+
import org.httpkit.PriorityQueue;
|
4
|
+
|
5
|
+
import javax.net.ssl.SSLException;
|
6
|
+
import java.net.InetSocketAddress;
|
7
|
+
import java.nio.ByteBuffer;
|
8
|
+
import java.nio.channels.SelectionKey;
|
9
|
+
|
10
|
+
public class Request implements Comparable<Request> {
|
11
|
+
|
12
|
+
final InetSocketAddress addr;
|
13
|
+
final Decoder decoder;
|
14
|
+
final ByteBuffer[] request; // HTTP request
|
15
|
+
final RequestConfig cfg;
|
16
|
+
private final PriorityQueue<Request> clients; // update timeout
|
17
|
+
|
18
|
+
// is modify from the loop thread. ensure only called once
|
19
|
+
private boolean isDone = false;
|
20
|
+
|
21
|
+
boolean isReuseConn = false; // a reused socket sent the request
|
22
|
+
boolean isConnected = false;
|
23
|
+
SelectionKey key; // for timeout, close connection
|
24
|
+
|
25
|
+
private long timeoutTs; // future time this request timeout, ms
|
26
|
+
|
27
|
+
public Request(InetSocketAddress addr, ByteBuffer[] request, IRespListener handler,
|
28
|
+
PriorityQueue<Request> clients, RequestConfig config) {
|
29
|
+
this.cfg = config;
|
30
|
+
this.decoder = new Decoder(handler, config.method);
|
31
|
+
this.request = request;
|
32
|
+
this.clients = clients;
|
33
|
+
this.addr = addr;
|
34
|
+
this.timeoutTs = config.timeout + System.currentTimeMillis();
|
35
|
+
}
|
36
|
+
|
37
|
+
public void onProgress(long now) {
|
38
|
+
if (cfg.timeout + now - timeoutTs > 800) {
|
39
|
+
// update time
|
40
|
+
clients.remove(this);
|
41
|
+
timeoutTs = cfg.timeout + now;
|
42
|
+
clients.offer(this);
|
43
|
+
}
|
44
|
+
}
|
45
|
+
|
46
|
+
public void finish() {
|
47
|
+
clients.remove(this);
|
48
|
+
if (isDone)
|
49
|
+
return;
|
50
|
+
isDone = true;
|
51
|
+
decoder.listener.onCompleted();
|
52
|
+
}
|
53
|
+
|
54
|
+
public boolean isTimeout(long now) {
|
55
|
+
return timeoutTs < now;
|
56
|
+
}
|
57
|
+
|
58
|
+
public void finish(Throwable t) {
|
59
|
+
clients.remove(this);
|
60
|
+
if (isDone)
|
61
|
+
return;
|
62
|
+
isDone = true;
|
63
|
+
decoder.listener.onThrowable(t);
|
64
|
+
}
|
65
|
+
|
66
|
+
public int compareTo(Request o) {
|
67
|
+
return (int) (timeoutTs - o.timeoutTs);
|
68
|
+
}
|
69
|
+
|
70
|
+
public void recycle(Request old) throws SSLException {
|
71
|
+
this.key = old.key;
|
72
|
+
}
|
73
|
+
}
|
74
|
+
|
Binary file
|
@@ -0,0 +1,28 @@
|
|
1
|
+
package org.httpkit.client;
|
2
|
+
|
3
|
+
import org.httpkit.HttpMethod;
|
4
|
+
|
5
|
+
import java.util.Map;
|
6
|
+
|
7
|
+
public class RequestConfig {
|
8
|
+
public static String DEFAULT_USER_AGENT = "http-kit/2.0";
|
9
|
+
|
10
|
+
final int timeout;
|
11
|
+
final int keepAlive;
|
12
|
+
final Object body;
|
13
|
+
final Map<String, Object> headers;
|
14
|
+
final HttpMethod method;
|
15
|
+
|
16
|
+
public RequestConfig(HttpMethod method, Map<String, Object> headers, Object body,
|
17
|
+
int timeoutMs, int keepAliveMs) {
|
18
|
+
this.timeout = timeoutMs;
|
19
|
+
this.keepAlive = keepAliveMs;
|
20
|
+
this.headers = headers;
|
21
|
+
this.body = body;
|
22
|
+
this.method = method;
|
23
|
+
}
|
24
|
+
|
25
|
+
public RequestConfig() { // for easy test only
|
26
|
+
this(HttpMethod.GET, null, null, 40000, -1);
|
27
|
+
}
|
28
|
+
}
|
Binary file
|
@@ -0,0 +1,160 @@
|
|
1
|
+
package org.httpkit.client;
|
2
|
+
|
3
|
+
import org.httpkit.*;
|
4
|
+
|
5
|
+
import java.io.IOException;
|
6
|
+
import java.io.InputStream;
|
7
|
+
import java.nio.charset.Charset;
|
8
|
+
import java.util.Map;
|
9
|
+
import java.util.TreeMap;
|
10
|
+
import java.util.concurrent.ExecutorService;
|
11
|
+
import java.util.zip.GZIPInputStream;
|
12
|
+
import java.util.zip.Inflater;
|
13
|
+
import java.util.zip.InflaterInputStream;
|
14
|
+
|
15
|
+
import static org.httpkit.HttpUtils.CONTENT_ENCODING;
|
16
|
+
import static org.httpkit.HttpUtils.CONTENT_TYPE;
|
17
|
+
|
18
|
+
class Handler implements Runnable {
|
19
|
+
|
20
|
+
private final int status;
|
21
|
+
private final Map<String, Object> headers;
|
22
|
+
private final Object body;
|
23
|
+
private final IResponseHandler handler;
|
24
|
+
|
25
|
+
public Handler(IResponseHandler handler, int status, Map<String, Object> headers,
|
26
|
+
Object body) {
|
27
|
+
this.handler = handler;
|
28
|
+
this.status = status;
|
29
|
+
this.headers = headers;
|
30
|
+
this.body = body;
|
31
|
+
}
|
32
|
+
|
33
|
+
public Handler(IResponseHandler handler, Throwable e) {
|
34
|
+
this(handler, 0, null, e);
|
35
|
+
}
|
36
|
+
|
37
|
+
public void run() {
|
38
|
+
if (body instanceof Throwable) {
|
39
|
+
handler.onThrowable((Throwable) body);
|
40
|
+
} else {
|
41
|
+
handler.onSuccess(status, headers, body);
|
42
|
+
}
|
43
|
+
}
|
44
|
+
}
|
45
|
+
|
46
|
+
/**
|
47
|
+
* Accumulate all the response, call upper logic at once, for easy use
|
48
|
+
*/
|
49
|
+
public class RespListener implements IRespListener {
|
50
|
+
|
51
|
+
private boolean isText() {
|
52
|
+
if (status.getCode() == 200) {
|
53
|
+
String type = HttpUtils.getStringValue(headers, CONTENT_TYPE);
|
54
|
+
if (type != null) {
|
55
|
+
type = type.toLowerCase();
|
56
|
+
// TODO may miss something
|
57
|
+
return type.contains("text") || type.contains("json") || type.contains("xml");
|
58
|
+
} else {
|
59
|
+
return false;
|
60
|
+
}
|
61
|
+
} else { // non 200: treat as text
|
62
|
+
return true;
|
63
|
+
}
|
64
|
+
}
|
65
|
+
|
66
|
+
private DynamicBytes unzipBody() throws IOException {
|
67
|
+
String encoding = HttpUtils.getStringValue(headers, CONTENT_ENCODING);
|
68
|
+
if (encoding == null || body.length() == 0) {
|
69
|
+
return body;
|
70
|
+
}
|
71
|
+
|
72
|
+
encoding = encoding.toLowerCase();
|
73
|
+
BytesInputStream bis = new BytesInputStream(body.get(), body.length());
|
74
|
+
InputStream is;
|
75
|
+
|
76
|
+
if ("gzip".equals(encoding) || "x-gzip".equals(encoding)) {
|
77
|
+
is = new GZIPInputStream(bis);
|
78
|
+
} else if ("deflate".equals(encoding) || "x-deflate".equals(encoding)) {
|
79
|
+
// http://stackoverflow.com/questions/3932117/handling-http-contentencoding-deflate
|
80
|
+
is = new InflaterInputStream(bis, new Inflater(true));
|
81
|
+
} else {
|
82
|
+
return body; // not compressed
|
83
|
+
}
|
84
|
+
|
85
|
+
DynamicBytes unzipped = new DynamicBytes(body.length() * 5);
|
86
|
+
byte[] buffer = new byte[4096];
|
87
|
+
int read;
|
88
|
+
while ((read = is.read(buffer)) != -1) {
|
89
|
+
unzipped.append(buffer, read);
|
90
|
+
}
|
91
|
+
is.close();
|
92
|
+
return unzipped;
|
93
|
+
}
|
94
|
+
|
95
|
+
private final DynamicBytes body;
|
96
|
+
|
97
|
+
// can be empty
|
98
|
+
private Map<String, Object> headers = new TreeMap<String, Object>();
|
99
|
+
private HttpStatus status;
|
100
|
+
private final IResponseHandler handler;
|
101
|
+
private final IFilter filter;
|
102
|
+
private final ExecutorService pool;
|
103
|
+
final int coercion;
|
104
|
+
|
105
|
+
public RespListener(IResponseHandler handler, IFilter filter, ExecutorService pool, int coercion) {
|
106
|
+
body = new DynamicBytes(1024 * 8);
|
107
|
+
this.filter = filter;
|
108
|
+
this.handler = handler;
|
109
|
+
this.coercion = coercion;
|
110
|
+
this.pool = pool;
|
111
|
+
}
|
112
|
+
|
113
|
+
public void onBodyReceived(byte[] buf, int length) throws AbortException {
|
114
|
+
body.append(buf, length);
|
115
|
+
if (filter != null && !filter.accept(body)) {
|
116
|
+
throw new AbortException("Rejected when reading body, length: " + body.length());
|
117
|
+
}
|
118
|
+
}
|
119
|
+
|
120
|
+
public void onCompleted() {
|
121
|
+
if (status == null) {
|
122
|
+
pool.submit(new Handler(handler, new ProtocolException("No status")));
|
123
|
+
return;
|
124
|
+
}
|
125
|
+
try {
|
126
|
+
DynamicBytes bytes = unzipBody();
|
127
|
+
// 1=> auto, 2=>text, 3=>stream, 4=>byte-array
|
128
|
+
if (coercion == 2 || (coercion == 1 && isText())) {
|
129
|
+
Charset charset = HttpUtils.detectCharset(headers, bytes);
|
130
|
+
String html = new String(bytes.get(), 0, bytes.length(), charset);
|
131
|
+
pool.submit(new Handler(handler, status.getCode(), headers, html));
|
132
|
+
} else {
|
133
|
+
BytesInputStream is = new BytesInputStream(bytes.get(), bytes.length());
|
134
|
+
if (coercion == 4) { // byte-array
|
135
|
+
pool.submit(new Handler(handler, status.getCode(), headers, is.bytes()));
|
136
|
+
} else {
|
137
|
+
pool.submit(new Handler(handler, status.getCode(), headers, is));
|
138
|
+
}
|
139
|
+
}
|
140
|
+
} catch (IOException e) {
|
141
|
+
handler.onThrowable(e);
|
142
|
+
}
|
143
|
+
}
|
144
|
+
|
145
|
+
public void onThrowable(Throwable t) {
|
146
|
+
pool.submit(new Handler(handler, t));
|
147
|
+
}
|
148
|
+
|
149
|
+
public void onHeadersReceived(Map<String, Object> headers) throws AbortException {
|
150
|
+
this.headers = headers;
|
151
|
+
if (filter != null && !filter.accept(headers)) {
|
152
|
+
throw new AbortException("Rejected when header received");
|
153
|
+
}
|
154
|
+
}
|
155
|
+
|
156
|
+
public void onInitialLineReceived(HttpVersion version, HttpStatus status)
|
157
|
+
throws AbortException {
|
158
|
+
this.status = status;
|
159
|
+
}
|
160
|
+
}
|
Binary file
|
@@ -0,0 +1,79 @@
|
|
1
|
+
package org.httpkit.client;
|
2
|
+
|
3
|
+
import javax.net.ssl.*;
|
4
|
+
import java.security.InvalidAlgorithmParameterException;
|
5
|
+
import java.security.KeyStore;
|
6
|
+
import java.security.KeyStoreException;
|
7
|
+
import java.security.cert.CertificateException;
|
8
|
+
import java.security.cert.X509Certificate;
|
9
|
+
|
10
|
+
|
11
|
+
public class SslContextFactory {
|
12
|
+
|
13
|
+
private static final String PROTOCOL = "TLS";
|
14
|
+
private static final SSLContext CLIENT_CONTEXT;
|
15
|
+
|
16
|
+
static {
|
17
|
+
SSLContext clientContext = null;
|
18
|
+
|
19
|
+
try {
|
20
|
+
clientContext = SSLContext.getInstance(PROTOCOL);
|
21
|
+
clientContext.init(null, TrustManagerFactory.getTrustManagers(),
|
22
|
+
null);
|
23
|
+
} catch (Exception e) {
|
24
|
+
throw new Error(
|
25
|
+
"Failed to initialize the client-side SSLContext", e);
|
26
|
+
}
|
27
|
+
|
28
|
+
CLIENT_CONTEXT = clientContext;
|
29
|
+
}
|
30
|
+
|
31
|
+
public static SSLContext getClientContext() {
|
32
|
+
return CLIENT_CONTEXT;
|
33
|
+
}
|
34
|
+
|
35
|
+
public static SSLEngine trustAnybody() {
|
36
|
+
return CLIENT_CONTEXT.createSSLEngine();
|
37
|
+
}
|
38
|
+
}
|
39
|
+
|
40
|
+
|
41
|
+
class TrustManagerFactory extends TrustManagerFactorySpi {
|
42
|
+
|
43
|
+
private static final TrustManager DUMMY_TRUST_MANAGER = new X509TrustManager() {
|
44
|
+
public X509Certificate[] getAcceptedIssuers() {
|
45
|
+
return new X509Certificate[0];
|
46
|
+
}
|
47
|
+
|
48
|
+
public void checkClientTrusted(X509Certificate[] chain,
|
49
|
+
String authType) throws CertificateException {
|
50
|
+
// Always trust
|
51
|
+
}
|
52
|
+
|
53
|
+
public void checkServerTrusted(X509Certificate[] chain,
|
54
|
+
String authType) throws CertificateException {
|
55
|
+
// Always trust
|
56
|
+
}
|
57
|
+
};
|
58
|
+
|
59
|
+
public static TrustManager[] getTrustManagers() {
|
60
|
+
return new TrustManager[]{DUMMY_TRUST_MANAGER};
|
61
|
+
}
|
62
|
+
|
63
|
+
@Override
|
64
|
+
protected TrustManager[] engineGetTrustManagers() {
|
65
|
+
return getTrustManagers();
|
66
|
+
}
|
67
|
+
|
68
|
+
@Override
|
69
|
+
protected void engineInit(KeyStore keystore) throws KeyStoreException {
|
70
|
+
// Unused
|
71
|
+
}
|
72
|
+
|
73
|
+
@Override
|
74
|
+
protected void engineInit(
|
75
|
+
ManagerFactoryParameters managerFactoryParameters)
|
76
|
+
throws InvalidAlgorithmParameterException {
|
77
|
+
// Unused
|
78
|
+
}
|
79
|
+
}
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
@@ -0,0 +1,286 @@
|
|
1
|
+
package org.httpkit.server;
|
2
|
+
|
3
|
+
import clojure.lang.IFn;
|
4
|
+
import clojure.lang.Keyword;
|
5
|
+
import org.httpkit.DynamicBytes;
|
6
|
+
import org.httpkit.HeaderMap;
|
7
|
+
import org.httpkit.HttpVersion;
|
8
|
+
import sun.misc.Unsafe;
|
9
|
+
|
10
|
+
import java.io.IOException;
|
11
|
+
import java.io.InputStream;
|
12
|
+
import java.lang.reflect.Field;
|
13
|
+
import java.net.Socket;
|
14
|
+
import java.nio.ByteBuffer;
|
15
|
+
import java.nio.channels.SelectionKey;
|
16
|
+
import java.nio.channels.SocketChannel;
|
17
|
+
import java.util.Map;
|
18
|
+
|
19
|
+
import static org.httpkit.HttpUtils.*;
|
20
|
+
import static org.httpkit.server.ClojureRing.*;
|
21
|
+
import static org.httpkit.server.WSDecoder.*;
|
22
|
+
|
23
|
+
@SuppressWarnings({"unchecked"})
|
24
|
+
public class AsyncChannel {
|
25
|
+
static final Unsafe unsafe;
|
26
|
+
static final long closedRanOffset;
|
27
|
+
static final long closeHandlerOffset;
|
28
|
+
static final long receiveHandlerOffset;
|
29
|
+
static final long headerSentOffset;
|
30
|
+
|
31
|
+
private final SelectionKey key;
|
32
|
+
private final HttpServer server;
|
33
|
+
|
34
|
+
private HttpRequest request; // package private, for http 1.0 keep-alive
|
35
|
+
|
36
|
+
volatile int closedRan = 0; // 0 => false, 1 => run
|
37
|
+
// streaming
|
38
|
+
private volatile int isHeaderSent = 0;
|
39
|
+
|
40
|
+
private volatile IFn receiveHandler = null;
|
41
|
+
volatile IFn closeHandler = null;
|
42
|
+
|
43
|
+
static {
|
44
|
+
try {
|
45
|
+
// Unsafe instead of AtomicReference to save few bytes of RAM per connection
|
46
|
+
Field field = Unsafe.class.getDeclaredField("theUnsafe");
|
47
|
+
field.setAccessible(true);
|
48
|
+
unsafe = (Unsafe) field.get(null);
|
49
|
+
|
50
|
+
closedRanOffset = unsafe.objectFieldOffset(
|
51
|
+
AsyncChannel.class.getDeclaredField("closedRan"));
|
52
|
+
closeHandlerOffset = unsafe.objectFieldOffset(
|
53
|
+
AsyncChannel.class.getDeclaredField("closeHandler"));
|
54
|
+
receiveHandlerOffset = unsafe.objectFieldOffset(
|
55
|
+
AsyncChannel.class.getDeclaredField("receiveHandler"));
|
56
|
+
headerSentOffset = unsafe.objectFieldOffset(
|
57
|
+
AsyncChannel.class.getDeclaredField("isHeaderSent"));
|
58
|
+
} catch (Exception e) {
|
59
|
+
throw new RuntimeException(e);
|
60
|
+
}
|
61
|
+
}
|
62
|
+
|
63
|
+
|
64
|
+
// messages sent from a WebSocket client should be handled orderly by server
|
65
|
+
// Changed from a Single Thread(IO event thread), no volatile needed
|
66
|
+
LinkingRunnable serialTask;
|
67
|
+
|
68
|
+
public AsyncChannel(SelectionKey key, HttpServer server) {
|
69
|
+
this.key = key;
|
70
|
+
this.server = server;
|
71
|
+
}
|
72
|
+
|
73
|
+
public void reset(HttpRequest request) {
|
74
|
+
this.request = request;
|
75
|
+
serialTask = null;
|
76
|
+
unsafe.putOrderedInt(this, closedRanOffset, 0); // lazySet to false
|
77
|
+
unsafe.putOrderedInt(this, headerSentOffset, 0);
|
78
|
+
|
79
|
+
unsafe.putOrderedObject(this, closeHandlerOffset, null); // lazySet to null
|
80
|
+
unsafe.putOrderedObject(this, receiveHandlerOffset, null); // lazySet to null
|
81
|
+
}
|
82
|
+
|
83
|
+
private static final byte[] finalChunkBytes = "0\r\n\r\n".getBytes();
|
84
|
+
private static final byte[] newLineBytes = "\r\n".getBytes();
|
85
|
+
|
86
|
+
private static ByteBuffer chunkSize(int size) {
|
87
|
+
String s = Integer.toHexString(size) + "\r\n";
|
88
|
+
return ByteBuffer.wrap(s.getBytes());
|
89
|
+
}
|
90
|
+
|
91
|
+
private void firstWrite(Object data, boolean close) throws IOException {
|
92
|
+
ByteBuffer buffers[];
|
93
|
+
int status = 200;
|
94
|
+
Object body = data;
|
95
|
+
HeaderMap headers;
|
96
|
+
if (data instanceof Map) {
|
97
|
+
Map<Keyword, Object> resp = (Map<Keyword, Object>) data;
|
98
|
+
headers = HeaderMap.camelCase((Map) resp.get(HEADERS));
|
99
|
+
status = getStatus(resp);
|
100
|
+
body = resp.get(BODY);
|
101
|
+
} else {
|
102
|
+
headers = new HeaderMap();
|
103
|
+
}
|
104
|
+
|
105
|
+
if (headers.isEmpty()) { // default 200 and text/html
|
106
|
+
headers.put("Content-Type", "text/html; charset=utf-8");
|
107
|
+
}
|
108
|
+
|
109
|
+
if (request.isKeepAlive && request.version == HttpVersion.HTTP_1_0) {
|
110
|
+
headers.put("Connection", "Keep-Alive");
|
111
|
+
}
|
112
|
+
|
113
|
+
if (close) { // normal response, Content-Length. Every http client understand it
|
114
|
+
buffers = HttpEncode(status, headers, body);
|
115
|
+
} else {
|
116
|
+
headers.put("Transfer-Encoding", "chunked"); // first chunk
|
117
|
+
ByteBuffer[] bb = HttpEncode(status, headers, body);
|
118
|
+
if (body == null) {
|
119
|
+
buffers = bb;
|
120
|
+
} else {
|
121
|
+
buffers = new ByteBuffer[]{bb[0], chunkSize(bb[1].remaining()), bb[1],
|
122
|
+
ByteBuffer.wrap(newLineBytes)};
|
123
|
+
}
|
124
|
+
}
|
125
|
+
if (close) {
|
126
|
+
onClose(0);
|
127
|
+
}
|
128
|
+
server.tryWrite(key, buffers);
|
129
|
+
}
|
130
|
+
|
131
|
+
|
132
|
+
private void writeChunk(Object body, boolean close) throws IOException {
|
133
|
+
if (body instanceof Map) { // only get body if a map
|
134
|
+
body = ((Map<Keyword, Object>) body).get(BODY);
|
135
|
+
}
|
136
|
+
if (body != null) { // null is ignored
|
137
|
+
ByteBuffer buffers[];
|
138
|
+
ByteBuffer t = bodyBuffer(body);
|
139
|
+
if (t.hasRemaining()) {
|
140
|
+
ByteBuffer size = chunkSize(t.remaining());
|
141
|
+
buffers = new ByteBuffer[]{size, t, ByteBuffer.wrap(newLineBytes)};
|
142
|
+
server.tryWrite(key, buffers);
|
143
|
+
}
|
144
|
+
}
|
145
|
+
if (close) {
|
146
|
+
serverClose(0);
|
147
|
+
}
|
148
|
+
}
|
149
|
+
|
150
|
+
public void setReceiveHandler(IFn fn) {
|
151
|
+
if (!unsafe.compareAndSwapObject(this, receiveHandlerOffset, null, fn)) {
|
152
|
+
throw new IllegalStateException("receive handler exist: " + receiveHandler);
|
153
|
+
}
|
154
|
+
}
|
155
|
+
|
156
|
+
public void messageReceived(final Object mesg) {
|
157
|
+
IFn f = receiveHandler;
|
158
|
+
if (f != null) {
|
159
|
+
f.invoke(mesg); // byte[] or String
|
160
|
+
}
|
161
|
+
}
|
162
|
+
|
163
|
+
public void sendHandshake(Map<String, Object> headers) {
|
164
|
+
HeaderMap map = HeaderMap.camelCase(headers);
|
165
|
+
server.tryWrite(key, HttpEncode(101, map, null));
|
166
|
+
}
|
167
|
+
|
168
|
+
public void setCloseHandler(IFn fn) {
|
169
|
+
if (!unsafe.compareAndSwapObject(this, closeHandlerOffset, null, fn)) { // only once
|
170
|
+
throw new IllegalStateException("close handler exist: " + closeHandler);
|
171
|
+
}
|
172
|
+
if (closedRan == 1) { // no handler, but already closed
|
173
|
+
fn.invoke(K_UNKNOWN);
|
174
|
+
}
|
175
|
+
}
|
176
|
+
|
177
|
+
public void onClose(int status) {
|
178
|
+
if (unsafe.compareAndSwapInt(this, closedRanOffset, 0, 1)) {
|
179
|
+
IFn f = closeHandler;
|
180
|
+
if (f != null) {
|
181
|
+
f.invoke(readable(status));
|
182
|
+
}
|
183
|
+
}
|
184
|
+
}
|
185
|
+
|
186
|
+
// also sent CloseFrame a final Chunk
|
187
|
+
public boolean serverClose(int status) {
|
188
|
+
if (!unsafe.compareAndSwapInt(this, closedRanOffset, 0, 1)) {
|
189
|
+
return false; // already closed
|
190
|
+
}
|
191
|
+
if (isWebSocket()) {
|
192
|
+
server.tryWrite(key, WsEncode(OPCODE_CLOSE, ByteBuffer.allocate(2)
|
193
|
+
.putShort((short) status).array()));
|
194
|
+
} else {
|
195
|
+
server.tryWrite(key, ByteBuffer.wrap(finalChunkBytes));
|
196
|
+
}
|
197
|
+
IFn f = closeHandler;
|
198
|
+
if (f != null) {
|
199
|
+
f.invoke(readable(0)); // server close is 0
|
200
|
+
}
|
201
|
+
return true;
|
202
|
+
}
|
203
|
+
|
204
|
+
public boolean send(Object data, boolean close) throws IOException {
|
205
|
+
if (closedRan == 1) {
|
206
|
+
return false;
|
207
|
+
}
|
208
|
+
|
209
|
+
if (isWebSocket()) {
|
210
|
+
if (data instanceof Map) { // only get the :body if map
|
211
|
+
Object tmp = ((Map<Keyword, Object>) data).get(BODY);
|
212
|
+
if (tmp != null) { // save contains(BODY) && get(BODY)
|
213
|
+
data = tmp;
|
214
|
+
}
|
215
|
+
}
|
216
|
+
|
217
|
+
if (data instanceof String) { // null is not allowed
|
218
|
+
server.tryWrite(key, WsEncode(OPCODE_TEXT, ((String) data).getBytes(UTF_8)));
|
219
|
+
} else if (data instanceof byte[]) {
|
220
|
+
server.tryWrite(key, WsEncode(OPCODE_BINARY, (byte[]) data));
|
221
|
+
} else if (data instanceof InputStream) {
|
222
|
+
DynamicBytes bytes = readAll((InputStream) data);
|
223
|
+
server.tryWrite(key, WsEncode(OPCODE_BINARY, bytes.get(), bytes.length()));
|
224
|
+
} else if (data != null) { // ignore null
|
225
|
+
String mesg = "send! called with data: " + data.toString() +
|
226
|
+
"(" + data.getClass() + "), but only string, byte[], InputStream expected";
|
227
|
+
throw new IllegalArgumentException(mesg);
|
228
|
+
}
|
229
|
+
|
230
|
+
if (close) {
|
231
|
+
serverClose(1000);
|
232
|
+
}
|
233
|
+
} else {
|
234
|
+
if (isHeaderSent == 1) { // HTTP Streaming
|
235
|
+
writeChunk(data, close);
|
236
|
+
} else {
|
237
|
+
isHeaderSent = 1;
|
238
|
+
firstWrite(data, close);
|
239
|
+
}
|
240
|
+
}
|
241
|
+
return true;
|
242
|
+
}
|
243
|
+
|
244
|
+
public String toString() {
|
245
|
+
Socket s = ((SocketChannel) key.channel()).socket();
|
246
|
+
return s.getLocalSocketAddress() + "<->" + s.getRemoteSocketAddress();
|
247
|
+
}
|
248
|
+
|
249
|
+
public boolean isWebSocket() {
|
250
|
+
return key.attachment() instanceof WsAtta;
|
251
|
+
}
|
252
|
+
|
253
|
+
public boolean isClosed() {
|
254
|
+
return closedRan == 1;
|
255
|
+
}
|
256
|
+
|
257
|
+
static Keyword K_BY_SERVER = Keyword.intern("server-close");
|
258
|
+
static Keyword K_CLIENT_CLOSED = Keyword.intern("client-close");
|
259
|
+
|
260
|
+
// http://datatracker.ietf.org/doc/rfc6455/?include_text=1
|
261
|
+
// 7.4.1. Defined Status Codes
|
262
|
+
static Keyword K_WS_1000 = Keyword.intern("normal");
|
263
|
+
static Keyword K_WS_1001 = Keyword.intern("going-away");
|
264
|
+
static Keyword K_WS_1002 = Keyword.intern("protocol-error");
|
265
|
+
static Keyword K_WS_1003 = Keyword.intern("unsupported");
|
266
|
+
static Keyword K_UNKNOWN = Keyword.intern("unknown");
|
267
|
+
|
268
|
+
private static Keyword readable(int status) {
|
269
|
+
switch (status) {
|
270
|
+
case 0:
|
271
|
+
return K_BY_SERVER;
|
272
|
+
case -1:
|
273
|
+
return K_CLIENT_CLOSED;
|
274
|
+
case 1000:
|
275
|
+
return K_WS_1000;
|
276
|
+
case 1001:
|
277
|
+
return K_WS_1001;
|
278
|
+
case 1002:
|
279
|
+
return K_WS_1002;
|
280
|
+
case 1003:
|
281
|
+
return K_WS_1003;
|
282
|
+
default:
|
283
|
+
return K_UNKNOWN;
|
284
|
+
}
|
285
|
+
}
|
286
|
+
}
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|