Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
97 commits
Select commit Hold shift + click to select a range
0b7fc9e
Add App Shield attestation API, and fix the broken iOS App Attest lin…
shai-almog Jul 29, 2026
19abeb5
Carry the shield status by id so ShieldException stays serializable
shai-almog Jul 29, 2026
6dc3c5e
Fix App Attest error handling and guard ordering found in review
shai-almog Jul 29, 2026
f2e6472
Build the shield simulation menu without adding it, like its siblings
shai-almog Jul 29, 2026
41c16d6
Honour the configured failure mode, refresh the chain, and measure ti…
shai-almog Jul 29, 2026
b4d167d
Stop the shield header following cross-host redirects
shai-almog Jul 29, 2026
cf0a137
Install the network guard, without which protect() did nothing
shai-almog Jul 29, 2026
de95895
Do not assert before the backend has registered the key
shai-almog Jul 29, 2026
67750ab
Keep tokens off plaintext URLs, and stop over-eager invalidation
shai-almog Jul 29, 2026
735483c
Fix an MCP transport leak, SPKI collection and attestation registration
shai-almog Jul 29, 2026
907297e
Guard the failure path, and stop asserting for a discarded key
shai-almog Jul 29, 2026
cba30f1
Satisfy the PMD gate for the new shield code
shai-almog Jul 29, 2026
2a9246c
Close five races and bypasses in the attestation client
shai-almog Jul 29, 2026
d3d94ae
Fold hostnames by ASCII, and close two more reset races
shai-almog Jul 29, 2026
598df78
Close the recovery race, stale guard state, and a legacy read
shai-almog Jul 29, 2026
29c5da8
Restore ASCII-only sources, and close three more locale/lock gaps
shai-almog Jul 29, 2026
693bab7
Give the request hook back its legacy certificate view
shai-almog Jul 29, 2026
3c7e541
Key WebSocket headers case-insensitively, fail a refused reset
shai-almog Jul 29, 2026
2266240
Invalidate on partial reset, and retry a flaky Central resolution
shai-almog Jul 29, 2026
38323ed
Keep the throttle deadline when the keychain refuses the write
shai-almog Jul 29, 2026
da97f4f
Ask the guard only about URLs it selected, and reset the backoff
shai-almog Jul 29, 2026
782103a
Fail closed for a registered engine, and serialize the iOS coordinator
shai-almog Jul 29, 2026
08aea2d
Acknowledge a specific attested key, not whatever is pending
shai-almog Jul 29, 2026
4bddbdf
Treat an absent chain as unavailable, and hold the key across the write
shai-almog Jul 29, 2026
7c5cefb
Fall back to the flat chain instead of skipping enforcement
shai-almog Jul 29, 2026
7706da8
Remove headers case-insensitively, and do not resubmit a spent key
shai-almog Jul 29, 2026
0d53327
Commit preference writes, guard the native callback, persist the retry
shai-almog Jul 29, 2026
86ad845
Check the marker write, and stop sharing the KeyStore across tiers
shai-almog Jul 29, 2026
4a001ce
Honour the exhausted recovery, and clear it on success
shai-almog Jul 29, 2026
1a9dd40
Run the request hook only when the request asked for it
shai-almog Jul 29, 2026
08cbb04
Resolve implicit host policies late, and hold the spent marker in memory
shai-almog Jul 29, 2026
c22f167
Use a per-start generation, and confirm the marker deletion
shai-almog Jul 29, 2026
4bba0ee
Compose the shield guard, and stop a stale MCP reader closing a live …
shai-almog Jul 29, 2026
b4efd02
Reuse the App Attest key when Apple says it never processed the attes…
shai-almog Jul 29, 2026
0d60509
Release the bootstrap queue when a native callback returns nothing
shai-almog Jul 29, 2026
a88c9a8
Hold one lock across the assertion callback's staleness check and wha…
shai-almog Jul 30, 2026
f599ebf
Close the transport under the monitor, and keep the key on any answer…
shai-almog Jul 30, 2026
c917081
Make the simulator's shield switches act, and bound the DER walk
shai-almog Jul 30, 2026
10f8ffd
Give the new test its header, and stop a Maven Central reset failing …
shai-almog Jul 30, 2026
2551b15
Keep an exhausted invalid-key attestation terminal
shai-almog Jul 30, 2026
52a2ddf
Keep the terminal markers until the key is confirmed gone, and pin wi…
shai-almog Jul 30, 2026
c4677c7
Arm the simulator engine from the outcome menu, and disarm the spent …
shai-almog Jul 30, 2026
a34ee58
Refuse to attest from a half-deleted identity, and two more simulator…
shai-almog Jul 30, 2026
9b164ac
Reopen the real loopback transport, and refuse Content-Type as the to…
shai-almog Jul 30, 2026
84640f4
Serialize transport reopen, and record a spent recovery before discar…
shai-almog Jul 31, 2026
872d84f
Escape control characters in the nonce instead of erasing them
shai-almog Jul 31, 2026
037e319
Do not run the native callbacks while retaining them
shai-almog Aug 1, 2026
f050250
Use the guard-flag retention idiom rather than a never-taken branch
shai-almog Aug 1, 2026
2df7c84
Close the restart race inside the transport lock, pin runtime hosts, …
shai-almog Aug 1, 2026
d9e14cb
Keep the App Attest identity readable when a deletion fails, and alwa…
shai-almog Aug 1, 2026
615cfbb
Merge remote-tracking branch 'origin/master' into feature/app-shield
shai-almog Aug 1, 2026
ae7e497
Merge master, and deliver a late-attached health listener on the EDT
shai-almog Aug 1, 2026
04de358
Wait for the component-inspector details panel before capturing, and …
shai-almog Aug 1, 2026
b96e68e
Take the invalid-key decision inside the lock, and add the header the…
shai-almog Aug 1, 2026
f35d46e
Refuse transport-owned headers as the token header
shai-almog Aug 1, 2026
a6060b8
Clear the start marker before rolling back, and stop re-reporting an …
shai-almog Aug 1, 2026
0938019
Leave the javase simulator screenshot suite exactly as master has it
shai-almog Aug 1, 2026
acf1dd6
Refuse Cookie as the token header, and finish initializing before say…
shai-almog Aug 1, 2026
4f13127
Wait for initialization at the request paths, and keep an accepted Ap…
shai-almog Aug 1, 2026
7c5d4c4
Hold the accepted key's state in memory when the keychain refuses to
shai-almog Aug 1, 2026
3a756d3
Retry the Windows clean-target Maven steps through a Central 429
shai-almog Aug 1, 2026
221ede6
Hand back the attestation even when its metadata cannot be stored
shai-almog Aug 1, 2026
96c7cf1
Install the shield guard before the engine, and honor a confirmation …
shai-almog Aug 1, 2026
200704c
Clear the promoted state in memory, stop exiting on a debugger, reope…
shai-almog Aug 1, 2026
04de9f5
Serialize MCP transport opens on a lock no transport can acquire
shai-almog Aug 1, 2026
5a106cc
Refuse the proxy-credential headers as the token header
shai-almog Aug 1, 2026
ccc6524
Iterate the open locks with foreach, which the PMD gate requires
shai-almog Aug 1, 2026
112dabb
Promote through one method, so the grace path finishes what confirmat…
shai-almog Aug 1, 2026
4f7705b
Fail closed on an interrupted wait, clear renamed headers, refresh re…
shai-almog Aug 1, 2026
304b888
Marshal every off-EDT registration, not only one that finds the resul…
shai-almog Aug 1, 2026
9386057
Ignore Sec-WebSocket-Extensions, which no reader here can decode
shai-almog Aug 1, 2026
8e315ab
Give WebSocketTest the copyright header the gate requires
shai-almog Aug 1, 2026
f36b1b3
Move fetchToken's wait into the background task
shai-almog Aug 1, 2026
8c27909
Clear only the headers this request was given, and stop headersFor bl…
shai-almog Aug 1, 2026
28534df
Record an accepted attestation durably, not only in process memory
shai-almog Aug 1, 2026
10fc6cb
Reject the two header names an Android request rewrites on its way out
shai-almog Aug 1, 2026
2f18db5
Release the open lock through an iterator rather than during a foreach
shai-almog Aug 1, 2026
285b8ec
Keep an attestation nobody took delivery of, rather than promoting a …
shai-almog Aug 1, 2026
60545af
Record an undelivered attestation durably, and release it only when d…
shai-almog Aug 1, 2026
353a124
Claim the result once, so cancelling and delivering cannot both win
shai-almog Aug 1, 2026
26df1bd
Make "attested but undelivered" a state, so it cannot half-persist
shai-almog Aug 1, 2026
a56e259
Record the delivery on the retry path too, not only the scheduled one
shai-almog Aug 1, 2026
fcc86d4
Rehydrate the acceptance fallback as undelivered, which is what it re…
shai-almog Aug 1, 2026
bebaf5b
Retain the attestation in the same critical section that publishes th…
shai-almog Aug 1, 2026
cdd6627
Record the delivery even when the undelivered state never persisted
shai-almog Aug 2, 2026
737b894
Run the Android app build with --stacktrace
shai-almog Aug 2, 2026
8e17d63
Retry and verify the Windows ffmpeg install, which failed silently
shai-almog Aug 2, 2026
3d4382e
Claim the retry handoff, and refuse two more headers a port rewrites
shai-almog Aug 2, 2026
645e09c
Make the header removal complete, and retry the Windows build only on…
shai-almog Aug 2, 2026
f9bec7e
Honour a cancelled token fetch, and make secure-storage writes durable
shai-almog Aug 2, 2026
753e2ac
Marshal the combined health registration, error half included
shai-almog Aug 2, 2026
9f7957f
Reserve a retained attestation for its own caller, and dispatch statu…
shai-almog Aug 2, 2026
328ed95
Fence a restarted MCP reader, drop superseded signal notifications, a…
shai-almog Aug 2, 2026
7e4e92f
Use a markdown comment for the new signal helper
shai-almog Aug 2, 2026
fa7c944
Four findings from review bodies I had never been reading
shai-almog Aug 3, 2026
97e7ae9
Settle a stale status at delivery, and reject header names that are n…
shai-almog Aug 3, 2026
6e20107
Import BackingStoreException in the simulator secure storage
shai-almog Aug 3, 2026
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
22 changes: 21 additions & 1 deletion .github/workflows/javase-cef-ffmpeg-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,27 @@ jobs:
- name: Install ffmpeg on Windows
if: runner.os == 'Windows'
shell: powershell
run: choco install ffmpeg -y
run: |
# Retried, and then VERIFIED. The community feed answers 504 often enough to
# matter, and chocolatey can report success after failing to fetch -- so the
# first sign of trouble was the smoke test dying three steps later with
# "FileNotFoundError: [WinError 2] The system cannot find the file specified",
# which names neither ffmpeg nor the feed. An install step that does not
# confirm the tool it installed is how a transient upstream outage becomes an
# unreadable failure in somebody else's test.
$ok = $false
foreach ($delay in 0, 30, 90) {
if ($delay -gt 0) {
Write-Host "choco install ffmpeg failed; retrying in $delay s..."
Start-Sleep -Seconds $delay
}
choco install ffmpeg -y --no-progress
if (Get-Command ffmpeg -ErrorAction SilentlyContinue) { $ok = $true; break }
}
if (-not $ok) {
throw "ffmpeg is not on PATH after installing it. The chocolatey feed is the usual reason -- look for a 504 above."
}
ffmpeg -version

