More work

This commit is contained in:
Colin McDonald 2016-05-04 17:31:03 -04:00
parent 2d05637103
commit 03bc01e5ce
23 changed files with 142 additions and 30 deletions

View File

@ -3,6 +3,8 @@ mongo.port=55505
mongo.database=minehqapi mongo.database=minehqapi
mongo.username=test mongo.username=test
mongo.password=test mongo.password=test
redis.address=localhost
redis.port=6379
http.address= http.address=
http.port=80 http.port=80
http.workerThreads=6 http.workerThreads=6

10
pom.xml
View File

@ -77,6 +77,11 @@
<artifactId>metrics-core</artifactId> <artifactId>metrics-core</artifactId>
<version>3.1.2</version> <version>3.1.2</version>
</dependency> </dependency>
<dependency>
<groupId>com.bugsnag</groupId>
<artifactId>bugsnag</artifactId>
<version>2.0.0</version>
</dependency>
<dependency> <dependency>
<groupId>org.mongodb</groupId> <groupId>org.mongodb</groupId>
<artifactId>mongo-java-driver</artifactId> <artifactId>mongo-java-driver</artifactId>
@ -87,6 +92,11 @@
<artifactId>mandrillClient</artifactId> <artifactId>mandrillClient</artifactId>
<version>1.1</version> <version>1.1</version>
</dependency> </dependency>
<dependency>
<groupId>com.twilio.sdk</groupId>
<artifactId>twilio-java-sdk</artifactId>
<version>6.3.0</version>
</dependency>
<dependency> <dependency>
<groupId>org.mindrot</groupId> <groupId>org.mindrot</groupId>
<artifactId>jbcrypt</artifactId> <artifactId>jbcrypt</artifactId>

View File

