From: Lucas Werkmeister Date: Thu, 4 Jan 2018 21:47:57 +0000 (+0100) Subject: Merge "chg: format show history link" X-Git-Url: https://code.wpia.club/?p=gigi.git;a=commitdiff_plain;h=39f9de33cf93e893cf61a3f21a2af61c7b1eebfc;hp=d505b191335de99c7ac9cfffcfe8f3f3c76fad40 Merge "chg: format show history link" --- diff --git a/config/generateTruststoreNRE.sh b/config/generateTruststoreNRE.sh index fbc5e6ed..23b7b989 100755 --- a/config/generateTruststoreNRE.sh +++ b/config/generateTruststoreNRE.sh @@ -30,6 +30,13 @@ cabasename=assured caname=${cabasename}_$(date +%Y)_1 ca=../signer/ca/$caname/ca if [[ -f "$ca.key" ]] && ! [[ -f keystore.pkcs12 ]]; then + if [[ -f serial_base ]]; then + serial_base=$(< serial_base) + else + serial_base=100000 + fi + serial_base=$((serial_base + 1)) + printf '%d\n' "$serial_base" >| serial_base # when the domain is provided externally as environment variable, use it and do not prompt for it. [[ -z $DOMAIN ]] && read -rp "I need to generate gigi-certificates. I need your base domain: " DOMAIN # Assuming we have access to the CA-keys we generate two certificates and present them to gigi @@ -39,9 +46,9 @@ if [[ -f "$ca.key" ]] && ! [[ -f keystore.pkcs12 ]]; then openssl req -newkey rsa:2048 -keyout www.key -out www.csr -nodes -subj "/CN=gigi server certificate" openssl req -newkey rsa:2048 -keyout mail.key -out mail.csr -nodes -subj "/CN=gigi system" - # Sign the two requests with the keys in the config of the simple signer. Use serials 1000001 and 1000002 to probably not collide with the "simple signer" - openssl x509 -req -in www.csr -out www.crt -CA $ca.crt -CAkey $ca.key -set_serial 1000001 -extfile <(printf "[ext]\nsubjectAltName=DNS:www.$DOMAIN,DNS:secure.$DOMAIN,DNS:static.$DOMAIN,DNS:api.$DOMAIN\nbasicConstraints=CA:FALSE\nextendedKeyUsage=serverAuth\nkeyUsage=digitalSignature,keyEncipherment\n") -extensions ext - openssl x509 -req -in mail.csr -out mail.crt -CA $ca.crt -CAkey $ca.key -set_serial 1000002 -extfile <(printf "[ext]\nsubjectAltName=email:support@$DOMAIN\nbasicConstraints=CA:FALSE\nextendedKeyUsage=emailProtection\nkeyUsage=digitalSignature,keyEncipherment\n") -extensions ext + # Sign the two requests with the keys in the config of the simple signer. Use the serial_base with extensions 1 and 2. These serials are long enough to probably not collide with the "simple signer" + openssl x509 -req -in www.csr -out www.crt -CA $ca.crt -CAkey $ca.key -set_serial ${serial_base}1 -extfile <(printf '[ext]\nsubjectAltName=DNS:www.%s,DNS:secure.%s,DNS:static.%s,DNS:api.%s\nbasicConstraints=CA:FALSE\nextendedKeyUsage=serverAuth\nkeyUsage=digitalSignature,keyEncipherment\n' "$DOMAIN" "$DOMAIN" "$DOMAIN" "$DOMAIN") -extensions ext + openssl x509 -req -in mail.csr -out mail.crt -CA $ca.crt -CAkey $ca.key -set_serial ${serial_base}2 -extfile <(printf '[ext]\nsubjectAltName=email:support@%s\nbasicConstraints=CA:FALSE\nextendedKeyUsage=emailProtection\nkeyUsage=digitalSignature,keyEncipherment\n' "$DOMAIN") -extensions ext # Store the webserver cert in 4 different pkcs12-keystores to have different "key aliases" and import them all into the "keystore.pkcs12" using the "importP"-method for t in www api secure static; do diff --git a/doc/jenkinsJob/config.xml b/doc/jenkinsJob/config.xml index 00a23c16..852e725d 100644 --- a/doc/jenkinsJob/config.xml +++ b/doc/jenkinsJob/config.xml @@ -29,11 +29,13 @@ BRANCH The branch to build from. $GERRIT_PATCHSET_REVISION + false JAVA_HOME /usr/lib/jvm/openjdk-8-jdk-gigi + false @@ -220,7 +222,9 @@ EOT localePath=$$$$LOCALE_FETCH_PATH$$$$ - cd gigi + rm -v *.deb +cd gigi +[ -f doc/scripts/genchangelog ] && doc/scripts/genchangelog dpkg-buildpackage -b -us -uc diff --git a/doc/scripts/genchangelog b/doc/scripts/genchangelog new file mode 100755 index 00000000..4a4e7167 --- /dev/null +++ b/doc/scripts/genchangelog @@ -0,0 +1,9 @@ +#!/bin/bash +cd "$(git rev-parse --show-toplevel)" +cat > debian/changelog < $(git show HEAD --pretty=format:%aD --no-patch) +EOF diff --git a/src/club/wpia/gigi/Gigi.java b/src/club/wpia/gigi/Gigi.java index 2c61c071..00993b52 100644 --- a/src/club/wpia/gigi/Gigi.java +++ b/src/club/wpia/gigi/Gigi.java @@ -147,11 +147,11 @@ public final class Gigi extends HttpServlet { Menu certificates = createMenu("Certificates"); putPage(Certificates.PATH + "/*", new Certificates(false), certificates); putPage(CertificateAdd.PATH, new CertificateAdd(), certificates); - putPage(MailOverview.DEFAULT_PATH, new MailOverview(), certificates); - putPage(DomainOverview.PATH, new DomainOverview(), certificates); - putPage(EditDomain.PATH + "*", new EditDomain(), null); Menu wot = createMenu("Verification"); + putPage(MailOverview.DEFAULT_PATH, new MailOverview(), wot); + putPage(DomainOverview.PATH, new DomainOverview(), wot); + putPage(EditDomain.PATH + "*", new EditDomain(), null); putPage(VerifyPage.PATH + "/*", new VerifyPage(), wot); putPage(Points.PATH, new Points(false), wot); putPage(RequestTTPPage.PATH, new RequestTTPPage(), wot); diff --git a/src/club/wpia/gigi/database/DatabaseConnection.java b/src/club/wpia/gigi/database/DatabaseConnection.java index 10e81e79..04351d1a 100644 --- a/src/club/wpia/gigi/database/DatabaseConnection.java +++ b/src/club/wpia/gigi/database/DatabaseConnection.java @@ -181,7 +181,7 @@ public class DatabaseConnection { } - public static final int CURRENT_SCHEMA_VERSION = 34; + public static final int CURRENT_SCHEMA_VERSION = 36; public static final int CONNECTION_TIMEOUT = 24 * 60 * 60; diff --git a/src/club/wpia/gigi/database/tableStructure.sql b/src/club/wpia/gigi/database/tableStructure.sql index 440bdec7..8c697f78 100644 --- a/src/club/wpia/gigi/database/tableStructure.sql +++ b/src/club/wpia/gigi/database/tableStructure.sql @@ -103,9 +103,11 @@ CREATE TABLE "domainPinglog" ( "configId" int NOT NULL, "state" "pingState" NOT NULL, "challenge" varchar(16), - "result" varchar(255) + "result" varchar(255), + "needsAction" boolean DEFAULT false ); CREATE INDEX ON "domainPinglog" ("configId","when"); +CREATE INDEX ON "domainPinglog" ("when", "needsAction"); DROP TABLE IF EXISTS "baddomains"; CREATE TABLE "baddomains" ( @@ -113,16 +115,6 @@ CREATE TABLE "baddomains" ( ); -DROP TABLE IF EXISTS "alerts"; -CREATE TABLE "alerts" ( - "memid" int NOT NULL DEFAULT '0', - "general" boolean NOT NULL DEFAULT 'false', - "country" boolean NOT NULL DEFAULT 'false', - "regional" boolean NOT NULL DEFAULT 'false', - "radius" boolean NOT NULL DEFAULT 'false', - PRIMARY KEY ("memid") -); - DROP TABLE IF EXISTS "user_agreements"; CREATE TABLE "user_agreements" ( "id" serial NOT NULL, @@ -378,7 +370,7 @@ CREATE TABLE "schemeVersion" ( "version" smallint NOT NULL, PRIMARY KEY ("version") ); -INSERT INTO "schemeVersion" (version) VALUES(34); +INSERT INTO "schemeVersion" (version) VALUES(36); DROP TABLE IF EXISTS `passwordResetTickets`; CREATE TABLE `passwordResetTickets` ( diff --git a/src/club/wpia/gigi/database/upgrade/from_34.sql b/src/club/wpia/gigi/database/upgrade/from_34.sql new file mode 100644 index 00000000..0c809ad8 --- /dev/null +++ b/src/club/wpia/gigi/database/upgrade/from_34.sql @@ -0,0 +1,2 @@ +ALTER TABLE "domainPinglog" ADD COLUMN "needsAction" boolean DEFAULT false; +CREATE INDEX ON "domainPinglog" ("when", "needsAction"); diff --git a/src/club/wpia/gigi/database/upgrade/from_35.sql b/src/club/wpia/gigi/database/upgrade/from_35.sql new file mode 100644 index 00000000..53dcc4d5 --- /dev/null +++ b/src/club/wpia/gigi/database/upgrade/from_35.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS "alerts"; diff --git a/src/club/wpia/gigi/dbObjects/CertificateOwner.java b/src/club/wpia/gigi/dbObjects/CertificateOwner.java index f608b2fc..007d98d3 100644 --- a/src/club/wpia/gigi/dbObjects/CertificateOwner.java +++ b/src/club/wpia/gigi/dbObjects/CertificateOwner.java @@ -24,7 +24,15 @@ public abstract class CertificateOwner implements IdCachable, Serializable { this.id = id; } - protected CertificateOwner() { + /** + * This constructor has a dummy parameter to allow callers to do checks + * before invoking the super constructor. + * + * @param dummy + * a parameter that is not used to allow callers to do checks + * before super constructor invocation. + */ + protected CertificateOwner(Void dummy) { try (GigiPreparedStatement ps = new GigiPreparedStatement("INSERT INTO `certOwners` DEFAULT VALUES")) { ps.execute(); id = ps.lastInsertId(); diff --git a/src/club/wpia/gigi/dbObjects/Domain.java b/src/club/wpia/gigi/dbObjects/Domain.java index e8accbca..9b356e60 100644 --- a/src/club/wpia/gigi/dbObjects/Domain.java +++ b/src/club/wpia/gigi/dbObjects/Domain.java @@ -90,7 +90,7 @@ public class Domain implements IdCachable, Verifyable { private LinkedList configs = null; - public List getConfiguredPings() throws GigiApiException { + public List getConfiguredPings() { LinkedList configs = this.configs; if (configs == null) { configs = new LinkedList<>(); @@ -143,12 +143,26 @@ public class Domain implements IdCachable, Verifyable { } } + /** + * Determines current domain validity. A domain is valid, iff at least two + * configured pings are currently successful. + * + * @return true, iff domain is valid + * @throws GigiApiException + */ public boolean isVerified() { - try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT 1 FROM `domainPinglog` INNER JOIN `pingconfig` ON `pingconfig`.`id`=`domainPinglog`.`configId` WHERE `domainid`=? AND `state`='success'")) { - ps.setInt(1, id); - GigiResultSet rs = ps.executeQuery(); - return rs.next(); + int count = 0; + boolean[] used = new boolean[DomainPingType.values().length]; + for (DomainPingConfiguration config : getConfiguredPings()) { + if (config.isValid() && !used[config.getType().ordinal()]) { + count++; + used[config.getType().ordinal()] = true; + } + if (count >= 2) { + return true; + } } + return false; } public DomainPingExecution[] getPings() throws GigiApiException { @@ -195,4 +209,22 @@ public class Domain implements IdCachable, Verifyable { } } + public Certificate[] fetchActiveCertificates() { + try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT `certs`.`id` FROM `certs` INNER JOIN `subjectAlternativeNames` ON `subjectAlternativeNames`.`certId` = `certs`.`id` WHERE (`contents`=? OR RIGHT(`contents`,LENGTH(?)+1)=CONCAT('.',?::VARCHAR)) AND `type`='DNS' AND `revoked` IS NULL AND `expire` > CURRENT_TIMESTAMP AND `memid`=? GROUP BY `certs`.`id`", true)) { + ps.setString(1, suffix); + ps.setString(2, suffix); + ps.setString(3, suffix); + ps.setInt(4, owner.getId()); + GigiResultSet rs = ps.executeQuery(); + rs.last(); + Certificate[] res = new Certificate[rs.getRow()]; + rs.beforeFirst(); + int i = 0; + while (rs.next()) { + res[i++] = Certificate.getById(rs.getInt(1)); + } + return res; + } + } + } diff --git a/src/club/wpia/gigi/dbObjects/DomainPingConfiguration.java b/src/club/wpia/gigi/dbObjects/DomainPingConfiguration.java index bdaad758..59db66cd 100644 --- a/src/club/wpia/gigi/dbObjects/DomainPingConfiguration.java +++ b/src/club/wpia/gigi/dbObjects/DomainPingConfiguration.java @@ -1,5 +1,6 @@ package club.wpia.gigi.dbObjects; +import java.sql.Timestamp; import java.util.Date; import club.wpia.gigi.Gigi; @@ -92,4 +93,47 @@ public class DomainPingConfiguration implements IdCachable { } throw new GigiApiException(SprintfCommand.createSimple("Reping is only allowed after {0} minutes, yours end at {1}.", REPING_MINIMUM_DELAY / 60 / 1000, new Date(lastExecution.getTime() + REPING_MINIMUM_DELAY))); } + + /** + * Return true when there was a last execution and it succeeded. + * + * @return if this ping is currently valid. + */ + public boolean isValid() { + try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT state='success' AS bool from `domainPinglog` WHERE `configId`=? ORDER BY `when` DESC LIMIT 1")) { + ps.setInt(1, id); + GigiResultSet rs = ps.executeQuery(); + if ( !rs.next()) { + return false; + } + return rs.getBoolean(1); + } + } + + /** + * Return true when this ping has not been successful within the last 2 + * weeks. + * + * @param time + * the point in time for which the determination is carried out. + * @return the value for this ping. + */ + public boolean isStrictlyInvalid(Date time) { + Date lastSuccess = getLastSuccess(); + if (lastSuccess.getTime() == 0) { + // never a successful ping + return true; + } + try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT `when` AS stamp from `domainPinglog` WHERE `configId`=? AND state='failed' AND `when` > ? ORDER BY `when` ASC LIMIT 1")) { + ps.setInt(1, id); + ps.setTimestamp(2, new Timestamp(lastSuccess.getTime())); + GigiResultSet rs = ps.executeQuery(); + if (rs.next()) { + Date turnedInvalid = new Date(rs.getTimestamp("stamp").getTime()); + // turned invalid older than 2 weeks ago + return turnedInvalid.getTime() < time.getTime() - 2L * 7 * 24 * 60 * 60 * 1000; + } + return false; + } + } } diff --git a/src/club/wpia/gigi/dbObjects/DomainPingExecution.java b/src/club/wpia/gigi/dbObjects/DomainPingExecution.java index 174b2a18..0f01c1d7 100644 --- a/src/club/wpia/gigi/dbObjects/DomainPingExecution.java +++ b/src/club/wpia/gigi/dbObjects/DomainPingExecution.java @@ -3,24 +3,26 @@ package club.wpia.gigi.dbObjects; import java.sql.Timestamp; import java.util.Date; +import club.wpia.gigi.database.GigiPreparedStatement; import club.wpia.gigi.database.GigiResultSet; +import club.wpia.gigi.ping.DomainPinger.PingState; public class DomainPingExecution { - private String state; + private final PingState state; - private String type; + private final String type; - private String info; + private final String info; - private String result; + private final String result; - private DomainPingConfiguration config; + private final DomainPingConfiguration config; - private Timestamp date; + private final Timestamp date; - public DomainPingExecution(GigiResultSet rs) { - state = rs.getString(1); + protected DomainPingExecution(GigiResultSet rs) { + state = PingState.valueOf(rs.getString(1).toUpperCase()); type = rs.getString(2); info = rs.getString(3); result = rs.getString(4); @@ -28,7 +30,27 @@ public class DomainPingExecution { date = rs.getTimestamp(6); } - public String getState() { + public DomainPingExecution(PingState state, String result, DomainPingConfiguration config, String challenge) { + this.state = state; + this.type = config.getType().getDBName(); + this.info = config.getInfo(); + this.result = result; + this.config = config; + this.date = new Timestamp(System.currentTimeMillis()); + try (GigiPreparedStatement enterPingResult = new GigiPreparedStatement("INSERT INTO `domainPinglog` SET `configId`=?, `state`=?::`pingState`, `result`=?, `challenge`=?, `when`=?, `needsAction`=?")) { + enterPingResult.setInt(1, config.getId()); + enterPingResult.setEnum(2, state); + enterPingResult.setString(3, result); + enterPingResult.setString(4, challenge); + enterPingResult.setTimestamp(5, this.date); + // Ping results with current state "failed" need followup action in + // two weeks to revoke any remaining active certificates. + enterPingResult.setBoolean(6, state == PingState.FAILED); + enterPingResult.execute(); + } + } + + public PingState getState() { return state; } diff --git a/src/club/wpia/gigi/dbObjects/Organisation.java b/src/club/wpia/gigi/dbObjects/Organisation.java index 4ee25d0e..c9754565 100644 --- a/src/club/wpia/gigi/dbObjects/Organisation.java +++ b/src/club/wpia/gigi/dbObjects/Organisation.java @@ -68,12 +68,7 @@ public class Organisation extends CertificateOwner { private String postalAddress; public Organisation(String name, Country country, String province, String city, String email, String optionalName, String postalAddress, User creator) throws GigiApiException { - if ( !creator.isInGroup(Group.ORG_AGENT)) { - throw new GigiApiException("Only Organisation RA Agents may create organisations."); - } - if (country == null) { - throw new GigiApiException("Got country code of illegal type."); - } + super(validate(creator, country)); this.name = name; this.country = country; this.province = province; @@ -98,6 +93,16 @@ public class Organisation extends CertificateOwner { } } + private static Void validate(User creator, Country country) throws GigiApiException { + if ( !creator.isInGroup(Group.ORG_AGENT)) { + throw new GigiApiException("Only Organisation RA Agents may create organisations."); + } + if (country == null) { + throw new GigiApiException("Got country code of illegal type."); + } + return null; + } + protected Organisation(GigiResultSet rs) throws GigiApiException { super(rs.getInt("id")); name = rs.getString("name"); diff --git a/src/club/wpia/gigi/dbObjects/User.java b/src/club/wpia/gigi/dbObjects/User.java index cdd00d6f..3c2cd6b0 100644 --- a/src/club/wpia/gigi/dbObjects/User.java +++ b/src/club/wpia/gigi/dbObjects/User.java @@ -105,10 +105,7 @@ public class User extends CertificateOwner { } public User(String email, String password, DayDate dob, Locale locale, Country residenceCountry, NamePart... preferred) throws GigiApiException { - // Avoid storing information that obviously won't get through - if ( !EmailProvider.isValidMailAddress(email)) { - throw new IllegalArgumentException("Invalid email."); - } + super(validate(email)); this.email = email; this.dob = dob; @@ -128,6 +125,14 @@ public class User extends CertificateOwner { new EmailAddress(this, email, locale); } + private static Void validate(String email) { + // Avoid storing information that obviously won't get through + if ( !EmailProvider.isValidMailAddress(email)) { + throw new IllegalArgumentException("Invalid email."); + } + return null; + } + public Name[] getNames() { try (GigiPreparedStatement gps = new GigiPreparedStatement("SELECT `id` FROM `names` WHERE `uid`=? AND `deleted` IS NULL", true)) { gps.setInt(1, getId()); diff --git a/src/club/wpia/gigi/output/template/Template.java b/src/club/wpia/gigi/output/template/Template.java index ad53ac2e..65db37fe 100644 --- a/src/club/wpia/gigi/output/template/Template.java +++ b/src/club/wpia/gigi/output/template/Template.java @@ -264,10 +264,14 @@ public class Template implements Outputable { out.print(((Boolean) s) ? l.getTranslation("yes") : l.getTranslation("no")); } else if (s instanceof Date) { SimpleDateFormat sdfUI = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); - out.print(""); + if (vars.containsKey(Outputable.OUT_KEY_PLAIN)) { + out.print(sdfUI.format(s)); + } else { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + out.print(""); + } } else { out.print(s == null ? "null" : (unescaped ? s.toString() : HTMLEncoder.encodeHTML(s.toString()))); } diff --git a/src/club/wpia/gigi/pages/LoginPage.templ b/src/club/wpia/gigi/pages/LoginPage.templ index f34ffb13..ad3060c5 100644 --- a/src/club/wpia/gigi/pages/LoginPage.templ +++ b/src/club/wpia/gigi/pages/LoginPage.templ @@ -2,7 +2,7 @@