- name: Run JavaSE CEF/FFmpeg smoke test
env:
Expand Down
47 changes: 43 additions & 4 deletions .github/workflows/parparvm-tests-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -192,10 +192,49 @@ jobs:
working-directory: vm
shell: pwsh
run: |
mvn -B clean package -pl JavaAPI -am -DskipTests
# Single-quote the -D args: PowerShell otherwise mangles the dotted
# property name (splitting it at the '.').
mvn -B test -pl tests -am '-Dtest=CleanTargetIntegrationTest' '-Dsurefire.failIfNoSpecifiedTests=false'
# Retried, in the same spirit as the Ninja install above: Maven Central
# answers 429 Too Many Requests under load and the build dies during
# dependency RESOLUTION, before a line of this project is compiled. Nothing
# about the failure involves the code under test, and a fresh runner IP is
# not something a re-run can be relied on to produce. Backoff is in tens of
# seconds rather than the 5 used for the pip blip, because a rate limit needs
# waiting out rather than retrying through.
# Written in the same plain loop shape as the Ninja install above rather
# than with a function and splatting, because that shape is already proven
# on these runners and this is not the place to find out about a quoting
# difference.
$ok = $false
foreach ($delay in 0, 30, 90) {
if ($delay -gt 0) {
Write-Host "mvn failed; retrying in $delay s..."
Start-Sleep -Seconds $delay
}
mvn -B clean package -pl JavaAPI -am -DskipTests
if ($LASTEXITCODE -eq 0) { $ok = $true; break }
}
if (-not $ok) { throw "mvn clean package failed after all retries" }
# This one RUNS TESTS, so the retry is restricted to the failure shape it
# exists for. A blanket loop lets an intermittent product regression pass on
# attempt two and turns a blocking gate green -- which is the opposite of what
# a gate is for, and worse than the flake it was hiding.
$resolutionFailure = 'status: (403|429|50[0-9])|Could not transfer artifact|Unresolveable build extension|Non-resolvable import POM|Could not resolve dependencies'
$ok = $false
foreach ($delay in 0, 30, 90) {
if ($delay -gt 0) {
Write-Host "mvn failed on a dependency-resolution error; retrying in $delay s..."
Start-Sleep -Seconds $delay
}
# Single-quote the -D args: PowerShell otherwise mangles the dotted
# property name (splitting it at the '.').
# Tee-Object keeps the log on the console AND gives us something to match.
mvn -B test -pl tests -am '-Dtest=CleanTargetIntegrationTest' '-Dsurefire.failIfNoSpecifiedTests=false' 2>&1 |
Tee-Object -Variable mvnOutput
if ($LASTEXITCODE -eq 0) { $ok = $true; break }
if (-not ($mvnOutput -match $resolutionFailure)) {
throw "mvn test failed for a reason that is not a transient dependency-resolution error; not retrying"
}
}
if (-not $ok) { throw "mvn test failed after all retries" }
env:
JDK_8_HOME: ${{ env.JDK_8_HOME }}
JDK_11_HOME: ${{ env.JDK_11_HOME }}
Expand Down
10 changes: 9 additions & 1 deletion .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,14 @@ jobs:
with:
node-version: '20'
- name: Run Unit Tests
# Maven Central answers 429 from the runner CDN edge often enough to kill
# this step outright -- it dies reading the root POM, before a line is
# compiled, so nothing about the branch is being tested when it happens.
# Retried through the shared helper, but ONLY for that failure shape:
# RETRY_ONLY_MATCHING keeps a failing test failing on the first attempt
# rather than letting a re-run launder a flake into a pass.
env:
RETRY_ONLY_MATCHING: 'status: (403|429|50[0-9])|Could not transfer artifact|Unresolveable build extension|Non-resolvable import POM'
run: |
MVN_GOAL="verify"
MVN_ARGS=""
Expand All @@ -98,7 +106,7 @@ jobs:
MVN_ARGS="-Dspotbugs.skip=true -Dpmd.skip=true -Dcheckstyle.skip=true -Djacoco.skip=true"
fi
cd maven
mvn clean "$MVN_GOAL" -DunitTests=true -pl core-unittests -am -Dmaven.javadoc.skip=true -Plocal-dev-javase $MVN_ARGS
bash ../scripts/ci/retry.sh mvn clean "$MVN_GOAL" -DunitTests=true -pl core-unittests -am -Dmaven.javadoc.skip=true -Plocal-dev-javase $MVN_ARGS
cd ..
- name: Run push service-worker contract
if: ${{ matrix.java-version == 8 }}
Expand Down
40 changes: 38 additions & 2 deletions .github/workflows/windows-cross-compile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,44 @@ jobs:
- name: Build codename1-core + the Windows port (JDK 8)
run: |
cd maven
JAVA_HOME="$JDK_8_HOME" mvn -B -pl windows -am -DskipTests \
'-Dmaven.javadoc.skip=true' '-Plocal-dev-javase' install
# Maven Central intermittently resets the connection or throttles the runner,
# and Maven treats that as a permanent resolution failure -- observed killing
# this job while resolving a build extension, before any project code compiled.
# The backoff grows because a flat retry lands inside the same window a 429 is
# still rate limiting in. A genuine build failure fails identically every time.
# Retried ONLY for that failure shape. A blanket loop lets an intermittent
# compiler, generator or packaging regression pass on a later attempt and turn
# a blocking gate green, which is worse than the flake it hides -- and the
# retries reuse the previous attempt's target directories, so a partial output
# can decide the result. Matching the output keeps a real failure terminal on
# its first occurrence, and the retry starts from clean.
resolution='status: (403|429|50[0-9])|Could not transfer artifact|Unresolveable build extension|Non-resolvable import POM|Could not resolve dependencies'
goal=install
for delay in 30 120 300 0; do
# PIPESTATUS, not the pipeline's status: tee succeeds even when mvn does not,
# and this step does not set pipefail. Reading the wrong one would make every
# attempt look successful, which is the opposite failure to the one being
# fixed and would hide everything.
JAVA_HOME="$JDK_8_HOME" mvn -B -pl windows -am -DskipTests \
'-Dmaven.javadoc.skip=true' '-Plocal-dev-javase' $goal 2>&1 \
| tee /tmp/windows-cross-build.log
status=${PIPESTATUS[0]}
if [ "$status" -eq 0 ]; then
break
fi
if ! grep -Eq "$resolution" /tmp/windows-cross-build.log; then
echo "core + Windows port build failed for a reason that is not a transient"
echo "dependency-resolution error; not retrying"
exit 1
fi
if [ "$delay" = "0" ]; then
echo "core + Windows port build failed after all retries"
exit 1
fi
echo "Maven Central looks flaky; retrying in ${delay}s from clean"
goal="clean install"
sleep "$delay"
done
test -f core/target/classes/com/codename1/ui/Form.class
test -f windows/target/classes/com/codename1/impl/windows/WindowsImplementation.class

