diff --git a/build.gradle b/build.gradle index 6401cb6903d6e48abb7d7e1b69fd7e791e61f69d..cecdf3f48767d896b6a7325ac8cdcbeed859a40a 100644 --- a/build.gradle +++ b/build.gradle @@ -36,6 +36,7 @@ dependencies { implementation 'org.aspectj:aspectjweaver:1.9.19' // common implementation 'org.aspectj:aspectjrt:1.9.19' // common implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.15.0-rc1' + implementation 'nl.basjes.parse.useragent:yauaa:7.17.1' testImplementation 'org.springframework.boot:spring-boot-starter-test' } diff --git a/src/main/java/edu/umd/dawn/common/interceptor/JWTInterceptor.java b/src/main/java/edu/umd/dawn/common/interceptor/JWTInterceptor.java index a2fff2263423defcee06e85ea04138e10d76c89a..a202269d74d1fa6ad84274a970486e47ecccefba 100644 --- a/src/main/java/edu/umd/dawn/common/interceptor/JWTInterceptor.java +++ b/src/main/java/edu/umd/dawn/common/interceptor/JWTInterceptor.java @@ -5,6 +5,9 @@ import edu.umd.dawn.common.jwt.JWTUtil; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.log4j.Log4j2; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; /** @@ -12,27 +15,30 @@ import org.springframework.web.servlet.HandlerInterceptor; * @implNote This does NOT throw any error if a jwt is not provided */ @Log4j2 +@Component public class JWTInterceptor implements HandlerInterceptor { + @Value("${config.serviceName:dawn-service}") + private String serviceName; + + @Value("${config.local:false}") private boolean local; + + @Value("${config.jwt.warn:true}") private boolean warn; + + @Value("${config.accessSecret:empty}") private String accessSecret; - /** - * - * @param accessSecret JWT Access Secret from configuration - * @param local is the environment a local env or not - * @param warn should a warning be thrown if no jwt is provided - */ - public JWTInterceptor(String accessSecret, boolean local, boolean warn) { - this.local = local; - this.accessSecret = accessSecret; - this.warn = warn; - } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + + if (accessSecret.equals("empty")) { + log.warn("This service has not been provided the configuration value for `config.accessSecret` in the application.properties configuration. Since it uses the JWTInterceptor, this is a required value for proper functionality."); + } + if (!local) { String token = request.getHeader("Authorization"); if (token != null && !token.equals("")) { diff --git a/src/main/java/edu/umd/dawn/common/interceptor/RequestInterceptor.java b/src/main/java/edu/umd/dawn/common/interceptor/RequestInterceptor.java index b1f61af59fa12eb732b259fc5ed84e1233b0a547..8b403669aadef42b6e4c28c20fe71b4242f93caa 100644 --- a/src/main/java/edu/umd/dawn/common/interceptor/RequestInterceptor.java +++ b/src/main/java/edu/umd/dawn/common/interceptor/RequestInterceptor.java @@ -1,6 +1,7 @@ package edu.umd.dawn.common.interceptor; import edu.umd.dawn.common.logging.RequestLog; +import edu.umd.dawn.common.services.UserAgentService; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.util.Enumeration; @@ -10,15 +11,24 @@ import java.util.UUID; import lombok.extern.log4j.Log4j2; import org.apache.logging.log4j.Level; import org.slf4j.MDC; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerMapping; @Log4j2 +@Component public class RequestInterceptor implements HandlerInterceptor { private String TRACE_HEADER = "X-Request-Id"; + + @Value("${config.serviceName:dawn-service}") private String serviceName; + @Autowired + private UserAgentService userAgentService; + public RequestInterceptor(String serviceName) { MDC.put("requestId", UUID.randomUUID().toString()); this.serviceName = serviceName; @@ -90,13 +100,16 @@ public class RequestInterceptor implements HandlerInterceptor { String path = fullPath.replace(servletPath, servletPattern); + Map<String, String> headers = getRequestHeaders(request); + RequestLog requestLog = RequestLog.builder() .path(path) .method(request.getMethod()) .statusCode(response.getStatus()) - .headers(getRequestHeaders(request)) + .headers(headers) .parameters(getParemeters(request)) .duration(executeTime) + .userAgent(userAgentService.parse(headers)) .build(); log.log(Level.forName("REQUEST", 10), requestLog); diff --git a/src/main/java/edu/umd/dawn/common/logging/RequestLog.java b/src/main/java/edu/umd/dawn/common/logging/RequestLog.java index 4136d37a68afbd2f4e84d014f96f2ae2d66390bb..da9c85ca89910bcbc9db1390bde77ea37225e0a1 100644 --- a/src/main/java/edu/umd/dawn/common/logging/RequestLog.java +++ b/src/main/java/edu/umd/dawn/common/logging/RequestLog.java @@ -1,6 +1,9 @@ package edu.umd.dawn.common.logging; +import java.util.List; import java.util.Map; + +import edu.umd.dawn.common.models.UserAgent; import lombok.Builder; import lombok.Data; import lombok.EqualsAndHashCode; @@ -19,10 +22,13 @@ public class RequestLog extends Log { private int statusCode; private String method; private String path; + private List<String> ips; @ToString.Exclude private Map<String, String> headers; @ToString.Exclude private Map<String, String> parameters; + + private UserAgent userAgent; } diff --git a/src/main/java/edu/umd/dawn/common/models/UserAgent.java b/src/main/java/edu/umd/dawn/common/models/UserAgent.java new file mode 100644 index 0000000000000000000000000000000000000000..278b71e52b7a06e2236c77a0b9d0300fe31e664e --- /dev/null +++ b/src/main/java/edu/umd/dawn/common/models/UserAgent.java @@ -0,0 +1,26 @@ +package edu.umd.dawn.common.models; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class UserAgent { + + + @Builder.Default + private boolean bot = false; + @Builder.Default + private boolean tablet = false; + @Builder.Default + private boolean mobile = false; + @Builder.Default + private boolean desktop = false; + + private String device; + private String os; + private String osVersion; + private String name; + private String version; + +} diff --git a/src/main/java/edu/umd/dawn/common/services/UserAgentService.java b/src/main/java/edu/umd/dawn/common/services/UserAgentService.java new file mode 100644 index 0000000000000000000000000000000000000000..f0984087208d8f9d3c4b381568e85d341fecc6ec --- /dev/null +++ b/src/main/java/edu/umd/dawn/common/services/UserAgentService.java @@ -0,0 +1,44 @@ +package edu.umd.dawn.common.services; + +import org.springframework.stereotype.Service; + +import edu.umd.dawn.common.models.UserAgent.UserAgentBuilder; +import nl.basjes.parse.useragent.UserAgent; +import nl.basjes.parse.useragent.UserAgentAnalyzer; + +import static nl.basjes.parse.useragent.classify.DeviceClass.TABLET; +import static nl.basjes.parse.useragent.classify.DeviceClass.PHONE;; + +@Service +public class UserAgentService { + private UserAgentAnalyzer uaa = UserAgentAnalyzer + .newBuilder() + .hideMatcherLoadStats() + .withCache(10000) + .build(); + + private UserAgentBuilder setDeviceType(UserAgent ua, UserAgentBuilder builder) { + String deviceType = ua.get(UserAgent.DEVICE_CLASS).getValue().toLowerCase(); + if (deviceType.equals(TABLET.getValue().toLowerCase())) { + builder.tablet(true); + } else if (deviceType.equals(PHONE.getValue().toLowerCase())) { + builder.mobile(true); + } else if (deviceType.startsWith("robot")) { + builder.bot(true); + } else { + builder.desktop(true); + } + return builder; + } + + public edu.umd.dawn.common.models.UserAgent parse(String userAgenString) { + UserAgent ua = uaa.parse(userAgenString); + + UserAgentBuilder builder = edu.umd.dawn.common.models.UserAgent.builder(); + builder = setDeviceType(ua, builder); + builder.device(ua.get(UserAgent.DEVICE_NAME).getValue()); + + + return builder.build(); + } +}