package net.mindoverflow.webmarker.webserver.controllers; import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTVerificationException; import com.auth0.jwt.interfaces.Claim; import com.auth0.jwt.interfaces.DecodedJWT; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import net.mindoverflow.webmarker.utils.FileUtils; import net.mindoverflow.webmarker.utils.config.ConfigEntries; import net.mindoverflow.webmarker.utils.security.SafetyCheck; import net.mindoverflow.webmarker.utils.sql.MarkerSQLUtils; import ro.pippo.controller.Controller; import ro.pippo.controller.POST; import ro.pippo.controller.Path; import ro.pippo.core.route.RouteContext; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.time.*; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.util.UUID; @Path("/api/v1/store") public class StorageController extends Controller { private RouteContext routeContext; @POST public void store() { // Load RouteContext routeContext = getRouteContext(); // Load JSON object from body String body = routeContext.getRequest().getBody(); JsonObject jsonObject = FileUtils.stringToJson(body); // Load JWT element JsonElement jwtElement = getJwtFromJson(jsonObject); if(jwtElement == null) return; // Decrypt/Verify JWT DecodedJWT jwt = decryptAndVerifyJwt(jwtElement); if(jwt == null) return; // Load username from JWT String username = getFromJwt(jwt, "username"); if(username == null) return; // Check some stuff about the user if(!usernameCheckPassed(username)) return; // Load UUID from username UUID uuid = MarkerSQLUtils.getUserUUID(username); if(uuid == null) { routeContext.send("Server error: missing UUID!"); return; } // Load url from Json body String url = getFromJson(jsonObject, "url"); if(url == null) return; // Verify url validity URI confirmedUrl = stringToURI(url); if(confirmedUrl == null) return; // Load ZonedDateTime from Json body String zonedDateTimeStr = getFromJson(jsonObject, "timestamp"); if(zonedDateTimeStr == null) return; // Transform String to ZonedDateTime ZonedDateTime timestamp = stringToZDT(zonedDateTimeStr); if(timestamp == null) return; System.out.println("Zoned Timestamp: " + timestamp.toString()); // Check if the timestamp is too old to be true ZonedDateTime now = ZonedDateTime.now(); System.out.println("NOW Timestamp: " + now.toString()); if(!dateCheckPassed(timestamp, now)) return; // Transform whatever GMT it's on to GMT+0 LocalDateTime timestampUTC = LocalDateTime.ofInstant(timestamp.toInstant(), ZoneOffset.UTC); System.out.println("UTC Timestamp: " + timestampUTC.toString()); MarkerSQLUtils.addHistoryRecord(uuid, confirmedUrl.toString(), timestampUTC); routeContext.send("OK!"); } // todo: move to safety class? private boolean dateCheckPassed(ZonedDateTime timestamp, ZonedDateTime now) { // allow up to 5 minutes in the past and 1 minute in the future //if(ChronoUnit.MINUTES.between(timestamp, now) > 5) todo: decide if(Duration.between(timestamp, now).getSeconds() >= 5 * 60 || Duration.between(timestamp, now).getSeconds() <= -1 * 60) { routeContext.send("Timestamp out of sync!"); return false; } return true; } private JsonElement getJwtFromJson(JsonObject jsonObject) { JsonElement jwtElement = jsonObject.get("jwt"); if(jwtElement == null || jwtElement.isJsonNull()) { routeContext.send("Invalid JWT!"); //todo: throw exception instead? return null; } return jwtElement; } private DecodedJWT decryptAndVerifyJwt(JsonElement jwtElement) { String token = jwtElement.getAsString(); try { Algorithm algorithm = Algorithm.HMAC256((String) ConfigEntries.JWT_SECRET.getValue()); JWTVerifier jwtVerifier = JWT.require(algorithm) .build(); return jwtVerifier.verify(token); } catch (JWTVerificationException e) { routeContext.send("Invalid JWT!"); return null; } } private String getFromJwt(DecodedJWT jwt, String claimName) { Claim claim = jwt.getClaim(claimName); if(claim == null || claim.isNull()) { routeContext.send("JWT missing '" + claimName + "' claim!"); return null; } return claim.asString(); } // todo: move to safety class? private boolean usernameCheckPassed(String username) { if(!SafetyCheck.isSafeUsername(username)) { routeContext.send("Invalid username!"); return false; } if(!MarkerSQLUtils.userExists(username)) { routeContext.send("User does not exist!"); return false; } return true; } private String getFromJson(JsonObject jsonObject, String name) { JsonElement jsonElement = jsonObject.get(name); if(jsonElement == null || jsonElement.isJsonNull()) { routeContext.send("JSON body missing '" + name + "' entry!"); return null; } return jsonElement.getAsString(); } private URI stringToURI(String url) { try { return new URL(url).toURI(); } catch (URISyntaxException e) { routeContext.send("Invalid URI!"); } catch (MalformedURLException e) { routeContext.send("Invalid URL!"); } return null; } private ZonedDateTime stringToZDT(String timestamp) { DateTimeFormatter formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME; return ZonedDateTime.parse(timestamp, formatter); // todo: error check } }