package net.rainbowcode.jpixelface;
import com.google.gson.JsonParseException;
import net.rainbowcode.jpixelface.exceptions.InvalidIdException;
import net.rainbowcode.jpixelface.exceptions.MojangException;
import net.rainbowcode.jpixelface.exceptions.ScaleOutOfBoundsException;
import net.rainbowcode.jpixelface.profile.ProfileFuture;
import net.rainbowcode.jpixelface.profile.ProfileRequestThread;
import net.rainbowcode.jpixelface.redis.RedisUtils;
import net.rainbowcode.jpixelface.routes.MutateRoute;
import net.rainbowcode.jpixelface.routes.ProfileRoute;
import net.rainbowcode.jpixelface.skin.Mutate;
import net.rainbowcode.jpixelface.skin.SkinManager;
import net.rainbowcode.jpixelface.svg.SVGGenerator;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import spark.Response;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
import static spark.Spark.*;
public final class HttpServer
{
static int PORT = 8000;
public static AtomicInteger requestCounter = new AtomicInteger(600);
public static Thread tickCounter = new Thread()
{
Logger logger = LogManager.getLogger("MojangRequestCounter");
@Override
public void run()
{
while (true)
{
try
{
sleep(1000);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
int i = HttpServer.requestCounter.incrementAndGet();
if (HttpServer.requestCounter.get() < 50)
{
logger.warn("Low amount of requests available to mojang = " + i);
}
}
}
};
private static final Pattern NAME = Pattern.compile("^[A-Za-z0-9_]{2,16}$");
private static final Pattern UUID_PATTERN = Pattern.compile("^[0-9a-f]{8}[0-9a-f]{4}[1-5][0-9a-f]{3}[89ab][0-9a-f]{3}[0-9a-f]{12}$");
private static final Pattern REAL_UUID_PATTERN = Pattern.compile("^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$");
public SkinManager skinManager = new SkinManager();
public ProfileRequestThread requestThread = new ProfileRequestThread();
private Logger log = LogManager.getLogger("HttpServer");
public HttpServer()
{
log.info("Starting profile request thread.");
requestThread.start();
}
public void init()
{
staticFileLocation("/public");
port(PORT);
before((request1, response1) -> {
response1.header("X-host", System.getenv("HOSTNAME"));
String key = "cache:" + request1.uri();
response1.header("Cache-Control", "public, max-age=86400");
ZonedDateTime now = LocalDateTime.now().atZone(ZoneId.of("GMT"));
String oldAge = now.format(DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss zzz"));
boolean shouldHalt = false;
if (RedisUtils.exists(key))
{
oldAge = RedisUtils.getAsString(key);
String modified = request1.headers("If-Modified-Since");
if (modified != null && modified.equals(oldAge))
{
shouldHalt = true;
}
}
else
{
RedisUtils.setAndExpire(key, oldAge, 14400);
}
response1.header("Last-Modified", oldAge);
if (shouldHalt)
{
halt(304);
}
});
for (Mutate mutate : Mutate.values())
{
MutateRoute mutateRoute = new MutateRoute(mutate);
mutateRoute.init(this);
log.info("Initialised route for mutator: " + mutate.name());
}
new ProfileRoute().init(this);
log.info("Initialised profile route");
// Client errors
exception(NumberFormatException.class, (e, request, response) -> {
response.status(Errors.NUMBER_FORMAT_EXCEPTION.getCode());
response.body(Errors.NUMBER_FORMAT_EXCEPTION.getText());
});
exception(InvalidIdException.class, (e, request, response) -> {
response.status(Errors.ID_NOT_VALID.getCode());
response.body(Errors.ID_NOT_VALID.getText());
});
exception(ScaleOutOfBoundsException.class, (e, request, response) -> {
ScaleOutOfBoundsException exception = (ScaleOutOfBoundsException) e;
response.status(Errors.SIZE_TOO_BIG_OR_TOO_SMALL.getCode());
response.body(String.format(Errors.SIZE_TOO_BIG_OR_TOO_SMALL.getText(), exception.getMinScale(), exception.getMaxScale()));
});
//Server errors
exception(MojangException.class, (e, request, response) -> {
MojangException mojangException = (MojangException) e;
response.status(Errors.MOJANG.getCode());
response.body(String.format(Errors.MOJANG.getText(), mojangException.getCode(), mojangException.getPath()));
});
exception(IOException.class, (e, request, response) -> {
response.status(Errors.INTERNAL.getCode());
response.body(Errors.INTERNAL.getText());
e.printStackTrace();
});
exception(JsonParseException.class, (e, request, response) -> {
response.status(Errors.INTERNAL.getCode());
response.body(Errors.INTERNAL.getText());
e.printStackTrace();
});
}
public static void main(String[] args) throws Exception
{
if (args.length == 1){
PORT = Integer.parseInt(args[0]);
}
tickCounter.start();
HttpServer server = new HttpServer();
server.init();
}
public ProfileFuture getProfile(String id) throws InvalidIdException
{
ProfileFuture future = null;
if (NAME.matcher(id).find())
{
future = requestThread.getProfileByName(id);
}
else if (UUID_PATTERN.matcher(id).find())
{
future = requestThread.getProfileByMojangID(id);
}
else if (REAL_UUID_PATTERN.matcher(id).find())
{
future = requestThread.getProfileByUUID(id);
}
if (future != null)
{
return future;
}
throw new InvalidIdException();
}
public String handleSVG(Response response, ProfileFuture future, Mutate mutate) throws Exception
{
future.await();
if (future.getException() != null && future.getException() instanceof MojangException)
{
MojangException mojangException = (MojangException) future.getException();
if (mojangException.getCode() == 204) // Handle people without profile
{
response.redirect(mutate.getPath() + "MHF_Steve.svg");
return "";
}
}
response.type("image/svg+xml");
return SVGGenerator.convert(skinManager.getBufferedMutated(future.get(), mutate.getSvgScale(), mutate));
}
public Response handleImage(Response response, ProfileFuture future, int size, Mutate mutate) throws Exception
{
response.type("image/png");
HttpServletResponse raw = response.raw();
future.await();
if (future.getException() != null && future.getException() instanceof MojangException)
{
MojangException mojangException = (MojangException) future.getException();
if (mojangException.getCode() == 204) // Handle people without profile
{
response.redirect(mutate.getPath() + "MHF_Steve" + "/" + size + ".png");
return response;
}
}
raw.getOutputStream().write(skinManager.getMutated(future.get(), size, mutate));
raw.getOutputStream().flush();
raw.getOutputStream().close();
return response;
}
}