1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package org.codelibs.fess.api.engine;
17
18 import java.io.IOException;
19 import java.io.InputStream;
20 import java.io.OutputStream;
21 import java.nio.file.Files;
22 import java.nio.file.Path;
23 import java.util.Locale;
24 import java.util.UUID;
25
26 import org.apache.catalina.connector.ClientAbortException;
27 import org.apache.logging.log4j.LogManager;
28 import org.apache.logging.log4j.Logger;
29 import org.codelibs.core.io.CopyUtil;
30 import org.codelibs.core.lang.StringUtil;
31 import org.codelibs.curl.Curl.Method;
32 import org.codelibs.curl.CurlRequest;
33 import org.codelibs.curl.CurlResponse;
34 import org.codelibs.fess.Constants;
35 import org.codelibs.fess.api.BaseApiManager;
36 import org.codelibs.fess.exception.FessSystemException;
37 import org.codelibs.fess.exception.WebApiException;
38 import org.codelibs.fess.mylasta.action.FessUserBean;
39 import org.codelibs.fess.util.ComponentUtil;
40 import org.codelibs.fess.util.ResourceUtil;
41 import org.lastaflute.web.servlet.request.RequestManager;
42 import org.lastaflute.web.servlet.session.SessionManager;
43
44 import jakarta.annotation.PostConstruct;
45 import jakarta.servlet.FilterChain;
46 import jakarta.servlet.ServletException;
47 import jakarta.servlet.ServletInputStream;
48 import jakarta.servlet.ServletOutputStream;
49 import jakarta.servlet.http.HttpServletRequest;
50 import jakarta.servlet.http.HttpServletResponse;
51
52
53
54
55
56 public class SearchEngineApiManager extends BaseApiManager {
57 private static final String ADMIN_SERVER = "/admin/server_";
58
59 private static final Logger logger = LogManager.getLogger(SearchEngineApiManager.class);
60
61
62 protected String[] acceptedRoles = { "admin" };
63
64
65
66
67
68 public SearchEngineApiManager() {
69 super();
70 setPathPrefix(ADMIN_SERVER);
71 }
72
73
74
75
76
77 @PostConstruct
78 public void register() {
79 if (logger.isInfoEnabled()) {
80 logger.info("Loaded {}", this.getClass().getSimpleName());
81 }
82 ComponentUtil.getWebApiManagerFactory().add(this);
83 }
84
85 @Override
86 public boolean matches(final HttpServletRequest request) {
87 final String servletPath = request.getServletPath();
88 return servletPath.startsWith(pathPrefix);
89 }
90
91 @Override
92 public void process(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain)
93 throws IOException, ServletException {
94 final RequestManager requestManager = ComponentUtil.getRequestManager();
95 if (!requestManager.findUserBean(FessUserBean.class).map(user -> user.hasRoles(acceptedRoles)).orElse(Boolean.FALSE)) {
96 response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized access: " + request.getServletPath());
97 return;
98 }
99
100 try {
101 getSessionManager().getAttribute(Constants.SEARCH_ENGINE_API_ACCESS_TOKEN, String.class).ifPresent(token -> {
102 final String servletPath = request.getServletPath();
103 final String pathPrefix = ADMIN_SERVER + token;
104 if (!servletPath.startsWith(pathPrefix)) {
105 throw new WebApiException(HttpServletResponse.SC_FORBIDDEN, "Invalid access token.");
106 }
107 final String path;
108 final String value = servletPath.substring(pathPrefix.length());
109 if (!value.startsWith("/")) {
110 path = "/" + value;
111 } else {
112 path = value;
113 }
114 processRequest(request, response, path);
115 }).orElse(() -> {
116 throw new WebApiException(HttpServletResponse.SC_FORBIDDEN, "Invalid session.");
117 });
118 } catch (final WebApiException e) {
119 final int statusCode = e.getStatusCode();
120 String message;
121 if (Constants.TRUE.equalsIgnoreCase(ComponentUtil.getFessConfig().getApiJsonResponseExceptionIncluded())) {
122 if (statusCode >= 400 && statusCode < 500) {
123 if (logger.isDebugEnabled()) {
124 logger.debug("Failed to access Web API.", e);
125 }
126 } else {
127 logger.warn("Failed to access Web API.", e);
128 }
129 message = e.getMessage();
130 } else {
131 final String errorCode = UUID.randomUUID().toString();
132 message = "[" + errorCode + "] Failed to access to Web API.";
133 if (statusCode >= 400 && statusCode < 500) {
134 if (logger.isDebugEnabled()) {
135 logger.debug(message, e);
136 }
137 } else {
138 logger.warn(message, e);
139 }
140 }
141 response.sendError(statusCode, message);
142 }
143 }
144
145
146
147
148
149
150
151
152
153 protected void processRequest(final HttpServletRequest request, final HttpServletResponse response, final String path) {
154 if ("/_plugin".equals(path) || path.startsWith("/_plugin/")) {
155 processPluginRequest(request, response, path.replaceFirst("^/_plugin", StringUtil.EMPTY));
156 return;
157 }
158
159 final Method httpMethod = Method.valueOf(request.getMethod().toUpperCase(Locale.ROOT));
160 final CurlRequest curlRequest = ComponentUtil.getCurlHelper().request(httpMethod, path);
161
162 final String contentType = request.getHeader("Content-Type");
163 if (StringUtil.isNotEmpty(contentType)) {
164 curlRequest.header("Content-Type", contentType);
165 }
166
167 request.getParameterMap().entrySet().stream().forEach(entry -> {
168 if (entry.getValue().length > 1) {
169 curlRequest.param(entry.getKey(), String.join(",", entry.getValue()));
170 } else if (entry.getValue().length == 1) {
171 curlRequest.param(entry.getKey(), entry.getValue()[0]);
172 }
173 });
174 try (final CurlResponse curlResponse = curlRequest.onConnect((req, con) -> {
175 con.setDoOutput(true);
176 if (httpMethod != Method.GET && request.getContentLength() > 2) {
177 try (ServletInputStream in = request.getInputStream(); OutputStream out = con.getOutputStream()) {
178 CopyUtil.copy(in, out);
179 } catch (final IOException e) {
180 throw new WebApiException(HttpServletResponse.SC_BAD_REQUEST, e);
181 }
182 }
183 }).execute()) {
184
185 try (ServletOutputStream out = response.getOutputStream(); InputStream in = curlResponse.getContentAsStream()) {
186 response.setStatus(curlResponse.getHttpStatusCode());
187 writeHeaders(response);
188 final String responseContentType = curlResponse.getHeaderValue("Content-Type");
189 if (StringUtil.isBlank(responseContentType)) {
190 response.setHeader("Content-Type", "application/json");
191 } else {
192 response.setHeader("Content-Type", responseContentType);
193 }
194 CopyUtil.copy(in, out);
195 } catch (final ClientAbortException e) {
196 logger.debug("Client aborts this request.", e);
197 }
198 } catch (final Exception e) {
199 if (!(e.getCause() instanceof ClientAbortException)) {
200 throw new WebApiException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e);
201 }
202 logger.debug("Client aborts this request.", e);
203 }
204 }
205
206
207
208
209
210
211
212
213
214 protected void processPluginRequest(final HttpServletRequest request, final HttpServletResponse response, final String path) {
215 if (StringUtil.isNotBlank(path)) {
216 final String lowerPath = path.toLowerCase(Locale.ROOT);
217 if (lowerPath.endsWith(".html")) {
218 response.setContentType("text/html;charset=utf-8");
219 } else if (lowerPath.endsWith(".css")) {
220 response.setContentType("text/css");
221 } else if (lowerPath.endsWith(".eot")) {
222 response.setContentType("application/vnd.ms-fontobject");
223 } else if (lowerPath.endsWith(".ico")) {
224 response.setContentType("image/vnd.microsoft.icon");
225 } else if (lowerPath.endsWith(".js")) {
226 response.setContentType("text/javascript");
227 } else if (lowerPath.endsWith(".json")) {
228 response.setContentType("application/json");
229 } else if (lowerPath.endsWith(".otf")) {
230 response.setContentType("font/otf");
231 } else if (lowerPath.endsWith(".svg")) {
232 response.setContentType("image/svg+xml");
233 } else if (lowerPath.endsWith(".ttf")) {
234 response.setContentType("font/ttf");
235 } else if (lowerPath.endsWith(".txt")) {
236 response.setContentType("text/plain");
237 } else if (lowerPath.endsWith(".woff")) {
238 response.setContentType("font/woff");
239 } else if (lowerPath.endsWith(".woff2")) {
240 response.setContentType("font/woff2");
241 } else if (lowerPath.endsWith("/")) {
242 response.setContentType("text/html;charset=utf-8");
243 }
244 }
245
246 Path filePath = ResourceUtil.getSitePath(path.replaceAll("\\.\\.+", StringUtil.EMPTY).replaceAll("/+", "/").split("/"));
247 if (Files.isDirectory(filePath)) {
248 filePath = filePath.resolve("index.html");
249 }
250 if (Files.exists(filePath)) {
251 try (InputStream in = Files.newInputStream(filePath); ServletOutputStream out = response.getOutputStream()) {
252 response.setStatus(HttpServletResponse.SC_OK);
253 writeHeaders(response);
254 CopyUtil.copy(in, out);
255 } catch (final ClientAbortException e) {
256 logger.debug("Client aborts this request.", e);
257 } catch (final IOException e) {
258 logger.error("Failed to read file: path={}, filePath={}", path, filePath);
259 throw new WebApiException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e);
260 }
261 } else {
262 try {
263 writeHeaders(response);
264 response.sendError(HttpServletResponse.SC_NOT_FOUND, path + " is not found.");
265 } catch (final ClientAbortException e) {
266 logger.debug("Client aborts this request.", e);
267 } catch (final IOException e) {
268 logger.error("Failed to read file: path={}, filePath={}", path, filePath);
269 throw new WebApiException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e);
270 }
271 }
272 }
273
274
275
276
277
278
279 public void setAcceptedRoles(final String[] acceptedRoles) {
280 this.acceptedRoles = acceptedRoles;
281 }
282
283
284
285
286
287
288
289 public String getServerPath() {
290 return getSessionManager().getAttribute(Constants.SEARCH_ENGINE_API_ACCESS_TOKEN, String.class)
291 .map(token -> ADMIN_SERVER + token)
292 .orElseThrow(() -> new FessSystemException("Cannot create an access token."));
293 }
294
295
296
297
298
299 public void saveToken() {
300 getSessionManager().setAttribute(Constants.SEARCH_ENGINE_API_ACCESS_TOKEN, UUID.randomUUID().toString().replace("-", ""));
301 }
302
303 private SessionManager getSessionManager() {
304 return ComponentUtil.getComponent(SessionManager.class);
305 }
306
307 @Override
308 protected void writeHeaders(final HttpServletResponse response) {
309 ComponentUtil.getFessConfig().getApiDashboardResponseHeaderList().forEach(e -> response.setHeader(e.getFirst(), e.getSecond()));
310 }
311 }