@ -44,6 +44,7 @@ import org.mongodb.morphia.Datastore;
import org.mongodb.morphia.Morphia; import org.mongodb.morphia.Morphia;
import org.mongodb.morphia.logging.MorphiaLoggerFactory; import org.mongodb.morphia.logging.MorphiaLoggerFactory;
import org.mongodb.morphia.logging.slf4j.SLF4JLoggerImplFactory; import org.mongodb.morphia.logging.slf4j.SLF4JLoggerImplFactory;
import redis.clients.jedis.JedisPool;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.InputStream; import java.io.InputStream;
@ -55,6 +56,7 @@ public final class APIv3 {
@Getter private static Datastore datastore; @Getter private static Datastore datastore;
@Getter private static Properties config = new Properties(); @Getter private static Properties config = new Properties();
@Getter private static JedisPool redisPool;
@Getter private static final Gson gson = new GsonBuilder() @Getter private static final Gson gson = new GsonBuilder()
.registerTypeAdapter(ObjectId.class, new ObjectIdTypeAdapter()) .registerTypeAdapter(ObjectId.class, new ObjectIdTypeAdapter())
.setExclusionStrategies(new FollowAnnotationExclusionStrategy()) .setExclusionStrategies(new FollowAnnotationExclusionStrategy())
@ -65,6 +67,7 @@ public final class APIv3 {
setupConfig(); setupConfig();
setupDatabase(); setupDatabase();
setupRedis();
setupHttp(); setupHttp();
} }
@ -97,6 +100,13 @@ public final class APIv3 {
datastore.ensureIndexes(); datastore.ensureIndexes();
} }
private void setupRedis() {
redisPool = new JedisPool(
config.getProperty("redis.address"),
Integer.parseInt(config.getProperty("redis.port"))
);
}
private void setupHttp() { private void setupHttp() {
ipAddress(config.getProperty("http.address")); ipAddress(config.getProperty("http.address"));
port(Integer.parseInt(config.getProperty("http.port"))); port(Integer.parseInt(config.getProperty("http.port")));
@ -106,6 +116,8 @@ public final class APIv3 {
before(new AuthorizationFilter()); before(new AuthorizationFilter());
exception(Exception.class, new LoggingExceptionHandler()); exception(Exception.class, new LoggingExceptionHandler());
// TODO: The commented out routes
get("/announcements", new GETAnnouncements(), gson::toJson); get("/announcements", new GETAnnouncements(), gson::toJson);
get("/auditLog", new GETAuditLog(), gson::toJson); get("/auditLog", new GETAuditLog(), gson::toJson);
get("/chatFilterList", new GETChatFilterList(), gson::toJson); get("/chatFilterList", new GETChatFilterList(), gson::toJson);
@ -151,8 +163,9 @@ public final class APIv3 {
get("/user/:id/meta/:serverGroup", new GETUserMeta(), gson::toJson); get("/user/:id/meta/:serverGroup", new GETUserMeta(), gson::toJson);
get("/user/:id/grants", new GETUserGrants(), gson::toJson); get("/user/:id/grants", new GETUserGrants(), gson::toJson);
get("/user/:id/ipLog", new GETUserIPLog(), gson::toJson); get("/user/:id/ipLog", new GETUserIPLog(), gson::toJson);
post("/user/:id/verifyTOTP", new POSTUserVerifyTOTP(), gson::toJson); get("/user/:id/requiresTOTP", new GETUserRequiresTOTP(), gson::toJson);
get("/user/:id", new GETUser(), gson::toJson); get("/user/:id", new GETUser(), gson::toJson);
post("/user/:id/verifyTOTP", new POSTUserVerifyTOTP(), gson::toJson);
post("/user/:id:/grant", new POSTUserGrant(), gson::toJson); post("/user/:id:/grant", new POSTUserGrant(), gson::toJson);
post("/user/:id:/punish", new POSTUserPunish(), gson::toJson); post("/user/:id:/punish", new POSTUserPunish(), gson::toJson);
post("/user/:id/login", new POSTUserLogin(), gson::toJson); post("/user/:id/login", new POSTUserLogin(), gson::toJson);

View File

@ -26,6 +26,7 @@ public final class ActorAttributeFilter implements Filter {
} }
} }
@SuppressWarnings("deprecation") // We purposely get the User by their last username.
private Actor processBasicAuthorization(String authHeader) { private Actor processBasicAuthorization(String authHeader) {
String encodedHeader = authHeader.substring("Basic ".length()); String encodedHeader = authHeader.substring("Basic ".length());
String[] credentials = Base64.base64Decode(encodedHeader).split(":"); String[] credentials = Base64.base64Decode(encodedHeader).split(":");

View File

@ -2,6 +2,7 @@ package net.frozenorb.apiv3.routes.auditLog;
import net.frozenorb.apiv3.APIv3; import net.frozenorb.apiv3.APIv3;
import net.frozenorb.apiv3.models.AuditLogEntry; import net.frozenorb.apiv3.models.AuditLogEntry;
import net.frozenorb.apiv3.utils.ErrorUtils;
import spark.Request; import spark.Request;
import spark.Response; import spark.Response;
import spark.Route; import spark.Route;
@ -9,10 +10,14 @@ import spark.Route;
public final class GETAuditLog implements Route { public final class GETAuditLog implements Route {
public Object handle(Request req, Response res) { public Object handle(Request req, Response res) {
try {
int limit = req.queryParams("limit") == null ? 100 : Integer.parseInt(req.queryParams("limit")); int limit = req.queryParams("limit") == null ? 100 : Integer.parseInt(req.queryParams("limit"));
int offset = req.queryParams("offset") == null ? 0 : Integer.parseInt(req.queryParams("offset")); int offset = req.queryParams("offset") == null ? 0 : Integer.parseInt(req.queryParams("offset"));
return APIv3.getDatastore().createQuery(AuditLogEntry.class).order("performedAt").limit(limit).offset(offset).asList(); return APIv3.getDatastore().createQuery(AuditLogEntry.class).order("performedAt").limit(limit).offset(offset).asList();
} catch (NumberFormatException ex) {
return ErrorUtils.invalidInput(";imit and offset must be numerical inputs.");
}
} }
} }

View File

@ -31,7 +31,11 @@ public final class DELETEGrant implements Route {
return ErrorUtils.unauthorized(requiredPermission); return ErrorUtils.unauthorized(requiredPermission);
} }
String reason = req.queryParams("removalReason"); String reason = req.queryParams("reason");
if (reason == null || reason.trim().isEmpty()) {
return ErrorUtils.requiredInput("reason");
}
grant.delete(removedBy, reason); grant.delete(removedBy, reason);
// TODO: Fix IP // TODO: Fix IP

View File

@ -2,6 +2,7 @@ package net.frozenorb.apiv3.routes.grants;
import net.frozenorb.apiv3.APIv3; import net.frozenorb.apiv3.APIv3;
import net.frozenorb.apiv3.models.Grant; import net.frozenorb.apiv3.models.Grant;
import net.frozenorb.apiv3.utils.ErrorUtils;
import spark.Request; import spark.Request;
import spark.Response; import spark.Response;
import spark.Route; import spark.Route;
@ -9,10 +10,14 @@ import spark.Route;
public final class GETGrants implements Route { public final class GETGrants implements Route {
public Object handle(Request req, Response res) { public Object handle(Request req, Response res) {
try {
int limit = req.queryParams("limit") == null ? 100 : Integer.parseInt(req.queryParams("limit")); int limit = req.queryParams("limit") == null ? 100 : Integer.parseInt(req.queryParams("limit"));
int offset = req.queryParams("offset") == null ? 0 : Integer.parseInt(req.queryParams("offset")); int offset = req.queryParams("offset") == null ? 0 : Integer.parseInt(req.queryParams("offset"));
return APIv3.getDatastore().createQuery(Grant.class).order("addedAt").limit(limit).offset(offset).asList(); return APIv3.getDatastore().createQuery(Grant.class).order("addedAt").limit(limit).offset(offset).asList();
} catch (NumberFormatException ex) {
return ErrorUtils.invalidInput("limit and offset must be numerical inputs.");
}
} }
} }

View File

@ -26,8 +26,8 @@ public final class POSTUserGrant implements Route {
String reason = req.queryParams("reason"); String reason = req.queryParams("reason");
if (reason.trim().isEmpty()) { if (reason == null || reason.trim().isEmpty()) {
return ErrorUtils.invalidInput("A reason must be provided."); return ErrorUtils.requiredInput("reason");
} }
Set<ServerGroup> scopes = new HashSet<>(); Set<ServerGroup> scopes = new HashSet<>();

View File

@ -33,6 +33,10 @@ public final class DELETEPunishment implements Route {
String reason = req.queryParams("removalReason"); String reason = req.queryParams("removalReason");
if (reason == null || reason.trim().isEmpty()) {
return ErrorUtils.requiredInput("reason");
}
punishment.delete(removedBy, reason); punishment.delete(removedBy, reason);
// TODO: Fix IP // TODO: Fix IP
AuditLog.log(removedBy, "", req.attribute("actor"), AuditLogActionType.DELETE_PUNISHMENT, new Document("punishmentId", punishment.getId())); AuditLog.log(removedBy, "", req.attribute("actor"), AuditLogActionType.DELETE_PUNISHMENT, new Document("punishmentId", punishment.getId()));

View File

@ -2,6 +2,7 @@ package net.frozenorb.apiv3.routes.punishments;
import net.frozenorb.apiv3.APIv3; import net.frozenorb.apiv3.APIv3;
import net.frozenorb.apiv3.models.Punishment; import net.frozenorb.apiv3.models.Punishment;
import net.frozenorb.apiv3.utils.ErrorUtils;
import spark.Request; import spark.Request;
import spark.Response; import spark.Response;
import spark.Route; import spark.Route;
@ -9,10 +10,14 @@ import spark.Route;
public final class GETPunishments implements Route { public final class GETPunishments implements Route {
public Object handle(Request req, Response res) { public Object handle(Request req, Response res) {
try {
int limit = req.queryParams("limit") == null ? 100 : Integer.parseInt(req.queryParams("limit")); int limit = req.queryParams("limit") == null ? 100 : Integer.parseInt(req.queryParams("limit"));
int offset = req.queryParams("offset") == null ? 0 : Integer.parseInt(req.queryParams("offset")); int offset = req.queryParams("offset") == null ? 0 : Integer.parseInt(req.queryParams("offset"));
return APIv3.getDatastore().createQuery(Punishment.class).order("addedAt").limit(limit).offset(offset).asList(); return APIv3.getDatastore().createQuery(Punishment.class).order("addedAt").limit(limit).offset(offset).asList();
} catch (NumberFormatException ex) {
return ErrorUtils.invalidInput("limit and offset must be numerical inputs.");
}
} }
} }

View File

@ -17,14 +17,16 @@ public final class POSTUserPunish implements Route {
public Object handle(Request req, Response res) { public Object handle(Request req, Response res) {
User target = User.byId(req.params("id")); User target = User.byId(req.params("id"));
// TODO: PROTECTED USERS
if (target == null) { if (target == null) {
return ErrorUtils.notFound("User", req.params("id")); return ErrorUtils.notFound("User", req.params("id"));
} }
String reason = req.queryParams("reason"); String reason = req.queryParams("reason");
if (reason.trim().isEmpty()) { if (reason == null || reason.trim().isEmpty()) {
return ErrorUtils.invalidInput("A reason must be provided."); return ErrorUtils.requiredInput("reason");
} }
Punishment.PunishmentType type = Punishment.PunishmentType.valueOf(req.queryParams("type")); Punishment.PunishmentType type = Punishment.PunishmentType.valueOf(req.queryParams("type"));

View File

@ -4,6 +4,7 @@ import net.frozenorb.apiv3.APIv3;
import net.frozenorb.apiv3.models.Server; import net.frozenorb.apiv3.models.Server;
import net.frozenorb.apiv3.models.ServerGroup; import net.frozenorb.apiv3.models.ServerGroup;
import net.frozenorb.apiv3.utils.ErrorUtils; import net.frozenorb.apiv3.utils.ErrorUtils;
import net.frozenorb.apiv3.utils.IPUtils;
import spark.Request; import spark.Request;
import spark.Response; import spark.Response;
import spark.Route; import spark.Route;
@ -22,6 +23,10 @@ public final class POSTServer implements Route {
return ErrorUtils.notFound("Server group", req.queryParams("group")); return ErrorUtils.notFound("Server group", req.queryParams("group"));
} }
if (!IPUtils.isValidIP(ip)) {
return ErrorUtils.invalidInput("IP address \"" + ip + "\" is not valid.");
}
Server server = new Server(id, bungeeId, displayName, apiKey, group, ip); Server server = new Server(id, bungeeId, displayName, apiKey, group, ip);
APIv3.getDatastore().save(server); APIv3.getDatastore().save(server);
return server; return server;

View File

@ -1,20 +1,14 @@
package net.frozenorb.apiv3.routes.servers; package net.frozenorb.apiv3.routes.servers;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.librato.metrics.LibratoBatch;
import net.frozenorb.apiv3.APIv3;
import net.frozenorb.apiv3.actors.Actor; import net.frozenorb.apiv3.actors.Actor;
import net.frozenorb.apiv3.actors.ActorType; import net.frozenorb.apiv3.actors.ActorType;
import net.frozenorb.apiv3.models.Server; import net.frozenorb.apiv3.models.Server;
import net.frozenorb.apiv3.models.User;
import net.frozenorb.apiv3.utils.ErrorUtils; import net.frozenorb.apiv3.utils.ErrorUtils;
import org.bson.Document;
import spark.Request; import spark.Request;
import spark.Response; import spark.Response;
import spark.Route; import spark.Route;
import java.util.*;
public final class POSTServerMetrics implements Route { public final class POSTServerMetrics implements Route {
public Object handle(Request req, Response res) { public Object handle(Request req, Response res) {

View File

@ -20,7 +20,8 @@ public final class GETUserDetails implements Route {
"user", user, "user", user,
"grants", user.getGrants(), "grants", user.getGrants(),
"ipLog", user.getIPLog(), "ipLog", user.getIPLog(),
"punishments", user.getPunishments() "punishments", user.getPunishments(),
"totpRequired", user.getTotpSecret() != null
); );
} }

View File

@ -0,0 +1,4 @@
package net.frozenorb.apiv3.routes.users;
public class GETUserRequiresTOTP {
}

View File

@ -6,6 +6,7 @@ import net.frozenorb.apiv3.actors.ActorType;
import net.frozenorb.apiv3.models.Server; import net.frozenorb.apiv3.models.Server;
import net.frozenorb.apiv3.models.User; import net.frozenorb.apiv3.models.User;
import net.frozenorb.apiv3.utils.ErrorUtils; import net.frozenorb.apiv3.utils.ErrorUtils;
import net.frozenorb.apiv3.utils.IPUtils;
import spark.Request; import spark.Request;
import spark.Response; import spark.Response;
import spark.Route; import spark.Route;
@ -24,6 +25,10 @@ public final class POSTUserLogin implements Route {
return ErrorUtils.serverOnly(); return ErrorUtils.serverOnly();
} }
if (!IPUtils.isValidIP(userIp)) {
return ErrorUtils.invalidInput("IP address \"" + userIp + "\" is not valid.");
}
if (user == null) { if (user == null) {
user = new User(UUID.fromString(req.params("id")), username); user = new User(UUID.fromString(req.params("id")), username);
APIv3.getDatastore().save(user); APIv3.getDatastore().save(user);

View File

@ -19,9 +19,12 @@ import java.util.regex.Pattern;
public final class POSTUserRegister implements Route { public final class POSTUserRegister implements Route {
private static final Pattern VALID_EMAIL_ADDRESS_REGEX = private static final Pattern VALID_EMAIL_PATTERN =
Pattern.compile("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6}$", Pattern.CASE_INSENSITIVE); Pattern.compile("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6}$", Pattern.CASE_INSENSITIVE);
// TODO: POSSIBLE? perms check
// TODO: make messages better
public Object handle(Request req, Response res) { public Object handle(Request req, Response res) {
User user = User.byId(req.params("id")); User user = User.byId(req.params("id"));
@ -35,7 +38,7 @@ public final class POSTUserRegister implements Route {
String email = req.queryParams("email"); String email = req.queryParams("email");
if (!VALID_EMAIL_ADDRESS_REGEX.matcher(email).find()) { if (!VALID_EMAIL_PATTERN.matcher(email).matches()) {
return ErrorUtils.error(email + " is not a valid email."); return ErrorUtils.error(email + " is not a valid email.");
} }

View File

@ -15,7 +15,7 @@ public final class LoggingExceptionHandler implements ExceptionHandler {
String code = new ObjectId().toHexString(); String code = new ObjectId().toHexString();
log.error(code + ":", ex); log.error(code + ":", ex);
res.body(APIv3.getGson().toJson(ErrorUtils.error("An unknown error has occurred. Please contact a developer with the code \"" + code + "\"."))); res.body(APIv3.getGson().toJson(ErrorUtils.error("An unknown error has occurred. Please contact an API developer with the code \"" + code + "\".")));
} }
} }

View File

@ -45,6 +45,7 @@ public final class Notification {
} }
public void sendAsText(String phoneNumber) throws IOException { public void sendAsText(String phoneNumber) throws IOException {
// TODO
throw new IOException(new NotImplementedException()); throw new IOException(new NotImplementedException());
} }

View File

@ -9,6 +9,6 @@ public class Permissions {
public static final String CREATE_GRANT = "minehq.grant.create"; // minehq.grant.create.%RANK% public static final String CREATE_GRANT = "minehq.grant.create"; // minehq.grant.create.%RANK%
public static final String REMOVE_PUNISHMENT = "minehq.punishment.remove"; // minehq.punishment.remove.%TYPE% public static final String REMOVE_PUNISHMENT = "minehq.punishment.remove"; // minehq.punishment.remove.%TYPE%
public static final String CREATE_PUNISHMENT = "minehq.punishment.create"; // minehq.punishment.remove.%TYPE% public static final String CREATE_PUNISHMENT = "minehq.punishment.create"; // minehq.punishment.create.%TYPE%
} }

View File

@ -24,6 +24,10 @@ public class ErrorUtils {
return error("Invalid input: " + message); return error("Invalid input: " + message);
} }
public static Map<String, Object> requiredInput(String field) {
return error("Field \"" + field + "\" is required.");
}
public static Map<String, Object> error(String message) { public static Map<String, Object> error(String message) {
return ImmutableMap.of( return ImmutableMap.of(
"success", false, "success", false,

View File

@ -0,0 +1,20 @@
package net.frozenorb.apiv3.utils;
import lombok.experimental.UtilityClass;
import java.util.regex.Pattern;
@UtilityClass
public class IPUtils {
private static final Pattern VALID_IP_PATTERN = Pattern.compile(
"^([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." +
"([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." +
"([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." +
"([01]?\\d\\d?|2[0-4]\\d|25[0-5])$");
public static boolean isValidIP(String ip) {
return ip != null && VALID_IP_PATTERN.matcher(ip).matches();
}
}

View File

@ -1,12 +1,14 @@
package net.frozenorb.apiv3.utils; package net.frozenorb.apiv3.utils;
import com.google.common.collect.ImmutableList;
import com.warrenstrange.googleauth.GoogleAuthenticator; import com.warrenstrange.googleauth.GoogleAuthenticator;
import com.warrenstrange.googleauth.GoogleAuthenticatorKey; import com.warrenstrange.googleauth.GoogleAuthenticatorKey;
import com.warrenstrange.googleauth.GoogleAuthenticatorQRGenerator; import com.warrenstrange.googleauth.GoogleAuthenticatorQRGenerator;
import lombok.experimental.UtilityClass; import lombok.experimental.UtilityClass;
import net.frozenorb.apiv3.APIv3;
import net.frozenorb.apiv3.models.User; import net.frozenorb.apiv3.models.User;
import org.apache.http.client.utils.URIBuilder; import redis.clients.jedis.Jedis;
import java.util.concurrent.TimeUnit;
@UtilityClass @UtilityClass
public class TOTPUtils { public class TOTPUtils {
@ -23,18 +25,40 @@ public class TOTPUtils {
public static String getQRCodeURL(User user, GoogleAuthenticatorKey key) { public static String getQRCodeURL(User user, GoogleAuthenticatorKey key) {
return GoogleAuthenticatorQRGenerator.getOtpAuthURL( return GoogleAuthenticatorQRGenerator.getOtpAuthURL(
"MineHQ v3", "MineHQ Network",
user.getLastUsername(), user.getLastUsername(),
key key
); );
} }
public static boolean wasRecentlyUsed(User user, int code) { public static boolean isPreAuthorized(User user, String ip) {
try (Jedis redis = APIv3.getRedisPool().getResource()) {
return redis.exists(user.getId() + ":preAuthorizedIP:" + ip.toLowerCase());
}
}
public static void markPreAuthorized(User user, String ip, long duration, TimeUnit unit) {
try (Jedis redis = APIv3.getRedisPool().getResource()) {
String key = user.getId() + ":preAuthorizedIP:" + ip.toLowerCase();
redis.set(key, "");
redis.expire(key, (int) unit.toSeconds(duration));
}
}
public static boolean wasRecentlyUsed(User user, int code) {
try (Jedis redis = APIv3.getRedisPool().getResource()) {
return redis.exists(user.getId() + ":recentTOTPCodes:" + code);
}
} }
public static void markRecentlyUsed(User user, int code) { public static void markRecentlyUsed(User user, int code) {
try (Jedis redis = APIv3.getRedisPool().getResource()) {
String key = user.getId() + ":recentTOTPCodes:" + code;
redis.set(key, "");
redis.expire(key, (int) TimeUnit.MINUTES.toSeconds(5));
}
} }
} }