From a8422ae5d5dc70c5a2776d3cead356c111e5b9d7 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Felix=20D=C3=B6rre?= Date: Tue, 14 Jun 2016 10:16:21 +0200 Subject: [PATCH] add: allow manually triggered email reping. (addresses #5) Change-Id: I13853b13de25a4f7257929bad987a9d4e9f0b5ab --- .../gigi/database/DatabaseConnection.java | 18 ++-- .../cacert/gigi/database/tableStructure.sql | 8 +- .../cacert/gigi/database/upgrade/from_9.sql | 5 + .../cacert/gigi/dbObjects/EmailAddress.java | 92 +++++++++++++------ .../account/mail/MailManagementForm.java | 51 +++++----- .../account/mail/MailManagementForm.templ | 12 +-- .../gigi/pages/account/mail/MailOverview.java | 2 +- .../pages/account/TestMailManagement.java | 22 ++--- .../org/cacert/gigi/pages/Manager.java | 43 ++++----- 9 files changed, 142 insertions(+), 111 deletions(-) create mode 100644 src/org/cacert/gigi/database/upgrade/from_9.sql diff --git a/src/org/cacert/gigi/database/DatabaseConnection.java b/src/org/cacert/gigi/database/DatabaseConnection.java index c85b5348..f1c1c011 100644 --- a/src/org/cacert/gigi/database/DatabaseConnection.java +++ b/src/org/cacert/gigi/database/DatabaseConnection.java @@ -99,7 +99,7 @@ public class DatabaseConnection { } - public static final int CURRENT_SCHEMA_VERSION = 9; + public static final int CURRENT_SCHEMA_VERSION = 10; public static final int CONNECTION_TIMEOUT = 24 * 60 * 60; @@ -222,12 +222,7 @@ public class DatabaseConnection { Statement s = getInstance().c.createStatement(); try { while (version < CURRENT_SCHEMA_VERSION) { - try (InputStream resourceAsStream = DatabaseConnection.class.getResourceAsStream("upgrade/from_" + version + ".sql")) { - if (resourceAsStream == null) { - throw new Error("Upgrade script from version " + version + " was not found."); - } - SQLFileManager.addFile(s, resourceAsStream, ImportType.PRODUCTION); - } + addUpgradeScript(Integer.toString(version), s); version++; } s.addBatch("UPDATE \"schemeVersion\" SET version='" + version + "'"); @@ -244,6 +239,15 @@ public class DatabaseConnection { } } + private static void addUpgradeScript(String version, Statement s) throws Error, IOException, SQLException { + try (InputStream resourceAsStream = DatabaseConnection.class.getResourceAsStream("upgrade/from_" + version + ".sql")) { + if (resourceAsStream == null) { + throw new Error("Upgrade script from version " + version + " was not found."); + } + SQLFileManager.addFile(s, resourceAsStream, ImportType.PRODUCTION); + } + } + public static final String preprocessQuery(String originalQuery) { originalQuery = originalQuery.replace('`', '"'); if (originalQuery.matches("^INSERT INTO [^ ]+ SET .*")) { diff --git a/src/org/cacert/gigi/database/tableStructure.sql b/src/org/cacert/gigi/database/tableStructure.sql index e49b84af..e7859420 100644 --- a/src/org/cacert/gigi/database/tableStructure.sql +++ b/src/org/cacert/gigi/database/tableStructure.sql @@ -67,12 +67,9 @@ CREATE TABLE "emails" ( "created" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, "modified" timestamp NULL DEFAULT NULL, "deleted" timestamp NULL DEFAULT NULL, - "hash" varchar(50) NOT NULL DEFAULT '', - "attempts" smallint NOT NULL DEFAULT '0', PRIMARY KEY ("id") ); CREATE INDEX ON "emails" ("memid"); -CREATE INDEX ON "emails" ("hash"); CREATE INDEX ON "emails" ("deleted"); CREATE INDEX ON "emails" ("email"); @@ -90,7 +87,8 @@ CREATE TABLE "emailPinglog" ( "email" varchar(255) NOT NULL, "type" "emailPingType" NOT NULL, "status" "pingState" NOT NULL, - "result" varchar(255) NOT NULL + "result" varchar(255) NOT NULL, + "challenge" varchar(255) NULL DEFAULT NULL ); DROP TABLE IF EXISTS "pingconfig"; @@ -374,7 +372,7 @@ CREATE TABLE "schemeVersion" ( "version" smallint NOT NULL, PRIMARY KEY ("version") ); -INSERT INTO "schemeVersion" (version) VALUES(9); +INSERT INTO "schemeVersion" (version) VALUES(10); DROP TABLE IF EXISTS `passwordResetTickets`; CREATE TABLE `passwordResetTickets` ( diff --git a/src/org/cacert/gigi/database/upgrade/from_9.sql b/src/org/cacert/gigi/database/upgrade/from_9.sql new file mode 100644 index 00000000..ec2b0be2 --- /dev/null +++ b/src/org/cacert/gigi/database/upgrade/from_9.sql @@ -0,0 +1,5 @@ +ALTER TABLE "emailPinglog" ADD COLUMN "challenge" varchar(255) NULL DEFAULT NULL; + +INSERT INTO "emailPinglog" SELECT CURRENT_TIMESTAMP AS "when", "memid" AS "uid", "email", 'active'::"emailPingType" AS "type", CASE WHEN "hash"='' THEN 'success'::"pingState" ELSE 'open'::"pingState" END AS state, '' AS result, "hash" AS "challenge" FROM "emails"; +ALTER TABLE "emails" DROP COLUMN "attempts"; +ALTER TABLE "emails" DROP COLUMN "hash"; diff --git a/src/org/cacert/gigi/dbObjects/EmailAddress.java b/src/org/cacert/gigi/dbObjects/EmailAddress.java index 7a053201..d92338ca 100644 --- a/src/org/cacert/gigi/dbObjects/EmailAddress.java +++ b/src/org/cacert/gigi/dbObjects/EmailAddress.java @@ -1,7 +1,11 @@ package org.cacert.gigi.dbObjects; import java.io.IOException; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; import java.util.Locale; +import java.util.Map; import org.cacert.gigi.GigiApiException; import org.cacert.gigi.database.GigiPreparedStatement; @@ -9,20 +13,22 @@ import org.cacert.gigi.database.GigiResultSet; import org.cacert.gigi.email.EmailProvider; import org.cacert.gigi.email.MailProbe; import org.cacert.gigi.localisation.Language; +import org.cacert.gigi.output.template.Scope; +import org.cacert.gigi.output.template.SprintfCommand; import org.cacert.gigi.util.RandomToken; public class EmailAddress implements IdCachable, Verifyable { + public static final int REPING_MINIMUM_DELAY = 5 * 60 * 1000; + private String address; private int id; private User owner; - private String hash = null; - private EmailAddress(int id) { - try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT `memid`, `email`, `hash` FROM `emails` WHERE `id`=? AND `deleted` IS NULL")) { + try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT `memid`, `email` FROM `emails` WHERE `id`=? AND `deleted` IS NULL")) { ps.setInt(1, id); GigiResultSet rs = ps.executeQuery(); @@ -32,7 +38,6 @@ public class EmailAddress implements IdCachable, Verifyable { this.id = id; owner = User.getById(rs.getInt(1)); address = rs.getString(2); - hash = rs.getString(3); } } @@ -42,7 +47,6 @@ public class EmailAddress implements IdCachable, Verifyable { } this.address = address; this.owner = owner; - this.hash = RandomToken.generateToken(16); insert(Language.getInstance(mailLocale)); } @@ -52,10 +56,9 @@ public class EmailAddress implements IdCachable, Verifyable { if (id != 0) { throw new IllegalStateException("already inserted."); } - try (GigiPreparedStatement psCheck = new GigiPreparedStatement("SELECT 1 FROM `emails` WHERE email=? AND deleted is NULL"); GigiPreparedStatement ps = new GigiPreparedStatement("INSERT INTO `emails` SET memid=?, hash=?, email=?")) { + try (GigiPreparedStatement psCheck = new GigiPreparedStatement("SELECT 1 FROM `emails` WHERE email=? AND deleted is NULL"); GigiPreparedStatement ps = new GigiPreparedStatement("INSERT INTO `emails` SET memid=?, email=?")) { ps.setInt(1, owner.getId()); - ps.setString(2, hash); - ps.setString(3, address); + ps.setString(2, address); psCheck.setString(1, address); GigiResultSet res = psCheck.executeQuery(); if (res.next()) { @@ -66,12 +69,24 @@ public class EmailAddress implements IdCachable, Verifyable { } myCache.put(this); } - MailProbe.sendMailProbe(l, "email", id, hash, address); + ping(l); } catch (IOException e) { e.printStackTrace(); } } + private void ping(Language l) throws IOException { + String hash = RandomToken.generateToken(16); + try (GigiPreparedStatement statmt = new GigiPreparedStatement("INSERT INTO `emailPinglog` SET `when`=NOW(), `email`=?, `result`='', `uid`=?, `type`='active', `status`='open'::`pingState`, `challenge`=?")) { + statmt.setString(1, address); + statmt.setInt(2, owner.getId()); + statmt.setString(3, hash); + statmt.execute(); + } + + MailProbe.sendMailProbe(l, "email", id, hash, address); + } + public int getId() { return id; } @@ -81,28 +96,53 @@ public class EmailAddress implements IdCachable, Verifyable { } public synchronized void verify(String hash) throws GigiApiException { - if (this.hash.equals(hash)) { - try (GigiPreparedStatement ps = new GigiPreparedStatement("UPDATE `emails` SET hash='' WHERE id=?")) { - ps.setInt(1, id); - ps.execute(); - } - hash = ""; + try (GigiPreparedStatement stmt = new GigiPreparedStatement("UPDATE `emailPinglog` SET `status`='success'::`pingState` WHERE `email`=? AND `uid`=? AND `type`='active' AND `challenge`=?")) { + stmt.setString(1, address); + stmt.setInt(2, owner.getId()); + stmt.setString(3, hash); + stmt.executeUpdate(); + } + // Verify user with that primary email + try (GigiPreparedStatement ps2 = new GigiPreparedStatement("update `users` set `verified`='1' where `id`=? and `email`=? and `verified`='0'")) { + ps2.setInt(1, owner.getId()); + ps2.setString(2, address); + ps2.execute(); + } + } - // Verify user with that primary email - try (GigiPreparedStatement ps2 = new GigiPreparedStatement("update `users` set `verified`='1' where `id`=? and `email`=? and `verified`='0'")) { - ps2.setInt(1, owner.getId()); - ps2.setString(2, address); - ps2.execute(); - } - this.hash = ""; + public boolean isVerified() { + try (GigiPreparedStatement statmt = new GigiPreparedStatement("SELECT 1 FROM `emailPinglog` WHERE `email`=? AND `uid`=? AND `type`='active' AND `status`='success'")) { + statmt.setString(1, address); + statmt.setInt(2, owner.getId()); + GigiResultSet e = statmt.executeQuery(); + return e.next(); + } + } - } else { - throw new GigiApiException("Email verification hash is invalid."); + public Date getLastPing(boolean onlySuccess) { + Date lastExecution; + try (GigiPreparedStatement statmt = new GigiPreparedStatement("SELECT MAX(`when`) FROM `emailPinglog` WHERE `email`=? AND `uid`=? AND `type`='active'" + (onlySuccess ? " AND `status`='success'" : ""))) { + statmt.setString(1, address); + statmt.setInt(2, owner.getId()); + GigiResultSet e = statmt.executeQuery(); + if ( !e.next()) { + return null; + } + lastExecution = e.getTimestamp(1); } + return lastExecution; } - public boolean isVerified() { - return hash.isEmpty(); + public synchronized void requestReping(Language l) throws IOException, GigiApiException { + Date lastExecution = getLastPing(false); + + if (lastExecution != null && lastExecution.getTime() + REPING_MINIMUM_DELAY >= System.currentTimeMillis()) { + Map data = new HashMap(); + data.put("data", new Date(lastExecution.getTime() + REPING_MINIMUM_DELAY)); + throw new GigiApiException(new Scope(new SprintfCommand("Reping is only allowed after 5 minutes, yours end at {0}.", Arrays.asList("${data}")), data)); + } + ping(l); + return; } private static ObjectCache myCache = new ObjectCache<>(); diff --git a/src/org/cacert/gigi/pages/account/mail/MailManagementForm.java b/src/org/cacert/gigi/pages/account/mail/MailManagementForm.java index c8245026..c082a1be 100644 --- a/src/org/cacert/gigi/pages/account/mail/MailManagementForm.java +++ b/src/org/cacert/gigi/pages/account/mail/MailManagementForm.java @@ -1,7 +1,9 @@ package org.cacert.gigi.pages.account.mail; +import java.io.IOException; import java.io.PrintWriter; import java.util.Map; +import java.util.Map.Entry; import javax.servlet.http.HttpServletRequest; @@ -30,36 +32,29 @@ public class MailManagementForm extends Form { @Override public boolean submit(PrintWriter out, HttpServletRequest req) { - if (req.getParameter("makedefault") != null) { - try { - String mailid = req.getParameter("emailid"); - if (mailid == null) { - return false; + Map map = req.getParameterMap(); + try { + for (Entry e : map.entrySet()) { + String k = e.getKey(); + String[] p = k.split(":", 2); + if (p[0].equals("default")) { + target.updateDefaultEmail(EmailAddress.getById(Integer.parseInt(p[1]))); } - target.updateDefaultEmail(EmailAddress.getById(Integer.parseInt(mailid.trim()))); - } catch (GigiApiException e) { - e.format(out, Page.getLanguage(req)); - return false; - } - return true; - } - if (req.getParameter("delete") != null) { - String[] toDel = req.getParameterValues("delid[]"); - if (toDel == null) { - return false; - } - for (int i = 0; i < toDel.length; i++) { - try { - target.deleteEmail(EmailAddress.getById(Integer.parseInt(toDel[i].trim()))); - } catch (GigiApiException e) { - e.format(out, Page.getLanguage(req)); - return false; + if (p[0].equals("delete")) { + target.deleteEmail(EmailAddress.getById(Integer.parseInt(p[1]))); + } + if (p[0].equals("reping")) { + EmailAddress.getById(Integer.parseInt(p[1])).requestReping(Page.getLanguage(req)); } } - return true; - + } catch (GigiApiException e) { + e.format(out, Page.getLanguage(req)); + return false; + } catch (IOException e1) { + new GigiApiException("Error while doing reping.").format(out, Page.getLanguage(req)); + return false; } - return false; + return true; } @Override @@ -78,9 +73,9 @@ public class MailManagementForm extends Form { int mailID = emailAddress.getId(); vars.put("id", mailID); if (emailAddress.getAddress().equals(target.getEmail())) { - vars.put("checked", "checked"); + vars.put("default", " disabled"); } else { - vars.put("checked", ""); + vars.put("default", ""); } if (emailAddress.isVerified()) { vars.put("verification", "Verified"); diff --git a/src/org/cacert/gigi/pages/account/mail/MailManagementForm.templ b/src/org/cacert/gigi/pages/account/mail/MailManagementForm.templ index 3b8d9400..6c6439bf 100644 --- a/src/org/cacert/gigi/pages/account/mail/MailManagementForm.templ +++ b/src/org/cacert/gigi/pages/account/mail/MailManagementForm.templ @@ -8,20 +8,18 @@ - + + - > + > - + > + - - - - diff --git a/src/org/cacert/gigi/pages/account/mail/MailOverview.java b/src/org/cacert/gigi/pages/account/mail/MailOverview.java index e3b59916..da3befd5 100644 --- a/src/org/cacert/gigi/pages/account/mail/MailOverview.java +++ b/src/org/cacert/gigi/pages/account/mail/MailOverview.java @@ -39,7 +39,7 @@ public class MailOverview extends Page { if (f.submit(out, req)) { resp.sendRedirect(MailOverview.DEFAULT_PATH); } - } else if (req.getParameter("makedefault") != null || req.getParameter("delete") != null) { + } else { MailManagementForm f = Form.getForm(req, MailManagementForm.class); if (f.submit(out, req)) { resp.sendRedirect(MailOverview.DEFAULT_PATH); diff --git a/tests/org/cacert/gigi/pages/account/TestMailManagement.java b/tests/org/cacert/gigi/pages/account/TestMailManagement.java index bf97c269..412e2d78 100644 --- a/tests/org/cacert/gigi/pages/account/TestMailManagement.java +++ b/tests/org/cacert/gigi/pages/account/TestMailManagement.java @@ -67,17 +67,17 @@ public class TestMailManagement extends ClientTest { @Test public void testMailSetDefaultWeb() throws MalformedURLException, UnsupportedEncodingException, IOException, InterruptedException, GigiApiException { - EmailAddress adrr = createVerifiedEmail(u); - assertNull(executeBasicWebInteraction(cookie, path, "makedefault&emailid=" + adrr.getId())); + EmailAddress addr = createVerifiedEmail(u); + assertNull(executeBasicWebInteraction(cookie, path, "default:" + addr.getId())); ObjectCache.clearAllCaches(); - assertEquals(User.getById(u.getId()).getEmail(), adrr.getAddress()); + assertEquals(User.getById(u.getId()).getEmail(), addr.getAddress()); } @Test public void testMailSetDefaultWebUnverified() throws MalformedURLException, UnsupportedEncodingException, IOException, InterruptedException, GigiApiException { - EmailAddress adrr = new EmailAddress(u, createUniqueName() + "test@test.tld", Locale.ENGLISH); - assertNotNull(executeBasicWebInteraction(cookie, path, "makedefault&emailid=" + adrr.getId())); - assertNotEquals(User.getById(u.getId()).getEmail(), adrr.getAddress()); + EmailAddress addr = new EmailAddress(u, createUniqueName() + "test@test.tld", Locale.ENGLISH); + assertNotNull(executeBasicWebInteraction(cookie, path, "default:" + addr.getId())); + assertNotEquals(User.getById(u.getId()).getEmail(), addr.getAddress()); getMailReciever().clearMails(); } @@ -92,7 +92,7 @@ public class TestMailManagement extends ClientTest { } } assertNotEquals(id, -1); - assertNotNull(executeBasicWebInteraction(cookie, path, "makedefault&emailid=" + id)); + assertNotNull(executeBasicWebInteraction(cookie, path, "default:" + id)); assertNotEquals(User.getById(u.getId()).getEmail(), u2.getEmail()); getMailReciever().clearMails(); } @@ -100,7 +100,7 @@ public class TestMailManagement extends ClientTest { @Test public void testMailDeleteWeb() throws InterruptedException, GigiApiException, MalformedURLException, UnsupportedEncodingException, IOException { EmailAddress addr = createVerifiedEmail(u); - assertNull(executeBasicWebInteraction(cookie, path, "delete&delid[]=" + addr.getId(), 0)); + assertNull(executeBasicWebInteraction(cookie, path, "delete:" + addr.getId(), 0)); User u = User.getById(this.u.getId()); EmailAddress[] addresses = u.getEmails(); for (int i = 0; i < addresses.length; i++) { @@ -113,7 +113,7 @@ public class TestMailManagement extends ClientTest { EmailAddress[] addr = new EmailAddress[] { createVerifiedEmail(u), createVerifiedEmail(u) }; - assertNull(executeBasicWebInteraction(cookie, path, "delete&delid[]=" + addr[0].getId() + "&delid[]=" + addr[1].getId(), 0)); + assertNull(executeBasicWebInteraction(cookie, path, "delete:" + addr[0].getId() + "&delete:" + addr[1].getId(), 0)); User u = User.getById(this.u.getId()); EmailAddress[] addresses = u.getEmails(); for (int i = 0; i < addresses.length; i++) { @@ -126,14 +126,14 @@ public class TestMailManagement extends ClientTest { public void testMailDeleteWebFaulty() throws MalformedURLException, UnsupportedEncodingException, IOException { User u2 = User.getById(createVerifiedUser("fn", "ln", createUniqueName() + "uni@test.tld", TEST_PASSWORD)); EmailAddress em = u2.getEmails()[0]; - assertNotNull(executeBasicWebInteraction(cookie, path, "delete&delid[]=" + em.getId(), 0)); + assertNotNull(executeBasicWebInteraction(cookie, path, "delete:" + em.getId(), 0)); u2 = User.getById(u2.getId()); assertNotEquals(u2.getEmails().length, 0); } @Test public void testMailDeleteWebPrimary() throws MalformedURLException, UnsupportedEncodingException, IOException { - assertNotNull(executeBasicWebInteraction(cookie, path, "delete&delid[]=" + u.getEmails()[0].getId(), 0)); + assertNotNull(executeBasicWebInteraction(cookie, path, "delete:" + u.getEmails()[0].getId(), 0)); assertNotEquals(u.getEmails().length, 0); } } diff --git a/util-testing/org/cacert/gigi/pages/Manager.java b/util-testing/org/cacert/gigi/pages/Manager.java index 460fc4d2..649b7944 100644 --- a/util-testing/org/cacert/gigi/pages/Manager.java +++ b/util-testing/org/cacert/gigi/pages/Manager.java @@ -17,6 +17,8 @@ import java.util.Locale; import java.util.Map; import java.util.Properties; import java.util.TreeSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -53,20 +55,10 @@ public class Manager extends Page { public static final String PATH = "/manager"; - Field f; - private static HashMap dps; private Manager() { super("Test Manager"); - try { - f = EmailAddress.class.getDeclaredField("hash"); - f.setAccessible(true); - } catch (ReflectiveOperationException e) { - // TODO - System.out.println("I don't have 'hash', we are working probably in layered mode. Test Manager may not work."); - // throw new Error(e); - } try { Field gigiInstance = Gigi.class.getDeclaredField("instance"); @@ -212,13 +204,21 @@ public class Manager extends Page { gc.set(1990, 0, 1); User u = new User(email, "xvXV12°§", new Name("Först", "Läst", "Müddle", "Süffix"), new DayDate(gc.getTime().getTime()), Locale.ENGLISH); EmailAddress ea = u.getEmails()[0]; - if (f == null) { - System.out.println("verification failed"); - return; - } - String hash = (String) f.get(ea); + verify(email, ea); + } - ea.verify(hash); + private void verify(String email, EmailAddress ea) throws GigiApiException { + LinkedList i = emails.get(email); + while (i.size() > 0 && !ea.isVerified()) { + String lst = i.getLast(); + Pattern p = Pattern.compile("hash=([a-zA-Z0-9]+)"); + Matcher m = p.matcher(lst); + if (m.find()) { + ea.verify(m.group(1)); + } + i.removeLast(); + } + // ea.verify(hash); } User[] assurers = new User[25]; @@ -283,19 +283,10 @@ public class Manager extends Page { User u = User.getByEmail(req.getParameter("addEmailEmail")); try { EmailAddress ea = new EmailAddress(u, req.getParameter("addEmailNew"), Locale.ENGLISH); - if (f != null) { - String hash = (String) f.get(ea); - ea.verify(hash); - resp.getWriter().println("Email added and verified"); - } else { - resp.getWriter().println("Email added but verificatio failed."); - } + verify(ea.getAddress(), ea); } catch (IllegalArgumentException e) { e.printStackTrace(); resp.getWriter().println("An internal error occured."); - } catch (IllegalAccessException e) { - e.printStackTrace(); - resp.getWriter().println("An internal error occured."); } catch (GigiApiException e) { e.format(resp.getWriter(), Language.getInstance(Locale.ENGLISH)); } -- 2.39.2