/*
* Copyright 2013 mpowers
*
* 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 com.trsst.ui;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.nio.charset.Charset;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.tika.Tika;
import com.trsst.Command;
import com.trsst.Common;
/**
* Private servlet for our private client. Exposes command-line client as a
* servlet for "post" command only. Parameters "attach" and "home" are rejected.
* Non-local clients are rejected by default.
*/
public class AppServlet extends HttpServlet {
private static final long serialVersionUID = -8082699767921771750L;
private static final String[] ALLOWED_FILES = { "boiler.css", "index.html",
"favicon.ico", "jquery-1.10.1.js", "model.js", "composer.js",
"pollster.js", "renderer.js", "controller.js", "ui.css",
"icon-256.png", "icon-back.png", "note.svg", "note.png",
"loading-on-white.gif", "loading-on-gray.gif",
"loading-on-orange.gif", "icon-rss.png" };
private final Map<String, byte[]> resources;
private boolean restricted;
public AppServlet() {
this(true);
}
public AppServlet(boolean restricted) {
this.restricted = restricted;
// load static resources
resources = new HashMap<String, byte[]>();
for (String s : ALLOWED_FILES) {
try {
resources.put(
s,
Common.readFully(this.getClass().getResourceAsStream(
"site/" + s)));
} catch (Exception e) {
log.error("Could not load static http resource: " + s, e);
}
}
}
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException {
doPost(request, response);
}
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws IOException {
// FLAG: limit access only to local clients
if (restricted
&& !request.getRemoteAddr().equals(request.getLocalAddr())) {
response.sendError(HttpServletResponse.SC_FORBIDDEN,
"Non-local clients are not allowed.");
return;
}
// in case of any posted files
InputStream inStream = null;
// determine if supported command: pull, push, post
String path = request.getPathInfo();
System.err.println(new Date().toString() + " " + path);
if (path != null) {
// FLAG: limit only to pull and post
if (path.startsWith("/pull/") || path.startsWith("/post")) {
// FLAG: we're sending the user's keystore
// password over the wire (over SSL)
List<String> args = new LinkedList<String>();
if (path.startsWith("/pull/")) {
path = path.substring("/pull/".length());
response.setContentType("application/atom+xml; type=feed; charset=utf-8");
// System.out.println("doPull: " +
// request.getParameterMap());
args.add("pull");
if (request.getParameterMap().size() > 0) {
boolean first = true;
for (Object name : request.getParameterMap().keySet()) {
// FLAG: don't allow "home" (server-abuse)
// FLAG: don't allow "attach" (file-system access)
if ("decrypt".equals(name) || "pass".equals(name)) {
for (String value : request
.getParameterValues(name.toString())) {
args.add("--" + name.toString());
args.add(value);
}
} else {
for (String value : request
.getParameterValues(name.toString())) {
if (first) {
path = path + '?';
first = false;
} else {
path = path + '&';
}
path = path + name + '=' + value;
}
}
}
}
args.add(path);
} else if (path.startsWith("/post")) {
// System.out.println("doPost: " +
// request.getParameterMap());
args.add("post");
try { // h/t http://stackoverflow.com/questions/2422468
List<FileItem> items = new ServletFileUpload(
new DiskFileItemFactory())
.parseRequest(request);
for (FileItem item : items) {
if (item.isFormField()) {
// process regular form field
String name = item.getFieldName();
String value = item.getString("UTF-8").trim();
// System.out.println("AppServlet: " + name
// + " : " + value);
if (value.length() > 0) {
// FLAG: don't allow "home" (server-abuse)
// FLAG: don't allow "attach" (file-system
// access)
if ("id".equals(name)) {
if (value.startsWith("urn:feed:")) {
value = value.substring("urn:feed:"
.length());
}
args.add(value);
} else if (!"home".equals(name)
&& !"attach".equals(name)) {
args.add("--" + name);
args.add(value);
}
} else {
log.debug("Empty form value for name: "
+ name);
}
} else if (item.getSize() > 0) {
// process form file field (input type="file").
// String filename = FilenameUtils.getName(item
// .getName());
if (item.getSize() > 1024 * 1024 * 10) {
throw new FileUploadException(
"Current maximum upload size is 10MB");
}
String name = item.getFieldName();
if ("icon".equals(name) || "logo".equals(name)) {
args.add("--" + name);
args.add("-");
}
inStream = item.getInputStream();
// NOTE: only handles one file!
} else {
log.debug("Ignored form field: "
+ item.getFieldName());
}
}
} catch (FileUploadException e) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST,
"Could not parse multipart request: " + e);
return;
}
}
// send post data if any to command input stream
if (inStream != null) {
args.add("--attach");
}
//System.out.println(args);
// make sure we don't create another local server
args.add("--host");
args.add(request.getScheme() + "://" + request.getServerName()
+ ":" + request.getServerPort() + "/feed");
PrintStream outStream = new PrintStream(
response.getOutputStream(), false, "UTF-8");
int result = new Command().doBegin(args.toArray(new String[0]),
outStream, inStream);
if (result != 0) {
response.sendError(
HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
"Internal error code: " + result);
} else {
outStream.flush();
}
return;
}
// otherwise: determine if static resource request
if (path.startsWith("/")) {
path = path.substring(1);
}
byte[] result = resources.get(path);
String mimetype = null;
if (result == null) {
// if ("".equals(path) || path.endsWith(".html")) {
// treat all html requests with index doc
result = resources.get("index.html");
mimetype = "text/html";
// }
}
if (result != null) {
if (mimetype == null) {
if (path.endsWith(".html")) {
mimetype = "text/html";
} else if (path.endsWith(".css")) {
mimetype = "text/css";
} else if (path.endsWith(".js")) {
mimetype = "application/javascript";
} else if (path.endsWith(".png")) {
mimetype = "image/png";
} else if (path.endsWith(".jpg")) {
mimetype = "image/jpeg";
} else if (path.endsWith(".jpeg")) {
mimetype = "image/jpeg";
} else if (path.endsWith(".gif")) {
mimetype = "image/gif";
} else {
mimetype = new Tika().detect(result);
}
}
if (request.getHeader("If-None-Match:") != null) {
// client should always use cached version
log.info("sending 304");
response.setStatus(304); // Not Modified
return;
}
// otherwise allow ETag/If-None-Match
response.setHeader("ETag", Long.toHexString(path.hashCode()));
if (mimetype != null) {
response.setContentType(mimetype);
}
response.setContentLength(result.length);
response.getOutputStream().write(result);
return;
}
}
// // otherwise: 404 Not Found
// response.sendError(HttpServletResponse.SC_NOT_FOUND);
}
private final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(this
.getClass());
}