Skip to content

Commit 8ef9963

Browse files
authored
Merge pull request #2 from devhamzat/dev
added password validation
2 parents 7bf71a7 + 55fb091 commit 8ef9963

15 files changed

Lines changed: 687 additions & 135 deletions

pom.xml

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
<modelVersion>4.0.0</modelVersion>
55

66

7-
<groupId>org.devhamzat</groupId>
7+
<groupId>org.constraynt</groupId>
88
<artifactId>constraynt</artifactId>
9-
<version>0.0.1-SNAPSHOT</version>
9+
<version>0.1.0-SNAPSHOT</version>
1010
<packaging>jar</packaging>
1111

1212
<name>Constraynt</name>
@@ -24,8 +24,8 @@
2424

2525
<developers>
2626
<developer>
27-
<id>yourFriendlyNeighbourhoodDev</id>
28-
<name>Dev</name>
27+
<id>yourFriendlyNeighbourhoodBatman</id>
28+
<name>BATMAN</name>
2929
<roles>
3030
<role>developer</role>
3131
<role>package manager</role>
@@ -73,17 +73,17 @@
7373
<artifactId>passay</artifactId>
7474
<version>1.6.4</version>
7575
</dependency>
76+
<dependency>
77+
<groupId>org.springframework</groupId>
78+
<artifactId>spring-web</artifactId>
79+
<version>6.1.11</version>
80+
</dependency>
7681
<dependency>
7782
<groupId>org.projectlombok</groupId>
7883
<artifactId>lombok</artifactId>
7984
<version>1.18.30</version>
8085
<optional>true</optional>
8186
</dependency>
82-
<dependency>
83-
<groupId>org.springframework.boot</groupId>
84-
<artifactId>spring-boot-starter-web</artifactId>
85-
<version>3.0.5</version>
86-
</dependency>
8787
<dependency>
8888
<groupId>org.springframework.boot</groupId>
8989
<artifactId>spring-boot-starter-test</artifactId>
Lines changed: 135 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,165 @@
1-
package org.devhamzat.email;
1+
package org.constraynt.email;
22

33
import jakarta.validation.ConstraintValidator;
44
import jakarta.validation.ConstraintValidatorContext;
55
import org.hibernate.validator.internal.util.DomainNameUtil;
66

7+
import java.net.IDN;
78
import java.util.regex.Matcher;
89
import java.util.regex.Pattern;
910

1011
import static java.util.regex.Pattern.CASE_INSENSITIVE;
1112

