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
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
<name>BRANCH</name>
<description>The branch to build from.</description>
<defaultValue>$GERRIT_PATCHSET_REVISION</defaultValue>
+ <trim>false</trim>
</hudson.model.StringParameterDefinition>
<hudson.model.StringParameterDefinition>
<name>JAVA_HOME</name>
<description></description>
<defaultValue>/usr/lib/jvm/openjdk-8-jdk-gigi</defaultValue>
+ <trim>false</trim>
</hudson.model.StringParameterDefinition>
</parameterDefinitions>
</hudson.model.ParametersDefinitionProperty>
localePath=$$$$LOCALE_FETCH_PATH$$$$</properties>
</hudson.tasks.Ant>
<hudson.tasks.Shell>
- <command>cd gigi
+ <command>rm -v *.deb
+cd gigi
+[ -f doc/scripts/genchangelog ] && doc/scripts/genchangelog
dpkg-buildpackage -b -us -uc</command>
</hudson.tasks.Shell>
</builders>
--- /dev/null
+#!/bin/bash
+cd "$(git rev-parse --show-toplevel)"
+cat > debian/changelog <<EOF
+wpia-gigi ($(git describe HEAD --tags --match "v*" | sed "s/^v//")) unstable; urgency=low
+
+ * Initial release
+
+ -- WPIA Software Team <software@wpia.club> $(git show HEAD --pretty=format:%aD --no-patch)
+EOF
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);
}
- 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;
"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" (
);
-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,
"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` (
--- /dev/null
+ALTER TABLE "domainPinglog" ADD COLUMN "needsAction" boolean DEFAULT false;
+CREATE INDEX ON "domainPinglog" ("when", "needsAction");
--- /dev/null
+DROP TABLE IF EXISTS "alerts";
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();
private LinkedList<DomainPingConfiguration> configs = null;
- public List<DomainPingConfiguration> getConfiguredPings() throws GigiApiException {
+ public List<DomainPingConfiguration> getConfiguredPings() {
LinkedList<DomainPingConfiguration> configs = this.configs;
if (configs == null) {
configs = new LinkedList<>();
}
}
+ /**
+ * 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 {
}
}
+ 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;
+ }
+ }
+
}
package club.wpia.gigi.dbObjects;
+import java.sql.Timestamp;
import java.util.Date;
import club.wpia.gigi.Gigi;
}
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;
+ }
+ }
}
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);
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;
}
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;
}
}
+ 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");
}
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;
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());
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("<time datetime=\"" + sdf.format(s) + "\">");
- out.print(sdfUI.format(s));
- out.print(" UTC</time>");
+ 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("<time datetime=\"" + sdf.format(s) + "\">");
+ out.print(sdfUI.format(s));
+ out.print(" UTC</time>");
+ }
} else {
out.print(s == null ? "null" : (unescaped ? s.toString() : HTMLEncoder.encodeHTML(s.toString())));
}
<h1><?=_Login?></h1>
<p class='smalltext'><?=_Warning! This site requires cookies to be enabled to ensure your privacy and security. This site uses session cookies to store temporary values to prevent people from copying and pasting the session ID to someone else exposing their account, personal details and identity theft as a result.?></p>
<label for="username"><?=_Email Address?>:</label><input class="form-control" type='text' name="username" required/><br />
-<label for="password"><?=_Pass Phrase?>:</label><input class="form-control" type='password' name='password' required/><br />
+<label for="password"><?=_Password?>:</label><input class="form-control" type='password' name='password' required/><br />
<button type='submit' name="process" value="Login" class="btn btn-primary"><?=_Login?></button><br /><br />
<p class='smalltext'><?=_If you are having trouble with your username or password, please visit our !(/wiki/lostPassword)wiki page!'</a>' for more information?></p>
</div>
<table class="table">
<thead>
<tr>
- <th colspan="2" class="title"><?=_Change Pass Phrase?></th>
+ <th colspan="2" class="title"><?=_Change Password?></th>
</tr>
</thead>
<tbody>
<td><input type="password" name="private_token" required></td>
</tr>
<tr>
- <td><?=_New Pass Phrase?><span class="formMandatory">*</span>: </td>
+ <td><?=_New Password?><span class="formMandatory">*</span>: </td>
<td><input type="password" name="pword1" required></td>
</tr>
<tr>
- <td><?=_Pass Phrase Again?><span class="formMandatory">*</span>: </td>
+ <td><?=_Password Again?><span class="formMandatory">*</span>: </td>
<td><input type="password" name="pword2" required></td>
</tr>
<tr>
<td colspan="2"><span class="formMandatory">*</span><?=_Your password is one of many factors to protect your account from unauthorised access. A good password is hard to guess, long, and contains a diverse set of characters. For the current requirements and to learn more, visit our !(/wiki/goodPassword)FAQ!'</a>'.?></td>
</tr>
<tr>
- <td colspan="2"><input type="submit" name="process" value="<?=_Update Pass Phrase?>"></td>
+ <td colspan="2"><input type="submit" name="process" value="<?=_Update Password?>"></td>
</tr>
</tbody>
</table>
<table class="table">
<thead>
<tr>
- <th colspan="2" class="title"><?=_Change Pass Phrase?></th>
+ <th colspan="2" class="title"><?=_Change Password?></th>
</tr>
</thead>
<tbody>
<tr>
- <td><?=_Old Pass Phrase?>: </td>
+ <td><?=_Old Password?>: </td>
<td><input class="form-control" type="password" name="oldpassword" required></td>
</tr>
<tr>
- <td><?=_New Pass Phrase?><span class="formMandatory">*</span>: </td>
+ <td><?=_New Password?><span class="formMandatory">*</span>: </td>
<td><input class="form-control" type="password" name="pword1" required></td>
</tr>
<tr>
- <td><?=_Pass Phrase Again?><span class="formMandatory">*</span>: </td>
+ <td><?=_Password Again?><span class="formMandatory">*</span>: </td>
<td><input class="form-control" type="password" name="pword2" required></td>
</tr>
<tr>
<td colspan="2"><span class="formMandatory">*</span><?=_Your password is one of many factors to protect your account from unauthorised access. A good password is hard to guess, long, and contains a diverse set of characters. For the current requirements and to learn more, visit our !(/wiki/goodPassword)FAQ!'</a>'.?></td>
</tr>
<tr>
- <td colspan="2"><input type="submit" name="process" value="<?=_Update Pass Phrase?>"></td>
+ <td colspan="2"><input type="submit" name="process" value="<?=_Update Password?>"></td>
</tr>
</tbody>
</table>
<label for='SANs'>SANs</label>
</td>
<td align="left">
- <textarea class="form-control" rows='5' name='SANs' placeholder="dns:my.domain.example.com, dns:*.example.com, email:my.email@example.com (or newline separated)"><?=$emails?></textarea><br />
- <?=_Syntax for SAN?>: dns:my.domain.example.com, dns:*.example.com, email:my.email@example.com <?=_(or newline separated)?><br />
+ <textarea class="form-control" rows='5' name='SANs' placeholder="dns:my.domain.example.org, dns:*.example.org, email:my.email@example.org (or newline separated)"><?=$emails?></textarea><br />
+ <?=_Syntax for SAN?>: dns:my.domain.example.org, dns:*.example.org, email:my.email@example.org <?=_(or newline separated)?><br />
<?=_Recommendation for inexperienced users: only use one email address for client certificates.?>
</td>
</tr>
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());
<table class="table">
<tr>
- <td><?=_Email Addresses?> </td>
+ <td><?=_Email Address?> </td>
<td><input class="form-control" type="text" name="newemail" value="" required></td>
</tr>
<tr>
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);
}
@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);
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) {
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", "!'</a>")));
vars.put("csrf", getCSRFToken());
vars.put("dobmin", User.MINIMUM_AGE + "");
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);
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", "!'</a>'"))));
+ 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", "!'</a>'"))));
}
if ( !ga.isEmpty()) {
throw ga;
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);
}
</tr>
<tr>
- <td><?=_Pass Phrase?><font color="red">*</font>: </td>
+ <td><?=_Password?><font color="red">*</font>: </td>
<td><input class="form-control" type="password" name="pword1" size="30" autocomplete="off"></td>
<td rowspan="2"> </td>
</tr>
<tr>
- <td><?=_Pass Phrase Again?><font color="red">*</font>: </td>
+ <td><?=_Password Again?><font color="red">*</font>: </td>
<td><input class="form-control" type="password" name="pword2" size="30" autocomplete="off"></td>
</tr>
<tr>
<td colspan="3"><font color="red">*</font><?=_Your password is one of many factors to protect your account from unauthorised access. A good password is hard to guess, long, and contains a diverse set of characters. For the current requirements and to learn more, visit our !(/wiki/goodPassword)FAQ!'</a>'.?></td>
</tr>
- <tr>
- <td colspan="3"><?=_It's possible to get notifications of up and coming events and even just general announcements, untick any notifications you don't wish to receive. For country, regional and radius notifications to work you must choose your location once you've verified your account and logged in.?></td>
- </tr>
<tr>
- <td valign="top"><?=_Alert me if?>: </td>
- <td align="left">
- <input type="checkbox" name="general" value="1"<?=$!general?>><?=_General Announcements?><br>
- <input type="checkbox" name="country" value="1"<?=$!country?>><?=_Country Announcements?><br>
- <input type="checkbox" name="regional" value="1"<?=$!regional?>><?=_Regional Announcements?><br>
- <input type="checkbox" name="radius" value="1"<?=$!radius?>><?=_Within 200km Announcements?></td>
- <td> </td>
+ <td colspan="3"><input type="checkbox" name="tos_agree" value="1"><?=_I agree to the Terms of Service (!(/policy/ToS)ToS!'</a>').?></td>
</tr>
<tr>
<td colspan="3"><?=_When you click on next, we will send a confirmation email to the email address you have entered above.?></td>
</tr>
- <tr>
- <td colspan="3"><input type="checkbox" name="tos_agree" value="1"><?=_I agree to the Terms of Service (!(/policy/ToS)ToS!'</a>').?></td>
- </tr>
<tr>
<td colspan="3"><input class="btn btn-primary" type="submit" name="process" value="<?=_Next?>"></td>
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<String> 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: ");
}
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);
}
}
}
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 {
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();
}
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;
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();
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;
}
}
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);
}
}
}
--- /dev/null
+Subject: <?=_Ping for domain '${domain}' failed.?>
+
+<?=_Hi,?>
+
+<?=_A check of the ownership for your domain '${domain}' failed.?> \
+<?=_This check might either have been triggered manually or automatically for routine housekeeping.?> \
+<?=_Seeing a check fail we assume you might have lost ownership of this domain.?>
+
+<? if($valid) { ?>
+<?=_However there are currently enough succeeding proofs so the state of your domain is not endangered yet.?> \
+<?=_You might however want to correct (or remove) this ping to ensure that your domain stays valid.?>
+<? } else { ?>
+<?=_If you keep lacking sufficient proof of ownership for this domain after the grace period of two weeks expired we are going to revoke all affected certificates.?>
+
+<?=_Affected certificates:?>
+<? foreach($certs){ ?>- <?=_serial: ${serial} issued by ${ca} valid from ${from} to ${to}.?>
+<? } ?>
+<? } ?>
+
+<?=_Visit ${domainLink} to check the current state of pings of your domain and to find out more information about which ping failed.?>
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 {
private Queue<DomainPingConfiguration> toExecute = new LinkedList<>();
+ private final MailTemplate pingFailedMail = new MailTemplate(PingerDaemon.class.getResource("PingFailedWithActiveCertificates.templ"));
+
public PingerDaemon(KeyStore truststore) {
this.truststore = truststore;
}
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 {
}
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);
}
}
- 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);
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<String, Object> 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<Certificate>(cs) {
+
+ @Override
+ public void apply(Certificate t, Language l, Map<String, Object> 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() + ")");
}
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;
}
@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);
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);
}
}
}
+ /**
+ * @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);
}
}
}
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", "!'</a>'"))));
+ 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", "!'</a>'"))));
}
}
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;
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 {
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"));
+ }
}
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);
assertThat(run, containsString("<option selected=\"selected\">28</option>"));
}
- @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();
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);
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);
--- /dev/null
+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<DomainPingConfiguration> 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;
+ }
+
+}
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;
// 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
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);
/**
* 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 {
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) {
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();
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;
}
@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);
}
}