1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.mycore.frontend.servlets;
20
21 import static org.mycore.frontend.MCRFrontendUtil.BASE_URL_ATTRIBUTE;
22
23 import java.io.IOException;
24 import java.net.MalformedURLException;
25 import java.net.URL;
26 import java.net.URLEncoder;
27 import java.net.UnknownHostException;
28 import java.nio.charset.StandardCharsets;
29 import java.text.MessageFormat;
30 import java.time.Instant;
31 import java.time.LocalDateTime;
32 import java.time.ZoneId;
33 import java.util.Enumeration;
34 import java.util.List;
35 import java.util.Locale;
36 import java.util.Optional;
37 import java.util.Properties;
38
39 import javax.xml.transform.TransformerException;
40
41 import org.apache.logging.log4j.LogManager;
42 import org.apache.logging.log4j.Logger;
43 import org.mycore.common.MCRException;
44 import org.mycore.common.MCRSession;
45 import org.mycore.common.MCRSessionMgr;
46 import org.mycore.common.MCRSessionResolver;
47 import org.mycore.common.MCRTransactionHelper;
48 import org.mycore.common.config.MCRConfiguration2;
49 import org.mycore.common.config.MCRConfigurationBase;
50 import org.mycore.common.config.MCRConfigurationDirSetup;
51 import org.mycore.common.xml.MCRLayoutService;
52 import org.mycore.common.xsl.MCRErrorListener;
53 import org.mycore.frontend.MCRFrontendUtil;
54 import org.mycore.services.i18n.MCRTranslation;
55 import org.xml.sax.SAXException;
56 import org.xml.sax.SAXParseException;
57
58 import jakarta.servlet.ServletContext;
59 import jakarta.servlet.ServletException;
60 import jakarta.servlet.http.HttpServlet;
61 import jakarta.servlet.http.HttpServletRequest;
62 import jakarta.servlet.http.HttpServletResponse;
63 import jakarta.servlet.http.HttpSession;
64
65
66
67
68
69
70
71
72
73
74 public class MCRServlet extends HttpServlet {
75 public static final String ATTR_MYCORE_SESSION = "mycore.session";
76
77 public static final String CURRENT_THREAD_NAME_KEY = "currentThreadName";
78
79 public static final String INITIAL_SERVLET_NAME_KEY = "currentServletName";
80
81 private static final long serialVersionUID = 1L;
82
83 private static Logger LOGGER = LogManager.getLogger();
84
85 private static String SERVLET_URL;
86
87 private static final boolean ENABLE_BROWSER_CACHE = MCRConfiguration2.getBoolean("MCR.Servlet.BrowserCache.enable")
88 .orElse(false);
89
90 private static MCRLayoutService LAYOUT_SERVICE;
91
92 private static String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin";
93
94 public static MCRLayoutService getLayoutService() {
95 return LAYOUT_SERVICE;
96 }
97
98 @Override
99 public void init() throws ServletException {
100 super.init();
101 if (LAYOUT_SERVICE == null) {
102 LAYOUT_SERVICE = MCRLayoutService.instance();
103 }
104 }
105
106
107
108
109 public static String getServletBaseURL() {
110 MCRSession session = MCRSessionMgr.getCurrentSession();
111 Object value = session.get(BASE_URL_ATTRIBUTE);
112 if (value != null) {
113 LOGGER.debug("Returning BaseURL {}servlets/ from user session.", value);
114 return value + "servlets/";
115 }
116 return SERVLET_URL != null ? SERVLET_URL : MCRFrontendUtil.getBaseURL() + "servlets/";
117 }
118
119
120
121
122 private static synchronized void prepareBaseURLs(ServletContext context, HttpServletRequest req) {
123 String contextPath = req.getContextPath() + "/";
124
125 String requestURL = req.getRequestURL().toString();
126 int pos = requestURL.indexOf(contextPath, 9);
127 String baseURLofRequest = requestURL.substring(0, pos) + contextPath;
128
129 prepareBaseURLs(baseURLofRequest);
130 }
131
132 private static void prepareBaseURLs(String baseURLofRequest) {
133 MCRFrontendUtil.prepareBaseURLs(baseURLofRequest);
134 SERVLET_URL = MCRFrontendUtil.getBaseURL() + "servlets/";
135 }
136
137
138
139
140 @Override
141 public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
142 try {
143 doGetPost(req, res);
144 } catch (SAXException | TransformerException e) {
145 throwIOException(e);
146 }
147 }
148
149 private void throwIOException(Exception e) throws IOException {
150 if (e instanceof IOException) {
151 throw (IOException) e;
152 }
153 if (e instanceof TransformerException) {
154 TransformerException te = MCRErrorListener.unwrapException((TransformerException) e);
155 String myMessageAndLocation = MCRErrorListener.getMyMessageAndLocation(te);
156 throw new IOException("Error while XSL Transformation: " + myMessageAndLocation, e);
157 }
158 if (e instanceof SAXParseException) {
159 SAXParseException spe = (SAXParseException) e;
160 String id = spe.getSystemId() != null ? spe.getSystemId() : spe.getPublicId();
161 int line = spe.getLineNumber();
162 int column = spe.getColumnNumber();
163 String msg = new MessageFormat("Error on {0}:{1} while parsing {2}", Locale.ROOT)
164 .format(new Object[] { line, column, id });
165 throw new IOException(msg, e);
166 }
167 throw new IOException(e);
168 }
169
170 protected void doGet(MCRServletJob job) throws Exception {
171 doGetPost(job);
172 }
173
174 @Override
175 public void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
176 try {
177 doGetPost(req, res);
178 } catch (SAXException | TransformerException e) {
179 throwIOException(e);
180 }
181 }
182
183 protected void doPost(MCRServletJob job) throws Exception {
184 doGetPost(job);
185 }
186
187 public static MCRSession getSession(HttpServletRequest req) {
188 boolean reusedSession = req.isRequestedSessionIdValid();
189 HttpSession theSession = req.getSession(true);
190 if (reusedSession) {
191 LOGGER.debug(() -> "Reused HTTP session: " + theSession.getId() + ", created: " + LocalDateTime
192 .ofInstant(Instant.ofEpochMilli(theSession.getCreationTime()), ZoneId.systemDefault()));
193 } else {
194 LOGGER.info(() -> "Created new HTTP session: " + theSession.getId());
195 }
196 MCRSession session = null;
197
198 MCRSession fromHttpSession = Optional
199 .ofNullable((MCRSessionResolver) theSession.getAttribute(ATTR_MYCORE_SESSION))
200 .flatMap(MCRSessionResolver::resolveSession)
201 .orElse(null);
202
203 MCRSessionMgr.unlock();
204 if (fromHttpSession != null && fromHttpSession.getID() != null) {
205
206 session = fromHttpSession;
207
208 String lastIP = session.getCurrentIP();
209 String newIP = MCRFrontendUtil.getRemoteAddr(req);
210
211 try {
212 if (!MCRFrontendUtil.isIPAddrAllowed(lastIP, newIP)) {
213 LOGGER.warn("Session steal attempt from IP {}, previous IP was {}. Session: {}", newIP, lastIP,
214 session);
215 MCRSessionMgr.releaseCurrentSession();
216 session.close();
217 MCRSessionMgr.unlock();
218 session = MCRSessionMgr.getCurrentSession();
219 session.setCurrentIP(newIP);
220 }
221 } catch (UnknownHostException e) {
222 throw new MCRException("Wrong transformation of IP address for this session.", e);
223 }
224 } else {
225
226 session = MCRSessionMgr.getCurrentSession();
227 }
228
229
230 theSession.setAttribute(ATTR_MYCORE_SESSION, new MCRSessionResolver(session));
231
232 if (session.put("http.session", theSession.getId()) == null) {
233
234
235 MCRTransactionHelper.beginTransaction();
236 try {
237 String acceptLanguage = req.getHeader("Accept-Language");
238 if (acceptLanguage != null) {
239 List<Locale.LanguageRange> languageRanges = Locale.LanguageRange.parse(acceptLanguage);
240 LOGGER.debug("accept languages: {}", languageRanges);
241 MCRSession finalSession = session;
242 Optional
243 .ofNullable(Locale.lookupTag(languageRanges, MCRTranslation.getAvailableLanguages()))
244 .ifPresent(selectedLanguage -> {
245 LOGGER.debug("selected language: {}", selectedLanguage);
246 finalSession.setCurrentLanguage(selectedLanguage);
247 });
248 }
249 } finally {
250 if (MCRTransactionHelper.transactionRequiresRollback()) {
251 MCRTransactionHelper.rollbackTransaction();
252 }
253 MCRTransactionHelper.commitTransaction();
254 }
255 }
256
257 req.setAttribute("XSL.MCRSessionID", session.getID());
258
259 return session;
260 }
261
262 private static void bindSessionToRequest(HttpServletRequest req, String servletName, MCRSession session) {
263 if (!isSessionBoundToRequest(req)) {
264
265 MCRSessionMgr.setCurrentSession(session);
266 req.setAttribute(CURRENT_THREAD_NAME_KEY, Thread.currentThread().getName());
267 req.setAttribute(INITIAL_SERVLET_NAME_KEY, servletName);
268 }
269 }
270
271 private static boolean isSessionBoundToRequest(HttpServletRequest req) {
272 String currentThread = getProperty(req, CURRENT_THREAD_NAME_KEY);
273
274
275 return currentThread != null && currentThread.equals(Thread.currentThread().getName());
276 }
277
278
279
280
281
282
283
284
285
286
287
288
289
290 private void doGetPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException,
291 SAXException, TransformerException {
292 initializeMCRSession(req, getServletName());
293
294 if (SERVLET_URL == null) {
295 prepareBaseURLs(getServletContext(), req);
296 }
297
298 MCRServletJob job = new MCRServletJob(req, res);
299 MCRSession session = MCRSessionMgr.getCurrentSession();
300
301 try {
302
303 Exception thinkException = processThinkPhase(job);
304
305 processRenderingPhase(job, thinkException);
306 } catch (Error error) {
307 if (getProperty(req, INITIAL_SERVLET_NAME_KEY).equals(getServletName())) {
308
309 MCRTransactionHelper.rollbackTransaction();
310 }
311 throw error;
312 } catch (Exception ex) {
313 if (getProperty(req, INITIAL_SERVLET_NAME_KEY).equals(getServletName())) {
314
315 MCRTransactionHelper.rollbackTransaction();
316 }
317 if (isBrokenPipe(ex)) {
318 LOGGER.info("Ignore broken pipe.");
319 return;
320 }
321 if (ex.getMessage() == null) {
322 LOGGER.error("Exception while in rendering phase.", ex);
323 } else {
324 LOGGER.error("Exception while in rendering phase: {}", ex.getMessage());
325 }
326 if (ex instanceof ServletException) {
327 throw (ServletException) ex;
328 } else if (ex instanceof IOException) {
329 throw (IOException) ex;
330 } else if (ex instanceof SAXException) {
331 throw (SAXException) ex;
332 } else if (ex instanceof TransformerException) {
333 throw (TransformerException) ex;
334 } else if (ex instanceof RuntimeException) {
335 throw (RuntimeException) ex;
336 } else {
337 throw new RuntimeException(ex);
338 }
339 } finally {
340 cleanupMCRSession(req, getServletName());
341 }
342 }
343
344
345
346
347
348
349
350
351
352 public static void initializeMCRSession(HttpServletRequest req, String servletName) throws IOException {
353
354 String reqCharEncoding = req.getCharacterEncoding();
355
356 if (reqCharEncoding == null) {
357
358 reqCharEncoding = MCRConfiguration2.getString("MCR.Request.CharEncoding").orElse("UTF-8");
359 req.setCharacterEncoding(reqCharEncoding);
360 LOGGER.debug("Setting ReqCharEncoding to: {}", reqCharEncoding);
361 }
362
363 if ("true".equals(req.getParameter("reload.properties"))) {
364 MCRConfigurationDirSetup setup = new MCRConfigurationDirSetup();
365 setup.startUp(req.getServletContext());
366 }
367 if (getProperty(req, INITIAL_SERVLET_NAME_KEY) == null) {
368 MCRSession session = getSession(req);
369 bindSessionToRequest(req, servletName, session);
370 }
371 }
372
373
374
375
376
377
378
379 public static void cleanupMCRSession(HttpServletRequest req, String servletName) {
380
381
382 if (getProperty(req, INITIAL_SERVLET_NAME_KEY).equals(servletName)) {
383
384 MCRSessionMgr.releaseCurrentSession();
385 }
386 }
387
388 private static boolean isBrokenPipe(Throwable throwable) {
389 String message = throwable.getMessage();
390 if (message != null && throwable instanceof IOException && message.contains("Broken pipe")) {
391 return true;
392 }
393 return throwable.getCause() != null && isBrokenPipe(throwable.getCause());
394 }
395
396 private void configureSession(MCRServletJob job) {
397 MCRSession session = MCRSessionMgr.getCurrentSession();
398
399 String longName = getClass().getName();
400 final String shortName = longName.substring(longName.lastIndexOf(".") + 1);
401
402 LOGGER.info(() -> String
403 .format(Locale.ROOT, "%s ip=%s mcr=%s path=%s", shortName, MCRFrontendUtil.getRemoteAddr(job.getRequest()),
404 session.getID(), job.getRequest().getPathInfo()));
405
406 MCRFrontendUtil.configureSession(session, job.getRequest(), job.getResponse());
407 }
408
409 private Exception processThinkPhase(MCRServletJob job) {
410 MCRSession session = MCRSessionMgr.getCurrentSession();
411 try {
412 if (getProperty(job.getRequest(), INITIAL_SERVLET_NAME_KEY).equals(getServletName())) {
413
414 MCRTransactionHelper.beginTransaction();
415 }
416 configureSession(job);
417 think(job);
418 if (getProperty(job.getRequest(), INITIAL_SERVLET_NAME_KEY).equals(getServletName())) {
419
420 MCRTransactionHelper.commitTransaction();
421 }
422 } catch (Exception ex) {
423 if (getProperty(job.getRequest(), INITIAL_SERVLET_NAME_KEY).equals(getServletName())) {
424
425 LOGGER.warn("Exception occurred, performing database rollback.");
426 MCRTransactionHelper.rollbackTransaction();
427 } else {
428 LOGGER.warn("Exception occurred, cannot rollback database transaction right now.");
429 }
430 return ex;
431 }
432 return null;
433 }
434
435
436
437
438
439
440
441 protected void think(MCRServletJob job) throws Exception {
442
443 }
444
445 private void processRenderingPhase(MCRServletJob job, Exception thinkException) throws Exception {
446 if (allowCrossDomainRequests() && !job.getResponse().containsHeader(ACCESS_CONTROL_ALLOW_ORIGIN)) {
447 job.getResponse().setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, "*");
448 }
449 MCRSession session = MCRSessionMgr.getCurrentSession();
450 if (getProperty(job.getRequest(), INITIAL_SERVLET_NAME_KEY).equals(getServletName())) {
451
452 MCRTransactionHelper.beginTransaction();
453 }
454 render(job, thinkException);
455 if (getProperty(job.getRequest(), INITIAL_SERVLET_NAME_KEY).equals(getServletName())) {
456
457 MCRTransactionHelper.commitTransaction();
458 }
459 }
460
461
462
463
464
465 protected boolean allowCrossDomainRequests() {
466 return false;
467 }
468
469
470
471
472
473
474
475
476
477
478
479
480
481 protected void render(MCRServletJob job, Exception ex) throws Exception {
482
483 if (ex != null) {
484 throw ex;
485 }
486 if (job.getRequest().getMethod().equals("POST")) {
487 doPost(job);
488 } else {
489 doGet(job);
490 }
491 }
492
493
494
495
496
497 protected void doGetPost(MCRServletJob job) throws Exception {
498 job.getResponse().sendError(HttpServletResponse.SC_NOT_IMPLEMENTED);
499 }
500
501
502 protected void handleException(Exception ex) {
503 try {
504 reportException(ex);
505 } catch (Exception ignored) {
506 LOGGER.error(ignored);
507 }
508 }
509
510
511 protected void reportException(Exception ex) throws Exception {
512 String cname = this.getClass().getName();
513 String servlet = cname.substring(cname.lastIndexOf(".") + 1);
514
515 LOGGER.warn("Exception caught in : {}", servlet, ex);
516 }
517
518
519
520
521
522
523
524
525
526
527 protected String buildRedirectURL(String baseURL, Properties parameters) {
528 StringBuilder redirectURL = new StringBuilder(baseURL);
529 boolean first = true;
530 for (Enumeration<?> e = parameters.keys(); e.hasMoreElements();) {
531 if (first) {
532 redirectURL.append("?");
533 first = false;
534 } else {
535 redirectURL.append("&");
536 }
537
538 String name = (String) e.nextElement();
539 String value = null;
540 value = URLEncoder.encode(parameters.getProperty(name), StandardCharsets.UTF_8);
541
542 redirectURL.append(name).append("=").append(value);
543 }
544 LOGGER.debug("Sending redirect to {}", redirectURL);
545 return redirectURL.toString();
546 }
547
548
549
550
551
552 @Override
553 protected long getLastModified(HttpServletRequest request) {
554 if (ENABLE_BROWSER_CACHE) {
555
556 long lastModified = MCRSessionMgr.getCurrentSession().getLoginTime() > MCRConfigurationBase
557 .getSystemLastModified() ? MCRSessionMgr.getCurrentSession().getLoginTime()
558 : MCRConfigurationBase.getSystemLastModified();
559 LOGGER.info("LastModified: {}", lastModified);
560 return lastModified;
561 }
562 return -1;
563 }
564
565 public static String getProperty(HttpServletRequest request, String name) {
566 return MCRFrontendUtil.getProperty(request, name).orElse(null);
567 }
568
569
570
571
572
573
574
575
576
577
578
579
580 protected String getErrorI18N(String prefix, String subIdentifier, Object... args) {
581 String key = new MessageFormat("{0}.{1}.{2}", Locale.ROOT)
582 .format(new Object[] { prefix, getClass().getSimpleName(), subIdentifier });
583 return MCRTranslation.translate(key, args);
584 }
585
586
587
588
589 protected URL getReferer(HttpServletRequest request) {
590 String referer;
591 referer = request.getHeader("Referer");
592 if (referer == null) {
593 return null;
594 }
595 try {
596 return new URL(referer);
597 } catch (MalformedURLException e) {
598
599 LOGGER.error("Referer is not a valid URL: {}", referer, e);
600 return null;
601 }
602 }
603
604
605
606
607
608 protected void toReferrer(HttpServletRequest request, HttpServletResponse response) throws IOException {
609 URL referrer = getReferer(request);
610 if (referrer != null) {
611 response.sendRedirect(response.encodeRedirectURL(referrer.toString()));
612 } else {
613 LOGGER.warn("Could not get referrer, returning to the application's base url");
614 response.sendRedirect(response.encodeRedirectURL(MCRFrontendUtil.getBaseURL()));
615 }
616 }
617
618
619
620
621
622 protected void toReferrer(HttpServletRequest request, HttpServletResponse response, String altURL)
623 throws IOException {
624 URL referrer = getReferer(request);
625 if (referrer != null) {
626 response.sendRedirect(response.encodeRedirectURL(referrer.toString()));
627 } else {
628 LOGGER.warn("Could not get referrer, returning to {}", altURL);
629 response.sendRedirect(response.encodeRedirectURL(altURL));
630 }
631 }
632 }