|
@@ -5,14 +5,15 @@
|
|
|
package com.mxgraph.online;
|
|
|
|
|
|
import java.io.BufferedInputStream;
|
|
|
+import java.io.FileNotFoundException;
|
|
|
import java.io.IOException;
|
|
|
+import java.io.InputStream;
|
|
|
import java.io.OutputStream;
|
|
|
import java.net.HttpURLConnection;
|
|
|
import java.net.URL;
|
|
|
import java.net.URLConnection;
|
|
|
+import java.net.UnknownHostException;
|
|
|
import java.util.Arrays;
|
|
|
-import java.util.HashSet;
|
|
|
-import java.util.Set;
|
|
|
import java.util.logging.Level;
|
|
|
import java.util.logging.Logger;
|
|
|
|
|
@@ -21,6 +22,8 @@ import javax.servlet.http.HttpServlet;
|
|
|
import javax.servlet.http.HttpServletRequest;
|
|
|
import javax.servlet.http.HttpServletResponse;
|
|
|
|
|
|
+import com.mxgraph.online.Utils.UnsupportedContentException;
|
|
|
+
|
|
|
/**
|
|
|
* Servlet implementation ProxyServlet
|
|
|
*/
|
|
@@ -29,14 +32,16 @@ public class ProxyServlet extends HttpServlet
|
|
|
{
|
|
|
private static final Logger log = Logger
|
|
|
.getLogger(HttpServlet.class.getName());
|
|
|
-
|
|
|
- private static final String[] setValues = new String[]
|
|
|
- {"image/svg+xml", "image/png", "application/vnd.jgraph.mxfile.realtime", "image/jpeg",
|
|
|
- "application/xml", "image/x-wmf", "image/gif", "image/webp", "text/plain",
|
|
|
- "application/x-font-ttf", "application/x-font-truetype", "application/x-font-opentype",
|
|
|
- "application/font-woff", "application/font-woff2", "application/vnd.ms-fontobject",
|
|
|
- "application/font-sfnt"};
|
|
|
- private static final Set<String> allowedContent = new HashSet<String>(Arrays.asList(setValues));
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Buffer size for content pass-through.
|
|
|
+ */
|
|
|
+ private static int BUFFER_SIZE = 3 * 1024;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * A resuable empty byte array instance.
|
|
|
+ */
|
|
|
+ private static byte[] emptyBytes = new byte[0];
|
|
|
|
|
|
/**
|
|
|
* @see HttpServlet#HttpServlet()
|
|
@@ -53,171 +58,215 @@ public class ProxyServlet extends HttpServlet
|
|
|
HttpServletResponse response) throws ServletException, IOException
|
|
|
{
|
|
|
String urlParam = request.getParameter("url");
|
|
|
-
|
|
|
- // build the UML source from the compressed request parameter
|
|
|
- String ua = request.getHeader("User-Agent");
|
|
|
- String ref = request.getHeader("referer");
|
|
|
- boolean contentTypeAllowed = false;
|
|
|
- boolean urlAllowed = true;
|
|
|
-
|
|
|
- String dom = "";
|
|
|
-
|
|
|
- if (urlParam != null && urlParam.toLowerCase().contains("://metadata.google.internal/"))
|
|
|
- {
|
|
|
- urlAllowed = false;
|
|
|
- log.log(Level.CONFIG, "proxy request to metadata.google.internal");
|
|
|
- }
|
|
|
- else if (ref != null && ref.toLowerCase()
|
|
|
- .matches("https?://([a-z0-9,-]+[.])*draw[.]io/.*"))
|
|
|
- {
|
|
|
- dom = ref.toLowerCase().substring(0, ref.indexOf(".draw.io/") + 8);
|
|
|
- }
|
|
|
- else if (ref != null && ref.toLowerCase()
|
|
|
- .matches("https?://([a-z0-9,-]+[.])*quipelements[.]com/.*"))
|
|
|
- {
|
|
|
- dom = ref.toLowerCase().substring(0,
|
|
|
- ref.indexOf(".quipelements.com/") + 17);
|
|
|
- }
|
|
|
- // Enables Confluence/Jira proxy via referer or hardcoded user-agent (for old versions)
|
|
|
- // UA refers to old FF on macOS so low risk and fixes requests from existing servers
|
|
|
- else if ((ref != null && ref.equals("draw.io Proxy Confluence Server"))
|
|
|
- || (ua != null && ua.equals(
|
|
|
- "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:50.0) Gecko/20100101 Firefox/50.0")))
|
|
|
- {
|
|
|
- dom = "";
|
|
|
- }
|
|
|
|
|
|
- if (urlAllowed && dom != null && urlParam != null && (urlParam.startsWith("http://")
|
|
|
- || urlParam.startsWith("https://")))
|
|
|
+ if (checkUrlParameter(urlParam))
|
|
|
{
|
|
|
- request.setCharacterEncoding("UTF-8");
|
|
|
- response.setCharacterEncoding("UTF-8");
|
|
|
-
|
|
|
- OutputStream out = response.getOutputStream();
|
|
|
+ // build the UML source from the compressed request parameter
|
|
|
+ String ref = request.getHeader("referer");
|
|
|
+ String ua = request.getHeader("User-Agent");
|
|
|
+ String dom = getCorsDomain(ref, ua);
|
|
|
|
|
|
try
|
|
|
{
|
|
|
- URL url = new URL(urlParam);
|
|
|
- URLConnection connection = url.openConnection();
|
|
|
- response.setHeader("Cache-Control", "private, max-age=86400");
|
|
|
+ request.setCharacterEncoding("UTF-8");
|
|
|
+ response.setCharacterEncoding("UTF-8");
|
|
|
|
|
|
- if (dom != null && dom.length() > 0)
|
|
|
+ if (dom != null)
|
|
|
{
|
|
|
- response.addHeader("Access-Control-Allow-Origin", dom);
|
|
|
- }
|
|
|
+ URL url = new URL(urlParam);
|
|
|
+ URLConnection connection = url.openConnection();
|
|
|
+ OutputStream out = response.getOutputStream();
|
|
|
+ response.setHeader("Cache-Control",
|
|
|
+ "private, max-age=86400");
|
|
|
|
|
|
- connection.setRequestProperty("User-Agent", "draw.io");
|
|
|
+ // Workaround for 451 response from Iconfinder CDN
|
|
|
+ connection.setRequestProperty("User-Agent", "draw.io");
|
|
|
|
|
|
- // Status code pass-through and follow redirects
|
|
|
- if (connection instanceof HttpURLConnection)
|
|
|
- {
|
|
|
- ((HttpURLConnection) connection)
|
|
|
- .setInstanceFollowRedirects(true);
|
|
|
+ if (dom.length() > 0)
|
|
|
+ {
|
|
|
+ response.addHeader("Access-Control-Allow-Origin", dom);
|
|
|
+ }
|
|
|
|
|
|
- // Workaround for 451 response from Iconfinder CDN
|
|
|
- int status = ((HttpURLConnection) connection)
|
|
|
- .getResponseCode();
|
|
|
- int counter = 0;
|
|
|
-
|
|
|
- // Follows a maximum of 2 redirects
|
|
|
- while (counter++ < 2
|
|
|
- && (status == HttpURLConnection.HTTP_MOVED_PERM
|
|
|
- || status == HttpURLConnection.HTTP_MOVED_TEMP))
|
|
|
+ // Status code pass-through and follow redirects
|
|
|
+ if (connection instanceof HttpURLConnection)
|
|
|
{
|
|
|
- url = new URL(connection.getHeaderField("Location"));
|
|
|
- connection = url.openConnection();
|
|
|
((HttpURLConnection) connection)
|
|
|
.setInstanceFollowRedirects(true);
|
|
|
-
|
|
|
- // Workaround for 451 response from Iconfinder CDN
|
|
|
- connection.setRequestProperty("User-Agent", "draw.io");
|
|
|
- status = ((HttpURLConnection) connection)
|
|
|
+ int status = ((HttpURLConnection) connection)
|
|
|
.getResponseCode();
|
|
|
- }
|
|
|
+ int counter = 0;
|
|
|
|
|
|
- response.setStatus(status);
|
|
|
- }
|
|
|
-
|
|
|
- String base64 = request.getParameter("base64");
|
|
|
-
|
|
|
- if (connection != null)
|
|
|
- {
|
|
|
- String contentType = connection.getContentType();
|
|
|
-
|
|
|
- if (contentType != null && allowedContent.contains(contentType))
|
|
|
- {
|
|
|
- contentTypeAllowed = true;
|
|
|
- }
|
|
|
-
|
|
|
- urlAllowed = false;
|
|
|
- boolean validateHtml = false;
|
|
|
-
|
|
|
- if (!contentTypeAllowed)
|
|
|
- {
|
|
|
- if (urlParam != null && urlParam.toLowerCase().startsWith("https://trello-attachments.s3.amazonaws.com/"))
|
|
|
+ // Follows a maximum of 2 redirects
|
|
|
+ while (counter++ < 2
|
|
|
+ && (status == HttpURLConnection.HTTP_MOVED_PERM
|
|
|
+ || status == HttpURLConnection.HTTP_MOVED_TEMP))
|
|
|
{
|
|
|
- urlAllowed = true;
|
|
|
- }
|
|
|
- }
|
|
|
+ url = new URL(
|
|
|
+ connection.getHeaderField("Location"));
|
|
|
+ connection = url.openConnection();
|
|
|
+ ((HttpURLConnection) connection)
|
|
|
+ .setInstanceFollowRedirects(true);
|
|
|
|
|
|
- if (!contentTypeAllowed && !urlAllowed)
|
|
|
- {
|
|
|
- if (contentType != null && contentType.equals("text/html"))
|
|
|
- {
|
|
|
- validateHtml = true;
|
|
|
+ // Workaround for 451 response from Iconfinder CDN
|
|
|
+ connection.setRequestProperty("User-Agent",
|
|
|
+ "draw.io");
|
|
|
+ status = ((HttpURLConnection) connection)
|
|
|
+ .getResponseCode();
|
|
|
}
|
|
|
|
|
|
- log.log(Level.CONFIG, "proxyContent=" + contentType);
|
|
|
- }
|
|
|
+ response.setStatus(status);
|
|
|
|
|
|
- response.setContentType("application/octet-stream");
|
|
|
+ // Copies input stream to output stream
|
|
|
+ InputStream is = connection.getInputStream();
|
|
|
+ byte[] head = (contentAlwaysAllowed(urlParam))
|
|
|
+ ? emptyBytes
|
|
|
+ : Utils.checkStreamContent(is);
|
|
|
+ response.setContentType("application/octet-stream");
|
|
|
+ String base64 = request.getParameter("base64");
|
|
|
+ copyResponse(is, out, head,
|
|
|
+ base64 != null && base64.equals("1"));
|
|
|
+ }
|
|
|
|
|
|
- if (base64 != null && base64.equals("1"))
|
|
|
- {
|
|
|
- int BUFFER_SIZE = 3 * 1024;
|
|
|
+ out.flush();
|
|
|
+ out.close();
|
|
|
|
|
|
- try (BufferedInputStream in = new BufferedInputStream(
|
|
|
- connection.getInputStream(), BUFFER_SIZE);)
|
|
|
- {
|
|
|
- StringBuilder result = new StringBuilder();
|
|
|
- byte[] chunk = new byte[BUFFER_SIZE];
|
|
|
- int len = 0;
|
|
|
- while ((len = in.read(chunk)) == BUFFER_SIZE)
|
|
|
- {
|
|
|
- result.append(
|
|
|
- mxBase64.encodeToString(chunk, false));
|
|
|
- }
|
|
|
-
|
|
|
- if (len > 0)
|
|
|
- {
|
|
|
- chunk = Arrays.copyOf(chunk, len);
|
|
|
- result.append(
|
|
|
- mxBase64.encodeToString(chunk, false));
|
|
|
- }
|
|
|
-
|
|
|
- out.write(result.toString().getBytes());
|
|
|
- }
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- Utils.copy(connection.getInputStream(), out);
|
|
|
- }
|
|
|
+ log.log(Level.FINEST, "processed proxy request: url="
|
|
|
+ + ((urlParam != null) ? urlParam : "[null]")
|
|
|
+ + ", referer=" + ((ref != null) ? ref : "[null]")
|
|
|
+ + ", user agent=" + ((ua != null) ? ua : "[null]"));
|
|
|
}
|
|
|
-
|
|
|
- out.flush();
|
|
|
- out.close();
|
|
|
+ else
|
|
|
+ {
|
|
|
+ response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
|
|
+ log.log(Level.SEVERE,
|
|
|
+ "proxy request with invalid referer: url="
|
|
|
+ + ((urlParam != null) ? urlParam : "[null]")
|
|
|
+ + ", referer="
|
|
|
+ + ((ref != null) ? ref : "[null]")
|
|
|
+ + ", user agent="
|
|
|
+ + ((ua != null) ? ua : "[null]"));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ catch (UnknownHostException | FileNotFoundException e)
|
|
|
+ {
|
|
|
+ // do not log 404 and DNS errors
|
|
|
+ response.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
|
|
+ }
|
|
|
+ catch (UnsupportedContentException e)
|
|
|
+ {
|
|
|
+ response.setStatus(HttpServletResponse.SC_FORBIDDEN);
|
|
|
+ log.log(Level.SEVERE, "proxy request with invalid content: url="
|
|
|
+ + ((urlParam != null) ? urlParam : "[null]")
|
|
|
+ + ", referer=" + ((ref != null) ? ref : "[null]")
|
|
|
+ + ", user agent=" + ((ua != null) ? ua : "[null]"));
|
|
|
}
|
|
|
catch (Exception e)
|
|
|
{
|
|
|
response.setStatus(
|
|
|
HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
|
|
+ log.log(Level.FINE, "proxy request failed: url="
|
|
|
+ + ((urlParam != null) ? urlParam : "[null]")
|
|
|
+ + ", referer=" + ((ref != null) ? ref : "[null]")
|
|
|
+ + ", user agent=" + ((ua != null) ? ua : "[null]"));
|
|
|
e.printStackTrace();
|
|
|
}
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
|
|
|
+ log.log(Level.SEVERE,
|
|
|
+ "proxy request with invalid URL parameter: url="
|
|
|
+ + ((urlParam != null) ? urlParam : "[null]"));
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Dynamically generated CORS header for known domains.
|
|
|
+ * @throws IOException
|
|
|
+ */
|
|
|
+ protected void copyResponse(InputStream is, OutputStream out, byte[] head,
|
|
|
+ boolean base64) throws IOException
|
|
|
+ {
|
|
|
+ if (base64)
|
|
|
+ {
|
|
|
+ try (BufferedInputStream in = new BufferedInputStream(is,
|
|
|
+ BUFFER_SIZE))
|
|
|
+ {
|
|
|
+ StringBuilder result = new StringBuilder();
|
|
|
+ result.append(mxBase64.encodeToString(head, false));
|
|
|
+ byte[] chunk = new byte[BUFFER_SIZE];
|
|
|
+ int len = 0;
|
|
|
+
|
|
|
+ while ((len = in.read(chunk)) == BUFFER_SIZE)
|
|
|
+ {
|
|
|
+ result.append(mxBase64.encodeToString(chunk, false));
|
|
|
+ }
|
|
|
+
|
|
|
+ if (len > 0)
|
|
|
+ {
|
|
|
+ chunk = Arrays.copyOf(chunk, len);
|
|
|
+ result.append(mxBase64.encodeToString(chunk, false));
|
|
|
+ }
|
|
|
+
|
|
|
+ out.write(result.toString().getBytes());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ out.write(head);
|
|
|
+ Utils.copy(is, out);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Checks if the URL parameter is legal.
|
|
|
+ */
|
|
|
+ public boolean checkUrlParameter(String url)
|
|
|
+ {
|
|
|
+ return url != null
|
|
|
+ && (url.startsWith("http://") || url.startsWith("https://"))
|
|
|
+ && !url.toLowerCase().contains("://metadata.google.internal/");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Returns true if the content check should be omitted.
|
|
|
+ */
|
|
|
+ public boolean contentAlwaysAllowed(String url)
|
|
|
+ {
|
|
|
+ return url.toLowerCase()
|
|
|
+ .startsWith("https://trello-attachments.s3.amazonaws.com/");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Gets CORS header for request. Returning null means do not respond.
|
|
|
+ */
|
|
|
+ protected String getCorsDomain(String referer, String userAgent)
|
|
|
+ {
|
|
|
+ String dom = null;
|
|
|
+
|
|
|
+ if (referer != null && referer.toLowerCase()
|
|
|
+ .matches("https?://([a-z0-9,-]+[.])*draw[.]io/.*"))
|
|
|
+ {
|
|
|
+ dom = referer.toLowerCase().substring(0,
|
|
|
+ referer.indexOf(".draw.io/") + 8);
|
|
|
+ }
|
|
|
+ else if (referer != null && referer.toLowerCase()
|
|
|
+ .matches("https?://([a-z0-9,-]+[.])*quipelements[.]com/.*"))
|
|
|
+ {
|
|
|
+ dom = referer.toLowerCase().substring(0,
|
|
|
+ referer.indexOf(".quipelements.com/") + 17);
|
|
|
+ }
|
|
|
+ // Enables Confluence/Jira proxy via referer or hardcoded user-agent (for old versions)
|
|
|
+ // UA refers to old FF on macOS so low risk and fixes requests from existing servers
|
|
|
+ else if ((referer != null
|
|
|
+ && referer.equals("draw.io Proxy Confluence Server"))
|
|
|
+ || (userAgent != null && userAgent.equals(
|
|
|
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:50.0) Gecko/20100101 Firefox/50.0")))
|
|
|
+ {
|
|
|
+ dom = "";
|
|
|
+ }
|
|
|
+
|
|
|
+ return dom;
|
|
|
+ }
|
|
|
+
|
|
|
}
|