From 3b47a56acc83c83317c05f27c6967973a75f0a22 Mon Sep 17 00:00:00 2001 From: Richard Cyganiak Date: Fri, 15 May 2015 15:13:23 +0100 Subject: [PATCH] Bugfix: Properly handle special characters in tagger ID --- .../org/topbraid/mauiserver/MauiServer.java | 2 +- .../topbraid/mauiserver/TaggerResource.java | 24 +++++++++- .../mauiserver/framework/RootServlet.java | 34 ++++++++++++-- .../mauiserver/tagger/TaggerStore.java | 47 +++++++++++++++++-- 4 files changed, 98 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/topbraid/mauiserver/MauiServer.java b/src/main/java/org/topbraid/mauiserver/MauiServer.java index f94176f..9f2f9ea 100644 --- a/src/main/java/org/topbraid/mauiserver/MauiServer.java +++ b/src/main/java/org/topbraid/mauiserver/MauiServer.java @@ -43,7 +43,7 @@ public Resource getResource(String requestURI, ServletContext context) { return new HomeResource(context, taggers); } String taggerId = path[0]; - Tagger tagger = taggers.getTagger(taggerId); + Tagger tagger = taggers.getTagger(TaggerResource.decodeTaggerIdFromURL(taggerId)); if (tagger == null) return null; if (path.length == 1) { return new TaggerResource(context, taggers, tagger); diff --git a/src/main/java/org/topbraid/mauiserver/TaggerResource.java b/src/main/java/org/topbraid/mauiserver/TaggerResource.java index 7ebacf7..9677915 100644 --- a/src/main/java/org/topbraid/mauiserver/TaggerResource.java +++ b/src/main/java/org/topbraid/mauiserver/TaggerResource.java @@ -1,5 +1,9 @@ package org.topbraid.mauiserver; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.net.URLEncoder; + import javax.servlet.ServletContext; import org.topbraid.mauiserver.framework.Request; @@ -8,8 +12,8 @@ import org.topbraid.mauiserver.framework.Resource.Gettable; import org.topbraid.mauiserver.framework.Response; import org.topbraid.mauiserver.framework.Response.JSONResponse; -import org.topbraid.mauiserver.tagger.TaggerCollection; import org.topbraid.mauiserver.tagger.Tagger; +import org.topbraid.mauiserver.tagger.TaggerCollection; import com.entopix.maui.vocab.VocabularyStore; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -65,6 +69,22 @@ public Response doDelete(Request request) { } public static String getRelativeTaggerURL(Tagger tagger) { - return "/" + tagger.getId(); + return "/" + encodeTaggerIdForURL(tagger.getId()); + } + + public static String encodeTaggerIdForURL(String id) { + try { + return URLEncoder.encode(id, "utf-8"); + } catch (UnsupportedEncodingException ex) { + throw new RuntimeException("Can't happen", ex); + } + } + + public static String decodeTaggerIdFromURL(String id) { + try { + return URLDecoder.decode(id, "utf-8"); + } catch (UnsupportedEncodingException ex) { + throw new RuntimeException("Can't happen", ex); + } } } diff --git a/src/main/java/org/topbraid/mauiserver/framework/RootServlet.java b/src/main/java/org/topbraid/mauiserver/framework/RootServlet.java index 5d7d641..55b105c 100644 --- a/src/main/java/org/topbraid/mauiserver/framework/RootServlet.java +++ b/src/main/java/org/topbraid/mauiserver/framework/RootServlet.java @@ -8,6 +8,8 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.topbraid.mauiserver.MauiServer; import org.topbraid.mauiserver.framework.Resource.Deletable; import org.topbraid.mauiserver.framework.Resource.Gettable; @@ -16,7 +18,8 @@ @SuppressWarnings("serial") public class RootServlet extends HttpServlet { - + Logger log = LoggerFactory.getLogger(RootServlet.class); + /** * Creates the server instance. This instance will be shared between * servlets through the {@link ServletContext}. If this was a true @@ -56,16 +59,41 @@ private void doRequest(HttpServletRequest req, HttpServletResponse resp) Request request = createRequest(req, resp); Response response; try { - String requestURI = (req.getServletPath() == null || "".equals(req.getServletPath())) - ? "/" : req.getServletPath(); + String requestURI = getLocalRequestUriWithoutQuery(req); + log.debug(req.getMethod() + " " + requestURI); Resource resource = getServer(req.getServletContext()).getResource(requestURI, req.getServletContext()); response = createResponse(request, resource); } catch (Exception ex) { + log.error("Uncaught exception in servlet", ex); response = request.serverError(ex); } response.send(); } + /** + * Returns a processed version of the request URI. The scheme, host + * name and port are omitted. The query string, such as + * ?param1=value¶m2=value, is omitted. If the webapp + * doesn't run as the servlet container's root application, then the + * path to the application is omitted. The resulting string always + * starts with a '/'. For example, if the webapp is deployed as + * webapp-name, then the URI + * http://myserver:8080/webapp-name/foo/bar?x=123 would be + * turned into /foo/bar. + */ + private String getLocalRequestUriWithoutQuery(HttpServletRequest req) { + String result = req.getRequestURI(); + if (req.getContextPath() != null) { + if (req.getRequestURI().startsWith(req.getContextPath())) { + return result.substring(req.getContextPath().length()); + } + log.warn("Mismatch between context path '" + + req.getContextPath() + "' and request URI '" + + req.getRequestURI() + "'"); + } + return result; + } + private Response createResponse(Request request, Resource resource) { if (resource == null) { return request.notFound(); diff --git a/src/main/java/org/topbraid/mauiserver/tagger/TaggerStore.java b/src/main/java/org/topbraid/mauiserver/tagger/TaggerStore.java index 60e3697..9b6546c 100644 --- a/src/main/java/org/topbraid/mauiserver/tagger/TaggerStore.java +++ b/src/main/java/org/topbraid/mauiserver/tagger/TaggerStore.java @@ -6,6 +6,9 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.net.URLEncoder; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -58,7 +61,12 @@ public Collection listTaggers() { List results = new ArrayList(); for (File f: path.listFiles()) { if (!f.isDirectory()) continue; - results.add(f.getName()); + String taggerId = decodeTaggerIdFromFilename(f.getName()); + if (encodeTaggerIdAsFilename(taggerId).equals(f.getName())) { + results.add(taggerId); + } else { + log.warn("Skipping malformed tagger directory " + f.getName()); + } } log.debug("Listed " + results.size() + " taggers: [" + StringUtils.join(results, ", ") + "]"); return results; @@ -221,10 +229,43 @@ public void writeMauiModel(String taggerId, MauiFilter mauiModel) { } private File getTaggerDirectory(String id) { - return new File(path.getAbsolutePath() + "/" + id); + return new File(path.getAbsolutePath() + "/" + encodeTaggerIdAsFilename(id)); } private File getTaggerFile(String id, String filename) { - return new File(path.getAbsolutePath() + "/" + id + "/" + filename); + return new File(path.getAbsolutePath() + "/" + encodeTaggerIdAsFilename(id) + "/" + filename); + } + + /** + * Encodes special characters in a string to make it safe for use + * as a filename. This is approximate; it encodes many characters + * that are in fact safe. Characters are %-encoded. + * + * @param s An arbitrary string + * @return A version of the input that is safe for use as a filename + */ + private String encodeTaggerIdAsFilename(String s) { + try { + s = URLEncoder.encode(s, "utf-8"); + if (s.startsWith(".")) { + // Catch cases like "." and ".." + s = "%2E" + s.substring(1); + } + if (s.startsWith("~")) { + // Catch cases like "~" and "~root" + s = "%7E" + s.substring(1); + } + return s; + } catch (UnsupportedEncodingException ex) { + throw new RuntimeException("Can't happen", ex); + } + } + + private String decodeTaggerIdFromFilename(String s) { + try { + return URLDecoder.decode(s, "utf-8"); + } catch (UnsupportedEncodingException ex) { + throw new RuntimeException("Can't happen", ex); + } } }