├── .gitignore
├── README.md
├── pom.xml
├── script
└── run.sh
└── src
├── main
├── java
│ └── me
│ │ └── shenfeng
│ │ └── http
│ │ ├── ButterflySoftCache.java
│ │ ├── ChangeRequest.java
│ │ ├── NioHttpServer.java
│ │ ├── RequestHandler.java
│ │ ├── RequestHeaderDecoder.java
│ │ ├── ResponceHeaderBuilder.java
│ │ └── Util.java
└── resources
│ ├── index.tpl
│ ├── log4j.xml
│ └── mime.types
└── test
└── java
└── me
└── shenfeng
└── http
├── RequestHeaderDecoderTest.java
└── UtilTest.java
/.gitignore:
--------------------------------------------------------------------------------
1 | .classpath
2 | .project
3 | .settings
4 | target/
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | A simple JAVA HTTP server
2 | ==================================
3 |
4 | A simple java HTTP server. Understand GET and HEAD. It will do
5 | directory list as well.
6 |
7 | I first write it as an exercise for leaning JAVA NIO in late 2010,
8 | `python -m SimpleHTTPSever [port]` is very handy, but a little slow for
9 | me, so I use this as a replacement for daily use.
10 |
11 | ### how to run
12 | 1. install maven
13 | 2. `mvn package`
14 | 3. `./script/run.sh [port] [www-root]`
15 |
16 | ### TODO
17 | 1. More MIME mapping
18 | 2. Delivering large file
19 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
3 | 4.0.0
4 | me.shenfeng.http
5 | httpserver
6 | jar
7 | 1.0-SNAPSHOT
8 | httpserver
9 | http://maven.apache.org
10 |
11 |
12 | log4j
13 | log4j
14 | 1.2.16
15 |
16 |
17 | junit
18 | junit
19 | 4.8.1
20 | test
21 |
22 |
23 | com.samskivert
24 | jmustache
25 | 1.3
26 |
27 |
28 |
29 |
30 |
31 | org.apache.maven.plugins
32 | maven-jar-plugin
33 | 2.3.1
34 |
35 |
36 |
37 | true
38 | me.shenfeng.http.NioHttpServer
39 |
40 |
41 |
42 |
43 |
44 | maven-assembly-plugin
45 |
46 |
47 | jar-with-dependencies
48 |
49 |
50 |
51 |
52 | org.apache.maven.plugins
53 | maven-dependency-plugin
54 |
55 |
56 | copy-dependencies
57 | package
58 |
59 | copy-dependencies
60 |
61 |
62 | target
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/script/run.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | mvn package && java -cp target/*: me.shenfeng.http.NioHttpServer $@
3 |
--------------------------------------------------------------------------------
/src/main/java/me/shenfeng/http/ButterflySoftCache.java:
--------------------------------------------------------------------------------
1 | package me.shenfeng.http;
2 |
3 | import java.lang.ref.ReferenceQueue;
4 | import java.lang.ref.SoftReference;
5 | import java.util.Map;
6 | import java.util.concurrent.ConcurrentHashMap;
7 |
8 | public class ButterflySoftCache {
9 |
10 | public static class CacheEntry {
11 | public byte[] header;
12 | public byte[] body;
13 |
14 | public CacheEntry(byte[] header, byte[] body) {
15 | this.header = header;
16 | this.body = body;
17 | }
18 |
19 | }
20 |
21 | public static class MapEntry extends SoftReference {
22 |
23 | String key;
24 |
25 | public MapEntry(String key, CacheEntry referent, ReferenceQueue q) {
26 | super(referent, q);
27 | this.key = key;
28 | }
29 |
30 | }
31 |
32 | private ReferenceQueue queue = new ReferenceQueue();
33 |
34 | /**
35 | * the back map used
36 | */
37 | private Map map = new ConcurrentHashMap();
38 |
39 | public CacheEntry get(String key) {
40 | CacheEntry result = null;
41 | MapEntry entry = map.get(key);
42 | if (entry != null) {
43 | result = entry.get();
44 | if (result == null) {
45 | map.remove(entry.key);
46 | }
47 | }
48 | return result;
49 | }
50 |
51 | private void processQueue() {
52 | MapEntry entry;
53 | while ((entry = (MapEntry) queue.poll()) != null) {
54 | map.remove(entry.key);
55 | }
56 | }
57 |
58 | public void put(String key, byte[] header, byte[] body) {
59 | processQueue();
60 | map.put(key, new MapEntry(key, new CacheEntry(header, body), queue));
61 | }
62 |
63 | /**
64 | * debug help
65 | */
66 | @Override
67 | public String toString() {
68 | StringBuilder sb = new StringBuilder();
69 | int memory = 0;
70 | for (String key : map.keySet()) {
71 | CacheEntry entry = map.get(key).get();
72 | if (entry != null) {
73 | int size = 0;
74 | if (entry.body != null)
75 | size += entry.body.length;
76 | if (entry.header != null)
77 | size += entry.header.length;
78 |
79 | sb.append(key).append("\t").append(size).append("\t").append(size / 1024).append("k\n");
80 | memory += size;
81 | }
82 | }
83 |
84 | StringBuilder sb2 = new StringBuilder();
85 | sb2.append("cache item count: ").append(map.size()).append("\n");
86 | sb2.append("memory size:\t").append(memory).append("\t").append((double) memory / 1024).append("k\n");
87 | sb2.append(sb);
88 | return sb2.toString();
89 |
90 | }
91 |
92 | }
93 |
--------------------------------------------------------------------------------
/src/main/java/me/shenfeng/http/ChangeRequest.java:
--------------------------------------------------------------------------------
1 | package me.shenfeng.http;
2 |
3 | import java.nio.channels.SocketChannel;
4 |
5 | public class ChangeRequest {
6 |
7 | public static final int REGISTER = 1;
8 | public static final int CHANGEOPS = 2;
9 |
10 | public SocketChannel socket;
11 | public int type;
12 | public int ops;
13 |
14 | public ChangeRequest(SocketChannel socket, int type, int ops) {
15 | this.socket = socket;
16 | this.type = type;
17 | this.ops = ops;
18 | }
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/java/me/shenfeng/http/NioHttpServer.java:
--------------------------------------------------------------------------------
1 | package me.shenfeng.http;
2 |
3 | import java.io.File;
4 | import java.io.IOException;
5 | import java.net.InetAddress;
6 | import java.net.InetSocketAddress;
7 | import java.nio.ByteBuffer;
8 | import java.nio.channels.SelectionKey;
9 | import java.nio.channels.Selector;
10 | import java.nio.channels.ServerSocketChannel;
11 | import java.nio.channels.SocketChannel;
12 | import java.util.ArrayList;
13 | import java.util.HashMap;
14 | import java.util.Iterator;
15 | import java.util.LinkedList;
16 | import java.util.List;
17 | import java.util.Map;
18 |
19 | import org.apache.log4j.Logger;
20 |
21 |
22 | public class NioHttpServer implements Runnable {
23 |
24 | private static Logger logger = Logger.getLogger(NioHttpServer.class);
25 |
26 | public static void main(String[] args) throws IOException {
27 |
28 | String root = new File(".").getAbsolutePath();
29 | int port = 8080;
30 | if (args.length > 0)
31 | port = Integer.parseInt(args[0]);
32 |
33 | if (args.length > 1)
34 | root = args[1];
35 | logger.info("listenning at *." + port + "; root: " + root);
36 | NioHttpServer server = new NioHttpServer(null, port);
37 | int cpu = Runtime.getRuntime().availableProcessors();
38 | ButterflySoftCache cache = new ButterflySoftCache();
39 | // int i = 0;
40 | for (int i = 0; i < cpu; ++i) {
41 | RequestHandler handler = new RequestHandler(server, root, cache);
42 | server.addRequestHanlder(handler);
43 | new Thread(handler, "worker" + i).start();
44 | }
45 |
46 | new Thread(server, "selector").start();
47 | }
48 |
49 | private ServerSocketChannel serverChannel;
50 | private Selector selector;
51 | private ByteBuffer readBuffer = ByteBuffer.allocate(8912);
52 | private List changeRequests = new LinkedList();
53 | private Map> pendingSent = new HashMap>();
54 | private List requestHandlers = new ArrayList();
55 |
56 | public NioHttpServer(InetAddress address, int port) throws IOException {
57 | selector = Selector.open();
58 | serverChannel = ServerSocketChannel.open();
59 | serverChannel.configureBlocking(false);
60 | serverChannel.socket().bind(new InetSocketAddress(address, port));
61 | serverChannel.register(selector, SelectionKey.OP_ACCEPT);
62 |
63 | }
64 |
65 | private void accept(SelectionKey key) throws IOException {
66 | SocketChannel socketChannel = serverChannel.accept();
67 | // logger.info("new connection:\t" + socketChannel);
68 | socketChannel.configureBlocking(false);
69 | socketChannel.register(selector, SelectionKey.OP_READ);
70 | }
71 |
72 | public void addRequestHanlder(RequestHandler handler) {
73 | requestHandlers.add(handler);
74 | }
75 |
76 | private void read(SelectionKey key) throws IOException {
77 | SocketChannel socketChannel = (SocketChannel) key.channel();
78 | readBuffer.clear();
79 | int numRead;
80 | try {
81 | numRead = socketChannel.read(readBuffer);
82 |
83 | } catch (IOException e) {
84 | // the remote forcibly closed the connection
85 | key.cancel();
86 | socketChannel.close();
87 | // logger.info("closed by exception" + socketChannel);
88 | return;
89 | }
90 |
91 | if (numRead == -1) {
92 | // remote entity shut the socket down cleanly.
93 | socketChannel.close();
94 | key.cancel();
95 | // logger.info("closed by shutdown" + socketChannel);
96 | return;
97 | }
98 |
99 | int worker = socketChannel.hashCode() % requestHandlers.size();
100 | if (logger.isDebugEnabled()) {
101 | logger.debug(selector.keys().size() + "\t" + worker + "\t"
102 | + socketChannel);
103 | }
104 | requestHandlers.get(worker).processData(socketChannel,
105 | readBuffer.array(), numRead);
106 | }
107 |
108 | @Override
109 | public void run() {
110 | SelectionKey key = null;
111 | while (true) {
112 | try {
113 | synchronized (changeRequests) {
114 | for (ChangeRequest request : changeRequests) {
115 | switch (request.type) {
116 | case ChangeRequest.CHANGEOPS:
117 | key = request.socket.keyFor(selector);
118 | if (key != null && key.isValid()) {
119 | key.interestOps(request.ops);
120 | }
121 | break;
122 | }
123 | }
124 | changeRequests.clear();
125 | }
126 |
127 | selector.select();
128 | Iterator selectedKeys = selector.selectedKeys()
129 | .iterator();
130 | while (selectedKeys.hasNext()) {
131 | key = selectedKeys.next();
132 | selectedKeys.remove();
133 | if (!key.isValid()) {
134 | continue;
135 | }
136 | if (key.isAcceptable()) {
137 | accept(key);
138 | } else if (key.isReadable()) {
139 | read(key);
140 | } else if (key.isWritable()) {
141 | write(key);
142 | }
143 | }
144 | } catch (Exception e) {
145 | if (key != null) {
146 | key.cancel();
147 | Util.closeQuietly(key.channel());
148 | }
149 | logger.error("closed" + key.channel(), e);
150 | }
151 | }
152 |
153 | }
154 |
155 | public void send(SocketChannel socket, byte[] data) {
156 | synchronized (changeRequests) {
157 | changeRequests.add(new ChangeRequest(socket,
158 | ChangeRequest.CHANGEOPS, SelectionKey.OP_WRITE));
159 | synchronized (pendingSent) {
160 | List queue = pendingSent.get(socket);
161 | if (queue == null) {
162 | queue = new ArrayList();
163 | pendingSent.put(socket, queue);
164 | }
165 | queue.add(ByteBuffer.wrap(data));
166 | }
167 | }
168 |
169 | selector.wakeup();
170 | }
171 |
172 | private void write(SelectionKey key) throws IOException {
173 | SocketChannel socketChannel = (SocketChannel) key.channel();
174 | synchronized (pendingSent) {
175 | List queue = pendingSent.get(socketChannel);
176 | while (!queue.isEmpty()) {
177 | ByteBuffer buf = queue.get(0);
178 | socketChannel.write(buf);
179 | // have more to send
180 | if (buf.remaining() > 0) {
181 | break;
182 | }
183 | queue.remove(0);
184 | }
185 | if (queue.isEmpty()) {
186 | key.interestOps(SelectionKey.OP_READ);
187 | }
188 | }
189 | }
190 | }
191 |
--------------------------------------------------------------------------------
/src/main/java/me/shenfeng/http/RequestHandler.java:
--------------------------------------------------------------------------------
1 | package me.shenfeng.http;
2 |
3 | import static me.shenfeng.http.ResponceHeaderBuilder.ACCEPT_ENCODING;
4 | import static me.shenfeng.http.ResponceHeaderBuilder.CONNECTION;
5 | import static me.shenfeng.http.ResponceHeaderBuilder.CONTENT_ENCODING;
6 | import static me.shenfeng.http.ResponceHeaderBuilder.CONTENT_LENGTH;
7 | import static me.shenfeng.http.ResponceHeaderBuilder.CONTENT_TYPE;
8 | import static me.shenfeng.http.ResponceHeaderBuilder.GZIP;
9 | import static me.shenfeng.http.ResponceHeaderBuilder.KEEP_ALIVE;
10 | import static me.shenfeng.http.ResponceHeaderBuilder.LAST_MODIFIED;
11 | import static me.shenfeng.http.ResponceHeaderBuilder.NOT_FOUND_404;
12 | import static me.shenfeng.http.ResponceHeaderBuilder.SERVER_ERROR_500;
13 |
14 | import java.io.File;
15 | import java.io.IOException;
16 | import java.nio.channels.SocketChannel;
17 | import java.text.DateFormat;
18 | import java.text.SimpleDateFormat;
19 | import java.util.ArrayList;
20 | import java.util.Date;
21 | import java.util.List;
22 | import java.util.Locale;
23 | import java.util.Map;
24 | import java.util.TimeZone;
25 | import java.util.WeakHashMap;
26 |
27 | import me.shenfeng.http.RequestHeaderDecoder.Verb;
28 |
29 | import org.apache.log4j.Logger;
30 |
31 |
32 | public class RequestHandler implements Runnable {
33 |
34 | private static final DateFormat formater = new SimpleDateFormat(
35 | "EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
36 | static {
37 | formater.setTimeZone(TimeZone.getTimeZone("GMT"));
38 | }
39 | private static final Logger logger = Logger.getLogger(RequestHandler.class);
40 | private ButterflySoftCache cache;
41 | private File currentFile;
42 | private Date lastModified;
43 | private List pendingRequestSegment = new ArrayList();
44 | private Map requestMap = new WeakHashMap();
45 | private NioHttpServer server;
46 | private String serverRoot;
47 | private String acceptEncoding;
48 |
49 | /**
50 | *
51 | * @param server
52 | * {@link NioHttpServer} the server
53 | * @param wwwroot
54 | * wwwroot
55 | * @param cache
56 | * cache implementation
57 | */
58 | public RequestHandler(NioHttpServer server, String wwwroot,
59 | ButterflySoftCache cache) {
60 | this.cache = cache;
61 | this.serverRoot = wwwroot;
62 | this.server = server;
63 | }
64 |
65 | public void processData(SocketChannel client, byte[] data, int count) {
66 |
67 | byte[] dataCopy = new byte[count];
68 | System.arraycopy(data, 0, dataCopy, 0, count);
69 |
70 | synchronized (pendingRequestSegment) {
71 | // add data
72 | pendingRequestSegment
73 | .add(new RequestSegmentHeader(client, dataCopy));
74 | pendingRequestSegment.notify();
75 | }
76 | }
77 |
78 | @Override
79 | public void run() {
80 |
81 | RequestSegmentHeader requestData = null;
82 | RequestHeaderDecoder header = null;
83 | ResponceHeaderBuilder builder = new ResponceHeaderBuilder();
84 | byte[] head = null;
85 | byte[] body = null;
86 | String file = null;
87 | String mime = null;
88 | boolean zip = false;
89 |
90 | // wait for data
91 | while (true) {
92 |
93 | synchronized (pendingRequestSegment) {
94 | while (pendingRequestSegment.isEmpty()) {
95 | try {
96 | pendingRequestSegment.wait();
97 | } catch (InterruptedException e) {
98 | }
99 | }
100 | requestData = pendingRequestSegment.remove(0);
101 | }
102 |
103 | header = requestMap.get(requestData.client);
104 | if (header == null) {
105 | header = new RequestHeaderDecoder();
106 | requestMap.put(requestData.client, header);
107 | }
108 | try {
109 | if (header.appendSegment(requestData.data)) {
110 | file = serverRoot + header.getResouce();
111 | currentFile = new File(file);
112 | mime = Util.getContentType(currentFile);
113 | // logger.info(currentFile + "\t" + mime);
114 | acceptEncoding = header.getHeader(ACCEPT_ENCODING);
115 | // gzip text
116 | zip = mime.contains("text")
117 | && acceptEncoding != null
118 | && (acceptEncoding.contains("gzip") || acceptEncoding
119 | .contains("gzip"));
120 | builder.clear(); // get ready for next request;
121 |
122 | // always keep alive
123 | builder.addHeader(CONNECTION, KEEP_ALIVE);
124 | builder.addHeader(CONTENT_TYPE, mime);
125 |
126 | // response body byte, exception throws here
127 | body = Util.file2ByteArray(currentFile, zip);
128 | builder.addHeader(CONTENT_LENGTH, body.length);
129 | if (zip) {
130 | // add zip header
131 | builder.addHeader(CONTENT_ENCODING, GZIP);
132 | }
133 |
134 | // last modified header
135 | lastModified = new Date(currentFile.lastModified());
136 | builder.addHeader(LAST_MODIFIED,
137 | formater.format(lastModified));
138 |
139 | // response header byte
140 | head = builder.getHeader();
141 |
142 | // data is prepared, send out to the client
143 | server.send(requestData.client, head);
144 | if (body != null && header.getVerb() == Verb.GET)
145 | server.send(requestData.client, body);
146 |
147 | logger.info(header.getResouce() + " 200");
148 |
149 | }
150 | } catch (IOException e) {
151 | builder.addHeader(CONTENT_LENGTH, 0);
152 | builder.setStatus(NOT_FOUND_404);
153 | head = builder.getHeader();
154 | server.send(requestData.client, head);
155 | // cache 404 if case client make a mistake again
156 | cache.put(file, head, body);
157 | logger.error(header.getResouce() + " 404");
158 |
159 | } catch (Exception e) {
160 | // any other, it's a 505 error
161 | builder.addHeader(CONTENT_LENGTH, 0);
162 | builder.setStatus(SERVER_ERROR_500);
163 | head = builder.getHeader();
164 | server.send(requestData.client, head);
165 | logger.error("505 error", e);
166 | }
167 | }
168 | }
169 | }
170 |
171 | class RequestSegmentHeader {
172 | SocketChannel client;
173 | byte[] data;
174 |
175 | public RequestSegmentHeader(SocketChannel client, byte[] data) {
176 | this.client = client;
177 | this.data = data;
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/src/main/java/me/shenfeng/http/RequestHeaderDecoder.java:
--------------------------------------------------------------------------------
1 | package me.shenfeng.http;
2 |
3 | import java.nio.ByteBuffer;
4 | import java.nio.CharBuffer;
5 | import java.nio.charset.Charset;
6 | import java.nio.charset.CharsetDecoder;
7 | import java.util.Map;
8 | import java.util.Set;
9 | import java.util.TreeMap;
10 |
11 | public class RequestHeaderDecoder {
12 |
13 | public static enum Verb {
14 | CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE
15 | }
16 |
17 | public static enum Version {
18 | HTTP10, HTTP11
19 | }
20 |
21 | private static CharsetDecoder decoder = Charset.forName("ISO-8859-1")
22 | .newDecoder();
23 | private static final byte[] END = new byte[] { 13, 10, 13, 10 };
24 | private static final byte[] GET = new byte[] { 71, 69, 84, 32 };
25 | private static final byte[] HEAD = new byte[] { 72, 69, 65, 68 };
26 |
27 | // private Version version;
28 | private boolean begin = false;
29 |
30 | private CharBuffer charBuffer = ByteBuffer.allocate(2048).asCharBuffer();
31 |
32 | private Map headerMap = new TreeMap();
33 |
34 | private String resouce;
35 | private Verb verb;
36 |
37 | public boolean appendSegment(byte[] segment) {
38 | int beginIndex = 0;
39 |
40 | if (begin == false) {
41 |
42 | if ((beginIndex = Util.subArray(segment, GET, 0)) != segment.length) {
43 | begin = true;
44 | headerMap.clear();
45 | verb = Verb.GET;
46 |
47 | } else if ((beginIndex = Util.subArray(segment, HEAD, 0)) != segment.length) {
48 | begin = true;
49 | headerMap.clear();
50 | verb = Verb.HEAD;
51 |
52 | } else {
53 | // not begin yet, and find no begin, just return false;
54 | return false;
55 |
56 | }
57 | }
58 |
59 | int endIndex = Util.subArrayFromEnd(segment, END, 0);
60 | ByteBuffer b = ByteBuffer.wrap(segment, beginIndex, endIndex);
61 | decoder.decode(b, charBuffer, endIndex != segment.length);
62 | if (endIndex != segment.length) {
63 | extractValueAndReset();
64 | return true;
65 | }
66 | return false;
67 | }
68 |
69 | private void extractValueAndReset() {
70 | charBuffer.flip();
71 | String head = charBuffer.toString();
72 | String[] lines = head.split("\r\n");
73 | String[] split = lines[0].split(" ");
74 |
75 | resouce = split[1];
76 |
77 | for (int i = 1; i < lines.length; ++i) {
78 | String[] temp = lines[i].split(":");
79 | headerMap.put(temp[0].trim(), temp[1].trim());
80 | }
81 |
82 | charBuffer.clear();
83 | decoder.reset();
84 | begin = false;
85 | }
86 |
87 | public String getHeader(String key) {
88 | return headerMap.get(key);
89 | }
90 |
91 | public Set getHeaders() {
92 | return headerMap.keySet();
93 | }
94 |
95 | public String getResouce() {
96 | return resouce;
97 | }
98 |
99 | /**
100 | *
101 | * @return currently, only GET [71,69,84,32],and HEAD [72, 69, 65, 68] is
102 | * supported
103 | */
104 | public Verb getVerb() {
105 | return verb;
106 | }
107 |
108 | public Version getVersion() {
109 | throw new RuntimeException("not implement yet");
110 | // return version;
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/src/main/java/me/shenfeng/http/ResponceHeaderBuilder.java:
--------------------------------------------------------------------------------
1 | package me.shenfeng.http;
2 |
3 | import java.util.Map;
4 | import java.util.Set;
5 | import java.util.TreeMap;
6 |
7 | public class ResponceHeaderBuilder {
8 | public static final String OK_200 = "HTTP/1.1 200 OK";
9 | public static final String NEWLINE = "\r\n";
10 | public static final String NOT_FOUND_404 = "HTTP/1.1 404 Not Find";
11 | public static final String SERVER_ERROR_500 = "HTTP/1.1 500 Internal Server Error";
12 | public static final String CONTENT_TYPE = "Content-Type";
13 | public static final String CONNECTION = "Connection";
14 | public static final String CONTENT_LENGTH = "Content-Length";
15 | public static final String KEEP_ALIVE = "keep-alive";
16 | public static final String CONTENT_ENCODING = "Content-Encoding";
17 | public static final String ACCEPT_ENCODING = "Accept-Encoding";
18 | public static final String LAST_MODIFIED = "Last-Modified";
19 | public static final String GZIP = "gzip";
20 |
21 | private String status;
22 | private Map header = new TreeMap();
23 |
24 | /**
25 | * status default to 200
26 | */
27 | public ResponceHeaderBuilder() {
28 | status = OK_200;
29 | }
30 |
31 | public ResponceHeaderBuilder addHeader(String key, Object value) {
32 | header.put(key, value);
33 | return this;
34 | }
35 |
36 | public void clear() {
37 | status = OK_200;
38 | header.clear();
39 | }
40 |
41 | public byte[] getHeader() {
42 | return toString().getBytes();
43 | }
44 |
45 | public ResponceHeaderBuilder setStatus(String status) {
46 | this.status = status;
47 | return this;
48 | }
49 |
50 | @Override
51 | public String toString() {
52 |
53 | StringBuilder sb = new StringBuilder(120);
54 | sb.append(status).append(NEWLINE);
55 | Set keySet = header.keySet();
56 | for (String key : keySet) {
57 | sb.append(key).append(": ").append(header.get(key)).append(NEWLINE);
58 | }
59 | sb.append(NEWLINE); // empty line;
60 | return sb.toString();
61 | }
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/src/main/java/me/shenfeng/http/Util.java:
--------------------------------------------------------------------------------
1 | package me.shenfeng.http;
2 |
3 | import java.io.BufferedInputStream;
4 | import java.io.BufferedReader;
5 | import java.io.ByteArrayOutputStream;
6 | import java.io.Closeable;
7 | import java.io.File;
8 | import java.io.FileInputStream;
9 | import java.io.IOException;
10 | import java.io.InputStream;
11 | import java.io.InputStreamReader;
12 | import java.text.DateFormat;
13 | import java.text.SimpleDateFormat;
14 | import java.util.ArrayList;
15 | import java.util.Collections;
16 | import java.util.Date;
17 | import java.util.HashMap;
18 | import java.util.List;
19 | import java.util.Map;
20 | import java.util.zip.GZIPOutputStream;
21 |
22 | import com.samskivert.mustache.Mustache;
23 |
24 | class FileItem implements Comparable{
25 | public final String href;
26 | public final String name;
27 | public final String size;
28 | public final String mtime;
29 |
30 | public FileItem(String href, String name, String size, String mtime) {
31 | this.href = href;
32 | this.name = name;
33 | this.size = size;
34 | this.mtime = mtime;
35 | }
36 | @Override
37 | public int compareTo(FileItem o) {
38 | return name.compareTo(o.name);
39 | }
40 | }
41 |
42 | public class Util {
43 |
44 | private static String defaultType = "application/octet-stream";
45 | private static String mapFile = "mime.types";
46 | private static String indexTmpl = "index.tpl";
47 | private static DateFormat df = new SimpleDateFormat("yyyy-HH-dd HH:mm:ss");
48 |
49 | public static String getExtension(File file) {
50 | String name = file.getName();
51 | int index = name.lastIndexOf('.');
52 | if (index != -1)
53 | return name.substring(index + 1).toLowerCase();
54 | else
55 | return "";
56 |
57 | }
58 |
59 | public static Object listDir(final File folder) {
60 | File[] files = folder.listFiles();
61 | final List fileItems = new ArrayList();
62 | for (File file : files) {
63 | String href = file.isDirectory() ? file.getName() + "/" : file
64 | .getName();
65 | String mtime = df.format(new Date(file.lastModified()));
66 | fileItems.add(new FileItem(href, file.getName(),
67 | file.length() + "", mtime));
68 | }
69 | Collections.sort(fileItems);
70 | return new Object() {
71 | Object files = fileItems;
72 | Object dir = folder.getName();
73 | };
74 | }
75 |
76 | public static byte[] directoryList(File dir, boolean zip) {
77 | StringBuilder sb = new StringBuilder(300);
78 |
79 | InputStream ins = Util.class.getClassLoader().getResourceAsStream(
80 | indexTmpl);
81 | BufferedReader br = new BufferedReader(new InputStreamReader(ins));
82 | String line = null;
83 | try {
84 | while ((line = br.readLine()) != null) {
85 | sb.append(line);
86 | sb.append("\n");
87 | }
88 | } catch (IOException e) {
89 | }
90 |
91 | String html = Mustache.compiler().compile(sb.toString())
92 | .execute(listDir(dir));
93 |
94 | if (zip) {
95 | try {
96 | ByteArrayOutputStream baos = new ByteArrayOutputStream(8912);
97 | GZIPOutputStream gzip = new GZIPOutputStream(baos);
98 | gzip.write(html.getBytes());
99 | closeQuietly(gzip);
100 | return baos.toByteArray();
101 | } catch (IOException e) {
102 | }
103 | } else {
104 | return html.getBytes();
105 | }
106 | return new byte[] {};
107 | }
108 |
109 | public static String getContentType(File file) {
110 |
111 | if (file.isDirectory())
112 | return "text/html";
113 |
114 | InputStream ins = Util.class.getClassLoader().getResourceAsStream(
115 | mapFile);
116 |
117 | String exten = getExtension(file);
118 | Map map = new HashMap();
119 |
120 | try {
121 | BufferedReader bis = new BufferedReader(new InputStreamReader(ins));
122 | String line = null;
123 | while ((line = bis.readLine()) != null) {
124 | String[] tmp = line.split("\\s+");
125 | map.put(tmp[0], tmp[1]);
126 | }
127 | } catch (IOException e) {
128 | }
129 |
130 | if (map.get(exten) == null)
131 | return defaultType;
132 | else
133 | return map.get(exten);
134 |
135 | }
136 |
137 | public static void closeQuietly(Closeable is) {
138 | if (is != null) {
139 | try {
140 | is.close();
141 | } catch (IOException e) {
142 | }
143 | }
144 | }
145 |
146 | /**
147 | *
148 | * @param file
149 | * the absolute file path
150 | * @param zip
151 | * gzip or not
152 | * @return byte array of the file
153 | *
154 | * @throws IOException
155 | */
156 | public static byte[] file2ByteArray(File file, boolean zip)
157 | throws IOException {
158 | if (file.isFile()) {
159 | InputStream is = null;
160 | GZIPOutputStream gzip = null;
161 | byte[] buffer = new byte[8912];
162 | ByteArrayOutputStream baos = new ByteArrayOutputStream(8912);
163 | try {
164 | if (zip) {
165 | gzip = new GZIPOutputStream(baos);
166 | }
167 | is = new BufferedInputStream(new FileInputStream(file));
168 | int read = 0;
169 | while ((read = is.read(buffer)) != -1) {
170 | if (zip) {
171 | gzip.write(buffer, 0, read);
172 | } else {
173 | baos.write(buffer, 0, read);
174 | }
175 | }
176 | } catch (IOException e) {
177 | throw e;
178 | } finally {
179 | closeQuietly(is);
180 | closeQuietly(gzip);
181 | }
182 | return baos.toByteArray();
183 | } else if (file.isDirectory()) {
184 | return directoryList(file, zip);
185 | } else {
186 | return new byte[] {};
187 | }
188 | }
189 |
190 | /**
191 | * same as {@link Util#subArray(byte[], byte[], int)},except find from end
192 | * to start;
193 | *
194 | * @param data
195 | * to search from
196 | * @param tofind
197 | * target
198 | * @param start
199 | * start index
200 | * @return index of the first find if find, data.length if not find
201 | */
202 | public static int subArrayFromEnd(byte[] data, byte[] tofind, int start) {
203 | int index = data.length;
204 | outer: for (int i = data.length - tofind.length; i > 0; --i) {
205 |
206 | for (int j = 0; j < tofind.length;) {
207 | if (data[i] == tofind[j]) {
208 | ++i;
209 | ++j;
210 | if (j == tofind.length) {
211 | index = i - tofind.length;
212 | break outer;
213 | }
214 | } else {
215 | i = i - j; // step back
216 | break;
217 | }
218 | }
219 | }
220 | return index;
221 | }
222 |
223 | /**
224 | *
225 | * @param data
226 | * to search from
227 | * @param tofind
228 | * target
229 | * @param start
230 | * start index
231 | * @return index of the first find if find, data.length if not find
232 | */
233 | public static int subArray(byte[] data, byte[] tofind, int start) {
234 | int index = data.length;
235 | outer: for (int i = start; i < data.length; ++i) {
236 |
237 | for (int j = 0; j < tofind.length;) {
238 | if (data[i] == tofind[j]) {
239 | ++i;
240 | ++j;
241 | if (j == tofind.length) {
242 | index = i - tofind.length;
243 | break outer;
244 | }
245 | } else {
246 | i = i - j; // step back
247 | break;
248 | }
249 | }
250 | }
251 | return index;
252 | }
253 | }
254 |
--------------------------------------------------------------------------------
/src/main/resources/index.tpl:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Direcoty List
5 |
24 |
25 |
26 | {{dir}}/
27 |
28 |
29 |
30 | Name |
31 | Size |
32 | Last Modified |
33 |
34 |
35 |
36 | {{#files}}
37 |
38 | {{name}} |
39 | {{size}} |
40 | {{mtime}} |
41 |
42 | {{/files}}
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/src/main/resources/log4j.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/main/resources/mime.types:
--------------------------------------------------------------------------------
1 | css text/css
2 | scss text/plain
3 | html text/html
4 | htm text/html
5 | js application/x-javascript
6 | git image/gif
7 | png image/png
8 | jpg image/jpeg
9 | jpeg image/jpeg
10 | clj text/plain
11 | conf text/plain
12 | xml text/xml
--------------------------------------------------------------------------------
/src/test/java/me/shenfeng/http/RequestHeaderDecoderTest.java:
--------------------------------------------------------------------------------
1 | package me.shenfeng.http;
2 |
3 | import me.shenfeng.http.RequestHeaderDecoder.Verb;
4 |
5 | import org.junit.Assert;
6 | import org.junit.Before;
7 | import org.junit.Test;
8 |
9 | public class RequestHeaderDecoderTest {
10 |
11 | private static String HOST = "www.google.com";
12 | private static String ACCEPT = "image/png,image/*;q=0.8,*/*;q=0.5";
13 | private static String END = "\r\n\r\n";
14 | private String GET = "GET /\r\n" + "Host: " + HOST + "\r\n" + "Accept: "
15 | + ACCEPT + END;
16 |
17 | private RequestHeaderDecoder decoder;
18 |
19 | @Before
20 | public void setup() {
21 | decoder = new RequestHeaderDecoder();
22 | }
23 |
24 | @Test
25 | public void testDecodeGet() {
26 | byte[] bytes = GET.getBytes();
27 | boolean b = decoder.appendSegment(bytes);
28 | Assert.assertEquals(b, true);
29 | Assert.assertEquals("/", decoder.getResouce());
30 | Assert.assertEquals(HOST, decoder.getHeader("Host"));
31 | Assert.assertEquals(ACCEPT, decoder.getHeader("Accept"));
32 | Assert.assertEquals(Verb.GET, decoder.getVerb());
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/src/test/java/me/shenfeng/http/UtilTest.java:
--------------------------------------------------------------------------------
1 | package me.shenfeng.http;
2 |
3 | import static org.junit.Assert.assertEquals;
4 |
5 | import java.io.File;
6 |
7 | import org.junit.Before;
8 | import org.junit.Test;
9 |
10 | public class UtilTest {
11 |
12 | private File f1;
13 | private File f2;
14 |
15 | @Before
16 | public void setUp() {
17 | f1 = new File("/path/to/what.css");
18 | f2 = new File("/path/to/what");
19 | }
20 |
21 | @Test
22 | public void testGetExtension() {
23 | assertEquals("css", Util.getExtension(f1));
24 | assertEquals("", Util.getExtension(f2));
25 | }
26 |
27 | @Test
28 | public void testGetContentType() {
29 | assertEquals("text/css", Util.getContentType(f1));
30 | assertEquals("application/octet-stream", Util.getContentType(f2));
31 | assertEquals("text/html", Util.getContentType(new File("/")));
32 | }
33 |
34 | @Test
35 | public void testSubArray() {
36 | byte[] s = { 72, 69, 65, 68 };
37 | byte[] f = { 71, 69, 84, 32 };
38 |
39 | byte[] data1 = { 71, 69, 84, 72, 69, 65, 68 };
40 | assertEquals(Util.subArray(data1, s, 0), 3);
41 | assertEquals(Util.subArray(data1, f, 0), data1.length);
42 |
43 | assertEquals(Util.subArrayFromEnd(data1, s, 0), 3);
44 | assertEquals(Util.subArrayFromEnd(data1, f, 0), data1.length);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------