(control) Add basic input validation to node actions

Will present a simple error message when required fields aren't populated, instead of a cryptic HTTP status error.
This commit is contained in:
Viktor Lofgren 2024-01-18 11:30:17 +01:00
parent aa2df327db
commit 19e781b104
3 changed files with 74 additions and 34 deletions

View file

@ -14,6 +14,7 @@ import nu.marginalia.executor.upload.UploadDirContents;
import nu.marginalia.model.gson.GsonFactory; import nu.marginalia.model.gson.GsonFactory;
import nu.marginalia.service.descriptor.ServiceDescriptors; import nu.marginalia.service.descriptor.ServiceDescriptors;
import nu.marginalia.service.id.ServiceId; import nu.marginalia.service.id.ServiceId;
import nu.marginalia.storage.model.FileStorage;
import nu.marginalia.storage.model.FileStorageId; import nu.marginalia.storage.model.FileStorageId;
import java.io.OutputStream; import java.io.OutputStream;
@ -21,7 +22,6 @@ import java.net.URLEncoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
public class ExecutorClient extends AbstractDynamicClient { public class ExecutorClient extends AbstractDynamicClient {
@ -97,13 +97,13 @@ public class ExecutorClient extends AbstractDynamicClient {
.blockingSubscribe(); .blockingSubscribe();
} }
public void exportAtags(Context ctx, int node, String fid) { public void exportAtags(Context ctx, int node, FileStorageId fid) {
post(ctx, node, "/export/atags?fid="+fid, "").blockingSubscribe(); post(ctx, node, "/export/atags?fid="+fid, "").blockingSubscribe();
} }
public void exportRssFeeds(Context ctx, int node, String fid) { public void exportRssFeeds(Context ctx, int node, FileStorageId fid) {
post(ctx, node, "/export/feeds?fid="+fid, "").blockingSubscribe(); post(ctx, node, "/export/feeds?fid="+fid, "").blockingSubscribe();
} }
public void exportTermFrequencies(Context ctx, int node, String fid) { public void exportTermFrequencies(Context ctx, int node, FileStorageId fid) {
post(ctx, node, "/export/termfreq?fid="+fid, "").blockingSubscribe(); post(ctx, node, "/export/termfreq?fid="+fid, "").blockingSubscribe();
} }
@ -111,7 +111,7 @@ public class ExecutorClient extends AbstractDynamicClient {
post(ctx, node, "/export/data", "").blockingSubscribe(); post(ctx, node, "/export/data", "").blockingSubscribe();
} }
public void restoreBackup(Context context, int node, String fid) { public void restoreBackup(Context context, int node, FileStorageId fid) {
post(context, node, "/backup/" + fid + "/restore", "").blockingSubscribe(); post(context, node, "/backup/" + fid + "/restore", "").blockingSubscribe();
} }

View file

