diff --git a/.gitignore b/.gitignore index 502a62f..d947dda 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,5 @@ build/ .DS_Store /.idea/ /.mvn/ +xcp.conf +xcpdata.mv.db diff --git a/app/src/main/java/ru/kirillius/XCP/Application.java b/app/src/main/java/ru/kirillius/XCP/Application.java index 7827ed5..6d697dc 100644 --- a/app/src/main/java/ru/kirillius/XCP/Application.java +++ b/app/src/main/java/ru/kirillius/XCP/Application.java @@ -16,6 +16,7 @@ import ru.kirillius.XCP.Services.ServiceLoadPriority; import ru.kirillius.XCP.Services.WebService; import ru.kirillius.XCP.web.WebServiceImpl; +import java.io.File; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.util.Arrays; @@ -39,23 +40,44 @@ public class Application implements Context { private final ConfigManager configManager; private final Map, Service> services = new ConcurrentHashMap<>(); + private Config loadConfig() { + var configFile = configManager.getConfigFile().getAbsolutePath(); + + try { + configFile = configManager.getConfigFile().getCanonicalPath(); + } catch (IOException e) { + log.warning("Unable to determine real path of file " + configFile); + } + + Config config; + if (configManager.isExist()) { + try { + config = configManager.load(); + log.info("Loaded config file: " + configFile); + } catch (IOException e) { + log.error("Error loading config file " + configFile, e); + throw new RuntimeException(e); + } + } else { + log.warning("Unable to find config file " + configFile + ". Using default values."); + config = configManager.create(); + log.info("Saving default config file to " + configFile); + try { + configManager.save(config); + } catch (IOException e) { + throw new RuntimeException("Unable to save config file", e); + } + } + return config; + } + public Application(String[] args) { launchArgs = Arrays.stream(args).toList(); loggingSystem = new LoggingSystemImpl(this); log = loggingSystem.createLogger(Application.class); configManager = new ConfigManagerImpl(this); - if (configManager.isExist()) { - try { - config = configManager.load(); - log.info("Loaded config file: " + configManager.getConfigFile().getAbsolutePath()); - } catch (IOException e) { - log.error("Error loading config file " + configManager.getConfigFile(), e); - throw new RuntimeException(e); - } - } else { - log.warning("Unable to find config file " + configManager.getConfigFile().getAbsolutePath() + ". Using default values."); - config = configManager.create(); - } + config = loadConfig(); + securityManager = new SecurityManagerImpl(); try { loadServices(); @@ -76,17 +98,18 @@ public class Application implements Context { var order = aClass.getAnnotation(ServiceLoadPriority.class); return order == null ? 100000 : order.value(); })).forEach(aClass -> { - log.info("Loading service " + aClass.getSimpleName()); + @SuppressWarnings("unchecked") var facade = (Class) Arrays.stream(aClass.getInterfaces()) + .filter(Service.class::isAssignableFrom) + .findFirst(). + orElseThrow(() -> new ClassCastException("Unable to get service interface from class " + aClass.getSimpleName())); + log.info("Loading service " + facade.getSimpleName()); try { var constructor = aClass.getConstructor(); try { var service = constructor.newInstance(); try { service.initialize(this); - @SuppressWarnings("unchecked") var facade = (Class) Arrays.stream(aClass.getInterfaces()) - .filter(Service.class::isAssignableFrom) - .findFirst(). - orElseThrow(() -> new ClassCastException("Unable to get service interface from class " + aClass.getSimpleName())); + services.put(facade, service); } catch (Throwable e) { try { @@ -94,14 +117,14 @@ public class Application implements Context { } catch (IOException ex) { e.addSuppressed(ex); } - throw new RuntimeException("Failed to start service " + aClass.getSimpleName(), e); + throw new RuntimeException("Failed to start " + facade.getSimpleName(), e); } } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { - throw new RuntimeException("Failed to instantiate service " + aClass.getSimpleName(), e); + throw new RuntimeException("Failed to instantiate " + facade.getSimpleName(), e); } } catch (NoSuchMethodException e) { - throw new RuntimeException("Failed to find default constructor of service " + aClass.getSimpleName(), e); + throw new RuntimeException("Failed to find default constructor of " + facade.getSimpleName(), e); } }); } diff --git a/core/src/main/java/ru/kirillius/XCP/Security/ConfigManagerImpl.java b/core/src/main/java/ru/kirillius/XCP/Security/ConfigManagerImpl.java index 13eabce..7e0fb5c 100644 --- a/core/src/main/java/ru/kirillius/XCP/Security/ConfigManagerImpl.java +++ b/core/src/main/java/ru/kirillius/XCP/Security/ConfigManagerImpl.java @@ -11,6 +11,7 @@ import ru.kirillius.XCP.Commons.Context; import tools.jackson.databind.ObjectMapper; import java.io.*; +import java.util.regex.Pattern; public final class ConfigManagerImpl implements ConfigManager { private final Context context; @@ -24,7 +25,11 @@ public final class ConfigManagerImpl implements ConfigManager { } private File findConfigFile() { - return new File(context.getLaunchArgs().stream().filter(a -> a.startsWith("--config=")).findFirst().orElse(DEFAULT_CONFIG_PATH)); + return new File(context.getLaunchArgs().stream() + .filter(a -> a.startsWith("--config=")) + .findFirst() + .map(a -> a.split(Pattern.quote("="))[1]) + .orElse(DEFAULT_CONFIG_PATH)); } @Getter @@ -52,7 +57,7 @@ public final class ConfigManagerImpl implements ConfigManager { @Override public void save(Config config) throws IOException { try (var stream = new FileOutputStream(configFile)) { - mapper.writeValue(stream, config); + mapper.writerWithDefaultPrettyPrinter().writeValue(stream, config); } } @@ -70,7 +75,7 @@ public final class ConfigManagerImpl implements ConfigManager { @Getter @Setter @JsonProperty - private File databaseFile = new File(DEFAULT_CONFIG_PATH); + private File databaseFile = new File(DEFAULT_DB_PATH); @Getter @Setter @JsonProperty diff --git a/database/pom.xml b/database/pom.xml index 38bb21c..ae3cb4e 100644 --- a/database/pom.xml +++ b/database/pom.xml @@ -12,11 +12,11 @@ database - + com.h2database h2 - 2.1.214 + 2.4.240 diff --git a/logging/src/main/java/ru/kirillius/XCP/Logging/LogHandlerImpl.java b/logging/src/main/java/ru/kirillius/XCP/Logging/LogHandlerImpl.java index 0a0ee03..3c53216 100644 --- a/logging/src/main/java/ru/kirillius/XCP/Logging/LogHandlerImpl.java +++ b/logging/src/main/java/ru/kirillius/XCP/Logging/LogHandlerImpl.java @@ -35,7 +35,7 @@ public class LogHandlerImpl extends Handler { builder.append(logRecord.getMessage().trim()); var thrown = logRecord.getThrown(); if (thrown != null) { - builder.append("\nError thrown ").append(thrown.getClass().getSimpleName()).append(":").append(thrown.getMessage()); + builder.append("\n\tThrown ").append(thrown.getClass().getSimpleName()).append(": ").append(thrown.getMessage()); if (debugging) { builder.append("\nStack trace:\n"); @@ -47,6 +47,13 @@ public class LogHandlerImpl extends Handler { } catch (IOException e) { throw new RuntimeException(e); } + } else { + var cause = thrown.getCause(); + while (cause != null) { + builder.append("\n\t\tCaused by ").append(cause.getClass().getSimpleName()).append(": ").append(cause.getMessage()); + cause = cause.getCause(); + } + builder.append("\n\t(Stack trace is hidden due to --debug flag)"); } } return builder.toString(); diff --git a/logging/src/main/java/ru/kirillius/XCP/Logging/LoggerImpl.java b/logging/src/main/java/ru/kirillius/XCP/Logging/LoggerImpl.java index d683901..20e9266 100644 --- a/logging/src/main/java/ru/kirillius/XCP/Logging/LoggerImpl.java +++ b/logging/src/main/java/ru/kirillius/XCP/Logging/LoggerImpl.java @@ -31,6 +31,6 @@ public class LoggerImpl extends java.util.logging.Logger implements Logger { @Override public void error(Throwable error) { - log(Level.SEVERE, getName() + SEPARATOR + "Thrown error", error); + log(Level.SEVERE, getName() + SEPARATOR + "Thrown unhandled exception", error); } } diff --git a/rpc/src/main/java/ru/kirillius/XCP/RPC/JSONRPC/JsonRpcMethod.java b/rpc/src/main/java/ru/kirillius/XCP/RPC/JSONRPC/JsonRpcMethod.java index 5fd636d..9a225cb 100644 --- a/rpc/src/main/java/ru/kirillius/XCP/RPC/JSONRPC/JsonRpcMethod.java +++ b/rpc/src/main/java/ru/kirillius/XCP/RPC/JSONRPC/JsonRpcMethod.java @@ -11,4 +11,23 @@ import java.lang.annotation.Target; @Target(ElementType.METHOD) public @interface JsonRpcMethod { UserRole accessLevel() default UserRole.Admin; + + String description() default ""; + + Parameter[] parameters() default {}; + + Class returnType() default Void.class; + + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + @interface Parameter { + String name(); + + String description() default ""; + + Class type() default Void.class; + + boolean optional() default false; + } } \ No newline at end of file diff --git a/rpc/src/main/java/ru/kirillius/XCP/RPC/JSONRPC/JsonRpcServlet.java b/rpc/src/main/java/ru/kirillius/XCP/RPC/JSONRPC/JsonRpcServlet.java index 3327318..35c1e94 100644 --- a/rpc/src/main/java/ru/kirillius/XCP/RPC/JSONRPC/JsonRpcServlet.java +++ b/rpc/src/main/java/ru/kirillius/XCP/RPC/JSONRPC/JsonRpcServlet.java @@ -133,8 +133,38 @@ public class JsonRpcServlet extends HttpServlet { mapper.writeValue(httpServletResponse.getWriter(), jsonRpcResponse); } + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + mapper.writeValue(resp.getWriter(), createErrorResponse(null, JsonRpcErrorCode.INVALID_REQUEST)); + } + + @Override + protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws IOException { + mapper.writeValue(resp.getWriter(), createErrorResponse(null, JsonRpcErrorCode.INVALID_REQUEST)); + } + + @Override + protected void doHead(HttpServletRequest req, HttpServletResponse resp) throws IOException { + mapper.writeValue(resp.getWriter(), createErrorResponse(null, JsonRpcErrorCode.INVALID_REQUEST)); + } + + @Override + protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IOException { + mapper.writeValue(resp.getWriter(), createErrorResponse(null, JsonRpcErrorCode.INVALID_REQUEST)); + } + + @Override + protected void doTrace(HttpServletRequest req, HttpServletResponse resp) throws IOException { + mapper.writeValue(resp.getWriter(), createErrorResponse(null, JsonRpcErrorCode.INVALID_REQUEST)); + } + + @Override + protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws IOException { + mapper.writeValue(resp.getWriter(), createErrorResponse(null, JsonRpcErrorCode.INVALID_REQUEST)); + } + private JsonRpcResponse createErrorResponse(JsonRpcRequest request, JsonRpcErrorCode code) { - return JsonRpcResponse.builder() + return JsonRpcResponse.builder().jsonrpc("2.0") .id(request == null ? -1L : request.getId()) .error(new JsonRpcError(code)) .build(); diff --git a/rpc/src/main/java/ru/kirillius/XCP/RPC/Services/Auth.java b/rpc/src/main/java/ru/kirillius/XCP/RPC/Services/Auth.java index 34dbe98..40f2ff7 100644 --- a/rpc/src/main/java/ru/kirillius/XCP/RPC/Services/Auth.java +++ b/rpc/src/main/java/ru/kirillius/XCP/RPC/Services/Auth.java @@ -21,7 +21,23 @@ import java.util.UUID; public class Auth extends JsonRpcService { - @JsonRpcMethod(accessLevel = UserRole.Guest) + @JsonRpcMethod( + accessLevel = UserRole.Guest, + description = "Authenticates a user using login and password. Returns true if authentication is successful.", + parameters = { + @JsonRpcMethod.Parameter( + name = "login", + description = "User's login name", + type = String.class + ), + @JsonRpcMethod.Parameter( + name = "password", + description = "User's password", + type = String.class + ) + }, + returnType = boolean.class + ) public boolean authenticateByPassword(CallContext call) { var login = requireParam(call, "login", JsonNode::asString); var passwd = requireParam(call, "password", JsonNode::asString); @@ -41,7 +57,11 @@ public class Auth extends JsonRpcService { } - @JsonRpcMethod(accessLevel = UserRole.User) + @JsonRpcMethod( + accessLevel = UserRole.User, + description = "Retrieves all API tokens associated with the current user", + returnType = ArrayNode.class + ) public ArrayNode getTokens(CallContext call) throws IOException { var tokenRepository = call.getContext().getService(RepositoryService.class).getRepository(ApiTokenRepository.class); var tokens = JsonNodeFactory.instance.arrayNode(); @@ -57,7 +77,26 @@ public class Auth extends JsonRpcService { return tokens; } - @JsonRpcMethod(accessLevel = UserRole.User) + + @JsonRpcMethod( + accessLevel = UserRole.User, + description = "Generates a new API token and returns its details", + parameters = { + @JsonRpcMethod.Parameter( + name = "permanent", + description = "If true, creates a token that never expires", + type = boolean.class, + optional = true + ), + @JsonRpcMethod.Parameter( + name = "name", + description = "Display name for the token. If not provided, the User-Agent header will be used", + type = String.class, + optional = true + ) + }, + returnType = ObjectNode.class + ) public ObjectNode generateToken(CallContext call) { var repositoryService = call.getContext().getService(RepositoryService.class); var tokenRepository = repositoryService.getRepository(ApiTokenRepository.class); @@ -79,8 +118,19 @@ public class Auth extends JsonRpcService { return tokenRepository.serialize(token); } - @JsonRpcMethod(accessLevel = UserRole.Guest) - public boolean authenticateByToken(CallContext call) throws IllegalAccessException { + @JsonRpcMethod( + accessLevel = UserRole.Guest, + description = "Authenticates a user using an API token. Returns true if authentication is successful.", + parameters = { + @JsonRpcMethod.Parameter( + name = "token", + description = "API token string for authentication", + type = String.class + ) + }, + returnType = boolean.class + ) + public boolean authenticateByToken(CallContext call) { var repositoryService = call.getContext().getService(RepositoryService.class); var tokenRepository = repositoryService.getRepository(ApiTokenRepository.class); var token = tokenRepository.get(UUID.fromString( @@ -105,7 +155,11 @@ public class Auth extends JsonRpcService { return true; } - @JsonRpcMethod(accessLevel = UserRole.User) + @JsonRpcMethod( + accessLevel = UserRole.User, + description = "Logs out the current user. Returns true if logout is successful, false if the user is not logged in", + returnType = boolean.class + ) public boolean logout(CallContext call) { ru.kirillius.XCP.Persistence.Entities.User user = null; @@ -117,4 +171,4 @@ public class Auth extends JsonRpcService { return session != null && user != null; } -} +} \ No newline at end of file diff --git a/web-server/src/main/java/ru/kirillius/XCP/web/WebServiceImpl.java b/web-server/src/main/java/ru/kirillius/XCP/web/WebServiceImpl.java index 8a5de83..526a622 100644 --- a/web-server/src/main/java/ru/kirillius/XCP/web/WebServiceImpl.java +++ b/web-server/src/main/java/ru/kirillius/XCP/web/WebServiceImpl.java @@ -24,7 +24,7 @@ public class WebServiceImpl implements WebService { @Override public void initialize(Context context) { if (server != null) { - throw new IllegalStateException("Server already started"); + throw new IllegalStateException("Server is started already"); } var jsonRpc = new JsonRpcServlet(context); jsonRpc.registerRpcService( @@ -47,6 +47,11 @@ public class WebServiceImpl implements WebService { } server.setHandler(servletContext); + try { + server.start(); + } catch (Exception e) { + throw new RuntimeException("Failed to start jetty web server", e); + } } @Override