implemented ssl stripping checks and hsts checks
This commit is contained in:
@@ -12,4 +12,9 @@ public class CommonConstants {
|
|||||||
public static final String QR_CODE_TYPE_SMS = "SMS";
|
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_URL = "URL";
|
||||||
public static final String QR_CODE_TYPE_WIFI = "WIFI";
|
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";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ package com.safeqr.app.qrcode.entity;
|
|||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
import org.hibernate.annotations.UuidGenerator;
|
import org.hibernate.annotations.UuidGenerator;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -13,10 +15,12 @@ import java.util.UUID;
|
|||||||
@Table(name = "email", schema = "safeqr")
|
@Table(name = "email", schema = "safeqr")
|
||||||
@Data
|
@Data
|
||||||
@Builder
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
public class EmailEntity {
|
public class EmailEntity {
|
||||||
@Id
|
@Id
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
@GeneratedValue(generator = "UUID")
|
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||||
@UuidGenerator
|
@UuidGenerator
|
||||||
@Column(updatable = false, nullable = false)
|
@Column(updatable = false, nullable = false)
|
||||||
private UUID id;
|
private UUID id;
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ package com.safeqr.app.qrcode.entity;
|
|||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
import org.hibernate.annotations.UuidGenerator;
|
import org.hibernate.annotations.UuidGenerator;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -13,10 +15,12 @@ import java.util.UUID;
|
|||||||
@Table(name = "phone", schema = "safeqr")
|
@Table(name = "phone", schema = "safeqr")
|
||||||
@Data
|
@Data
|
||||||
@Builder
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
public class PhoneEntity {
|
public class PhoneEntity {
|
||||||
@Id
|
@Id
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
@GeneratedValue(generator = "UUID")
|
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||||
@UuidGenerator
|
@UuidGenerator
|
||||||
@Column(updatable = false, nullable = false)
|
@Column(updatable = false, nullable = false)
|
||||||
private UUID id;
|
private UUID id;
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ package com.safeqr.app.qrcode.entity;
|
|||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
import org.hibernate.annotations.UuidGenerator;
|
import org.hibernate.annotations.UuidGenerator;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -13,10 +15,12 @@ import java.util.UUID;
|
|||||||
@Table(name = "sms", schema = "safeqr")
|
@Table(name = "sms", schema = "safeqr")
|
||||||
@Data
|
@Data
|
||||||
@Builder
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
public class SMSEntity {
|
public class SMSEntity {
|
||||||
@Id
|
@Id
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
@GeneratedValue(generator = "UUID")
|
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||||
@UuidGenerator
|
@UuidGenerator
|
||||||
@Column(updatable = false, nullable = false)
|
@Column(updatable = false, nullable = false)
|
||||||
private UUID id;
|
private UUID id;
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
package com.safeqr.app.qrcode.entity;
|
package com.safeqr.app.qrcode.entity;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Builder
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
@Table(name = "scan_history", schema = "safeqr")
|
@Table(name = "scan_history", schema = "safeqr")
|
||||||
public class ScanHistoryEntity {
|
public class ScanHistoryEntity {
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ package com.safeqr.app.qrcode.entity;
|
|||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
import org.hibernate.annotations.UuidGenerator;
|
import org.hibernate.annotations.UuidGenerator;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -13,10 +15,12 @@ import java.util.UUID;
|
|||||||
@Table(name = "text", schema = "safeqr")
|
@Table(name = "text", schema = "safeqr")
|
||||||
@Data
|
@Data
|
||||||
@Builder
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
public class TextEntity {
|
public class TextEntity {
|
||||||
@Id
|
@Id
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
@GeneratedValue(generator = "UUID")
|
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||||
@UuidGenerator
|
@UuidGenerator
|
||||||
@Column(updatable = false, nullable = false)
|
@Column(updatable = false, nullable = false)
|
||||||
private UUID id;
|
private UUID id;
|
||||||
|
|||||||
@@ -45,6 +45,12 @@ public class URLEntity {
|
|||||||
|
|
||||||
private int redirect = 0;
|
private int redirect = 0;
|
||||||
|
|
||||||
|
@Column(name = "hsts_header")
|
||||||
|
private List<String> hstsHeader;
|
||||||
|
|
||||||
|
@Column(name = "ssl_stripping")
|
||||||
|
private List<Boolean> sslStripping;
|
||||||
|
|
||||||
@Column(name = "redirect_chain")
|
@Column(name = "redirect_chain")
|
||||||
private List<String> redirectChain;
|
private List<String> redirectChain;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ package com.safeqr.app.qrcode.entity;
|
|||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
import org.hibernate.annotations.UuidGenerator;
|
import org.hibernate.annotations.UuidGenerator;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -13,10 +15,12 @@ import java.util.UUID;
|
|||||||
@Table(name = "wifi", schema = "safeqr")
|
@Table(name = "wifi", schema = "safeqr")
|
||||||
@Data
|
@Data
|
||||||
@Builder
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
public class WifiEntity {
|
public class WifiEntity {
|
||||||
@Id
|
@Id
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
@GeneratedValue(generator = "UUID")
|
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||||
@UuidGenerator
|
@UuidGenerator
|
||||||
@Column(updatable = false, nullable = false)
|
@Column(updatable = false, nullable = false)
|
||||||
private UUID id;
|
private UUID id;
|
||||||
|
|||||||
@@ -35,11 +35,9 @@ public class URLModel extends QRCodeModel {
|
|||||||
String url = scannedQRCode.getContents();
|
String url = scannedQRCode.getContents();
|
||||||
try {
|
try {
|
||||||
details = urlVerificationService.breakdownURL(url);
|
details = urlVerificationService.breakdownURL(url);
|
||||||
List<String> redirectChain = urlVerificationService.countAndTrackRedirects(url);
|
urlVerificationService.countAndTrackRedirects(url, details);
|
||||||
// set qrCode Identifier
|
// set qrCode Identifier
|
||||||
details.setQrCodeId(scannedQRCode.getId());
|
details.setQrCodeId(scannedQRCode.getId());
|
||||||
details.setRedirect(redirectChain.size() - 1);
|
|
||||||
details.setRedirectChain(redirectChain);
|
|
||||||
|
|
||||||
// Insert into URL table
|
// Insert into URL table
|
||||||
urlVerificationService.insertDB(details);
|
urlVerificationService.insertDB(details);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
package com.safeqr.app.qrcode.service;
|
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.QRCodePayload;
|
||||||
import com.safeqr.app.qrcode.dto.response.BaseScanResponse;
|
import com.safeqr.app.qrcode.dto.response.BaseScanResponse;
|
||||||
import com.safeqr.app.qrcode.entity.QRCodeEntity;
|
import com.safeqr.app.qrcode.entity.QRCodeEntity;
|
||||||
@@ -22,6 +22,8 @@ import reactor.core.publisher.Mono;
|
|||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class QRCodeTypeService {
|
public class QRCodeTypeService {
|
||||||
@@ -49,6 +51,7 @@ public class QRCodeTypeService {
|
|||||||
|
|
||||||
private List<QRCodeTypeEntity> configs;
|
private List<QRCodeTypeEntity> configs;
|
||||||
private QRCodeTypeEntity defaultQRCodeTypeEntity;
|
private QRCodeTypeEntity defaultQRCodeTypeEntity;
|
||||||
|
private Map<Long, String> tableMap;
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void loadQRCodeTypes() {
|
public void loadQRCodeTypes() {
|
||||||
@@ -56,9 +59,12 @@ public class QRCodeTypeService {
|
|||||||
configs = qrCodeTypeRepository.findAll();
|
configs = qrCodeTypeRepository.findAll();
|
||||||
// Set the default QR Code Type
|
// Set the default QR Code Type
|
||||||
defaultQRCodeTypeEntity = configs.stream()
|
defaultQRCodeTypeEntity = configs.stream()
|
||||||
.filter(config -> config.getType().equals(CommonConstants.DEFAULT_QR_CODE_TYPE))
|
.filter(config -> config.getType().equals(DEFAULT_QR_CODE_TYPE))
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.orElse(null);
|
.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<QRCodeTypeEntity> getAllTypes() {
|
public List<QRCodeTypeEntity> getAllTypes() {
|
||||||
@@ -95,12 +101,17 @@ public class QRCodeTypeService {
|
|||||||
|
|
||||||
return BaseScanResponse.builder().qrcode(qrCodeModel).build();
|
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) {
|
private QRCodeTypeEntity getQRCodeType(String data) {
|
||||||
return configs.stream()
|
return configs.stream()
|
||||||
.filter(config -> data.toLowerCase().startsWith(config.getPrefix().toLowerCase()))
|
.filter(config -> data.toLowerCase().startsWith(config.getPrefix().toLowerCase()))
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.orElse(defaultQRCodeTypeEntity);
|
.orElse(defaultQRCodeTypeEntity);
|
||||||
}
|
}
|
||||||
|
// Returns name of table given type
|
||||||
|
public String getTableMap(Long qrTypeId) {
|
||||||
|
return tableMap.get(qrTypeId);
|
||||||
|
}
|
||||||
|
|
||||||
public Mono<String> detectType(QRCodePayload payload) {
|
public Mono<String> detectType(QRCodePayload payload) {
|
||||||
String data = payload.getData();
|
String data = payload.getData();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.safeqr.app.qrcode.service;
|
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.QRCodePayload;
|
||||||
import com.safeqr.app.qrcode.dto.URLVerificationResponse;
|
import com.safeqr.app.qrcode.dto.URLVerificationResponse;
|
||||||
import com.safeqr.app.qrcode.entity.URLEntity;
|
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.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import javax.net.ssl.HttpsURLConnection;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.*;
|
import java.net.*;
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class URLVerificationService {
|
public class URLVerificationService {
|
||||||
@@ -70,15 +69,17 @@ public class URLVerificationService {
|
|||||||
// set query params to URL query
|
// set query params to URL query
|
||||||
urlObj.setQuery(queryParams.toString());
|
urlObj.setQuery(queryParams.toString());
|
||||||
// set fragment to URL ref
|
// set fragment to URL ref
|
||||||
urlObj.setFragment(url.getRef());
|
urlObj.setFragment(Optional.ofNullable(url.getRef()).orElse(""));
|
||||||
|
|
||||||
return urlObj;
|
return urlObj;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<String> countAndTrackRedirects(String urlString) throws IOException, URISyntaxException {
|
public void countAndTrackRedirects(String urlString, URLEntity details) throws IOException, URISyntaxException {
|
||||||
URI uri = new URI(urlString);
|
URI uri = new URI(urlString);
|
||||||
URL url = uri.toURL();
|
URL url = uri.toURL();
|
||||||
List<String> redirectChain = new ArrayList<>();
|
List<String> redirectChain = new ArrayList<>();
|
||||||
|
List<String> hstsHeaderList = new ArrayList<>();
|
||||||
|
List<Boolean> sslStrippingList = new ArrayList<>();
|
||||||
|
|
||||||
// Add the initial URL to the chain
|
// Add the initial URL to the chain
|
||||||
redirectChain.add(urlString);
|
redirectChain.add(urlString);
|
||||||
@@ -86,12 +87,25 @@ public class URLVerificationService {
|
|||||||
int redirectCount = 0;
|
int redirectCount = 0;
|
||||||
|
|
||||||
do {
|
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();
|
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||||
connection.setRequestMethod("GET");
|
connection.setRequestMethod("GET");
|
||||||
connection.setInstanceFollowRedirects(false);
|
connection.setInstanceFollowRedirects(false);
|
||||||
|
|
||||||
int responseCode = connection.getResponseCode();
|
int responseCode = connection.getResponseCode();
|
||||||
redirected = (responseCode >= 300 && responseCode < 400);
|
redirected = (responseCode >= 300 && responseCode < 400);
|
||||||
|
|
||||||
|
// Checks for HSTS Header
|
||||||
|
hstsHeaderList.add(detectHSTSHeader(url, connection));
|
||||||
|
|
||||||
// Handle redirects
|
// Handle redirects
|
||||||
if (redirected) {
|
if (redirected) {
|
||||||
// Location header contains the URL to redirect to
|
// Location header contains the URL to redirect to
|
||||||
@@ -99,19 +113,49 @@ public class URLVerificationService {
|
|||||||
if (newUrl == null) {
|
if (newUrl == null) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
URI newUri = uri.resolve(newUrl);
|
||||||
|
// check for SSL stripping during redirect
|
||||||
|
sslStrippingList.add(checkRedirectForSSLStripping(uri, newUri));
|
||||||
|
|
||||||
// Handle relative URLs
|
// Handle relative URLs
|
||||||
uri = uri.resolve(newUrl);
|
uri = uri.resolve(newUrl);
|
||||||
url = uri.toURL();
|
url = uri.toURL();
|
||||||
redirectChain.add(url.toString());
|
redirectChain.add(url.toString());
|
||||||
redirectCount++;
|
redirectCount++;
|
||||||
logger.info("Redirect #{}: {}",redirectCount, newUrl);
|
logger.info("Redirect #{}: {}",redirectCount, newUrl);
|
||||||
|
} else {
|
||||||
|
// No redirect, so no SSL stripping
|
||||||
|
sslStrippingList.add(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
connection.disconnect();
|
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) {
|
public URLVerificationResponse verifyURL(QRCodePayload payload) {
|
||||||
URLVerificationResponse response = new URLVerificationResponse();
|
URLVerificationResponse response = new URLVerificationResponse();
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user