Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ Changes log
- Misc
- Added an optional origin matcher to CORS (`CorsService`, `CorsFilter`, `setAllowedOriginMatcher(Predicate<String>)`) to accept origins that are not listed, for example any sub-domain. The response always contains the exact request origin; it has no effect while the allowed origins contain `*`. Issue #1611.
- CorsService and CorsFilter now log a warning when `*` is allowed as origin together with credentials (any web site could then send credentialed requests), and their Javadoc references the Fetch standard instead of the W3C CORS draft. Issue #1611.
- Added an opt-in UTF-8 handling of the HTTP Basic credentials (RFC 7617), enabled with the `org.restlet.engine.security.basic.utf8` system property (`HttpBasicHelper.UTF8_PROPERTY`): a Basic challenge then advertises `charset="UTF-8"`, a server decodes the credentials as UTF-8 (falling back to ISO-8859-1 if they are not valid UTF-8), and a client answering such a challenge encodes its credentials in UTF-8. Disabled by default (ISO-8859-1). Issue #1455.
- Bugs fixed
- Missing throw in JsonConverter.java. Issue #729.
- Type mismatch on container access in MethodAnnotationInfo. Issue #1278.
- Boolean canAppend in ZipClientHelper can only be false. Issue #1279.
- CorsService and CorsFilter now add `Vary: Origin` to every response when the CORS answer depends on the request origin (origin allow-list or credentials allowed), including requests without an `Origin` header, so shared caches no longer serve the response of one origin to another. A static `*` without credentials still sends no `Vary`, as recommended by the Fetch standard. Issue #1611.
- Fixed the parsing of a `WWW-Authenticate` challenge made of a token68 ending with `=` (such as a `Negotiate` token): the whole header was dropped. Issue #191.
- The `realm` and `charset` parameters of an HTTP Basic challenge are now recognized whatever their case (`Realm=`, `Charset=`), as required by RFC 9110. Issue #1455.

- 2.6.1 (30-09-2026)
- Bugs fixed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ public HeaderWriter<Parameter> append(Parameter value) {
}
};

// A challenge may carry a single token68 instead of parameters
// (RFC 9110, section 11.2), such as a base64 Negotiate token.
String token68 = readToken68();
if (token68 != null) {
w.append(token68);
}