@ -3,6 +3,7 @@ package nu.marginalia.control.node.svc;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Singleton; import com.google.inject.Singleton;
import nu.marginalia.client.Context; import nu.marginalia.client.Context;
import nu.marginalia.control.ControlValidationError;
import nu.marginalia.control.RedirectControl; import nu.marginalia.control.RedirectControl;
import nu.marginalia.executor.client.ExecutorClient; import nu.marginalia.executor.client.ExecutorClient;
import nu.marginalia.executor.model.load.LoadParameters; import nu.marginalia.executor.model.load.LoadParameters;
@ -87,12 +88,16 @@ public class ControlNodeActionsService {
); );
} }
public Object sideloadEncyclopedia(Request request, Response response) throws Exception { public Object sideloadEncyclopedia(Request request, Response response) {
Path sourcePath = Path.of(request.queryParams("source")); String source = request.queryParams("source");
String baseUrl = request.queryParams("baseUrl"); String baseUrl = request.queryParams("baseUrl");
int nodeId = Integer.parseInt(request.params("node"));
final int nodeId = Integer.parseInt(request.params("node")); if (baseUrl == null)
throw new ControlValidationError("No baseUrl specified", "A baseUrl must be specified", "..");
Path sourcePath = parseSourcePath(source);
eventLog.logEvent("USER-ACTION", "SIDELOAD ENCYCLOPEDIA " + nodeId); eventLog.logEvent("USER-ACTION", "SIDELOAD ENCYCLOPEDIA " + nodeId);
@ -101,11 +106,12 @@ public class ControlNodeActionsService {
return ""; return "";
} }
public Object sideloadDirtree(Request request, Response response) throws Exception { public Object sideloadDirtree(Request request, Response response) {
Path sourcePath = Path.of(request.queryParams("source"));
final int nodeId = Integer.parseInt(request.params("node")); final int nodeId = Integer.parseInt(request.params("node"));
Path sourcePath = parseSourcePath(request.queryParams("source"));
eventLog.logEvent("USER-ACTION", "SIDELOAD DIRTREE " + nodeId); eventLog.logEvent("USER-ACTION", "SIDELOAD DIRTREE " + nodeId);
executorClient.sideloadDirtree(Context.fromRequest(request), nodeId, sourcePath); executorClient.sideloadDirtree(Context.fromRequest(request), nodeId, sourcePath);
@ -113,10 +119,10 @@ public class ControlNodeActionsService {
return ""; return "";
} }
public Object sideloadWarc(Request request, Response response) throws Exception { public Object sideloadWarc(Request request, Response response) {
Path sourcePath = Path.of(request.queryParams("source"));
final int nodeId = Integer.parseInt(request.params("node")); final int nodeId = Integer.parseInt(request.params("node"));
Path sourcePath = parseSourcePath(request.queryParams("source"));
eventLog.logEvent("USER-ACTION", "SIDELOAD WARC " + nodeId); eventLog.logEvent("USER-ACTION", "SIDELOAD WARC " + nodeId);
@ -124,11 +130,15 @@ public class ControlNodeActionsService {
return ""; return "";
} }
public Object sideloadStackexchange(Request request, Response response) throws Exception { public Object sideloadStackexchange(Request request, Response response) {
Path sourcePath = Path.of(request.queryParams("source"));
final int nodeId = Integer.parseInt(request.params("node")); final int nodeId = Integer.parseInt(request.params("node"));
String source = request.queryParams("source");
if (source == null)
throw new ControlValidationError("No source specified", "A source file/directory must be specified", "..");
Path sourcePath = Path.of(source);
eventLog.logEvent("USER-ACTION", "SIDELOAD STACKEXCHANGE " + nodeId); eventLog.logEvent("USER-ACTION", "SIDELOAD STACKEXCHANGE " + nodeId);
executorClient.sideloadStackexchange(Context.fromRequest(request), nodeId, sourcePath); executorClient.sideloadStackexchange(Context.fromRequest(request), nodeId, sourcePath);
@ -143,7 +153,8 @@ public class ControlNodeActionsService {
private Object triggerAutoRecrawl(Request request, Response response) throws SQLException { private Object triggerAutoRecrawl(Request request, Response response) throws SQLException {
int nodeId = Integer.parseInt(request.params("id")); int nodeId = Integer.parseInt(request.params("id"));
var toCrawl = FileStorageId.parse(request.queryParams("source"));
var toCrawl = parseSourceFileStorageId(request.queryParams("source"));
changeActiveStorage(nodeId, FileStorageType.CRAWL_DATA, toCrawl); changeActiveStorage(nodeId, FileStorageType.CRAWL_DATA, toCrawl);
@ -159,7 +170,7 @@ public class ControlNodeActionsService {
private Object triggerNewCrawl(Request request, Response response) throws SQLException { private Object triggerNewCrawl(Request request, Response response) throws SQLException {
int nodeId = Integer.parseInt(request.params("id")); int nodeId = Integer.parseInt(request.params("id"));
var toCrawl = FileStorageId.parse(request.queryParams("source")); var toCrawl = parseSourceFileStorageId(request.queryParams("source"));
changeActiveStorage(nodeId, FileStorageType.CRAWL_SPEC, toCrawl); changeActiveStorage(nodeId, FileStorageType.CRAWL_SPEC, toCrawl);
@ -174,7 +185,8 @@ public class ControlNodeActionsService {
private Object triggerAutoProcess(Request request, Response response) throws SQLException { private Object triggerAutoProcess(Request request, Response response) throws SQLException {
int nodeId = Integer.parseInt(request.params("id")); int nodeId = Integer.parseInt(request.params("id"));
var toProcess = FileStorageId.parse(request.queryParams("source"));
var toProcess = parseSourceFileStorageId(request.queryParams("source"));
changeActiveStorage(nodeId, FileStorageType.PROCESSED_DATA, toProcess); changeActiveStorage(nodeId, FileStorageType.PROCESSED_DATA, toProcess);
@ -189,6 +201,10 @@ public class ControlNodeActionsService {
int nodeId = Integer.parseInt(request.params("id")); int nodeId = Integer.parseInt(request.params("id"));
String[] values = request.queryParamsValues("source"); String[] values = request.queryParamsValues("source");
if (values.length == 0) {
throw new ControlValidationError("No source specified", "At least one source storage must be specified", "..");
}
List<FileStorageId> ids = Arrays.stream(values).map(FileStorageId::parse).toList(); List<FileStorageId> ids = Arrays.stream(values).map(FileStorageId::parse).toList();
changeActiveStorage(nodeId, FileStorageType.PROCESSED_DATA, ids.toArray(new FileStorageId[0])); changeActiveStorage(nodeId, FileStorageType.PROCESSED_DATA, ids.toArray(new FileStorageId[0]));
@ -204,11 +220,14 @@ public class ControlNodeActionsService {
private Object triggerRestoreBackup(Request request, Response response) { private Object triggerRestoreBackup(Request request, Response response) {
int nodeId = Integer.parseInt(request.params("id")); int nodeId = Integer.parseInt(request.params("id"));
executorClient.restoreBackup(Context.fromRequest(request), nodeId, request.queryParams("source")); var toLoad = parseSourceFileStorageId(request.queryParams("source"));
executorClient.restoreBackup(Context.fromRequest(request), nodeId, toLoad);
return ""; return "";
} }
/** Change the active storage for a node of a particular type. */ /** Change the active storage for a node of a particular type. */
private void changeActiveStorage(int nodeId, FileStorageType type, FileStorageId... newActiveStorage) throws SQLException { private void changeActiveStorage(int nodeId, FileStorageType type, FileStorageId... newActiveStorage) throws SQLException {
// It is desirable to have the active storage set to reflect which storage was last used // It is desirable to have the active storage set to reflect which storage was last used
@ -231,6 +250,10 @@ public class ControlNodeActionsService {
final String url = request.queryParams("url"); final String url = request.queryParams("url");
int nodeId = Integer.parseInt(request.params("id")); int nodeId = Integer.parseInt(request.params("id"));
if (url == null || url.isBlank()) {
throw new ControlValidationError("No url specified", "A url must be specified", "..");
}
executorClient.createCrawlSpecFromDownload(Context.fromRequest(request), nodeId, description, url); executorClient.createCrawlSpecFromDownload(Context.fromRequest(request), nodeId, description, url);
return ""; return "";
@ -244,21 +267,41 @@ public class ControlNodeActionsService {
private Object exportFromCrawlData(Request req, Response rsp) { private Object exportFromCrawlData(Request req, Response rsp) {
String exportType = req.queryParams("exportType"); String exportType = req.queryParams("exportType");
String source = req.queryParams("source"); FileStorageId source = parseSourceFileStorageId(req.queryParams("source"));
if (exportType.equals("atags")) { switch (exportType) {
executorClient.exportAtags(Context.fromRequest(req), Integer.parseInt(req.params("id")), source); case "atags" -> executorClient.exportAtags(Context.fromRequest(req), Integer.parseInt(req.params("id")), source);
} case "rss" -> executorClient.exportRssFeeds(Context.fromRequest(req), Integer.parseInt(req.params("id")), source);
else if (exportType.equals("rss")) { case "termFreq" -> executorClient.exportTermFrequencies(Context.fromRequest(req), Integer.parseInt(req.params("id")), source);
executorClient.exportRssFeeds(Context.fromRequest(req), Integer.parseInt(req.params("id")), source); default -> throw new ControlValidationError("No export type specified", "An export type must be specified", "..");
}
else if (exportType.equals("termFreq")) {
executorClient.exportTermFrequencies(Context.fromRequest(req), Integer.parseInt(req.params("id")), source);
}
else {
rsp.status(404);
} }
return ""; return "";
} }
private Path parseSourcePath(String source) {
if (source == null) {
throw new ControlValidationError("No source specified",
"A source file/directory must be specified",
"..");
}
return Path.of(source);
}
private FileStorageId parseSourceFileStorageId(String source) {
if (source == null) {
throw new ControlValidationError("No source specified",
"A source file storage must be specified",
"..");
}
try {
return FileStorageId.parse(source);
}
catch (Exception e) { // Typically NumberFormatException
throw new ControlValidationError("Invalid source specified", "The source file storage is invalid", "..");
}
}
} }

View file

@ -16,15 +16,12 @@ import java.util.Map;
@Singleton @Singleton
public class DataSetsService { public class DataSetsService {
private final HikariDataSource dataSource;
private final ControlRendererFactory rendererFactory; private final ControlRendererFactory rendererFactory;
private final DomainTypes domainTypes; private final DomainTypes domainTypes;
@Inject @Inject
public DataSetsService(HikariDataSource dataSource, public DataSetsService(ControlRendererFactory rendererFactory,
ControlRendererFactory rendererFactory,
DomainTypes domainTypes) { DomainTypes domainTypes) {
this.dataSource = dataSource;
this.rendererFactory = rendererFactory; this.rendererFactory = rendererFactory;
this.domainTypes = domainTypes; this.domainTypes = domainTypes;
} }