Expand Down
52 changes: 52 additions & 0 deletions CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java
Original file line number Diff line number Diff line change
Expand Up @@ -6363,6 +6363,28 @@ public boolean canGetSSLCertificates() {
return false;
}

/// True when this port can report a digest of each certificate's subject public key info,
/// enabling public-key pinning through [#getSSLCertificatesEx(Object, String)].
///
/// Public-key pins survive certificate renewal on the same key pair; whole-certificate
/// fingerprints do not, which is why a renewal can otherwise take a pinning app offline.
public boolean canGetPublicKeyDigests() {
return false;
}

/// The richer certificate list, grouped per certificate.
///
/// Same `algorithm:value` encoding as [#getSSLCertificates(Object, String)], with two
/// additions: a `CHAIN:<n>` entry starts each certificate's group (0 is the leaf), and a
/// `SPKI-SHA-256:<base64>` entry carries the public-key digest.
///
/// Called only when something asked for public-key digests, so a port that does not override
/// this simply never sees it. The default delegates to the flat form, which parses correctly
/// and just yields no digests.
public String[] getSSLCertificatesEx(Object connection, String url) throws IOException {
return getSSLCertificates(connection, url);
}

/// SSL certificate checks must be performed via a callback from the native side,
/// rather than explicitly checking as part of NetworkManager's connection
/// flow. This is mainly for iOS POST requests. If we try to get the SSL certs
Expand Down Expand Up @@ -11261,6 +11283,36 @@ public String[] getEnabledAccessibilityServices() {
return new String[0];
}