-
+


' for more information?>

diff --git a/src/club/wpia/gigi/pages/PasswordResetForm.templ b/src/club/wpia/gigi/pages/PasswordResetForm.templ index a0e46a4f..9663547b 100644 --- a/src/club/wpia/gigi/pages/PasswordResetForm.templ +++ b/src/club/wpia/gigi/pages/PasswordResetForm.templ @@ -1,7 +1,7 @@ - + @@ -10,18 +10,18 @@ - + - + - +
*: *:
*: *:
*'.?>
diff --git a/src/club/wpia/gigi/pages/account/ChangePasswordForm.templ b/src/club/wpia/gigi/pages/account/ChangePasswordForm.templ index 0fd8f76a..1e9136ea 100644 --- a/src/club/wpia/gigi/pages/account/ChangePasswordForm.templ +++ b/src/club/wpia/gigi/pages/account/ChangePasswordForm.templ @@ -1,27 +1,27 @@ - + - + - + - + - +
: :
*: *:
*: *:
*'.?>
diff --git a/src/club/wpia/gigi/pages/account/certs/CertificateIssueForm.templ b/src/club/wpia/gigi/pages/account/certs/CertificateIssueForm.templ index 7b9c5353..494a7ff5 100644 --- a/src/club/wpia/gigi/pages/account/certs/CertificateIssueForm.templ +++ b/src/club/wpia/gigi/pages/account/certs/CertificateIssueForm.templ @@ -48,8 +48,8 @@ -
- : dns:my.domain.example.com, dns:*.example.com, email:my.email@example.com
+
+ : dns:my.domain.example.org, dns:*.example.org, email:my.email@example.org
diff --git a/src/club/wpia/gigi/pages/account/domain/DomainPinglogForm.java b/src/club/wpia/gigi/pages/account/domain/DomainPinglogForm.java index e752557e..63f82d57 100644 --- a/src/club/wpia/gigi/pages/account/domain/DomainPinglogForm.java +++ b/src/club/wpia/gigi/pages/account/domain/DomainPinglogForm.java @@ -64,7 +64,7 @@ public class DomainPinglogForm extends Form { if (counter >= pings.length) { return false; } - vars.put("state", pings[counter].getState()); + vars.put("state", pings[counter].getState().getDBName()); vars.put("type", pings[counter].getType()); vars.put("config", pings[counter].getInfo()); vars.put("date", pings[counter].getDate()); diff --git a/src/club/wpia/gigi/pages/account/mail/MailAddForm.templ b/src/club/wpia/gigi/pages/account/mail/MailAddForm.templ index c709391a..a8fbcb8d 100644 --- a/src/club/wpia/gigi/pages/account/mail/MailAddForm.templ +++ b/src/club/wpia/gigi/pages/account/mail/MailAddForm.templ @@ -1,6 +1,6 @@ - + diff --git a/src/club/wpia/gigi/pages/admin/support/SupportEnterTicketForm.java b/src/club/wpia/gigi/pages/admin/support/SupportEnterTicketForm.java index 0a98dfd1..58b2997e 100644 --- a/src/club/wpia/gigi/pages/admin/support/SupportEnterTicketForm.java +++ b/src/club/wpia/gigi/pages/admin/support/SupportEnterTicketForm.java @@ -12,11 +12,14 @@ import club.wpia.gigi.output.template.Form; import club.wpia.gigi.output.template.Template; import club.wpia.gigi.pages.LoginPage; import club.wpia.gigi.util.AuthorizationContext; +import club.wpia.gigi.util.CalendarUtil; public class SupportEnterTicketForm extends Form { private static final Template t = new Template(SupportEnterTicketForm.class.getResource("SupportEnterTicketForm.templ")); + public static final String TICKET_PREFIX = "acdhi"; + public SupportEnterTicketForm(HttpServletRequest hsr) { super(hsr); } @@ -24,9 +27,9 @@ public class SupportEnterTicketForm extends Form { @Override public SubmissionResult submit(HttpServletRequest req) throws GigiApiException { if (req.getParameter("setTicket") != null) { - // [asdmASDM]\d{8}\.\d+ - String ticket = req.getParameter("ticketno"); - if (ticket.matches("[asdmASDM]\\d{8}\\.\\d+")) { + // [acdhi]\d{8}\.\d+ according to numbering scheme + String ticket = req.getParameter("ticketno").toLowerCase(); + if (ticket.matches("[" + TICKET_PREFIX + "]\\d{8}\\.\\d+") && CalendarUtil.isDateValid(ticket.substring(1, 9))) { AuthorizationContext ac = LoginPage.getAuthorizationContext(req); req.getSession().setAttribute(Gigi.AUTH_CONTEXT, new AuthorizationContext(ac.getActor(), ticket)); return new RedirectResult(SupportEnterTicketPage.PATH); diff --git a/src/club/wpia/gigi/pages/main/Signup.java b/src/club/wpia/gigi/pages/main/Signup.java index 07d7f912..d9b2cc1f 100644 --- a/src/club/wpia/gigi/pages/main/Signup.java +++ b/src/club/wpia/gigi/pages/main/Signup.java @@ -37,8 +37,6 @@ public class Signup extends Form { private static final Template t = new Template(Signup.class.getResource("Signup.templ")); - private boolean general = true, country = true, regional = true, radius = true; - private CountrySelector cs; public Signup(HttpServletRequest hsr) { @@ -55,10 +53,6 @@ public class Signup extends Form { vars.put("name", ni); vars.put("dob", myDoB); vars.put("email", HTMLEncoder.encodeHTML(email)); - vars.put("general", general ? " checked=\"checked\"" : ""); - vars.put("country", country ? " checked=\"checked\"" : ""); - vars.put("regional", regional ? " checked=\"checked\"" : ""); - vars.put("radius", radius ? " checked=\"checked\"" : ""); vars.put("helpOnNames", new SprintfCommand("Help on Names {0}in the wiki{1}", Arrays.asList("!(/wiki/names", "!'"))); vars.put("csrf", getCSRFToken()); vars.put("dobmin", User.MINIMUM_AGE + ""); @@ -70,10 +64,6 @@ public class Signup extends Form { if (r.getParameter("email") != null) { email = r.getParameter("email"); } - general = "1".equals(r.getParameter("general")); - country = "1".equals(r.getParameter("country")); - regional = "1".equals(r.getParameter("regional")); - radius = "1".equals(r.getParameter("radius")); GigiApiException problems = new GigiApiException(); try { ni.update(r); @@ -133,13 +123,13 @@ public class Signup extends Form { String pw1 = req.getParameter("pword1"); String pw2 = req.getParameter("pword2"); if (pw1 == null || pw1.equals("")) { - ga.mergeInto(new GigiApiException("Pass Phrases were blank")); + ga.mergeInto(new GigiApiException("Passwords were blank")); } else if ( !pw1.equals(pw2)) { - ga.mergeInto(new GigiApiException("Pass Phrases don't match")); + ga.mergeInto(new GigiApiException("Passwords don't match")); } int pwpoints = PasswordStrengthChecker.checkpw(pw1, ni.getNamePartsPlain(), email); if (pwpoints < 3) { - ga.mergeInto(new GigiApiException(new SprintfCommand("The Pass Phrase you submitted failed to contain enough differing characters and/or contained words from your name and/or email address. For the current requirements and to learn more, visit our {0}FAQ{1}.", Arrays.asList("!(/wiki/goodPassword", "!''")))); + ga.mergeInto(new GigiApiException(new SprintfCommand("The Password you submitted failed to contain enough differing characters and/or contained words from your name and/or email address. For the current requirements and to learn more, visit our {0}FAQ{1}.", Arrays.asList("!(/wiki/goodPassword", "!''")))); } if ( !ga.isEmpty()) { throw ga; @@ -190,15 +180,6 @@ public class Signup extends Form { private void run(HttpServletRequest req, String password) throws GigiApiException { User u = new User(email, password, myDoB.getDate(), Page.getLanguage(req).getLocale(), cs.getCountry(), ni.getNameParts()); - - try (GigiPreparedStatement ps = new GigiPreparedStatement("INSERT INTO `alerts` SET `memid`=?," + " `general`=?, `country`=?, `regional`=?, `radius`=?")) { - ps.setInt(1, u.getId()); - ps.setBoolean(2, general); - ps.setBoolean(3, country); - ps.setBoolean(4, regional); - ps.setBoolean(5, radius); - ps.execute(); - } Notary.writeUserAgreement(u, "ToS", "account creation", "", true, 0); } diff --git a/src/club/wpia/gigi/pages/main/Signup.templ b/src/club/wpia/gigi/pages/main/Signup.templ index 466eaaa3..bc602e78 100644 --- a/src/club/wpia/gigi/pages/main/Signup.templ +++ b/src/club/wpia/gigi/pages/main/Signup.templ @@ -32,39 +32,27 @@ - + - + - - - - - - + - - - diff --git a/src/club/wpia/gigi/ping/DNSPinger.java b/src/club/wpia/gigi/ping/DNSPinger.java index aeb41e5f..a7c74bd2 100644 --- a/src/club/wpia/gigi/ping/DNSPinger.java +++ b/src/club/wpia/gigi/ping/DNSPinger.java @@ -7,20 +7,21 @@ import javax.naming.NamingException; import club.wpia.gigi.dbObjects.CertificateOwner; import club.wpia.gigi.dbObjects.Domain; +import club.wpia.gigi.dbObjects.DomainPingConfiguration; +import club.wpia.gigi.dbObjects.DomainPingExecution; import club.wpia.gigi.util.DNSUtil; import club.wpia.gigi.util.SystemKeywords; public class DNSPinger extends DomainPinger { @Override - public void ping(Domain domain, String expToken, CertificateOwner u, int confId) { + public DomainPingExecution ping(Domain domain, String expToken, CertificateOwner u, DomainPingConfiguration conf) { String[] tokenParts = expToken.split(":", 2); List nameservers; try { nameservers = Arrays.asList(DNSUtil.getNSNames(domain.getSuffix())); } catch (NamingException e) { - enterPingResult(confId, "error", "No authorative nameserver found.", null); - return; + return enterPingResult(conf, "error", "No authorative nameserver found.", null); } StringBuffer result = new StringBuffer(); result.append("failed: "); @@ -51,9 +52,9 @@ public class DNSPinger extends DomainPinger { } if ( !failed) { - enterPingResult(confId, PING_SUCCEDED, "", null); + return enterPingResult(conf, PING_SUCCEDED, "", null); } else { - enterPingResult(confId, "error", result.toString(), null); + return enterPingResult(conf, "error", result.toString(), null); } } } diff --git a/src/club/wpia/gigi/ping/DomainPinger.java b/src/club/wpia/gigi/ping/DomainPinger.java index fcb8f12e..7b1d404c 100644 --- a/src/club/wpia/gigi/ping/DomainPinger.java +++ b/src/club/wpia/gigi/ping/DomainPinger.java @@ -4,6 +4,8 @@ import club.wpia.gigi.database.DBEnum; import club.wpia.gigi.database.GigiPreparedStatement; import club.wpia.gigi.dbObjects.CertificateOwner; import club.wpia.gigi.dbObjects.Domain; +import club.wpia.gigi.dbObjects.DomainPingConfiguration; +import club.wpia.gigi.dbObjects.DomainPingExecution; public abstract class DomainPinger { @@ -20,24 +22,18 @@ public abstract class DomainPinger { public static final String PING_SUCCEDED = ""; - public abstract void ping(Domain domain, String configuration, CertificateOwner target, int confId); + public abstract DomainPingExecution ping(Domain domain, String configuration, CertificateOwner target, DomainPingConfiguration conf); - protected static void enterPingResult(int configId, String state, String result, String token) { + protected static DomainPingExecution enterPingResult(DomainPingConfiguration config, String state, String result, String token) { PingState estate = DomainPinger.PING_STILL_PENDING == state ? PingState.OPEN : DomainPinger.PING_SUCCEDED.equals(state) ? PingState.SUCCESS : PingState.FAILED; - try (GigiPreparedStatement enterPingResult = new GigiPreparedStatement("INSERT INTO `domainPinglog` SET `configId`=?, `state`=?::`pingState`, `result`=?, `challenge`=?")) { - enterPingResult.setInt(1, configId); - enterPingResult.setEnum(2, estate); - enterPingResult.setString(3, result); - enterPingResult.setString(4, token); - enterPingResult.execute(); - } + return new DomainPingExecution(estate, result, config, token); } - protected static void updatePingResult(int configId, String state, String result, String token) { + protected static void updatePingResult(DomainPingConfiguration config, String state, String result, String token) { try (GigiPreparedStatement updatePingResult = new GigiPreparedStatement("UPDATE `domainPinglog` SET `state`=?::`pingState`, `result`=? WHERE `configId`=? AND `challenge`=?")) { updatePingResult.setString(1, DomainPinger.PING_STILL_PENDING == state ? "open" : DomainPinger.PING_SUCCEDED.equals(state) ? "success" : "failed"); updatePingResult.setString(2, result); - updatePingResult.setInt(3, configId); + updatePingResult.setInt(3, config.getId()); updatePingResult.setString(4, token); updatePingResult.execute(); } diff --git a/src/club/wpia/gigi/ping/EmailPinger.java b/src/club/wpia/gigi/ping/EmailPinger.java index 454ba81d..7468f94a 100644 --- a/src/club/wpia/gigi/ping/EmailPinger.java +++ b/src/club/wpia/gigi/ping/EmailPinger.java @@ -5,6 +5,8 @@ import java.util.Locale; import club.wpia.gigi.dbObjects.CertificateOwner; import club.wpia.gigi.dbObjects.Domain; +import club.wpia.gigi.dbObjects.DomainPingConfiguration; +import club.wpia.gigi.dbObjects.DomainPingExecution; import club.wpia.gigi.dbObjects.User; import club.wpia.gigi.email.MailProbe; import club.wpia.gigi.localisation.Language; @@ -13,11 +15,11 @@ import club.wpia.gigi.util.RandomToken; public class EmailPinger extends DomainPinger { @Override - public void ping(Domain domain, String configuration, CertificateOwner u, int confId) { + public DomainPingExecution ping(Domain domain, String configuration, CertificateOwner u, DomainPingConfiguration conf) { String mail = configuration + "@" + domain.getSuffix(); String token = RandomToken.generateToken(16); + DomainPingExecution r = enterPingResult(conf, PING_STILL_PENDING, "", token); try { - enterPingResult(confId, PING_STILL_PENDING, "", token); Locale l = Locale.ENGLISH; if (u instanceof User) { l = ((User) u).getPreferredLocale(); @@ -26,8 +28,9 @@ public class EmailPinger extends DomainPinger { MailProbe.sendMailProbe(Language.getInstance(l), "domain", domain.getId(), token, mail); } catch (IOException e) { e.printStackTrace(); - updatePingResult(confId, "error", "Mail connection interrupted", token); + updatePingResult(conf, "error", "Mail connection interrupted", token); } + return r; } } diff --git a/src/club/wpia/gigi/ping/HTTPFetch.java b/src/club/wpia/gigi/ping/HTTPFetch.java index 7ad08b55..61f8b467 100644 --- a/src/club/wpia/gigi/ping/HTTPFetch.java +++ b/src/club/wpia/gigi/ping/HTTPFetch.java @@ -8,36 +8,33 @@ import java.net.URL; import club.wpia.gigi.dbObjects.CertificateOwner; import club.wpia.gigi.dbObjects.Domain; +import club.wpia.gigi.dbObjects.DomainPingConfiguration; +import club.wpia.gigi.dbObjects.DomainPingExecution; import club.wpia.gigi.util.SystemKeywords; public class HTTPFetch extends DomainPinger { @Override - public void ping(Domain domain, String expToken, CertificateOwner user, int confId) { + public DomainPingExecution ping(Domain domain, String expToken, CertificateOwner user, DomainPingConfiguration conf) { try { String[] tokenParts = expToken.split(":", 2); URL u = new URL("http://" + domain.getSuffix() + "/" + SystemKeywords.HTTP_CHALLENGE_PREFIX + tokenParts[0] + ".txt"); HttpURLConnection huc = (HttpURLConnection) u.openConnection(); if (huc.getResponseCode() != 200) { - enterPingResult(confId, "error", "Invalid status code " + huc.getResponseCode() + ".", null); - return; + return enterPingResult(conf, "error", "Invalid status code " + huc.getResponseCode() + ".", null); } BufferedReader br = new BufferedReader(new InputStreamReader(huc.getInputStream(), "UTF-8")); String line = br.readLine(); if (line == null) { - enterPingResult(confId, "error", "Empty document.", null); - return; + return enterPingResult(conf, "error", "Empty document.", null); } if (line.trim().equals(tokenParts[1])) { - enterPingResult(confId, PING_SUCCEDED, "", null); - return; + return enterPingResult(conf, PING_SUCCEDED, "", null); } - enterPingResult(confId, "error", "Challenge tokens differed.", null); - return; + return enterPingResult(conf, "error", "Challenge tokens differed.", null); } catch (IOException e) { e.printStackTrace(); - enterPingResult(confId, "error", "Exception: connection closed.", null); - return; + return enterPingResult(conf, "error", "Exception: connection closed.", null); } } } diff --git a/src/club/wpia/gigi/ping/PingFailedWithActiveCertificates.templ b/src/club/wpia/gigi/ping/PingFailedWithActiveCertificates.templ new file mode 100644 index 00000000..2ff34d7c --- /dev/null +++ b/src/club/wpia/gigi/ping/PingFailedWithActiveCertificates.templ @@ -0,0 +1,20 @@ +Subject: + + + + \ + \ + + + + \ + + + + + +- + + + + diff --git a/src/club/wpia/gigi/ping/PingerDaemon.java b/src/club/wpia/gigi/ping/PingerDaemon.java index d6016c67..895e8c83 100644 --- a/src/club/wpia/gigi/ping/PingerDaemon.java +++ b/src/club/wpia/gigi/ping/PingerDaemon.java @@ -1,17 +1,38 @@ package club.wpia.gigi.ping; +import java.io.IOException; +import java.security.GeneralSecurityException; import java.security.KeyStore; +import java.security.cert.X509Certificate; +import java.sql.Timestamp; +import java.util.Date; import java.util.HashMap; import java.util.LinkedList; +import java.util.Locale; +import java.util.Map; import java.util.Queue; +import club.wpia.gigi.GigiApiException; import club.wpia.gigi.database.DatabaseConnection; import club.wpia.gigi.database.DatabaseConnection.Link; import club.wpia.gigi.database.GigiPreparedStatement; import club.wpia.gigi.database.GigiResultSet; +import club.wpia.gigi.dbObjects.Certificate; +import club.wpia.gigi.dbObjects.Certificate.RevocationType; +import club.wpia.gigi.dbObjects.CertificateOwner; import club.wpia.gigi.dbObjects.Domain; import club.wpia.gigi.dbObjects.DomainPingConfiguration; +import club.wpia.gigi.dbObjects.DomainPingExecution; import club.wpia.gigi.dbObjects.DomainPingType; +import club.wpia.gigi.dbObjects.Organisation; +import club.wpia.gigi.dbObjects.User; +import club.wpia.gigi.localisation.Language; +import club.wpia.gigi.output.ArrayIterable; +import club.wpia.gigi.output.template.MailTemplate; +import club.wpia.gigi.pages.account.domain.EditDomain; +import club.wpia.gigi.ping.DomainPinger.PingState; +import club.wpia.gigi.util.ServerConstants; +import club.wpia.gigi.util.ServerConstants.Host; public class PingerDaemon extends Thread { @@ -21,6 +42,8 @@ public class PingerDaemon extends Thread { private Queue toExecute = new LinkedList<>(); + private final MailTemplate pingFailedMail = new MailTemplate(PingerDaemon.class.getResource("PingFailedWithActiveCertificates.templ")); + public PingerDaemon(KeyStore truststore) { this.truststore = truststore; } @@ -36,10 +59,7 @@ public class PingerDaemon extends Thread { public void runWithConnection() { - pingers.put(DomainPingType.EMAIL, new EmailPinger()); - pingers.put(DomainPingType.SSL, new SSLPinger(truststore)); - pingers.put(DomainPingType.HTTP, new HTTPFetch()); - pingers.put(DomainPingType.DNS, new DNSPinger()); + initializeConnectionUsage(); while (true) { try { @@ -53,22 +73,8 @@ public class PingerDaemon extends Thread { } notifyAll(); } - try (GigiPreparedStatement searchNeededPings = new GigiPreparedStatement("SELECT `pc`.`id`" // - + " FROM `pingconfig` AS `pc`" // - + " INNER JOIN `domains` AS `d` ON `d`.`id` = `pc`.`domainid`" // - + " WHERE `d`.`deleted` IS NULL" // - + " AND `pc`.`deleted` IS NULL" // - + " AND NOT EXISTS (" // - + " SELECT 1 FROM `domainPinglog` AS `dpl`" // - + " WHERE `dpl`.`configId` = `pc`.`id`" // - + " AND `dpl`.`when` >= CURRENT_TIMESTAMP - interval '6 mons')")) { - - GigiResultSet rs = searchNeededPings.executeQuery(); - while (rs.next()) { - worked = true; - handle(DomainPingConfiguration.getById(rs.getInt("id"))); - } - } + long time = System.currentTimeMillis(); + worked |= executeNeededPings(new Date(time)); try { if ( !worked) { Thread.sleep(5000); @@ -85,7 +91,90 @@ public class PingerDaemon extends Thread { } } - private void handle(DomainPingConfiguration conf) { + protected void initializeConnectionUsage() { + pingers.put(DomainPingType.EMAIL, new EmailPinger()); + pingers.put(DomainPingType.SSL, new SSLPinger(truststore)); + pingers.put(DomainPingType.HTTP, new HTTPFetch()); + pingers.put(DomainPingType.DNS, new DNSPinger()); + } + + public synchronized boolean executeNeededPings(Date time) { + boolean worked = false; + try (GigiPreparedStatement searchNeededPings = new GigiPreparedStatement("SELECT `d`.`id`, `dpl`.`configId`, `dpl`.`when`," // + // .. for all found pings we want to know, if we do not have a more + // recent successful ping + + " NOT EXISTS (" // + + " SELECT 1 FROM `domainPinglog` AS `dpl2`" // + + " WHERE `dpl`.`configId` = `dpl2`.`configId`" // + + " AND `dpl2`.state = 'success' AND `dpl2`.`when` > `dpl`.`when`) AS `resucceeded`" // + + " FROM `domainPinglog` AS `dpl`" // + // We search valid pings + + " INNER JOIN `pingconfig` AS `pc` ON `pc`.`id` = `dpl`.`configId` AND `pc`.`deleted` IS NULL" // + + " INNER JOIN `domains` AS `d` ON `d`.`id` = `pc`.`domainid` AND `d`.`deleted` IS NULL" // + // .. that failed, .. + + " WHERE `dpl`.`state` = 'failed'" // + // .. are older than 2 weeks + + " AND `dpl`.`when` <= ?::timestamp - interval '2 weeks'" // + // .. and are flagged for corrective action + + " AND `dpl`.`needsAction`" // + )) { + searchNeededPings.setTimestamp(1, new Timestamp(time.getTime())); + GigiResultSet rs = searchNeededPings.executeQuery(); + try (GigiPreparedStatement updateDone = new GigiPreparedStatement("UPDATE `domainPinglog` SET `needsAction`=false WHERE `configId`=? AND `when`=?")) { + while (rs.next()) { + worked = true; + // Give this ping a last chance to succeed. + handle(DomainPingConfiguration.getById(rs.getInt(2))); + // We only consider revoking if this ping has not been + // superseded by a following successful ping. + if (rs.getBoolean(4)) { + Domain d = Domain.getById(rs.getInt(1)); + int ct = 0; + boolean[] used = new boolean[DomainPingType.values().length]; + // We only revoke, there are not 2 pings that are not + // 'strictly invalid' + for (DomainPingConfiguration cfg : d.getConfiguredPings()) { + if ( !cfg.isStrictlyInvalid(time) && !used[cfg.getType().ordinal()]) { + ct++; + used[cfg.getType().ordinal()] = true; + } + if (ct >= 2) { + break; + } + } + if (ct < 2) { + for (Certificate c : d.fetchActiveCertificates()) { + // TODO notify user + c.revoke(RevocationType.PING_TIMEOUT); + } + } + } + updateDone.setInt(1, rs.getInt(2)); + updateDone.setTimestamp(2, rs.getTimestamp(3)); + updateDone.executeUpdate(); + } + } + } + try (GigiPreparedStatement searchNeededPings = new GigiPreparedStatement("SELECT `pc`.`id`" // + + " FROM `pingconfig` AS `pc`" // + + " INNER JOIN `domains` AS `d` ON `d`.`id` = `pc`.`domainid`" // + + " WHERE `d`.`deleted` IS NULL" // + + " AND `pc`.`deleted` IS NULL" // + + " AND NOT EXISTS (" // + + " SELECT 1 FROM `domainPinglog` AS `dpl`" // + + " WHERE `dpl`.`configId` = `pc`.`id`" // + + " AND `dpl`.`when` >= ?::timestamp - interval '6 mons')")) { + searchNeededPings.setTimestamp(1, new Timestamp(time.getTime())); + GigiResultSet rs = searchNeededPings.executeQuery(); + while (rs.next()) { + worked = true; + handle(DomainPingConfiguration.getById(rs.getInt("id"))); + } + } + return worked; + } + + protected void handle(DomainPingConfiguration conf) { DomainPingType type = conf.getType(); String config = conf.getInfo(); DomainPinger dp = pingers.get(type); @@ -93,10 +182,53 @@ public class PingerDaemon extends Thread { Domain target = conf.getTarget(); System.err.println("Executing " + dp + " on " + target + " (" + System.currentTimeMillis() + ")"); try { - dp.ping(target, config, target.getOwner(), conf.getId()); + DomainPingExecution x = dp.ping(target, config, target.getOwner(), conf); + if (x.getState() == PingState.FAILED) { + Certificate[] cs = target.fetchActiveCertificates(); + if (cs.length != 0) { + CertificateOwner o = target.getOwner(); + Locale l = Locale.ENGLISH; + String contact; + if (o instanceof User) { + l = ((User) o).getPreferredLocale(); + contact = ((User) o).getEmail(); + } else if (o instanceof Organisation) { + contact = ((Organisation) o).getContactEmail(); + + } else { + throw new Error(); + } + HashMap vars = new HashMap<>(); + vars.put("valid", target.isVerified()); + vars.put("domain", target.getSuffix()); + vars.put("domainLink", "https://" + ServerConstants.getHostNamePortSecure(Host.WWW) + "/" + EditDomain.PATH + target.getId()); + vars.put("certs", new ArrayIterable(cs) { + + @Override + public void apply(Certificate t, Language l, Map vars) { + vars.put("serial", t.getSerial()); + vars.put("ca", t.getParent().getKeyname()); + try { + X509Certificate c = t.cert(); + vars.put("from", c.getNotBefore()); + vars.put("to", c.getNotAfter()); + } catch (IOException e) { + e.printStackTrace(); + } catch (GeneralSecurityException e) { + e.printStackTrace(); + } catch (GigiApiException e) { + e.printStackTrace(); + } + } + + }); + pingFailedMail.sendMail(Language.getInstance(l), vars, contact); + System.out.println("Ping failed with active certificates"); + } + } } catch (Throwable t) { t.printStackTrace(); - DomainPinger.enterPingResult(conf.getId(), "error", "exception", null); + DomainPinger.enterPingResult(conf, "error", "exception", null); } System.err.println("done (" + System.currentTimeMillis() + ")"); } diff --git a/src/club/wpia/gigi/ping/SSLPinger.java b/src/club/wpia/gigi/ping/SSLPinger.java index db0f0f52..97fb30da 100644 --- a/src/club/wpia/gigi/ping/SSLPinger.java +++ b/src/club/wpia/gigi/ping/SSLPinger.java @@ -33,6 +33,8 @@ import club.wpia.gigi.dbObjects.CACertificate; import club.wpia.gigi.dbObjects.Certificate; import club.wpia.gigi.dbObjects.CertificateOwner; import club.wpia.gigi.dbObjects.Domain; +import club.wpia.gigi.dbObjects.DomainPingConfiguration; +import club.wpia.gigi.dbObjects.DomainPingExecution; import sun.security.x509.AVA; import sun.security.x509.X500Name; @@ -51,7 +53,7 @@ public class SSLPinger extends DomainPinger { } @Override - public void ping(Domain domain, String configuration, CertificateOwner u, int confId) { + public DomainPingExecution ping(Domain domain, String configuration, CertificateOwner u, DomainPingConfiguration conf) { try (SocketChannel sch = SocketChannel.open()) { sch.socket().setSoTimeout(5000); String[] parts = configuration.split(":", 4); @@ -76,11 +78,9 @@ public class SSLPinger extends DomainPinger { String key = parts[0]; String value = parts[1]; String res = test(sch, domain.getSuffix(), u, value); - enterPingResult(confId, res, res, null); - return; + return enterPingResult(conf, res, res, null); } catch (IOException e) { - enterPingResult(confId, "error", "connection Failed", null); - return; + return enterPingResult(conf, "error", "connection Failed", null); } } diff --git a/src/club/wpia/gigi/util/CalendarUtil.java b/src/club/wpia/gigi/util/CalendarUtil.java index c7b2bb0f..9c4cf70a 100644 --- a/src/club/wpia/gigi/util/CalendarUtil.java +++ b/src/club/wpia/gigi/util/CalendarUtil.java @@ -15,6 +15,17 @@ public class CalendarUtil { } + /** + * @param date + * YYYYMMDD + */ + public static boolean isDateValid(String date) { + int year = Integer.parseInt(date.substring(0, 4)); + int month = Integer.parseInt(date.substring(4, 6)); + int day = Integer.parseInt(date.substring(6, 8)); + return isDateValid(year, month, day); + } + public static boolean isOfAge(DayDate dob, int age) { return isYearsInFuture(dob.start(), age); } diff --git a/src/club/wpia/gigi/util/PasswordStrengthChecker.java b/src/club/wpia/gigi/util/PasswordStrengthChecker.java index 0df62480..112f7128 100644 --- a/src/club/wpia/gigi/util/PasswordStrengthChecker.java +++ b/src/club/wpia/gigi/util/PasswordStrengthChecker.java @@ -80,7 +80,7 @@ public class PasswordStrengthChecker { } } if (checkpw(pw, parts.toArray(new String[parts.size()]), email) < 3) { - throw (new GigiApiException(new SprintfCommand("The Pass Phrase you submitted failed to contain enough differing characters and/or contained words from your name and/or email address. For the current requirements and to learn more, visit our {0}FAQ{1}.", Arrays.asList("!(/wiki/goodPassword", "!''")))); + throw (new GigiApiException(new SprintfCommand("The Password you submitted failed to contain enough differing characters and/or contained words from your name and/or email address. For the current requirements and to learn more, visit our {0}FAQ{1}.", Arrays.asList("!(/wiki/goodPassword", "!''")))); } } diff --git a/tests/club/wpia/gigi/pages/admin/TestSEAdminTicketSetting.java b/tests/club/wpia/gigi/pages/admin/TestSEAdminTicketSetting.java index 59f04279..e20b4944 100644 --- a/tests/club/wpia/gigi/pages/admin/TestSEAdminTicketSetting.java +++ b/tests/club/wpia/gigi/pages/admin/TestSEAdminTicketSetting.java @@ -1,10 +1,13 @@ package club.wpia.gigi.pages.admin; +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.*; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; +import java.util.Random; import org.junit.Test; @@ -12,8 +15,10 @@ import club.wpia.gigi.GigiApiException; import club.wpia.gigi.dbObjects.Group; import club.wpia.gigi.pages.admin.support.FindUserByDomainPage; import club.wpia.gigi.pages.admin.support.FindUserByEmailPage; +import club.wpia.gigi.pages.admin.support.SupportEnterTicketForm; import club.wpia.gigi.pages.admin.support.SupportEnterTicketPage; import club.wpia.gigi.testUtils.ClientTest; +import club.wpia.gigi.testUtils.IOUtils; public class TestSEAdminTicketSetting extends ClientTest { @@ -32,4 +37,66 @@ public class TestSEAdminTicketSetting extends ClientTest { assertEquals(403, get(FindUserByEmailPage.PATH).getResponseCode()); } + @Test + public void testSetTicketNumberCharacter() throws MalformedURLException, UnsupportedEncodingException, IOException { + String ticket; + String alphabet = "abcdefghijklmnopqrstuvwxyz"; + + // test allowed character + for (char ch : SupportEnterTicketForm.TICKET_PREFIX.toCharArray()) { + ticket = ch + "20171212.1"; + assertEquals(302, post(cookie, SupportEnterTicketPage.PATH, "ticketno=" + ticket + "&setTicket=action", 0).getResponseCode()); + ticket = Character.toUpperCase(ch) + "20171212.1"; + assertEquals(302, post(cookie, SupportEnterTicketPage.PATH, "ticketno=" + ticket + "&setTicket=action", 0).getResponseCode()); + alphabet = alphabet.replaceAll(Character.toString(ch), ""); + } + + // test not allowed character + Random rnd = new Random(); + char ch = alphabet.charAt(rnd.nextInt(alphabet.length())); + assertWrongTicketNumber(ch + "20171212.1"); + } + + @Test + public void testSetTicketNumberDatepart() throws MalformedURLException, UnsupportedEncodingException, IOException { + char ch = getValidCharacter(); + + assertWrongTicketNumber(ch + "220171212.1"); + + assertWrongTicketNumber(ch + "0171212.1"); + + assertWrongTicketNumber(ch + "20171512.1"); + + assertWrongTicketNumber(ch + "20170229.1"); + + assertWrongTicketNumber(ch + ch + "20171212.1"); + + assertWrongTicketNumber("20171212.1"); + + assertWrongTicketNumber(ch + "20171212" + ch + ".1"); + + assertWrongTicketNumber(ch + "201721" + ch + "21.1"); + } + + @Test + public void testSetTicketNumberNumberpart() throws MalformedURLException, UnsupportedEncodingException, IOException { + char ch = getValidCharacter(); + + assertWrongTicketNumber(ch + "20171212."); + + assertWrongTicketNumber(ch + "20171212"); + + assertWrongTicketNumber(ch + "20171212.1" + ch); + + } + + private char getValidCharacter() { + Random rnd = new Random(); + return SupportEnterTicketForm.TICKET_PREFIX.charAt(rnd.nextInt(SupportEnterTicketForm.TICKET_PREFIX.length())); + } + + private void assertWrongTicketNumber(String ticket) throws IOException { + String res = IOUtils.readURL(post(SupportEnterTicketPage.PATH, "ticketno=" + ticket + "&setTicket=action")); + assertThat(res, containsString("Ticket format malformed")); + } } diff --git a/tests/club/wpia/gigi/pages/main/RegisterPageTest.java b/tests/club/wpia/gigi/pages/main/RegisterPageTest.java index d9de3a91..85e34b92 100644 --- a/tests/club/wpia/gigi/pages/main/RegisterPageTest.java +++ b/tests/club/wpia/gigi/pages/main/RegisterPageTest.java @@ -49,14 +49,14 @@ public class RegisterPageTest extends ManagedTest { String defaultSignup = "fname=" + URLEncoder.encode("ab", "UTF-8") + "&lname=" + URLEncoder.encode("b", "UTF-8") + "&pword1=" + URLEncoder.encode(TEST_PASSWORD, "UTF-8") + "&pword2=" + URLEncoder.encode(TEST_PASSWORD, "UTF-8") + "&day=1&month=1&year=1910&tos_agree=1&mname=mn&suffix=sf&email="; - String query = defaultSignup + URLEncoder.encode("correct3_" + uniq + "@email.de", "UTF-8") + "&general=1&country=1®ional=1&radius=1&name-type=western"; + String query = defaultSignup + URLEncoder.encode("correct3_" + uniq + "@email.de", "UTF-8") + "&name-type=western"; String data = fetchStartErrorMessage(runRegister(query)); assertNull(data); assertSuccessfullRegMail("correct3_" + uniq + "@email.de"); getMailReceiver().setEmailCheckError("400 Greylisted"); getMailReceiver().setApproveRegex(Pattern.compile("a")); - query = defaultSignup + URLEncoder.encode("correct4_" + uniq + "@email.de", "UTF-8") + "&general=1&country=1®ional=1&radius=1"; + query = defaultSignup + URLEncoder.encode("correct4_" + uniq + "@email.de", "UTF-8"); data = fetchStartErrorMessage(runRegister(query)); assertNotNull(data); @@ -170,20 +170,6 @@ public class RegisterPageTest extends ManagedTest { assertThat(run, containsString("")); } - @Test - public void testCheckboxesStay() throws IOException { - String run2 = runRegister("general=1&country=a®ional=1&radius=0"); - assertThat(run2, containsString("name=\"general\" value=\"1\" checked=\"checked\">")); - assertThat(run2, containsString("name=\"country\" value=\"1\">")); - assertThat(run2, containsString("name=\"regional\" value=\"1\" checked=\"checked\">")); - assertThat(run2, containsString("name=\"radius\" value=\"1\">")); - run2 = runRegister("general=0&country=1&radius=1"); - assertThat(run2, containsString("name=\"general\" value=\"1\">")); - assertThat(run2, containsString("name=\"country\" value=\"1\" checked=\"checked\">")); - assertThat(run2, containsString("name=\"regional\" value=\"1\">")); - assertThat(run2, containsString("name=\"radius\" value=\"1\" checked=\"checked\">")); - } - @Test public void testDoubleMail() throws IOException { long uniq = System.currentTimeMillis(); @@ -221,7 +207,7 @@ public class RegisterPageTest extends ManagedTest { String defaultSignup = "fname=" + URLEncoder.encode("ab", "UTF-8") + "&lname=" + URLEncoder.encode("b", "UTF-8") + "&pword1=" + URLEncoder.encode(TEST_PASSWORD, "UTF-8") + "&pword2=" + URLEncoder.encode(TEST_PASSWORD, "UTF-8") + "&day=1&month=1&year=1910&tos_agree=1&mname=mn&suffix=sf&email="; - String query = defaultSignup + URLEncoder.encode(email, "UTF-8") + "&general=1&country=1®ional=1&radius=1&name-type=western&residenceCountry=DE"; + String query = defaultSignup + URLEncoder.encode(email, "UTF-8") + "&name-type=western&residenceCountry=DE"; String data = fetchStartErrorMessage(runRegister(query)); assertNull(data); User u = User.getByEmail(email); @@ -236,7 +222,7 @@ public class RegisterPageTest extends ManagedTest { String defaultSignup = "fname=" + URLEncoder.encode("ab", "UTF-8") + "&lname=" + URLEncoder.encode("b", "UTF-8") + "&pword1=" + URLEncoder.encode(TEST_PASSWORD, "UTF-8") + "&pword2=" + URLEncoder.encode(TEST_PASSWORD, "UTF-8") + "&day=1&month=1&year=1910&tos_agree=1&mname=mn&suffix=sf&email="; - String query = defaultSignup + URLEncoder.encode(email, "UTF-8") + "&general=1&country=1®ional=1&radius=1&name-type=western&residenceCountry=invalid"; + String query = defaultSignup + URLEncoder.encode(email, "UTF-8") + "&name-type=western&residenceCountry=invalid"; String data = fetchStartErrorMessage(runRegister(query)); assertNull(data); User u = User.getByEmail(email); diff --git a/tests/club/wpia/gigi/ping/TestTiming.java b/tests/club/wpia/gigi/ping/TestTiming.java new file mode 100644 index 00000000..f2b6fe10 --- /dev/null +++ b/tests/club/wpia/gigi/ping/TestTiming.java @@ -0,0 +1,164 @@ +package club.wpia.gigi.ping; + +import static org.junit.Assert.*; +import static org.junit.Assume.*; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.util.Date; +import java.util.List; + +import org.hamcrest.CoreMatchers; +import org.junit.Test; + +import club.wpia.gigi.GigiApiException; +import club.wpia.gigi.dbObjects.Certificate; +import club.wpia.gigi.dbObjects.Certificate.CSRType; +import club.wpia.gigi.dbObjects.Certificate.CertificateStatus; +import club.wpia.gigi.dbObjects.Certificate.SANType; +import club.wpia.gigi.dbObjects.CertificateProfile; +import club.wpia.gigi.dbObjects.Digest; +import club.wpia.gigi.dbObjects.Domain; +import club.wpia.gigi.dbObjects.DomainPingConfiguration; +import club.wpia.gigi.dbObjects.DomainPingExecution; +import club.wpia.gigi.dbObjects.DomainPingType; +import club.wpia.gigi.ping.DomainPinger.PingState; +import club.wpia.gigi.testUtils.PingTest; +import club.wpia.gigi.testUtils.TestEmailReceiver.TestMail; +import club.wpia.gigi.util.RandomToken; +import club.wpia.gigi.util.SimpleSigner; + +public class TestTiming extends PingTest { + + @Test + public void httpAndMailSuccessCert() throws GigiApiException, IOException, InterruptedException, GeneralSecurityException { + httpAndMailSuccess(true, false); + } + + @Test + public void httpAndMailSuccessCertAndCorrect() throws GigiApiException, IOException, InterruptedException, GeneralSecurityException { + httpAndMailSuccess(true, true); + } + + @Test + public void httpAndMailSuccessNoCert() throws GigiApiException, IOException, InterruptedException, GeneralSecurityException { + httpAndMailSuccess(false, false); + } + + public void httpAndMailSuccess(boolean certs, boolean correct) throws GigiApiException, IOException, InterruptedException, GeneralSecurityException { + String test = getTestProps().getProperty("domain.http"); + assumeNotNull(test); + + // When we have a domain. + Domain d = new Domain(u, u, test); + String token = RandomToken.generateToken(16); + String value = RandomToken.generateToken(16); + + // If we run the sub case that we have certificates on the domain, + // create a certificate now. + Certificate c = null; + if (certs) { + KeyPair kp = generateKeypair(); + String key = generatePEMCSR(kp, "CN=testmail@example.com"); + c = new Certificate(u, u, Certificate.buildDN("CN", "testmail@example.com"), Digest.SHA256, key, CSRType.CSR, CertificateProfile.getByName("server"), new Certificate.SubjectAlternateName(SANType.DNS, test)); + await(c.issue(null, "2y", u)); + } + + // Register HTTP and Email pings. + updateService(token, value, "http"); + d.addPing(DomainPingType.EMAIL, "postmaster"); + d.addPing(DomainPingType.HTTP, token + ":" + value); + + // Two successful pings + getMailReceiver().receive("postmaster@" + test).verify(); + waitForPings(2); + + assertEquals(0, countFailed(d.getPings(), 2)); + + // An own Pinger Daemon to control ping execution locally. + PingerDaemon pd = new PingerDaemon(null); + pd.initializeConnectionUsage(); + + // After 6 months the pings are executed again + pd.executeNeededPings(new Date(System.currentTimeMillis() + 6 * 31 * 24 * 60 * 60L * 1000)); + getMailReceiver().receive("postmaster@" + test).verify(); + waitForPings(4); + assertEquals(0, countFailed(d.getPings(), 4)); + + // After 6 months the pings are executed again, but when the HTTP file + // is wrong, that ping fails. + updateService(token, value + "broken", "http"); + // Note that the time is still 6 months in the future, as the pings from + // before were still executed (and logged) + // as executed now. + pd.executeNeededPings(new Date(System.currentTimeMillis() + 6 * 31 * 24 * 60 * 60L * 1000)); + getMailReceiver().receive("postmaster@" + test).verify(); + waitForPings(6); + assertEquals(1, countFailed(d.getPings(), 6)); + // Which renders the domain invalid + assertFalse(d.isVerified()); + + if (certs) { + // And the user gets a warning-mail if there was a cert + TestMail mail = getMailReceiver().receive(u.getEmail()); + assertThat(mail.getMessage(), CoreMatchers.containsString(d.getSuffix())); + assertThat(mail.getMessage(), CoreMatchers.containsString(c.getSerial())); + if ( !correct) { + // If the user ignores the warning, after two weeks + pd.executeNeededPings(new Date(System.currentTimeMillis() + 15 * 24 * 60 * 60L * 1000)); + // The user receives another warning mail. + mail = getMailReceiver().receive(u.getEmail()); + assertThat(mail.getMessage(), CoreMatchers.containsString(d.getSuffix())); + assertThat(mail.getMessage(), CoreMatchers.containsString(c.getSerial())); + // And when the revocation is carried out + SimpleSigner.ping(); + // ... and the certificate gets revoked. + assertEquals(CertificateStatus.REVOKED, c.getStatus()); + } else { + // But if the user corrects the ping, ... + updateService(token, value, "http"); + // ... and the ping is re-executed, + pd.handle(getPing(d.getConfiguredPings(), DomainPingType.HTTP)); + waitForPings(7); + assertEquals(1, countFailed(d.getPings(), 7)); + + // Even after two weeks + pd.executeNeededPings(new Date(System.currentTimeMillis() + 15 * 24 * 60 * 60L * 1000)); + // and all resulting jobs are executed + SimpleSigner.ping(); + // ... the certificate stays valid. + assertEquals(CertificateStatus.ISSUED, c.getStatus()); + } + } else { + // otherwise there is no mail + } + + } + + private DomainPingConfiguration getPing(List cp, DomainPingType tp) { + for (DomainPingConfiguration d : cp) { + if (d.getType() == tp) { + return d; + } + } + throw new Error("Type not found."); + } + + private int countFailed(DomainPingExecution[] pg, int count) { + assertEquals(count, pg.length); + int fld = 0; + for (DomainPingExecution e : pg) { + PingState state = e.getState(); + if (e.getConfig().getType() == DomainPingType.HTTP) { + if (state == PingState.FAILED) { + fld++; + continue; + } + } + assertEquals(PingState.SUCCESS, state); + } + return fld; + } + +} diff --git a/tests/club/wpia/gigi/testUtils/ConfiguredTest.java b/tests/club/wpia/gigi/testUtils/ConfiguredTest.java index 16abb06c..3ece611c 100644 --- a/tests/club/wpia/gigi/testUtils/ConfiguredTest.java +++ b/tests/club/wpia/gigi/testUtils/ConfiguredTest.java @@ -210,6 +210,16 @@ public abstract class ConfiguredTest { int keySize = 4096; long r_lv = 7331; + // The generated numbers p q and r fall into the + // following ranges: + // - p: 2^(lp-1) < p < 2^lp + // - q: 2^(lq-1) < q < 2^lq + // - r: 2^12 < r < 2^13 + // Thus the generated number has at least lp+lq+11 bit and + // can have at most lp+lq+13 bit. + // Thus for random selection of p and q the algorithm will + // at some point select a number of length n=n/2+lr+(n-n/2-lr)=>n + // bit. int lp = (keySize + 1) >> 1; int lr = BigInteger.valueOf(r_lv).bitLength(); int lq = keySize - lp - lr; @@ -221,24 +231,17 @@ public abstract class ConfiguredTest { // generate two random primes of size lp/lq BigInteger p, q, r, n; - p = BigInteger.probablePrime(lp, random); r = BigInteger.valueOf(r_lv); do { + p = BigInteger.probablePrime(lp, random); q = BigInteger.probablePrime(lq, random); - // convention is for p > q > r - if (p.compareTo(q) < 0) { - BigInteger tmp = p; - p = q; - q = tmp; - } - // modulus n = p * q * r n = p.multiply(q).multiply(r); // even with correctly sized p, q and r, there is a chance - // that n will be one bit short. re-generate the smaller - // prime if so. + // that n will be one bit short. re-generate the + // primes if so. } while (n.bitLength() < keySize); // phi = (p - 1) * (q - 1) * (r - 1) must be relative prime to e @@ -348,6 +351,12 @@ public abstract class ConfiguredTest { d.addPing(DomainPingType.EMAIL, "admin"); TestMail testMail = getMailReceiver().receive("admin@" + d.getSuffix()); testMail.verify(); + // Enforce successful ping :-) + d.addPing(DomainPingType.HTTP, "a:b"); + try (GigiPreparedStatement gps = new GigiPreparedStatement("INSERT INTO `domainPinglog` SET `configId`=(SELECT `id` FROM `pingconfig` WHERE `domainid`=? AND `type`='http'), state='success', needsAction=false")) { + gps.setInt(1, d.getId()); + gps.execute(); + } assertTrue(d.isVerified()); } catch (GigiApiException e) { throw new Error(e); diff --git a/tests/club/wpia/gigi/testUtils/PingTest.java b/tests/club/wpia/gigi/testUtils/PingTest.java index 87ea982f..06a94441 100644 --- a/tests/club/wpia/gigi/testUtils/PingTest.java +++ b/tests/club/wpia/gigi/testUtils/PingTest.java @@ -21,7 +21,7 @@ import club.wpia.gigi.util.SystemKeywords; /** * Base class for test suites that check extensively if the domain-ping - * functionality wroks as expected. + * functionality works as expected. */ public abstract class PingTest extends ClientTest { @@ -34,7 +34,7 @@ public abstract class PingTest extends ClientTest { assertEquals(200, ((HttpURLConnection) new URL(url).openConnection()).getResponseCode()); } - protected void waitForPings(int count) throws SQLException, InterruptedException { + protected void waitForPings(int count) throws InterruptedException { try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT COUNT(*) FROM `domainPinglog`")) { long start = System.currentTimeMillis(); while (System.currentTimeMillis() - start < 10000) { @@ -77,6 +77,12 @@ public abstract class PingTest extends ClientTest { return m; } + /** + * Needs to be @After class. In order to init {@link ClientTest#id} + * correctly, this needs to be after test. Before test this might be + * executed after the init of {@link ClientTest#id} and make that value + * invalid. + */ @After public void purgeDbAfterTest() throws SQLException, IOException { purgeDatabase(); diff --git a/util-testing/club/wpia/gigi/pages/Manager.java b/util-testing/club/wpia/gigi/pages/Manager.java index 26aeec06..f48c5bed 100644 --- a/util-testing/club/wpia/gigi/pages/Manager.java +++ b/util-testing/club/wpia/gigi/pages/Manager.java @@ -39,6 +39,8 @@ import club.wpia.gigi.dbObjects.CertificateOwner; import club.wpia.gigi.dbObjects.Country; import club.wpia.gigi.dbObjects.Digest; import club.wpia.gigi.dbObjects.Domain; +import club.wpia.gigi.dbObjects.DomainPingConfiguration; +import club.wpia.gigi.dbObjects.DomainPingExecution; import club.wpia.gigi.dbObjects.DomainPingType; import club.wpia.gigi.dbObjects.EmailAddress; import club.wpia.gigi.dbObjects.Group; @@ -233,14 +235,14 @@ public class Manager extends Page { } @Override - public void ping(Domain domain, String configuration, CertificateOwner target, int confId) { + public DomainPingExecution ping(Domain domain, String configuration, CertificateOwner target, DomainPingConfiguration conf) { System.err.println("TestManager: " + domain.getSuffix()); if (pingExempt.contains(domain.getSuffix())) { - enterPingResult(confId, DomainPinger.PING_SUCCEDED, "Succeeded by TestManager pass-by", null); + return enterPingResult(conf, DomainPinger.PING_SUCCEDED, "Succeeded by TestManager pass-by", null); } else { DomainPinger pinger = dps.get(dpt); System.err.println("Forward to old pinger: " + pinger); - pinger.ping(domain, configuration, target, confId); + return pinger.ping(domain, configuration, target, conf); } }
*: *:  
*: *:
*'.?>
: - >
- >
- >
- >
 ').?>
').?>