001package jmri.web.servlet.help; 002 003import java.io.*; 004import java.nio.charset.StandardCharsets; 005import java.nio.file.Files; 006import java.nio.file.Paths; 007import java.util.regex.*; 008 009import javax.servlet.ServletException; 010import javax.servlet.annotation.WebServlet; 011import javax.servlet.http.HttpServlet; 012import javax.servlet.http.HttpServletRequest; 013import javax.servlet.http.HttpServletResponse; 014 015import static jmri.web.servlet.ServletUtil.APPLICATION_JAVASCRIPT; 016import static jmri.web.servlet.ServletUtil.UTF8_TEXT_HTML; 017import jmri.util.FileUtil; 018 019import org.eclipse.jetty.server.Request; 020import org.openide.util.lookup.ServiceProvider; 021 022/** 023 * Parse server side include tags on web pages 024 * @author Randall Wood (C) 2014, 2016 025 * @author Daniel Bergqvist (C) 2021 026 * @author mstevetodd (C) 2023 027 */ 028@WebServlet(name = "HelpSSIServlet", 029 urlPatterns = { 030 "/help", 031 "/plugin" 032 }) 033@ServiceProvider(service = HttpServlet.class) 034public class HelpSSIServlet extends HttpServlet { 035 036 private void handleRegularFile(String fileName, HttpServletResponse response) throws IOException { 037 response.setHeader("Connection", "Keep-Alive"); // NOI18N 038 String ext = fileName.substring(fileName.lastIndexOf('.')+1).toLowerCase(); 039 switch (ext) { 040 case "svg": 041 response.setContentType("image/svg"); 042 break; 043 case "png": 044 response.setContentType("image/png"); 045 break; 046 case "gif": 047 response.setContentType("image/gif"); 048 break; 049 case "jpg": 050 case "jpeg": 051 response.setContentType("image/jpeg"); 052 break; 053 case "js": 054 response.setContentType(APPLICATION_JAVASCRIPT); 055 break; 056 default: 057 response.setContentType("application/octet-stream"); 058 } 059 byte[] b = new byte[1024]; 060 if (fileName.startsWith("/plugin/")) { 061 String resourceName = fileName.substring("/plugin/".length()); 062 try (InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(resourceName)) { 063 if (inputStream != null) { 064 int byteRead; 065 while ((byteRead = inputStream.read(b)) != -1) { 066 response.getOutputStream().write(b, 0, byteRead); 067 } 068 } else { 069 String error = String.format("--- ERROR: Plugin resource \"%s\" couldn't be found", resourceName); 070 response.getOutputStream().write(error.getBytes()); 071 log.warn(error); 072 } 073 } 074 } else { 075 try (InputStream inputStream = new FileInputStream(fileName);) { 076 int byteRead; 077 while ((byteRead = inputStream.read(b)) != -1) { 078 response.getOutputStream().write(b, 0, byteRead); 079 } 080 } 081 } 082 response.getOutputStream().flush(); 083 } 084 085 private String convertDotDotFolders(String theFileName, String path) { 086 if (theFileName.startsWith("../")) { 087 String[] paths = path.split("/"); 088 int numDotDots = 0; 089 while (theFileName.startsWith("../")) { 090 theFileName = theFileName.substring(3); 091 numDotDots++; 092 } 093 if (numDotDots < paths.length) { 094 StringBuilder sb = new StringBuilder(); 095 for (int i=0; i < (paths.length - numDotDots); i++) { 096 sb.append(paths[i]).append('/'); 097 } 098 theFileName = sb.toString() + theFileName; 099 } else { 100 // We have more ../ than subfolders in path 101 theFileName = '/' + theFileName; 102 } 103 } 104 return theFileName; 105 } 106 107 private String quoteBackslash(String content) { 108 // A single backslash needs to be replaced by a double backslash 109 return content.replaceAll("\\\\", "\\\\\\\\"); 110 } 111 112 private String readAndParseFile(String fileName) throws IOException { 113 log.debug("readAndParseFile('{}')", fileName); 114 115 int lastSlash = fileName.lastIndexOf('/'); 116 String path = lastSlash != -1 ? fileName.substring(0, lastSlash+1) : ""; 117 118 if (!fileName.startsWith("/plugin/")) { 119 fileName = FileUtil.getProgramPath() + fileName; 120 } 121 122 String content; 123 try { 124 if (fileName.startsWith("/plugin/")) { 125 String resourceName = fileName.substring("/plugin/".length()); 126 try (InputStream is = this.getClass().getClassLoader().getResourceAsStream(resourceName)) { 127 if (is != null) { 128 try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { 129 130 StringBuilder sb = new StringBuilder(); 131 String line; 132 while ((line = reader.readLine()) != null) { 133 sb.append(line); 134 } 135 content = sb.toString(); 136 } 137 } else { 138 content = String.format("%n<br>%nERROR: Plugin resource \"%s\" couldn't be found%n<br>%n", resourceName); 139 log.warn("Plugin resource \"{}\" couldn't be found", resourceName); 140 } 141 } 142 } else { 143 content = new String(Files.readAllBytes(Paths.get(fileName))); 144 } 145 } catch (IOException ex) { 146 content = "Exception thrown: " + ex.getMessage(); 147 log.warn("Cannot read file: {}", fileName, ex); 148 } 149 150 String serverSideIncludePattern = "<!--#include\\s*virtual=\"(.+?)\"\\s*-->"; 151 152 Pattern pattern = Pattern.compile(serverSideIncludePattern); 153 Matcher matcher = pattern.matcher(content); 154 155 content = matcher.replaceAll((MatchResult t) -> { 156 String theFileName = t.group(1); 157 try { 158 theFileName = convertDotDotFolders(theFileName, path); 159 160 if (path.startsWith("/")) { 161 if (theFileName.startsWith("/")) { 162 return quoteBackslash(readAndParseFile(theFileName)); 163 } else { 164 return quoteBackslash(readAndParseFile(path + theFileName)); 165 } 166 } else { 167 return quoteBackslash(readAndParseFile("web/" + path + theFileName)); 168 } 169 } catch (IOException ex) { 170 log.warn("Cannot include SSI: {}", theFileName, ex); 171 return ""; 172 } 173 }); 174 return content; 175 } 176 177 protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 178 179 String uri = request.getRequestURI(); 180 if (!uri.endsWith(".shtml")) { 181 if (!(request instanceof Request)) throw new IllegalArgumentException("request is not a Request"); 182 log.debug("Handling regular file: '{}'", uri); 183 String fileName = uri; 184 if (!fileName.startsWith("/plugin/")) { 185 fileName = FileUtil.getProgramPath() + uri; 186 } 187 handleRegularFile(fileName, response); 188 return; 189 } 190 191 log.debug("Handling .shtml file: '{}'", uri); 192 String content = readAndParseFile(uri); 193 194 response.setHeader("Connection", "Keep-Alive"); // NOI18N 195 response.setContentType(UTF8_TEXT_HTML); 196 response.getWriter().write(content); 197 } 198 199// <editor-fold defaultstate="collapsed" desc="HttpServlet methods. Click on the + sign on the left to edit the code."> 200 /** 201 * Handles the HTTP <code>GET</code> method. 202 * 203 * @param request servlet request 204 * @param response servlet response 205 * @throws ServletException if a servlet-specific error occurs 206 * @throws IOException if an I/O error occurs 207 */ 208 @Override 209 protected void doGet(HttpServletRequest request, HttpServletResponse response) 210 throws ServletException, IOException { 211 processRequest(request, response); 212 } 213 214 /** 215 * Handles the HTTP <code>POST</code> method. 216 * 217 * @param request servlet request 218 * @param response servlet response 219 * @throws ServletException if a servlet-specific error occurs 220 * @throws IOException if an I/O error occurs 221 */ 222 @Override 223 protected void doPost(HttpServletRequest request, HttpServletResponse response) 224 throws ServletException, IOException { 225 processRequest(request, response); 226 } 227 228 /** 229 * Returns a short description of the servlet. 230 * 231 * @return a String containing servlet description 232 */ 233 @Override 234 public String getServletInfo() { 235 return "Help SSI Servlet"; 236 }// </editor-fold> 237 238 239 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(HelpSSIServlet.class); 240}