/// Discards cached platform attestation state so the next [#requestIntegrityToken(String)] starts
/// from a fresh hardware key.
///
/// Apple's App Attest model is attest once, then assert many times against the key the server
/// recorded. When the server no longer recognises that key -- the app was reinstalled, the device
/// was restored from a backup, or the key was invalidated by the OS -- the client has to throw the
/// key away and attest again. This is how the attestation layer is told to do that. No-op where
/// attestation is unsupported or stateless (Play Integrity holds no client-side key).
public void resetAttestation() {
}

/// Acknowledges that a verifying backend has recorded the attested key, so subsequent requests can
/// take the cheap assertion path. See
/// [com.codename1.security.DeviceIntegrity#confirmAttestation()]. No-op where attestation holds no
/// client-side key.
public void confirmAttestation(String keyId) {
}

/// Returns digests of the certificates the running application is actually signed with, so a build
/// can be compared against the identity it was built under and repackaging can be reported.
///
/// Deliberately not surfaced on [com.codename1.security.DeviceIntegrity]: an app has no use for its
/// own signature, and a comparison performed on the device is defeated by the same patch that did
/// the repackaging. The value of this is that it is reported to a verifying service, which checks it
/// against what the build server recorded. Returns an empty array where the platform has no such
/// concept.
public String[] getAppSignerDigests() {
return new String[0];
}

