/*
* Copyright 2017 ZhangJiupeng
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cc.agentx.ui;
import cc.agentx.Constants;
import java.io.*;
import java.net.*;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* <h2>A tiny local web server for agentx</h2>
* <p>it supports GET and HEAD method only, HOWEVER, will
* gradually improved.</p>
* <b>Notice:</b> this project is not for a web server,
* you can see that the page configuration is embedded into
* <code>cc.agentx.http.Initializer</code>. <br/>However,
* <code>cc.agentx.http</code> is designed separately,
* it can be extracted into a single project.
* <br/>
* Thus, this web server project might be developed in the future,
* hold on and keep attention :)
*/
public class HttpServer {
private static final int READ_TIMEOUT = 30 * 1000;
private static final int BUFFER_SIZE = 1024;
private final PageHandler handler;
private final ExecutorService executor;
private final AtomicBoolean isRunning;
private final SimpleDateFormat dateFormat;
private final int port;
private final boolean info;
public HttpServer(final int port, final String baseDir) {
this(port, baseDir, false);
}
public HttpServer(final int port, final String baseDir, boolean info) {
this.handler = new PageHandler(baseDir);
this.executor = Executors.newCachedThreadPool();
this.isRunning = new AtomicBoolean(false);
this.dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
this.dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
this.port = port;
this.info = info;
}
public static int getIdlePort() throws IOException {
int idlePort = 0;
ServerSocket serverSocket = new ServerSocket();
serverSocket.setReuseAddress(true);
serverSocket.bind(new InetSocketAddress(idlePort));
idlePort = serverSocket.getLocalPort();
serverSocket.close();
return idlePort;
}
public static void main(final String[] args) throws Throwable {
int port;
String baseDir;
if (args.length == 1 && args[0].contains("help")) {
System.out.println(HttpServer.class.getName() + "<port> <webroot>");
return;
}
if (args.length == 2) {
port = Integer.parseInt(args[0]);
baseDir = args[1];
} else {
port = getIdlePort();
baseDir = System.getProperty("user.dir").replaceAll("\\\\", "/") + "/www/";
}
new HttpServer(port, baseDir).start();
}
public int start() {
isRunning.set(true);
executor.submit(new ServiceListener(port));
return port;
}
public void stop() {
isRunning.set(false);
shutdown(executor);
}
boolean shutdown(final ExecutorService pool) {
pool.shutdown();
try {
if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
pool.shutdownNow();
if (!pool.awaitTermination(60, TimeUnit.SECONDS))
return false;
}
} catch (InterruptedException ie) {
pool.shutdownNow();
Thread.currentThread().interrupt();
}
if (info)
System.out.println(Constants.WEB_SERVER_NAME + " stopped.");
return true;
}
public boolean isRunning() {
return isRunning.get();
}
static class Closer {
static void close(final Closeable c) {
if (c != null) {
try {
c.close();
} catch (Exception ignored) {
}
}
}
static void close(final Socket c) {
if (c != null) {
try {
c.close();
} catch (Exception ignored) {
}
}
}
static void close(final ServerSocket c) {
if (c != null) {
try {
c.close();
} catch (Exception ignored) {
}
}
}
}
class ServiceListener implements Runnable {
final int port;
ServiceListener(final int port) {
this.port = port;
}
@Override
public void run() {
ServerSocket server = null;
try {
server = new ServerSocket();
server.setSoTimeout(1000);
server.setReuseAddress(true);
server.bind(new InetSocketAddress(port));
if (info)
System.out.println(Constants.WEB_SERVER_NAME + " started"
+ ((port == 80) ? "." : " at localhost:" + port + "."));
while (isRunning.get()) {
Socket client = null;
try {
client = server.accept();
client.setSoTimeout(READ_TIMEOUT);
executor.submit(new ServiceProvider(client));
} catch (SocketTimeoutException ignored) {
} catch (Exception e) {
Closer.close(client);
}
}
} catch (IOException e) {
e.printStackTrace(System.err);
} finally {
Closer.close(server);
}
}
}
class ServiceProvider implements Runnable {
private static final String HTTP_VER = "HTTP/1.0";
private static final String CACHE_CONTROL = "Cache-Control: private, max-age=0";
private static final String CONNECTION_CLOSE = "Connection: close";
private static final String SERVER_NAME = "Server: " + Constants.WEB_SERVER_NAME + " " + Constants.APP_VERSION;
private static final String CRLF = "\r\n";
final Socket client;
ServiceProvider(final Socket client) {
this.client = client;
}
@Override
public void run() {
BufferedReader in = null;
PrintStream out = null;
try {
in = new BufferedReader(new InputStreamReader(client.getInputStream()), BUFFER_SIZE);
out = new PrintStream(new BufferedOutputStream(client.getOutputStream(), BUFFER_SIZE));
// Read Head (GET / HTTP/1.0)
final String header = in.readLine();
final String[] headerTokens = header.split(" ");
final String method = headerTokens[0];
final String uri = URLDecoder.decode(headerTokens[1], "ISO-8859-1");
final String version = headerTokens[2];
// Read Headers
// noinspection StatementWithEmptyBody
while (!in.readLine().isEmpty()) {
}
if (!"HTTP/1.0".equals(version) && !"HTTP/1.1".equals(version)) {
throw HttpError.HTTP_400;
} else if (!"GET".equals(method) && !"HEAD".equals(method)) {
throw HttpError.HTTP_405;
} else {
Map<String, Object> result;
try {
String pureUri = uri;
String paramStr = uri.substring(uri.indexOf('?') + 1);
Map<String, String> parameters = null;
if (!paramStr.equals(pureUri)) {
parameters = new HashMap<>();
pureUri = uri.substring(0, uri.indexOf('?'));
if (paramStr.contains("&")) {
for (String group : paramStr.split("&")) {
parameters.put(group.split("=")[0], group.split("=")[1]);
}
} else if (paramStr.contains("=")) {
parameters.put(paramStr.split("=")[0], paramStr.split("=")[1]);
}
}
if (parameters == null || paramStr.equals("")) {
result = handler.fetch(pureUri);
} else {
result = handler.fetch(pureUri, parameters);
}
} catch (Exception e) {
e.printStackTrace();
throw HttpError.HTTP_400;
}
// result parse
try {
switch (result.get("type").toString()) {
case "file":
InputStream inputStream = (InputStream) result.get("inputStream");
long length = Long.parseLong(result.get("length").toString());
Date lastModified = (Date) result.get("lastModified");
String mimeType = result.get("mimeType") == null ? null : result.get("mimeType").toString();
sendResponse(inputStream, out, length, lastModified, mimeType, !"HEAD".equals(method));
inputStream.close();
break;
case "redirect":
Object redirectUrl = result.get("url");
if (redirectUrl == null) {
throw HttpError.HTTP_404;
}
sendRedirect(out, redirectUrl.toString());
break;
}
} catch (Exception e) {
e.printStackTrace();
throw HttpError.HTTP_500;
}
}
} catch (HttpError e) {
sendError(out, e, e.getHttpText());
} catch (SocketTimeoutException e) {
sendError(out, HttpError.HTTP_408, e.getMessage());
} catch (IOException e) {
sendError(out, HttpError.HTTP_500, e.getMessage());
} finally {
Closer.close(in);
Closer.close(out);
Closer.close(client);
}
}
synchronized String getHttpDate(final Date date) {
return dateFormat.format(date);
}
void sendRedirect(final PrintStream out, String url) {
out.append(HTTP_VER).append(" 302 Found").append(CRLF);
out.append("Location: ").append(url);
out.flush();
}
void sendResponse(final InputStream is, final PrintStream out, long length, final Date lastModified,
final String mimeType, final boolean body) throws IOException {
out.append(HTTP_VER).append(" 200 OK").append(CRLF);
out.append("Content-Length: ").append(String.valueOf(length)).append(CRLF);
if (mimeType != null) {
out.append("Content-Type: ").append(mimeType).append(CRLF);
}
out.append("Date: ").append(getHttpDate(new Date())).append(CRLF);
if (lastModified != null) {
out.append("Last-Modified: ").append(getHttpDate(lastModified)).append(CRLF);
}
// For Cross Domain!
out.append("Access-Control-Allow-Origin: *").append(CRLF);
out.append(CACHE_CONTROL).append(CRLF);
out.append(CONNECTION_CLOSE).append(CRLF);
out.append(SERVER_NAME).append(CRLF);
out.append(CRLF);
if (body) {
final byte[] buf = new byte[BUFFER_SIZE];
int len;
while ((len = is.read(buf)) != -1) {
out.write(buf, 0, len);
}
}
out.flush();
}
void sendError(final PrintStream out, final HttpError e, final String bodyText) {
sendError(out, e.getHttpCode(), e.getHttpText(), bodyText);
}
void sendError(final PrintStream out, final int code, final String reason, final String bodyText) {
String bodyPage = HttpError.wrapInErrorPage(bodyText);
out.append(HTTP_VER).append(' ').append(String.valueOf(code)).append(' ').append(reason).append(CRLF);
out.append("Content-Length: ").append(String.valueOf(bodyPage.length())).append(CRLF);
out.append("Content-Type: text/html; charset=ISO-8859-1").append(CRLF);
out.append(CACHE_CONTROL).append(CRLF);
out.append(CONNECTION_CLOSE).append(CRLF);
out.append(SERVER_NAME).append(CRLF);
out.append(CRLF);
out.append(bodyPage);
out.flush();
}
}
}