ProxyServlet.java 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. /**
  2. * $Id: ProxyServlet.java,v 1.4 2013/12/13 13:18:11 david Exp $
  3. * Copyright (c) 2011-2012, JGraph Ltd
  4. */
  5. package com.mxgraph.online;
  6. import java.io.BufferedInputStream;
  7. import java.io.ByteArrayOutputStream;
  8. import java.io.FileNotFoundException;
  9. import java.io.IOException;
  10. import java.io.InputStream;
  11. import java.io.OutputStream;
  12. import java.net.HttpURLConnection;
  13. import java.net.URL;
  14. import java.net.URLConnection;
  15. import java.net.UnknownHostException;
  16. import java.util.logging.Level;
  17. import java.util.logging.Logger;
  18. import javax.servlet.ServletException;
  19. import javax.servlet.http.HttpServlet;
  20. import javax.servlet.http.HttpServletRequest;
  21. import javax.servlet.http.HttpServletResponse;
  22. import com.google.apphosting.api.DeadlineExceededException;
  23. import com.mxgraph.online.Utils.UnsupportedContentException;
  24. /**
  25. * Servlet implementation ProxyServlet
  26. */
  27. @SuppressWarnings("serial")
  28. public class ProxyServlet extends HttpServlet
  29. {
  30. private static final Logger log = Logger
  31. .getLogger(HttpServlet.class.getName());
  32. /**
  33. * Buffer size for content pass-through.
  34. */
  35. private static int BUFFER_SIZE = 3 * 1024;
  36. /**
  37. * A resuable empty byte array instance.
  38. */
  39. private static byte[] emptyBytes = new byte[0];
  40. /**
  41. * @see HttpServlet#HttpServlet()
  42. */
  43. public ProxyServlet()
  44. {
  45. super();
  46. }
  47. /**
  48. * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
  49. */
  50. protected void doGet(HttpServletRequest request,
  51. HttpServletResponse response) throws ServletException, IOException
  52. {
  53. String urlParam = request.getParameter("url");
  54. if (checkUrlParameter(urlParam))
  55. {
  56. // build the UML source from the compressed request parameter
  57. String ref = request.getHeader("referer");
  58. String ua = request.getHeader("User-Agent");
  59. String dom = getCorsDomain(ref, ua);
  60. try(OutputStream out = response.getOutputStream())
  61. {
  62. request.setCharacterEncoding("UTF-8");
  63. response.setCharacterEncoding("UTF-8");
  64. URL url = new URL(urlParam);
  65. URLConnection connection = url.openConnection();
  66. response.setHeader("Cache-Control", "private, max-age=86400");
  67. // Workaround for 451 response from Iconfinder CDN
  68. connection.setRequestProperty("User-Agent", "draw.io");
  69. if (dom != null && dom.length() > 0)
  70. {
  71. response.addHeader("Access-Control-Allow-Origin", dom);
  72. }
  73. // Status code pass-through and follow redirects
  74. if (connection instanceof HttpURLConnection)
  75. {
  76. ((HttpURLConnection) connection)
  77. .setInstanceFollowRedirects(true);
  78. int status = ((HttpURLConnection) connection)
  79. .getResponseCode();
  80. int counter = 0;
  81. // Follows a maximum of 2 redirects
  82. while (counter++ < 2
  83. && (status == HttpURLConnection.HTTP_MOVED_PERM
  84. || status == HttpURLConnection.HTTP_MOVED_TEMP))
  85. {
  86. url = new URL(connection.getHeaderField("Location"));
  87. connection = url.openConnection();
  88. ((HttpURLConnection) connection)
  89. .setInstanceFollowRedirects(true);
  90. // Workaround for 451 response from Iconfinder CDN
  91. connection.setRequestProperty("User-Agent", "draw.io");
  92. status = ((HttpURLConnection) connection)
  93. .getResponseCode();
  94. }
  95. response.setStatus(status);
  96. // Copies input stream to output stream
  97. InputStream is = connection.getInputStream();
  98. byte[] head = (contentAlwaysAllowed(urlParam)) ? emptyBytes
  99. : Utils.checkStreamContent(is);
  100. response.setContentType("application/octet-stream");
  101. String base64 = request.getParameter("base64");
  102. copyResponse(is, out, head,
  103. base64 != null && base64.equals("1"));
  104. }
  105. out.flush();
  106. log.log(Level.FINEST, "processed proxy request: url="
  107. + ((urlParam != null) ? urlParam : "[null]")
  108. + ", referer=" + ((ref != null) ? ref : "[null]")
  109. + ", user agent=" + ((ua != null) ? ua : "[null]"));
  110. }
  111. catch (DeadlineExceededException e)
  112. {
  113. response.setStatus(HttpServletResponse.SC_REQUEST_TIMEOUT);
  114. }
  115. catch (UnknownHostException | FileNotFoundException e)
  116. {
  117. // do not log 404 and DNS errors
  118. response.setStatus(HttpServletResponse.SC_NOT_FOUND);
  119. }
  120. catch (UnsupportedContentException e)
  121. {
  122. response.setStatus(HttpServletResponse.SC_FORBIDDEN);
  123. log.log(Level.SEVERE, "proxy request with invalid content: url="
  124. + ((urlParam != null) ? urlParam : "[null]")
  125. + ", referer=" + ((ref != null) ? ref : "[null]")
  126. + ", user agent=" + ((ua != null) ? ua : "[null]"));
  127. }
  128. catch (Exception e)
  129. {
  130. response.setStatus(
  131. HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
  132. log.log(Level.FINE, "proxy request failed: url="
  133. + ((urlParam != null) ? urlParam : "[null]")
  134. + ", referer=" + ((ref != null) ? ref : "[null]")
  135. + ", user agent=" + ((ua != null) ? ua : "[null]"));
  136. e.printStackTrace();
  137. }
  138. }
  139. else
  140. {
  141. response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
  142. log.log(Level.SEVERE,
  143. "proxy request with invalid URL parameter: url="
  144. + ((urlParam != null) ? urlParam : "[null]"));
  145. }
  146. }
  147. /**
  148. * Dynamically generated CORS header for known domains.
  149. * @throws IOException
  150. */
  151. protected void copyResponse(InputStream is, OutputStream out, byte[] head,
  152. boolean base64) throws IOException
  153. {
  154. if (base64)
  155. {
  156. try (BufferedInputStream in = new BufferedInputStream(is,
  157. BUFFER_SIZE))
  158. {
  159. ByteArrayOutputStream os = new ByteArrayOutputStream();
  160. byte[] buffer = new byte[0xFFFF];
  161. os.write(head, 0, head.length);
  162. for (int len = is.read(buffer); len != -1; len = is.read(buffer))
  163. {
  164. os.write(buffer, 0, len);
  165. }
  166. out.write(mxBase64.encodeToString(os.toByteArray(), false).getBytes());
  167. }
  168. }
  169. else
  170. {
  171. out.write(head);
  172. Utils.copy(is, out);
  173. }
  174. }
  175. /**
  176. * Checks if the URL parameter is legal.
  177. */
  178. public boolean checkUrlParameter(String url)
  179. {
  180. return url != null
  181. && (url.startsWith("http://") || url.startsWith("https://"))
  182. && !url.toLowerCase().contains("://metadata.google.internal/");
  183. }
  184. /**
  185. * Returns true if the content check should be omitted.
  186. */
  187. public boolean contentAlwaysAllowed(String url)
  188. {
  189. return url.toLowerCase()
  190. .startsWith("https://trello-attachments.s3.amazonaws.com/")
  191. || url.toLowerCase().startsWith("https://docs.google.com/");
  192. }
  193. /**
  194. * Gets CORS header for request. Returning null means do not respond.
  195. */
  196. protected String getCorsDomain(String referer, String userAgent)
  197. {
  198. String dom = null;
  199. if (referer != null && referer.toLowerCase()
  200. .matches("https?://([a-z0-9,-]+[.])*draw[.]io/.*"))
  201. {
  202. dom = referer.toLowerCase().substring(0,
  203. referer.indexOf(".draw.io/") + 8);
  204. }
  205. else if (referer != null && referer.toLowerCase()
  206. .matches("https?://([a-z0-9,-]+[.])*quipelements[.]com/.*"))
  207. {
  208. dom = referer.toLowerCase().substring(0,
  209. referer.indexOf(".quipelements.com/") + 17);
  210. }
  211. // Enables Confluence/Jira proxy via referer or hardcoded user-agent (for old versions)
  212. // UA refers to old FF on macOS so low risk and fixes requests from existing servers
  213. else if ((referer != null
  214. && referer.equals("draw.io Proxy Confluence Server"))
  215. || (userAgent != null && userAgent.equals(
  216. "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:50.0) Gecko/20100101 Firefox/50.0")))
  217. {
  218. dom = "";
  219. }
  220. return dom;
  221. }
  222. }