View Javadoc
1   /*
2    * Copyright 2012-2021 CodeLibs Project and the Others.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13   * either express or implied. See the License for the specific language
14   * governing permissions and limitations under the License.
15   */
16  package org.codelibs.fess.api.es;
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 javax.annotation.PostConstruct;
27  import javax.servlet.FilterChain;
28  import javax.servlet.ServletException;
29  import javax.servlet.ServletInputStream;
30  import javax.servlet.ServletOutputStream;
31  import javax.servlet.http.HttpServletRequest;
32  import javax.servlet.http.HttpServletResponse;
33  
34  import org.apache.catalina.connector.ClientAbortException;
35  import org.apache.logging.log4j.LogManager;
36  import org.apache.logging.log4j.Logger;
37  import org.codelibs.core.io.CopyUtil;
38  import org.codelibs.core.lang.StringUtil;
39  import org.codelibs.curl.Curl.Method;
40  import org.codelibs.curl.CurlRequest;
41  import org.codelibs.curl.CurlResponse;
42  import org.codelibs.fess.Constants;
43  import org.codelibs.fess.api.BaseApiManager;
44  import org.codelibs.fess.exception.FessSystemException;
45  import org.codelibs.fess.exception.WebApiException;
46  import org.codelibs.fess.mylasta.action.FessUserBean;
47  import org.codelibs.fess.util.ComponentUtil;
48  import org.codelibs.fess.util.ResourceUtil;
49  import org.lastaflute.web.servlet.request.RequestManager;
50  import org.lastaflute.web.servlet.session.SessionManager;
51  
52  public class EsApiManager extends BaseApiManager {
53      private static final String ADMIN_SERVER = "/admin/server_";
54  
55      private static final Logger logger = LogManager.getLogger(EsApiManager.class);
56  
57      protected String[] acceptedRoles = new String[] { "admin" };
58  
59      public EsApiManager() {
60          setPathPrefix(ADMIN_SERVER);
61      }
62  
63      @PostConstruct
64      public void register() {
65          if (logger.isInfoEnabled()) {
66              logger.info("Load {}", this.getClass().getSimpleName());
67          }
68          ComponentUtil.getWebApiManagerFactory().add(this);
69      }
70  
71      @Override
72      public boolean matches(final HttpServletRequest request) {
73          final String servletPath = request.getServletPath();
74          return servletPath.startsWith(pathPrefix);
75      }
76  
77      @Override
78      public void process(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain)
79              throws IOException, ServletException {
80          final RequestManager requestManager = ComponentUtil.getRequestManager();
81          if (!requestManager.findUserBean(FessUserBean.class).map(user -> user.hasRoles(acceptedRoles)).orElse(Boolean.FALSE)) {
82              response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized access: " + request.getServletPath());
83              return;
84          }
85  
86          try {
87              getSessionManager().getAttribute(Constants.ES_API_ACCESS_TOKEN, String.class).ifPresent(token -> {
88                  final String servletPath = request.getServletPath();
89                  final String pathPrefix = ADMIN_SERVER + token;
90                  if (!servletPath.startsWith(pathPrefix)) {
91                      throw new WebApiException(HttpServletResponse.SC_FORBIDDEN, "Invalid access token.");
92                  }
93                  final String path;
94                  final String value = servletPath.substring(pathPrefix.length());
95                  if (!value.startsWith("/")) {
96                      path = "/" + value;
97                  } else {
98                      path = value;
99                  }
100                 processRequest(request, response, path);
101             }).orElse(() -> {
102                 throw new WebApiException(HttpServletResponse.SC_FORBIDDEN, "Invalid session.");
103             });
104         } catch (final WebApiException e) {
105             logger.warn("Web API access error.", e);
106             response.sendError(e.getStatusCode(), "Web API access error.");
107         }
108     }
109 
110     protected void processRequest(final HttpServletRequest request, final HttpServletResponse response, final String path) {
111         if (StringUtil.isNotBlank(path)) {
112             final String lowerPath = path.toLowerCase(Locale.ROOT);
113             if (lowerPath.endsWith(".html")) {
114                 response.setContentType("text/html;charset=utf-8");
115             } else if (lowerPath.endsWith(".txt")) {
116                 response.setContentType("text/plain");
117             } else if (lowerPath.endsWith(".css")) {
118                 response.setContentType("text/css");
119             }
120         }
121 
122         if ("/_plugin".equals(path) || path.startsWith("/_plugin/")) {
123             processPluginRequest(request, response, path.replaceFirst("^/_plugin", StringUtil.EMPTY));
124             return;
125         }
126 
127         final Method httpMethod = Method.valueOf(request.getMethod().toUpperCase(Locale.ROOT));
128         final CurlRequest curlRequest = ComponentUtil.getCurlHelper().request(httpMethod, path);
129 
130         final String contentType = request.getHeader("Content-Type");
131         if (StringUtil.isNotEmpty(contentType)) {
132             curlRequest.header("Content-Type", contentType);
133         }
134 
135         request.getParameterMap().entrySet().stream().forEach(entry -> {
136             if (entry.getValue().length > 1) {
137                 curlRequest.param(entry.getKey(), String.join(",", entry.getValue()));
138             } else if (entry.getValue().length == 1) {
139                 curlRequest.param(entry.getKey(), entry.getValue()[0]);
140             }
141         });
142         try (final CurlResponse curlResponse = curlRequest.onConnect((req, con) -> {
143             con.setDoOutput(true);
144             if (httpMethod != Method.GET && request.getContentLength() > 2) {
145                 try (ServletInputStream in = request.getInputStream(); OutputStream out = con.getOutputStream()) {
146                     CopyUtil.copy(in, out);
147                 } catch (final IOException e) {
148                     throw new WebApiException(HttpServletResponse.SC_BAD_REQUEST, e);
149                 }
150             }
151         }).execute()) {
152 
153             try (ServletOutputStream out = response.getOutputStream(); InputStream in = curlResponse.getContentAsStream()) {
154                 response.setStatus(curlResponse.getHttpStatusCode());
155                 writeHeaders(response);
156                 CopyUtil.copy(in, out);
157             } catch (final ClientAbortException e) {
158                 logger.debug("Client aborts this request.", e);
159             }
160         } catch (final Exception e) {
161             if (!(e.getCause() instanceof ClientAbortException)) {
162                 throw new WebApiException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e);
163             }
164             logger.debug("Client aborts this request.", e);
165         }
166     }
167 
168     protected void processPluginRequest(final HttpServletRequest request, final HttpServletResponse response, final String path) {
169         Path filePath = ResourceUtil.getSitePath(path.replaceAll("\\.\\.+", StringUtil.EMPTY).replaceAll("/+", "/").split("/"));
170         if (Files.isDirectory(filePath)) {
171             filePath = filePath.resolve("index.html");
172         }
173         if (Files.exists(filePath)) {
174             try (InputStream in = Files.newInputStream(filePath); ServletOutputStream out = response.getOutputStream()) {
175                 response.setStatus(HttpServletResponse.SC_OK);
176                 writeHeaders(response);
177                 CopyUtil.copy(in, out);
178             } catch (final ClientAbortException e) {
179                 logger.debug("Client aborts this request.", e);
180             } catch (final IOException e) {
181                 logger.error("Failed to read {} from {}", path, filePath);
182                 throw new WebApiException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e);
183             }
184         } else {
185             try {
186                 writeHeaders(response);
187                 response.sendError(HttpServletResponse.SC_NOT_FOUND, path + " is not found.");
188             } catch (final ClientAbortException e) {
189                 logger.debug("Client aborts this request.", e);
190             } catch (final IOException e) {
191                 logger.error("Failed to read {} from {}", path, filePath);
192                 throw new WebApiException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e);
193             }
194         }
195     }
196 
197     public void setAcceptedRoles(final String[] acceptedRoles) {
198         this.acceptedRoles = acceptedRoles;
199     }
200 
201     public String getServerPath() {
202         return getSessionManager().getAttribute(Constants.ES_API_ACCESS_TOKEN, String.class).map(token -> ADMIN_SERVER + token)
203                 .orElseThrow(() -> new FessSystemException("Cannot create an access token."));
204     }
205 
206     public void saveToken() {
207         getSessionManager().setAttribute(Constants.ES_API_ACCESS_TOKEN, UUID.randomUUID().toString().replace("-", ""));
208     }
209 
210     private SessionManager getSessionManager() {
211         return ComponentUtil.getComponent(SessionManager.class);
212     }
213 
214     @Override
215     protected void writeHeaders(final HttpServletResponse response) {
216         ComponentUtil.getFessConfig().getApiDashboardResponseHeaderList().forEach(e -> response.setHeader(e.getFirst(), e.getSecond()));
217     }
218 }