From c434d7d4188ada5096d6990bdaff62a350a33a70 Mon Sep 17 00:00:00 2001 From: jExpress - Hello Summer! Date: Sun, 8 Sep 2024 14:59:41 -0400 Subject: [PATCH] release2.4.13 (#285) --- pom.xml | 83 +++--- .../jexpress/boot/BootConstant.java | 2 +- .../jexpress/boot/BootGuiceModule.java | 16 +- .../jexpress/boot/ScanedGuiceModule.java | 4 +- .../jexpress/boot/SummerApplication.java | 57 +++- .../jexpress/boot/SummerBigBang.java | 16 +- .../boot/event/AppLifecycleHandler.java | 10 +- .../boot/event/HttpLifecycleHandler.java | 2 +- .../boot/event/HttpLifecycleListener.java | 5 +- .../boot/instrumentation/HealthMonitor.java | 71 ++++- .../integration/smtp/BootPostOfficeImpl.java | 82 +++--- .../nio/grpc/BearerAuthCredential.java | 4 - .../nio/grpc/BootLoadBalancerProvider.java | 56 ++-- .../jexpress/nio/grpc/GRPCClient.java | 273 ++++++++---------- .../jexpress/nio/grpc/GRPCClientConfig.java | 219 +++++++++++++- .../jexpress/nio/grpc/GRPCServer.java | 105 +++---- .../nio/server/BootHttpFileUploadHandler.java | 11 +- .../nio/server/BootHttpRequestHandler.java | 26 +- .../jexpress/nio/server/NioConfig.java | 2 +- .../jexpress/nio/server/NioServer.java | 51 ++-- .../jexpress/nio/server/RequestProcessor.java | 2 +- .../nio/server/domain/ServiceContext.java | 63 +++- .../nio/server/ws/rs/BootController.java | 57 ++++ .../server/ws/rs/JaxRsRequestProcessor.java | 9 +- .../summerboot/jexpress/security/JwtUtil.java | 109 +++---- .../jexpress/security/auth/AuthConfig.java | 9 +- .../security/auth/BootAuthenticator.java | 40 ++- .../jexpress/template/log4j2.xml.temp | 45 +-- 28 files changed, 906 insertions(+), 523 deletions(-) diff --git a/pom.xml b/pom.xml index bbfb8262..f6ad3691 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 org.summerboot jexpress - 2.4.10 + 2.4.13 jar Summer Boot jExpress Summer Boot jExpress focuses on solving non-functional and operational maintainability requirements, @@ -62,14 +62,15 @@ + org.apache.maven.plugins maven-surefire-plugin - 3.2.5 + 3.5.0 org.apache.maven.plugins maven-scm-plugin - 2.0.0-M1 + 2.1.0 @@ -90,7 +91,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.6.3 + 3.10.0 17 @@ -111,9 +112,10 @@ + org.apache.maven.plugins maven-deploy-plugin - 3.1.1 + 3.1.3 default-deploy @@ -128,7 +130,7 @@ org.apache.maven.plugins maven-gpg-plugin - 3.2.2 + 3.2.5 verify @@ -143,7 +145,7 @@ org.sonatype.plugins nexus-staging-maven-plugin - 1.6.13 + 1.7.0 true ossrh @@ -190,28 +192,28 @@ 3.3.1 3.2.5 - 3.15.0 - 1.8.0 + 3.17.0 + 1.9.0 2.16.1 - 2.23.1 + 2.24.0 4.0.0 2.0.1 1.78.1 - 0.11.5 + 0.12.6 - 4.1.112.Final - 2.0.65.Final + 4.1.113.Final + 2.0.66.Final - 1.65.1 - 33.2.1-jre - 4.27.3 + 1.66.0 + 33.3.0-jre + 4.28.0 2.2.22 @@ -227,20 +229,21 @@ 2.17.2 - 8.0.1.Final 6.0.1 + 11.0.0-M24 + 8.0.1.Final 7.0.0 - 6.5.2.Final + 6.6.0.Final 5.1.0 - 5.1.4 + 5.1.5 - 2.5.0-rc1 + 2.3.2 1.2.5 @@ -263,12 +266,6 @@ - - - org.apache.commons - commons-lang3 - ${commons-lang3.version} - commons-cli @@ -282,6 +279,12 @@ commons-io ${commons-io.version} + + + org.apache.commons + commons-lang3 + ${commons-lang3.version} + - + - org.hibernate.validator - hibernate-validator - ${hibernate-validator.version} + jakarta.el + jakarta.el-api + ${jakarta.el.version} + + + + + + - org.glassfish - jakarta.el - 4.0.2 + org.apache.tomcat.embed + tomcat-embed-el + ${tomcat-embed-el.version} - + - jakarta.el - jakarta.el-api - ${jakarta.el.version} + org.hibernate.validator + hibernate-validator + ${hibernate-validator.version} diff --git a/src/main/java/org/summerboot/jexpress/boot/BootConstant.java b/src/main/java/org/summerboot/jexpress/boot/BootConstant.java index 8f7469e3..c58ae175 100644 --- a/src/main/java/org/summerboot/jexpress/boot/BootConstant.java +++ b/src/main/java/org/summerboot/jexpress/boot/BootConstant.java @@ -25,7 +25,7 @@ public interface BootConstant { String APP_ID = String.format("%06d", new Random().nextInt(999999)); //version - String VERSION = "SummerBoot.jExpress 2.4.10"; + String VERSION = "jExpress 2.4.13"; String JEXPRESS_PACKAGE_NAME = "org.summerboot.jexpress"; String DEFAULT_ADMIN_MM = "changeit"; diff --git a/src/main/java/org/summerboot/jexpress/boot/BootGuiceModule.java b/src/main/java/org/summerboot/jexpress/boot/BootGuiceModule.java index 7fd6f1b8..7d8d089b 100644 --- a/src/main/java/org/summerboot/jexpress/boot/BootGuiceModule.java +++ b/src/main/java/org/summerboot/jexpress/boot/BootGuiceModule.java @@ -25,6 +25,7 @@ import org.quartz.Scheduler; import org.summerboot.jexpress.boot.annotation.Controller; import org.summerboot.jexpress.boot.annotation.Inspector; +import org.summerboot.jexpress.boot.annotation.Service; import org.summerboot.jexpress.boot.event.AppLifecycleHandler; import org.summerboot.jexpress.boot.event.AppLifecycleListener; import org.summerboot.jexpress.boot.event.HttpExceptionHandler; @@ -76,7 +77,7 @@ public BootGuiceModule(Object caller, Class callerClass, Set userSpecifi this.memo = memo; } - protected boolean isCliUseImplTag(String implTag) { + protected boolean isTagSpecifiedViaCLI(String implTag) { return userSpecifiedImplTags != null && userSpecifiedImplTags.contains(implTag); } @@ -171,13 +172,20 @@ protected void scanAnnotation_BindInstance(Binder binder, Class specified + if (StringUtils.isNotBlank(implTag) && !isTagSpecifiedViaCLI(implTag)) { + continue; + } classesAll.add(c); } //} diff --git a/src/main/java/org/summerboot/jexpress/boot/ScanedGuiceModule.java b/src/main/java/org/summerboot/jexpress/boot/ScanedGuiceModule.java index 9dc1b145..6a2f3cc2 100644 --- a/src/main/java/org/summerboot/jexpress/boot/ScanedGuiceModule.java +++ b/src/main/java/org/summerboot/jexpress/boot/ScanedGuiceModule.java @@ -50,7 +50,7 @@ public ScanedGuiceModule(Map BootConstant.BR + BootConstant.BR + I18n.info.launched.format(userSpecifiedResourceBundle, appVersion + " pid#" + BootConstant.PID) + serviceStatus); if (appLifecycleListener != null) { - appLifecycleListener.onApplicationStart(super.appVersion, serviceStatus); + appLifecycleListener.onApplicationStart(super.appVersion, startingMemo.toString()); } } catch (java.net.BindException ex) {// from NioServer log.fatal(ex + BootConstant.BR + BackOffice.agent.getPortInUseAlertMessage()); @@ -350,11 +360,42 @@ public void start() { log.fatal(I18n.info.unlaunched.format(userSpecifiedResourceBundle), ex); } System.exit(1); + } finally { + // show prompt only with default log4j2.xml in case user is not familiar with log4j2.xml (only one ConsoleAppender with maxLevel is NOT "ALL"), no prompt if user modify the default log4j2.xml due to user knows what he/she is doing + String prompt = null; + try { + org.apache.logging.log4j.core.Logger c = (org.apache.logging.log4j.core.Logger) log; + Map as = c.getContext().getConfiguration().getAppenders(); + int countConsoleAppender = 0; + for (Map.Entry entry : as.entrySet()) { + Appender appender = entry.getValue(); + if (appender instanceof ConsoleAppender) { + countConsoleAppender++; + if (countConsoleAppender > 1) { + prompt = null; + break; + } + ConsoleAppender sa = (ConsoleAppender) appender; + Filter f = sa.getFilter(); + if (f instanceof LevelRangeFilter) { + LevelRangeFilter lrf = (LevelRangeFilter) f; + Level maxLevel = lrf.getMaxLevel(); + if (!Level.ALL.equals(maxLevel)) { + prompt = "\nTo show logs in console, please edit " + this.userSpecifiedConfigDir + File.separator + + "log4j2.xml \n\t\n\t \n\t \n\t \n\tchange around line#13: set maxLevel=\"" + Level.ALL + "\""; + } + } + } + } + } catch (Throwable ex) { + log.error("Failed to inspect " + this.userSpecifiedConfigDir + File.separator + "log4j2.xml", ex); + } + if (prompt != null) { + System.out.println(prompt); + } } } - protected static boolean a = true; - public void shutdown() { log.trace(""); if (gRPCServerList != null && !gRPCServerList.isEmpty()) { diff --git a/src/main/java/org/summerboot/jexpress/boot/SummerBigBang.java b/src/main/java/org/summerboot/jexpress/boot/SummerBigBang.java index 6e4488e5..f8aad606 100644 --- a/src/main/java/org/summerboot/jexpress/boot/SummerBigBang.java +++ b/src/main/java/org/summerboot/jexpress/boot/SummerBigBang.java @@ -21,7 +21,8 @@ import com.google.inject.Module; import com.google.inject.Stage; import com.google.inject.util.Modules; -import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.MacAlgorithm; import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.DefaultParser; import org.apache.commons.cli.Option; @@ -310,8 +311,17 @@ protected boolean runCLI_Utils() { // generate CLI_JWT root signing key if (cli.hasOption(BootConstant.CLI_JWT)) { continueCLI = false; - String algorithm = cli.getOptionValue(BootConstant.CLI_JWT); - SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.forName(algorithm); + String algorithm = cli.getOptionValue(BootConstant.CLI_JWT);// + MacAlgorithm signatureAlgorithm; + switch (algorithm) { + case "HS256" -> signatureAlgorithm = Jwts.SIG.HS256; + case "HS384" -> signatureAlgorithm = Jwts.SIG.HS384; + case "HS512" -> signatureAlgorithm = Jwts.SIG.HS512; + default -> { + System.out.println("invalid -" + BootConstant.CLI_JWT + " value: " + algorithm + ", valid -" + BootConstant.CLI_JWT + " values: "); + return false; + } + } String jwt = JwtUtil.buildSigningKey(signatureAlgorithm); System.out.println(jwt); } diff --git a/src/main/java/org/summerboot/jexpress/boot/event/AppLifecycleHandler.java b/src/main/java/org/summerboot/jexpress/boot/event/AppLifecycleHandler.java index 9d337921..d15732a6 100644 --- a/src/main/java/org/summerboot/jexpress/boot/event/AppLifecycleHandler.java +++ b/src/main/java/org/summerboot/jexpress/boot/event/AppLifecycleHandler.java @@ -19,13 +19,13 @@ import com.google.inject.Singleton; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.summerboot.jexpress.boot.BootConstant; import org.summerboot.jexpress.boot.config.JExpressConfig; import org.summerboot.jexpress.boot.instrumentation.HealthMonitor; import org.summerboot.jexpress.integration.smtp.PostOffice; import org.summerboot.jexpress.integration.smtp.SMTPClientConfig; import java.io.File; -import java.time.OffsetDateTime; /** * @author Changski Tie Zheng Zhang 张铁铮, 魏泽北, 杜旺财, 杜富贵 @@ -41,15 +41,15 @@ public class AppLifecycleHandler implements AppLifecycleListener { @Override public void onApplicationStart(String appVersion, String fullConfigInfo) { if (postOffice != null) { - postOffice.sendAlertAsync(SMTPClientConfig.cfg.getEmailToAppSupport(), "Started at " + OffsetDateTime.now(), fullConfigInfo, null, false); + postOffice.sendAlertAsync(SMTPClientConfig.cfg.getEmailToAppSupport(), "Started", fullConfigInfo, null, false); } } @Override public void onApplicationStop(String appVersion) { - log.info(appVersion); + log.warn(appVersion); if (postOffice != null) { - postOffice.sendAlertSync(SMTPClientConfig.cfg.getEmailToAppSupport(), "Shutdown at " + OffsetDateTime.now() + " - " + appVersion, "EOM", null, false); + postOffice.sendAlertSync(SMTPClientConfig.cfg.getEmailToAppSupport(), "Shutdown", "pid#" + BootConstant.PID, null, false); } } @@ -68,7 +68,7 @@ public void onApplicationStatusUpdated(boolean healthOk, boolean paused, boolean boolean serviceAvaliable = healthOk && !paused; String content = HealthMonitor.buildMessage(); if (postOffice != null) { - postOffice.sendAlertAsync(SMTPClientConfig.cfg.getEmailToAppSupport(), "Service Status Changed at " + OffsetDateTime.now(), content, null, false); + postOffice.sendAlertAsync(SMTPClientConfig.cfg.getEmailToAppSupport(), "Service Status Changed", content, null, false); } } } diff --git a/src/main/java/org/summerboot/jexpress/boot/event/HttpLifecycleHandler.java b/src/main/java/org/summerboot/jexpress/boot/event/HttpLifecycleHandler.java index 2e3415e9..38ad97f4 100644 --- a/src/main/java/org/summerboot/jexpress/boot/event/HttpLifecycleHandler.java +++ b/src/main/java/org/summerboot/jexpress/boot/event/HttpLifecycleHandler.java @@ -47,7 +47,7 @@ public boolean beforeProcess(RequestProcessor processor, HttpHeaders httpRequest } @Override - public void afterProcess(RequestProcessor processor, ChannelHandlerContext ctx, HttpHeaders httpRequestHeaders, HttpMethod httptMethod, String httpRequestPath, Map> queryParams, String httpPostRequestBody, ServiceContext context) { + public void afterProcess(boolean preProcessResult, Object processResult, Throwable processException, RequestProcessor processor, ChannelHandlerContext ctx, HttpHeaders httpRequestHeaders, HttpMethod httptMethod, String httpRequestPath, Map> queryParams, String httpPostRequestBody, ServiceContext context) { // if (httpRequestHeaders.contains(HttpHeaderNames.Sensitive_Header)) { // httpRequestHeaders.set(HttpHeaderNames.Sensitive_Header, "***");// protect Sensitive_Header from being logged // } diff --git a/src/main/java/org/summerboot/jexpress/boot/event/HttpLifecycleListener.java b/src/main/java/org/summerboot/jexpress/boot/event/HttpLifecycleListener.java index af3f1cdf..101198aa 100644 --- a/src/main/java/org/summerboot/jexpress/boot/event/HttpLifecycleListener.java +++ b/src/main/java/org/summerboot/jexpress/boot/event/HttpLifecycleListener.java @@ -49,6 +49,9 @@ public interface HttpLifecycleListener { /** * step1 - after process is done, before sending response to client * + * @param preProcessResult + * @param processResult + * @param processException * @param processor * @param ctx * @param httpRequestHeaders @@ -58,7 +61,7 @@ public interface HttpLifecycleListener { * @param httpPostRequestBody * @param context */ - void afterProcess(RequestProcessor processor, ChannelHandlerContext ctx, HttpHeaders httpRequestHeaders, HttpMethod httptMethod, String httpRequestPath, + void afterProcess(boolean preProcessResult, Object processResult, Throwable processException, RequestProcessor processor, ChannelHandlerContext ctx, HttpHeaders httpRequestHeaders, HttpMethod httptMethod, String httpRequestPath, Map> queryParams, String httpPostRequestBody, ServiceContext context); diff --git a/src/main/java/org/summerboot/jexpress/boot/instrumentation/HealthMonitor.java b/src/main/java/org/summerboot/jexpress/boot/instrumentation/HealthMonitor.java index 551790b5..9537f83e 100644 --- a/src/main/java/org/summerboot/jexpress/boot/instrumentation/HealthMonitor.java +++ b/src/main/java/org/summerboot/jexpress/boot/instrumentation/HealthMonitor.java @@ -15,11 +15,13 @@ */ package org.summerboot.jexpress.boot.instrumentation; +import com.google.inject.Injector; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.summerboot.jexpress.boot.BackOffice; import org.summerboot.jexpress.boot.BootConstant; import org.summerboot.jexpress.boot.annotation.Inspector; +import org.summerboot.jexpress.boot.annotation.Service; import org.summerboot.jexpress.boot.event.AppLifecycleListener; import org.summerboot.jexpress.nio.server.NioConfig; import org.summerboot.jexpress.nio.server.domain.Err; @@ -27,6 +29,7 @@ import org.summerboot.jexpress.util.BeanUtil; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -82,7 +85,7 @@ public static void registerDefaultHealthInspectors(Map defaultHe Object healthInspector = entry.getValue(); if (healthInspector instanceof HealthInspector) { registeredHealthInspectors.add((HealthInspector) healthInspector); - memo.append(BootConstant.BR).append("\t- @DefaultHealthInspector registered: ").append(name).append("=").append(healthInspector.getClass().getName()); + memo.append(BootConstant.BR).append("\t- @Inspector registered: ").append(name).append("=").append(healthInspector.getClass().getName()); } else { error = true; sb.append(BootConstant.BR).append("\tCoding Error: class ").append(healthInspector.getClass().getName()).append(" has annotation @").append(Inspector.class.getSimpleName()).append(", should implement ").append(HealthInspector.class.getName()); @@ -97,8 +100,9 @@ public static void registerDefaultHealthInspectors(Map defaultHe /** * use default inspectors */ - public static void inspect() { + public static int inspect() { registeredHealthInspectors.forEach(healthInspectorQueue::offer); + return registeredHealthInspectors.size(); } /** @@ -184,18 +188,19 @@ public static void inspect(HealthInspector... healthInspectors) { String inspectionReport; if (healthCheckAllPassed) { inspectionReport = "Current all health inspectors passed"; + setHealthStatus(healthCheckAllPassed, inspectionReport); } else { try { inspectionReport = BeanUtil.toJson(healthCheckFailedReport, true, true); } catch (Throwable ex) { inspectionReport = " toJson failed " + ex; } + setHealthStatus(healthCheckAllPassed, inspectionReport); long retryIndex = HealthInspector.retryIndex.get();// not being set yet - if (appLifecycleListener != null) { + if (appLifecycleListener != null && started) { appLifecycleListener.onHealthInspectionFailed(isHealthCheckSuccess, isServicePaused, retryIndex, inspectionIntervalSeconds); } } - setHealthStatus(healthCheckAllPassed, inspectionReport); } started = true; @@ -208,17 +213,57 @@ public static void inspect(HealthInspector... healthInspectors) { } while (keepRunning); }; - public static String start(boolean returnRsult) { + public static String start(boolean returnRsult, Injector guiceInjector) { + if (keepRunning) { + return "HealthMonitor is already running"; + } + StringBuilder memo = new StringBuilder(); + boolean hasUnregistered = false; + // 1. remove unused (via -use ) inspectors with @Service annotation + Iterator iterator = registeredHealthInspectors.iterator(); + while (iterator.hasNext()) { + HealthInspector healthInspector = iterator.next(); + Service serviceAnnotation = healthInspector.getClass().getAnnotation(Service.class); + if (serviceAnnotation != null) { + Class c = healthInspector.getClass(); + boolean usedByTag = false; + Class[] bindingClasses = serviceAnnotation.binding(); + if (bindingClasses == null || bindingClasses.length < 1) { + bindingClasses = c.getInterfaces(); + } + for (Class bindingClasse : bindingClasses) { + Object o = guiceInjector.getInstance(bindingClasse); + if (o.getClass().equals(c)) { + usedByTag = true; + break; + } + } + if (!usedByTag) { + hasUnregistered = true; + memo.append(BootConstant.BR).append("\t- @Inspector unused due to CLI argument -" + BootConstant.CLI_USE_IMPL + " : ").append(c.getName()); + iterator.remove(); + } + } + } + if (hasUnregistered) { + log.warn(memo); + } + + // 2. start health monitor sync to return result String ret = null; if (returnRsult) { // start sync to get result - inspect(); + int size = inspect(); keepRunning = false; - AsyncTask.run(); - ret = buildMessage(); + if (size > 0) { + AsyncTask.run(); + ret = buildMessage(); + } else { + ret = "No health inspectors registered"; + } } - // start async in background + // 3. start async in background keepRunning = true; if (!isServiceAvailable()) { inspect(); @@ -286,12 +331,12 @@ protected static void updateServiceStatus(boolean serviceStatusChanged, String r public static String buildMessage() { StringBuilder sb = new StringBuilder(); sb.append(BootConstant.BR) - .append("\t Self Inspection Result: ").append(isHealthCheckSuccess ? "passed" : "failed").append(BootConstant.BR); + .append("Self Inspection Result: ").append(isHealthCheckSuccess ? "passed" : "failed").append(BootConstant.BR); if (!isHealthCheckSuccess) { - sb.append("\t\t cause: ").append(statusReasonHealthCheck).append(BootConstant.BR); + sb.append("\t cause: ").append(statusReasonHealthCheck).append(BootConstant.BR); } - sb.append("\t Service Status: ").append(isServicePaused ? "paused" : "running").append(BootConstant.BR) - .append("\t\t cause: ").append(statusReasonPaused).append(BootConstant.BR); + sb.append("Service Status: ").append(isServicePaused ? "paused" : "running").append(BootConstant.BR) + .append("\t cause: ").append(statusReasonPaused).append(BootConstant.BR); return sb.toString(); } diff --git a/src/main/java/org/summerboot/jexpress/integration/smtp/BootPostOfficeImpl.java b/src/main/java/org/summerboot/jexpress/integration/smtp/BootPostOfficeImpl.java index 4404e89a..bb643ab8 100644 --- a/src/main/java/org/summerboot/jexpress/integration/smtp/BootPostOfficeImpl.java +++ b/src/main/java/org/summerboot/jexpress/integration/smtp/BootPostOfficeImpl.java @@ -25,6 +25,7 @@ import org.summerboot.jexpress.boot.SummerApplication; import org.summerboot.jexpress.nio.server.domain.Err; +import java.time.OffsetDateTime; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -54,7 +55,7 @@ public void setAppVersion(String appVersion) { * @return */ protected String updateAlertTitle(String title) { - return "Alert@" + SummerApplication.HOST + " " + appVersion + " - " + title; + return "Alert@" + SummerApplication.HOST + " " + appVersion + "[" + BootConstant.APP_ID + "] - " + title + " [" + OffsetDateTime.now() + "]"; } /** @@ -93,36 +94,20 @@ public void sendAlertSync(Collection to, final String title, final Strin * @param async */ protected void sendAlert(Collection to, final String title, final String content, final Throwable cause, boolean debouncing, boolean async) { - if (to == null || to.isEmpty()) { - //log.warn(() -> "unknown recipient: " + title + "\n" + _content); - return; - } - Runnable postman = () -> { - if (debouncing) { - String key = title; - Throwable rootCause = ExceptionUtils.getRootCause(cause); - if (rootCause == null) { - rootCause = cause; - } - if (rootCause != null) { - key = key + rootCause.getClass().getName(); - } - if (debounced(key, SMTPClientConfig.cfg.getEmailAlertDebouncingIntervalMinutes())) { - return; - } + if (debouncing) { + String key = title; + Throwable rootCause = ExceptionUtils.getRootCause(cause); + if (rootCause == null) { + rootCause = cause; } - Email email = Email.compose(updateAlertTitle(title), updateAlertContent(content, cause), Email.Format.text).to(to); - try { - email.send(SMTPClientConfig.cfg.getMailSession()); - } catch (Throwable ex) { - log.fatal("Failed to send email: " + ExceptionUtils.getRootCause(ex).toString()); + if (rootCause != null) { + key = key + rootCause.getClass().getName(); + } + if (debounced(key, SMTPClientConfig.cfg.getEmailAlertDebouncingIntervalMinutes())) { + return; } - }; - if (async) { - BackOffice.execute(postman); - } else { - postman.run(); } + sendEmail(to, updateAlertTitle(title), updateAlertContent(content, cause), false, async); } @Override @@ -136,28 +121,29 @@ public boolean sendEmailSync(Collection to, String title, String content } protected boolean sendEmail(Collection to, String title, String content, boolean isHTMLFormat, boolean async) { - boolean success = false; Email email = Email.compose(title, content, isHTMLFormat ? Email.Format.html : Email.Format.text).to(to); - if (to != null && !to.isEmpty()) { - try { - if (async) { - Runnable postman = () -> { - try { - email.send(SMTPClientConfig.cfg.getMailSession()); - } catch (Throwable ex) { - log.fatal("Failed to send email: " + ExceptionUtils.getRootCause(ex).toString()); - } - }; - BackOffice.execute(postman); - } else { - email.send(SMTPClientConfig.cfg.getMailSession()); - } - success = true; - } catch (Throwable ex) { - log.fatal("Failed to send email: " + ExceptionUtils.getRootCause(ex).toString()); + if (to == null || to.isEmpty()) { + log.warn(() -> "unknown recipient: " + email); + return false; + } + + boolean success = false; + try { + if (async) { + Runnable postman = () -> { + try { + email.send(SMTPClientConfig.cfg.getMailSession()); + } catch (Throwable ex) { + log.fatal("Failed to send email: " + ExceptionUtils.getRootCause(ex).toString()); + } + }; + BackOffice.execute(postman); + } else { + email.send(SMTPClientConfig.cfg.getMailSession()); } - } else { - log.error(() -> "unknown recipient: " + email); + success = true; + } catch (Throwable ex) { + log.fatal("Failed to send email: " + ExceptionUtils.getRootCause(ex).toString()); } return success; } diff --git a/src/main/java/org/summerboot/jexpress/nio/grpc/BearerAuthCredential.java b/src/main/java/org/summerboot/jexpress/nio/grpc/BearerAuthCredential.java index 24262512..fd558306 100644 --- a/src/main/java/org/summerboot/jexpress/nio/grpc/BearerAuthCredential.java +++ b/src/main/java/org/summerboot/jexpress/nio/grpc/BearerAuthCredential.java @@ -48,8 +48,4 @@ public void applyRequestMetadata(RequestInfo requestInfo, Executor executor, Met } }); } - - @Override - public void thisUsesUnstableApi() { - } } diff --git a/src/main/java/org/summerboot/jexpress/nio/grpc/BootLoadBalancerProvider.java b/src/main/java/org/summerboot/jexpress/nio/grpc/BootLoadBalancerProvider.java index b46b86ed..63d5e734 100644 --- a/src/main/java/org/summerboot/jexpress/nio/grpc/BootLoadBalancerProvider.java +++ b/src/main/java/org/summerboot/jexpress/nio/grpc/BootLoadBalancerProvider.java @@ -33,45 +33,59 @@ public class BootLoadBalancerProvider extends NameResolverProvider { protected final List servers; protected final String scheme; - protected final String authority; + protected final int priority; + protected final String defaultAuthorityWhitoutTrustManager; - public BootLoadBalancerProvider(String scheme, InetSocketAddress... addresses) { + public BootLoadBalancerProvider(String scheme, int priority, InetSocketAddress... addresses) { this.scheme = scheme; -// this.authority = Arrays.stream(addresses) -// .map(InetSocketAddress::getHostName) -// .collect(Collectors.joining(", ")); - if (addresses != null && addresses.length > 0) { - this.authority = addresses[0].getHostName(); - } else { - this.authority = "unknownhost"; - } + this.priority = priority; + this.defaultAuthorityWhitoutTrustManager = getAuthorityFromAddress(addresses); this.servers = Arrays.stream(addresses) .map(EquivalentAddressGroup::new) .collect(Collectors.toList()); } - public BootLoadBalancerProvider(String scheme, List addresses) { + public BootLoadBalancerProvider(String scheme, int priority, List addresses) { this.scheme = scheme; + this.priority = priority; + this.defaultAuthorityWhitoutTrustManager = getAuthorityFromAddress(addresses); + this.servers = addresses.stream() + .map(EquivalentAddressGroup::new) + .collect(Collectors.toList()); + } + + public String getAuthorityFromAddress(InetSocketAddress... addresses) { +// this.authority = Arrays.stream(addresses) +// .map(InetSocketAddress::getHostName) +// .collect(Collectors.joining(", ")); + if (addresses == null || addresses.length < 1) { + return "unknownhost"; + } + InetSocketAddress addr = addresses[0]; + return addr.getHostName() + ":" + addr.getPort(); + } + + public String getAuthorityFromAddress(List addresses) { // this.authority = addresses.stream() // .map(InetSocketAddress::getHostName)// getHostString // .collect(Collectors.joining(", ")); - if (addresses != null && !addresses.isEmpty()) { - this.authority = addresses.get(0).getHostName(); - } else { - this.authority = "unknownhost"; + if (addresses == null || addresses.isEmpty()) { + return "unknownhost"; } - this.servers = addresses.stream() - .map(EquivalentAddressGroup::new) - .collect(Collectors.toList()); + InetSocketAddress addr = addresses.get(0); + return addr.getHostName() + ":" + addr.getPort(); } @Override public NameResolver newNameResolver(URI notUsedTargetUri, NameResolver.Args args) { return new NameResolver() { @Override - public String getServiceAuthority() { - //return notUsedTargetUri.toString(); - return authority; + public String getServiceAuthority() {// called when trust manager is null + String auth = notUsedTargetUri.getAuthority(); + if (auth == null) { + auth = defaultAuthorityWhitoutTrustManager; + } + return auth; } @Override diff --git a/src/main/java/org/summerboot/jexpress/nio/grpc/GRPCClient.java b/src/main/java/org/summerboot/jexpress/nio/grpc/GRPCClient.java index 7faf184b..866e5393 100644 --- a/src/main/java/org/summerboot/jexpress/nio/grpc/GRPCClient.java +++ b/src/main/java/org/summerboot/jexpress/nio/grpc/GRPCClient.java @@ -1,3 +1,4 @@ + /* * Copyright 2005-2022 Du Law Office - The Summer Boot Framework Project * @@ -16,25 +17,12 @@ package org.summerboot.jexpress.nio.grpc; import io.grpc.ManagedChannel; -import io.grpc.NameResolverProvider; -import io.grpc.NameResolverRegistry; -import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts; import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder; -import io.grpc.netty.shaded.io.netty.channel.epoll.EpollDomainSocketChannel; -import io.grpc.netty.shaded.io.netty.channel.epoll.EpollEventLoopGroup; -import io.grpc.netty.shaded.io.netty.channel.unix.DomainSocketAddress; -import io.grpc.netty.shaded.io.netty.handler.ssl.SslContext; -import io.grpc.netty.shaded.io.netty.handler.ssl.SslContextBuilder; -import io.grpc.netty.shaded.io.netty.handler.ssl.SslProvider; -import io.grpc.netty.shaded.io.netty.handler.ssl.util.InsecureTrustManagerFactory; -import jakarta.annotation.Nullable; - -import javax.net.ssl.KeyManagerFactory; -import javax.net.ssl.SSLException; -import javax.net.ssl.TrustManagerFactory; -import java.net.URI; -import java.util.ArrayList; -import java.util.List; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; /** * @param @@ -42,179 +30,152 @@ */ public abstract class GRPCClient> { - public enum LoadBalancingPolicy { - ROUND_ROBIN("round_robin"), PICK_FIRST("pick_first"); - - private final String value; + protected NettyChannelBuilder channelBuilder; + protected ManagedChannel channel; + protected final ReadWriteLock rwLock = new ReentrantReadWriteLock(); + protected final Lock readLock = rwLock.readLock(); + protected Thread shutdownHook; - private LoadBalancingPolicy(String value) { - this.value = value; - } + public T withConfig(GRPCClientConfig cfg) { + this.channelBuilder = cfg.getChannelBuilder(); + cfg.addConfigUpdateListener(this); + return (T) this; + } - public String getValue() { - return value; - } + /** + * @param channelBuilder + */ + public T withNettyChannelBuilder(NettyChannelBuilder channelBuilder) { + this.channelBuilder = channelBuilder; + return (T) this; + } + /** + * callback when config file updated if GRPCClientConfig.addConfigUpdateListener(this); + * + * @param channelBuilder + */ + protected void updateChannelBuilder(NettyChannelBuilder channelBuilder) { + rwLock.writeLock().lock(); + this.channelBuilder = channelBuilder; + rwLock.writeLock().unlock(); + onChannelBuilderUpdated(); } - protected static final List NR_Providers = new ArrayList(); + /** + * By default, just call connect() to establish a new connection with the updated settings; or do nothing to keep using current connection. + */ + protected void onChannelBuilderUpdated() { + connect(); + } /** - * @param nameResolverProvider for client side load balancing - * @param loadBalancingPolicy - * @param uri The URI format should be one of grpc://host:port, - * grpcs://host:port, or unix:///path/to/uds.sock - * @param keyManagerFactory The Remote Caller identity - * @param trustManagerFactory The Remote Caller trusted identities - * @param overrideAuthority - * @param ciphers - * @param tlsVersionProtocols "TLSv1.2", "TLSv1.3" + * Disconnect the current connection and build a new connection within a write lock + * * @return - * @throws javax.net.ssl.SSLException */ - public static NettyChannelBuilder getNettyChannelBuilder(NameResolverProvider nameResolverProvider, LoadBalancingPolicy loadBalancingPolicy, URI uri, @Nullable KeyManagerFactory keyManagerFactory, @Nullable TrustManagerFactory trustManagerFactory, - @Nullable String overrideAuthority, @Nullable Iterable ciphers, @Nullable String... tlsVersionProtocols) throws SSLException { - final NettyChannelBuilder channelBuilder; - String target = uri.toString();//"grpcs://"+uri.getAuthority()+"/service";// "grpcs:///" - switch (uri.getScheme()) { - case "unix": //https://github.com/grpc/grpc-java/issues/1539 - channelBuilder = NettyChannelBuilder.forAddress(new DomainSocketAddress(uri.getPath())) - .eventLoopGroup(new EpollEventLoopGroup()) - .channelType(EpollDomainSocketChannel.class) - .usePlaintext(); - break; - default: - if (nameResolverProvider != null) { - NameResolverRegistry nameResolverRegistry = NameResolverRegistry.getDefaultRegistry(); - for (NameResolverProvider nrp : NR_Providers) { - nameResolverRegistry.deregister(nrp); - } - nameResolverRegistry.register(nameResolverProvider);// use client side load balancing - NR_Providers.add(nameResolverProvider); - String policy = loadBalancingPolicy.getValue(); - channelBuilder = NettyChannelBuilder.forTarget(target).defaultLoadBalancingPolicy(policy); - } else { - String host = uri.getHost(); - int port = uri.getPort(); - if (host == null) { - throw new IllegalArgumentException("The URI format should contains host information, like ://[host:port]/[service], like grpc:///, grpc://host:port, grpcs://host:port, or unix:///path/to/uds.sock. gRpc.client.LoadBalancing.servers should be provided when host/port are not provided."); - } - channelBuilder = NettyChannelBuilder.forAddress(host, port); + public T connect() { + rwLock.writeLock().lock(); + try { + disconnect(false); + channel = channelBuilder.build(); + String info = channel.authority(); + shutdownHook = new Thread(() -> { + try { + channel.shutdownNow(); + } catch (Throwable ex) { } - break; + }, "GRPCClient.shutdown and disconnect from " + info); + Runtime.getRuntime().addShutdownHook(shutdownHook); + onConnected(channel); + } finally { + rwLock.writeLock().unlock(); } - if (keyManagerFactory == null) { - channelBuilder.usePlaintext(); - } else { - final SslContextBuilder sslBuilder = GrpcSslContexts.forClient(); - sslBuilder.keyManager(keyManagerFactory); - if (trustManagerFactory == null) {//ignore Server Certificate - sslBuilder.trustManager(InsecureTrustManagerFactory.INSTANCE); - } else { - sslBuilder.trustManager(trustManagerFactory); - if (overrideAuthority != null) { - channelBuilder.overrideAuthority(overrideAuthority); + return (T) this; + } + + /** + * @param channel + */ + protected abstract void onConnected(ManagedChannel channel); + + /** + * Disconnect the current connection + */ + public void disconnect() { + disconnect(true); + } + + protected void disconnect(boolean withLock) { +// ManagedChannel c = (ManagedChannel) blockingStub.getChannel(); + if (withLock) { + rwLock.writeLock().lock(); + } + try { + if (channel != null) { + try { + channel.shutdownNow(); + } catch (Throwable ex) { + } finally { + channel = null; } } - GrpcSslContexts.configure(sslBuilder, SslProvider.OPENSSL); - if (tlsVersionProtocols != null) { - sslBuilder.protocols(tlsVersionProtocols); + if (shutdownHook != null) { + try { + Runtime.getRuntime().removeShutdownHook(shutdownHook); + } catch (Throwable ex) { + } finally { + shutdownHook = null; + } } - if (ciphers != null) { - sslBuilder.ciphers(ciphers); + } finally { + if (withLock) { + rwLock.writeLock().unlock(); } - SslContext sslContext = sslBuilder.build(); - channelBuilder.sslContext(sslContext).useTransportSecurity(); } - return channelBuilder; } /** - * @param nameResolverProvider for client side load balancing - * @param uri The URI format should be one of grpc://host:port or - * unix:///path/to/uds.sock - * @return - * @throws SSLException + * Set a read lock for business method to prevent being called while connect/disconnect */ - public static NettyChannelBuilder NettyChannelBuilder(NameResolverProvider nameResolverProvider, URI uri) throws SSLException { - return getNettyChannelBuilder(nameResolverProvider, LoadBalancingPolicy.ROUND_ROBIN, uri, null, null, null, null); + protected void lock() { + readLock.lock(); } - protected final NameResolverProvider nameResolverProvider; - protected final URI uri; - protected final NettyChannelBuilder channelBuilder; - protected ManagedChannel channel; - /** - * @param nameResolverProvider for client side load balancing - * @param uri The URI format should be one of grpc://host:port or - * unix:///path/to/uds.sock - * @throws SSLException + * Try a read lock for business method to prevent being called while connect/disconnect + * + * @return */ - public GRPCClient(NameResolverProvider nameResolverProvider, URI uri) throws SSLException { - this(nameResolverProvider, uri, null, null, null, null); + protected boolean tryLock() { + return readLock.tryLock(); } /** - * @param nameResolverProvider for client side load balancing - * @param uri The URI format should be one of grpc://host:port, - * grpcs://host:port, or unix:///path/to/uds.sock - * @param keyManagerFactory The Remote Caller identity - * @param trustManagerFactory The Remote Caller trusted identities - * @param overrideAuthority - * @param ciphers - * @param tlsVersionProtocols "TLSv1.2", "TLSv1.3" - * @throws SSLException + * Try a read lock in a given time period for business method to prevent being called while connect/disconnect + * + * @param time + * @param unit + * @return + * @throws InterruptedException */ - public GRPCClient(NameResolverProvider nameResolverProvider, URI uri, @Nullable KeyManagerFactory keyManagerFactory, @Nullable TrustManagerFactory trustManagerFactory, - @Nullable String overrideAuthority, @Nullable Iterable ciphers, @Nullable String... tlsVersionProtocols) throws SSLException { - this.nameResolverProvider = nameResolverProvider; - this.uri = uri; - this.channelBuilder = getNettyChannelBuilder(nameResolverProvider, LoadBalancingPolicy.ROUND_ROBIN, uri, keyManagerFactory, trustManagerFactory, overrideAuthority, ciphers, tlsVersionProtocols); + protected boolean tryLock(long time, TimeUnit unit) throws InterruptedException { + return readLock.tryLock(time, unit); } /** - * @param channelBuilder + * Release the read lock for business method to prevent being called while connect/disconnect */ - public GRPCClient(NettyChannelBuilder channelBuilder) { - this.nameResolverProvider = null; - this.uri = null; - this.channelBuilder = channelBuilder; - } - - public T connect() { - disconnect(); - channel = channelBuilder.build(); - //String info = uri == null ? channel.toString() : uri.toString(); - String info = channel.authority(); - Runtime.getRuntime().addShutdownHook( - new Thread(() -> { - try { - channel.shutdownNow(); - } catch (Throwable ex) { - } - }, "GRPCClient.shutdown and disconnect from " + info)); - onConnected(channel); - return (T) this; + protected void unlock() { + readLock.unlock(); } /** - * @param channel + * Get the read lock + * + * @return */ - protected abstract void onConnected(ManagedChannel channel); - - public void disconnect() { -// ManagedChannel c = (ManagedChannel) blockingStub.getChannel(); - if (channel != null) { - try { - channel.shutdownNow(); - } catch (Throwable ex) { - } finally { - channel = null; - } - } -// if(nameResolverProvider!=null) { -// NameResolverRegistry.getDefaultRegistry().deregister(nameResolverProvider); -// } + protected Lock getLock() { + return readLock; } } diff --git a/src/main/java/org/summerboot/jexpress/nio/grpc/GRPCClientConfig.java b/src/main/java/org/summerboot/jexpress/nio/grpc/GRPCClientConfig.java index 98b0a2b7..f5efaf68 100644 --- a/src/main/java/org/summerboot/jexpress/nio/grpc/GRPCClientConfig.java +++ b/src/main/java/org/summerboot/jexpress/nio/grpc/GRPCClientConfig.java @@ -18,7 +18,17 @@ import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonIgnore; import io.grpc.NameResolverProvider; +import io.grpc.NameResolverRegistry; +import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts; import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder; +import io.grpc.netty.shaded.io.netty.channel.epoll.EpollDomainSocketChannel; +import io.grpc.netty.shaded.io.netty.channel.epoll.EpollEventLoopGroup; +import io.grpc.netty.shaded.io.netty.channel.unix.DomainSocketAddress; +import io.grpc.netty.shaded.io.netty.handler.ssl.SslContext; +import io.grpc.netty.shaded.io.netty.handler.ssl.SslContextBuilder; +import io.grpc.netty.shaded.io.netty.handler.ssl.SslProvider; +import io.grpc.netty.shaded.io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import jakarta.annotation.Nullable; import org.summerboot.jexpress.boot.BootConstant; import org.summerboot.jexpress.boot.config.BootConfig; import org.summerboot.jexpress.boot.config.ConfigUtil; @@ -26,13 +36,17 @@ import org.summerboot.jexpress.boot.config.annotation.ConfigHeader; import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLException; import javax.net.ssl.TrustManagerFactory; import java.io.File; import java.io.IOException; import java.net.InetSocketAddress; import java.net.URI; +import java.util.HashSet; import java.util.List; import java.util.Properties; +import java.util.Set; +import java.util.concurrent.TimeUnit; /** * @author Changski Tie Zheng Zhang 张铁铮, 魏泽北, 杜旺财, 杜富贵 @@ -52,6 +66,21 @@ class a extends GRPCClientConfig { protected final static String ID = "gRpc.client"; + public enum LoadBalancingPolicy { + ROUND_ROBIN("round_robin"), PICK_FIRST("pick_first"); + + private final String value; + + private LoadBalancingPolicy(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + } + protected GRPCClientConfig() { } @@ -61,9 +90,11 @@ protected GRPCClientConfig() { example = "localhost:8424, remotehost:8425, 127.0.0.1:8426") @Config(key = ID + ".LoadBalancing.servers", predefinedValue = "0.0.0.0:8424, 0.0.0.0:8425", required = false) protected volatile List loadBalancingServers; + @Config(key = ID + ".LoadBalancing.scheme", defaultValue = "grpc", desc = "In case you have more than one gRPC client needs to connect to different gRPC services, you can set this to distinguish them") + protected volatile String loadBalancingTargetScheme = "grpc"; @Config(key = ID + ".LoadBalancing.policy", defaultValue = "ROUND_ROBIN", desc = "available options: ROUND_ROBIN, PICK_FIRST") - protected volatile GRPCClient.LoadBalancingPolicy loadBalancingPolicy; + protected volatile LoadBalancingPolicy loadBalancingPolicy; protected volatile NameResolverProvider nameResolverProvider; @@ -122,32 +153,206 @@ protected void generateTemplate_truststore(StringBuilder sb) { @JsonIgnore protected volatile NettyChannelBuilder channelBuilder; + @ConfigHeader(title = "4. " + ID + " Channel Settings", + desc = "The following settings are for NettyChannelBuilder, which is used to create a gRPC channel") + @Config(key = ID + ".channel.userAgent", desc = "string: default null") + protected volatile String userAgent = null; + @Config(key = ID + ".channel.maxInboundMessageSize", desc = "int: default 4194304 if not set") + protected volatile Integer maxInboundMessageSize = null;//4194304; + @Config(key = ID + ".channel.maxHeaderListSize", desc = "int: default 8192 if not set") + protected volatile Integer maxHeaderListSize = null;//8192; + @Config(key = ID + ".channel.perRpcBufferLimit", desc = "long: default 1048576L if not set") + protected volatile Long perRpcBufferLimit = null;//1048576L; + @Config(key = ID + ".channel.maxHedgedAttempts", desc = "int: default 5 if not set") + protected volatile Integer maxHedgedAttempts = null;//5; + + @Config(key = ID + ".channel.idleTimeoutSeconds", desc = "long: default 1800 (30 minutes) if not set") + protected volatile Long idleTimeoutSeconds = null;//TimeUnit.MINUTES.toSeconds(30L); + @Config(key = ID + ".channel.keepAliveWithoutCalls", desc = "boolean: default false if not set. keepAliveWithoutCalls is used when you are willing to spend client, server, and network resources to have lower latency for very infrequent RPCs") + protected volatile Boolean keepAliveWithoutCalls = null;//false + @Config(key = ID + ".channel.keepAliveTimeSeconds", desc = "long: default Long.MAX_VALUE (never) if not set. The interval in seconds between PING frames.") + protected volatile Long keepAliveTimeSeconds = null;//Long.MAX_VALUE; + @Config(key = ID + ".channel.keepAliveTimeoutSeconds", desc = "long: default 20 seconds if not set. The timeout in seconds for a PING frame to be acknowledged. If sender does not receive an acknowledgment within this time, it will close the connection.") + protected volatile Long keepAliveTimeoutSeconds = null;//TimeUnit.SECONDS.toSeconds(20L); + + @Config(key = ID + ".channel.retryEnabled", desc = "boolean: default true if not set") + protected volatile Boolean retryEnabled = null;// true + @Config(key = ID + ".channel.maxRetryAttempts", desc = "int: default 5 if not set") + protected volatile Integer maxRetryAttempts = null;//5; + @Config(key = ID + ".channel.retryBufferSize", desc = "int: default 16777216L if not set") + protected volatile Long retryBufferSize = null;//16777216L + @Override protected void preLoad(File cfgFile, boolean isReal, ConfigUtil helper, Properties props) { - loadBalancingServers = null; - nameResolverProvider = null; - channelBuilder = null; createIfNotExist(FILENAME_KEYSTORE, FILENAME_KEYSTORE); createIfNotExist(FILENAME_SRC_TRUSTSTORE, FILENAME_TRUSTSTORE_4CLIENT); } + protected static int priority = 0; + @Override protected void loadCustomizedConfigs(File cfgFile, boolean isReal, ConfigUtil helper, Properties props) throws IOException { + if (!isReal) { + return; + } + NameResolverRegistry nameResolverRegistry = NameResolverRegistry.getDefaultRegistry();// Use singleton instance in new API to replace deprecated channelBuilder.nameResolverFactory(new nameResolverRegistry().asFactory()); + if (nameResolverProvider != null) { + nameResolverRegistry.deregister(nameResolverProvider); + } if (loadBalancingServers != null && !loadBalancingServers.isEmpty()) { - nameResolverProvider = new BootLoadBalancerProvider(uri.getScheme(), loadBalancingServers); + nameResolverProvider = new BootLoadBalancerProvider(loadBalancingTargetScheme, ++priority, loadBalancingServers); + nameResolverRegistry.register(nameResolverProvider); + } + channelBuilder = initNettyChannelBuilder(nameResolverProvider, loadBalancingPolicy, uri, kmf, tmf, overrideAuthority, ciphers, sslProtocols); + configNettyChannelBuilder(channelBuilder); + for (GRPCClient listener : listeners) { + listener.updateChannelBuilder(channelBuilder); + } + } + + protected void configNettyChannelBuilder(NettyChannelBuilder nettyChannelBuilder) { + if (userAgent != null) { + nettyChannelBuilder.userAgent(userAgent); + } + if (maxInboundMessageSize != null) { + nettyChannelBuilder.maxInboundMessageSize(maxInboundMessageSize); + } + if (maxHeaderListSize != null) { + nettyChannelBuilder.maxInboundMetadataSize(maxHeaderListSize); + } + if (perRpcBufferLimit != null) { + nettyChannelBuilder.perRpcBufferLimit(perRpcBufferLimit); + } + if (maxHedgedAttempts != null) { + nettyChannelBuilder.maxHedgedAttempts(maxHedgedAttempts); } - channelBuilder = GRPCClient.getNettyChannelBuilder(nameResolverProvider, loadBalancingPolicy, uri, kmf, tmf, overrideAuthority, ciphers, sslProtocols); + + // channel timeout + if (idleTimeoutSeconds != null) { + nettyChannelBuilder.idleTimeout(idleTimeoutSeconds, TimeUnit.SECONDS); + } + if (keepAliveWithoutCalls != null) { + nettyChannelBuilder.keepAliveWithoutCalls(keepAliveWithoutCalls); + } + if (keepAliveTimeSeconds != null) { + nettyChannelBuilder.keepAliveTime(keepAliveTimeSeconds, TimeUnit.SECONDS); + } + if (keepAliveTimeoutSeconds != null) { + nettyChannelBuilder.keepAliveTimeout(keepAliveTimeoutSeconds, TimeUnit.SECONDS); + } + + // channel retry + if (retryEnabled != null) { + if (retryEnabled) { + nettyChannelBuilder.enableRetry(); + if (maxRetryAttempts != null) { + nettyChannelBuilder.maxRetryAttempts(maxRetryAttempts); + } + if (retryBufferSize != null) { + nettyChannelBuilder.retryBufferSize(retryBufferSize); + } + } else { + nettyChannelBuilder.disableRetry(); + } + } + + //nettyChannelBuilder.flowControlWindow(NettyChannelBuilder.DEFAULT_FLOW_CONTROL_WINDOW); + //nettyChannelBuilder.initialFlowControlWindow(NettyChannelBuilder.DEFAULT_FLOW_CONTROL_WINDOW); } @Override public void shutdown() { } + private Set listeners = new HashSet<>(); + + public void addConfigUpdateListener(GRPCClient listener) { + if (listener == null) { + return; + } + listeners.add(listener); + } + + public void removeConfigUpdateListener(GRPCClient listener) { + if (listener == null) { + return; + } + listeners.remove(listener); + } + + /** + * @param nameResolverProvider for client side load balancing + * @param loadBalancingPolicy + * @param uri The URI format should be one of grpc://host:port, + * grpcs://host:port, or unix:///path/to/uds.sock + * @param keyManagerFactory The Remote Caller identity + * @param trustManagerFactory The Remote Caller trusted identities + * @param overrideAuthority + * @param ciphers + * @param tlsVersionProtocols "TLSv1.2", "TLSv1.3" + * @return + * @throws javax.net.ssl.SSLException + */ + public static NettyChannelBuilder initNettyChannelBuilder(NameResolverProvider nameResolverProvider, LoadBalancingPolicy loadBalancingPolicy, URI uri, @Nullable KeyManagerFactory keyManagerFactory, @Nullable TrustManagerFactory trustManagerFactory, + @Nullable String overrideAuthority, @Nullable Iterable ciphers, @Nullable String... tlsVersionProtocols) throws SSLException { + final NettyChannelBuilder channelBuilder; + if (nameResolverProvider != null) {// use client side load balancing + // register + NameResolverRegistry nameResolverRegistry = NameResolverRegistry.getDefaultRegistry();// Use singleton instance in new API to replace deprecated channelBuilder.nameResolverFactory(new nameResolverRegistry().asFactory()); + nameResolverRegistry.register(nameResolverProvider); + // init + String policy = loadBalancingPolicy.getValue(); + String target = nameResolverProvider.getDefaultScheme() + ":///"; // build target as URI + channelBuilder = NettyChannelBuilder.forTarget(target) + .defaultLoadBalancingPolicy(policy); + } else { + switch (uri.getScheme()) { + case "unix": //https://github.com/grpc/grpc-java/issues/1539 + channelBuilder = NettyChannelBuilder.forAddress(new DomainSocketAddress(uri.getPath())) + .eventLoopGroup(new EpollEventLoopGroup()) + .channelType(EpollDomainSocketChannel.class); + break; + default: + String host = uri.getHost(); + int port = uri.getPort(); + if (host == null) { + throw new IllegalArgumentException("The URI format should contains host information, like ://[host:port]/[service], like grpc:///, grpc://host:port, grpcs://host:port, or unix:///path/to/uds.sock. gRpc.client.LoadBalancing.servers should be provided when host/port are not provided."); + } + channelBuilder = NettyChannelBuilder.forAddress(host, port); + break; + } + } + if (keyManagerFactory == null) { + channelBuilder.usePlaintext(); + } else { + final SslContextBuilder sslBuilder = GrpcSslContexts.forClient(); + sslBuilder.keyManager(keyManagerFactory); + if (trustManagerFactory == null) {//ignore Server Certificate + sslBuilder.trustManager(InsecureTrustManagerFactory.INSTANCE); + } else { + sslBuilder.trustManager(trustManagerFactory); + if (overrideAuthority != null) { + channelBuilder.overrideAuthority(overrideAuthority); + } + } + GrpcSslContexts.configure(sslBuilder, SslProvider.OPENSSL); + if (tlsVersionProtocols != null) { + sslBuilder.protocols(tlsVersionProtocols); + } + if (ciphers != null) { + sslBuilder.ciphers(ciphers); + } + SslContext sslContext = sslBuilder.build(); + channelBuilder.sslContext(sslContext).useTransportSecurity(); + } + return channelBuilder; + } + public List getLoadBalancingServers() { return loadBalancingServers; } - public GRPCClient.LoadBalancingPolicy getLoadBalancingPolicy() { + public LoadBalancingPolicy getLoadBalancingPolicy() { return loadBalancingPolicy; } diff --git a/src/main/java/org/summerboot/jexpress/nio/grpc/GRPCServer.java b/src/main/java/org/summerboot/jexpress/nio/grpc/GRPCServer.java index ba0b6e36..3d195c6c 100644 --- a/src/main/java/org/summerboot/jexpress/nio/grpc/GRPCServer.java +++ b/src/main/java/org/summerboot/jexpress/nio/grpc/GRPCServer.java @@ -24,6 +24,7 @@ import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.summerboot.jexpress.boot.BootConstant; import org.summerboot.jexpress.boot.config.NamedDefaultThreadFactory; import org.summerboot.jexpress.boot.instrumentation.NIOStatusListener; @@ -35,6 +36,7 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; /** @@ -44,10 +46,6 @@ public class GRPCServer { protected static final Logger log = LogManager.getLogger(GRPCServer.class.getName()); - // @Inject -// protected NIOStatusListener nioListener; -// @Inject -// protected ServerInterceptor serverInterceptor; protected final String bindingAddr; protected final int port; protected final ServerCredentials serverCredentials; @@ -57,8 +55,7 @@ public class GRPCServer { protected Server server = null; protected ScheduledExecutorService statusReporter = null; - //protected ThreadPoolExecutor tpe = null; - protected boolean servicePaused = false; + //protected boolean servicePaused = false; protected final GRPCServiceCounter serviceCounter = new GRPCServiceCounter(); public ServerBuilder getServerBuilder() { @@ -68,46 +65,6 @@ public ServerBuilder getServerBuilder() { public GRPCServiceCounter getServiceCounter() { return serviceCounter; } -// public GRPCServer(String bindingAddr, int port, KeyManagerFactory kmf, TrustManagerFactory tmf) { -// this(bindingAddr, port, initTLS(kmf, tmf)); -// } -// -// public GRPCServer(String bindingAddr, int port, ServerCredentials serverCredentials) { -// this.bindingAddr = bindingAddr; -// this.port = port; -// this.serverCredentials = serverCredentials; -// if (serverCredentials == null) { -// serverBuilder = NettyServerBuilder.forAddress(new InetSocketAddress(bindingAddr, port)); -// } else { -// serverBuilder = Grpc.newServerBuilderForPort(port, serverCredentials); -// } -// if (serverInterceptor != null) { -// serverBuilder.intercept(serverInterceptor); -// } -// //serverBuilder.executor(tpe) -// //AbstractImplBase implBase = -// //serverBuilder.addService(implBase); -// } -// public ServerInterceptor getServerInterceptor() { -// return serverInterceptor; -// } -// -// public void setContext(SummerRunner.RunnerContext context) { -// this.context = context; -// try { -// serverInterceptor = context.getGuiceInjector().getInstance(ServerInterceptor.class); -// } catch (ConfigurationException ex) { -// -// } -// if (serverInterceptor != null) { -// serverBuilder.intercept(serverInterceptor); -// } -// } - - public GRPCServer(String bindingAddr, int port, KeyManagerFactory kmf, TrustManagerFactory tmf) { - //ServerInterceptor serverInterceptor, int poolCoreSize, int poolMaxSizeMaxSize, int poolQueueSize, long keepAliveSeconds, NIOStatusListener nioListener; - this(bindingAddr, port, kmf, tmf, null, GRPCServerConfig.cfg.getTpe(), null); - } public GRPCServer(String bindingAddr, int port, KeyManagerFactory kmf, TrustManagerFactory tmf, ServerInterceptor serverInterceptor, ThreadPoolExecutor tpe, NIOStatusListener nioListener) { this.bindingAddr = bindingAddr; @@ -121,7 +78,8 @@ public GRPCServer(String bindingAddr, int port, KeyManagerFactory kmf, TrustMana if (serverInterceptor != null) { serverBuilder.intercept(serverInterceptor); } - initThreadPool(tpe, nioListener); + serverBuilder.executor(tpe); + initThreadPool(tpe, nioListener, bindingAddr, port); } protected ServerCredentials initTLS(KeyManagerFactory kmf, TrustManagerFactory tmf) { @@ -141,25 +99,24 @@ protected ServerCredentials initTLS(KeyManagerFactory kmf, TrustManagerFactory t * @param nioListener * @return */ - protected GRPCServiceCounter initThreadPool(ThreadPoolExecutor tpe, NIOStatusListener nioListener) { - serverBuilder.executor(tpe); - + protected GRPCServiceCounter initThreadPool(ThreadPoolExecutor tpe, NIOStatusListener nioListener, String bindingAddr, int port) { int interval = 1; final AtomicReference lastBizHitRef = new AtomicReference<>(); lastBizHitRef.set(-1L); long totalChannel = -1;//NioServerContext.COUNTER_TOTAL_CHANNEL.get(); long activeChannel = -1;//NioServerContext.COUNTER_ACTIVE_CHANNEL.get(); ScheduledExecutorService old2 = statusReporter; - statusReporter = Executors.newSingleThreadScheduledExecutor(new NamedDefaultThreadFactory("gRPC.QPS_SERVICE")); + statusReporter = Executors.newSingleThreadScheduledExecutor(new NamedDefaultThreadFactory("gRPC.QPS_SERVICE@" + bindingAddr + ":" + port)); + final AtomicLong lastChecksum = new AtomicLong(0); + String appInfo = "gRPC@" + BootConstant.VERSION + " " + BootConstant.PID; statusReporter.scheduleAtFixedRate(() -> { if (nioListener == null && !log.isDebugEnabled()) { return; } long bizHit = serviceCounter.getBiz(); - //if (lastBizHit[0] == bizHit && !servicePaused) { - if (lastBizHitRef.get() == bizHit && !servicePaused) { - return; - } +// if (lastBizHitRef.get() == bizHit && !servicePaused) { +// return; +// } lastBizHitRef.set(bizHit); long hps = serviceCounter.getHitAndReset(); long tps = serviceCounter.getProcessedAndReset(); @@ -168,22 +125,28 @@ protected GRPCServiceCounter initThreadPool(ThreadPoolExecutor tpe, NIOStatusLis int active = tpe.getActiveCount(); int queue = tpe.getQueue().size(); - if (hps > 0 || tps > 0 || active > 0 || queue > 0 || servicePaused) { + //if (hps > 0 || tps > 0 || active > 0 || queue > 0 || servicePaused) { // long totalChannel = NioServerContext.COUNTER_TOTAL_CHANNEL.get(); // long activeChannel = NioServerContext.COUNTER_ACTIVE_CHANNEL.get(); - long pool = tpe.getPoolSize(); - int core = tpe.getCorePoolSize(); - //int queueRemainingCapacity = tpe.getQueue().remainingCapacity(); - long max = tpe.getMaximumPoolSize(); - long largest = tpe.getLargestPoolSize(); - long task = tpe.getTaskCount(); - long completed = tpe.getCompletedTaskCount(); - log.debug(() -> "hps=" + hps + ", tps=" + tps + ", totalHit=" + totalHit + " (ping " + pingHit + " + biz " + bizHit + "), queue=" + queue + ", active=" + active + ", pool=" + pool + ", core=" + core + ", max=" + max + ", largest=" + largest + ", task=" + task + ", completed=" + completed + ", activeChannel=" + activeChannel + ", totalChannel=" + totalChannel); + long pool = tpe.getPoolSize(); + int core = tpe.getCorePoolSize(); + //int queueRemainingCapacity = tpe.getQueue().remainingCapacity(); + long max = tpe.getMaximumPoolSize(); + long largest = tpe.getLargestPoolSize(); + long task = tpe.getTaskCount(); + long completed = tpe.getCompletedTaskCount(); + //long checksum = hps + tps + active + queue + activeChannel + totalChannel + totalHit + bizHit + task + completed + active + pool + core + max + largest; + long checksum = hps + tps + active + queue /*+ activeChannel*/ + bizHit + task + completed + active + pool + core + max + largest; + if (lastChecksum.get() != checksum) { + lastChecksum.set(checksum); + //log.debug(() -> "hps=" + hps + ", tps=" + tps + ", activeChannel=" + activeChannel + ", totalChannel=" + totalChannel + ", totalHit=" + totalHit + " (ping" + pingHit + " + biz" + bizHit + "), task=" + task + ", completed=" + completed + ", queue=" + queue + ", active=" + active + ", pool=" + pool + ", core=" + core + ", max=" + max + ", largest=" + largest); + log.debug(() -> "hps=" + hps + ", tps=" + tps + ", totalHit=" + totalHit + " (ping" + pingHit + " + biz" + bizHit + "), task=" + task + ", completed=" + completed + ", queue=" + queue + ", active=" + active + ", pool=" + pool + ", core=" + core + ", max=" + max + ", largest=" + largest); if (nioListener != null) { - nioListener.onNIOAccessReportUpdate("gRPC", hps, tps, totalHit, pingHit, bizHit, totalChannel, activeChannel, task, completed, queue, active, pool, core, max, largest); + nioListener.onNIOAccessReportUpdate(appInfo, hps, tps, totalHit, pingHit, bizHit, totalChannel, activeChannel, task, completed, queue, active, pool, core, max, largest); //listener.onUpdate(data);//bad performance } } + //} }, 0, interval, TimeUnit.SECONDS); if (old2 != null) { old2.shutdownNow(); @@ -191,8 +154,8 @@ protected GRPCServiceCounter initThreadPool(ThreadPoolExecutor tpe, NIOStatusLis return serviceCounter; } - public void start() throws IOException { - this.start(false); + public void start(StringBuilder memo) throws IOException { + this.start(false, memo); } /** @@ -201,14 +164,16 @@ public void start() throws IOException { * @param isBlockingMode * @throws IOException */ - public void start(boolean isBlockingMode) throws IOException { + public void start(boolean isBlockingMode, StringBuilder memo) throws IOException { if (server != null) { shutdown(); } - + String appInfo = BootConstant.VERSION + " " + BootConstant.PID; server = serverBuilder.build().start(); String schema = serverCredentials == null ? "grpc" : "grpcs"; - log.info("*** GRPCServer is listening on " + schema + "://" + bindingAddr + ":" + port); + String info = "Netty GRPC server [" + appInfo + "] is listening on " + schema + "://" + bindingAddr + ":" + port; + memo.append(BootConstant.BR).append(info); + log.info(info); Runtime.getRuntime().addShutdownHook( new Thread(() -> { shutdown(); diff --git a/src/main/java/org/summerboot/jexpress/nio/server/BootHttpFileUploadHandler.java b/src/main/java/org/summerboot/jexpress/nio/server/BootHttpFileUploadHandler.java index 3c3e8363..a6f563e6 100644 --- a/src/main/java/org/summerboot/jexpress/nio/server/BootHttpFileUploadHandler.java +++ b/src/main/java/org/summerboot/jexpress/nio/server/BootHttpFileUploadHandler.java @@ -60,7 +60,7 @@ */ //NOT @ChannelHandler.Sharable due to BootHttpFileUploadHandler is stateful //NOT @Singleton -public abstract class BootHttpFileUploadHandler extends SimpleChannelInboundHandler { +public abstract class BootHttpFileUploadHandler extends SimpleChannelInboundHandler { protected Logger log = LogManager.getLogger(this.getClass()); @@ -209,7 +209,9 @@ protected boolean onPartialChunk(ChannelHandlerContext ctx, long maxAllowedSize) FileUpload fileUpload = (FileUpload) data; if (fileUpload.isCompleted()) { log.debug("file completed " + fileUpload.length()); - onFileUploaded(ctx, fileUpload.getFilename(), fileUpload.getFile(), params, caller, context); + T ret = onFileUploaded(ctx, fileUpload.getFilename(), fileUpload.getFile(), params, caller, context); + context.content(ret); + NioHttpUtil.sendResponse(ctx, true, context, null, null); } break; } @@ -307,6 +309,9 @@ protected long precheck(ChannelHandlerContext ctx, HttpRequest req) { NioHttpUtil.sendResponse(ctx, true, context, null, null); return 0; } + + context.clientAcceptContentType(httpHeaders.get(HttpHeaderNames.ACCEPT)); + return maxAllowedSize; } @@ -316,6 +321,6 @@ protected long precheck(ChannelHandlerContext ctx, HttpRequest req) { protected abstract long getCallerFileUploadSizeLimit_Bytes(Caller caller, ServiceContext context); - protected abstract void onFileUploaded(ChannelHandlerContext ctx, String fileName, File file, Map params, Caller caller, ServiceContext context); + protected abstract T onFileUploaded(ChannelHandlerContext ctx, String fileName, File file, Map params, Caller caller, ServiceContext context); } diff --git a/src/main/java/org/summerboot/jexpress/nio/server/BootHttpRequestHandler.java b/src/main/java/org/summerboot/jexpress/nio/server/BootHttpRequestHandler.java index b7fa9e57..50844e00 100644 --- a/src/main/java/org/summerboot/jexpress/nio/server/BootHttpRequestHandler.java +++ b/src/main/java/org/summerboot/jexpress/nio/server/BootHttpRequestHandler.java @@ -68,7 +68,9 @@ protected ProcessorSettings service(final ChannelHandlerContext ctx, final HttpH final String httpRequestPath, final Map> queryParams, final String httpPostRequestBody, final ServiceContext context) { ProcessorSettings processorSettings = null; RequestProcessor processor = null; - boolean preProcessSuccess = false; + boolean preProcessResult = false; + Object processResult = null; + Throwable processException = null; try { // step1. find controller and the action in it processor = getRequestProcessor(httptMethod, httpRequestPath); @@ -106,29 +108,33 @@ protected ProcessorSettings service(final ChannelHandlerContext ctx, final HttpH if (authenticator != null && !authenticator.customizedAuthorizationCheck(processor, httpRequestHeaders, httpRequestPath, context)) { return processorSettings; } - preProcessSuccess = httpLifecycleListener.beforeProcess(processor, httpRequestHeaders, httpRequestPath, context); - if (preProcessSuccess) { - processor.process(ctx, httpRequestHeaders, httpRequestPath, queryParams, httpPostRequestBody, context); - } else { + preProcessResult = httpLifecycleListener.beforeProcess(processor, httpRequestHeaders, httpRequestPath, context); + if (!preProcessResult) { return processorSettings; } - //} catch (ExpiredJwtException | SignatureException | MalformedJwtException ex) { - // nak(context, HttpResponseStatus.UNAUTHORIZED, BootErrorCode.AUTH_INVALID_TOKEN, "Invalid JWT"); + processResult = processor.process(ctx, httpRequestHeaders, httpRequestPath, queryParams, httpPostRequestBody, context); } catch (NamingException ex) { + processException = ex; httpExceptionListener.onNamingException(ex, httptMethod, httpRequestPath, context); } catch (PersistenceException ex) { + processException = ex; httpExceptionListener.onPersistenceException(ex, httptMethod, httpRequestPath, context); } catch (HttpConnectTimeoutException ex) { + processException = ex; // a connection, over which an HttpRequest is intended to be sent, is not successfully established within a specified time period. httpExceptionListener.onHttpConnectTimeoutException(ex, httptMethod, httpRequestPath, context); } catch (HttpTimeoutException ex) { + processException = ex; // a context is not received within a specified time period. httpExceptionListener.onHttpTimeoutException(ex, httptMethod, httpRequestPath, context); } catch (RejectedExecutionException ex) { + processException = ex; httpExceptionListener.onRejectedExecutionException(ex, httptMethod, httpRequestPath, context); } catch (ConnectException ex) { + processException = ex; httpExceptionListener.onConnectException(ex, httptMethod, httpRequestPath, context); } catch (IOException | UnresolvedAddressException ex) {//SocketException, + processException = ex; Throwable cause = ExceptionUtils.getRootCause(ex); if (cause == null) { cause = ex; @@ -139,13 +145,13 @@ protected ProcessorSettings service(final ChannelHandlerContext ctx, final HttpH httpExceptionListener.onIOException(ex, httptMethod, httpRequestPath, context); } } catch (InterruptedException ex) { + processException = ex; httpExceptionListener.onInterruptedException(ex, httptMethod, httpRequestPath, context); } catch (Throwable ex) { + processException = ex; httpExceptionListener.onUnexpectedException(ex, processor, ctx, httpRequestHeaders, httptMethod, httpRequestPath, queryParams, httpPostRequestBody, context); } finally { - if (preProcessSuccess) { - httpLifecycleListener.afterProcess(processor, ctx, httpRequestHeaders, httptMethod, httpRequestPath, queryParams, httpPostRequestBody, context); - } + httpLifecycleListener.afterProcess(preProcessResult, processResult, processException, processor, ctx, httpRequestHeaders, httptMethod, httpRequestPath, queryParams, httpPostRequestBody, context); context.poi(BootPOI.PROCESS_END); } return processorSettings; diff --git a/src/main/java/org/summerboot/jexpress/nio/server/NioConfig.java b/src/main/java/org/summerboot/jexpress/nio/server/NioConfig.java index 1daf4b0b..b24ef370 100644 --- a/src/main/java/org/summerboot/jexpress/nio/server/NioConfig.java +++ b/src/main/java/org/summerboot/jexpress/nio/server/NioConfig.java @@ -389,7 +389,7 @@ protected void loadCustomizedConfigs(File cfgFile, boolean isReal, ConfigUtil he tempUoloadDir = rootFolder.getAbsolutePath() + File.separator + tempUoload; //8. Default NIO Response HTTP Headers - serverDefaultResponseHeaders = new DefaultHttpHeaders(true); + serverDefaultResponseHeaders = new DefaultHttpHeaders(); Set _keys = props.keySet().stream().map(o -> o.toString()).collect(Collectors.toSet()); List keys = new ArrayList<>(_keys); keys.forEach((name) -> { diff --git a/src/main/java/org/summerboot/jexpress/nio/server/NioServer.java b/src/main/java/org/summerboot/jexpress/nio/server/NioServer.java index 508023bb..eb6cf14f 100644 --- a/src/main/java/org/summerboot/jexpress/nio/server/NioServer.java +++ b/src/main/java/org/summerboot/jexpress/nio/server/NioServer.java @@ -42,7 +42,6 @@ import org.summerboot.jexpress.boot.BackOffice; import org.summerboot.jexpress.boot.BootConstant; import org.summerboot.jexpress.boot.config.NamedDefaultThreadFactory; -import org.summerboot.jexpress.boot.instrumentation.HealthMonitor; import org.summerboot.jexpress.boot.instrumentation.NIOStatusListener; import javax.net.ssl.KeyManagerFactory; @@ -57,6 +56,7 @@ import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; /** @@ -84,7 +84,7 @@ public NioServer(NioChannelInitializer channelInitializer, NIOStatusListener nio * @throws InterruptedException * @throws SSLException */ - public void bind(NioConfig nioCfg) throws InterruptedException, SSLException { + public void bind(NioConfig nioCfg, StringBuilder memo) throws InterruptedException, SSLException { List bindingAddresses = nioCfg.getBindingAddresses(); if (bindingAddresses == null || bindingAddresses.isEmpty()) { log.info("Skip HTTP server due to no bindingAddresses in config file: " + nioCfg.getCfgFile()); @@ -188,26 +188,29 @@ public void bind(NioConfig nioCfg) throws InterruptedException, SSLException { String protocol; if (jdkSslContext == null && nettySslContext == null) { sslMode = "non-ssl"; - protocol = multiplexer + " http://"; + protocol = "http://"; } else { sslMode = "Client Auth: " + clientAuth; - protocol = multiplexer + " https://"; + protocol = "https://"; } + String listenerInfo = "[multiplexer=" + multiplexer + "] " + sslMode; String bindAddr = addr.getAddress().getHostAddress(); int listeningPort = addr.getPort(); - // bind +// bind ChannelFuture f = boot.bind(bindAddr, listeningPort).sync(); f.channel().closeFuture().addListener((ChannelFutureListener) (ChannelFuture f1) -> { //shutdown(); - System.out.println("Server " + appInfo + " (" + sslMode + ") is stopped"); + System.out.println("Server " + appInfo + " (" + listenerInfo + ") is stopped"); }); List loadBalancingPingEndpoints = BackOffice.agent.getLoadBalancingPingEndpoints(); for (String loadBalancingPingEndpoint : loadBalancingPingEndpoints) { - log.info(() -> "Server " + appInfo + " (" + sslMode + ") is listening on " + protocol + bindAddr + ":" + listeningPort + (loadBalancingPingEndpoint == null ? "" : loadBalancingPingEndpoint)); + String info = "Netty HTTP server [" + appInfo + "] (" + listenerInfo + ") is listening on " + protocol + bindAddr + ":" + listeningPort + (loadBalancingPingEndpoint == null ? "" : loadBalancingPingEndpoint); + memo.append(BootConstant.BR).append(info); + log.info(() -> info); } if (nioListener != null) { - nioListener.onNIOBindNewPort(appInfo, sslMode, protocol, bindAddr, listeningPort, loadBalancingEndpoints); + nioListener.onNIOBindNewPort(appInfo, listenerInfo, protocol, bindAddr, listeningPort, loadBalancingEndpoints); } } @@ -215,6 +218,7 @@ public void bind(NioConfig nioCfg) throws InterruptedException, SSLException { final AtomicReference lastBizHitRef = new AtomicReference<>(); lastBizHitRef.set(-1L); if (nioListener != null || log.isDebugEnabled()) { + final AtomicLong lastChecksum = new AtomicLong(0); int interval = 1; QPS_SERVICE = Executors.newSingleThreadScheduledExecutor(new NamedDefaultThreadFactory("NIO.QPS_SERVICE")); QPS_SERVICE.scheduleAtFixedRate(() -> { @@ -225,26 +229,29 @@ public void bind(NioConfig nioCfg) throws InterruptedException, SSLException { } long bizHit = NioCounter.COUNTER_BIZ_HIT.get(); //if (lastBizHit[0] == bizHit && !servicePaused) { - if (lastBizHitRef.get() == bizHit && !HealthMonitor.isServicePaused()) { - return; - } +// if (lastBizHitRef.get() == bizHit && !HealthMonitor.isServicePaused()) { +// return; +// } //lastBizHit[0] = bizHit; lastBizHitRef.set(bizHit); ThreadPoolExecutor tpe = nioCfg.getBizExecutor(); int active = tpe.getActiveCount(); int queue = tpe.getQueue().size(); long activeChannel = NioCounter.COUNTER_ACTIVE_CHANNEL.get(); - if (hps > 0 || tps > 0 || active > 0 || queue > 0 || activeChannel > 0) { - long totalChannel = NioCounter.COUNTER_TOTAL_CHANNEL.get(); - long pool = tpe.getPoolSize(); - int core = tpe.getCorePoolSize(); - //int queueRemainingCapacity = tpe.getQueue().remainingCapacity(); - long max = tpe.getMaximumPoolSize(); - long largest = tpe.getLargestPoolSize(); - long task = tpe.getTaskCount(); - long completed = tpe.getCompletedTaskCount(); - long pingHit = NioCounter.COUNTER_PING_HIT.get(); - long totalHit = bizHit + pingHit; + //if (hps > 0 || tps > 0 || active > 0 || queue > 0 || activeChannel > 0) { + long totalChannel = NioCounter.COUNTER_TOTAL_CHANNEL.get(); + long pool = tpe.getPoolSize(); + int core = tpe.getCorePoolSize(); + //int queueRemainingCapacity = tpe.getQueue().remainingCapacity(); + long max = tpe.getMaximumPoolSize(); + long largest = tpe.getLargestPoolSize(); + long task = tpe.getTaskCount(); + long completed = tpe.getCompletedTaskCount(); + long pingHit = NioCounter.COUNTER_PING_HIT.get(); + long totalHit = bizHit + pingHit; + long checksum = hps + tps + active + queue + activeChannel + bizHit + task + completed + active + pool + core + max + largest; + if (lastChecksum.get() != checksum) { + lastChecksum.set(checksum); log.debug(() -> "hps=" + hps + ", tps=" + tps + ", activeChannel=" + activeChannel + ", totalChannel=" + totalChannel + ", totalHit=" + totalHit + " (ping" + pingHit + " + biz" + bizHit + "), task=" + task + ", completed=" + completed + ", queue=" + queue + ", active=" + active + ", pool=" + pool + ", core=" + core + ", max=" + max + ", largest=" + largest); if (nioListener != null) { nioListener.onNIOAccessReportUpdate(appInfo, hps, tps, totalHit, pingHit, bizHit, totalChannel, activeChannel, task, completed, queue, active, pool, core, max, largest); diff --git a/src/main/java/org/summerboot/jexpress/nio/server/RequestProcessor.java b/src/main/java/org/summerboot/jexpress/nio/server/RequestProcessor.java index 6c9d7f72..e70223a3 100644 --- a/src/main/java/org/summerboot/jexpress/nio/server/RequestProcessor.java +++ b/src/main/java/org/summerboot/jexpress/nio/server/RequestProcessor.java @@ -41,6 +41,6 @@ public interface RequestProcessor { boolean authorizationCheck(final ChannelHandlerContext channelHandlerCtx, final HttpHeaders httpHeaders, final String httpRequestPath, final Map> queryParams, final String httpPostRequestBody, final ServiceContext context, int badRequestErrorCode) throws Throwable; - void process(ChannelHandlerContext ctx, HttpHeaders httpRequestHeaders, String httpRequestPath, Map> queryParams, String httpPostRequestBody, ServiceContext context) throws Throwable; + Object process(ChannelHandlerContext ctx, HttpHeaders httpRequestHeaders, String httpRequestPath, Map> queryParams, String httpPostRequestBody, ServiceContext context) throws Throwable; } diff --git a/src/main/java/org/summerboot/jexpress/nio/server/domain/ServiceContext.java b/src/main/java/org/summerboot/jexpress/nio/server/domain/ServiceContext.java index b72013ca..8a5fdc90 100644 --- a/src/main/java/org/summerboot/jexpress/nio/server/domain/ServiceContext.java +++ b/src/main/java/org/summerboot/jexpress/nio/server/domain/ServiceContext.java @@ -16,12 +16,14 @@ package org.summerboot.jexpress.nio.server.domain; import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.core.JsonProcessingException; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.DefaultHttpHeaders; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpResponseStatus; +import jakarta.ws.rs.core.MediaType; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.logging.log4j.Level; @@ -34,6 +36,7 @@ import org.summerboot.jexpress.nio.server.ResponseEncoder; import org.summerboot.jexpress.security.auth.Caller; import org.summerboot.jexpress.util.ApplicationUtil; +import org.summerboot.jexpress.util.BeanUtil; import java.io.BufferedReader; import java.io.File; @@ -306,7 +309,7 @@ public ServiceContext responseHeaders(HttpHeaders headers) { return this; } if (this.responseHeaders == null) { - this.responseHeaders = new DefaultHttpHeaders(true); + this.responseHeaders = new DefaultHttpHeaders(); } this.responseHeaders.set(headers); return this; @@ -327,7 +330,7 @@ public ServiceContext responseHeader(String key, Object value) { return this; } if (responseHeaders == null) { - responseHeaders = new DefaultHttpHeaders(true); + responseHeaders = new DefaultHttpHeaders(); } if (value == null) { responseHeaders.remove(key); @@ -352,7 +355,7 @@ public ServiceContext responseHeader(String key, Iterable values) { return this; } if (responseHeaders == null) { - responseHeaders = new DefaultHttpHeaders(true); + responseHeaders = new DefaultHttpHeaders(); } if (values == null) { responseHeaders.remove(key); @@ -367,7 +370,7 @@ public ServiceContext responseHeaders(Map> hs) { return this; } if (responseHeaders == null) { - responseHeaders = new DefaultHttpHeaders(true); + responseHeaders = new DefaultHttpHeaders(); } hs.keySet().stream().filter((key) -> (StringUtils.isNotBlank(key))).forEachOrdered((key) -> { Iterable values = hs.get(key); @@ -614,7 +617,7 @@ public ServiceContext file(File file, boolean isDownloadMode) { // } if (responseHeaders == null) { - responseHeaders = new DefaultHttpHeaders(true); + responseHeaders = new DefaultHttpHeaders(); } long fileLength = file.length(); if (fileLength > Integer.MAX_VALUE) { @@ -634,6 +637,54 @@ public ServiceContext file(File file, boolean isDownloadMode) { return this; } + public ServiceContext content(Object ret) throws JsonProcessingException { + if (ret == null) { + return this; + } + if (ret instanceof File) { + this.file((File) ret, true); + } else { + String responseContentType; + //1. calculate responseContentType + if (clientAcceptContentType == null) {// client not specified + responseContentType = MediaType.APPLICATION_JSON; + } else if (clientAcceptContentType.contains("json")) { + responseContentType = MediaType.APPLICATION_JSON; + } else if (clientAcceptContentType.contains("xml")) { + responseContentType = MediaType.APPLICATION_XML; + } else if (clientAcceptContentType.contains("txt")) { + responseContentType = MediaType.TEXT_HTML; + } else { + responseContentType = MediaType.APPLICATION_JSON; + } + + //2. set content and contentType + if (ret instanceof String) { + this.txt((String) ret); + } else { + switch (responseContentType) { + case MediaType.APPLICATION_JSON: + this.txt(BeanUtil.toJson(ret)); + break; + case MediaType.APPLICATION_XML: + case MediaType.TEXT_XML: + this.txt(BeanUtil.toXML(ret)); + break; + case MediaType.TEXT_HTML: + case MediaType.TEXT_PLAIN: + this.txt(ret.toString()); + break; + } + } + //3. update content type + if (this.contentType() == null) { + this.contentType(responseContentType); + } + } + + return this; + } + //@JsonInclude(JsonInclude.Include.NON_NULL) public T caller() { return (T) caller; @@ -654,7 +705,7 @@ public ServiceContext callerId(String callerId) { return this; } - // public int errorCode() { +// public int errorCode() { // return errorCode; // } // diff --git a/src/main/java/org/summerboot/jexpress/nio/server/ws/rs/BootController.java b/src/main/java/org/summerboot/jexpress/nio/server/ws/rs/BootController.java index 3c774794..42ed13f9 100644 --- a/src/main/java/org/summerboot/jexpress/nio/server/ws/rs/BootController.java +++ b/src/main/java/org/summerboot/jexpress/nio/server/ws/rs/BootController.java @@ -103,9 +103,12 @@ abstract public class BootController extends PingController { public static final String DESC_401 = "Unauthorized. The client should sign-on again, but not retransmit the same request again"; public static final String DESC_403 = "Client has no permission. Client should not retransmit the same request again."; public static final String DESC_404 = "Not Found. The client should not retransmit the same request again."; + public static final String DESC_429 = "Too Many Requests"; public static final String DESC_500 = "All other 5xx code. Server errors due to unexpected failures. The client can continue and try again with the request without modification."; public static final String DESC_501 = "Not Implemented. The client can continue and try again with the request without modification."; public static final String DESC_503 = "Service Unavailable. The client can continue and try again with the request without modification."; + public static final String DESC_504 = "Gateway Timeout. The client can continue and try again with the request without modification."; + public static final String DESC_507 = "Insufficient Storage. The client should contact the system administrator. Do not try the request again."; @Inject protected AuthTokenCache authTokenCache; @@ -135,6 +138,9 @@ abstract public class BootController extends PingController { @ApiResponse(responseCode = "404", description = DESC_404, content = @Content(schema = @Schema(implementation = ServiceError.class)) ), + @ApiResponse(responseCode = "429", description = DESC_429, + content = @Content(schema = @Schema(implementation = ServiceError.class)) + ), @ApiResponse(responseCode = "500", description = DESC_500, content = @Content(schema = @Schema(implementation = ServiceError.class)) ), @@ -143,6 +149,12 @@ abstract public class BootController extends PingController { ), @ApiResponse(responseCode = "503", description = DESC_503, content = @Content(schema = @Schema(implementation = ServiceError.class)) + ), + @ApiResponse(responseCode = "504", description = DESC_504, + content = @Content(schema = @Schema(implementation = ServiceError.class)) + ), + @ApiResponse(responseCode = "507", description = DESC_507, + content = @Content(schema = @Schema(implementation = ServiceError.class)) ) }, security = { @@ -186,6 +198,9 @@ protected String getVersion() { @ApiResponse(responseCode = "404", description = DESC_404, content = @Content(schema = @Schema(implementation = ServiceError.class)) ), + @ApiResponse(responseCode = "429", description = DESC_429, + content = @Content(schema = @Schema(implementation = ServiceError.class)) + ), @ApiResponse(responseCode = "500", description = DESC_500, content = @Content(schema = @Schema(implementation = ServiceError.class)) ), @@ -194,6 +209,12 @@ protected String getVersion() { ), @ApiResponse(responseCode = "503", description = DESC_503, content = @Content(schema = @Schema(implementation = ServiceError.class)) + ), + @ApiResponse(responseCode = "504", description = DESC_504, + content = @Content(schema = @Schema(implementation = ServiceError.class)) + ), + @ApiResponse(responseCode = "507", description = DESC_507, + content = @Content(schema = @Schema(implementation = ServiceError.class)) ) }, security = { @@ -227,6 +248,9 @@ public void inspect(@Parameter(hidden = true) final ServiceContext context) { @ApiResponse(responseCode = "404", description = DESC_404, content = @Content(schema = @Schema(implementation = ServiceError.class)) ), + @ApiResponse(responseCode = "429", description = DESC_429, + content = @Content(schema = @Schema(implementation = ServiceError.class)) + ), @ApiResponse(responseCode = "500", description = DESC_500, content = @Content(schema = @Schema(implementation = ServiceError.class)) ), @@ -235,6 +259,12 @@ public void inspect(@Parameter(hidden = true) final ServiceContext context) { ), @ApiResponse(responseCode = "503", description = DESC_503, content = @Content(schema = @Schema(implementation = ServiceError.class)) + ), + @ApiResponse(responseCode = "504", description = DESC_504, + content = @Content(schema = @Schema(implementation = ServiceError.class)) + ), + @ApiResponse(responseCode = "507", description = DESC_507, + content = @Content(schema = @Schema(implementation = ServiceError.class)) ) }, security = { @@ -273,6 +303,9 @@ public void pause(@QueryParam("pause") boolean pause, @Parameter(hidden = true) @ApiResponse(responseCode = "404", description = DESC_404, content = @Content(schema = @Schema(implementation = ServiceError.class)) ), + @ApiResponse(responseCode = "429", description = DESC_429, + content = @Content(schema = @Schema(implementation = ServiceError.class)) + ), @ApiResponse(responseCode = "500", description = DESC_500, content = @Content(schema = @Schema(implementation = ServiceError.class)) ), @@ -281,6 +314,12 @@ public void pause(@QueryParam("pause") boolean pause, @Parameter(hidden = true) ), @ApiResponse(responseCode = "503", description = DESC_503, content = @Content(schema = @Schema(implementation = ServiceError.class)) + ), + @ApiResponse(responseCode = "504", description = DESC_504, + content = @Content(schema = @Schema(implementation = ServiceError.class)) + ), + @ApiResponse(responseCode = "507", description = DESC_507, + content = @Content(schema = @Schema(implementation = ServiceError.class)) ) } ) @@ -319,6 +358,9 @@ public Caller longin_jSecurityCheck(@Parameter(required = true) @Nonnull @FormPa @ApiResponse(responseCode = "404", description = DESC_404, content = @Content(schema = @Schema(implementation = ServiceError.class)) ), + @ApiResponse(responseCode = "429", description = DESC_429, + content = @Content(schema = @Schema(implementation = ServiceError.class)) + ), @ApiResponse(responseCode = "500", description = DESC_500, content = @Content(schema = @Schema(implementation = ServiceError.class)) ), @@ -327,6 +369,12 @@ public Caller longin_jSecurityCheck(@Parameter(required = true) @Nonnull @FormPa ), @ApiResponse(responseCode = "503", description = DESC_503, content = @Content(schema = @Schema(implementation = ServiceError.class)) + ), + @ApiResponse(responseCode = "504", description = DESC_504, + content = @Content(schema = @Schema(implementation = ServiceError.class)) + ), + @ApiResponse(responseCode = "507", description = DESC_507, + content = @Content(schema = @Schema(implementation = ServiceError.class)) ) } ) @@ -373,6 +421,9 @@ public static Caller login(Authenticator auth, String userId, String password, S @ApiResponse(responseCode = "404", description = DESC_404, content = @Content(schema = @Schema(implementation = ServiceError.class)) ), + @ApiResponse(responseCode = "429", description = DESC_429, + content = @Content(schema = @Schema(implementation = ServiceError.class)) + ), @ApiResponse(responseCode = "500", description = DESC_500, content = @Content(schema = @Schema(implementation = ServiceError.class)) ), @@ -381,6 +432,12 @@ public static Caller login(Authenticator auth, String userId, String password, S ), @ApiResponse(responseCode = "503", description = DESC_503, content = @Content(schema = @Schema(implementation = ServiceError.class)) + ), + @ApiResponse(responseCode = "504", description = DESC_504, + content = @Content(schema = @Schema(implementation = ServiceError.class)) + ), + @ApiResponse(responseCode = "507", description = DESC_507, + content = @Content(schema = @Schema(implementation = ServiceError.class)) ) }, security = { diff --git a/src/main/java/org/summerboot/jexpress/nio/server/ws/rs/JaxRsRequestProcessor.java b/src/main/java/org/summerboot/jexpress/nio/server/ws/rs/JaxRsRequestProcessor.java index 9551ca52..8f0a1d6d 100644 --- a/src/main/java/org/summerboot/jexpress/nio/server/ws/rs/JaxRsRequestProcessor.java +++ b/src/main/java/org/summerboot/jexpress/nio/server/ws/rs/JaxRsRequestProcessor.java @@ -393,7 +393,7 @@ public boolean authorizationCheck(final ChannelHandlerContext channelHandlerCtx, } @Override - public void process(final ChannelHandlerContext channelHandlerCtx, final HttpHeaders httpHeaders, final String httpRequestPath, final Map> queryParams, final String httpPostRequestBody, final ServiceContext context) throws Throwable { + public Object process(final ChannelHandlerContext channelHandlerCtx, final HttpHeaders httpHeaders, final String httpRequestPath, final Map> queryParams, final String httpPostRequestBody, final ServiceContext context) throws Throwable { //2. invoke Object ret; Object[] paramValues = new Object[parameterSize]; @@ -403,7 +403,7 @@ public void process(final ChannelHandlerContext channelHandlerCtx, final HttpHea paramValues[i] = parameterList.get(i).value(request, context); } if (context.error() != null) { - return; + return null; } } try { @@ -411,12 +411,12 @@ public void process(final ChannelHandlerContext channelHandlerCtx, final HttpHea if (rejectWhenHealthCheckFailed && !HealthMonitor.isHealthCheckSuccess()) { context.status(HttpResponseStatus.BAD_GATEWAY) .error(new Err(BootErrorCode.SERVICE_HEALTH_CHECK_FAILED, null, null, null, "Service health check failed: " + HealthMonitor.getStatusReasonHealthCheck())); - return; + return null; } if (rejectWhenPaused && HealthMonitor.isServicePaused()) { context.status(HttpResponseStatus.SERVICE_UNAVAILABLE) .error(new Err(BootErrorCode.SERVICE_PAUSED, null, null, null, "Service is paused: " + HealthMonitor.getStatusReasonPaused())); - return; + return null; } ret = javaMethod.invoke(javaInstance, paramValues); @@ -489,6 +489,7 @@ public void process(final ChannelHandlerContext channelHandlerCtx, final HttpHea } } } + return ret; } public boolean hasMatrixPara() { diff --git a/src/main/java/org/summerboot/jexpress/security/JwtUtil.java b/src/main/java/org/summerboot/jexpress/security/JwtUtil.java index 50d45e0e..21d2f6c7 100644 --- a/src/main/java/org/summerboot/jexpress/security/JwtUtil.java +++ b/src/main/java/org/summerboot/jexpress/security/JwtUtil.java @@ -20,14 +20,18 @@ import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.JwtParser; import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; +import io.jsonwebtoken.security.MacAlgorithm; +import io.jsonwebtoken.security.SignatureAlgorithm; +import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.security.Key; import java.security.KeyPair; +import java.security.PublicKey; import java.time.Duration; import java.util.Base64; +import java.util.Collection; import java.util.Date; /** @@ -35,9 +39,15 @@ */ public class JwtUtil { - public static String buildSigningKey(SignatureAlgorithm signatureAlgorithm) { - final Key signingKey = Keys.secretKeyFor(signatureAlgorithm); - return EncryptorUtil.keyToString(signingKey); + public static String buildSigningKey(MacAlgorithm signatureAlgorithm) { + SecretKey key = signatureAlgorithm.key().build(); + return EncryptorUtil.keyToString(key); + } + + public static Key parseSigningKey(String encodedKey) { + //return EncryptorUtil.keyFromString(sk, signatureAlgorithm.getJcaName()); "HmacSHA256" + byte[] decodedKey = Base64.getDecoder().decode(encodedKey); + return Keys.hmacShaKeyFor(decodedKey); } /** @@ -50,13 +60,7 @@ public static String buildSigningKey(SignatureAlgorithm signatureAlgorithm) { * @return */ public static KeyPair buildSigningParsingKeyPair(SignatureAlgorithm signatureAlgorithm) { - return Keys.keyPairFor(signatureAlgorithm); - } - - public static Key parseSigningKey(String encodedKey) { - //return EncryptorUtil.keyFromString(sk, signatureAlgorithm.getJcaName()); "HmacSHA256" - byte[] decodedKey = Base64.getDecoder().decode(encodedKey); - return Keys.hmacShaKeyFor(decodedKey); + return signatureAlgorithm.keyPair().build(); } // @Deprecated @@ -119,12 +123,12 @@ public static Key parseSigningKey(String encodedKey) { // return builder.compact(); // } - public static String createJWT(String keyAlgorithm, String jwtSigningKey, String id, String issuer, String subject, String audience, int ttlSeconds) { + public static String createJWT(String keyAlgorithm, String jwtSigningKey, int ttlSeconds, String id, String issuer, String subject, Collection audience) { JwtBuilder builder = Jwts.builder() - .setId(id) - .setIssuer(issuer) - .setSubject(subject) - .setAudience(audience); + .id(id) + .issuer(issuer) + .subject(subject); + builder.audience().add(audience); return createJWT(keyAlgorithm, jwtSigningKey, builder, Duration.ofSeconds(ttlSeconds)); } @@ -133,12 +137,12 @@ public static String createJWT(String keyAlgorithm, String jwtSigningKey, JwtBui return createJWT(keyAlgorithm, key, builder, ttl); } - public static String createJWT(String keyAlgorithm, byte[] jwtSigningKey, String id, String issuer, String subject, String audience, int ttlSeconds) { + public static String createJWT(String keyAlgorithm, byte[] jwtSigningKey, int ttlSeconds, String id, String issuer, String subject, Collection audience) { JwtBuilder builder = Jwts.builder() - .setId(id) - .setIssuer(issuer) - .setSubject(subject) - .setAudience(audience); + .id(id) + .issuer(issuer) + .subject(subject); + builder.audience().add(audience); return createJWT(keyAlgorithm, jwtSigningKey, builder, Duration.ofSeconds(ttlSeconds)); } @@ -156,56 +160,61 @@ public static void setJwtExpireTime(JwtBuilder builder, Duration ttl) { long expireTimeMilsec = System.currentTimeMillis() + ttlMilsec; if (expireTimeMilsec > 0) {// no expire if (nowMillis + ttlMillis) overflow Date exp = new Date(expireTimeMilsec); - builder.setExpiration(exp); + builder.expiration(exp); } } } - public static String createJWT(Key privateKey, String id, String issuer, String subject, String audience, int ttlSeconds) { + public static String createJWT(Key privateKey, int ttlSeconds, String id, String issuer, String subject, Collection audience) { JwtBuilder builder = Jwts.builder() - .setId(id) - .setIssuer(issuer) - .setSubject(subject) - .setAudience(audience); + .id(id) + .issuer(issuer) + .subject(subject); + builder.audience().add(audience); return createJWT(privateKey, builder, Duration.ofSeconds(ttlSeconds)); } public static String createJWT(Key privateKey, JwtBuilder builder, Duration ttl) { - //0. We will sign our JWT with our ApiKey secret - //byte[] apiKeySecretBytes = parseSigningKey(jwtRootSigningKeyString); - //The JWT signature algorithm we will be using to sign the token - //Key signingKey = new SecretKeySpec(jwtSigningKey, signatureAlgorithm.getJcaName()); - - //1. set ecpire time setJwtExpireTime(builder, ttl); + builder.issuedAt(new Date()).signWith(privateKey); + // Builds the JWT and serializes it to a compact, URL-safe string + return builder.compact(); + } - //2. Let's set the JWT Claims - builder.setIssuedAt(new Date()); - //builder.signWith(signatureAlgorithm, privateKey); - builder.signWith(privateKey); + public static Jws parseJWT(Key verifyKey, String token) { + if (verifyKey instanceof SecretKey) { + return parseJWT((SecretKey) verifyKey, token); + } else if (verifyKey instanceof PublicKey) { + return parseJWT((PublicKey) verifyKey, token); + } else { + throw new IllegalArgumentException("Unsupported Key type: " + verifyKey.getClass().getName()); + } + } - //3. Builds the JWT and serializes it to a compact, URL-safe string - return builder.compact(); + public static Jws parseJWT(SecretKey jwtRootSigningKey, String token) { + JwtParser parser = Jwts.parser() // (1) + .verifyWith(jwtRootSigningKey) // (2) + .build(); // (3) + return parseJWT(parser, token); // (4) } - public static Jws parseJWT(Key jwtRootSigningKey, String token) { - JwtParser parser = Jwts.parserBuilder() // (1) - .setSigningKey(jwtRootSigningKey) // (2) + public static Jws parseJWT(PublicKey publicKey, String token) { + JwtParser parser = Jwts.parser() // (1) + .verifyWith(publicKey) // (2) .build(); // (3) - return parser.parseClaimsJws(token); // (4) -// JwsHeader h = ret.getHeader(); -// return ret.getBody(); + return parseJWT(parser, token); // (4) } - public static Jws parseJWT(byte[] jwtRootSigningKey, String token) { - JwtParser parser = Jwts.parserBuilder() // (1) - .setSigningKey(jwtRootSigningKey) // (2) + /*public static Jws parseJWT(byte[] jwtRootSigningKey, String token) { + JwtParser parser = Jwts.parser() // (1) + .verifyWith(jwtRootSigningKey) // (2) .build(); // (3) return parser.parseClaimsJws(token); // (4) - } + }*/ public static Jws parseJWT(JwtParser parser, String token) { - return parser.parseClaimsJws(token); // (4) + //return parser.parseClaimsJws(token); // (4) + return parser.parseSignedClaims(token); } } diff --git a/src/main/java/org/summerboot/jexpress/security/auth/AuthConfig.java b/src/main/java/org/summerboot/jexpress/security/auth/AuthConfig.java index 1d84dc39..7e2a40c9 100644 --- a/src/main/java/org/summerboot/jexpress/security/auth/AuthConfig.java +++ b/src/main/java/org/summerboot/jexpress/security/auth/AuthConfig.java @@ -29,6 +29,7 @@ import org.summerboot.jexpress.security.EncryptorUtil; import org.summerboot.jexpress.security.JwtUtil; +import javax.crypto.SecretKey; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.TrustManagerFactory; import java.io.File; @@ -239,8 +240,8 @@ protected void loadCustomizedConfigs(File cfgFile, boolean isReal, ConfigUtil he if (symmetricKey != null) { //jwtSigningKey = EncryptorUtil.keyFromString(jwtSigningKeyString, jwtSignatureAlgorithm.getJcaName()); jwtSigningKey = JwtUtil.parseSigningKey(symmetricKey); - jwtParser = Jwts.parserBuilder() // (1) - .setSigningKey(jwtSigningKey) // (2) + jwtParser = Jwts.parser() // (1) + .verifyWith((SecretKey) jwtSigningKey) // (2) .build(); // (3) } //File rootFolder = cfgFile.getParentFile().getParentFile(); @@ -251,8 +252,8 @@ protected void loadCustomizedConfigs(File cfgFile, boolean isReal, ConfigUtil he if (publicKeyFile != null) { createIfNotExist(JWT_PUBLIC_KEY_FILE, JWT_PUBLIC_KEY_FILE); PublicKey publicKey = EncryptorUtil.loadPublicKey(EncryptorUtil.KeyFileType.PKCS12, publicKeyFile); - jwtParser = Jwts.parserBuilder() // (1) - .setSigningKey(publicKey) // (2) + jwtParser = Jwts.parser() // (1) + .verifyWith(publicKey) // (2) .build(); // (3) } diff --git a/src/main/java/org/summerboot/jexpress/security/auth/BootAuthenticator.java b/src/main/java/org/summerboot/jexpress/security/auth/BootAuthenticator.java index cfd45726..ba5b0852 100644 --- a/src/main/java/org/summerboot/jexpress/security/auth/BootAuthenticator.java +++ b/src/main/java/org/summerboot/jexpress/security/auth/BootAuthenticator.java @@ -42,14 +42,12 @@ import org.summerboot.jexpress.nio.server.domain.Err; import org.summerboot.jexpress.nio.server.domain.ServiceContext; import org.summerboot.jexpress.security.JwtUtil; -import org.summerboot.jexpress.util.FormatterUtil; import javax.naming.NamingException; import java.security.Key; import java.time.Duration; import java.util.Date; import java.util.Set; -import java.util.stream.Collectors; /** * @param authenticate(T metaData) @@ -137,35 +135,34 @@ public JwtBuilder toJwt(Caller caller) { String issuer = AuthConfig.cfg.getJwtIssuer(); String userName = caller.getUid(); Set groups = caller.getGroups(); - String groupsCsv = groups == null || groups.size() < 1 + /*String groupsCsv = groups == null || groups.size() < 1 ? null : groups.stream().collect(Collectors.joining(",")); - String audience = groupsCsv; + String audience = groupsCsv;*/ - Claims claims = Jwts.claims(); - claims.setId(jti) - .setIssuer(issuer) - .setSubject(userName) - .setAudience(audience); + JwtBuilder builder = Jwts.builder(); + builder.id(jti) + .issuer(issuer) + .subject(userName) + .audience().add(groups); if (caller.getId() != null) { - claims.put("callerId", caller.getId()); + builder.claim("callerId", caller.getId()); } if (caller.getTenantId() != null) { - claims.put("tenantId", caller.getTenantId()); + builder.claim("tenantId", caller.getTenantId()); } if (caller.getTenantName() != null) { - claims.put("tenantName", caller.getTenantName()); + builder.claim("tenantName", caller.getTenantName()); } Set keys = caller.propKeySet(); if (keys != null) { for (String key : keys) { Object v = caller.getProp(key, Object.class); - claims.put(key, v); + builder.claim(key, v); } } - JwtBuilder builder = Jwts.builder().setClaims(claims); - + //JwtBuilder builder = Jwts.builder().setClaims(claims); return builder; } @@ -174,7 +171,7 @@ protected Claims parseJWT(String jwt) { if (jwtParser == null) { throw new UnsupportedOperationException(ERROR_NO_CFG); } - return JwtUtil.parseJWT(jwtParser, jwt).getBody(); + return JwtUtil.parseJWT(jwtParser, jwt).getPayload(); } /** @@ -188,17 +185,15 @@ protected Caller fromJwt(Claims claims) { //String jti = claims.getId(); //String issuer = claims.getIssuer(); String userName = claims.getSubject(); - String audience = claims.getAudience(); + Set audience = claims.getAudience(); Long userId = claims.get("callerId", Long.class); Long tenantId = claims.get("tenantId", Long.class); String tenantName = claims.get("tenantName", String.class); User caller = new User(tenantId, tenantName, userId, userName); - String userGroups = audience; - if (StringUtils.isNotBlank(userGroups)) { - String[] groups = FormatterUtil.parseCsv(userGroups); - for (String group : groups) { + if (audience != null) { + for (String group : audience) { caller.addGroup(group); } } @@ -391,6 +386,9 @@ public ServerCall.Listener interceptCall(ServerCall - + - + - + + @@ -21,7 +23,7 @@ @@ -33,7 +35,7 @@ @@ -45,7 +47,7 @@ @@ -53,16 +55,28 @@ + + + + + + + - + @@ -74,41 +88,32 @@ - - + - - + - - - + - - + - -