/* * ELW : e-learning workspace * Copyright (C) 2010 Anton Kraievoy * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package elw.web; import base.pattern.Result; import com.google.common.base.Charsets; import com.google.common.base.Strings; import com.google.common.io.ByteStreams; import com.google.common.io.CharStreams; import com.google.common.io.InputSupplier; import elw.dao.*; import elw.dao.Ctx; import elw.miniweb.Message; import elw.vo.*; import elw.vo.Class; import elw.web.core.Core; import elw.web.core.W; import elw.webauth.ControllerAuth; import org.akraievoy.base.Parse; import org.akraievoy.couch.Squab; import org.apache.commons.fileupload.FileItemIterator; import org.apache.commons.fileupload.FileItemStream; import org.apache.commons.fileupload.FileUploadException; import org.apache.commons.fileupload.disk.DiskFileItemFactory; import org.apache.commons.fileupload.servlet.ServletFileUpload; import org.joda.time.DateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.mvc.multiaction.MultiActionController; import org.springframework.web.servlet.support.RequestContextUtils; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.Map; import java.util.SortedMap; import java.util.TreeMap; import static elw.dao.Auth.SESSION_KEY; public abstract class ControllerElw extends MultiActionController implements WebSymbols { private static final Logger log = LoggerFactory.getLogger(ControllerElw.class); private static final DiskFileItemFactory fileItemFactory = createFileItemFactory(); protected final Core core; protected final ElwServerConfig elwServerConfig; protected ControllerElw( final Core core, final ElwServerConfig elwServerConfig ) { this.core = core; this.elwServerConfig = elwServerConfig; } private static DiskFileItemFactory createFileItemFactory() { DiskFileItemFactory fileItemFactory = new DiskFileItemFactory(); fileItemFactory.setRepository(new java.io.File(System.getProperty("java.io.tmpdir"))); fileItemFactory.setSizeThreshold(2 * 1024 * 1024); return fileItemFactory; } // LATER move to ControllerAuth (required direct instance-level call dispatch) public HashMap<String, Object> noAuth( HttpServletRequest req, HttpServletResponse resp, boolean page, final String message ) throws IOException { if (page) { Message.addWarn(req, message); ControllerAuth.storeSuccessRedirect(req); resp.sendRedirect(elwServerConfig.getBaseUrl() + "auth.html"); } else { resp.sendError( HttpServletResponse.SC_FORBIDDEN, message ); } return null; } // TODO abstract away from returning concrete hashmap class protected HashMap<String, Object> prepareDefaultModel( final HttpServletRequest req, final Auth auth, final Ctx ctx ) { final HashMap<String, Object> model = new HashMap<String, Object>(); model.put( FormatTool.MODEL_KEY, FormatTool.forLocale(RequestContextUtils.getLocale(req)) ); model.put(VelocityTemplates.MODEL_KEY, core.getTemplates()); model.put(ElwUri.MODEL_KEY, core.getUri()); model.put(Auth.MODEL_KEY, auth); model.put(QueriesSecure.MODEL_KEY, core.getQueries().secure(auth)); return model; } protected static Auth auth(HttpServletRequest req) { return (Auth) req.getSession(true).getAttribute(SESSION_KEY); } protected static Auth auth(Map<String, Object> model) { return (Auth) model.get(Auth.MODEL_KEY); } protected static QueriesSecure queries(Map<String, Object> model) { return (QueriesSecure) model.get(QueriesSecure.MODEL_KEY); } // TODO better to avoid returning nulls and throw a specific exception protected HashMap<String, Object> auth( final HttpServletRequest req, final HttpServletResponse resp, final boolean page, final boolean verified ) throws IOException { final HttpSession session = req.getSession(true); final Auth auth = (Auth) session.getAttribute(SESSION_KEY); if (auth == null) { return noAuth(req, resp, page, "Auth required"); } if (!W.resolveRemoteAddress(req).equals(auth.getSourceAddr())) { return noAuth(req, resp, page, "Source address changed"); } auth.renew(core.getQueries()); if (auth.isEmpty()) { return noAuth(req, resp, page, "Non-empty Auth required"); } final String extraValidationMessage = extraAuthValidations(auth); if (!Strings.isNullOrEmpty(extraValidationMessage)) { return noAuth(req, resp, page, extraValidationMessage); } if (verified && !auth.isVerified()) { return noAuth(req, resp, page, "Verified Auth required"); } session.removeAttribute(ControllerAuth.SESSION_SUCCESS_REDIRECT); return prepareDefaultModel(req, auth, null); } protected String extraAuthValidations(Auth auth) { return null; } protected static interface WebMethodScore { ModelAndView handleScore( HttpServletRequest req, HttpServletResponse resp, Ctx ctx, FileSlot slot, Solution file, Long stamp, Map<String, Object> model ) throws IOException; } // FIXME some of file VT parameters are broken protected ModelAndView wmScore( final HttpServletRequest req, final HttpServletResponse resp, final boolean page, final boolean verified, final WebMethodScore wm ) throws IOException { final HashMap<String, Object> model = auth(req, resp, page, verified); if (model == null) { return null; } final Ctx ctx = (Ctx) model.get(R_CTX); if (!ctx.resolved(Ctx.STATE_EGSCIV)) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Path problem, please check the logs"); return null; } final String slotId = req.getParameter("sId"); if ((slotId == null || slotId.trim().length() == 0)) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "no slotId (sId) defined"); return null; } final FileSlot slot = ctx.getAssType().getFileSlots().get(slotId); if (slot == null) { resp.sendError(HttpServletResponse.SC_NOT_FOUND, "slot for id " + slotId + " not found"); return null; } final String fileId = req.getParameter("fId"); if ((fileId == null || fileId.trim().length() == 0)) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "no fileId (fId) defined"); return null; } final Solution file = core.getQueries().solution(ctx.ctxSlot(slot), fileId); if (file == null) { resp.sendError(HttpServletResponse.SC_NOT_FOUND, "file for id " + fileId + " not found"); return null; } final Long stamp = Parse.oneLong(req.getParameter("stamp"), null); return wm.handleScore(req, resp, ctx, slot, file, stamp, model); } protected static abstract class WebMethodCtx { protected HttpServletRequest req; protected HttpServletResponse resp; protected Ctx ctx; protected Map<String, Object> model; protected void init( HttpServletRequest req, HttpServletResponse resp, Ctx ctx, Map<String, Object> model ) { this.ctx = ctx; this.model = model; this.req = req; this.resp = resp; } public abstract ModelAndView handleCtx() throws IOException; } protected ModelAndView wmECG( HttpServletRequest req, HttpServletResponse resp, final boolean page, final boolean verified, final WebMethodCtx wm ) throws IOException { return wm(req, resp, Ctx.STATE_ECG, wm, page, verified); } private ModelAndView wm( final HttpServletRequest req, final HttpServletResponse resp, final String ctxResolveState, final WebMethodCtx wm, final boolean page, final boolean verified ) throws IOException { final HashMap<String, Object> model = auth(req, resp, page, verified); if (model == null) { return null; } final Ctx ctx = (Ctx) model.get(R_CTX); if (!ctx.resolved(ctxResolveState)) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Path problem, please check the logs"); return null; } wm.init(req, resp, ctx, model); return wm.handleCtx(); } protected ModelAndView wmFile( final HttpServletRequest req, final HttpServletResponse resp, final String scopeForced, final boolean page, final boolean verified, final WebMethodFile wm ) throws IOException { final HashMap<String, Object> model = auth(req, resp, page, verified); if (model == null) { return null; } final Ctx ctx = (Ctx) model.get(R_CTX); wm.init(req, resp, ctx, model); wm.setScopeForced(scopeForced); return wm.handleCtx(); } protected static abstract class WebMethodFile extends WebMethodCtx { protected String scopeForced = null; private void setScopeForced(String scopeForced) { this.scopeForced = scopeForced; } public ModelAndView handleCtx() throws IOException { final String scope = scopeForced == null ? req.getParameter("s") : scopeForced; if (scope == null) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "scope not set"); return null; } if (Attachment.SCOPE.equals(scope)) { if (!ctx.resolved(Ctx.STATE_CTAV)) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "context path problem, please check the logs"); return null; } } else if (Solution.SCOPE.equals(scope)) { if (!ctx.resolved(Ctx.STATE_EGSCIV)) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "context path problem, please check the logs"); return null; } } else { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "bad scope: " + scope); return null; } final String slotId = req.getParameter("sId"); if (slotId == null || slotId.trim().length() == 0) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "no slotId (sId) defined"); return null; } final FileSlot slot = ctx.getAssType().getFileSlots().get(slotId); if (slot == null) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "slot '" + slotId + "' not found"); return null; } return handleFile(scope, slot); } protected abstract ModelAndView handleFile(String scope, FileSlot slot) throws IOException; protected void retrieveFile( FileBase fileBase, FileSlot slot, final Queries queries ) throws IOException { final InputSupplier<InputStream> fileInput; if (fileBase instanceof Solution) { fileInput = queries.solutionInput( ctx.ctxSlot(slot).solution((Solution) fileBase), FileBase.CONTENT ); } else if (fileBase instanceof Attachment) { fileInput = queries.attachmentInput( ctx.ctxSlot(slot).attachment((Attachment) fileBase), FileBase.CONTENT ); } else { throw new IllegalStateException( "not supported fileBase instance: " + fileBase ); } storeContentHeaders(fileBase, resp); ByteStreams.copy(fileInput, resp.getOutputStream()); } public ModelAndView storeFile( FileSlot slot, String refreshUri, String failureUri, String authorName, Queries queries, final FileBase file ) throws IOException { // FIXME upload page should contain full listing of validation rules final SortedMap<String, FileType> allTypes = slot.getFileTypes(); final SortedMap<String, FileType> validTypes = new TreeMap<String, FileType>(allTypes); final boolean put = "PUT".equalsIgnoreCase(req.getMethod()); final int length = req.getContentLength(); final String stamp = Long.toString( Squab.Stamped.genStamp(), 36 ); file.setId(stamp); file.setAuthor(authorName); InputSupplier<? extends InputStream> inputSupplier = null; String contentType = null; if (put) { // FIXME remove this extra variant with non-encoded put uploads inputSupplier = supplierForRequest(req); contentType = "text/plain"; file.setName("upload_" + stamp + ".txt"); } else { try { final ServletFileUpload sfu = new ServletFileUpload(fileItemFactory); final FileItemIterator fii = sfu.getItemIterator(req); while (fii.hasNext()) { final FileItemStream item = fii.next(); if (item.isFormField()) { final String fieldName = item.getFieldName(); if ("comment".equals(fieldName)) { file.setComment(fieldText(item)); } if (auth(req).getAdmin() != null) { if ("sourceAddr".equals(fieldName)) { final String sourceAddr = fieldText(item); if (!Strings.isNullOrEmpty(sourceAddr)) { file.setSourceAddress(sourceAddr); } } else if ("dateTime".equals(fieldName)) { final String dateTime = fieldText(item); if (!Strings.isNullOrEmpty(dateTime)) { final String[] parts = dateTime.trim().split("\\s+"); final String date = parts[0]; final String time = parts.length > 1 ? parts[1] : "11:00"; final DateTime instant = Class.parseDateTime(date, time); file.setStamp(instant.getMillis()); } } } continue; } file.setName(Strings.nullToEmpty(extractNameFromPath(item))); contentType = item.getContentType(); inputSupplier = supplierForFileItem(item); // TODO find length of this particular item, if implied by protocol break; } } catch (FileUploadException e) { throw new IOException(e); } if (inputSupplier == null) { return fail(put, failureUri, "File being uploaded not found in the form"); } if (contentType == null) { return fail(put, failureUri, "Content Type not reported in upload"); } } if (Strings.isNullOrEmpty(file.getSourceAddress())) { file.setSourceAddress(W.resolveRemoteAddress(req)); } if (length == -1) { final String message = "Upload size not reported"; return fail(put, failureUri, message); } FileType._.filterByLength(validTypes, length); if (validTypes.isEmpty()) { return fail(put, failureUri, "Size " + org.akraievoy.base.Format.formatMem(length) + " exceeds size limits"); } FileType._.filterByName(validTypes, file.getName().toLowerCase()); if (validTypes.isEmpty()) { return fail(put, failureUri, "File name failed all regex checks"); } if (validTypes.size() > 1) { Message.addWarn(req, "More than one valid file type left: " + validTypes.keySet()); } final FileType fileType = validTypes.get(validTypes.firstKey()); if (!fileType.getContentTypes().contains(Strings.nullToEmpty(contentType))) { Message.addWarn(req, "contentType '" + contentType + "': not listed in the file type"); } file.setFileType(IdNamed._.singleton(fileType)); if (!fileType.isBinary()) { if (length > FileBase.DETECT_SIZE_LIMIT) { return fail(put, failureUri, "Non-binary file is too big for content check"); } final byte[] bytes = ByteStreams.toByteArray(inputSupplier); // implemented as per this SO answer: // http://stackoverflow.com/questions/277521/identify-file-binary/277568#277568 for (byte b : bytes) { if (b >= 0 && b < 9 || b > 13 && b < 32) { return fail(put, failureUri, "Non-binary file contains binary data"); } } inputSupplier = ByteStreams.newInputStreamSupplier(bytes); } // TODO validate headers and binary content here final Result result = queries.createFile(ctx, slot, file, inputSupplier, contentType); if (result.isSuccess()) { return succeed(put, refreshUri, result); } else { return fail(put, failureUri, result.getMessage()); } } protected ModelAndView succeed(boolean put, String refreshUri, Result result) throws IOException { if (!put) { Message.addResult(req, result); resp.sendRedirect(refreshUri); } return null; } protected ModelAndView fail(boolean put, String failureUri, String message) throws IOException { if (put) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, message); } else { Message.addWarn(req, message); resp.sendRedirect(failureUri); } return null; } } public static void storeContentHeaders( final FileBase fileBase, final HttpServletResponse resp ) { final FileType fileType = IdNamed._.one(fileBase.getFileType()); final String contentType; if (!fileType.getContentTypes().isEmpty()) { contentType = fileType.getContentTypes().get(0); } else { contentType = fileType.isBinary() ? "binary/octet-stream" : "text/plain"; } if (fileType.isBinary()) { resp.setContentType(contentType); } else { resp.setContentType(contentType + "; charset=UTF-8"); resp.setCharacterEncoding("UTF-8"); } resp.setContentLength( fileBase.getCouchFile(FileBase.CONTENT).getLength().intValue() ); resp.setHeader("Content-Disposition", "attachment;"); } protected static String fieldText( final FileItemStream item ) throws IOException { return CharStreams.toString( CharStreams.newReaderSupplier( supplierForFileItem(item), Charsets.UTF_8 ) ); } protected static String extractNameFromPath(FileItemStream item) { final String name = item.getName(); if (name == null) { return null; } final int lastSlash = Math.max(name.lastIndexOf("\\"), name.lastIndexOf("/")); final String fName = lastSlash >= 0 ? name.substring(lastSlash + 1) : name; return fName; } protected static InputSupplier<InputStream> supplierForRequest( final HttpServletRequest myReq ) { return new InputSupplier<InputStream>() { public InputStream getInput() throws IOException { return myReq.getInputStream(); } }; } protected static InputSupplier<InputStream> supplierForFileItem( final FileItemStream item ) { return new InputSupplier<InputStream>() { public InputStream getInput() throws IOException { return item.openStream(); } }; } }