boolean stop = false;
while (peek() != -1 && !stop) {
boolean sepSkipped = skipValueSeparator();
Expand Down Expand Up @@ -83,4 +90,41 @@ public HeaderWriter<Parameter> append(Parameter value) {

return result;
}

/**
* Reads a token68 ending with at least one "=" padding character, if the next
* characters are one. A token68 without padding is read as a regular parameter,
* as before.
*
* @return The token68, or null (nothing consumed) if the next characters are
* not one.
*/
private String readToken68() {
mark();
StringBuilder token = new StringBuilder();
int next = peek();
while (next != -1 && isToken68Char(next)) {
token.append((char) read());
next = peek();
}

int padding = 0;
while (next == '=') {
token.append((char) read());
padding++;
next = peek();
}

if (token.length() > padding && padding > 0 && (next == -1 || next == ',' || isSpace(next))) {
return token.toString();
}

reset();
return null;
}

private static boolean isToken68Char(int c) {
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '.'
|| c == '_' || c == '~' || c == '+' || c == '/';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
*/
public class AuthenticatorUtils {

/** Name of the challenge parameter that advertises the credentials charset. */
private static final String CHARSET_PARAMETER = "charset";

/**
* Indicates if any of the objects is null.
*
Expand All @@ -56,40 +59,42 @@
* @return The {@link HeaderConstants#HEADER_AUTHENTICATION_INFO} header value.
*/
public static String formatAuthenticationInfo(AuthenticationInfo info) {
ChallengeWriter cw = new ChallengeWriter();
boolean firstParameter = true;

if (info != null) {
if (info.getNextServerNonce() != null && !info.getNextServerNonce().isEmpty()) {
cw.setFirstChallengeParameter(firstParameter);
cw.appendQuotedChallengeParameter("nextnonce", info.getNextServerNonce());
firstParameter = false;
}

if (info.getQuality() != null && !info.getQuality().isEmpty()) {
cw.setFirstChallengeParameter(firstParameter);
cw.appendChallengeParameter("qop", info.getQuality());
firstParameter = false;
final ChallengeWriter cw = new ChallengeWriter();

if (info.getNonceCount() > 0) {
cw.appendChallengeParameter("nc", formatNonceCount(info.getNonceCount()));
}
}

if (info.getResponseDigest() != null && !info.getResponseDigest().isEmpty()) {
cw.setFirstChallengeParameter(firstParameter);
cw.appendQuotedChallengeParameter("rspauth", info.getResponseDigest());
firstParameter = false;
}

if (info.getClientNonce() != null && !info.getClientNonce().isEmpty()) {
cw.setFirstChallengeParameter(firstParameter);
cw.appendChallengeParameter("cnonce", info.getClientNonce());
firstParameter = false;
}
if (info == null) {
return cw.toString();
}

return cw.toString();
boolean firstParameter = true;
if (info.getNextServerNonce() != null && !info.getNextServerNonce().isEmpty()) {
cw.setFirstChallengeParameter(firstParameter);
cw.appendQuotedChallengeParameter("nextnonce", info.getNextServerNonce());
firstParameter = false;
}

if (info.getQuality() != null && !info.getQuality().isEmpty()) {
cw.setFirstChallengeParameter(firstParameter);
cw.appendChallengeParameter("qop", info.getQuality());
firstParameter = false;

if (info.getNonceCount() > 0) {
cw.appendChallengeParameter("nc", formatNonceCount(info.getNonceCount()));
}
}

if (info.getResponseDigest() != null && !info.getResponseDigest().isEmpty()) {
cw.setFirstChallengeParameter(firstParameter);
cw.appendQuotedChallengeParameter("rspauth", info.getResponseDigest());
firstParameter = false;
}

if (info.getClientNonce() != null && !info.getClientNonce().isEmpty()) {
cw.setFirstChallengeParameter(firstParameter);
cw.appendChallengeParameter("cnonce", info.getClientNonce());
firstParameter = false;
}

return cw.toString();
}

/**
Expand Down Expand Up @@ -130,33 +135,42 @@
} else if (challenge.getScheme().getTechnicalName() == null) {
Context.getCurrentLogger().warning("A challenge scheme must have a technical name defined.");
} else {
ChallengeWriter cw = new ChallengeWriter();
cw.append(challenge.getScheme().getTechnicalName()).appendSpace();
int cwInitialLength = cw.getBuffer().length();
result = doFormatRequest(challenge, response, httpHeaders);
}

return result;
}

if (challenge.getRawValue() != null) {
cw.append(challenge.getRawValue());
private static String doFormatRequest(ChallengeRequest challenge, Response response, Series<Header> httpHeaders) {
ChallengeWriter cw = new ChallengeWriter();
cw.append(challenge.getScheme().getTechnicalName()).appendSpace();
int cwInitialLength = cw.getBuffer().length();

if (challenge.getRawValue() == null) {
AuthenticatorHelper helper = Engine.getInstance().findHelper(challenge.getScheme(), false, true);

if (helper == null) {
logUnsupportedChallengeScheme(challenge.getScheme());
} else {
AuthenticatorHelper helper = Engine.getInstance().findHelper(challenge.getScheme(), false, true);

if (helper != null) {
try {
helper.formatRequest(cw, challenge, response, httpHeaders);
} catch (Exception e) {
Context.getCurrentLogger().log(Level.WARNING,
"Unable to format the challenge request: " + challenge, e);
}
} else {
result = "?";
Context.getCurrentLogger().warning(
"Challenge scheme " + challenge.getScheme() + " not supported by the Restlet engine.");
try {
helper.formatRequest(cw, challenge, response, httpHeaders);
} catch (Exception e) {
Context.getCurrentLogger().log(Level.WARNING, e,
() -> "Unable to format the challenge request: " + challenge);
}
}

result = (cw.getBuffer().length() > cwInitialLength) ? cw.toString() : null;
} else {
cw.append(challenge.getRawValue());
}

return result;
return (cw.getBuffer().length() > cwInitialLength)
? cw.toString()
: null;
}

private static void logUnsupportedChallengeScheme(ChallengeScheme challengeScheme) {
Context.getCurrentLogger().log(Level.WARNING,
() -> "Challenge scheme " + challengeScheme + " not supported by the Restlet engine.");
}

/**
Expand All @@ -182,34 +196,39 @@
} else if (challenge.getScheme().getTechnicalName() == null) {
Context.getCurrentLogger().warning("A challenge scheme must have a technical name defined.");
} else {
ChallengeWriter cw = new ChallengeWriter();
cw.append(challenge.getScheme().getTechnicalName()).appendSpace();
int cwInitialLength = cw.getBuffer().length();

if (challenge.getRawValue() != null) {
cw.append(challenge.getRawValue());
} else {
AuthenticatorHelper helper = Engine.getInstance().findHelper(challenge.getScheme(), true, false);

if (helper != null) {
try {
helper.formatResponse(cw, challenge, request, httpHeaders);
} catch (Exception e) {
Context.getCurrentLogger().log(Level.WARNING,
"Unable to format the challenge response: " + challenge, e);
}
} else {
Context.getCurrentLogger().warning(
"Challenge scheme " + challenge.getScheme() + " not supported by the Restlet engine.");
}
}

result = (cw.getBuffer().length() > cwInitialLength) ? cw.toString() : null;
result = doFormatResponse(challenge, request, httpHeaders);
}

return result;
}

private static String doFormatResponse(ChallengeResponse challenge, Request request, Series<Header> httpHeaders) {
ChallengeWriter cw = new ChallengeWriter();
cw.append(challenge.getScheme().getTechnicalName()).appendSpace();
int cwInitialLength = cw.getBuffer().length();

if (challenge.getRawValue() == null) {
AuthenticatorHelper helper = Engine.getInstance().findHelper(challenge.getScheme(), true, false);

if (helper == null) {
logUnsupportedChallengeScheme(challenge.getScheme());
} else {
try {
helper.formatResponse(cw, challenge, request, httpHeaders);
} catch (Exception e) {
Context.getCurrentLogger().log(Level.WARNING, e,
() -> "Unable to format the challenge response: " + challenge);
}
}
} else {
cw.append(challenge.getRawValue());
}

return (cw.getBuffer().length() > cwInitialLength)
? cw.toString()
: null;
}

/**
* Parses the "Authentication-Info" header.
*
Expand All @@ -218,7 +237,7 @@
*/
public static AuthenticationInfo parseAuthenticationInfo(String header) {
AuthenticationInfo result = null;
HeaderReader<Parameter> hr = new HeaderReader<Parameter>(header);
HeaderReader<Parameter> hr = new HeaderReader<>(header);

try {
String nextNonce = null;
Expand All @@ -229,7 +248,7 @@
Parameter param = hr.readParameter();

while (param != null) {
try {

Check warning on line 251 in org.restlet.java/org.restlet/src/main/java/org/restlet/engine/security/AuthenticatorUtils.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested try block into a separate method.

See more on https://sonarcloud.io/project/issues?id=restlet_restlet-framework-java&issues=AaGTojX8WV1cgC4s_1uT&open=AaGTojX8WV1cgC4s_1uT&pullRequest=1617
if ("nextnonce".equals(param.getName())) {
nextNonce = param.getValue();
} else if ("qop".equals(param.getName())) {
Expand All @@ -255,8 +274,8 @@

result = new AuthenticationInfo(nextNonce, nonceCount, cnonce, qop, responseAuth);
} catch (IOException e) {
Context.getCurrentLogger().log(Level.WARNING, "Unable to parse the authentication info header: " + header,
e);
Context.getCurrentLogger().log(Level.WARNING, e,
() -> "Unable to parse the authentication info header: " + header);
}

return result;
Expand All @@ -271,22 +290,20 @@
* @return The list of parsed challenge request.
*/
public static List<ChallengeRequest> parseRequest(Response response, String header, Series<Header> httpHeaders) {
List<ChallengeRequest> result = new ArrayList<ChallengeRequest>();
List<ChallengeRequest> result = new ArrayList<>();

if (header != null) {
result = new ChallengeRequestReader(header).readValues();
for (ChallengeRequest cr : result) {
// Give a chance to the authenticator helper to do further
// parsing
// Give a chance to the authenticator helper to do further parsing
AuthenticatorHelper helper = Engine.getInstance().findHelper(cr.getScheme(), true, false);

if (helper != null) {
helper.parseRequest(cr, response, httpHeaders);
} else {
Context.getCurrentLogger()
.warning("Couldn't find any helper support the " + cr.getScheme() + " challenge scheme.");
}
}
if (helper == null) {
logUnsupportedChallengeScheme(cr.getScheme());
} else {
helper.parseRequest(cr, response, httpHeaders);
}
}
}

return result;
Expand Down Expand Up @@ -320,13 +337,12 @@
// Give a chance to the authenticator helper to do further parsing
AuthenticatorHelper helper = Engine.getInstance().findHelper(result.getScheme(), true, false);

if (helper != null) {
helper.parseResponse(result, request, httpHeaders);
} else {
Context.getCurrentLogger()
.warning("Couldn't find any helper support the " + result.getScheme() + " challenge scheme.");
}
}
if (helper == null) {
logUnsupportedChallengeScheme(result.getScheme());
} else {
helper.parseResponse(result, request, httpHeaders);
}
}

return result;

Expand Down Expand Up @@ -357,6 +373,13 @@
realm = challengeRequest.getRealm();
nonce = challengeRequest.getServerNonce();
challengeResponse.setOpaque(challengeRequest.getOpaque());

// Credentials charset advertised by the server (RFC 7617, section 2.1)
String charset = challengeRequest.getParameters().getFirstValue(CHARSET_PARAMETER, true);
if (HttpBasicHelper.isUtf8Enabled() && ChallengeScheme.HTTP_BASIC.equals(challengeResponse.getScheme())
&& charset != null && challengeResponse.getParameters().getFirstValue(CHARSET_PARAMETER, true) == null) {
challengeResponse.getParameters().add(CHARSET_PARAMETER, charset);
}
}

challengeResponse.setRealm(realm);
Expand Down Expand Up @@ -384,8 +407,7 @@
if (helper != null) {
resourceRef = helper.updateReference(resourceRef, challengeResponse, request);
} else {
Context.getCurrentLogger().warning(
"Challenge scheme " + challengeResponse.getScheme() + " not supported by the Restlet engine.");
logUnsupportedChallengeScheme(challengeResponse.getScheme());
}
}

Expand Down
Loading
Loading