From a02eaab0b1b301919b244a011556dfb706462971 Mon Sep 17 00:00:00 2001 From: ltiongku Date: Tue, 16 Jul 2024 22:45:49 +0800 Subject: [PATCH] includes QRCodeURL entity and redirection logic --- .../safeqr/app/constants/CommonConstants.java | 1 + .../controller/QRCodeTypeController.java | 6 +- ...canResponse.java => BaseScanResponse.java} | 10 +- .../app/qrcode/dto/response/URLResponse.java | 14 +++ .../entity/{URL.java => QRCodeURL.java} | 12 +- .../app/qrcode/repository/URLRepository.java | 4 +- .../app/qrcode/service/QRCodeTypeService.java | 116 +++++++++++++----- src/main/resources/application.properties | 2 +- 8 files changed, 124 insertions(+), 41 deletions(-) rename src/main/java/com/safeqr/app/qrcode/dto/response/{ScanResponse.java => BaseScanResponse.java} (54%) create mode 100644 src/main/java/com/safeqr/app/qrcode/dto/response/URLResponse.java rename src/main/java/com/safeqr/app/qrcode/entity/{URL.java => QRCodeURL.java} (76%) diff --git a/src/main/java/com/safeqr/app/constants/CommonConstants.java b/src/main/java/com/safeqr/app/constants/CommonConstants.java index 6c16e19..0b6b68b 100644 --- a/src/main/java/com/safeqr/app/constants/CommonConstants.java +++ b/src/main/java/com/safeqr/app/constants/CommonConstants.java @@ -6,4 +6,5 @@ public class CommonConstants { } public static final String HEADER_USER_ID = "X-USER-ID"; public static final String DEFAULT_QR_CODE_TYPE = "TEXT"; + public static final int MAX_REDIRECT_COUNT = 20; } diff --git a/src/main/java/com/safeqr/app/qrcode/controller/QRCodeTypeController.java b/src/main/java/com/safeqr/app/qrcode/controller/QRCodeTypeController.java index 604e84d..6779c09 100644 --- a/src/main/java/com/safeqr/app/qrcode/controller/QRCodeTypeController.java +++ b/src/main/java/com/safeqr/app/qrcode/controller/QRCodeTypeController.java @@ -5,7 +5,7 @@ import com.safeqr.app.constants.CommonConstants; import com.safeqr.app.qrcode.dto.QRCodePayload; import com.safeqr.app.qrcode.dto.RedirectCountResponse; import com.safeqr.app.qrcode.dto.URLVerificationResponse; -import com.safeqr.app.qrcode.dto.response.ScanResponse; +import com.safeqr.app.qrcode.dto.response.BaseScanResponse; import com.safeqr.app.qrcode.entity.QRCodeType; import com.safeqr.app.qrcode.service.QRCodeTypeService; import com.safeqr.app.qrcode.service.RedirectCountService; @@ -44,8 +44,8 @@ public class QRCodeTypeController { } @PostMapping(value = APIConstants.API_URL_QRCODE_SCAN, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity scanQRCode(@RequestBody QRCodePayload payload, - @RequestHeader(required = false, name = CommonConstants.HEADER_USER_ID) String userId) { + public ResponseEntity scanQRCode(@RequestBody QRCodePayload payload, + @RequestHeader(required = false, name = CommonConstants.HEADER_USER_ID) String userId) { logger.info("Invoking scan endpoint"); return ResponseEntity.ok(qrCodeTypeService.scanQRCode(userId, payload)); } diff --git a/src/main/java/com/safeqr/app/qrcode/dto/response/ScanResponse.java b/src/main/java/com/safeqr/app/qrcode/dto/response/BaseScanResponse.java similarity index 54% rename from src/main/java/com/safeqr/app/qrcode/dto/response/ScanResponse.java rename to src/main/java/com/safeqr/app/qrcode/dto/response/BaseScanResponse.java index d5c7252..16b71ee 100644 --- a/src/main/java/com/safeqr/app/qrcode/dto/response/ScanResponse.java +++ b/src/main/java/com/safeqr/app/qrcode/dto/response/BaseScanResponse.java @@ -2,12 +2,16 @@ package com.safeqr.app.qrcode.dto.response; import com.safeqr.app.qrcode.entity.QRCode; import com.safeqr.app.qrcode.entity.QRCodeType; -import lombok.Builder; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; @Data -@Builder -public class ScanResponse { +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +public class BaseScanResponse { private QRCode scannedQRCode; private QRCodeType qrCodeType; } diff --git a/src/main/java/com/safeqr/app/qrcode/dto/response/URLResponse.java b/src/main/java/com/safeqr/app/qrcode/dto/response/URLResponse.java new file mode 100644 index 0000000..1c38967 --- /dev/null +++ b/src/main/java/com/safeqr/app/qrcode/dto/response/URLResponse.java @@ -0,0 +1,14 @@ +package com.safeqr.app.qrcode.dto.response; + +import com.safeqr.app.qrcode.entity.QRCodeURL; +import lombok.*; +import lombok.experimental.SuperBuilder; + +@EqualsAndHashCode(callSuper = true) +@Data +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +public final class URLResponse extends BaseScanResponse{ + private QRCodeURL qrCodeURL; +} diff --git a/src/main/java/com/safeqr/app/qrcode/entity/URL.java b/src/main/java/com/safeqr/app/qrcode/entity/QRCodeURL.java similarity index 76% rename from src/main/java/com/safeqr/app/qrcode/entity/URL.java rename to src/main/java/com/safeqr/app/qrcode/entity/QRCodeURL.java index 0549d5c..2c16bc0 100644 --- a/src/main/java/com/safeqr/app/qrcode/entity/URL.java +++ b/src/main/java/com/safeqr/app/qrcode/entity/QRCodeURL.java @@ -1,6 +1,7 @@ package com.safeqr.app.qrcode.entity; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; import jakarta.persistence.*; import lombok.Data; import lombok.NoArgsConstructor; @@ -8,6 +9,8 @@ import lombok.AllArgsConstructor; import lombok.Builder; import org.hibernate.annotations.GenericGenerator; +import java.util.List; +import java.util.Map; import java.util.UUID; @Entity @@ -16,7 +19,7 @@ import java.util.UUID; @Builder @NoArgsConstructor @AllArgsConstructor -public class URL { +public class QRCodeURL { @Id @JsonIgnore @@ -25,6 +28,7 @@ public class URL { @Column(updatable = false, nullable = false) private UUID id; + @JsonIgnore @Column(name = "qr_code_id") private UUID qrCodeId; @@ -34,9 +38,15 @@ public class URL { private String topLevelDomain; + private String path; + + @JsonProperty private String query; private String fragment; private int redirect = 0; + + @Column(name = "redirect_chain") + private List redirectChain; } diff --git a/src/main/java/com/safeqr/app/qrcode/repository/URLRepository.java b/src/main/java/com/safeqr/app/qrcode/repository/URLRepository.java index 3ae0681..ca40dbc 100644 --- a/src/main/java/com/safeqr/app/qrcode/repository/URLRepository.java +++ b/src/main/java/com/safeqr/app/qrcode/repository/URLRepository.java @@ -1,8 +1,8 @@ package com.safeqr.app.qrcode.repository; -import com.safeqr.app.qrcode.entity.URL; +import com.safeqr.app.qrcode.entity.QRCodeURL; import org.springframework.data.jpa.repository.JpaRepository; import java.util.UUID; -public interface URLRepository extends JpaRepository { +public interface URLRepository extends JpaRepository { } \ No newline at end of file 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 4441650..22a63b6 100644 --- a/src/main/java/com/safeqr/app/qrcode/service/QRCodeTypeService.java +++ b/src/main/java/com/safeqr/app/qrcode/service/QRCodeTypeService.java @@ -3,13 +3,16 @@ package com.safeqr.app.qrcode.service; import com.safeqr.app.constants.CommonConstants; import com.safeqr.app.qrcode.dto.QRCodePayload; -import com.safeqr.app.qrcode.dto.response.ScanResponse; +import com.safeqr.app.qrcode.dto.response.BaseScanResponse; +import com.safeqr.app.qrcode.dto.response.URLResponse; import com.safeqr.app.qrcode.entity.QRCode; import com.safeqr.app.qrcode.entity.QRCodeType; +import com.safeqr.app.qrcode.entity.QRCodeURL; import com.safeqr.app.qrcode.entity.ScanHistory; import com.safeqr.app.qrcode.repository.QRCodeRepository; import com.safeqr.app.qrcode.repository.QRCodeTypeRepository; import com.safeqr.app.qrcode.repository.ScanHistoryRepository; +import com.safeqr.app.qrcode.repository.URLRepository; import jakarta.annotation.PostConstruct; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -17,9 +20,12 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import reactor.core.publisher.Mono; +import java.io.IOException; +import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.security.NoSuchAlgorithmException; import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -35,6 +41,8 @@ public class QRCodeTypeService { private ScanHistoryRepository scanHistoryRepository; @Autowired private QRCodeRepository qrCodeRepository; + @Autowired + private URLRepository urlRepository; @Autowired private SafeBrowsingService safeBrowsingService; @@ -57,7 +65,7 @@ public class QRCodeTypeService { return configs; } - public ScanResponse scanQRCode(String userId, QRCodePayload payload) { + public BaseScanResponse scanQRCode(String userId, QRCodePayload payload) { String data = payload.getData(); logger.info("scanQRCode: userId={}, data={}", userId, data); @@ -72,9 +80,6 @@ public class QRCodeTypeService { .createdAt(LocalDateTime.now()) .build()); - // Insert qrcode into respective table based on type - insertIntoRespectiveTable(scannedQR); - // Insert into Scan History table if userId is not null logger.info("scanQRCode: scannedQR new ID={}", scannedQR.getId()); if (userId != null) { @@ -84,11 +89,8 @@ public class QRCodeTypeService { .scanStatus(ScanHistory.ScanStatus.ACTIVE) .build()); } - - return ScanResponse.builder() - .scannedQRCode(scannedQR) - .qrCodeType(qrType) - .build(); + // Insert into various tables + return insertIntoRespectiveTable(scannedQR, qrType); } private QRCodeType getQRCodeType(String data) { return configs.stream() @@ -96,56 +98,108 @@ public class QRCodeTypeService { .findFirst() .orElse(defaultQRCodeType); } - private void insertIntoRespectiveTable(QRCode qrCode) { + private BaseScanResponse insertIntoRespectiveTable(QRCode qrCode, QRCodeType qrCodeType) { try { String url = qrCode.getContents(); - Map breakdown = breakdownURL(url); - breakdown.forEach((key, value) -> logger.info("{}: {}", key, value)); - } catch (MalformedURLException e) { - e.printStackTrace(); + QRCodeURL urlObj = breakdownURL(url); + urlObj.setQrCodeId(qrCode.getId()); + List redirectChain = countAndTrackRedirects(url); + logger.info("Redirect chain: {}", redirectChain); + for (int i = 0; i < redirectChain.size(); i++) { + logger.info((i == 0 ? "Initial URL: " : "Redirect #" + i + ": ") + redirectChain.get(i)); + } + urlObj.setRedirect(redirectChain.size() - 1); + urlObj.setRedirectChain(redirectChain); + urlRepository.save(urlObj); + + return URLResponse.builder() + .scannedQRCode(qrCode) + .qrCodeType(qrCodeType) + .qrCodeURL(urlObj) + .build(); + } catch (IOException e) { + logger.error("Error: ", e); } + return BaseScanResponse.builder() + .scannedQRCode(qrCode) + .qrCodeType(qrCodeType) + .build(); } // Function to breakdown URL into subdomain, domain, topLevelDomain, query params, fragment - public Map breakdownURL(String urlString) throws MalformedURLException { + public QRCodeURL breakdownURL(String urlString) throws MalformedURLException { URL url = new URL(urlString); - Map breakdown = new HashMap<>(); + QRCodeURL urlObj = new QRCodeURL(); String host = url.getHost(); String[] hostParts = host.split("\\."); - String subdomain = ""; - String domain = ""; - String topLevelDomain = ""; if (hostParts.length >= 2) { - topLevelDomain = hostParts[hostParts.length - 1]; - domain = hostParts[hostParts.length - 2]; + urlObj.setTopLevelDomain(hostParts[hostParts.length - 1]); + urlObj.setDomain(hostParts[hostParts.length - 2]); if (hostParts.length > 2) { subdomain = String.join(".", java.util.Arrays.copyOfRange(hostParts, 0, hostParts.length - 2)); } } - breakdown.put("Subdomain", subdomain.isEmpty() ? "None" : subdomain); - breakdown.put("Domain", domain); - breakdown.put("Top Level Domain", topLevelDomain); + urlObj.setSubdomain(subdomain); + + String path = url.getPath(); + urlObj.setPath(path.isEmpty() ? "/" : path); String query = url.getQuery(); + Map queryParams = new HashMap<>(); if (query != null) { - Map queryParams = new HashMap<>(); for (String param : query.split("&")) { String[] pair = param.split("="); queryParams.put(pair[0], pair.length > 1 ? pair[1] : ""); } - breakdown.put("Query Parameters", queryParams); - } else { - breakdown.put("Query Parameters", "None"); + logger.info("queryParams: {}", queryParams); } + urlObj.setQuery(queryParams.toString()); String fragment = url.getRef(); - breakdown.put("Fragment", fragment != null ? fragment : "None"); - return breakdown; + urlObj.setFragment(fragment); + + return urlObj; + } + + List countAndTrackRedirects(String urlString) throws IOException { + URL url = new URL(urlString); + List redirectChain = new ArrayList<>(); + redirectChain.add(urlString); // Add the initial URL to the chain + boolean redirected; + int redirectCount = 0; + + do { + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + connection.setInstanceFollowRedirects(false); + + int responseCode = connection.getResponseCode(); + redirected = (responseCode >= 300 && responseCode < 400); + + if (redirected) { + String newUrl = connection.getHeaderField("Location"); + if (newUrl == null) { + break; + } + // Handle relative URLs + if (!newUrl.startsWith("http://") && !newUrl.startsWith("https://")) { + newUrl = new URL(url, newUrl).toString(); + } + url = new URL(newUrl); + redirectChain.add(newUrl); + redirectCount++; + logger.info("Redirect #{}: {}",redirectCount, newUrl); + } + + connection.disconnect(); + } while (redirected && redirectCount < CommonConstants.MAX_REDIRECT_COUNT); + + return redirectChain; } public Mono detectType(QRCodePayload payload) { diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8b4ec08..7e0b90c 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -15,7 +15,7 @@ spring.datasource.username=${SERVER_DB_USERNAME} spring.datasource.password=${SERVER_DB_PASSWORD} spring.datasource.driver-class-name=${SERVER_DB_DRIVER_CLASS_NAME} #spring.jpa.database-platform=${SERVER_DB_DIALECT} -spring.jpa.hibernate.ddl-auto=update +spring.jpa.hibernate.ddl-auto=none spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true