From 5588d971fcd697916962d4f08246c28a671b9efe Mon Sep 17 00:00:00 2001 From: Jesse Eichar Date: Thu, 24 Apr 2014 08:33:15 +0200 Subject: [PATCH] Encode path segments in urls to prevent illegal uris thanks to layer names with spaces. I have a geoserver instance where users can publish layers to the geoserver. Because there are so many users that can publish to the geoserver we have been unable to prevent them from publishing layers with spaces in the names. When there are spaces (or other certain characters), the rest api is broken because the urls have spaces in them. Obviously the best solution is to fix Geoserver so that a layer with a space in the name (or in the workspace name) does not break the REST API. However that is a substantial amount of work and this change makes this library more robust in the face or inconsiderate geoserver administrators so I decided to make the change here. When I have time I would like to patch Geoserver as well but that is another issue. --- pom.xml | 1 + .../geoserver/rest/HTTPUtils.java | 155 ++++++++++++------ .../geoserver/rest/HTTPUtilsTest.java | 16 ++ 3 files changed, 125 insertions(+), 47 deletions(-) create mode 100644 src/test/java/it/geosolutions/geoserver/rest/HTTPUtilsTest.java diff --git a/pom.xml b/pom.xml index fa5b1deb..d4ac5a4f 100644 --- a/pom.xml +++ b/pom.xml @@ -203,6 +203,7 @@ 1.5.11 + UTF-8 diff --git a/src/main/java/it/geosolutions/geoserver/rest/HTTPUtils.java b/src/main/java/it/geosolutions/geoserver/rest/HTTPUtils.java index ae1fc7af..1738269c 100644 --- a/src/main/java/it/geosolutions/geoserver/rest/HTTPUtils.java +++ b/src/main/java/it/geosolutions/geoserver/rest/HTTPUtils.java @@ -25,15 +25,6 @@ package it.geosolutions.geoserver.rest; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import java.net.ConnectException; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; - import org.apache.commons.httpclient.Credentials; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.HttpConnectionManager; @@ -52,6 +43,17 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.ConnectException; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; + /** * Low level HTTP utilities. */ @@ -60,7 +62,7 @@ public class HTTPUtils { /** * Performs an HTTP GET on the given URL. - * + * * @param url The URL where to connect to. * @return The HTTP response as a String if the HTTP response code was 200 * (OK). @@ -73,7 +75,7 @@ public static String get(String url) throws MalformedURLException { /** * Performs an HTTP GET on the given URL.
* Basic auth is used if both username and pw are not null. - * + * * @param url The URL where to connect to. * @param username Basic auth credential. No basic auth if null. * @param pw Basic auth credential. No basic auth if null. @@ -87,8 +89,9 @@ public static String get(String url, String username, String pw) { HttpClient client = new HttpClient(); HttpConnectionManager connectionManager = client.getHttpConnectionManager(); try { - setAuth(client, url, username, pw); - httpMethod = new GetMethod(url); + String encodedUrl = encodeUrl(url); + setAuth(client, encodedUrl, username, pw); + httpMethod = new GetMethod(encodedUrl); connectionManager.getParams().setConnectionTimeout(5000); int status = client.executeMethod(httpMethod); if (status == HttpStatus.SC_OK) { @@ -117,10 +120,62 @@ public static String get(String url, String username, String pw) { return null; } + static String encodeUrl(String url) { + // perform some simple encoding to the path names. This takes care of cases where + // layers or workspaces have illegal http characters. + // it cannot fix all + String protocol, authority, path, query, fragment = null; + String[] protocolPathParts = url.split("://", 2); + if (protocolPathParts.length == 1) { + // unexpected format so just try out url + return url; + } + + protocol = protocolPathParts[0]; + String[] pathQueryParts = protocolPathParts[1].split("\\?", 2); + path = pathQueryParts[0]; + if (pathQueryParts.length == 1) { + query = null; + } else { + query = pathQueryParts[1]; + } + + + if (query == null) { + String[] fragmentParts = path.split("#", 2); + if (fragmentParts.length > 1) { + path = fragmentParts[0]; + fragment = fragmentParts[1]; + } + } else { + String[] fragmentParts = query.split("#", 2); + if (fragmentParts.length > 1) { + query = fragmentParts[0]; + fragment = fragmentParts[1]; + } + } + + int firstSlash = path.indexOf('/'); + if (firstSlash > -1) { + authority = path.substring(0, firstSlash); + path = path.substring(firstSlash); + } else { + authority = path; + path = null; + } + + try { + return new URI(protocol, authority, path, query, fragment).toString(); + } catch (URISyntaxException e) { + // fallback to original string + return url; + } + } + /** * PUTs a File to the given URL.
* Basic auth is used if both username and pw are not null. - * + * * @param url The URL where to connect to. * @param file The File to be sent. * @param contentType The content-type to advert in the PUT. @@ -138,7 +193,7 @@ public static String put(String url, File file, String contentType, String usern /** * PUTs a String to the given URL.
* Basic auth is used if both username and pw are not null. - * + * * @param url The URL where to connect to. * @param content The content to be sent as a String. * @param contentType The content-type to advert in the PUT. @@ -161,7 +216,7 @@ public static String put(String url, String content, String contentType, String /** * PUTs a String representing an XML document to the given URL.
* Basic auth is used if both username and pw are not null. - * + * * @param url The URL where to connect to. * @param content The XML content to be sent as a String. * @param username Basic auth credential. No basic auth if null. @@ -178,7 +233,7 @@ public static String putXml(String url, String content, String username, String /** * Performs a PUT to the given URL.
* Basic auth is used if both username and pw are not null. - * + * * @param url The URL where to connect to. * @param requestEntity The request to be sent. * @param username Basic auth credential. No basic auth if null. @@ -195,7 +250,7 @@ public static String put(String url, RequestEntity requestEntity, String usernam /** * POSTs a File to the given URL.
* Basic auth is used if both username and pw are not null. - * + * * @param url The URL where to connect to. * @param file The File to be sent. * @param contentType The content-type to advert in the POST. @@ -213,7 +268,7 @@ public static String post(String url, File file, String contentType, String user /** * POSTs a String to the given URL.
* Basic auth is used if both username and pw are not null. - * + * * @param url The URL where to connect to. * @param content The content to be sent as a String. * @param contentType The content-type to advert in the POST. @@ -236,7 +291,7 @@ public static String post(String url, String content, String contentType, String /** * POSTs a String representing an XML document to the given URL.
* Basic auth is used if both username and pw are not null. - * + * * @param url The URL where to connect to. * @param content The XML content to be sent as a String. * @param username Basic auth credential. No basic auth if null. @@ -253,7 +308,7 @@ public static String postXml(String url, String content, String username, String /** * Performs a POST to the given URL.
* Basic auth is used if both username and pw are not null. - * + * * @param url The URL where to connect to. * @param requestEntity The request to be sent. * @param username Basic auth credential. No basic auth if null. @@ -279,15 +334,16 @@ public static String post(String url, RequestEntity requestEntity, String userna * * are accepted as successful codes; in these cases the response string will * be returned. - * + * * @return the HTTP response or null on errors. */ private static String send(final EntityEnclosingMethod httpMethod, String url, RequestEntity requestEntity, String username, String pw) { HttpClient client = new HttpClient(); HttpConnectionManager connectionManager = client.getHttpConnectionManager(); + String encodedUrl = encodeUrl(url); try { - setAuth(client, url, username, pw); + setAuth(client, encodedUrl, username, pw); connectionManager.getParams().setConnectionTimeout(5000); if (requestEntity != null) httpMethod.setRequestEntity(requestEntity); @@ -304,15 +360,15 @@ private static String send(final EntityEnclosingMethod httpMethod, String url, return response; default: LOGGER.warn("Bad response: code[" + status + "]" + " msg[" + httpMethod.getStatusText() + "]" - + " url[" + url + "]" + " method[" + httpMethod.getClass().getSimpleName() + + " url[" + encodedUrl + "]" + " method[" + httpMethod.getClass().getSimpleName() + "]: " + IOUtils.toString(httpMethod.getResponseBodyAsStream())); return null; } } catch (ConnectException e) { - LOGGER.info("Couldn't connect to [" + url + "]"); + LOGGER.info("Couldn't connect to [" + encodedUrl + "]"); return null; } catch (IOException e) { - LOGGER.error("Error talking to " + url + " : " + e.getLocalizedMessage()); + LOGGER.error("Error talking to " + encodedUrl + " : " + e.getLocalizedMessage()); return null; } finally { if (httpMethod != null) @@ -326,9 +382,10 @@ public static boolean delete(String url, final String user, final String pw) { DeleteMethod httpMethod = null; HttpClient client = new HttpClient(); HttpConnectionManager connectionManager = client.getHttpConnectionManager(); + String encodedUrl = encodeUrl(url); try { - setAuth(client, url, user, pw); - httpMethod = new DeleteMethod(url); + setAuth(client, encodedUrl, user, pw); + httpMethod = new DeleteMethod(encodedUrl); connectionManager.getParams().setConnectionTimeout(5000); int status = client.executeMethod(httpMethod); String response = ""; @@ -336,23 +393,23 @@ public static boolean delete(String url, final String user, final String pw) { InputStream is = httpMethod.getResponseBodyAsStream(); response = IOUtils.toString(is); IOUtils.closeQuietly(is); - if (response.trim().equals("")) { + if (response.trim().equals("")) { if (LOGGER.isDebugEnabled()) LOGGER .debug("ResponseBody is empty (this may be not an error since we just performed a DELETE call)"); return true; } if (LOGGER.isDebugEnabled()) - LOGGER.debug("(" + status + ") " + httpMethod.getStatusText() + " -- " + url); + LOGGER.debug("(" + status + ") " + httpMethod.getStatusText() + " -- " + encodedUrl); return true; } else { - LOGGER.info("(" + status + ") " + httpMethod.getStatusText() + " -- " + url); + LOGGER.info("(" + status + ") " + httpMethod.getStatusText() + " -- " + encodedUrl); LOGGER.info("Response: '" + response + "'"); } } catch (ConnectException e) { - LOGGER.info("Couldn't connect to [" + url + "]"); + LOGGER.info("Couldn't connect to [" + encodedUrl + "]"); } catch (IOException e) { - LOGGER.info("Error talking to [" + url + "]", e); + LOGGER.info("Error talking to [" + encodedUrl + "]", e); } finally { if (httpMethod != null) httpMethod.releaseConnection(); @@ -375,12 +432,13 @@ public static boolean httpPing(String url, String username, String pw) { HttpClient client = new HttpClient(); HttpConnectionManager connectionManager = client.getHttpConnectionManager(); try { - setAuth(client, url, username, pw); - httpMethod = new GetMethod(url); + String encodedUrl = encodeUrl(url); + setAuth(client, encodedUrl, username, pw); + httpMethod = new GetMethod(encodedUrl); connectionManager.getParams().setConnectionTimeout(2000); int status = client.executeMethod(httpMethod); if (status != HttpStatus.SC_OK) { - LOGGER.warn("PING failed at '" + url + "': (" + status + ") " + httpMethod.getStatusText()); + LOGGER.warn("PING failed at '" + encodedUrl + "': (" + status + ") " + httpMethod.getStatusText()); return false; } else { return true; @@ -399,7 +457,7 @@ public static boolean httpPing(String url, String username, String pw) { /** * Used to query for REST resources. - * + * * @param url The URL of the REST resource to query about. * @param username * @param pw @@ -412,8 +470,9 @@ public static boolean exists(String url, String username, String pw) { HttpClient client = new HttpClient(); HttpConnectionManager connectionManager = client.getHttpConnectionManager(); try { - setAuth(client, url, username, pw); - httpMethod = new GetMethod(url); + String encodedUrl = encodeUrl(url); + setAuth(client, encodedUrl, username, pw); + httpMethod = new GetMethod(encodedUrl); connectionManager.getParams().setConnectionTimeout(2000); int status = client.executeMethod(httpMethod); switch (status) { @@ -422,7 +481,7 @@ public static boolean exists(String url, String username, String pw) { case HttpStatus.SC_NOT_FOUND: return false; default: - throw new RuntimeException("Unhandled response status at '" + url + "': (" + status + ") " + throw new RuntimeException("Unhandled response status at '" + encodedUrl + "': (" + status + ") " + httpMethod.getStatusText()); } } catch (ConnectException e) { @@ -438,7 +497,9 @@ public static boolean exists(String url, String username, String pw) { private static void setAuth(HttpClient client, String url, String username, String pw) throws MalformedURLException { - URL u = new URL(url); + + String encodedUrl = encodeUrl(url); + URL u = new URL(encodedUrl); if (username != null && pw != null) { Credentials defaultcreds = new UsernamePasswordCredentials(username, pw); client.getState().setCredentials(new AuthScope(u.getHost(), u.getPort()), defaultcreds); @@ -449,14 +510,14 @@ private static void setAuth(HttpClient client, String url, String username, Stri // authentication } else { if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Not setting credentials to access to " + url); + LOGGER.debug("Not setting credentials to access to " + encodedUrl); } } } /** * @param geoserverURL - * @return recursively remove ending slashes + * @return recursively remove ending slashes */ public static String decurtSlash(String geoserverURL) { if (geoserverURL!=null && geoserverURL.endsWith("/")) { @@ -464,7 +525,7 @@ public static String decurtSlash(String geoserverURL) { } return geoserverURL; } - + /** * @param str a string array * @return create a StringBuilder appending all the passed arguments @@ -473,7 +534,7 @@ public static StringBuilder append(String ... str){ if (str==null){ return null; } - + StringBuilder buf=new StringBuilder(); for (String s: str){ if (s!=null) @@ -481,7 +542,7 @@ public static StringBuilder append(String ... str){ } return buf; } - + /** * Wrapper for {@link #append(String...)} * @param base base URL @@ -492,7 +553,7 @@ public static StringBuilder append(URL base, String ... str){ if (str==null){ return append(base.toString()); } - + StringBuilder buf=new StringBuilder(base.toString()); for (String s: str){ if (s!=null) diff --git a/src/test/java/it/geosolutions/geoserver/rest/HTTPUtilsTest.java b/src/test/java/it/geosolutions/geoserver/rest/HTTPUtilsTest.java new file mode 100644 index 00000000..22bfc898 --- /dev/null +++ b/src/test/java/it/geosolutions/geoserver/rest/HTTPUtilsTest.java @@ -0,0 +1,16 @@ +package it.geosolutions.geoserver.rest; + +import junit.framework.TestCase; + +public class HTTPUtilsTest extends TestCase { + public void testEncodeUrl() throws Exception { + assertEquals("http://with%20spaces", HTTPUtils.encodeUrl("http://with spaces")); + assertEquals("http://with%20spaces?p1=v1", HTTPUtils.encodeUrl("http://with spaces?p1=v1")); + assertEquals("http://without/spaces?p1=v1", HTTPUtils.encodeUrl("http://without/spaces?p1=v1")); + assertEquals("http://without/spaces", HTTPUtils.encodeUrl("http://without/spaces")); + assertEquals("http://without/spaces#fragment", HTTPUtils.encodeUrl("http://without/spaces#fragment")); + assertEquals("http://without/spaces?p1=v1#fragment", HTTPUtils.encodeUrl("http://without/spaces?p1=v1#fragment")); + assertEquals("http://with%20spaces#fragment", HTTPUtils.encodeUrl("http://with spaces#fragment")); + assertEquals("brokenurl?p1=v1", HTTPUtils.encodeUrl("brokenurl?p1=v1")); + } +}