diff --git a/src/main/java/com/safeqr/app/constants/CommonConstants.java b/src/main/java/com/safeqr/app/constants/CommonConstants.java index 523f5ab..c823020 100644 --- a/src/main/java/com/safeqr/app/constants/CommonConstants.java +++ b/src/main/java/com/safeqr/app/constants/CommonConstants.java @@ -12,4 +12,9 @@ public class CommonConstants { public static final String QR_CODE_TYPE_SMS = "SMS"; public static final String QR_CODE_TYPE_URL = "URL"; public static final String QR_CODE_TYPE_WIFI = "WIFI"; + + public static final String INFO_NON_SECURE_CONNECTION = "Not an HTTPS connection"; + public static final String INFO_NO_HSTS_HEADER = "No HSTS Header detected"; + public static final String INFO_HSTS_HEADER_PREFIX = "HSTS Header: "; + public static final String INFO_HSTS_NOT_APPLICABLE = "N/A"; } diff --git a/src/main/java/com/safeqr/app/qrcode/entity/EmailEntity.java b/src/main/java/com/safeqr/app/qrcode/entity/EmailEntity.java index 192a4db..d4f6d83 100644 --- a/src/main/java/com/safeqr/app/qrcode/entity/EmailEntity.java +++ b/src/main/java/com/safeqr/app/qrcode/entity/EmailEntity.java @@ -3,8 +3,10 @@ package com.safeqr.app.qrcode.entity; import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; +import lombok.AllArgsConstructor; import lombok.Data; import lombok.Builder; +import lombok.NoArgsConstructor; import org.hibernate.annotations.UuidGenerator; import java.util.UUID; @@ -13,10 +15,12 @@ import java.util.UUID; @Table(name = "email", schema = "safeqr") @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class EmailEntity { @Id @JsonIgnore - @GeneratedValue(generator = "UUID") + @GeneratedValue(strategy = GenerationType.AUTO) @UuidGenerator @Column(updatable = false, nullable = false) private UUID id; diff --git a/src/main/java/com/safeqr/app/qrcode/entity/PhoneEntity.java b/src/main/java/com/safeqr/app/qrcode/entity/PhoneEntity.java index f265a75..863f24b 100644 --- a/src/main/java/com/safeqr/app/qrcode/entity/PhoneEntity.java +++ b/src/main/java/com/safeqr/app/qrcode/entity/PhoneEntity.java @@ -3,8 +3,10 @@ package com.safeqr.app.qrcode.entity; import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; +import lombok.AllArgsConstructor; import lombok.Data; import lombok.Builder; +import lombok.NoArgsConstructor; import org.hibernate.annotations.UuidGenerator; import java.util.UUID; @@ -13,10 +15,12 @@ import java.util.UUID; @Table(name = "phone", schema = "safeqr") @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class PhoneEntity { @Id @JsonIgnore - @GeneratedValue(generator = "UUID") + @GeneratedValue(strategy = GenerationType.AUTO) @UuidGenerator @Column(updatable = false, nullable = false) private UUID id; diff --git a/src/main/java/com/safeqr/app/qrcode/entity/SMSEntity.java b/src/main/java/com/safeqr/app/qrcode/entity/SMSEntity.java index 07cfb7e..38a9693 100644 --- a/src/main/java/com/safeqr/app/qrcode/entity/SMSEntity.java +++ b/src/main/java/com/safeqr/app/qrcode/entity/SMSEntity.java @@ -3,8 +3,10 @@ package com.safeqr.app.qrcode.entity; import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; +import lombok.AllArgsConstructor; import lombok.Data; import lombok.Builder; +import lombok.NoArgsConstructor; import org.hibernate.annotations.UuidGenerator; import java.util.UUID; @@ -13,10 +15,12 @@ import java.util.UUID; @Table(name = "sms", schema = "safeqr") @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class SMSEntity { @Id @JsonIgnore - @GeneratedValue(generator = "UUID") + @GeneratedValue(strategy = GenerationType.AUTO) @UuidGenerator @Column(updatable = false, nullable = false) private UUID id; diff --git a/src/main/java/com/safeqr/app/qrcode/entity/ScanHistoryEntity.java b/src/main/java/com/safeqr/app/qrcode/entity/ScanHistoryEntity.java index 99be919..3af92c3 100644 --- a/src/main/java/com/safeqr/app/qrcode/entity/ScanHistoryEntity.java +++ b/src/main/java/com/safeqr/app/qrcode/entity/ScanHistoryEntity.java @@ -1,12 +1,16 @@ package com.safeqr.app.qrcode.entity; import jakarta.persistence.*; +import lombok.AllArgsConstructor; import lombok.Builder; +import lombok.NoArgsConstructor; import java.util.UUID; @Entity @Builder +@NoArgsConstructor +@AllArgsConstructor @Table(name = "scan_history", schema = "safeqr") public class ScanHistoryEntity { diff --git a/src/main/java/com/safeqr/app/qrcode/entity/TextEntity.java b/src/main/java/com/safeqr/app/qrcode/entity/TextEntity.java index 7afaeae..e8e3364 100644 --- a/src/main/java/com/safeqr/app/qrcode/entity/TextEntity.java +++ b/src/main/java/com/safeqr/app/qrcode/entity/TextEntity.java @@ -3,8 +3,10 @@ package com.safeqr.app.qrcode.entity; import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; +import lombok.AllArgsConstructor; import lombok.Data; import lombok.Builder; +import lombok.NoArgsConstructor; import org.hibernate.annotations.UuidGenerator; import java.util.UUID; @@ -13,10 +15,12 @@ import java.util.UUID; @Table(name = "text", schema = "safeqr") @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class TextEntity { @Id @JsonIgnore - @GeneratedValue(generator = "UUID") + @GeneratedValue(strategy = GenerationType.AUTO) @UuidGenerator @Column(updatable = false, nullable = false) private UUID id; diff --git a/src/main/java/com/safeqr/app/qrcode/entity/URLEntity.java b/src/main/java/com/safeqr/app/qrcode/entity/URLEntity.java index ce7a38f..d72120a 100644 --- a/src/main/java/com/safeqr/app/qrcode/entity/URLEntity.java +++ b/src/main/java/com/safeqr/app/qrcode/entity/URLEntity.java @@ -45,6 +45,12 @@ public class URLEntity { private int redirect = 0; + @Column(name = "hsts_header") + private List hstsHeader; + + @Column(name = "ssl_stripping") + private List sslStripping; + @Column(name = "redirect_chain") private List redirectChain; } diff --git a/src/main/java/com/safeqr/app/qrcode/entity/WifiEntity.java b/src/main/java/com/safeqr/app/qrcode/entity/WifiEntity.java index 0a028b1..d5b9837 100644 --- a/src/main/java/com/safeqr/app/qrcode/entity/WifiEntity.java +++ b/src/main/java/com/safeqr/app/qrcode/entity/WifiEntity.java @@ -3,8 +3,10 @@ package com.safeqr.app.qrcode.entity; import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; +import lombok.AllArgsConstructor; import lombok.Data; import lombok.Builder; +import lombok.NoArgsConstructor; import org.hibernate.annotations.UuidGenerator; import java.util.UUID; @@ -13,10 +15,12 @@ import java.util.UUID; @Table(name = "wifi", schema = "safeqr") @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class WifiEntity { @Id @JsonIgnore - @GeneratedValue(generator = "UUID") + @GeneratedValue(strategy = GenerationType.AUTO) @UuidGenerator @Column(updatable = false, nullable = false) private UUID id; diff --git a/src/main/java/com/safeqr/app/qrcode/model/URLModel.java b/src/main/java/com/safeqr/app/qrcode/model/URLModel.java index 78636fe..72fb692 100644 --- a/src/main/java/com/safeqr/app/qrcode/model/URLModel.java +++ b/src/main/java/com/safeqr/app/qrcode/model/URLModel.java @@ -35,11 +35,9 @@ public class URLModel extends QRCodeModel { String url = scannedQRCode.getContents(); try { details = urlVerificationService.breakdownURL(url); - List redirectChain = urlVerificationService.countAndTrackRedirects(url); + urlVerificationService.countAndTrackRedirects(url, details); // set qrCode Identifier details.setQrCodeId(scannedQRCode.getId()); - details.setRedirect(redirectChain.size() - 1); - details.setRedirectChain(redirectChain); // Insert into URL table urlVerificationService.insertDB(details); diff --git a/src/main/java/com/safeqr/app/qrcode/service/QRCodeTypeService.java b/src/main/java/com/safeqr/app/qrcode/service/QRCodeTypeService.java index ac33221..0b251af 100644 --- a/src/main/java/com/safeqr/app/qrcode/service/QRCodeTypeService.java +++ b/src/main/java/com/safeqr/app/qrcode/service/QRCodeTypeService.java @@ -1,7 +1,7 @@ package com.safeqr.app.qrcode.service; -import com.safeqr.app.constants.CommonConstants; +import static com.safeqr.app.constants.CommonConstants.*; import com.safeqr.app.qrcode.dto.QRCodePayload; import com.safeqr.app.qrcode.dto.response.BaseScanResponse; import com.safeqr.app.qrcode.entity.QRCodeEntity; @@ -22,6 +22,8 @@ import reactor.core.publisher.Mono; import java.security.NoSuchAlgorithmException; import java.time.LocalDateTime; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; @Service public class QRCodeTypeService { @@ -49,6 +51,7 @@ public class QRCodeTypeService { private List configs; private QRCodeTypeEntity defaultQRCodeTypeEntity; + private Map tableMap; @PostConstruct public void loadQRCodeTypes() { @@ -56,9 +59,12 @@ public class QRCodeTypeService { configs = qrCodeTypeRepository.findAll(); // Set the default QR Code Type defaultQRCodeTypeEntity = configs.stream() - .filter(config -> config.getType().equals(CommonConstants.DEFAULT_QR_CODE_TYPE)) + .filter(config -> config.getType().equals(DEFAULT_QR_CODE_TYPE)) .findFirst() .orElse(null); + // Construct the tableMap with key = qrCodeTypeId, value = tableName + tableMap = configs.stream().collect(Collectors.toMap(QRCodeTypeEntity::getId, QRCodeTypeEntity::getTableName)); + logger.info("Table map: {}", tableMap); } public List getAllTypes() { @@ -95,12 +101,17 @@ public class QRCodeTypeService { return BaseScanResponse.builder().qrcode(qrCodeModel).build(); } + // Returns Default type as text if it does not fit into any of the category private QRCodeTypeEntity getQRCodeType(String data) { return configs.stream() .filter(config -> data.toLowerCase().startsWith(config.getPrefix().toLowerCase())) .findFirst() .orElse(defaultQRCodeTypeEntity); } + // Returns name of table given type + public String getTableMap(Long qrTypeId) { + return tableMap.get(qrTypeId); + } public Mono detectType(QRCodePayload payload) { String data = payload.getData(); diff --git a/src/main/java/com/safeqr/app/qrcode/service/URLVerificationService.java b/src/main/java/com/safeqr/app/qrcode/service/URLVerificationService.java index 14a8395..25fcbf0 100644 --- a/src/main/java/com/safeqr/app/qrcode/service/URLVerificationService.java +++ b/src/main/java/com/safeqr/app/qrcode/service/URLVerificationService.java @@ -1,6 +1,6 @@ package com.safeqr.app.qrcode.service; -import com.safeqr.app.constants.CommonConstants; +import static com.safeqr.app.constants.CommonConstants.*; import com.safeqr.app.qrcode.dto.QRCodePayload; import com.safeqr.app.qrcode.dto.URLVerificationResponse; import com.safeqr.app.qrcode.entity.URLEntity; @@ -10,12 +10,11 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import javax.net.ssl.HttpsURLConnection; import java.io.IOException; import java.net.*; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; + @Service public class URLVerificationService { @@ -70,15 +69,17 @@ public class URLVerificationService { // set query params to URL query urlObj.setQuery(queryParams.toString()); // set fragment to URL ref - urlObj.setFragment(url.getRef()); + urlObj.setFragment(Optional.ofNullable(url.getRef()).orElse("")); return urlObj; } - public List countAndTrackRedirects(String urlString) throws IOException, URISyntaxException { + public void countAndTrackRedirects(String urlString, URLEntity details) throws IOException, URISyntaxException { URI uri = new URI(urlString); URL url = uri.toURL(); List redirectChain = new ArrayList<>(); + List hstsHeaderList = new ArrayList<>(); + List sslStrippingList = new ArrayList<>(); // Add the initial URL to the chain redirectChain.add(urlString); @@ -86,12 +87,25 @@ public class URLVerificationService { int redirectCount = 0; do { + URLConnection testConnection = url.openConnection(); + + if (!(testConnection instanceof HttpURLConnection)) { + // Handle non-HTTP connections (like mailto:) + logger.info("Non-HTTP URL encountered: {}", url); + hstsHeaderList.add(INFO_HSTS_NOT_APPLICABLE); + sslStrippingList.add(false); + break; + } HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("GET"); connection.setInstanceFollowRedirects(false); int responseCode = connection.getResponseCode(); redirected = (responseCode >= 300 && responseCode < 400); + + // Checks for HSTS Header + hstsHeaderList.add(detectHSTSHeader(url, connection)); + // Handle redirects if (redirected) { // Location header contains the URL to redirect to @@ -99,19 +113,49 @@ public class URLVerificationService { if (newUrl == null) { break; } + URI newUri = uri.resolve(newUrl); + // check for SSL stripping during redirect + sslStrippingList.add(checkRedirectForSSLStripping(uri, newUri)); + // Handle relative URLs uri = uri.resolve(newUrl); url = uri.toURL(); redirectChain.add(url.toString()); redirectCount++; logger.info("Redirect #{}: {}",redirectCount, newUrl); + } else { + // No redirect, so no SSL stripping + sslStrippingList.add(false); } connection.disconnect(); - } while (redirected && redirectCount < CommonConstants.MAX_REDIRECT_COUNT); + } while (redirected && redirectCount < MAX_REDIRECT_COUNT); - return redirectChain; + details.setRedirect(redirectChain.size() - 1); + details.setRedirectChain(redirectChain); + details.setSslStripping(sslStrippingList); + details.setHstsHeader(hstsHeaderList); } + // Function to check if the redirect is from HTTPS to HTTP + private boolean checkRedirectForSSLStripping(URI originalUri, URI newUri) { + return originalUri.getScheme().equalsIgnoreCase("https") && + newUri.getScheme().equalsIgnoreCase("http"); + } + // Function to check if HSTS header is present for HTTPS connections + private String detectHSTSHeader(URL url, HttpURLConnection connection) { + if (connection instanceof HttpsURLConnection) { + String hstsHeader = connection.getHeaderField("Strict-Transport-Security"); + if (hstsHeader != null && !hstsHeader.isEmpty()) { + logger.info("HSTS Header detected for {}: {}", url, hstsHeader); + return INFO_HSTS_HEADER_PREFIX + hstsHeader; + } else { + logger.warn("No HSTS Header for HTTPS connection to {}", url); + return INFO_NO_HSTS_HEADER; + } + } + return INFO_NON_SECURE_CONNECTION; + } + public URLVerificationResponse verifyURL(QRCodePayload payload) { URLVerificationResponse response = new URLVerificationResponse(); try {