/// Marks the current screen as secure, blocking OS screenshots, screen recording and accessibility
/// screen scraping while it is displayed (Android `FLAG_SECURE`). No-op where unsupported.
///
Expand Down
130 changes: 130 additions & 0 deletions CodenameOne/src/com/codename1/impl/WebSocketImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public abstract class WebSocketImpl {
private final String url;
private WebSocketEventSink sink;
private String[] requestedSubprotocols;
private final java.util.Hashtable requestHeaders = new java.util.Hashtable();
// Written by the port's connect/handshake thread, read by the user's
// connect handler -- volatile to publish the value across threads.
@SuppressWarnings("PMD.AvoidUsingVolatile")
Expand Down Expand Up @@ -71,6 +72,135 @@ protected final String[] requestedSubprotocols() {
return requestedSubprotocols;
}

/// Adds a header to the opening handshake. Called by the public facade
/// before `connect(int)`. A null or empty name is ignored; a null value
/// removes a previously set header.
///
/// Ports that build the handshake themselves emit these; ports built on a
/// platform WebSocket that does not expose the handshake ignore them. See
/// the facade's `header` method for which those are.
public final void setRequestHeader(String name, String value) {
if (name == null || name.length() == 0) {
return;
}
// Keyed by the folded name, because HTTP header names are case-insensitive.
// Keying by the spelling the caller happened to use meant
// header("Authorization", v) followed by header("authorization", null) left the
// credential in place, and setting both emitted the field twice -- leaving the
// server to pick one, which is not a thing to leave to chance for an
// Authorization header. The caller's spelling is kept for emission.
String key = asciiLower(name);
if (value == null) {
requestHeaders.remove(key);
} else {
requestHeaders.put(key, new String[] {name, value});
}
}

/// The extra handshake headers, keyed by the ASCII-folded name. Each value is a
/// two-element `String[]` of `{ name as the caller spelled it, value }`, so lookups
/// and removals are case-insensitive the way HTTP is while emission preserves the
/// caller's capitalization. Never null; ports read this while building the
/// handshake. Ports must not emit an entry whose name collides with a header the
/// handshake sets itself.
protected final java.util.Hashtable requestHeaders() {
return requestHeaders;
}

/// Appends the extra handshake headers in `name: value` CRLF form, skipping
/// any that would collide with a header the handshake already wrote.
///
/// Shared here rather than copied per port so the collision list and the
/// header-injection guard stay in one place: a header value carrying CR or
/// LF would otherwise let a caller inject arbitrary handshake headers.
protected final void appendRequestHeaders(StringBuilder req) {
java.util.Enumeration keys = requestHeaders.keys();
while (keys.hasMoreElements()) {
String key = (String) keys.nextElement();
String[] pair = (String[]) requestHeaders.get(key);
String name = pair[0];
String value = pair[1];
// The NAME is checked for being a legal field-name token before anything is
// decided about it. Screening only for CR and LF let "Sec-WebSocket-Extensions "
// -- one trailing space -- past the reserved-name comparison, and a lenient
// server trims that and negotiates the extension anyway. These readers do not
// process RSV1 or inflate payloads, so a compressed frame arrives as garbage:
// the reserved list exists precisely to stop that being negotiable, and a
// comparison that any non-token character walks around is not a list.
if (!isFieldNameToken(name)) {
continue;
}
if (isReservedHandshakeHeader(name)) {
continue;
}
if (containsCrLf(value)) {
continue;
}
req.append(name).append(": ").append(value).append("\r\n");
}
}

/// Whether this is a legal HTTP field name -- RFC 9110 token, so no spaces, no
/// separators, nothing outside printable ASCII.
///
/// Rejecting rather than trimming: a caller that wrote a trailing space meant one
/// header and a lenient server would read another, and quietly repairing the
/// difference is how the two ends stop agreeing about what was sent.
private static boolean isFieldNameToken(String name) {
if (name == null || name.length() == 0) {
return false;
}
for (int i = 0; i < name.length(); i++) {
char c = name.charAt(i);
if (c <= 0x20 || c >= 0x7f) {
return false;
}
if (c == '(' || c == ')' || c == '<' || c == '>' || c == '@' || c == ','
|| c == ';' || c == ':' || c == '\\' || c == '"' || c == '/'
|| c == '[' || c == ']' || c == '?' || c == '=' || c == '{'
|| c == '}') {
return false;
}
}
return true;
}

private static boolean isReservedHandshakeHeader(String name) {
// ASCII folding, not toLowerCase(): under the Turkish and Azerbaijani locales an
// uppercase I folds to a dotless letter outside ASCII, so a header spelled
// CONNECTION would not be recognised as reserved and would be emitted alongside
// the handshake's own -- producing a conflicting or rejected opening handshake
// while the API promises reserved names are ignored.
String n = asciiLower(name);
return "host".equals(n) || "upgrade".equals(n) || "connection".equals(n)
|| "sec-websocket-key".equals(n) || "sec-websocket-version".equals(n)
|| "sec-websocket-protocol".equals(n)
// Extensions negotiate what the FRAMES mean, and no reader here
// implements one. Emitting it let a caller ask for permessage-deflate;
// a compliant server then agrees, sets RSV1 and sends compressed
// payloads -- and the readers mask off the opcode and pass the bytes
// straight through, so text arrives as mojibake and binary arrives
// compressed. The failure appears at the application, far from the one
// header that caused it, and only against servers that happen to offer
// the extension. Ignored until a port can actually inflate.
|| "sec-websocket-extensions".equals(n)
|| "content-length".equals(n);
}

/// Lowercases ASCII letters only, so the result never depends on the device locale.
private static String asciiLower(String s) {
StringBuilder sb = new StringBuilder(s.length());
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
sb.append(c >= 'A' && c <= 'Z' ? (char) (c + 32) : c);
}
return sb.toString();
}

private static boolean containsCrLf(String s) {
return s.indexOf('\r') >= 0 || s.indexOf('\n') >= 0;
}

/// Records the subprotocol the server selected. Ports call this once
/// the handshake completes, before firing `sink().onConnect()`, so the
/// value is visible to the user's connect handler.
Expand Down
Loading
Loading