255 lines
11 KiB
Java
255 lines
11 KiB
Java
package de.gurkengewuerz.postfix_rest_send;
|
|
|
|
import de.gurkengewuerz.postfix_rest_send.objects.BruteforceFilter;
|
|
import de.gurkengewuerz.postfix_rest_send.objects.ReturnHolder;
|
|
import de.gurkengewuerz.postfix_rest_send.utils.BashUtils;
|
|
import de.gurkengewuerz.postfix_rest_send.utils.HtmlToPlainText;
|
|
import de.gurkengewuerz.postfix_rest_send.utils.RandomUtils;
|
|
import fi.iki.elonen.NanoHTTPD;
|
|
import org.hazlewood.connor.bottema.emailaddress.EmailAddressCriteria;
|
|
import org.hazlewood.connor.bottema.emailaddress.EmailAddressValidator;
|
|
import org.json.JSONObject;
|
|
import org.simplejavamail.email.Email;
|
|
import org.simplejavamail.mailer.Mailer;
|
|
import org.simplejavamail.mailer.config.ServerConfig;
|
|
|
|
import javax.mail.Message;
|
|
import java.io.File;
|
|
import java.io.IOException;
|
|
import java.sql.ResultSet;
|
|
import java.sql.SQLException;
|
|
import java.util.ArrayList;
|
|
import java.util.EnumSet;
|
|
import java.util.Map;
|
|
import java.util.logging.Level;
|
|
import java.util.logging.Logger;
|
|
|
|
/**
|
|
* Created by gurkengewuerz.de on 12.07.2017.
|
|
*/
|
|
public class Main extends NanoHTTPD {
|
|
private static MySQL db;
|
|
private static Config conf;
|
|
private static Mailer mailer;
|
|
private static BruteforceFilter filter;
|
|
|
|
/*
|
|
TODO:
|
|
Main umbennen
|
|
Configurierbare Domains/IPs
|
|
Post JSON Support
|
|
*/
|
|
|
|
public Main() throws IOException {
|
|
super(conf.getInt("http_port"));
|
|
Logger.getLogger(Main.class.getName()).log(Level.SEVERE, "Started Server :" + getListeningPort());
|
|
start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
|
|
}
|
|
|
|
public static void main(String[] args) throws IOException, SQLException {
|
|
String settingsFile = "." + File.separator + "settings.json";
|
|
if (args.length >= 1) {
|
|
settingsFile = args[0];
|
|
}
|
|
File f = new File(settingsFile);
|
|
|
|
conf = new Config(f);
|
|
conf.load();
|
|
|
|
if (conf.debug())
|
|
Logger.getLogger(Main.class.getName()).log(Level.INFO, "Using Settingsfile " + f.getAbsolutePath());
|
|
|
|
if (conf.isFirstRun()) System.exit(0);
|
|
|
|
db = new MySQL(MySQL.Type.MYSQL, conf.getDBURL(), conf.getDatabaseConfig().getString("username"), conf.getDatabaseConfig().getString("password"));
|
|
db.executeUpdate("CREATE TABLE IF NOT EXISTS token (" +
|
|
" tokenid int(11) NOT NULL AUTO_INCREMENT," +
|
|
" username VARCHAR(255) NOT NULL," +
|
|
" token VARCHAR(255) NOT NULL," +
|
|
" created INT NOT NULL," +
|
|
" expire INT NOT NULL," +
|
|
" PRIMARY KEY (tokenid), " +
|
|
" UNIQUE (token)" +
|
|
" ); ");
|
|
db.executeUpdate("CREATE TABLE IF NOT EXISTS token_bruteforce (" +
|
|
" id int(11) NOT NULL AUTO_INCREMENT," +
|
|
" occurred INT NOT NULL," +
|
|
" ip VARCHAR(255) NOT NULL," +
|
|
" PRIMARY KEY (id)" +
|
|
" ); ");
|
|
mailer = new Mailer(new ServerConfig("localhost", 25));
|
|
mailer.setDebug(conf.debug());
|
|
|
|
filter = new BruteforceFilter(6);
|
|
|
|
new Main();
|
|
}
|
|
|
|
@Override
|
|
public Response serve(IHTTPSession session) {
|
|
Map<String, String> headers = session.getHeaders();
|
|
String ip = headers.get("remote-addr");
|
|
Logger.getLogger(Main.class.getName()).log(Level.INFO, session.getMethod().name() + ": " + session.getUri() + " " + ip);
|
|
Map<String, String> parms = session.getParms();
|
|
JSONObject json = new JSONObject("{'error':'not found'}");
|
|
Response.Status status = Response.Status.NOT_IMPLEMENTED;
|
|
|
|
if (filter.banned(ip))
|
|
return getError(Response.Status.FORBIDDEN, "you are temporary banned");
|
|
|
|
try {
|
|
if (session.getMethod().equals(Method.POST))
|
|
session.parseBody(parms);
|
|
} catch (IOException | ResponseException e) {
|
|
return getError(Response.Status.INTERNAL_ERROR);
|
|
}
|
|
|
|
if (db == null) return getError(Response.Status.INTERNAL_ERROR, "Database Error");
|
|
|
|
try {
|
|
if (session.getUri().startsWith("/authorize")) {
|
|
if (session.getUri().startsWith("/authorize/add")) {
|
|
String username = parms.get("username");
|
|
String password = parms.get("password");
|
|
|
|
if (username == null || password == null) return getError(Response.Status.UNAUTHORIZED);
|
|
|
|
boolean usernameValid = EmailAddressValidator.isValid(username, EnumSet.of(EmailAddressCriteria.ALLOW_DOMAIN_LITERALS));
|
|
if (!usernameValid) {
|
|
filter.failed(ip);
|
|
return getError(Response.Status.UNAUTHORIZED, "invalid user");
|
|
}
|
|
|
|
ResultSet rs = db.executeQuery("SELECT * FROM mailbox WHERE username = ? AND active = 1;", username);
|
|
int rowcount = 0;
|
|
while (rs.next()) {
|
|
rowcount++;
|
|
}
|
|
rs.beforeFirst();
|
|
rs.next();
|
|
if (rowcount <= 0) {
|
|
filter.failed(ip);
|
|
return getError(Response.Status.UNAUTHORIZED);
|
|
}
|
|
|
|
boolean correct = checkPassword(conf.getString("postfixadmin_encryption"), password, rs.getString("password"));
|
|
if (!correct) {
|
|
filter.failed(ip);
|
|
return getError(Response.Status.UNAUTHORIZED);
|
|
}
|
|
String token = RandomUtils.rndToken(username);
|
|
long expires = (System.currentTimeMillis() / 1000) + (conf.getInt("token_expire") * 60 * 60);
|
|
db.executeUpdate("INSERT INTO token (username, token, created, expire) VALUES (?,?,?,?);", username, token, (System.currentTimeMillis() / 1000), expires);
|
|
status = Response.Status.OK;
|
|
json = new JSONObject("{'token':'" + token + "', 'expires': " + expires + "}");
|
|
} else if (session.getUri().startsWith("/authorize/revoke")) {
|
|
String token = parms.get("token");
|
|
if (token == null) return getError(Response.Status.BAD_REQUEST);
|
|
db.executeUpdate("DELETE FROM token WHERE token = ?;", token);
|
|
status = Response.Status.OK;
|
|
json = new JSONObject("{'status':'ok'}");
|
|
}
|
|
} else if (session.getUri().startsWith("/validate/address")) {
|
|
String mail = parms.get("mail");
|
|
if (mail == null) return getError(Response.Status.BAD_REQUEST);
|
|
boolean valid = EmailAddressValidator.isValid(mail, EnumSet.of(EmailAddressCriteria.ALLOW_DOMAIN_LITERALS));
|
|
status = Response.Status.OK;
|
|
json = new JSONObject("{'valid':" + valid + "}");
|
|
} else if (session.getUri().startsWith("/send")) {
|
|
String token = parms.get("token");
|
|
String to = parms.get("to");
|
|
String subject = parms.get("to");
|
|
String text = parms.get("text");
|
|
String html = parms.get("html");
|
|
|
|
if (token == null) return getError(Response.Status.UNAUTHORIZED);
|
|
|
|
ResultSet rs = db.executeQuery("SELECT * FROM token WHERE token = ?;", token);
|
|
int rowcount = 0;
|
|
while (rs.next()) {
|
|
rowcount++;
|
|
}
|
|
rs.beforeFirst();
|
|
rs.next();
|
|
if (rowcount <= 0){
|
|
filter.banned(ip);
|
|
return getError(Response.Status.UNAUTHORIZED);
|
|
}
|
|
|
|
if (rs.getLong("expire") <= System.currentTimeMillis() / 1000)
|
|
return getError(Response.Status.UNAUTHORIZED, "token expired");
|
|
|
|
String username = rs.getString("username");
|
|
|
|
if (to == null || subject == null || (text == null && html == null))
|
|
return getError(Response.Status.BAD_REQUEST);
|
|
|
|
ArrayList<String> toList = new ArrayList<>();
|
|
|
|
if (to.contains(";")) {
|
|
for (String mailTo : to.split(";")) {
|
|
if (!EmailAddressValidator.isValid(mailTo, EnumSet.of(EmailAddressCriteria.ALLOW_DOMAIN_LITERALS)))
|
|
return getError(Response.Status.BAD_REQUEST, "invalid recipient");
|
|
toList.add(mailTo);
|
|
}
|
|
} else {
|
|
if (!EmailAddressValidator.isValid(to, EnumSet.of(EmailAddressCriteria.ALLOW_DOMAIN_LITERALS)))
|
|
return getError(Response.Status.BAD_REQUEST, "invalid recipient");
|
|
toList.add(to);
|
|
}
|
|
|
|
Email email = new Email();
|
|
email.setFromAddress(username, username);
|
|
toList.forEach(s -> email.addRecipient(s, s, Message.RecipientType.TO));
|
|
email.setSubject(subject);
|
|
if (text != null) email.setText(text);
|
|
if (html != null) email.setTextHTML(html);
|
|
if (text == null && html != null) {
|
|
HtmlToPlainText formatter = new HtmlToPlainText();
|
|
email.setText(formatter.getPlainText(html));
|
|
}
|
|
|
|
mailer.sendMail(email);
|
|
status = Response.Status.OK;
|
|
json = new JSONObject("{'status':'send'}");
|
|
}
|
|
} catch (SQLException e) {
|
|
Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, e);
|
|
return getError(Response.Status.INTERNAL_ERROR);
|
|
}
|
|
return newFixedLengthResponse(status, "application/json", json.toString());
|
|
}
|
|
|
|
public boolean checkPassword(String encryption, String plain, String dbPW) {
|
|
ReturnHolder crypted = BashUtils.run(conf.debug(), "php", "-r", "$CONF = array(); $CONF[\"encrypt\"] = \"" + encryption + "\"; include \"" + conf.getString("postfixadmin_path") + "/functions.inc.php" + "\"; echo(pacrypt(\"" + plain + "\", \"" + dbPW + "\")).\"\\n\";");
|
|
return String.join("", crypted.output).equals(dbPW);
|
|
}
|
|
|
|
|
|
public Response getError(Response.Status status, String error) {
|
|
JSONObject json = new JSONObject("{'error':'unknown error'}");
|
|
json.put("error", error);
|
|
return newFixedLengthResponse(status, "application/json", json.toString());
|
|
}
|
|
|
|
public Response getError(Response.Status status) {
|
|
switch (status) {
|
|
case INTERNAL_ERROR:
|
|
return getError(status, "internal server rror");
|
|
case BAD_REQUEST:
|
|
return getError(status, "bad request");
|
|
case UNAUTHORIZED:
|
|
return getError(status, "unauthorized");
|
|
}
|
|
return getError(status, "unknown error");
|
|
}
|
|
|
|
public static MySQL getDatabase() {
|
|
return db;
|
|
}
|
|
|
|
public static Config getConfig() {
|
|
return conf;
|
|
}
|
|
}
|