--- /dev/null
+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.
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;
}
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 {
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 {
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);
}
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;
* 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
if (password == null || password.isEmpty()) {
return null;
}
+
if (hash.contains("$")) {
if (SCryptUtil.check(password, hash)) {
return hash;
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) {
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));
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);
}
@AfterClass
public static void closeDBLink() {
- if (l != null) {
- l.close();
- l = null;
+ synchronized (ConfiguredTest.class) {
+ if (l != null) {
+ l.close();
+ l = null;
+ }
}
}
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("$")));
}
}
}
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;
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;
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 {
@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) {