From: Lucas Werkmeister Date: Wed, 22 Mar 2017 21:42:43 +0000 (+0100) Subject: Merge "add: gigi.properties(5) manpage" X-Git-Url: https://code.wpia.club/?p=gigi.git;a=commitdiff_plain;h=5e75c08b32119d8b45ed0a1d900ad523f4503bb3;hp=c209d279826b7f4a63286b70725d1e659b3eab89 Merge "add: gigi.properties(5) manpage" --- diff --git a/natives/README.md b/natives/README.md new file mode 100644 index 00000000..49144dfe --- /dev/null +++ b/natives/README.md @@ -0,0 +1,10 @@ +This native method exposes the *man:setuid(2)* and *man:setgid(2)* system calls to Java. +Java code can call `club.wpia.gigi.natives.SetUID.setUid(uid, gid)` to set the user and group ID to the specified values if they’re currently different. + +Gigi can use this to bind to Internet domain privileged ports (port numbers below 1024) +when started as root and then drop privileges by changing to a non-root user. + +It should be noted that this is rarely necessary; +it is much safer to start Gigi as a regular user with `CAP_NET_BIND_SERVICE` (see *man:capabilities(7)*). +Gigi can also inherit its socket from the environment (file descriptor 0), +e. g. from systemd (see *man:systemd.socket(5)*) or (x)inetd. diff --git a/src/club/wpia/gigi/email/EmailProvider.java b/src/club/wpia/gigi/email/EmailProvider.java index c6bef2f9..f2c175e0 100644 --- a/src/club/wpia/gigi/email/EmailProvider.java +++ b/src/club/wpia/gigi/email/EmailProvider.java @@ -83,106 +83,152 @@ public abstract class EmailProvider { private static final Pattern MAIL_ADDRESS = Pattern.compile("^" + MAIL_P_RFC_ADDRESS + "$"); - public String checkEmailServer(int forUid, String address) throws IOException { - if (isValidMailAddress(address)) { - String[] parts = address.split("@", 2); - String domain = parts[1]; - - String[] mxhosts; - try { - mxhosts = DNSUtil.getMXEntries(domain); - } catch (NamingException e1) { - return "MX lookup for your hostname failed."; + public String checkEmailServer(int forUid, final String address) throws IOException { + if ( !isValidMailAddress(address)) { + try (GigiPreparedStatement statmt = new GigiPreparedStatement("INSERT INTO `emailPinglog` SET `when`=NOW(), `email`=?, `result`=?, `uid`=?, `type`='fast'::`emailPingType`, `status`='failed'::`pingState`")) { + statmt.setString(1, address); + statmt.setString(2, "Invalid email address provided"); + statmt.setInt(3, forUid); + statmt.execute(); } - sortMX(mxhosts); + return FAIL; + } - for (String host : mxhosts) { - host = host.split(" ", 2)[1]; - if (host.endsWith(".")) { - host = host.substring(0, host.length() - 1); - } else { - return "Strange MX records."; - } - try (Socket s = new Socket(host, 25); - BufferedReader br0 = new BufferedReader(new InputStreamReader(s.getInputStream(), "UTF-8"));// - PrintWriter pw0 = new PrintWriter(new OutputStreamWriter(s.getOutputStream(), "UTF-8"))) { - BufferedReader br = br0; - PrintWriter pw = pw0; + String[] parts = address.split("@", 2); + String domain = parts[1]; + + String[] mxhosts; + try { + mxhosts = DNSUtil.getMXEntries(domain); + } catch (NamingException e1) { + return "MX lookup for your hostname failed."; + } + sortMX(mxhosts); + + for (String host : mxhosts) { + host = host.split(" ", 2)[1]; + if (host.endsWith(".")) { + host = host.substring(0, host.length() - 1); + } else { + return "Strange MX records."; + } + + class SMTPSessionHandler { + + public boolean detectedSTARTTLS = false; + + public boolean initiateSMTPSession(BufferedReader r, PrintWriter w) throws IOException { String line; - if ( !SendMail.readSMTPResponse(br, 220)) { - continue; + + if ( !SendMail.readSMTPResponse(r, 220)) { + return false; } - pw.print("EHLO " + SystemKeywords.SMTP_NAME + "\r\n"); - pw.flush(); - boolean starttls = false; + w.print("EHLO " + SystemKeywords.SMTP_NAME + "\r\n"); + w.flush(); + + detectedSTARTTLS = false; do { - line = br.readLine(); + line = r.readLine(); if (line == null) { break; } - starttls |= line.substring(4).equals("STARTTLS"); + detectedSTARTTLS |= line.substring(4).equals("STARTTLS"); } while (line.startsWith("250-")); + if (line == null || !line.startsWith("250 ")) { - continue; + return false; } - if (starttls) { - pw.print("STARTTLS\r\n"); - pw.flush(); - if ( !SendMail.readSMTPResponse(br, 220)) { - continue; - } - Socket s1 = ((SSLSocketFactory) SSLSocketFactory.getDefault()).createSocket(s, host, 25, true); - br = new BufferedReader(new InputStreamReader(s1.getInputStream(), "UTF-8")); - pw = new PrintWriter(new OutputStreamWriter(s1.getOutputStream(), "UTF-8")); - pw.print("EHLO " + SystemKeywords.SMTP_NAME + "\r\n"); - pw.flush(); - if ( !SendMail.readSMTPResponse(br, 250)) { - continue; - } + return true; + } + + public boolean trySendEmail(BufferedReader r, PrintWriter w) throws IOException { + w.print("MAIL FROM: <" + SystemKeywords.SMTP_PSEUDO_FROM + ">\r\n"); + w.flush(); + + if ( !SendMail.readSMTPResponse(r, 250)) { + return false; } - pw.print("MAIL FROM: <" + SystemKeywords.SMTP_PSEUDO_FROM + ">\r\n"); - pw.flush(); + w.print("RCPT TO: <" + address + ">\r\n"); + w.flush(); - if ( !SendMail.readSMTPResponse(br, 250)) { - continue; + if ( !SendMail.readSMTPResponse(r, 250)) { + return false; } - pw.print("RCPT TO: <" + address + ">\r\n"); - pw.flush(); - if ( !SendMail.readSMTPResponse(br, 250)) { - continue; + w.print("QUIT\r\n"); + w.flush(); + + if ( !SendMail.readSMTPResponse(r, 221)) { + return false; } - pw.print("QUIT\r\n"); - pw.flush(); - if ( !SendMail.readSMTPResponse(br, 221)) { + + return true; + } + + } + + SMTPSessionHandler sh = new SMTPSessionHandler(); + + try (Socket plainSocket = new Socket(host, 25); // + BufferedReader plainReader = new BufferedReader(new InputStreamReader(plainSocket.getInputStream(), "UTF-8")); // + PrintWriter plainWriter = new PrintWriter(new OutputStreamWriter(plainSocket.getOutputStream(), "UTF-8"))) { + + if ( !sh.initiateSMTPSession(plainReader, plainWriter)) { + continue; + } + + boolean canSend = false; + + if (sh.detectedSTARTTLS) { + plainWriter.print("STARTTLS\r\n"); + plainWriter.flush(); + + if ( !SendMail.readSMTPResponse(plainReader, 220)) { continue; } - try (GigiPreparedStatement statmt = new GigiPreparedStatement("INSERT INTO `emailPinglog` SET `when`=NOW(), `email`=?, `result`=?, `uid`=?, `type`='fast', `status`='success'::`pingState`")) { - statmt.setString(1, address); - statmt.setString(2, line); - statmt.setInt(3, forUid); - statmt.execute(); - } + try (Socket tlsSocket = ((SSLSocketFactory) SSLSocketFactory.getDefault()).createSocket(plainSocket, host, 25, true); // + BufferedReader tlsReader = new BufferedReader(new InputStreamReader(tlsSocket.getInputStream(), "UTF-8")); // + PrintWriter tlsWriter = new PrintWriter(new OutputStreamWriter(tlsSocket.getOutputStream(), "UTF-8"))) { + + tlsWriter.print("EHLO " + SystemKeywords.SMTP_NAME + "\r\n"); + tlsWriter.flush(); + + if ( !SendMail.readSMTPResponse(tlsReader, 250)) { + continue; + } - if (line == null || !line.startsWith("250")) { - return line; - } else { - return OK; + canSend = sh.trySendEmail(tlsReader, tlsWriter); } + } else { + canSend = sh.trySendEmail(plainReader, plainWriter); } + if ( !canSend) { + continue; + } + + try (GigiPreparedStatement statmt = new GigiPreparedStatement("INSERT INTO `emailPinglog` SET `when`=NOW(), `email`=?, `result`=?, `uid`=?, `type`='fast', `status`='success'::`pingState`")) { + statmt.setString(1, address); + statmt.setString(2, OK); + statmt.setInt(3, forUid); + statmt.execute(); + } + + return OK; } } + try (GigiPreparedStatement statmt = new GigiPreparedStatement("INSERT INTO `emailPinglog` SET `when`=NOW(), `email`=?, `result`=?, `uid`=?, `type`='fast'::`emailPingType`, `status`='failed'::`pingState`")) { statmt.setString(1, address); statmt.setString(2, "Failed to make a connection to the mail server"); statmt.setInt(3, forUid); statmt.execute(); } + return FAIL; } diff --git a/src/club/wpia/gigi/util/DNSUtil.java b/src/club/wpia/gigi/util/DNSUtil.java index af664359..64a20964 100644 --- a/src/club/wpia/gigi/util/DNSUtil.java +++ b/src/club/wpia/gigi/util/DNSUtil.java @@ -38,17 +38,17 @@ public class DNSUtil { env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory"); env.put(Context.AUTHORITATIVE, "true"); env.put(Context.PROVIDER_URL, "dns://" + server); + InitialDirContext context = new InitialDirContext(env); try { - Attributes dnsLookup = context.getAttributes(name, new String[] { "TXT" }); + return extractTextEntries(dnsLookup.get("TXT")); } finally { context.close(); } - } private static String[] extractTextEntries(Attribute nsRecords) throws NamingException { @@ -72,27 +72,35 @@ public class DNSUtil { public static CAARecord[] getCAAEntries(String domain) throws NamingException { Hashtable env = new Hashtable(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory"); + InitialDirContext context = new InitialDirContext(env); - Attributes dnsLookup; try { - dnsLookup = context.getAttributes(domain, new String[] { + Attributes dnsLookup; + try { + dnsLookup = context.getAttributes(domain, new String[] { "257" - }); - } catch (NameNotFoundException e) { - // We treat non-existing names as names without CAA-records - return new CAARecord[0]; - } - Attribute nsRecords = dnsLookup.get("257"); - if (nsRecords == null) { - return new CAARecord[] {}; - } - CAA.CAARecord[] result = new CAA.CAARecord[nsRecords.size()]; - for (int i = 0; i < result.length; i++) { - byte[] rec = (byte[]) nsRecords.get(i); + }); + } catch (NameNotFoundException e) { + // We treat non-existing names as names without CAA-records + return new CAARecord[0]; + } + + Attribute nsRecords = dnsLookup.get("257"); + if (nsRecords == null) { + return new CAARecord[] {}; + } + + CAA.CAARecord[] result = new CAA.CAARecord[nsRecords.size()]; + for (int i = 0; i < result.length; i++) { + byte[] rec = (byte[]) nsRecords.get(i); - result[i] = new CAA.CAARecord(rec); + result[i] = new CAA.CAARecord(rec); + } + + return result; + } finally { + context.close(); } - return result; } public static void main(String[] args) throws NamingException { diff --git a/src/club/wpia/gigi/util/DomainAssessment.java b/src/club/wpia/gigi/util/DomainAssessment.java index d24404ed..e17f2eea 100644 --- a/src/club/wpia/gigi/util/DomainAssessment.java +++ b/src/club/wpia/gigi/util/DomainAssessment.java @@ -148,11 +148,14 @@ public class DomainAssessment { public static void init(Properties conf) { String financialName = conf.getProperty("highFinancialValue"); + if (financialName == null) { throw new Error("No property highFinancialValue was configured"); } - try { - financial = new DomainSet(new InputStreamReader(new FileInputStream(new File(financialName)), "UTF-8")); + + try (FileInputStream fis = new FileInputStream(new File(financialName)); // + InputStreamReader isr = new InputStreamReader(fis, "UTF-8")) { + financial = new DomainSet(isr); } catch (IOException e) { throw new Error(e); } diff --git a/src/club/wpia/gigi/util/PasswordHash.java b/src/club/wpia/gigi/util/PasswordHash.java index 51dffb70..40700d32 100644 --- a/src/club/wpia/gigi/util/PasswordHash.java +++ b/src/club/wpia/gigi/util/PasswordHash.java @@ -1,8 +1,5 @@ package club.wpia.gigi.util; -import java.io.UnsupportedEncodingException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.util.Properties; import com.lambdaworks.crypto.SCryptUtil; @@ -18,7 +15,7 @@ public class PasswordHash { * The hash to verify the password against. * @return *
    - *
  • null, if the password was valid
  • + *
  • null, if the password was invalid
  • *
  • hash, if the password is valid and the hash * doesn't need to be updated
  • *
  • a new hash, if the password is valid but the hash in the @@ -29,6 +26,7 @@ public class PasswordHash { if (password == null || password.isEmpty()) { return null; } + if (hash.contains("$")) { if (SCryptUtil.check(password, hash)) { return hash; @@ -36,36 +34,8 @@ public class PasswordHash { return null; } } - String newhash = sha1(password); - boolean match = true; - if (newhash.length() != hash.length()) { - match = false; - } - for (int i = 0; i < newhash.length(); i++) { - match &= newhash.charAt(i) == hash.charAt(i); - } - if (match) { - return hash(password); - } else { - return null; - } - } - public static String sha1(String password) { - try { - MessageDigest md = MessageDigest.getInstance("SHA1"); - byte[] digest = md.digest(password.getBytes("UTF-8")); - StringBuffer res = new StringBuffer(digest.length * 2); - for (int i = 0; i < digest.length; i++) { - res.append(Integer.toHexString((digest[i] & 0xF0) >> 4)); - res.append(Integer.toHexString(digest[i] & 0xF)); - } - return res.toString(); - } catch (NoSuchAlgorithmException e) { - throw new Error(e); - } catch (UnsupportedEncodingException e) { - throw new Error(e); - } + return null; } public static String hash(String password) { diff --git a/src/club/wpia/gigi/util/ServerConstants.java b/src/club/wpia/gigi/util/ServerConstants.java index 8ff3883c..3fd2e70e 100644 --- a/src/club/wpia/gigi/util/ServerConstants.java +++ b/src/club/wpia/gigi/util/ServerConstants.java @@ -73,7 +73,7 @@ public class ServerConstants { secureBindPort = conf.getProperty("https.bindPort", conf.getProperty("https.port")); bindPort = conf.getProperty("http.bindPort", conf.getProperty("http.port")); - suffix = conf.getProperty("name.suffix", conf.getProperty("name.www", "www.wpia.local").substring(4)); + suffix = conf.getProperty("name.suffix", "wpia.local"); HashMap hostnames = new HashMap<>(); for (Host h : Host.values()) { hostnames.put(h, conf.getProperty("name." + h.getConfigName(), h.getHostDefaultPrefix() + "." + suffix)); diff --git a/tests/club/wpia/gigi/testUtils/ConfiguredTest.java b/tests/club/wpia/gigi/testUtils/ConfiguredTest.java index cdacf692..711007a0 100644 --- a/tests/club/wpia/gigi/testUtils/ConfiguredTest.java +++ b/tests/club/wpia/gigi/testUtils/ConfiguredTest.java @@ -93,7 +93,9 @@ public abstract class ConfiguredTest { if ( !DatabaseConnection.isInited()) { DatabaseConnection.init(testProps); try { - l = DatabaseConnection.newLink(false); + synchronized (ConfiguredTest.class) { + l = DatabaseConnection.newLink(false); + } } catch (InterruptedException e) { throw new Error(e); } @@ -104,9 +106,11 @@ public abstract class ConfiguredTest { @AfterClass public static void closeDBLink() { - if (l != null) { - l.close(); - l = null; + synchronized (ConfiguredTest.class) { + if (l != null) { + l.close(); + l = null; + } } } diff --git a/tests/club/wpia/gigi/util/TestPasswordMigration.java b/tests/club/wpia/gigi/util/TestPasswordMigration.java index 17544a0b..1ad8ae8a 100644 --- a/tests/club/wpia/gigi/util/TestPasswordMigration.java +++ b/tests/club/wpia/gigi/util/TestPasswordMigration.java @@ -12,29 +12,39 @@ import club.wpia.gigi.database.GigiPreparedStatement; import club.wpia.gigi.database.GigiResultSet; import club.wpia.gigi.testUtils.ManagedTest; import club.wpia.gigi.testUtils.RegisteredUser; -import club.wpia.gigi.util.PasswordHash; public class TestPasswordMigration extends ManagedTest { @Rule public RegisteredUser ru = new RegisteredUser(); + /** + * Gigi used to support plain SHA-1 password hashes, for compatibility with + * legacy software. Since there currently is only one accepted hash format, + * this test now verifies that plain SHA-1 hashes are no longer accepted nor + * migrated to more recent hash formats. + * + * @see PasswordHash.verifyHash + * @see PasswordHash.hash + * @throws IOException + */ @Test - public void testPasswordMigration() throws IOException { + public void testNoSHA1PasswordMigration() throws IOException { try (GigiPreparedStatement stmt = new GigiPreparedStatement("UPDATE users SET `password`=? WHERE id=?")) { - stmt.setString(1, PasswordHash.sha1("a")); + stmt.setString(1, "86f7e437faa5a7fce15d1ddcb9eaeaea377667b8"); // sha1("a") stmt.setInt(2, ru.getUser().getId()); stmt.execute(); } + String cookie = login(ru.getUser().getEmail(), "a"); - assertTrue(isLoggedin(cookie)); + assertFalse(isLoggedin(cookie)); try (GigiPreparedStatement stmt = new GigiPreparedStatement("SELECT `password` FROM users WHERE id=?")) { stmt.setInt(1, ru.getUser().getId()); GigiResultSet res = stmt.executeQuery(); assertTrue(res.next()); String newHash = res.getString(1); - assertThat(newHash, containsString("$")); + assertThat(newHash, not(containsString("$"))); } } } diff --git a/util-testing/club/wpia/gigi/pages/Manager.java b/util-testing/club/wpia/gigi/pages/Manager.java index b33d52c4..f0991aac 100644 --- a/util-testing/club/wpia/gigi/pages/Manager.java +++ b/util-testing/club/wpia/gigi/pages/Manager.java @@ -27,6 +27,7 @@ import java.util.regex.Pattern; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import sun.security.x509.X509Key; import club.wpia.gigi.Gigi; import club.wpia.gigi.GigiApiException; import club.wpia.gigi.crypto.SPKAC; @@ -47,6 +48,7 @@ import club.wpia.gigi.dbObjects.NamePart.NamePartType; import club.wpia.gigi.dbObjects.User; import club.wpia.gigi.dbObjects.Verification.VerificationType; import club.wpia.gigi.email.DelegateMailProvider; +import club.wpia.gigi.email.EmailProvider; import club.wpia.gigi.localisation.Language; import club.wpia.gigi.output.template.IterableDataset; import club.wpia.gigi.output.template.Template; @@ -55,10 +57,10 @@ import club.wpia.gigi.ping.DomainPinger; import club.wpia.gigi.ping.PingerDaemon; import club.wpia.gigi.util.AuthorizationContext; import club.wpia.gigi.util.DayDate; +import club.wpia.gigi.util.DomainAssessment; import club.wpia.gigi.util.HTMLEncoder; import club.wpia.gigi.util.Notary; import club.wpia.gigi.util.TimeConditions; -import sun.security.x509.X509Key; public class Manager extends Page { @@ -297,8 +299,27 @@ public class Manager extends Page { @Override public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { if (req.getParameter("create") != null) { - batchCreateUsers(req.getParameter("prefix"), req.getParameter("suffix"), Integer.parseInt(req.getParameter("amount")), resp.getWriter()); - resp.getWriter().println("User batch created."); + String prefix = req.getParameter("prefix"); + String domain = req.getParameter("suffix"); + try { + if (null == prefix) { + throw new GigiApiException("No prefix given."); + } + if (null == domain) { + throw new GigiApiException("No domain given."); + } + + DomainAssessment.checkCertifiableDomain(domain, false, true); + + if ( !EmailProvider.isValidMailAddress(prefix + "@" + domain)) { + throw new GigiApiException("Invalid email address template."); + } + + batchCreateUsers(prefix, domain, Integer.parseInt(req.getParameter("amount")), resp.getWriter()); + resp.getWriter().println("User batch created."); + } catch (GigiApiException e) { + throw new Error(e); + } } else if (req.getParameter("addpriv") != null || req.getParameter("delpriv") != null) { User u = User.getByEmail(req.getParameter("email")); if (u == null) {