1213
public class Email implements ConstraintValidator<ValidateEmail, CharSequence> {
1314
private static final int MAX_LOCAL_PART_LENGTH = 64;
14-
private static final String LOCAL_PART_ATOM = "[a-z0-9!#$%&'*+/=?^_`{|}~\u0080-\uFFFF-]";
15-
private static final String LOCAL_PART_INSIDE_QUOTES_ATOM = "[a-z0-9!#$%&'*.(),<>\\[\\]:; @+/=?^_`{|}~\u0080-\uFFFF-]|\\\\\\\\|\\\\\"";
15+
private static final int MAX_DOMAIN_TOTAL_LENGTH = 255;
16+
private static final int MAX_LABEL_LENGTH = 63;
1617

17-
private static final Pattern LOCAL_PART_PATTERN = Pattern.compile(
18-
"(?:" + LOCAL_PART_ATOM + "+|\"" + LOCAL_PART_INSIDE_QUOTES_ATOM + "+\")" +
19-
"(?:\\." + "(?:" + LOCAL_PART_ATOM + "+|\"" + LOCAL_PART_INSIDE_QUOTES_ATOM + "+\")" + ")*", CASE_INSENSITIVE
18+
// Fast path ASCII email regex (common case)
19+
private static final Pattern FAST_PATH_ASCII = Pattern.compile(
20+
"^[A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]+@" +
21+
"(?:(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)\\.)+" +
22+
"[A-Za-z]{2,63}$"
2023
);
2124

25+
private static final String LOCAL_PART_ATOM_ALL = "[a-z0-9!#$%&'*+/=?^_`{|}~\\u0080-\\uFFFF-]";
26+
private static final String LOCAL_PART_ATOM_NO_PLUS = "[a-z0-9!#$%&'*//=?^_`{|}~\\u0080-\\uFFFF-]"; // no '+'
27+
private static final String LOCAL_PART_INSIDE_QUOTES_ATOM = "(?:[a-z0-9!#$%&'*.(),<>\\[\\]:; @+/=?^_`{|}~\\u0080-\\uFFFF-]|\\\\\\\\|\\\\\\\")";
28+
29+
private Pattern localPartPattern;
30+
private boolean allowTld;
31+
private boolean allowIpDomain;
32+
private boolean allowPlusSign;
33+
2234
@Override
2335
public void initialize(ValidateEmail constraintAnnotation) {
24-
ConstraintValidator.super.initialize(constraintAnnotation);
36+
this.allowTld = constraintAnnotation.allowTld();
37+
this.allowIpDomain = constraintAnnotation.allowIpDomain();
38+
this.allowPlusSign = constraintAnnotation.allowPlusSign();
39+
String atom = allowPlusSign ? LOCAL_PART_ATOM_ALL : LOCAL_PART_ATOM_NO_PLUS;
40+
this.localPartPattern = Pattern.compile(
41+
"(?:" + atom + "+|\"" + LOCAL_PART_INSIDE_QUOTES_ATOM + "+\")" +
42+
"(?:\\." + "(?:" + atom + "+|\"" + LOCAL_PART_INSIDE_QUOTES_ATOM + "+\")" + ")*",
43+
CASE_INSENSITIVE
44+
);
2545
}
2646

2747
@Override
28-
public boolean isValid(CharSequence email, ConstraintValidatorContext context) {
29-
if (email == null || email.isEmpty()) {
30-
return false;
48+
public boolean isValid(CharSequence value, ConstraintValidatorContext context) {
49+
if (value == null || value.length() == 0) {
50+
return true; // null/empty considered valid; use @NotBlank to require
3151
}
32-
String emailString = email.toString();
33-
int split = emailString.lastIndexOf("@");
34-
if (split <= 0) {
35-
return false;
52+
final String email = value.toString();
53+
54+
// Fast path for common ASCII emails (only when TLDs are allowed)
55+
if (allowTld && FAST_PATH_ASCII.matcher(email).matches()) {
56+
return true;
3657
}
3758

38-
String localPart = emailString.substring(0, split);
39-
String domainPart = emailString.substring(split + 1);
40-
if (!isValidEmailLocalPart(localPart)) {
41-
return false;
59+
int at = email.lastIndexOf('@');
60+
if (at <= 0 || at == email.length() - 1) {
61+
return violation(context, "{constraynt.email.missingAt}");
62+
}
63+
64+
String local = email.substring(0, at);
65+
String domain = email.substring(at + 1);
66+
67+
if (!isValidLocal(local)) {
68+
return violation(context, "{constraynt.email.local.invalid}");
69+
}
70+
71+
if (!isValidDomain(domain, context)) {
72+
return false; // violation already added
4273
}
43-
return DomainNameUtil.isValidEmailDomainAddress( domainPart );
74+
return true;
4475
}
4576

46-
private boolean isValidEmailLocalPart(String localPart) {
47-
if (localPart.length() > MAX_LOCAL_PART_LENGTH) {
77+
private boolean isValidLocal(String local) {
78+
if (local.length() > MAX_LOCAL_PART_LENGTH) {
4879
return false;
4980
}
50-
Matcher matcher = LOCAL_PART_PATTERN.matcher(localPart);
51-
return matcher.matches();
81+
// Disallow leading/trailing dot and consecutive dots when not quoted
82+
if (!(local.startsWith("\"") && local.endsWith("\""))) {
83+
if (local.startsWith(".") || local.endsWith(".")) return false;
84+
if (local.contains("..")) return false;
85+
}
86+
Matcher m = localPartPattern.matcher(local);
87+
return m.matches();
88+
}
89+
90+
private boolean isValidDomain(String domain, ConstraintValidatorContext context) {
91+
// IP literal: [x.x.x.x] or [IPv6:...]
92+
if (domain.startsWith("[") && domain.endsWith("]")) {
93+
if (!allowIpDomain) {
94+
return violation(context, "{constraynt.email.domain.ipNotAllowed}");
95+
}
96+
String inner = domain.substring(1, domain.length() - 1);
97+
if (inner.regionMatches(true, 0, "IPv6:", 0, 5)) {
98+
String ipv6 = inner.substring(5);
99+
if (!IPV6_PATTERN.matcher(ipv6).matches()) {
100+
return violation(context, "{constraynt.email.domain.invalid}");
101+
}
102+
return true;
103+
} else {
104+
if (!IPV4_PATTERN.matcher(inner).matches()) {
105+
return violation(context, "{constraynt.email.domain.invalid}");
106+
}
107+
return true;
108+
}
109+
}
110+
111+
String[] labels = domain.split("\\.");
112+
if (labels.length == 0) {
113+
return violation(context, "{constraynt.email.domain.invalid}");
114+
}
115+
if (!allowTld && labels.length < 2) {
116+
return violation(context, "{constraynt.email.domain.tldNotAllowed}");
117+
}
118+
119+
StringBuilder asciiBuilder = new StringBuilder();
120+
try {
121+
for (int i = 0; i < labels.length; i++) {
122+
String label = labels[i];
123+
if (label.isEmpty()) {
124+
return violation(context, "{constraynt.email.domain.invalid}");
125+
}
126+
String ascii = IDN.toASCII(label, IDN.USE_STD3_ASCII_RULES);
127+
if (ascii.isEmpty() || ascii.length() > MAX_LABEL_LENGTH) {
128+
return violation(context, "{constraynt.email.domain.labelTooLong}");
129+
}
130+
if (!DOMAIN_LABEL_ASCII.matcher(ascii).matches()) {
131+
return violation(context, "{constraynt.email.domain.invalid}");
132+
}
133+
if (i > 0) asciiBuilder.append('.');
134+
asciiBuilder.append(ascii);
135+
}
136+
} catch (IllegalArgumentException ex) {
137+
return violation(context, "{constraynt.email.domain.invalid}");
138+
}
139+
140+
String asciiDomain = asciiBuilder.toString();
141+
if (asciiDomain.length() > MAX_DOMAIN_TOTAL_LENGTH) {
142+
return violation(context, "{constraynt.email.domain.tooLong}");
143+
}
144+
if (!DomainNameUtil.isValidEmailDomainAddress(asciiDomain)) {
145+
return violation(context, "{constraynt.email.domain.invalid}");
146+
}
147+
return true;
148+
}
149+
150+
private boolean violation(ConstraintValidatorContext context, String messageTemplate) {
151+
context.disableDefaultConstraintViolation();
152+
context.buildConstraintViolationWithTemplate(messageTemplate).addConstraintViolation();
153+
return false;
52154
}
155+
156+
// Conservative IPv4 and IPv6 patterns
157+
private static final Pattern IPV4_PATTERN = Pattern.compile(
158+
"^(25[0-5]|2[0-4]\\d|1?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|1?\\d?\\d)){3}$");
159+
160+
private static final Pattern IPV6_PATTERN = Pattern.compile(
161+
"^([0-9A-Fa-f]{1,4})(:([0-9A-Fa-f]{1,4})){7}$|^(([0-9A-Fa-f]{1,4}:){1,7}:)$|^(:(:[0-9A-Fa-f]{1,4}){1,7})$|^((([0-9A-Fa-f]{1,4}:){1,6}|:):([0-9A-Fa-f]{1,4})(:([0-9A-Fa-f]{1,4})){0,5})$");
162+
163+
private static final Pattern DOMAIN_LABEL_ASCII = Pattern.compile(
164+
"^(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)$");
53165
}

src/main/java/org/devhamzat/email/ValidateEmail.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import jakarta.validation.Constraint;
44
import jakarta.validation.Payload;
55

6+
import java.lang.annotation.Documented;
67
import java.lang.annotation.Retention;
78
import java.lang.annotation.Target;
89

@@ -12,11 +13,18 @@
1213
@Target({FIELD})
1314
@Retention(value = RUNTIME)
1415
@Constraint(validatedBy = Email.class)
16+
@Documented
1517
public @interface ValidateEmail {
16-
String message() default "Invalid email format";
18+
String message() default "{org.constraynt.email.ValidateEmail}";
1719

1820
Class<?>[] groups() default {};
1921

2022
Class<? extends Payload>[] payload() default {};
2123

24+
boolean allowTld() default true;
25+
26+
boolean allowIpDomain() default false;
27+
28+
boolean allowPlusSign() default true;
29+
2230
}
Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,15 @@
11
package org.devhamzat.password;
22

3-
import java.util.List;
4-
53
/**
64
* Interface for password validation in the Constraynt library.
75
* Implementations of this interface provide custom password validation logic.
86
*/
97
public interface ConstrayntPasswordValidator {
108
/**
11-
* Validates the given password against the implemented rules.
12-
*
13-
* @param password The password to validate.
14-
* @return true if the password is valid, false otherwise.
15-
*/
16-
boolean validate(String password);
17-
/**
18-
* Retrieves the error messages generated during the last validation.
19-
*
20-
* @return A list of error messages, or an empty list if no errors occurred.
9+
* Validates the given password against the provided policy.
10+
* @param rawPassword the password text to validate.
11+
* @param policy the policy describing rule configuration.
12+
* @return validation result including message codes when invalid.
2113
*/
22-
List<String> getErrorMessages();
23-
14+
PasswordValidationResult validate(String rawPassword, PasswordPolicy policy);
2415
}

0 commit comments

Comments
 (0)