]> WPIA git - gigi.git/blob - src/club/wpia/gigi/Gigi.java
chg: ensure actor, target and support ticket are non-null
[gigi.git] / src / club / wpia / gigi / Gigi.java
1 package club.wpia.gigi;
2
3 import java.io.IOException;
4 import java.io.PrintWriter;
5 import java.io.UnsupportedEncodingException;
6 import java.math.BigInteger;
7 import java.security.KeyStore;
8 import java.security.cert.X509Certificate;
9 import java.util.Calendar;
10 import java.util.Collections;
11 import java.util.HashMap;
12 import java.util.LinkedList;
13 import java.util.Locale;
14 import java.util.Map;
15 import java.util.Properties;
16 import java.util.regex.Pattern;
17
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 javax.servlet.http.HttpSession;
23
24 import club.wpia.gigi.database.DatabaseConnection;
25 import club.wpia.gigi.database.DatabaseConnection.Link;
26 import club.wpia.gigi.dbObjects.CACertificate;
27 import club.wpia.gigi.dbObjects.CATS.CATSType;
28 import club.wpia.gigi.dbObjects.CertificateProfile;
29 import club.wpia.gigi.dbObjects.DomainPingConfiguration;
30 import club.wpia.gigi.localisation.Language;
31 import club.wpia.gigi.output.Menu;
32 import club.wpia.gigi.output.MenuCollector;
33 import club.wpia.gigi.output.PageMenuItem;
34 import club.wpia.gigi.output.SimpleMenuItem;
35 import club.wpia.gigi.output.SimpleUntranslatedMenuItem;
36 import club.wpia.gigi.output.template.Form.CSRFException;
37 import club.wpia.gigi.output.template.Outputable;
38 import club.wpia.gigi.output.template.PlainOutputable;
39 import club.wpia.gigi.output.template.Template;
40 import club.wpia.gigi.output.template.TranslateCommand;
41 import club.wpia.gigi.pages.AboutPage;
42 import club.wpia.gigi.pages.HandlesMixedRequest;
43 import club.wpia.gigi.pages.LoginPage;
44 import club.wpia.gigi.pages.LogoutPage;
45 import club.wpia.gigi.pages.MainPage;
46 import club.wpia.gigi.pages.OneFormPage;
47 import club.wpia.gigi.pages.Page;
48 import club.wpia.gigi.pages.PasswordResetPage;
49 import club.wpia.gigi.pages.RootCertPage;
50 import club.wpia.gigi.pages.StaticPage;
51 import club.wpia.gigi.pages.Verify;
52 import club.wpia.gigi.pages.account.ChangePasswordPage;
53 import club.wpia.gigi.pages.account.FindAgentAccess;
54 import club.wpia.gigi.pages.account.History;
55 import club.wpia.gigi.pages.account.MyDetails;
56 import club.wpia.gigi.pages.account.UserTrainings;
57 import club.wpia.gigi.pages.account.certs.CertificateAdd;
58 import club.wpia.gigi.pages.account.certs.Certificates;
59 import club.wpia.gigi.pages.account.domain.DomainOverview;
60 import club.wpia.gigi.pages.account.domain.EditDomain;
61 import club.wpia.gigi.pages.account.mail.MailOverview;
62 import club.wpia.gigi.pages.admin.TTPAdminPage;
63 import club.wpia.gigi.pages.admin.support.FindCertPage;
64 import club.wpia.gigi.pages.admin.support.FindUserByDomainPage;
65 import club.wpia.gigi.pages.admin.support.FindUserByEmailPage;
66 import club.wpia.gigi.pages.admin.support.SupportEnterTicketPage;
67 import club.wpia.gigi.pages.admin.support.SupportUserDetailsPage;
68 import club.wpia.gigi.pages.error.AccessDenied;
69 import club.wpia.gigi.pages.error.PageNotFound;
70 import club.wpia.gigi.pages.main.CertStatusRequestPage;
71 import club.wpia.gigi.pages.main.KeyCompromisePage;
72 import club.wpia.gigi.pages.main.RegisterPage;
73 import club.wpia.gigi.pages.orga.CreateOrgPage;
74 import club.wpia.gigi.pages.orga.ViewOrgPage;
75 import club.wpia.gigi.pages.statistics.StatisticsRoles;
76 import club.wpia.gigi.pages.wot.Points;
77 import club.wpia.gigi.pages.wot.RequestTTPPage;
78 import club.wpia.gigi.pages.wot.VerifyPage;
79 import club.wpia.gigi.ping.PingerDaemon;
80 import club.wpia.gigi.util.AuthorizationContext;
81 import club.wpia.gigi.util.DomainAssessment;
82 import club.wpia.gigi.util.PasswordHash;
83 import club.wpia.gigi.util.ServerConstants;
84 import club.wpia.gigi.util.ServerConstants.Host;
85 import club.wpia.gigi.util.TimeConditions;
86
87 public final class Gigi extends HttpServlet {
88
89     public static final String LINK_HOST = "linkHost";
90
91     private class MenuBuilder {
92
93         private LinkedList<Menu> categories = new LinkedList<Menu>();
94
95         private HashMap<String, Page> pages = new HashMap<String, Page>();
96
97         private MenuCollector rootMenu;
98
99         public MenuBuilder() {}
100
101         private void putPage(String path, Page p, Menu m) {
102             pages.put(path, p);
103             if (m == null) {
104                 return;
105             }
106             m.addItem(new PageMenuItem(p, path.replaceFirst("/?\\*$", "")));
107
108         }
109
110         private Menu createMenu(String name) {
111             Menu m = new Menu(new TranslateCommand(name));
112             categories.add(m);
113             return m;
114         }
115
116         private Menu createMenu(Outputable name) {
117             Menu m = new Menu(name);
118             categories.add(m);
119             return m;
120         }
121
122         public MenuCollector generateMenu() throws ServletException {
123             putPage("/denied", new AccessDenied(), null);
124             putPage("/error", new PageNotFound(), null);
125             putPage("/login", new LoginPage(), null);
126             Menu mainMenu = createMenu(new PlainOutputable(ServerConstants.getAppName()));
127             mainMenu.addItem(new SimpleMenuItem("https://" + ServerConstants.getHostNamePort(Host.WWW) + "/login", "Password Login") {
128
129                 @Override
130                 public boolean isPermitted(AuthorizationContext ac) {
131                     return ac == null;
132                 }
133             });
134             mainMenu.addItem(new SimpleMenuItem("https://" + ServerConstants.getHostNamePortSecure(Host.SECURE) + "/login", "Certificate Login") {
135
136                 @Override
137                 public boolean isPermitted(AuthorizationContext ac) {
138                     return ac == null;
139                 }
140             });
141             putPage("/", new MainPage(), null);
142             putPage("/roots", new RootCertPage(truststore), mainMenu);
143             putPage(StatisticsRoles.PATH, new StatisticsRoles(), mainMenu);
144             putPage("/about", new AboutPage(), mainMenu);
145             putPage(RegisterPage.PATH, new RegisterPage(), mainMenu);
146             putPage(CertStatusRequestPage.PATH, new CertStatusRequestPage(), mainMenu);
147             putPage(KeyCompromisePage.PATH, new KeyCompromisePage(), mainMenu);
148
149             putPage(Verify.PATH, new Verify(), null);
150             Menu certificates = createMenu("Certificates");
151             putPage(Certificates.PATH + "/*", new Certificates(false), certificates);
152             putPage(CertificateAdd.PATH, new CertificateAdd(), certificates);
153
154             Menu wot = createMenu("Verification");
155             putPage(MailOverview.DEFAULT_PATH, new MailOverview(), wot);
156             putPage(DomainOverview.PATH, new DomainOverview(), wot);
157             putPage(EditDomain.PATH + "*", new EditDomain(), null);
158             putPage(VerifyPage.PATH + "/*", new VerifyPage(), wot);
159             putPage(Points.PATH, new Points(false), wot);
160             putPage(RequestTTPPage.PATH, new RequestTTPPage(), wot);
161
162             Menu admMenu = createMenu("Admin");
163             Menu orgAdm = createMenu("Organisation Admin");
164             putPage(TTPAdminPage.PATH + "/*", new TTPAdminPage(), admMenu);
165             putPage(CreateOrgPage.DEFAULT_PATH, new CreateOrgPage(), orgAdm);
166             putPage(ViewOrgPage.DEFAULT_PATH + "/*", new ViewOrgPage(), orgAdm);
167
168             Menu support = createMenu("Support Console");
169             putPage(SupportEnterTicketPage.PATH, new SupportEnterTicketPage(), support);
170             putPage(FindUserByEmailPage.PATH, new FindUserByEmailPage(), support);
171             putPage(FindUserByDomainPage.PATH, new FindUserByDomainPage(), support);
172             putPage(FindCertPage.PATH, new FindCertPage(), support);
173
174             Menu account = createMenu("My Account");
175             putPage(SupportUserDetailsPage.PATH + "*", new SupportUserDetailsPage(), null);
176             putPage(ChangePasswordPage.PATH, new ChangePasswordPage(), account);
177             putPage(History.PATH, new History(false), account);
178             putPage(FindAgentAccess.PATH, new OneFormPage("Access to Find Agent", FindAgentAccess.class), account);
179             putPage(History.SUPPORT_PATH, new History(true), null);
180             putPage(UserTrainings.PATH, new UserTrainings(false), account);
181             putPage(MyDetails.PATH, new MyDetails(), account);
182             putPage(UserTrainings.SUPPORT_PATH, new UserTrainings(true), null);
183             putPage(Points.SUPPORT_PATH, new Points(true), null);
184             putPage(Certificates.SUPPORT_PATH + "/*", new Certificates(true), null);
185
186             putPage(PasswordResetPage.PATH, new PasswordResetPage(), null);
187             putPage(LogoutPage.PATH, new LogoutPage(), null);
188
189             if (testing) {
190                 try {
191                     Class<?> manager = Class.forName("club.wpia.gigi.pages.Manager");
192                     Page p = (Page) manager.getMethod("getInstance").invoke(null);
193                     String pa = (String) manager.getField("PATH").get(null);
194                     Menu testServer = createMenu("Gigi test server");
195                     putPage(pa + "/*", p, testServer);
196                 } catch (ReflectiveOperationException e) {
197                     e.printStackTrace();
198                 }
199             }
200
201             try {
202                 putPage("/wot/rules", new StaticPage("Verification Rules", VerifyPage.class.getResourceAsStream("Rules.templ")), wot);
203             } catch (UnsupportedEncodingException e) {
204                 throw new ServletException(e);
205             }
206             rootMenu = new MenuCollector();
207
208             Menu languages = createMenu("Language");
209             addLanguages(languages);
210             for (Menu menu : categories) {
211                 menu.prepare();
212                 rootMenu.put(menu);
213             }
214
215             // rootMenu.prepare();
216             return rootMenu;
217         }
218
219         private void addLanguages(Menu languages) {
220             for (Locale l : Language.getSupportedLocales()) {
221                 languages.addItem(new SimpleUntranslatedMenuItem("?lang=" + l.toString(), l.getDisplayName(l)));
222             }
223         }
224
225         public Map<String, Page> getPages() {
226             return Collections.unmodifiableMap(pages);
227         }
228     }
229
230     public static final String LOGGEDIN = "loggedin";
231
232     public static final String CERT_SERIAL = "club.wpia.gigi.serial";
233
234     public static final String CERT_ISSUER = "club.wpia.gigi.issuer";
235
236     public static final String AUTH_CONTEXT = "auth";
237
238     public static final String LOGIN_METHOD = "club.wpia.gigi.loginMethod";
239
240     private static final long serialVersionUID = -6386785421902852904L;
241
242     private static Gigi instance;
243
244     private static final Template baseTemplate = new Template(Gigi.class.getResource("Gigi.templ"));
245
246     private PingerDaemon pinger;
247
248     private KeyStore truststore;
249
250     private boolean testing;
251
252     private MenuCollector rootMenu;
253
254     private Map<String, Page> pages;
255
256     private boolean firstInstanceInited = false;
257
258     public Gigi(Properties conf, KeyStore truststore) {
259         synchronized (Gigi.class) {
260             if (instance != null) {
261                 throw new IllegalStateException("Multiple Gigi instances!");
262             }
263             testing = conf.getProperty("testing") != null;
264             instance = this;
265             DomainAssessment.init(conf);
266             DatabaseConnection.init(conf);
267             TimeConditions.init(conf);
268             PasswordHash.init(conf);
269             this.truststore = truststore;
270             pinger = new PingerDaemon(truststore);
271             pinger.start();
272         }
273     }
274
275     @Override
276     public synchronized void init() throws ServletException {
277         if (firstInstanceInited) {
278             super.init();
279             return;
280         }
281         // ensure those static initializers are finished
282         try (Link l = DatabaseConnection.newLink(false)) {
283             CACertificate.getById(1);
284             CertificateProfile.getById(1);
285             CATSType.AGENT_CHALLENGE.getDisplayName();
286         } catch (InterruptedException e) {
287             throw new Error(e);
288         }
289
290         MenuBuilder mb = new MenuBuilder();
291         rootMenu = mb.generateMenu();
292         pages = mb.getPages();
293
294         firstInstanceInited = true;
295         super.init();
296     }
297
298     private Page getPage(String pathInfo) {
299         if (pathInfo.endsWith("/") && !pathInfo.equals("/")) {
300             pathInfo = pathInfo.substring(0, pathInfo.length() - 1);
301         }
302         Page page = pages.get(pathInfo);
303         if (page != null) {
304             return page;
305         }
306         page = pages.get(pathInfo + "/*");
307         if (page != null) {
308             return page;
309         }
310         int idx = pathInfo.lastIndexOf('/');
311         if (idx == -1 || idx == 0) {
312             return null;
313         }
314
315         page = pages.get(pathInfo.substring(0, idx) + "/*");
316         if (page != null) {
317             return page;
318         }
319         int lIdx = pathInfo.lastIndexOf('/', idx - 1);
320         if (lIdx == -1) {
321             return null;
322         }
323         String lastResort = pathInfo.substring(0, lIdx) + "/*" + pathInfo.substring(idx);
324         page = pages.get(lastResort);
325         return page;
326
327     }
328
329     private static String staticTemplateVar = "//" + ServerConstants.getHostNamePort(Host.STATIC);
330
331     private static String staticTemplateVarSecure = "//" + ServerConstants.getHostNamePortSecure(Host.STATIC);
332
333     @Override
334     protected void service(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
335         if ("/error".equals(req.getPathInfo()) || "/denied".equals(req.getPathInfo())) {
336             if (DatabaseConnection.hasInstance()) {
337                 serviceWithConnection(req, resp);
338                 return;
339             }
340         }
341         try (DatabaseConnection.Link l = DatabaseConnection.newLink( !req.getMethod().equals("POST"))) {
342             serviceWithConnection(req, resp);
343         } catch (InterruptedException e) {
344             e.printStackTrace();
345         }
346     }
347
348     protected void serviceWithConnection(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
349         boolean isSecure = req.isSecure();
350         addXSSHeaders(resp, isSecure);
351         // Firefox only sends this, if it's a cross domain access; safari sends
352         // it always
353         String originHeader = req.getHeader("Origin");
354         if (originHeader != null //
355                 && !(originHeader.matches("^" + Pattern.quote("https://" + ServerConstants.getHostNamePortSecure(Host.WWW)) + "(/.*|)") || //
356                         originHeader.matches("^" + Pattern.quote("http://" + ServerConstants.getHostNamePort(Host.WWW)) + "(/.*|)") || //
357                         originHeader.matches("^" + Pattern.quote("https://" + ServerConstants.getHostNamePortSecure(Host.SECURE)) + "(/.*|)"))) {
358             resp.setContentType("text/html; charset=utf-8");
359             resp.getWriter().println("<html><head><title>Alert</title></head><body>No cross domain access allowed.<br/><b>If you don't know why you're seeing this you may have been fished! Please change your password immediately!</b></body></html>");
360             return;
361         }
362         HttpSession hs = req.getSession();
363         BigInteger clientSerial = (BigInteger) hs.getAttribute(CERT_SERIAL);
364         if (clientSerial != null) {
365             X509Certificate[] cert = (X509Certificate[]) req.getAttribute("javax.servlet.request.X509Certificate");
366             if (cert == null || cert[0] == null//
367                     || !cert[0].getSerialNumber().equals(clientSerial) //
368                     || !cert[0].getIssuerDN().equals(hs.getAttribute(CERT_ISSUER))) {
369                 hs.invalidate();
370                 resp.sendError(403, "Certificate mismatch.");
371                 return;
372             }
373
374         }
375         if (req.getParameter("lang") != null) {
376             Locale l = Language.getLocaleFromString(req.getParameter("lang"));
377             Language lu = Language.getInstance(l);
378             req.getSession().setAttribute(Language.SESSION_ATTRIB_NAME, lu != null ? lu.getLocale() : Locale.ENGLISH);
379         }
380         final Page p = getPage(req.getPathInfo());
381
382         if (p != null) {
383             if ( !isSecure && (p.needsLogin() || p instanceof LoginPage || p instanceof RegisterPage)) {
384                 resp.sendRedirect("https://" + ServerConstants.getHostNamePortSecure(Host.WWW) + req.getPathInfo());
385                 return;
386             }
387             AuthorizationContext currentAuthContext = LoginPage.getAuthorizationContext(req);
388             if ( !p.isPermitted(currentAuthContext)) {
389                 if (hs.getAttribute("loggedin") == null) {
390                     String request = req.getPathInfo();
391                     request = request.split("\\?")[0];
392                     hs.setAttribute(LoginPage.LOGIN_RETURNPATH, request);
393                     resp.sendRedirect("/login");
394                     return;
395                 }
396                 resp.sendError(403);
397                 return;
398             }
399             try {
400                 if (p.beforeTemplate(req, resp)) {
401                     return;
402                 }
403             } catch (CSRFException e) {
404                 resp.sendError(500, "CSRF invalid");
405                 return;
406             }
407             HashMap<String, Object> vars = new HashMap<String, Object>();
408             // System.out.println(req.getMethod() + ": " + req.getPathInfo() +
409             // " -> " + p);
410             Outputable content = new Outputable() {
411
412                 @Override
413                 public void output(PrintWriter out, Language l, Map<String, Object> vars) {
414                     try {
415                         if (req.getMethod().equals("POST")) {
416                             if (req.getQueryString() != null && !(p instanceof HandlesMixedRequest)) {
417                                 return;
418                             }
419                             p.doPost(req, resp);
420                         } else {
421                             p.doGet(req, resp);
422                         }
423                     } catch (CSRFException err) {
424                         try {
425                             resp.sendError(500, "CSRF invalid");
426                         } catch (IOException e) {
427                             e.printStackTrace();
428                         }
429                     } catch (IOException e) {
430                         e.printStackTrace();
431                     }
432
433                 }
434             };
435             Language lang = Page.getLanguage(req);
436
437             vars.put(Menu.AUTH_VALUE, currentAuthContext);
438             vars.put("menu", rootMenu);
439             vars.put("title", lang.getTranslation(p.getTitle()));
440             vars.put("static", isSecure ? staticTemplateVarSecure : staticTemplateVar);
441             vars.put("year", Calendar.getInstance().get(Calendar.YEAR));
442             vars.put("content", content);
443             if (isSecure) {
444                 req.setAttribute(LINK_HOST, ServerConstants.getHostNamePortSecure(Host.LINK));
445             } else {
446                 req.setAttribute(LINK_HOST, ServerConstants.getHostNamePort(Host.LINK));
447             }
448             vars.put(Gigi.LINK_HOST, req.getAttribute(Gigi.LINK_HOST));
449             if (currentAuthContext != null) {
450                 // TODO maybe move this information into the AuthContext object
451                 vars.put("loginMethod", req.getSession().getAttribute(LOGIN_METHOD));
452                 vars.put("authContext", currentAuthContext);
453
454             }
455             vars.put("appName", ServerConstants.getAppName());
456             resp.setContentType("text/html; charset=utf-8");
457             baseTemplate.output(resp.getWriter(), lang, vars);
458         } else {
459             resp.sendError(404, "Page not found.");
460         }
461
462     }
463
464     public static void addXSSHeaders(HttpServletResponse hsr, boolean doHttps) {
465         hsr.addHeader("Access-Control-Allow-Origin", "https://" + ServerConstants.getHostNamePortSecure(Host.WWW) + " https://" + ServerConstants.getHostNamePortSecure(Host.SECURE));
466         hsr.addHeader("Access-Control-Max-Age", "60");
467         if (doHttps) {
468             hsr.addHeader("Content-Security-Policy", httpsCSP);
469         } else {
470             hsr.addHeader("Content-Security-Policy", httpCSP);
471         }
472         hsr.addHeader("Strict-Transport-Security", "max-age=31536000");
473
474     }
475
476     private static String httpsCSP = genHttpsCSP();
477
478     private static String httpCSP = genHttpCSP();
479
480     private static String genHttpsCSP() {
481         StringBuffer csp = new StringBuffer();
482         csp.append("default-src 'none'");
483         csp.append(";font-src https://" + ServerConstants.getHostNamePortSecure(Host.STATIC));
484         csp.append(";img-src https://" + ServerConstants.getHostNamePortSecure(Host.STATIC));
485         csp.append(";media-src 'none'; object-src 'none'");
486         csp.append(";script-src https://" + ServerConstants.getHostNamePortSecure(Host.STATIC));
487         csp.append(";style-src https://" + ServerConstants.getHostNamePortSecure(Host.STATIC));
488         csp.append(";form-action https://" + ServerConstants.getHostNamePortSecure(Host.SECURE) + " https://" + ServerConstants.getHostNamePortSecure(Host.WWW));
489         // csp.append(";report-url https://api.wpia.club/security/csp/report");
490         return csp.toString();
491     }
492
493     private static String genHttpCSP() {
494         StringBuffer csp = new StringBuffer();
495         csp.append("default-src 'none'");
496         csp.append(";font-src http://" + ServerConstants.getHostNamePort(Host.STATIC));
497         csp.append(";img-src http://" + ServerConstants.getHostNamePort(Host.STATIC));
498         csp.append(";media-src 'none'; object-src 'none'");
499         csp.append(";script-src http://" + ServerConstants.getHostNamePort(Host.STATIC));
500         csp.append(";style-src http://" + ServerConstants.getHostNamePort(Host.STATIC));
501         csp.append(";form-action http://" + ServerConstants.getHostNamePortSecure(Host.SECURE) + " http://" + ServerConstants.getHostNamePort(Host.WWW));
502         // csp.append(";report-url http://api.wpia.club/security/csp/report");
503         return csp.toString();
504     }
505
506     /**
507      * Requests Pinging of domains.
508      * 
509      * @param toReping
510      *            if not null, the {@link DomainPingConfiguration} to test, if
511      *            null, just re-check if there is something to do.
512      */
513     public static void notifyPinger(DomainPingConfiguration toReping) {
514         if (toReping != null) {
515             instance.pinger.queue(toReping);
516         }
517         instance.pinger.interrupt();
518     }
519
520 }