Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ dependencies {
implementation libs.webjar.chartjs
implementation libs.webjar.chartjs.plugin.autocolors
implementation libs.webjar.humanize.duration
implementation(libs.webjar.glightbox)
implementation(libs.webjar.plyr)

implementation libs.mongodb.driver
implementation libs.mongojack
Expand Down
5 changes: 4 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ unpoly = "3.3.0"
chartjs = "4.2.1"
chartjs-plugin-autocolors = "0.2.2"
humanize-duration = "3.28.0"
glightbox = "3.2.0"
plyr = "3.7.8"

[libraries]
javalin = { module = "io.javalin:javalin", version.ref = "javalin" }
Expand Down Expand Up @@ -78,7 +80,8 @@ webjar-unpoly = { module = "org.webjars.npm:unpoly", version.ref = "unpoly" }
webjar-chartjs = { module = "org.webjars.npm:chart.js", version.ref = "chartjs" }
webjar-chartjs-plugin-autocolors = { module = "org.webjars.npm:chartjs-plugin-autocolors", version.ref = "chartjs-plugin-autocolors" }
webjar-humanize-duration = { module = "org.webjars.npm:humanize-duration", version.ref = "humanize-duration" }

webjar-glightbox = { module = "org.webjars.npm:glightbox", version.ref = "glightbox" }
webjar-plyr = { module = "org.webjars.npm:plyr", version.ref = "plyr" }

[plugins]
jte-gradle = { id = "gg.jte.gradle", version.ref = "jte" }
Expand Down
61 changes: 14 additions & 47 deletions src/main/java/com/github/khakers/modmailviewer/Main.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.github.khakers.modmailviewer;

import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.github.khakers.modmailviewer.attachments.MongoAttachmentClient;
import com.github.khakers.modmailviewer.auditlog.AuditEventDAO;
import com.github.khakers.modmailviewer.auditlog.MongoAuditEventLogger;
import com.github.khakers.modmailviewer.auditlog.NoopAuditEventLogger;
Expand All @@ -9,6 +10,7 @@
import com.github.khakers.modmailviewer.auth.Role;
import com.github.khakers.modmailviewer.configuration.AppConfig;
import com.github.khakers.modmailviewer.configuration.CSPConfig;
import com.github.khakers.modmailviewer.configuration.Config;
import com.github.khakers.modmailviewer.configuration.SSLConfig;
import com.github.khakers.modmailviewer.log.LogController;
import com.github.khakers.modmailviewer.markdown.channelmention.ChannelMentionExtension;
Expand All @@ -18,6 +20,7 @@
import com.github.khakers.modmailviewer.markdown.underline.UnderlineExtension;
import com.github.khakers.modmailviewer.markdown.usermention.UserMentionExtension;
import com.github.khakers.modmailviewer.page.admin.AdminController;
import com.github.khakers.modmailviewer.page.attachment.AttachmentController;
import com.github.khakers.modmailviewer.page.audit.AuditController;
import com.github.khakers.modmailviewer.page.dashboard.DashboardController;
import com.github.khakers.modmailviewer.page.dashboard.MetricsAccessor;
Expand Down Expand Up @@ -49,13 +52,7 @@
import org.apache.logging.log4j.Logger;
import org.bson.codecs.configuration.CodecRegistries;
import org.bson.codecs.pojo.PojoCodecProvider;
import org.github.gestalt.config.Gestalt;
import org.github.gestalt.config.builder.GestaltBuilder;
import org.github.gestalt.config.exceptions.GestaltException;
import org.github.gestalt.config.path.mapper.SnakeCasePathMapper;
import org.github.gestalt.config.source.ClassPathConfigSourceBuilder;
import org.github.gestalt.config.source.EnvironmentConfigSourceBuilder;
import org.github.gestalt.config.source.SystemPropertiesConfigSourceBuilder;
import org.jetbrains.annotations.NotNull;

import java.net.URI;
Expand Down Expand Up @@ -102,47 +99,13 @@ public class Main {

public static void main(String[] args) throws GestaltException {

Gestalt gestalt = new GestaltBuilder()
.setTreatNullValuesInClassAsErrors(true)
.setTreatMissingValuesAsErrors(false)
.addSource(ClassPathConfigSourceBuilder.builder()
.setResource("/default.properties")
.build()) // Load the default property files from resources.
.addSource(EnvironmentConfigSourceBuilder.builder()
.setPrefix(envPrepend)
.setRemovePrefix(true)
.build())
.addSource(SystemPropertiesConfigSourceBuilder.builder()
.setFailOnErrors(false)
.build())
.addDefaultPathMappers()
.addPathMapper(new SnakeCasePathMapper())
.build();

try {
gestalt.loadConfigs();
} catch (GestaltException e) {
logger.fatal(e);
System.exit(1);
throw new RuntimeException();
}

AppConfig appConfigInit;
try {
appConfigInit = gestalt.getConfig("app", AppConfig.class);
} catch (GestaltException e) {
logger.fatal(e);
System.exit(1);
throw new RuntimeException();
}
var appConfig = appConfigInit;
var appConfig = Config.appConfig;
logger.debug(appConfig.toString());
var auditLogConfig = appConfig.auditLogConfig();
// var cspConfig = appConfig.cspConfig();
var authConfig = appConfig.auth().orElse(null);



TemplateEngine templateEngine;

if (appConfig.dev()) {
Expand Down Expand Up @@ -206,6 +169,7 @@ public static void main(String[] args) throws GestaltException {
var logController = new LogController(auditLogger);
var dashboardController = new DashboardController();

var attachmentController = new AttachmentController(new MongoAttachmentClient(mongoClientDatabase));

var app = Javalin.create(javalinConfig -> {
try {
Expand Down Expand Up @@ -237,6 +201,7 @@ public static void main(String[] args) throws GestaltException {
.get("/dashboard", dashboardController.serveDashboardPage, RoleUtils.atLeastSupporter())
.get("/admin", adminController.serveAdminPage, RoleUtils.atLeastAdministrator())
.get("/audit/{id}", auditController.serveAuditPage, RoleUtils.atLeastAdministrator())
.get("/attachment/{id}", attachmentController.getHandler(), RoleUtils.atLeastSupporter())
.after("/api/*", ctx -> {
if (auditLogConfig.isApiAuditingEnabled()) {
if (ctx.statusCode() == HttpStatus.FORBIDDEN.getCode()) {
Expand Down Expand Up @@ -274,7 +239,7 @@ private static void configure(JavalinConfig config, AppConfig appConfig) throws

config.showJavalinBanner = false;
config.jsonMapper(new JavalinJackson().updateMapper(objectMapper -> objectMapper.registerModule(new Jdk8Module())));
config.plugins.enableGlobalHeaders(() -> configureHeaders(sslOptions.get(), cspConfig));
config.plugins.enableGlobalHeaders(() -> configureHeaders(sslOptions.get(), cspConfig, appConfig));
if (sslOptions.isPresent() && sslOptions.get().httpsOnly()) {
logger.info("HTTPS only is ENABLED");
config.plugins.enableSslRedirects();
Expand Down Expand Up @@ -333,7 +298,7 @@ private static SSLPlugin getSslPlugin(AppConfig appConfig, SSLConfig sslOptions)
});
}

private static GlobalHeaderConfig configureHeaders(SSLConfig sslOptions, CSPConfig cspConfig) {
private static GlobalHeaderConfig configureHeaders(SSLConfig sslOptions, CSPConfig cspConfig, AppConfig appConfig) {
var globalHeaderConfig = new GlobalHeaderConfig();
globalHeaderConfig.xFrameOptions(GlobalHeaderConfig.XFrameOptions.DENY);
globalHeaderConfig.xContentTypeOptionsNoSniff();
Expand All @@ -347,11 +312,13 @@ private static GlobalHeaderConfig configureHeaders(SSLConfig sslOptions, CSPConf
globalHeaderConfig.contentSecurityPolicy(cspConfig.override().get());
} else {
globalHeaderConfig.contentSecurityPolicy(String.format(
"default-src 'self'; " +
"img-src * 'self' data:; " +
"default-src 'self'; " +
"img-src * 'self' data: "+appConfig.s3Url().orElse("")+"; " +
"object-src 'none'; " +
"media-src media.discordapp.com; " +
"style-src-attr 'unsafe-hashes' 'self' 'sha256-biLFinpqYMtWHmXfkA1BPeCY0/fNt46SAZ+BBk5YUog='; " +
"media-src 'self' media.discordapp.com cdn.discordapp.com "+appConfig.s3Url().orElse("")+"; " +
"style-src-attr 'unsafe-hashes' 'self' 'sha256-biLFinpqYMtWHmXfkA1BPeCY0/fNt46SAZ+BBk5YUog=' 'sha256-ubXkvHkNI/o3njlOwWcW1Nrt3/3G2eJn8mN1u9LCnXo='; " +
"style-src 'self' 'sha256-Jt4TB/uiervjq+0TSAyeKjWbMJlLUrE4uXVVOyC/xQA='; "+
"frame-src 'self' https://cdn.discordapp.com https://media.discordapp.com "+appConfig.s3Url().orElse("")+"; " +
"script-src-elem 'self' https://cdn.jsdelivr.net/npm/@twemoji/[email protected]/dist/twemoji.min.js %s;",
cspConfig.extraScriptSources().orElse("")));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.github.khakers.modmailviewer.attachments;

public interface AttachmentClient {
AttachmentResult getAttachment(long id) throws AttachmentNotFoundException, UnsupportedAttachmentException;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.github.khakers.modmailviewer.attachments;

public class AttachmentNotFoundException extends Exception {
public AttachmentNotFoundException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.github.khakers.modmailviewer.attachments;

public record AttachmentResult(
byte[] attachmentData,
String content_type

) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.github.khakers.modmailviewer.attachments;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.bson.codecs.pojo.annotations.BsonProperty;
import org.jetbrains.annotations.Nullable;

import java.time.Instant;
@JsonIgnoreProperties(ignoreUnknown = true)
public record MongoAttachment(
@BsonProperty("_id")
@JsonProperty("_id")
long id,
@BsonProperty("content_type")
@JsonProperty("content_type")
String contentType,
byte[] data,
// @Nullable String description,
String filename,
int size,
@Nullable
Integer height,
@Nullable
Integer width,
@BsonProperty("uploaded_at")
@JsonProperty("uploaded_at")
Instant uploadTime

) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.github.khakers.modmailviewer.attachments;

import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.Filters;
import org.bson.UuidRepresentation;
import org.mongojack.JacksonMongoCollection;

public class MongoAttachmentClient implements AttachmentClient {
protected MongoDatabase mongoDatabase;
protected JacksonMongoCollection<MongoAttachment> collection;

public MongoAttachmentClient(MongoDatabase mongoDatabase) {
this.mongoDatabase = mongoDatabase;
// I wanted to use the mongodb java driver's pojo support, but it doesn't support integer types being null...
// So I'm using mongojack instead
this.collection = JacksonMongoCollection.builder().build(mongoDatabase, "attachments", MongoAttachment.class, UuidRepresentation.STANDARD);
}

@Override
public AttachmentResult getAttachment(long id) throws AttachmentNotFoundException {
var result = collection.find(Filters.eq("_id", id)).first();
if (result == null) {
throw new AttachmentNotFoundException("attachment of id " + id + " not found");
}

return new AttachmentResult(result.data(), result.contentType());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.github.khakers.modmailviewer.attachments;

/**
* The attachment was found, but does not contain all the data required to properly return it.
*/
public class UnsupportedAttachmentException extends Exception{
public UnsupportedAttachmentException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ public record AppConfig(
CSPConfig cspConfig,
long botId,
String branding,
Optional<String> analytics
Optional<String> analytics,

Optional<String> s3Url
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,7 @@
import org.github.gestalt.config.builder.GestaltBuilder;
import org.github.gestalt.config.exceptions.GestaltException;
import org.github.gestalt.config.path.mapper.SnakeCasePathMapper;
import org.github.gestalt.config.source.ClassPathConfigSource;
import org.github.gestalt.config.source.EnvironmentConfigSource;
import org.github.gestalt.config.source.FileConfigSource;
import org.github.gestalt.config.source.SystemPropertiesConfigSource;
import org.github.gestalt.config.source.*;

import java.io.File;

Expand All @@ -26,7 +23,9 @@ public class Config {
var gestaltBuilder = new GestaltBuilder()
.setTreatNullValuesInClassAsErrors(true)
.setTreatMissingValuesAsErrors(false)
.addSource(new ClassPathConfigSource("default.properties"));
.addSource(ClassPathConfigSourceBuilder.builder()
.setResource("/default.properties")
.build()); // Load the default property files from resources.

if (configURI != null) {
File file = new File(configURI);
Expand All @@ -40,11 +39,16 @@ public class Config {

}

gestalt = gestaltBuilder.addSource(new EnvironmentConfigSource(Main.envPrepend))
.addSource(new SystemPropertiesConfigSource())
gestalt = gestaltBuilder
.addSource(EnvironmentConfigSourceBuilder.builder()
.setPrefix(Main.envPrepend)
.setRemovePrefix(true)
.build())
.addSource(SystemPropertiesConfigSourceBuilder.builder()
.setFailOnErrors(false)
.build())
.addDefaultPathMappers()
.addPathMapper(new SnakeCasePathMapper())
// .addSource(new FileConfigSource(devFile))
.build();

} catch (GestaltException e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.github.khakers.modmailviewer.configuration;

/**
* Thrown when the supplied configuration is invalid
*/
public class InvalidConfigurationException extends Exception {
public InvalidConfigurationException(String message) {
super(message);
}

public InvalidConfigurationException(String message, Throwable cause) {
super(message, cause);
}

public InvalidConfigurationException(Throwable cause) {
super(cause);
}

public InvalidConfigurationException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
Loading