]> WPIA git - gigi.git/commitdiff
Merge "add: gigi.properties(5) manpage"
authorLucas Werkmeister <mail@lucaswerkmeister.de>
Wed, 22 Mar 2017 21:42:43 +0000 (22:42 +0100)
committerGerrit Code Review <gigi-system@dogcraft.de>
Wed, 22 Mar 2017 21:42:43 +0000 (22:42 +0100)
natives/README.md [new file with mode: 0644]
src/club/wpia/gigi/email/EmailProvider.java
src/club/wpia/gigi/util/DNSUtil.java
src/club/wpia/gigi/util/DomainAssessment.java
src/club/wpia/gigi/util/PasswordHash.java
src/club/wpia/gigi/util/ServerConstants.java
tests/club/wpia/gigi/testUtils/ConfiguredTest.java
tests/club/wpia/gigi/util/TestPasswordMigration.java
util-testing/club/wpia/gigi/pages/Manager.java

diff --git a/natives/README.md b/natives/README.md
new file mode 100644 (file)
index 0000000..49144df
--- /dev/null
@@ -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.
index c6bef2f9e4df9786ad0f2125d01c079808be5f15..f2c175e07a35b29948f59b2e37dc800942212cd2 100644 (file)
@@ -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;
     }
 
index af664359b68bf71ed6cfe78f39be9e7a7e019176..64a2096461d5d0ce1c322eddc10a0e47f0bc6712 100644 (file)
@@ -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<String, String> env = new Hashtable<String, String>();
         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 {
index d24404ed4676752aa9aab075c2186c43a2c9f400..e17f2eea2ae724173c5f4f6846996690077a47eb 100644 (file)
@@ -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);
         }
index 51dffb70d8ed0763548ce7be6fed104364682c30..40700d32765f941062c0627d92e8597e40956a36 100644 (file)
@@ -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
      *         <ul>
-     *         <li><code>null</code>, if the password was valid</li>
+     *         <li><code>null</code>, if the password was invalid</li>
      *         <li><code>hash</code>, if the password is valid and the hash
      *         doesn't need to be updated</li>
      *         <li>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) {
index 8ff3883cf99f4f923a2b634743d0f1a064edff70..3fd2e70e7f57a72aefff38847c811f84ab0cf7da 100644 (file)
@@ -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<Host, String> hostnames = new HashMap<>();
         for (Host h : Host.values()) {
             hostnames.put(h, conf.getProperty("name." + h.getConfigName(), h.getHostDefaultPrefix() + "." + suffix));
index cdacf6922f0f4af08d51e5484f210211fa96b654..711007a047eafe94703507f2ddac532650e8a8eb 100644 (file)
@@ -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;
+            }
         }
     }
 
index 17544a0bdc610737aa0af8be9771f3c5f2de614d..1ad8ae8a04a55ad829a1f8b78bc7509a35dc0575 100644 (file)
@@ -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("$")));
         }
     }
 }
index b33d52c439429be5e4d744cbb7341865be91cb24..f0991aac25b07cfa33616cecd05a5680140290b9 100644 (file)
@@ -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) {