Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dba861bb7f | ||
|
|
6a0e6925e5 | ||
|
|
aa45b9b123 | ||
|
|
9890560867 | ||
|
|
7e503478c4 |
No files matched your search
@@ -44,11 +44,24 @@ jobs:
|
||||
docker run --rm -v "$PWD:/src" -w /src mcr.microsoft.com/playwright:v1.63.0-noble \
|
||||
bash -lc 'mkdir -p /tmp/c4f-browser; cd /tmp/c4f-browser; npm init -y >/dev/null; npm install playwright@1.63.0; cd /src; NODE_PATH=/tmp/c4f-browser/node_modules node tests/fake_gamepads_test.cjs'
|
||||
|
||||
- name: Experimental feature checks
|
||||
run: |
|
||||
docker run --rm -v "$PWD:/src" -w /src mcr.microsoft.com/playwright:v1.63.0-noble \
|
||||
bash -lc 'mkdir -p /tmp/c4f-browser; cd /tmp/c4f-browser; npm init -y >/dev/null; npm install playwright@1.63.0; cd /src; NODE_PATH=/tmp/c4f-browser/node_modules node tests/experimental_test.cjs'
|
||||
|
||||
- name: WebRTC browser and input recovery checks
|
||||
run: |
|
||||
docker run --rm -v "$PWD:/src" -w /src mcr.microsoft.com/playwright:v1.63.0-noble \
|
||||
bash -lc 'mkdir -p /tmp/c4f-browser; cd /tmp/c4f-browser; npm init -y >/dev/null; npm install playwright@1.63.0; cd /src; NODE_PATH=/tmp/c4f-browser/node_modules node tests/rtc_e2e.cjs && NODE_PATH=/tmp/c4f-browser/node_modules node tests/rtc_input_e2e.cjs'
|
||||
|
||||
- name: Collect what was built
|
||||
run: |
|
||||
version=$(cat VERSION)
|
||||
mkdir -p artifacts
|
||||
cp build/control4free.elf "build/Control4Free-$version.pkg" "build/Control4Free-$version-diag.pkg" artifacts/
|
||||
cp build/control4free.elf "build/Control4Free-$version.pkg" artifacts/
|
||||
# GitHub sorts assets by name. Keep the diagnostic download below the
|
||||
# normal installer, payload and checksums, with its purpose in the name.
|
||||
cp "build/Control4Free-$version-diag.pkg" "artifacts/troubleshooting-Control4Free-$version-diag.pkg"
|
||||
cd artifacts && sha256sum * > SHA256SUMS && cat SHA256SUMS
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
@@ -72,4 +85,9 @@ jobs:
|
||||
version=$(cat VERSION)
|
||||
python3 tools/release_notes.py CHANGELOG.md "$version" "$GITHUB_REPOSITORY" > notes.md
|
||||
test -s notes.md || { echo "CHANGELOG.md has no section for $version"; exit 1; }
|
||||
gh release create "${GITHUB_REF#refs/tags/}" artifacts/* --verify-tag --title "Control4Free $version" --notes-file notes.md
|
||||
gh release create "${GITHUB_REF#refs/tags/}" \
|
||||
"artifacts/Control4Free-$version.pkg#Normal package (recommended)" \
|
||||
'artifacts/control4free.elf#Payload for manual loading' \
|
||||
'artifacts/SHA256SUMS#Download checksums' \
|
||||
"artifacts/troubleshooting-Control4Free-$version-diag.pkg#Troubleshooting only - diagnostic package" \
|
||||
--verify-tag --title "Control4Free $version" --notes-file notes.md
|
||||
@@ -1,7 +1,31 @@
|
||||
# Changelog
|
||||
|
||||
Versions follow [Semantic Versioning](https://semver.org): PATCH for fixes, MINOR for features that break
|
||||
nothing, MAJOR for anything people rely on that changes.
|
||||
## 1.3.0 — 2026-10-22
|
||||
|
||||
WebRTC connections, controller testing on the PS4, and new experimental controller features.
|
||||
|
||||
- **Rebuilt controller connections around WebRTC.** The old WebSocket connection delivered everything
|
||||
in order, so recovering a lost packet could hold up newer input. Input now uses an unordered channel
|
||||
without retransmissions, while controller selection and status use a separate reliable channel.
|
||||
This reduces delayed and bunched-up inputs during packet loss. After a brief disconnection, the page
|
||||
reconnects and restores its controller selection.
|
||||
- **Controller tester in the PS4 app:** see buttons, sticks, analog triggers, touchpad, motion and
|
||||
connection latency; test rumble, choose light-bar colours with RGB sliders and reset orientation.
|
||||
Supports controllers and HDMI-CEC remotes. See [Testing controllers](docs/playing.md#testing-controllers-on-the-ps4).
|
||||
- **DualShock 4 and DualSense touchpads now work when linked:** both finger positions and the click.
|
||||
Experimental Sony controller support also adds direct rumble and PS4 light-bar colours.
|
||||
- **Experimental motion controls** use a phone's sensors or a linked DualShock 4 / DualSense.
|
||||
- **Experimental trigger rumble** adds vibration to the trigger you're pulling on supported Xbox
|
||||
controllers and a linked DualSense. It follows the game's rumble; when both triggers are held, the
|
||||
last one pulled vibrates. See [Experimental features](docs/playing.md#experimental-features) for setup
|
||||
and how it works.
|
||||
- Corrected the large and small rumble motors being swapped on connected gamepads.
|
||||
|
||||
Experimental features are off by default. Sony linking and motion need the
|
||||
[Chrome / Edge setup](docs/troubleshooting.md#experimental-features).
|
||||
|
||||
**Updating:** update the PS4 service, then reload the controller page. Replace any saved copy with a
|
||||
fresh one from the updated service.
|
||||
|
||||
## 1.2.1 — 2026-10-11
|
||||
|
||||
|
||||
@@ -45,16 +45,36 @@ the page and the app show the version the service reports.
|
||||
docker run --rm --network none -v "$PWD:/src" -w /src control4free-launcher-build python3 -B tests/run.py
|
||||
```
|
||||
|
||||
The suites build the real sources for the host with only the PS4 calls stubbed, so they need no console:
|
||||
The six suites build the real sources for the host with only the PS4 calls stubbed, so they need no console:
|
||||
- `test_web` and `test_recovery` run the actual service against a fake kernel log
|
||||
(`tests/web_stub.c`) that writes the same lines firmware 10.01 does, over real HTTP and WebSocket
|
||||
connections;
|
||||
(`tests/web_stub.c`) that writes the same lines firmware 10.01 does, over real HTTP and a test-only
|
||||
WebSocket adapter; production controller connections use WebRTC;
|
||||
- `test_frontend` runs the page's own functions under Node;
|
||||
- `test_launcher` drives the app's logic, renders every state of its screen to `build/launcher-preview-*.png`,
|
||||
and checks the package's `param.sfo`;
|
||||
- `test_logging` covers the log, the kernel-log opener and DeviceId matching.
|
||||
- `test_launcher` drives the app's logic and input worker, compares warm tester renders with fresh
|
||||
renders, produces `build/launcher-preview-*.png`, and checks the package's `param.sfo`;
|
||||
- `test_logging` covers the log, the kernel-log opener and DeviceId matching;
|
||||
- `test_rtc` checks STUN/SCTP, congestion and receive windows, UDP backpressure, and the production HTTP
|
||||
signalling endpoint with the WebSocket adapter disabled.
|
||||
|
||||
`tests/fake_gamepads_test.cjs` runs researched gamepad emulations against the controller page in the Playwright Docker image, without console access or hardware compatibility claims.
|
||||
The [build workflow](.github/workflows/build.yml) gives the Docker commands for the browser checks, using
|
||||
the pinned Playwright image and package. Run these after the host suites, which build their test server:
|
||||
|
||||
- `tests/browser_test.cjs`: controller-page editor, mapping and connection lifecycle;
|
||||
- `tests/fake_gamepads_test.cjs`: researched gamepad emulations, without hardware compatibility claims;
|
||||
- `tests/experimental_test.cjs`: optional HID, motion, output and reconnect behavior;
|
||||
- `tests/rtc_e2e.cjs`: real Chromium and Firefox WebRTC connections and reliable fragmented replies;
|
||||
- `tests/rtc_input_e2e.cjs`: input freshness and recovery through dropped, delayed and reordered UDP packets.
|
||||
|
||||
Run packaging and launcher preview tests sequentially because they share renderer scratch files.
|
||||
|
||||
For controller-test drawing changes, `tests/test_tester_render.py` runs the cache-transition checks on
|
||||
their own. Its optional `--benchmark` reports host CPU drawing time, excluding input, framebuffer copy
|
||||
and display presentation; do not describe those results as PS4 frame timings.
|
||||
|
||||
Changes to mapping belong in the page's mapping tests as well as its browser checks. Exercise independent
|
||||
analog trigger pressures and digital flags, multiple sources driving a slot, profile changes and
|
||||
reconnect gating. For connection RTT changes, check probe matching, expiry, ownership changes and the
|
||||
launcher's unavailable/measuring states. RTT is client-reported diagnostic data, separate from pad input.
|
||||
|
||||
Add a test with your fix: one that fails without it. Then test on a real console if you can, and say in your
|
||||
pull request which firmware, GoldHEN version and games you tried.
|
||||
@@ -80,8 +100,8 @@ Build without new warnings; the app is built with `-Werror`.
|
||||
|
||||
1. Update `VERSION` and the changelog's release date and notes. Review mapping, compatibility and
|
||||
diagnostic documentation; distinguish hardware reports from emulated-input tests.
|
||||
2. Run all five Docker host suites and the browser/editor and emulated-gamepad checks. Build normal and
|
||||
diagnostic packages without new warnings. Inspect launcher previews and browser layouts.
|
||||
2. Run all six Docker host suites and all five browser checks above, including WebRTC impairment checks.
|
||||
Build normal and diagnostic packages without new warnings. Inspect launcher previews and browser layouts.
|
||||
3. Validate standard controllers on real hardware: simultaneous devices, source mixing, rumble,
|
||||
reconnect, logout and rest/wake. Recheck firmware 10.01 and a modern tested firmware, and record the
|
||||
results. Wheel/guitar model compatibility remains unverified until device reports establish it.
|
||||
|
||||
@@ -20,7 +20,10 @@ VERSION := $(shell cat VERSION)
|
||||
NAME := control4free
|
||||
ELF := build/$(NAME).elf
|
||||
OBJDIR := build/payload
|
||||
SOURCES := src/main.c src/log.c src/vda.c src/klog_line.c src/feedback.c src/web.c src/net.c
|
||||
SOURCES := src/main.c src/log.c src/vda.c src/klog_line.c src/feedback.c src/web.c src/net.c src/rtc.c src/stun.c src/sctp.c
|
||||
# The pages' WebRTC connection is encrypted with Mbed TLS (vendor/mbedtls, Apache-2.0),
|
||||
# built with only what include/c4f_mbedtls_config.h asks for.
|
||||
MBEDTLS := $(patsubst vendor/mbedtls/library/%.c,%,$(wildcard vendor/mbedtls/library/*.c))
|
||||
|
||||
# Research build: logs what scePadVirtualDeviceGetRemoteSetting returns. Its ELF
|
||||
# gets its own name, so it can never be mistaken for a release.
|
||||
@@ -39,11 +42,13 @@ ifdef C4F_DIAG
|
||||
OBJDIR := build/diag
|
||||
endif
|
||||
|
||||
OBJECTS := $(SOURCES:src/%.c=$(OBJDIR)/%.o) $(OBJDIR)/client.o $(OBJDIR)/assets.o $(OBJDIR)/qrcodegen.o
|
||||
OBJECTS := $(SOURCES:src/%.c=$(OBJDIR)/%.o) $(OBJDIR)/client.o $(OBJDIR)/assets.o $(OBJDIR)/qrcodegen.o \
|
||||
$(MBEDTLS:%=$(OBJDIR)/mbedtls/%.o)
|
||||
|
||||
CFLAGS += -std=gnu11 -Wall -Wextra -Wpointer-arith -g -O2
|
||||
CFLAGS += -MMD -MP
|
||||
CFLAGS += -Iinclude -Ivendor/jsmn -Ivendor/qrcodegen -DC4F_VERSION='"$(VERSION)"' $(EXTRA_CFLAGS)
|
||||
CFLAGS += -Ivendor/mbedtls/include -DMBEDTLS_CONFIG_FILE='<c4f_mbedtls_config.h>'
|
||||
LDFLAGS += -lScePad -lSceUserService -ldl -lpthread
|
||||
|
||||
.PHONY: all clean
|
||||
@@ -53,7 +58,7 @@ LDFLAGS += -lScePad -lSceUserService -ldl -lpthread
|
||||
|
||||
all: $(ELF)
|
||||
|
||||
$(OBJDIR) build:
|
||||
$(OBJDIR) $(OBJDIR)/mbedtls build:
|
||||
mkdir -p $@
|
||||
|
||||
# VERSION is compiled into the objects, and make doesn't notice a changed flag,
|
||||
@@ -61,6 +66,9 @@ $(OBJDIR) build:
|
||||
$(OBJDIR)/%.o: src/%.c VERSION | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c $< -o $@
|
||||
|
||||
$(OBJDIR)/mbedtls/%.o: vendor/mbedtls/library/%.c include/c4f_mbedtls_config.h | $(OBJDIR)/mbedtls
|
||||
$(CC) $(CFLAGS) -c $< -o $@
|
||||
|
||||
build/client.c: client/index.html tools/embed_client.py | build
|
||||
python3 tools/embed_client.py $< $@
|
||||
|
||||
|
||||
@@ -46,15 +46,41 @@ sees a virtual DualShock 4, which provides:
|
||||
- **Rumble and light bar.** When a game rumbles, your phone buzzes (Android), and so does your gamepad,
|
||||
though not every controller supports vibration in the browser. The page glows in the controller's
|
||||
light-bar colour, the one the PS4 or the game sets.
|
||||
- **Fast.** Input reaches the console the moment it arrives, at a DualShock 4's own rate while you play.
|
||||
- **DualShock 4 and DualSense touchpads**, including two finger positions, when linked in Chrome or Edge
|
||||
on a computer. Experimental Sony controller support also adds direct rumble and PS4 light-bar colours.
|
||||
- **Experimental motion controls** from a phone or a linked DualShock 4 / DualSense, and **trigger rumble**
|
||||
on supported Xbox controllers and a linked DualSense. Turn them on in **Settings → Experimental**.
|
||||
See [Experimental features](docs/playing.md#experimental-features) for what each switch does and setup.
|
||||
- **WebRTC controller connections** keep newer input moving while reliable messages handle controller
|
||||
selection and status. See [why the connection changed](#controller-connection).
|
||||
- **Reconnects after a brief interruption**, reclaiming controllers the service still retains.
|
||||
- **An app on the PS4** that shows the address and a QR code, how many controllers are connected, and starts
|
||||
or stops Control4Free. Use a controller or your TV remote through [HDMI-CEC](docs/hdmi-cec.md).
|
||||
- **Test controllers on the PS4.** Press **Options** in the app to see what each signed-in user's controller
|
||||
sends: buttons, sticks, analog triggers, touchpad and motion. Check connection latency and test rumble,
|
||||
light-bar colours and orientation. See [Testing controllers](docs/playing.md#testing-controllers-on-the-ps4).
|
||||
- **Starts by itself.** One button in the app sets up GoldHEN's AutoRun, so Control4Free is ready after
|
||||
every restart.
|
||||
- **On your phone's home screen**, like an app.
|
||||
- **Stays awake**: your phone's screen doesn't turn off while you play.
|
||||
- **Gets through rest mode.** After the PS4 wakes up, open the page and pick your controller again.
|
||||
|
||||
## Controller connection
|
||||
|
||||
The controller page now connects directly to the PS4 through WebRTC data channels over UDP. Previously,
|
||||
all messages travelled through one ordered
|
||||
WebSocket connection over TCP: if a packet was lost, newer input could wait behind its retransmission.
|
||||
For a controller, that can turn a brief loss into late presses or several updates arriving together.
|
||||
|
||||
WebRTC lets the page use separate channels for different jobs. Controller input is unordered and is not
|
||||
retransmitted; the PS4 accepts newer state and ignores older updates arriving late. Controller selection
|
||||
and status use a reliable channel. A short history of button changes helps recover isolated missed
|
||||
presses, and automatic reconnection restores controllers after a brief interruption.
|
||||
|
||||
The improvement is how input behaves when the connection loses packets. An already healthy connection
|
||||
may have a similar round-trip time. For the implementation and message formats, see
|
||||
[How it works](docs/how-it-works.md#browser-connection) and [Protocol](docs/protocol.md).
|
||||
|
||||
## Quick start
|
||||
|
||||
1. Download `Control4Free-<version>.pkg` from the [latest release](https://github.com/MoHadiShibli/Control4Free/releases/latest).
|
||||
@@ -73,6 +99,10 @@ New to GoldHEN packages? The **[installation guide](docs/installation.md)** walk
|
||||
|---|---|---|
|
||||
|  |  |  |
|
||||
|
||||
| Controller test | Controller tools |
|
||||
|---|---|
|
||||
|  |  |
|
||||
|
||||
## Documentation
|
||||
|
||||
- **[Installation](docs/installation.md)**: requirements, installing, auto-start, updating, uninstalling.
|
||||
@@ -81,7 +111,9 @@ New to GoldHEN packages? The **[installation guide](docs/installation.md)** walk
|
||||
- **[HDMI-CEC navigation](docs/hdmi-cec.md)**: using your TV remote in the PS4 app.
|
||||
- **[Troubleshooting](docs/troubleshooting.md)**: the messages you might see and what to do about each.
|
||||
- **[How it works](docs/how-it-works.md)**: the virtual-device API, sign-in, and what runs where.
|
||||
- **[Protocol](docs/protocol.md)**: the WebSocket protocol between the page and the console.
|
||||
- **[Virtual DS4 controls and values](docs/how-it-works.md#virtual-ds4-controls-and-values)**: individual
|
||||
buttons, sticks, triggers, touch, motion, and motor/light-bar feedback exposed by Control4Free.
|
||||
- **[Protocol](docs/protocol.md)**: the WebRTC channels and messages between the page and the console.
|
||||
- **[Security](SECURITY.md)**: what being open on your home network does and doesn't mean.
|
||||
- **[Contributing](CONTRIBUTING.md)**: building, testing and code style.
|
||||
- **[Changelog](CHANGELOG.md)**.
|
||||
@@ -91,7 +123,8 @@ New to GoldHEN packages? The **[installation guide](docs/installation.md)** walk
|
||||
- A PS4 with GoldHEN v2.4b18.10 or later. Tested on firmware 10.01, 11.02, 13.02, 13.04 and 13.52; the
|
||||
[compatibility table](docs/installation.md#compatibility) lists every firmware and the GoldHEN version it
|
||||
needs.
|
||||
- A phone, tablet or computer with a browser, on the same network as the PS4.
|
||||
- A phone, tablet or computer with a browser supporting WebRTC data channels, on the same network as the PS4.
|
||||
TCP and UDP port 4264 must be reachable; guest Wi-Fi or client isolation can block the connection.
|
||||
- To start the service directly from the app, enable **GoldHEN → Server Settings → Enable PayLoader
|
||||
Server**. AutoRun can start it without this server.
|
||||
- Turn off other GoldHEN controller plugins in games you play with Control4Free. A plugin that takes over a
|
||||
@@ -106,9 +139,11 @@ New to GoldHEN packages? The **[installation guide](docs/installation.md)** walk
|
||||
- **Browser input limits apply.** Hidden buttons and lost input identity cannot be recovered by mapping.
|
||||
With several Xbox controllers, some Chrome/Edge Windows backends combine their Xbox buttons; see the
|
||||
[GameInput workaround](docs/troubleshooting.md#gamepads).
|
||||
- **Six pages at a time.** Up to six phones or computers can have the page open at once.
|
||||
- **No motion controls.** Browsers only give phone motion sensors to secure (`https`) pages, and the
|
||||
console's page is plain `http`.
|
||||
- **Eight connections at a time.** Up to eight phones or computers can connect to the service at once.
|
||||
- **Motion and direct controller lights are experimental.** Browsers only give motion sensors and direct
|
||||
controller access to secure pages, and the console's page is plain `http`. Chrome and Edge can treat it
|
||||
as secure with one setting; Safari on an iPhone can't. Direct controller access works in Chrome and Edge
|
||||
on a computer only. See [Experimental features](docs/troubleshooting.md#experimental-features).
|
||||
- **iPhones don't vibrate.** Safari doesn't let pages use the phone's vibration. A gamepad connected to the
|
||||
iPhone can still rumble, if the browser supports it.
|
||||
- **One controller is added at a time.** If two people pick at once, the second is asked to try again a
|
||||
@@ -116,7 +151,7 @@ New to GoldHEN packages? The **[installation guide](docs/installation.md)** walk
|
||||
- **Rest mode signs everyone out**, so controllers have to be picked again after the PS4 wakes up, just like
|
||||
real ones.
|
||||
- **No pairing or password.** Anyone on your network can take a free controller. Read [SECURITY.md](SECURITY.md)
|
||||
and keep port 4264 off the internet.
|
||||
and keep TCP and UDP port 4264 off the internet.
|
||||
|
||||
## Support
|
||||
|
||||
|
||||
@@ -8,8 +8,10 @@ repository (**Security → Report a vulnerability**), not in a public issue.
|
||||
## What Control4Free assumes
|
||||
|
||||
Control4Free turns a phone or PC into a controller for your PS4 over your own
|
||||
network. There is no pairing, no password and no encryption, by design: a party
|
||||
guest should be able to open a link and play.
|
||||
network. There is no pairing or password: a party guest can open a link and play.
|
||||
The WebRTC channels are encrypted with DTLS. The controller page and connection
|
||||
handshake are served over plain HTTP, so this does not authenticate the console
|
||||
against an attacker who can modify traffic on your network.
|
||||
|
||||
**So treat the console's port 4264 the way you treat your TV remote.** Anyone who
|
||||
can reach your PS4 over the network can:
|
||||
@@ -17,13 +19,13 @@ can reach your PS4 over the network can:
|
||||
- take a free controller and play, on the home screen, at sign-in and in games;
|
||||
- press PS and Share, which reach the system menus;
|
||||
- stop Control4Free, which disconnects every controller;
|
||||
- see the names of the users signed in on the controllers.
|
||||
- see the names and IDs of the users signed in on the controllers, and the reported connection RTT.
|
||||
|
||||
Controller input can operate the PS4's user-selection screen, so someone on your
|
||||
network can select an available user. The PS4's own login restrictions still apply.
|
||||
Control4Free does not stream the screen.
|
||||
|
||||
Run Control4Free on a network you trust. Do not forward port 4264 through your
|
||||
Run Control4Free on a network you trust. Do not forward TCP or UDP port 4264 through your
|
||||
router, and do not run it on open or guest Wi-Fi. When you are finished, stop it
|
||||
from the launcher app or the controller page, or turn the console off.
|
||||
|
||||
@@ -36,26 +38,27 @@ are the limits the code does enforce.
|
||||
visit can make your browser send requests to your PS4, so the service refuses the
|
||||
ones that matter:
|
||||
|
||||
- A WebSocket handshake carrying another site's `Origin` is refused with 403, so an
|
||||
ordinary page's script cannot open the control socket.
|
||||
- A WebRTC offer posted to `/rtc` carrying another site's `Origin` is refused with
|
||||
403, so an ordinary page's script cannot establish the controller connection.
|
||||
- The page is only served for a literal IP address or `localhost` in the `Host`
|
||||
header, so a domain name that resolves to your console (DNS rebinding) gets 403.
|
||||
- The launcher's own `GET /api/status` and `POST /api/stop` require the header
|
||||
`X-Control4Free-Launcher: 1` and refuse any request that carries an `Origin` at
|
||||
all. A browser only sends a custom header cross-origin after a CORS preflight,
|
||||
which this service never answers.
|
||||
which the launcher API never answers.
|
||||
- The page is served with a Content-Security-Policy that keeps it to its own
|
||||
resources.
|
||||
|
||||
The gap: a handshake with `Origin: null` is accepted. That is what a browser sends
|
||||
for a controller page opened from a saved file, and the saved file is how gamepads
|
||||
The gap: an offer with `Origin: null` is accepted, with CORS responses and a
|
||||
preflight for `/rtc`. That is what a browser sends for a controller page opened
|
||||
from a saved file, and the saved file is how gamepads
|
||||
work in browsers that only allow the Gamepad API on secure pages. A website can
|
||||
produce the same `Origin: null` from a sandboxed frame, so a page you have open
|
||||
while on your home network could take a free controller and press buttons, as
|
||||
anyone on your network can. It cannot take a controller someone is using or stream
|
||||
your screen, but its input can operate the user-selection screen.
|
||||
|
||||
**One controller has one owner.** A slot is claimed by one WebSocket connection.
|
||||
**One controller has one owner.** A slot is claimed by one WebRTC connection.
|
||||
Another device asking for the same slot is refused (409), and input never claims a
|
||||
slot implicitly. A client that leaves has its buttons released at once.
|
||||
|
||||
@@ -63,9 +66,20 @@ slot implicitly. A client that leaves has its buttons released at once.
|
||||
are range-checked before they reach the virtual device, and a request that is not
|
||||
well-formed JSON of the expected shape is rejected rather than guessed at.
|
||||
|
||||
**The service cannot be made to grow.** At most 4 controllers and 8 connections
|
||||
exist at a time, each connection has fixed-size buffers, and a request body larger
|
||||
than those buffers is refused.
|
||||
**Connections and buffers are bounded.** At most 4 controllers and 8 WebRTC peers
|
||||
exist at a time. HTTP connections and transport queues also have limits, and an
|
||||
HTTP request larger than the receive buffer is refused.
|
||||
|
||||
**WebRTC validates its peer.** ICE checks use the handshake credentials, and the
|
||||
DTLS certificate must match the SHA-256 fingerprint in the offer. The browser
|
||||
checks the answer's fingerprint too. This ties the encrypted connection to the
|
||||
HTTP exchange; it does not add a player password or protect a modified HTTP page.
|
||||
|
||||
**Connection RTT is diagnostic data.** Each page reports the duration of its own
|
||||
echoed probe. The service matches a recent probe from that connection, associates
|
||||
the result only with controllers it owns, and expires old reports. A modified
|
||||
client can still misreport its own RTT. The value does not grant ownership,
|
||||
change input handling or act as an authentication signal.
|
||||
|
||||
**Stopping from the page is deliberate.** The page's Stop is refused while another
|
||||
device owns a controller, so one player cannot cut another off by accident. The
|
||||
|
||||
@@ -6,6 +6,18 @@
|
||||
Zaitsev, under the MIT license included in `vendor/jsmn/LICENSE`.
|
||||
Retrieved 2026-10-04; SHA-256:
|
||||
`c04533e9181e1e33baceb0f55ac449b05145bb936e8c68cc77dfe0d8277514fb`.
|
||||
- `vendor/mbedtls/` contains the Mbed TLS headers and library sources used for
|
||||
WebRTC's DTLS encryption. The included version header identifies Mbed TLS 3.6.7;
|
||||
upstream reference: [tag `mbedtls-3.6.7`](https://github.com/Mbed-TLS/mbedtls/tree/mbedtls-3.6.7).
|
||||
Its dual Apache-2.0 OR GPL-2.0-or-later license is retained in
|
||||
`vendor/mbedtls/LICENSE`; Control4Free uses the Apache-2.0 option.
|
||||
The SHA-256 of the vendored 126-file source manifest is
|
||||
`cdc9b4a0025cb796015477377dbd3b2c2c4b13118670cd930dfff60ec9ce4daa`.
|
||||
To reproduce this content fingerprint, sort all file paths relative to
|
||||
`vendor/mbedtls/` by ordinal order, with `/` separators. For each file, append
|
||||
its lowercase SHA-256, two spaces, its relative path and a line feed. Hash that
|
||||
UTF-8 manifest without a byte-order mark. This identifies the included subset,
|
||||
rather than an upstream download archive.
|
||||
- The toolchain uses https://github.com/ps4-payload-dev/sdk; its version is pinned
|
||||
in `docker/Dockerfile` and it retains its own licensing.
|
||||
- The native launcher uses the OpenOrbis PS4 Toolchain and its software-display
|
||||
@@ -36,8 +48,13 @@
|
||||
by Tokyoship, licensed under CC BY 3.0
|
||||
(https://creativecommons.org/licenses/by/3.0/). The PS button's mark uses the
|
||||
diagram's paths, resized and recolored; the surrounding buttons are adapted
|
||||
to the interactive page.
|
||||
to the interactive page. The PS4 app's controller test (`launcher/tester_draw.c`)
|
||||
draws the same picture, PS mark included.
|
||||
- The DualSense trigger vibration in `client/index.html` follows the effect format
|
||||
of John "Nielk1" Klein's DualSense trigger effect generator
|
||||
(https://gist.github.com/Nielk1/6d54cc2c00d2201ccb8c2720ad7538db), MIT.
|
||||
|
||||
These license notices are also included inside the package.
|
||||
|
||||
The HTTP/WebSocket transport in `src/net.c` is a new implementation.
|
||||
The HTTP, ICE/STUN and SCTP transport code is implemented in this project;
|
||||
the WebRTC DTLS layer uses Mbed TLS as credited above.
|
||||
@@ -5,6 +5,11 @@ the four virtual DualShock 4 controllers. Wheel and guitar model compatibility r
|
||||
are mapping capabilities, not native accessory emulation. You map on a picture of a DualShock 4. The
|
||||
keyboard is mapped the same way. Mappings are kept in the browser, like your layout.
|
||||
|
||||
An experimentally linked DualShock 4 or DualSense sends its own buttons, sticks and analog trigger
|
||||
pressure directly, so its mapping is bypassed while linked. Its touchpad also sends up to two fingers.
|
||||
Use **Unlink**, reconnecting the controller if needed, to return to browser input and the saved mapping. See
|
||||
[Experimental features](playing.md#experimental-features).
|
||||
|
||||
With several gamepads on Chrome or Edge for Windows, Xbox/Guide presses can be reported on the wrong
|
||||
gamepad by the browser. Check **Gamepad Controls** first; see the
|
||||
[Xbox-button workaround](troubleshooting.md#gamepads). Mapping cannot restore an input's missing source.
|
||||
|
||||
@@ -19,7 +19,7 @@ after a restart without the server; see [Installation](installation.md).
|
||||
|
||||
1. Press **OK** to open **Actions**. A Menu key can also open it if exposed by the PS4.
|
||||
2. Use the arrows to choose an action, then press **OK** again. The menu includes setup/start,
|
||||
auto-start, Stop and Close when those actions are available.
|
||||
auto-start, **Test controllers**, Stop and Close when those actions are available.
|
||||
3. **Stop** opens a separate confirmation with **Cancel** selected. Choose **Stop**, release
|
||||
OK, then press it again to confirm. Stopping disconnects every virtual controller.
|
||||
4. **Back**, when the PS4 exposes it, returns from the menu or cancels confirmation. **Resume**
|
||||
@@ -29,6 +29,17 @@ Existing DualShock 4 shortcuts work outside the menu. Its D-pad can also open an
|
||||
Actions in the normal app. Service operations finish before another operation or Close is
|
||||
available. Release held buttons after opening the app, reconnecting or returning to it.
|
||||
|
||||
## Controller test
|
||||
|
||||
Choose **Actions → Test controllers**. The live picture and values show input received for the selected
|
||||
PS4 user. Press **OK** for the controller-tools grid. Arrows move between its icons and wrap at the
|
||||
edges; **OK** runs the selected tool. **Switch user** cycles through signed-in users. Tools include
|
||||
rumble, light-bar colors and orientation reset; unavailable ones are dimmed and explain why.
|
||||
|
||||
**Resume** or **Back** returns to live input. **Leave test** returns to the launcher; Back from the live
|
||||
test does the same. **Controller page RTT** shows the connection reading for that user's virtual
|
||||
controllers when available. See [Testing controllers](playing.md#testing-controllers-on-the-ps4).
|
||||
|
||||
## Diagnostic app
|
||||
|
||||
**Left/Right** change pages and **Up/Down** scroll while the menu is
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
```
|
||||
phone / PC browser PS4
|
||||
┌──────────────────┐ WebSocket ┌───────────────────────────────────────────────┐
|
||||
┌──────────────────┐ WebRTC ┌───────────────────────────────────────────────┐
|
||||
│ controller page │ ────────────► │ Control4Free service (GoldHEN payload, :4264) │
|
||||
│ touch · keys · │ JSON, LAN │ ├─ scePadVirtualDevice* ─► virtual DS4 ×4 │
|
||||
│ touch · keys · │ JSON, UDP │ ├─ scePadVirtualDevice* ─► virtual DS4 ×4 │
|
||||
│ gamepads │ ◄──────────── │ └─ /dev/klog (only while one signs in) │
|
||||
└──────────────────┘ status │ │
|
||||
│ Control4Free app (home screen) │
|
||||
@@ -16,8 +16,123 @@
|
||||
|
||||
GoldHEN loads plugins into game processes only. The home screen and the sign-in screen belong to the system's
|
||||
own processes, which never load plugins, so a plugin can't reach them. Control4Free is a payload instead, and
|
||||
uses Sony's virtual-device API: the one Remote Play goes through. A device made with it is a real MBus device,
|
||||
and the whole system sees it like a DualShock 4 plugged in over USB.
|
||||
uses Sony's virtual-device API: the one Remote Play goes through. A device made with it is an MBus device
|
||||
that the system reads as a virtual DualShock 4, including on the home and sign-in screens. The individual
|
||||
controller values and Control4Free's access to them are listed below.
|
||||
|
||||
## Virtual DS4 controls and values
|
||||
|
||||
These tables describe the virtual-controller values Control4Free exposes. Input goes into the
|
||||
[`ScePadData` sample](../include/c4f_sce.h) passed to `scePadVirtualDeviceInsertData`; motor and light-bar
|
||||
feedback comes back through `scePadVirtualDeviceGetRemoteSetting`.
|
||||
|
||||
The ranges below are accepted by Control4Free's [input parser](../src/web.c). Names such as `i.lx`,
|
||||
`m.ax` and `v.large` identify positions in message arrays, not JSON object properties. See
|
||||
[Protocol](protocol.md) for the exact array order.
|
||||
|
||||
### Buttons and analog controls
|
||||
|
||||
`0 / 1` means released / pressed. A pressed button sets its bit in `i.buttons`; several bits can be set
|
||||
together. Sticks use `128` as neutral in Control4Free; low X is left and low Y is up.
|
||||
|
||||
| Individual value | Values | `ScePadData.buttons` bit, sent in `i.buttons` |
|
||||
|---|---|---|
|
||||
| D-pad Up | 0 / 1 | `0x0010` |
|
||||
| D-pad Right | 0 / 1 | `0x0020` |
|
||||
| D-pad Down | 0 / 1 | `0x0040` |
|
||||
| D-pad Left | 0 / 1 | `0x0080` |
|
||||
| Square | 0 / 1 | `0x8000` |
|
||||
| Cross | 0 / 1 | `0x4000` |
|
||||
| Circle | 0 / 1 | `0x2000` |
|
||||
| Triangle | 0 / 1 | `0x1000` |
|
||||
| L1 | 0 / 1 | `0x0400` |
|
||||
| R1 | 0 / 1 | `0x0800` |
|
||||
| L2 digital press | 0 / 1 | `0x0100` |
|
||||
| R2 digital press | 0 / 1 | `0x0200` |
|
||||
| L3, left-stick click | 0 / 1 | `0x0002` |
|
||||
| R3, right-stick click | 0 / 1 | `0x0004` |
|
||||
| Share | 0 / 1 | `0x0001` |
|
||||
| Options | 0 / 1 | `0x0008` |
|
||||
| PS | 0 / 1 | `0x10000` |
|
||||
| Touchpad click | 0 / 1 | `0x100000` |
|
||||
|
||||
| Individual value | Accepted values | Native sample field / page field |
|
||||
|---|---|---|
|
||||
| Left stick X | 0–255 | `lx` / `i.lx` |
|
||||
| Left stick Y | 0–255 | `ly` / `i.ly` |
|
||||
| Right stick X | 0–255 | `rx` / `i.rx` |
|
||||
| Right stick Y | 0–255 | `ry` / `i.ry` |
|
||||
| L2 analog pressure | 0–255; 0 released, 255 fully pulled | `l2` / `i.l2` |
|
||||
| R2 analog pressure | 0–255; 0 released, 255 fully pulled | `r2` / `i.r2` |
|
||||
|
||||
L2 and R2 each have a digital button bit and a separate analog-pressure field. A diagonal D-pad direction
|
||||
sets two direction bits together.
|
||||
|
||||
### Touch and motion values
|
||||
|
||||
`touchData.fingers` selects how many contacts are active, up to two. Each contact has its own ID and
|
||||
coordinates; touchpad click remains the separate button above.
|
||||
|
||||
| Individual value | Native sample field | Page field and accepted values |
|
||||
|---|---|---|
|
||||
| Active finger count | `touchData.fingers` | `i.fingers`: 0–2 |
|
||||
| Finger 1 ID | `touchData.touch[0].finger` | First touch `id`: 0–127 |
|
||||
| Finger 1 X | `touchData.touch[0].x` | First touch `x`: 0–1919 |
|
||||
| Finger 1 Y | `touchData.touch[0].y` | First touch `y`: 0–941 |
|
||||
| Finger 2 ID | `touchData.touch[1].finger` | Second touch `id`: 0–127 |
|
||||
| Finger 2 X | `touchData.touch[1].x` | Second touch `x`: 0–1919 |
|
||||
| Finger 2 Y | `touchData.touch[1].y` | Second touch `y`: 0–941 |
|
||||
|
||||
Motion fields in `ScePadData` are floating-point values. The service divides the message's quaternion
|
||||
components by 10000 and its acceleration and angular-velocity components by 1000 before inserting them.
|
||||
|
||||
| Individual value | Native sample field and units | Page field and accepted values |
|
||||
|---|---|---|
|
||||
| Acceleration X | `accel[0]`, G | `m.ax`: −16000…16000 |
|
||||
| Acceleration Y | `accel[1]`, G | `m.ay`: −16000…16000 |
|
||||
| Acceleration Z | `accel[2]`, G | `m.az`: −16000…16000 |
|
||||
| Angular velocity X | `gyro[0]`, rad/s | `m.gx`: −35000…35000 |
|
||||
| Angular velocity Y | `gyro[1]`, rad/s | `m.gy`: −35000…35000 |
|
||||
| Angular velocity Z | `gyro[2]`, rad/s | `m.gz`: −35000…35000 |
|
||||
| Orientation quaternion X | `quat[0]` | `m.qx`: −10000…10000 |
|
||||
| Orientation quaternion Y | `quat[1]` | `m.qy`: −10000…10000 |
|
||||
| Orientation quaternion Z | `quat[2]` | `m.qz`: −10000…10000 |
|
||||
| Orientation quaternion W | `quat[3]` | `m.qw`: −10000…10000 |
|
||||
|
||||
The neutral sample has orientation `[0, 0, 0, 1]` and zero acceleration and angular velocity.
|
||||
|
||||
### Motor and light-bar feedback
|
||||
|
||||
The service [decodes five feedback values](../src/feedback.c) requested by the PS4 game or system.
|
||||
These travel from the PS4 to the page; the page protocol has no request for setting arbitrary feedback.
|
||||
The native app's test tools can exercise the selected user's motors and light bar separately.
|
||||
|
||||
| Individual value | Decoded field and range | Page output |
|
||||
|---|---|---|
|
||||
| Large motor | `C4fPadFeedback.large`: 0–255; 0 stops it | `v.large`: 0–255 |
|
||||
| Small motor | `C4fPadFeedback.small`: 0–255; 0 stops it | `v.small`: 0–255 |
|
||||
| Light-bar red | `C4fPadFeedback.r`: 0–255 | Status `color[0]` |
|
||||
| Light-bar green | `C4fPadFeedback.g`: 0–255 | Status `color[1]` |
|
||||
| Light-bar blue | `C4fPadFeedback.b`: 0–255 | Status `color[2]` |
|
||||
|
||||
The `v` message preserves both 0–255 strengths. For a browser gamepad, the page divides each by 255
|
||||
to use the [browser API's 0–1 scale](https://w3c.github.io/gamepad/); for example, 128 becomes about 0.502.
|
||||
Linked Sony controllers receive the original strength bytes directly.
|
||||
|
||||
Red, green and blue are independent colour channels; for example, `[255, 255, 0]` combines red and green
|
||||
to make yellow. The native tester's RGB selector adjusts each channel independently from 0 to 255.
|
||||
The status colour preserves the RGB ratio but brightens its strongest channel to 255 for display.
|
||||
When the feedback colour is all zero, the page keeps that controller's default colour.
|
||||
|
||||
### Other sample fields and current limits
|
||||
|
||||
The [native insertion code](../src/vda.c) sets `connected` and generates `timestamp` and `count`.
|
||||
These are sample bookkeeping, not page controls. Extension, padding and unknown fields are zero-filled;
|
||||
Control4Free assigns no controls to them.
|
||||
|
||||
Battery/charging status and headset detection are not fields in the virtual pad sample used here.
|
||||
Control4Free also creates no audio device for speaker/headset playback or microphone capture, so
|
||||
connecting a headset to a linked physical controller does not provide PS4 audio through Control4Free.
|
||||
|
||||
## Where it runs
|
||||
|
||||
@@ -32,13 +147,27 @@ GoldHEN's PayLoader doesn't start a process of its own: it runs the ELF inside a
|
||||
payload, and calling into `libScePad` first can kill the process;
|
||||
4. calls `scePadInit()`, then `scePadSetProcessPrivilege(1)`, in that order: the other way round the privilege
|
||||
call fails with *not initialised*;
|
||||
5. serves the page and the WebSocket on port 4264 until it's stopped.
|
||||
5. serves the page and the WebRTC handshake on TCP port 4264, and handles the resulting controller
|
||||
connections on UDP port 4264 until it's stopped.
|
||||
|
||||
## Browser connection
|
||||
|
||||
The page creates two negotiated WebRTC data channels and posts its SDP offer to `POST /rtc`; the service
|
||||
returns an SDP answer over HTTP. ICE checks establish the local UDP path, and DTLS encrypts the SCTP
|
||||
connection. Channel 0 carries reliable, ordered controller selection, status and errors. Channel 1 carries
|
||||
unordered input without retransmissions, rumble and link probes. Both channels share the connection's
|
||||
congestion limits. The page reconnects with a new HTTP offer when the connection fails.
|
||||
|
||||
The service advertises its local address only, with no relay or internet service. Both TCP and UDP port
|
||||
4264 must be reachable. The PS4 app still uses HTTP for its local status and stop requests. See
|
||||
[Protocol](protocol.md) for the handshake and messages, and [Security](../SECURITY.md) for the limits of an
|
||||
HTTP page with encrypted WebRTC channels.
|
||||
|
||||
## Creating a controller
|
||||
|
||||
`scePadVirtualDeviceAddDevice` returns a status, not a handle; on this console it returns `0x803b0006` even
|
||||
when the device is created. The handle is the device's MBus *DeviceId*, and the only place a payload can learn
|
||||
it is the kernel log, where the login manager announces every new device:
|
||||
when the device is created. The service obtains the handle, the device's MBus *DeviceId*, from the kernel
|
||||
log, where the login manager announces every new device:
|
||||
|
||||
```
|
||||
#LOGIN MGR# Receive Event : SCE_MBUS_EVENT_DEVICE_ADDED [DeviceId:0x7030d][type:1][subType:2]
|
||||
@@ -60,7 +189,7 @@ status clears the user and ownership. A failed login-list read leaves controller
|
||||
user is first observed in the list before absence counts as logout. The browser clears affected gamepad
|
||||
selections and returns to its home screen, requiring an explicit choice to reconnect.
|
||||
|
||||
None of this blocks. Creating a controller is a small state machine the main loop advances: drain the log's
|
||||
Creating a controller is a small state machine the main loop advances: drain the log's
|
||||
backlog, write a marker and wait for it to come back (proof that the reader really delivers), call
|
||||
`AddDevice`, wait for the line. The other players keep playing and keep being answered meanwhile.
|
||||
|
||||
@@ -77,34 +206,46 @@ The page sends the whole controller state as a compact array (see [protocol.md](
|
||||
turns it into a `ScePadData` sample and hands it to `scePadVirtualDeviceInsertData`:
|
||||
|
||||
- as soon as it arrives, unless a report went out in the last 4 ms;
|
||||
- every 4 ms while input changed in the last half second, as fast as a real DualShock 4 reports;
|
||||
- every 4 ms while input changed in the last half second;
|
||||
- every 16 ms otherwise, to keep the controller alive;
|
||||
- each sample carries a rising timestamp and counter, as a real pad's do;
|
||||
- a short queue keeps every button press and release, so a quick tap can't fall between two reports, while
|
||||
stick movement only ever keeps the latest position.
|
||||
- a bounded queue holds received button transitions between reports, while unchanged-button samples
|
||||
coalesce to the latest stick position.
|
||||
|
||||
When a connection closes, its buttons are released at once. With no new input for 3 seconds the controller
|
||||
goes neutral, and after 15 it's removed. A page that reconnects forgets which controllers it had; picking the same one again
|
||||
within those 15 seconds gets it back, still signed in.
|
||||
Input includes a per-controller sequence number and up to four recent button changes. The service ignores
|
||||
state older than the newest it applied and can recover missed button changes still present in that history.
|
||||
Motion has its own sequence. Finite history cannot recover every press during a long loss burst, and
|
||||
sequence numbers do not establish the age of packets held up by the network. A radio outage still delays
|
||||
input, and recovered events cannot restore their original timing in a running game.
|
||||
|
||||
When a connection closes, its buttons are released at once. With no new controller-state input for 3 seconds
|
||||
the controller goes neutral, and after 15 it's removed. A page reconnects automatically and tries to reclaim
|
||||
controllers the service still retains, still signed in. Controllers removed after a timeout or PS4 logout
|
||||
require a new selection.
|
||||
|
||||
Gamepad mapping happens on the page, before anything is sent. Each gamepad's inputs go through its profile's
|
||||
bindings, and everything driving one controller is combined: buttons are pressed if any source presses them,
|
||||
triggers take the strongest pull, and sticks take the input pushed furthest. See
|
||||
bindings and calibration. Standard gamepads have a default layout; other devices can be mapped in the
|
||||
editor, with profiles stored in the browser and imported or exported as JSON. Everything driving one
|
||||
controller is combined: buttons are pressed if any source presses them, triggers take the strongest pull,
|
||||
and sticks take the input pushed furthest. Linked Sony HID input keeps its analog trigger pressures
|
||||
separate from its digital trigger flags. See
|
||||
[Gamepad mapping](gamepad-mapping.md).
|
||||
|
||||
The service sends each page a WebSocket ping every 5 seconds and closes a connection that hasn't answered
|
||||
within 10, so a phone that left the network doesn't hold a connection for ever. At most six pages are
|
||||
connected at once, which keeps room for loading the page itself.
|
||||
WebRTC connectivity checks and the page's probes keep the connection active. A peer from which nothing
|
||||
arrives for 10 seconds is closed, so a device that left the network cannot hold a connection indefinitely.
|
||||
At most eight WebRTC peers are connected at once; HTTP requests use separate bounded connection slots.
|
||||
|
||||
## Rumble and light bar
|
||||
|
||||
A game talks to a virtual controller the way it talks to a real one, and the system keeps what it asked for.
|
||||
`scePadVirtualDeviceGetRemoteSetting` reads it back, in the layout of a DualShock 4's own output report: the
|
||||
two motors, then the light bar's red, green and blue. The service reads it every 16 ms for each controller,
|
||||
`scePadVirtualDeviceGetRemoteSetting` reads back the two motor strengths and light-bar red, green and blue
|
||||
values decoded by Control4Free. The service reads it every 16 ms for each controller,
|
||||
because games pulse rumble for only tens of milliseconds:
|
||||
|
||||
- a change in rumble is sent to the controller's owner, whose page buzzes the phone and rumbles any gamepad
|
||||
playing as that controller. Someone who takes a controller over is told its current state at once;
|
||||
playing as that controller. Active rumble is repeated periodically to repair a missed message. Someone
|
||||
who takes a controller over is told its current state at once. The page stops rumble after 650 ms without
|
||||
a renewal, so a lost stop message cannot leave it running indefinitely;
|
||||
- the light bar goes into the status every page sees. The PS4 sets player colours at a quarter of full
|
||||
brightness, so the page brightens them to full and keeps the hue.
|
||||
|
||||
@@ -128,6 +269,19 @@ call (syscall 500, command 2), because the sandbox refuses connections to the co
|
||||
`[AutoRun]` section of `/data/GoldHEN/payloads.ini`, written to a temporary file and renamed over the
|
||||
original so a power cut can't leave it half written.
|
||||
|
||||
The native controller test reads signed-in users' controllers through `scePadReadState`. It samples input
|
||||
on a separate worker with a 4 ms target interval, while the display requests 60 Hz. Static controller
|
||||
artwork and the tools grid are cached; drawing does not block the input worker. The grid offers user
|
||||
switching, orientation reset, motor tests and light-bar tests. Those output actions use the native pad
|
||||
APIs for the selected user's controller.
|
||||
|
||||
The test's **Controller page RTT** line comes from the browser connection, independently of these local
|
||||
pad reads. Every two seconds the page times an echoed input-channel probe and sends the result back to
|
||||
the service. The app reads it through `/api/status`, matches virtual-controller slots to the selected
|
||||
PS4 user and labels them C1 to C4. Reports expire after six seconds, and unavailable data is shown without
|
||||
a number. RTT measures the browser-to-service round trip, including scheduling on that path; it does not
|
||||
measure physical-controller input delay or the TV's display delay.
|
||||
|
||||
## Security
|
||||
|
||||
Being open on the home network is deliberate. What that does and doesn't protect is in
|
||||
|
||||
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 504 KiB After Width: | Height: | Size: 642 KiB |
|
After Width: | Height: | Size: 640 KiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 151 KiB |
|
Before Width: | Height: | Size: 840 KiB After Width: | Height: | Size: 859 KiB |
|
Before Width: | Height: | Size: 561 KiB After Width: | Height: | Size: 672 KiB |
|
Before Width: | Height: | Size: 495 KiB After Width: | Height: | Size: 587 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 967 KiB |
|
After Width: | Height: | Size: 895 KiB |
@@ -6,7 +6,8 @@ Control4Free comes in two pieces:
|
||||
controllers, and serves the controller page on port 4264.
|
||||
- **The app** (`Control4Free-<version>.pkg`), which goes on the PS4's home screen. It starts and stops the
|
||||
service, shows the address and a QR code, and sets up auto-start. The package carries its own copy of the
|
||||
service, so the package is all you need.
|
||||
service, so the package is all you need. **Test controllers** shows received input, motion and the
|
||||
controller-page connection RTT, with tools for rumble, light bar and orientation.
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -17,12 +18,12 @@ Control4Free comes in two pieces:
|
||||
- **PayLoader Server** enabled for starting the service directly from the app:
|
||||
**GoldHEN → Server Settings → Enable PayLoader Server**. AutoRun and Payloader LaunchPad can start the
|
||||
service without this server.
|
||||
- A phone, tablet or computer with a modern browser, on the same network as the PS4.
|
||||
- A phone, tablet or computer with a modern browser supporting WebRTC data channels, on the same network
|
||||
as the PS4. Both TCP and UDP port 4264 must be reachable between them.
|
||||
|
||||
## Compatibility
|
||||
|
||||
Every firmware GoldHEN supports has a GoldHEN version of v2.4b18.10 or later, so updating GoldHEN is all it
|
||||
takes. **Tested** means someone confirmed it on a console; the other rows have GoldHEN support and support in
|
||||
**Tested** means someone confirmed it on a console; the other rows have GoldHEN support and support in
|
||||
Control4Free's start-up code, but nobody has tried them yet.
|
||||
|
||||
| Firmware | GoldHEN | Status |
|
||||
@@ -83,6 +84,7 @@ screen, that's fine too: the app doesn't get in the way of Control4Free.
|
||||
| **Triangle** | Turns auto-start on or off. After you install a newer package, it updates the auto-start copy. |
|
||||
| **Square**, then **Cross** | Stops Control4Free and disconnects every controller. |
|
||||
| **Circle** | Closes the app. Control4Free keeps running without it. |
|
||||
| **Options** | Opens **Test controllers** in the normal app. Hold **L3 + R3** for two seconds as another way in. |
|
||||
|
||||
With a TV remote over HDMI-CEC, press **OK** to open **Actions**, use the arrows to choose,
|
||||
and press **OK** again. **Back** returns. See [HDMI-CEC navigation](hdmi-cec.md) for setup
|
||||
@@ -93,14 +95,17 @@ and compatibility.
|
||||
1. Install the new package over the old one. Delete the old app first if the installer refuses.
|
||||
You don't need to stop Control4Free first: deleting or replacing the app leaves the running service and
|
||||
its auto-start copy alone.
|
||||
2. Open Control4Free. It says the auto-start copy is older than the one in the app: press **Triangle** to
|
||||
update it.
|
||||
2. Fully close the old app if it was left open, then open Control4Free. If it says the auto-start copy
|
||||
differs from the one in the app, press **Triangle** to update it.
|
||||
3. The old version is still the one running. Press **Square**, then **Cross** to stop it, and **Cross**
|
||||
again to start the new one (this needs GoldHEN's PayLoader). Or restart the PS4 and run the jailbreak:
|
||||
auto-start then starts the new one.
|
||||
|
||||
Your controller layouts, keys and gamepad mappings live in each phone's or computer's browser, so they survive updates.
|
||||
|
||||
Reload the controller page after the new service starts. If you kept a saved copy, load the page from
|
||||
the updated service and save a fresh copy so it includes the current connection and controller features.
|
||||
|
||||
## Without the app
|
||||
|
||||
The service is an ordinary GoldHEN payload, so the app is optional.
|
||||
|
||||
@@ -15,6 +15,10 @@ Signing out of that user on the PS4 disconnects their virtual controllers. The p
|
||||
returns to the home screen, and switches gamepads assigned to those controllers to **Off**. Pick a
|
||||
controller again to reconnect. Other signed-in players keep playing.
|
||||
|
||||
After a brief network interruption, leave the page open: it reconnects and tries to reclaim its
|
||||
controllers while the service still retains them. If they have been removed or another device owns them,
|
||||
pick a free controller again. Rest mode signs users out, so it also requires a new selection.
|
||||
|
||||
The home screen shows all four controllers live:
|
||||
|
||||
| State | Meaning |
|
||||
@@ -23,7 +27,7 @@ The home screen shows all four controllers live:
|
||||
| **Connecting** | The PS4 is setting it up. Takes a moment. |
|
||||
| **Choose user on PS4** | Waiting for someone to pick a user on the PS4's screen. |
|
||||
| **Connected** | Signed in and ready. The tile shows the user's name. |
|
||||
| **Paused** | Its phone stopped sending input, for example because the screen locked. |
|
||||
| **Paused** | Its phone or PC stopped sending input, for example because the screen locked. |
|
||||
| **This device** / **+1 device** | Who's driving it: this browser, or other ones. |
|
||||
|
||||
## Inviting someone
|
||||
@@ -78,6 +82,10 @@ appears under **Gamepads on this device**:
|
||||
|
||||
Keep the page's window in front: browsers only read gamepads for the active window.
|
||||
|
||||
Without a direct link, a DualShock 4 or DualSense provides its touchpad click through the browser's
|
||||
gamepad input. Link it under [Experimental features](#experimental-features) to send the positions of
|
||||
up to two fingers as well.
|
||||
|
||||
With several Xbox controllers on Chrome or Edge for Windows, check that each Xbox button appears on its
|
||||
own gamepad in **Gamepad Controls**. Some browser backends combine those presses; see the
|
||||
[GameInput workaround](troubleshooting.md#gamepads). This is not a general Control4Free requirement.
|
||||
@@ -87,10 +95,15 @@ pages, and the console's page is plain `http`. Save the page to the device (**Sa
|
||||
open the saved file, and enter the console's address in its connection box. Touch and the keyboard work either
|
||||
way.
|
||||
|
||||
After updating Control4Free, load the page from the updated service before saving it again. A saved page
|
||||
keeps its old connection code until you replace it. If the browser asks for
|
||||
local-network access, allow it for the controller page so it can reach your PS4.
|
||||
|
||||
## Settings
|
||||
|
||||
**☰ → All settings** has this device's settings. They stay in this browser.
|
||||
**Gamepads** in the settings changes each gamepad's mapping, and **Keyboard** the keys.
|
||||
**Gamepads** in the settings changes each gamepad's mapping, and **Keyboard** the keys. **Experimental** has
|
||||
extras that stay off until you turn them on: see [Experimental features](#experimental-features).
|
||||
|
||||
| Setting | What it does |
|
||||
|---|---|
|
||||
@@ -101,8 +114,8 @@ way.
|
||||
| **Double-tap a stick for L3 / R3** | Off: L3 and R3 get their own buttons. |
|
||||
| **Show keyboard keys** | Labels the buttons with their keyboard keys. |
|
||||
|
||||
The bottom of the home screen shows the version, and the round trip to the console in milliseconds: a quick
|
||||
way to see whether your Wi-Fi is the slow part.
|
||||
The bottom of the home screen shows the version and the round trip to the console in milliseconds. It
|
||||
helps spot network delays, but it does not measure the full delay from a button press to a game's response.
|
||||
|
||||
## On your phone's home screen
|
||||
|
||||
@@ -127,16 +140,94 @@ When a game rumbles the controller, the device driving it feels it:
|
||||
- **a gamepad** playing as that controller rumbles too, though not every controller supports vibration in
|
||||
the browser. When the browser knows it can't, the gamepad's row says *no rumble in this browser*.
|
||||
|
||||
A gamepad's own light bar stays as it is: browsers don't let pages change it.
|
||||
A gamepad's own light bar stays as it is, unless you link a DualShock 4 or DualSense under
|
||||
[Experimental features](#experimental-features).
|
||||
|
||||
The page also glows in the controller's **light-bar colour**: the player colour the PS4 gives each user, or
|
||||
whatever colour the game sets. Until one is set, a controller shows its own colour (1 blue, 2 red, 3 green,
|
||||
4 pink).
|
||||
|
||||
## Experimental features
|
||||
|
||||
**☰ → All settings → Experimental** has three switches. Each stays off until you turn it on, and is saved in
|
||||
this browser only. With all three off, the page works exactly as it does without them.
|
||||
|
||||

|
||||
|
||||
| Switch | What it does | Where it works |
|
||||
|---|---|---|
|
||||
| **Lights and rumble on Sony controllers** | Link a DualShock 4 or DualSense for its light bar, direct rumble and up to two touchpad fingers. While linked, its buttons, sticks and analog triggers come directly from the controller; its **Mapping** is bypassed. | Chrome or Edge on a computer |
|
||||
| **Trigger rumble** | Adds an effect to the trigger you're pulling, derived from the game's normal rumble. See [Trigger rumble](#trigger-rumble) below for setup and behavior. | Xbox controllers with browser trigger-rumble support; a linked DualSense |
|
||||
| **Motion controls** | Tilting the phone, or a linked DualShock 4 or DualSense, moves the PS4 controller's motion sensors. Hold the phone sideways with the screen towards you, like a controller. **Recenter** makes the way you hold it now the resting position. | Chrome on Android; a linked controller on a computer |
|
||||
|
||||
For **Lights and rumble** or **Motion controls**, first follow the short
|
||||
[Chrome/Edge flag setup](troubleshooting.md#experimental-features). Then turn the switch on. For a Sony
|
||||
controller, press **Link**, select it in the browser's device list, and allow access. For phone motion,
|
||||
allow the sensor prompt if shown and press **Recenter** while holding the phone still. A linked Sony
|
||||
controller can provide motion too.
|
||||
|
||||
### Trigger rumble
|
||||
|
||||
Turn on **Trigger rumble** and assign the gamepad to the controller you are playing as. An Xbox
|
||||
controller uses the browser's trigger-rumble support; it does not need the Sony **Link** button.
|
||||
For a DualSense, also turn on **Lights and rumble on Sony controllers** and **Link** it.
|
||||
|
||||
The effect follows normal rumble from the PS4 game and the physical trigger you are pulling:
|
||||
|
||||
- Pull only L2 or R2 and that trigger receives the effect. With both held, the one pulled last receives
|
||||
it. Release that trigger while keeping the other pulled and the effect moves to the other one.
|
||||
- With neither trigger pulled, neither receives an effect. The game must be rumbling for there to be
|
||||
anything to feel.
|
||||
- Stronger game rumble produces stronger trigger vibration. A linked DualSense pulses more slowly for
|
||||
heavy rumble and faster for light rumble.
|
||||
- Selection follows the gamepad's physical triggers, before any mapping. Remapping an unrelated button
|
||||
to L2 does not make that button select a trigger effect.
|
||||
|
||||
On a DualSense this uses a vibration effect in its adaptive triggers. Control4Free generates it from
|
||||
ordinary PS4 rumble; it does not reproduce a game's authored adaptive resistance or PS5 haptics.
|
||||
It changes the feedback you feel, not the analog L2/R2 pressure sent to the game. A DualShock 4 keeps
|
||||
its normal rumble and has no separate trigger effect.
|
||||
|
||||
For a quick check, use a game that rumbles while you pull a trigger, or select the same user in the PS4
|
||||
app's **Test controllers** and run **Large motor** or **Small motor** while holding a trigger.
|
||||
|
||||
## Testing controllers on the PS4
|
||||
|
||||
The app on the PS4 shows what each signed-in user's controller sends, read the way a game reads it. Press
|
||||
**Options** in the app, or choose **Actions → Test controllers** with the TV remote.
|
||||
|
||||
- The picture lights up each button as it's pressed, moves the sticks, fills L2 and R2 as far as they're
|
||||
pulled, and shows each finger on the touchpad. The panel beside it has the numbers, motion included.
|
||||
- **PS** and **Share** go to the PS4 itself, so the test doesn't show them.
|
||||
- Hold **L3 + R3** for two seconds for its menu: choose another signed-in user, rumble the large or the small
|
||||
motor, choose a light-bar colour, reset it, reset the orientation, or go back. The light bar
|
||||
goes back to normal when you leave.
|
||||
- The eight tools are arranged in a four-column, two-row grid. Move with the D-pad or TV remote arrows and choose with **Cross / OK**;
|
||||
**Circle / Back** returns to the live readings. Arrows wrap at the grid edges, and holding a
|
||||
controller's D-pad repeats navigation.
|
||||
Choose **Switch user** to cycle through signed-in users. Unavailable tools are dimmed and explain why
|
||||
they cannot run. Release the stick buttons that opened the menu before activating a tool.
|
||||
- **Light bar** opens RGB sliders with a colour preview. **Up / Down** chooses red, green or blue;
|
||||
**Left / Right** adjusts that channel from 0 to 255 and previews it on the selected controller.
|
||||
**Cross / OK** switches between fast adjustments of 16 and precise adjustments of 1. **Circle / Back**
|
||||
returns to the tools, keeping the test colour active. **Reset light** restores the PS4's normal colour;
|
||||
leaving the tester or suspending the app also restores it.
|
||||
|
||||

|
||||
- The tools view replaces the live picture while open. **Resume** returns to live buttons, sticks, touch
|
||||
and motion. Controller sampling continues independently of the screen drawing.
|
||||
- A controller from the page shows up under the user signed in on it, so the test also checks the page, a
|
||||
gamepad or a keyboard from the PS4's side.
|
||||
- **Controller page RTT** shows the connection round trip measured by that page, in milliseconds, in
|
||||
both the live view and tools. Controller numbers identify each virtual controller for the selected
|
||||
user. It needs the updated service and a freshly loaded or saved controller page. Missing readings
|
||||
show as measuring or unavailable; old values expire. A local physical controller has no controller-page
|
||||
connection to measure. RTT does not include the full controller, game and TV delay.
|
||||
|
||||
## Leaving
|
||||
|
||||
- **☰ → Disconnect controller** removes it from the PS4 straight away.
|
||||
- **Closing the page or locking the phone** releases its buttons at once. After 3 seconds without input the
|
||||
- **Closing the page or locking the phone** normally releases its buttons. After 3 seconds without input the
|
||||
controller goes idle, and after 15 seconds it's removed. Pick it again within those 15 seconds and you keep
|
||||
it, still signed in.
|
||||
- **☰ → Stop Control4Free** stops the service and disconnects everyone. It's refused while someone else is
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
# Protocol
|
||||
|
||||
The service listens on TCP port 4264 and speaks plain HTTP/1.1 and WebSocket (RFC 6455). This page is for
|
||||
anyone writing their own client or checking what the page does.
|
||||
The service uses TCP port 4264 for plain HTTP/1.1 and UDP port 4264 for WebRTC data channels. This page is
|
||||
for anyone writing their own client or checking what the controller page does. Replies include
|
||||
`protocol: 3`, an internal message-compatibility integer checked by the controller page, separate from
|
||||
the application version and the WebRTC transport. The launcher HTTP API uses `api: 1`.
|
||||
For the native input fields and feedback values Control4Free exposes, see
|
||||
[Virtual DS4 controls and values](how-it-works.md#virtual-ds4-controls-and-values).
|
||||
|
||||
## HTTP
|
||||
|
||||
@@ -10,19 +14,50 @@ anyone writing their own client or checking what the page does.
|
||||
| `GET /`, `GET /index.html`, `GET /?…` | The controller page, gzip-compressed, with a Content-Security-Policy. |
|
||||
| `GET /manifest.webmanifest` | The web app manifest, for a home-screen shortcut. |
|
||||
| `GET /icon-192.png` | The app icon. |
|
||||
| `GET /ws` with `Upgrade: websocket` | The control socket, below. |
|
||||
| `POST /rtc` | A WebRTC SDP answer to the offer in the request body, as `text/plain`. |
|
||||
| `OPTIONS /rtc` | A saved-page preflight, below. |
|
||||
| `GET /api/status`, `POST /api/stop` | The launcher API, below. |
|
||||
| Anything else | `404`. |
|
||||
|
||||
Every request needs a `Host` header that is a literal IPv4 address or `localhost`, with or without a port;
|
||||
anything else gets `403`. This keeps out domain names that resolve to the console (DNS rebinding).
|
||||
|
||||
## The control socket
|
||||
## Establishing a WebRTC connection
|
||||
|
||||
`GET /ws` upgrades to a WebSocket. The handshake is refused with `403` when it carries an `Origin` other than
|
||||
`http://<the Host header>` or `null`; no `Origin` at all is accepted. Messages are UTF-8 JSON text frames of at
|
||||
most 4096 bytes; a longer frame closes the connection. Fragmented messages and ping/pong control frames are
|
||||
handled.
|
||||
1. Create an `RTCPeerConnection` with no STUN or TURN servers. Create the two channels below with
|
||||
`negotiated: true` before making the offer.
|
||||
2. Create an SDP offer and set it as the local description. The ICE-lite service supplies its host
|
||||
candidate in the answer, so the page does not need to wait for local candidate gathering to finish.
|
||||
3. POST the local description's SDP to `http://<PS4-address>:4264/rtc` with `Content-Type: text/plain`.
|
||||
This is raw SDP, not a JSON object. A `Content-Length` is required; `Transfer-Encoding` is refused.
|
||||
Headers and body together must fit the 8 KiB HTTP receive buffer.
|
||||
4. Set the returned text as the remote description with type `answer`, then wait for both channels to open.
|
||||
|
||||
| Channel | ID | Options | Messages |
|
||||
|---|---|---|---|
|
||||
| `control` | 0 | `negotiated: true`, `ordered: true`; no partial-reliability limit | Requests and replies for selection, status, errors and other control methods. |
|
||||
| `input` | 1 | `negotiated: true`, `ordered: false`, `maxRetransmits: 0` | Controller state `i`, motion `m`, link probes `e`, optional `latency` telemetry, and rumble `v`. |
|
||||
|
||||
No DCEP channel-opening exchange is used. The service is ICE-lite and returns one IPv4 host candidate on
|
||||
UDP port 4264. The browser performs ICE checks, DTLS and SCTP establishment. DTLS certificate fingerprints
|
||||
use SHA-256; the service checks the browser's certificate against its offer. No media, STUN discovery
|
||||
service or TURN relay is involved. See [SECURITY.md](../SECURITY.md) for what the unencrypted HTTP exchange
|
||||
means for authentication.
|
||||
|
||||
`POST /rtc` is refused with `403` when its `Origin` is other than `http://<the Host header>` or `null`;
|
||||
an absent `Origin` is accepted. For `Origin: null`, the answer includes `Access-Control-Allow-Origin: null`.
|
||||
`OPTIONS /rtc` accepts only `Origin: null` and returns `204`, allowing `POST`, `Content-Type` and private-network
|
||||
access. Other endpoints do not inherit this CORS permission. Browser local-network permission rules can
|
||||
still apply to a saved page.
|
||||
|
||||
Malformed offers get `400`; an oversized request gets `413`; a full or unavailable service gets `503`.
|
||||
At most eight WebRTC peers can exist at once. An unfinished setup expires after 20 seconds, and an established peer is
|
||||
closed after 10 seconds without incoming traffic. The page creates a new HTTP offer when reconnecting.
|
||||
|
||||
Channel messages are UTF-8 JSON strings, with an application limit of 4096 bytes. Input is unordered and
|
||||
not retransmitted by the data-channel sender; it is not guaranteed to arrive. Control is reliable and
|
||||
ordered while the connection remains usable. They share one SCTP association and its congestion limits;
|
||||
separate channel ordering does not remove network delay or packet loss.
|
||||
|
||||
### Requests
|
||||
|
||||
@@ -31,21 +66,24 @@ handled.
|
||||
```
|
||||
|
||||
- `method`: a string.
|
||||
- `params`: an array of up to 16 integers; required, even if empty.
|
||||
- `params`: an array of up to 32 integers; required, even if empty.
|
||||
- `id`: optional, an integer from 0 to 2^53−1. Requests with an `id` get a reply carrying the same `id`.
|
||||
- `jsonrpc: "2.0"` is allowed and ignored. Any other key, or anything that isn't this shape, is answered with
|
||||
an `Invalid request` error.
|
||||
|
||||
Controllers are numbered 0 to 3 in the protocol and 1 to 4 on screen.
|
||||
`<version>` in the examples is a placeholder for the service's version, read from `VERSION`.
|
||||
The control protocol remains version 2 and the launcher API remains version 1.
|
||||
Send methods on `control` unless their channel is identified below.
|
||||
|
||||
| Method | Params | Reply |
|
||||
|---|---|---|
|
||||
| `info` | `[]` | `{"version": "<version>", "protocol": 2, "pads": 4}` |
|
||||
| `info` | `[]` | `{"version": "<version>", "protocol": 3, "pads": 4, "motion": true, "input": true, "latency": true}` |
|
||||
| `status` | `[]` | A status object, below. |
|
||||
| `claim` | the controllers this connection wants, for example `[0]` or `[0, 2]` | A status object, once every controller in the claim exists. |
|
||||
| `u` | input, below | None. |
|
||||
| `i` | sequenced input, below, on `input` | None. |
|
||||
| `m` | sequenced motion, below, on `input` | None. |
|
||||
| `e` | `[sequence, clientTime]`, on `input` | The same parameters as an `e` message on `input`. |
|
||||
| `latency` | `[probeSequence, rttMs]`, on `input`, when `info.latency` is true | None. Informational RTT for the PS4 tester. |
|
||||
| `leave` | `[pad]` | A status object. |
|
||||
| `stop` | `[]` | None; the service stops and the connection closes. |
|
||||
| `ping` | `[]` | `{}` |
|
||||
@@ -57,21 +95,35 @@ usually within a second; the service keeps serving every other connection in the
|
||||
changing nothing, if any controller in it belongs to another connection (`409`), if another controller is
|
||||
being created at that moment (`409`), or if controllers can't be created (`503`).
|
||||
|
||||
**`u`** sends the complete state of one controller this connection owns. Input for any other controller is
|
||||
**`i`** sends the complete state of one controller this connection owns. Input for any other controller is
|
||||
ignored: input never claims a controller.
|
||||
|
||||
```
|
||||
[pad, buttons, lx, ly, rx, ry, l2, r2, fingers, (id, x, y) × fingers]
|
||||
[pad, seq, buttons, lx, ly, rx, ry, l2, r2, fingers, (id, x, y) × fingers,
|
||||
historyCount, (eventSeq, eventButtons) × historyCount]
|
||||
```
|
||||
|
||||
| Field | Range |
|
||||
|---|---|
|
||||
| `seq` | a nonnegative sequence number that increases for each input message for this controller |
|
||||
| `buttons` | a bit mask, below |
|
||||
| `lx`, `ly`, `rx`, `ry` | 0 to 255, 128 is centred; up and left are 0 |
|
||||
| `l2`, `r2` | 0 to 255 |
|
||||
| `fingers` | 0 to 2 touches on the touchpad |
|
||||
| touch `id` | 0 to 127, stable while the finger stays down |
|
||||
| touch `x`, `y` | 0 to 1919, 0 to 941 |
|
||||
| `historyCount` | 0 to 4 recent button changes |
|
||||
| `eventSeq`, `eventButtons` | the input sequence and complete button mask at that change |
|
||||
|
||||
Use integers exactly representable in JavaScript, at most 2^53−1. Keep button history in increasing sequence
|
||||
order; each entry must precede the current `seq`. Once an input sequence has been applied, messages at or
|
||||
below it are ignored. On a later accepted message, history entries newer than the previous applied
|
||||
sequence are queued before its current state. History is not replayed before the first accepted state.
|
||||
|
||||
History carries button masks, not the original timestamps or analog positions. Recovery uses the current
|
||||
message's sticks, triggers and touches. It can repair some isolated losses but cannot guarantee every press,
|
||||
preserve its original timing, or reconstruct an analog sequence through a long loss burst. Retain a
|
||||
periodic complete-state message so a final release can be repaired even when nothing changes afterwards.
|
||||
|
||||
| Button | Bit | Button | Bit |
|
||||
|---|---|---|---|
|
||||
@@ -85,15 +137,47 @@ ignored: input never claims a controller.
|
||||
| D-pad left | `0x0080` | Square | `0x8000` |
|
||||
| PS | `0x10000` | Touchpad click | `0x100000` |
|
||||
|
||||
Send the state whenever it changes, and at least once a second while the controller is in use. After 3
|
||||
seconds without input the controller goes neutral and shows as `paused`; after 15 it's removed. When a
|
||||
connection closes, its controllers' buttons are released at once, and they're removed 15 seconds later unless
|
||||
a connection claims them again. The page doesn't do that by itself after reconnecting: the player picks the
|
||||
controller again.
|
||||
**`m`** sends the motion sensors of one controller this connection owns. Like `i`, it is ignored for any
|
||||
other controller.
|
||||
|
||||
At most six WebSocket connections are open at once; a seventh handshake gets `503`. The service pings each
|
||||
connection every 5 seconds and closes it if the matching pong hasn't come back within 10. Browsers answer
|
||||
pings by themselves; another client has to answer them too. Other messages don't count as an answer.
|
||||
```
|
||||
[pad, seq, qx, qy, qz, qw, ax, ay, az, gx, gy, gz]
|
||||
```
|
||||
|
||||
| Field | Range |
|
||||
|---|---|
|
||||
| `seq` | an increasing nonnegative motion sequence, separate from `i` |
|
||||
| `qx`, `qy`, `qz`, `qw` | orientation as a quaternion, times 10000: −10000 to 10000 |
|
||||
| `ax`, `ay`, `az` | acceleration in thousandths of a G: −16000 to 16000 |
|
||||
| `gx`, `gy`, `gz` | angular velocity in thousandths of a radian per second: −35000 to 35000 |
|
||||
|
||||
The axes are the ones the PS4 reads a DualShock 4 in: x to the right, y up out of its face, z towards the
|
||||
player. A controller lying flat and still reads `+1` G on y. The values go into every sample from then on,
|
||||
together with the buttons, sticks and touches of the last `i`. Before the first `m`, and whenever the
|
||||
controller goes neutral, it reports no motion: orientation
|
||||
`0, 0, 0, 1` and everything else 0. Late or duplicate motion sequences are ignored. Motion messages do not
|
||||
renew the controller-state input lease: continue sending `i` while the controller is in use. A service
|
||||
that understands `m` says
|
||||
`"motion": true` in its `info` reply.
|
||||
|
||||
Send the state whenever it changes, and at least once a second while the controller is in use; the page sends
|
||||
complete-state keepalives after one second without a change. After 3 seconds without controller-state input the controller goes
|
||||
neutral and shows as `paused`; after 15 it's removed. When a
|
||||
connection closes, its controllers' buttons are released at once, and they're removed 15 seconds later unless
|
||||
a connection claims them again. The page reconnects and tries to reclaim its retained controllers. PS4
|
||||
logout or controller removal clears the selection instead. Input and motion sequence state is maintained
|
||||
for the current owner; a different owner starts a new sequence context.
|
||||
|
||||
`e` probes are echoed over the input channel for round-trip measurements. The client time
|
||||
is returned unchanged; it is not compared with a PS4 clock and does not impose an input freshness deadline.
|
||||
The page sends a probe every two seconds while connected and calculates RTT using its own monotonic clock.
|
||||
|
||||
When `info.latency` is true, a client may send `latency: [probeSequence, rttMs]` after receiving an echo.
|
||||
Use a nonnegative, increasing probe sequence no greater than 2^53−1 and an RTT from 0 to 60000 milliseconds.
|
||||
The service accepts one report matching this connection's most recently echoed probe, within six seconds
|
||||
of that echo; unmatched, repeated, expired or invalid reports are ignored. This optional diagnostic method
|
||||
does not acknowledge controller input or renew its lease. The number is supplied by the client, not
|
||||
independently measured or authenticated by the PS4.
|
||||
|
||||
While a claim is being answered, every controller in it is reserved: another `claim` from the same
|
||||
connection, or one that wants any of those controllers, gets `409`, and so does `leave` from that connection.
|
||||
@@ -110,10 +194,10 @@ modules around it when you draw it.
|
||||
```json
|
||||
{
|
||||
"version": "<version>",
|
||||
"protocol": 2,
|
||||
"protocol": 3,
|
||||
"pads": [
|
||||
{"pad": 0, "name": "Controller 1", "user": "Alex", "enabled": true, "open": true, "connected": true, "clients": 1,
|
||||
"mine": true, "state": "ready", "uid": "1a2b3c4d", "color": [32, 96, 255], "reports": 5120, "error": 0}
|
||||
"mine": true, "state": "ready", "uid": "1A2B3C4D", "color": [32, 96, 255], "reports": 5120, "error": 0}
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -137,9 +221,12 @@ PS4 user logout removes that user's virtual controllers. Their status becomes `f
|
||||
`connected` and `mine` false, `clients: 0`, and an empty `user`. Owners receive `[pad, 0, 0]` to stop rumble
|
||||
and a disconnection message. Pages clear the affected selections instead of automatically claiming them.
|
||||
|
||||
- `{"method": "s", "params": <status>}`: the status, whenever something changes and at least once a second.
|
||||
- `{"method": "v", "params": [pad, large, small]}`: rumble, 0 to 255 for each motor, sent to the controller's
|
||||
owner when it changes and when a connection takes the controller over. `[pad, 0, 0]` stops it.
|
||||
- `{"method": "s", "params": <status>}` on `control`: the status, whenever something changes and at least once a second.
|
||||
- `{"method": "v", "params": [pad, large, small]}` on `input`: rumble, 0 to 255 for each motor, sent to the controller's
|
||||
owner when it changes and when a connection takes the controller over. Nonzero rumble is repeated every
|
||||
200 ms. `[pad, 0, 0]` stops it; messages can be lost on this channel. The page stops rumble if no renewal
|
||||
arrives for 650 ms, including when a final stop message is lost.
|
||||
- `{"method": "e", "params": [sequence, clientTime]}` on `input`: the reply to a link probe.
|
||||
- `{"method": "error", "params": {"message": "…"}}`: a problem that isn't the reply to a request: a controller
|
||||
removed for sitting unused or PS4 user logout, a request that couldn't be parsed, or an error for a request sent without an
|
||||
`id`.
|
||||
@@ -161,11 +248,33 @@ Errors in reply to a request:
|
||||
|
||||
For the app on the PS4. Both requests need the header `X-Control4Free-Launcher: 1`, and are refused with `403`
|
||||
if they carry an `Origin`, an `Upgrade`, a body or `Transfer-Encoding`. A browser only sends a custom header
|
||||
cross-origin after a CORS preflight, which the service never answers, so websites can't use these.
|
||||
cross-origin after a CORS preflight, which these API endpoints never answer, so websites can't use them.
|
||||
|
||||
`GET /api/status` returns:
|
||||
|
||||
```json
|
||||
{
|
||||
"application": "Control4Free", "api": 1, "version": "<version>",
|
||||
"controllers": 1, "stopping": false, "latency": true,
|
||||
"pad0User": 439041101, "pad0Rtt": 8, "pad0Age": 120,
|
||||
"pad1User": -1, "pad1Rtt": -1, "pad1Age": -1,
|
||||
"pad2User": -1, "pad2Rtt": -1, "pad2Age": -1,
|
||||
"pad3User": -1, "pad3Rtt": -1, "pad3Age": -1
|
||||
}
|
||||
```
|
||||
GET /api/status → {"application": "Control4Free", "api": 1, "version": "<version>", "controllers": 2, "stopping": false}
|
||||
POST /api/stop → {"application": "Control4Free", "stopping": true}
|
||||
```
|
||||
|
||||
The flat `padN` fields use zero-based slots. `padNUser` is the numeric PS4 user ID for an active,
|
||||
signed-in controller with an owner and fresh input; otherwise it is `-1`. `padNRtt` is that owner's
|
||||
latest accepted RTT, or `-1` when none is fresh. `padNAge` is the number of milliseconds since the service
|
||||
echoed the measured probe, or `-1` with an unknown RTT. A page owning multiple controllers supplies one
|
||||
connection RTT shared by those slots.
|
||||
|
||||
The service expires RTT measurements six seconds after their probe and requires a new probe after controller
|
||||
neutralization or ownership changes. The launcher also accounts for the report's age and time since
|
||||
its HTTP check. It shows every matching slot for the selected PS4 user: a fresh number, `measuring...`
|
||||
without a fresh report, or no controller page when no slot matches. An unavailable service or one
|
||||
without these optional fields produces `--`; it does not assign a network RTT to a local controller.
|
||||
|
||||
`POST /api/stop` returns `{"application": "Control4Free", "stopping": true}`.
|
||||
|
||||
`/api/stop` removes every controller, even ones in use, and the service exits a quarter of a second later.
|
||||
@@ -48,6 +48,19 @@ see [Updating](installation.md#updating).
|
||||
- If the address worked before and doesn't now, the router may have given the PS4 a new one. Reserving an
|
||||
address for the PS4 in the router's settings keeps it the same.
|
||||
|
||||
## The page opens but cannot connect
|
||||
|
||||
The page loads over HTTP, then opens its controller connection over WebRTC. Loading successfully does not
|
||||
prove that the controller connection can get through.
|
||||
|
||||
- Reload the page from the PS4. After an update, replace saved copies so they use the current connection code.
|
||||
- Use a browser with WebRTC data channels enabled. If the browser asks for local-network access, allow
|
||||
it for the controller page.
|
||||
- Both TCP and UDP port 4264 must be reachable between the phone or PC and the PS4. Guest networks,
|
||||
client isolation, VPNs or firewall rules can allow page loading while blocking the controller connection.
|
||||
Keep these ports on your own network; internet port forwarding is not needed.
|
||||
- Close unused controller pages if the service has reached its connection limit.
|
||||
|
||||
## Picking a user
|
||||
|
||||
**The *"Who's using this controller?"* screen doesn't appear.** Wait a few seconds after picking the
|
||||
@@ -65,13 +78,25 @@ way.
|
||||
**A game ignores the virtual controller.** Turn off other GoldHEN controller plugins for that game. A plugin
|
||||
that takes over a signed-in user's controller can stop games from reading Control4Free's.
|
||||
|
||||
**Buttons feel slow.** The number next to the version at the bottom of the home screen is the round trip to
|
||||
the console. Above about 30 ms, your Wi-Fi is the slow part: move closer to the router or use 5 GHz.
|
||||
**Buttons feel slow, arrive late or stay held briefly.** The number next to the version at the bottom of
|
||||
the home screen is the round trip to the console, not the full button-to-game delay. Keep the controller
|
||||
page visible and the device awake. Note whether the problem happens continuously or in short bursts, the
|
||||
devices and browser used, and whether motion or linked controllers were enabled when reporting it.
|
||||
|
||||
**No vibration from the game.** Check **Vibration** is on in the settings. Only Android phones can vibrate:
|
||||
Safari on iPhone doesn't let pages do it. Not every controller supports vibration in the browser, and one
|
||||
whose row says *no rumble in this browser* can't be rumbled from the page at all. Keep the page's window in
|
||||
front. And not every game rumbles every player.
|
||||
For bursts of lag, ghost inputs or brief held buttons, connect the **PS4 to the router by wired LAN**
|
||||
first. The phone or PC can stay on Wi-Fi. This removes the PS4's wireless link from the path and is a
|
||||
useful first check even when the usual reading is only a few milliseconds. Compare the same game and
|
||||
devices before changing other settings.
|
||||
|
||||
If bursts remain with the PS4 wired, check the phone or PC's Wi-Fi signal and compare another device or
|
||||
browser. Report whether the RTT number spikes at the same time and whether the issue also occurs with
|
||||
Control4Free stopped; that helps separate connection trouble from the controller or game.
|
||||
|
||||
**No vibration from the game.** For an Android phone, turn on **Vibration** in the settings; Safari on
|
||||
iPhone cannot vibrate the phone. For a gamepad, check its assigned controller and keep the page in front.
|
||||
A gamepad marked *no rumble in this browser* needs another supported browser or, for a Sony controller,
|
||||
the experimental direct link. The phone's **Vibration** switch does not control gamepad rumble.
|
||||
Not every game rumbles every player.
|
||||
|
||||
## Gamepads
|
||||
|
||||
@@ -149,16 +174,57 @@ the app and open it again; if that doesn't help, restart the PS4 and run the jai
|
||||
|
||||
**"The bundled payload is damaged."** Reinstall the package.
|
||||
|
||||
**Controller page RTT is measuring or unavailable.** Update and start the service bundled with the app,
|
||||
then reload the controller page from the PS4; replace any saved copy too. Select the user signed in on
|
||||
that page's controller. A local physical controller has no page connection to measure. The reading
|
||||
expires when reports stop; it measures the page's connection round trip, not the full game or TV delay.
|
||||
|
||||
**Control4Free is listed under Games instead of Applications.** That's a package from before 1.0.0. Delete the
|
||||
app and install the current package.
|
||||
|
||||
## Experimental features
|
||||
|
||||
**A switch is greyed out: "This browser treats the page as insecure".** Browsers give motion sensors and
|
||||
direct controller access only to secure pages, and the PS4 serves the page as plain `http`. Chrome and Edge
|
||||
can treat your PS4's address as secure:
|
||||
|
||||
1. Open `chrome://flags/#unsafely-treat-insecure-origin-as-secure`, or in Edge
|
||||
`edge://flags/#unsafely-treat-insecure-origin-as-secure`. On Android, open the same address in Chrome.
|
||||
2. Type the page's address exactly as the app on the PS4 shows it, for example `http://192.168.1.20:4264`,
|
||||
and set the option to **Enabled**.
|
||||
3. Press **Relaunch**.
|
||||
|
||||
The setting belongs to that browser on that device. If the PS4's address changes, update it here too.
|
||||
Add only your PS4's address: the flag allows those browser features but does not encrypt the HTTP page.
|
||||
Safari on an iPhone has no equivalent flag.
|
||||
|
||||
**"This browser can't talk to controllers directly."** Linking a DualShock 4 or DualSense needs Chrome or
|
||||
Edge on a computer. Android, Firefox and Safari don't offer it.
|
||||
|
||||
**The controller isn't in the browser's list, or linking fails.** Close other programs that use it, such as
|
||||
Steam or DS4Windows, reconnect it, press a button on it, then press **Link** again.
|
||||
|
||||
**Motion feels off.** Hold the phone sideways with the screen towards you, keep it still, and press
|
||||
**Recenter** in **Settings → Experimental**.
|
||||
|
||||
**A linked DS4 or DualSense only clicks its touchpad, without showing fingers.** Turn on **Lights and
|
||||
rumble on Sony controllers**, press **Link** and select the matching controller. Once linked, up to two
|
||||
fingers are sent. Use **Test controllers** on the PS4 to check the received touch positions.
|
||||
|
||||
**Trigger rumble does nothing.** Turn on **Trigger rumble**, assign the gamepad to the controller being
|
||||
used, and pull a trigger while the game is rumbling. Xbox controllers need a browser that exposes
|
||||
trigger-rumble support. A DualSense also needs **Lights and rumble on Sony controllers** enabled and a
|
||||
direct **Link**. A DS4 has no separate trigger effect. With both triggers held, only the one pulled last
|
||||
receives the effect; release it to move the effect to the other. See
|
||||
[Trigger rumble](playing.md#trigger-rumble) for what the effect does and a quick test.
|
||||
|
||||
## Reporting a problem
|
||||
|
||||
Every release comes with a diagnostic package, `Control4Free-<version>-diag.pkg`. It's the same Control4Free,
|
||||
Every release comes with a diagnostic package, `troubleshooting-Control4Free-<version>-diag.pkg`. It's the same Control4Free,
|
||||
except that its app shows on screen what the PS4 reports, so you can send screenshots instead of digging out
|
||||
log files. Please use it before you open an issue:
|
||||
|
||||
1. Download `Control4Free-<version>-diag.pkg` from the
|
||||
1. Download `troubleshooting-Control4Free-<version>-diag.pkg` from the
|
||||
[latest release](https://github.com/MoHadiShibli/Control4Free/releases/latest) and install it over the app.
|
||||
It replaces the app and shows up as **Control4Free (diagnostic)**.
|
||||
2. Open it. If it says RUNNING, the normal version is still the one running: press **Square**, then **Cross**
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
#ifndef C4F_CONN_H
|
||||
#define C4F_CONN_H
|
||||
/* A page's connection, whichever way it arrived: a WebRTC peer (the product)
|
||||
* or a WebSocket (host tests only). The service treats both the same way. */
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct C4fConn C4fConn;
|
||||
struct C4fConn {
|
||||
/* Reliable and ordered while connected: replies, status, errors. 0, or -1 when it
|
||||
* could not be queued (the connection is then closed). */
|
||||
int (*text)(C4fConn *, const char *);
|
||||
/* The newest state wins and a lost one is not resent: rumble. NULL means
|
||||
* the connection has no such path and text is used. */
|
||||
int (*input)(C4fConn *, const char *);
|
||||
};
|
||||
|
||||
static inline int c4fConnText(C4fConn *c, const char *text) { return c->text(c, text); }
|
||||
static inline int c4fConnInput(C4fConn *c, const char *text) { return (c->input ? c->input : c->text)(c, text); }
|
||||
|
||||
enum { C4F_NET_OPEN, C4F_NET_MESSAGE_EVENT, C4F_NET_INPUT_EVENT, C4F_NET_CLOSE,
|
||||
C4F_NET_HTTP_STATUS, C4F_NET_HTTP_STOP };
|
||||
typedef void (*C4fNetHandler)(C4fConn *, int event, const char *text, size_t len, void *context);
|
||||
|
||||
/* Milliseconds on a clock that only moves forward (net.c). */
|
||||
uint64_t c4fTimeMs(void);
|
||||
#endif
|
||||
@@ -0,0 +1,61 @@
|
||||
/* Mbed TLS 3.6 configuration for Control4Free (MBEDTLS_CONFIG_FILE).
|
||||
*
|
||||
* Just what a WebRTC data channel needs on the PS4 side: a DTLS 1.2 server
|
||||
* with ECDHE-ECDSA and AES-128-GCM, the self-signed P-256 certificate it shows
|
||||
* the browser, parsing the browser's certificate to check its fingerprint,
|
||||
* and HMAC-SHA1 for STUN. No files, sockets, threads or PSA: the service
|
||||
* feeds datagrams in and sends them out itself. */
|
||||
#ifndef C4F_MBEDTLS_CONFIG_H
|
||||
#define C4F_MBEDTLS_CONFIG_H
|
||||
|
||||
/* System */
|
||||
#define MBEDTLS_HAVE_ASM
|
||||
|
||||
/* Features */
|
||||
#define MBEDTLS_ECP_DP_SECP256R1_ENABLED
|
||||
#define MBEDTLS_ECP_NIST_OPTIM
|
||||
#define MBEDTLS_ECDSA_DETERMINISTIC
|
||||
#define MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED
|
||||
#define MBEDTLS_NO_PLATFORM_ENTROPY
|
||||
#define MBEDTLS_SSL_EXTENDED_MASTER_SECRET
|
||||
#define MBEDTLS_SSL_KEEP_PEER_CERTIFICATE
|
||||
#define MBEDTLS_SSL_PROTO_TLS1_2
|
||||
#define MBEDTLS_SSL_PROTO_DTLS
|
||||
#define MBEDTLS_SSL_DTLS_ANTI_REPLAY
|
||||
|
||||
/* Modules */
|
||||
#define MBEDTLS_AES_C
|
||||
#define MBEDTLS_ASN1_PARSE_C
|
||||
#define MBEDTLS_ASN1_WRITE_C
|
||||
#define MBEDTLS_BIGNUM_C
|
||||
#define MBEDTLS_CIPHER_C
|
||||
#define MBEDTLS_CTR_DRBG_C
|
||||
#define MBEDTLS_ECDH_C
|
||||
#define MBEDTLS_ECDSA_C
|
||||
#define MBEDTLS_ECP_C
|
||||
#define MBEDTLS_ENTROPY_C
|
||||
#define MBEDTLS_GCM_C
|
||||
#define MBEDTLS_HMAC_DRBG_C
|
||||
#define MBEDTLS_MD_C
|
||||
#define MBEDTLS_OID_C
|
||||
#define MBEDTLS_PK_C
|
||||
#define MBEDTLS_PK_PARSE_C
|
||||
#define MBEDTLS_PK_WRITE_C
|
||||
#define MBEDTLS_SHA1_C
|
||||
#define MBEDTLS_SHA224_C
|
||||
#define MBEDTLS_SHA256_C
|
||||
#define MBEDTLS_SSL_SRV_C
|
||||
#define MBEDTLS_SSL_TLS_C
|
||||
#define MBEDTLS_X509_USE_C
|
||||
#define MBEDTLS_X509_CRT_PARSE_C
|
||||
#define MBEDTLS_X509_CREATE_C
|
||||
#define MBEDTLS_X509_CRT_WRITE_C
|
||||
|
||||
/* Sizes: SCTP over DTLS stays under 1200 bytes a datagram, and the largest
|
||||
* handshake message is a certificate of a few hundred bytes. */
|
||||
#define MBEDTLS_SSL_IN_CONTENT_LEN 4096
|
||||
#define MBEDTLS_SSL_OUT_CONTENT_LEN 4096
|
||||
#define MBEDTLS_SSL_CIPHERSUITES MBEDTLS_TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
|
||||
#define MBEDTLS_ENTROPY_MAX_SOURCES 2
|
||||
|
||||
#endif
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include "c4f_conn.h"
|
||||
#include "c4f_rtc.h"
|
||||
|
||||
#define C4F_NET_CLIENTS 8
|
||||
#define C4F_NET_WEBSOCKETS 6
|
||||
@@ -14,7 +16,8 @@
|
||||
#endif
|
||||
#define C4F_NET_MESSAGE 4096
|
||||
typedef struct C4fNetClient {
|
||||
int fd, websocket, closing, fragmented;
|
||||
C4fConn conn; /* first: a WebSocket is one of the service's connections */
|
||||
int fd, websocket, closing, fragmented, savedOrigin;
|
||||
uint64_t openedMs, pingAt, pongDeadline, pingToken;
|
||||
unsigned char rx[8192], tx[8192];
|
||||
size_t rxUsed, txUsed, txSent, messageUsed;
|
||||
@@ -27,25 +30,27 @@ typedef struct C4fNetClient {
|
||||
#endif
|
||||
} C4fNetClient;
|
||||
|
||||
enum { C4F_NET_OPEN, C4F_NET_MESSAGE_EVENT, C4F_NET_CLOSE,
|
||||
C4F_NET_HTTP_STATUS, C4F_NET_HTTP_STOP };
|
||||
typedef void (*C4fNetHandler)(C4fNetClient *, int, const char *, size_t, void *);
|
||||
typedef struct {
|
||||
int fd;
|
||||
uint64_t checkedMs;
|
||||
C4fNetClient clients[C4F_NET_CLIENTS];
|
||||
C4fNetHandler handler;
|
||||
void *context;
|
||||
C4fRtc rtc; /* the pages' WebRTC peers, on the same port number over UDP */
|
||||
} C4fNet;
|
||||
|
||||
uint64_t c4fTimeMs(void);
|
||||
/* Opens the HTTP listener and the WebRTC port. A certificate made by an
|
||||
* earlier open is kept. */
|
||||
int c4fNetOpen(C4fNet *, int port, C4fNetHandler, void *);
|
||||
/* -1: listener/select failure; -2: long pause inside select. Reopen transport. */
|
||||
int c4fNetPoll(C4fNet *, int timeoutMs);
|
||||
void c4fNetClose(C4fNet *);
|
||||
#ifdef C4F_TEST_WEBSOCKET
|
||||
int c4fNetText(C4fNetClient *, const char *);
|
||||
void c4fNetHttpJson(C4fNetClient *, const char *);
|
||||
void c4fWebSocketAccept(const char *key, char out[29]);
|
||||
#endif
|
||||
/* The JSON answer to a launcher request (a C4F_NET_HTTP_* event). */
|
||||
void c4fNetHttpJson(C4fConn *, const char *);
|
||||
|
||||
extern const unsigned char c4fPage[];
|
||||
extern const size_t c4fPageSize;
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
#ifndef C4F_RTC_H
|
||||
#define C4F_RTC_H
|
||||
/* WebRTC for the controller page: the PS4 end of a data-channel connection.
|
||||
*
|
||||
* The page posts its offer to POST /rtc and gets the answer. From then on
|
||||
* everything goes over UDP: ICE-lite connectivity checks (STUN), DTLS 1.2, and
|
||||
* SCTP with two negotiated channels, 0 for replies and status (reliable, in
|
||||
* order) and 1 for input and rumble (newest wins, never resent). A lost packet
|
||||
* on channel 1 is simply skipped, so it can't hold up the ones after it. */
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include "c4f_conn.h"
|
||||
|
||||
#define C4F_RTC_PEERS 8
|
||||
#define C4F_RTC_SILENT_MS 10000 /* nothing from a peer for this long: gone */
|
||||
#define C4F_RTC_SETUP_MS 20000 /* an offer that never got as far as SCTP */
|
||||
|
||||
typedef struct C4fRtcPeer C4fRtcPeer;
|
||||
typedef struct C4fRtcCrypto C4fRtcCrypto;
|
||||
|
||||
typedef struct {
|
||||
int fd; /* UDP, or -1 */
|
||||
int port;
|
||||
C4fRtcCrypto *crypto; /* certificate, DTLS settings, random numbers: made once */
|
||||
char fingerprint[96]; /* our certificate's SHA-256, "AB:CD:...", for answers */
|
||||
C4fRtcPeer *peers[C4F_RTC_PEERS];
|
||||
C4fNetHandler handler;
|
||||
void *context;
|
||||
unsigned nextId;
|
||||
unsigned writeCursor; /* rotate the first peer drained on UDP writability */
|
||||
} C4fRtc;
|
||||
|
||||
/* Makes the certificate the first time, then opens the UDP port. 0 or -1. */
|
||||
int c4fRtcOpen(C4fRtc *, int port, C4fNetHandler, void *context);
|
||||
/* Closes every peer (those that were open get a close event) and the socket.
|
||||
* The certificate is kept for the next open. */
|
||||
void c4fRtcClose(C4fRtc *);
|
||||
/* Permanent service shutdown: close peers/socket and release certificate state. */
|
||||
void c4fRtcDestroy(C4fRtc *);
|
||||
/* An offer from POST /rtc. `local` is the console's address the page reached,
|
||||
* in network byte order: the only candidate the answer offers. Writes the
|
||||
* answer and returns 0, or an HTTP status: 400 for an offer that isn't one,
|
||||
* 503 when no peer slot is free. */
|
||||
int c4fRtcOffer(C4fRtc *, const char *offer, size_t len, uint32_t local, char *answer, size_t size);
|
||||
/* Reads every datagram waiting on the socket. */
|
||||
void c4fRtcReadable(C4fRtc *);
|
||||
/* Register UDP writability only while a nonblocking DTLS write is pending. */
|
||||
int c4fRtcWantsWrite(const C4fRtc *);
|
||||
void c4fRtcWritable(C4fRtc *);
|
||||
/* DTLS and SCTP resends, silent and failed peers. */
|
||||
void c4fRtcTick(C4fRtc *, uint64_t now);
|
||||
/* Milliseconds until c4fRtcTick has work, at most `limit`. */
|
||||
int c4fRtcWait(const C4fRtc *, uint64_t now, int limit);
|
||||
#endif
|
||||
@@ -82,9 +82,9 @@ typedef struct {
|
||||
uint8_t rx, ry;
|
||||
uint8_t l2, r2;
|
||||
uint16_t padding;
|
||||
float quat[4];
|
||||
float vel[3];
|
||||
float accel[3];
|
||||
float quat[4]; /* orientation, x y z w */
|
||||
float accel[3]; /* acceleration in G */
|
||||
float gyro[3]; /* angular velocity in rad/s */
|
||||
ScePadTouchData touchData;
|
||||
uint8_t connected;
|
||||
uint8_t align[3];
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
#ifndef C4F_SCTP_H
|
||||
#define C4F_SCTP_H
|
||||
/* A minimal SCTP association for WebRTC data channels, carried inside DTLS
|
||||
* (RFC 9260, RFC 8261, RFC 8831). The browser opens it with INIT; the PS4 only
|
||||
* answers. Two negotiated channels, so no DCEP: stream 0 is reliable and in
|
||||
* order, stream 1 unordered and never resent (partial reliability, RFC 3758).
|
||||
*
|
||||
* No sockets and no clock of its own: packets go in through c4fSctpReceive,
|
||||
* out through ops.send, and the caller passes the time. */
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define C4F_SCTP_MTU 1150 /* the biggest packet sent: DTLS adds ~40 bytes under WebRTC's 1200 */
|
||||
#define C4F_SCTP_MAX_MESSAGE 16384 /* the biggest message either way (a=max-message-size) */
|
||||
#define C4F_SCTP_PPID_STRING 51
|
||||
#define C4F_SCTP_PPID_BINARY 53
|
||||
#define C4F_SCTP_PPID_STRING_EMPTY 56
|
||||
#define C4F_SCTP_PPID_BINARY_EMPTY 57
|
||||
|
||||
typedef struct {
|
||||
void *user;
|
||||
/* One SCTP packet to send (inside a DTLS record). The receiver copies or
|
||||
* sends it before returning: 0 accepted, -1 fatal. */
|
||||
int (*send)(void *user, const uint8_t *packet, size_t len);
|
||||
/* One whole message. The association may be used (sent on) from here. */
|
||||
void (*deliver)(void *user, uint16_t stream, uint32_t ppid, const uint8_t *data, size_t len);
|
||||
/* Unpredictable bytes, for tags and the first TSN. */
|
||||
void (*random)(void *user, void *out, size_t len);
|
||||
} C4fSctpOps;
|
||||
|
||||
typedef struct C4fSctpOut C4fSctpOut; /* a sent chunk waiting for its acknowledgement */
|
||||
typedef struct C4fSctpIn C4fSctpIn; /* a received chunk waiting for the rest of its message or its turn */
|
||||
|
||||
enum { C4F_SCTP_CLOSED, C4F_SCTP_COOKIE_WAIT, C4F_SCTP_ESTABLISHED, C4F_SCTP_ENDED };
|
||||
|
||||
typedef struct {
|
||||
C4fSctpOps ops;
|
||||
int state;
|
||||
uint16_t localPort, remotePort;
|
||||
uint32_t localTag, remoteTag;
|
||||
uint32_t localInitialTsn, remoteInitialTsn;
|
||||
uint8_t cookie[16];
|
||||
|
||||
/* Receiving */
|
||||
uint32_t cumTsn; /* every TSN up to here has arrived */
|
||||
uint32_t highTsn; /* the highest arrived */
|
||||
uint8_t seen[128]; /* TSNs cumTsn+1 .. cumTsn+1024, one bit each, by TSN % 1024 */
|
||||
uint16_t nextSsn[2]; /* the next message due on an ordered stream */
|
||||
C4fSctpIn *in; /* waiting chunks, by TSN */
|
||||
size_t inBytes;
|
||||
int sackDue;
|
||||
|
||||
/* Sending */
|
||||
uint32_t nextTsn;
|
||||
uint16_t ssn[2];
|
||||
C4fSctpOut *out, *outTail; /* in TSN order */
|
||||
size_t outBytes;
|
||||
unsigned outChunks;
|
||||
uint32_t peerCumTsn; /* the browser has everything up to here */
|
||||
uint32_t highestSent; /* unsent queued chunks cannot be acknowledged */
|
||||
uint32_t forwardTsn; /* given up on (or acknowledged) up to here */
|
||||
uint32_t peerRwnd, cwnd, ssthresh, partialBytesAcked;
|
||||
uint32_t flightBytes, flightData, recoveryTsn;
|
||||
int fastRecovery;
|
||||
uint64_t rto, srtt, rttvar;
|
||||
int rttKnown;
|
||||
uint64_t t3; /* when the oldest unacknowledged chunk is due, 0 when nothing is */
|
||||
uint64_t probeAt; /* a closed receive window is probed after one RTO */
|
||||
uint64_t sendAt; /* continue a bounded burst on the next service turn */
|
||||
unsigned timeouts;
|
||||
uint32_t reconfigSeq;
|
||||
|
||||
/* For the log */
|
||||
uint64_t packetsIn, packetsOut, resent, abandoned, duplicates, badChecksums;
|
||||
} C4fSctp;
|
||||
|
||||
void c4fSctpInit(C4fSctp *, const C4fSctpOps *ops);
|
||||
void c4fSctpFree(C4fSctp *);
|
||||
/* One SCTP packet, as it came out of DTLS. */
|
||||
void c4fSctpReceive(C4fSctp *, const uint8_t *packet, size_t len, uint64_t now);
|
||||
/* Queues stream 0 reliably and in order, subject to congestion/receive windows.
|
||||
* Stream 1 is a single datagram, sent once; -1 when blocked so the application
|
||||
* can replace it with fresh state instead of buffering old rumble/input.
|
||||
* 0 accepted, -1 when not established, too big, blocked or queue full. */
|
||||
int c4fSctpSend(C4fSctp *, uint16_t stream, uint32_t ppid, const uint8_t *data, size_t len, uint64_t now);
|
||||
/* Resends what is overdue and gives up on what may be lost. */
|
||||
void c4fSctpTick(C4fSctp *, uint64_t now);
|
||||
/* When c4fSctpTick next has work, or 0 for nothing pending. */
|
||||
uint64_t c4fSctpDeadline(const C4fSctp *);
|
||||
uint32_t c4fCrc32c(const uint8_t *data, size_t len);
|
||||
#endif
|
||||
@@ -0,0 +1,32 @@
|
||||
#ifndef C4F_STUN_H
|
||||
#define C4F_STUN_H
|
||||
/* STUN for an ICE-lite WebRTC endpoint (RFC 8489, RFC 8445): the PS4 only
|
||||
* answers the browser's connectivity checks. */
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define C4F_STUN_MAGIC 0x2112A442u
|
||||
#define C4F_STUN_ANSWER_SIZE 64
|
||||
|
||||
/* RFC 7983: on the shared port, a first byte of 0-3 is STUN and 20-63 DTLS. */
|
||||
static inline int c4fIsStun(const uint8_t *data, size_t len) { return len >= 20 && data[0] <= 3; }
|
||||
static inline int c4fIsDtls(const uint8_t *data, size_t len) { return len >= 13 && data[0] >= 20 && data[0] <= 63; }
|
||||
|
||||
/* CRC-32 (IEEE 802.3), as STUN's FINGERPRINT uses it. */
|
||||
uint32_t c4fCrc32(const uint8_t *data, size_t len);
|
||||
|
||||
/* The USERNAME of a well-formed Binding request ("ours:theirs"), or NULL. */
|
||||
const uint8_t *c4fStunUsername(const uint8_t *msg, size_t len, size_t *size);
|
||||
|
||||
/* Whether the message's MESSAGE-INTEGRITY matches the key (ICE uses the
|
||||
* password as is), and its FINGERPRINT, when it has one, is right. */
|
||||
int c4fStunVerify(const uint8_t *msg, size_t len, const char *key);
|
||||
|
||||
/* Answers a Binding request made with our password: writes the success
|
||||
* response, carrying the address and port it came from (both in network byte
|
||||
* order), into out (C4F_STUN_ANSWER_SIZE bytes). Returns its length, or 0 when
|
||||
* the request is not one to answer. *nominate is set when the browser picked
|
||||
* this path (USE-CANDIDATE). */
|
||||
size_t c4fStunAnswer(const uint8_t *msg, size_t len, const char *password,
|
||||
uint32_t address, uint16_t port, uint8_t out[C4F_STUN_ANSWER_SIZE], int *nominate);
|
||||
#endif
|
||||
@@ -20,7 +20,7 @@ CC := clang-18
|
||||
CXX := clang++-18
|
||||
LD := ld.lld-18
|
||||
CFLAGS := --target=x86_64-pc-freebsd12-elf -fPIC -funwind-tables -O2 -Wall -Wextra -Werror -MMD -MP -D_DEFAULT_SOURCE -isysroot $(SDK) -isystem $(SDK)/include -I../vendor/jsmn -I../vendor/qrcodegen -I../vendor/stb -DC4F_LAUNCHER_VERSION='"$(VERSION)"' $(DIAG_FLAGS)
|
||||
OBJECTS += $(OUT)/main.o $(OUT)/input.o $(OUT)/navigation.o $(OUT)/screen.o $(OUT)/draw.o $(OUT)/service.o $(OUT)/autorun.o $(OUT)/sandbox.o $(OUT)/logo.o $(OUT)/qrcodegen.o $(OUT)/stb_truetype.o $(OUT)/fonts.o
|
||||
OBJECTS += $(OUT)/main.o $(OUT)/input.o $(OUT)/navigation.o $(OUT)/screen.o $(OUT)/tester.o $(OUT)/tester_draw.o $(OUT)/draw.o $(OUT)/service.o $(OUT)/autorun.o $(OUT)/sandbox.o $(OUT)/logo.o $(OUT)/qrcodegen.o $(OUT)/stb_truetype.o $(OUT)/fonts.o
|
||||
LIBS := -lc -lkernel -lSceVideoOut -lScePad -lSceUserService -lSceSystemService -lSceNetCtl
|
||||
FONTS := ../vendor/roboto/Roboto-Light.ttf ../vendor/roboto/Roboto-Regular.ttf
|
||||
.PHONY: all package
|
||||
|
||||
@@ -617,7 +617,9 @@ void c4fDrawDiag(uint32_t *pixels, const C4fLauncherScreen *s)
|
||||
}
|
||||
int visible = (C4F_D_BOTTOM - C4F_D_TOP) / C4F_D_ROW;
|
||||
int maxScroll = count > visible ? count - visible : 0;
|
||||
pthread_mutex_lock(&c4fLock);
|
||||
c4fMaxScroll[page] = maxScroll;
|
||||
pthread_mutex_unlock(&c4fLock);
|
||||
int first = s->diagScroll[page] >= C4F_DIAG_END ? maxScroll : s->diagScroll[page];
|
||||
if (first > maxScroll) first = maxScroll;
|
||||
if (first < 0) first = 0;
|
||||
@@ -638,5 +640,8 @@ void c4fDrawDiag(uint32_t *pixels, const C4fLauncherScreen *s)
|
||||
|
||||
int c4fDiagMaxScroll(int page)
|
||||
{
|
||||
return page >= 0 && page < C4F_DIAG_PAGES ? c4fMaxScroll[page] : 0;
|
||||
pthread_mutex_lock(&c4fLock);
|
||||
int maximum = page >= 0 && page < C4F_DIAG_PAGES ? c4fMaxScroll[page] : 0;
|
||||
pthread_mutex_unlock(&c4fLock);
|
||||
return maximum;
|
||||
}
|
||||
@@ -548,3 +548,44 @@ int c4fTextWrapped(C4fCanvas *c, int font, float size, float x, float y, float m
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
void c4fDrawSolidarityBadge(C4fCanvas *c, int right, int top)
|
||||
{
|
||||
enum { FLAG_W = 45, FLAG_H = 30, FLAG_GAP = 10, LABEL_GAP = 14 };
|
||||
const int palestine = right - 2 * FLAG_W - FLAG_GAP, syria = right - FLAG_W;
|
||||
const C4fColor black = C4F_RGBA(0, 0, 0, 1), white = C4F_RGBA(255, 255, 255, 1);
|
||||
const float textWidth = c4fTextWidth(C4F_FONT_REGULAR, 24, "Free Palestine");
|
||||
c4fText(c, C4F_FONT_REGULAR, 24, palestine - LABEL_GAP - textWidth, top + 23,
|
||||
C4F_RGBA(236, 240, 248, .95f), "Free Palestine");
|
||||
|
||||
c4fFillRect(c, palestine, top, FLAG_W, FLAG_H / 3, black);
|
||||
c4fFillRect(c, palestine, top + FLAG_H / 3, FLAG_W, FLAG_H / 3, white);
|
||||
c4fFillRect(c, palestine, top + 2 * FLAG_H / 3, FLAG_W, FLAG_H / 3, C4F_RGBA(0, 151, 54, 1));
|
||||
C4fPath triangle = { 0 };
|
||||
c4fPathMove(&triangle, palestine, top);
|
||||
c4fPathLine(&triangle, palestine + FLAG_W / 2, top + FLAG_H / 2);
|
||||
c4fPathLine(&triangle, palestine, top + FLAG_H);
|
||||
c4fPathLine(&triangle, palestine, top);
|
||||
c4fFillPath(c, &triangle, (C4fPlacement){ 0, 0, 1, 0, 0, 0 }, C4F_RGBA(237, 46, 56, 1));
|
||||
c4fPathFree(&triangle);
|
||||
|
||||
c4fFillRect(c, syria, top, FLAG_W, FLAG_H / 3, C4F_RGBA(0, 122, 61, 1));
|
||||
c4fFillRect(c, syria, top + FLAG_H / 3, FLAG_W, FLAG_H / 3, white);
|
||||
c4fFillRect(c, syria, top + 2 * FLAG_H / 3, FLAG_W, FLAG_H / 3, black);
|
||||
for (int star = 1; star <= 3; star++) {
|
||||
C4fPath path = { 0 };
|
||||
const float cx = syria + star * FLAG_W / 4.0f, cy = top + FLAG_H / 2.0f;
|
||||
for (int point = 0; point <= 10; point++) {
|
||||
const float angle = (point % 10) * .628318531f - 1.570796327f;
|
||||
const float radius = point % 2 ? 1.25f : 3.2f;
|
||||
const float x = cx + cosf(angle) * radius, y = cy + sinf(angle) * radius;
|
||||
if (!point) c4fPathMove(&path, x, y);
|
||||
else c4fPathLine(&path, x, y);
|
||||
}
|
||||
c4fFillPath(c, &path, (C4fPlacement){ 0, 0, 1, 0, 0, 0 }, C4F_RGBA(206, 17, 38, 1));
|
||||
c4fPathFree(&path);
|
||||
}
|
||||
const C4fColor edge = C4F_RGBA(255, 255, 255, .24f);
|
||||
c4fInsetRoundRect(c, palestine, top, FLAG_W, FLAG_H, 0, 1, edge);
|
||||
c4fInsetRoundRect(c, syria, top, FLAG_W, FLAG_H, 0, 1, edge);
|
||||
}
|
||||
@@ -70,4 +70,8 @@ float c4fTextWidth(int font, float size, const char *text);
|
||||
int c4fTextWrapped(C4fCanvas *c, int font, float size, float x, float y, float maxWidth,
|
||||
float lineHeight, int maxLines, C4fColor color, const char *text);
|
||||
|
||||
/* Corner label and two equally sized vector flags; paint into static layers.
|
||||
* `right` is the right edge, `top` the top of the 60 x 30 pixel flags. */
|
||||
void c4fDrawSolidarityBadge(C4fCanvas *c, int right, int top);
|
||||
|
||||
#endif
|
||||
@@ -9,18 +9,23 @@ enum { C4F_INPUT_PORT_STANDARD = 0, C4F_INPUT_PORT_REMOTE = 16 };
|
||||
#define C4F_INPUT_SYSTEM_USER 0xff
|
||||
#define C4F_INPUT_RETRY_MS 1000
|
||||
|
||||
/* Native read-side buttons used by launcher navigation. */
|
||||
/* Native read-side buttons used by launcher navigation and the controller test. */
|
||||
#define C4F_INPUT_BUTTON_L3 UINT32_C(0x000002)
|
||||
#define C4F_INPUT_BUTTON_R3 UINT32_C(0x000004)
|
||||
#define C4F_INPUT_BUTTON_OPTIONS UINT32_C(0x000008)
|
||||
#define C4F_INPUT_BUTTON_UP UINT32_C(0x000010)
|
||||
#define C4F_INPUT_BUTTON_RIGHT UINT32_C(0x000020)
|
||||
#define C4F_INPUT_BUTTON_DOWN UINT32_C(0x000040)
|
||||
#define C4F_INPUT_BUTTON_LEFT UINT32_C(0x000080)
|
||||
#define C4F_INPUT_BUTTON_L2 UINT32_C(0x000100)
|
||||
#define C4F_INPUT_BUTTON_R2 UINT32_C(0x000200)
|
||||
#define C4F_INPUT_BUTTON_L1 UINT32_C(0x000400)
|
||||
#define C4F_INPUT_BUTTON_R1 UINT32_C(0x000800)
|
||||
#define C4F_INPUT_BUTTON_TRIANGLE UINT32_C(0x001000)
|
||||
#define C4F_INPUT_BUTTON_CIRCLE UINT32_C(0x002000)
|
||||
#define C4F_INPUT_BUTTON_CROSS UINT32_C(0x004000)
|
||||
#define C4F_INPUT_BUTTON_SQUARE UINT32_C(0x008000)
|
||||
#define C4F_INPUT_BUTTON_TOUCHPAD UINT32_C(0x100000)
|
||||
/* PS4 reverse definitions identify bit31 as system-intercepted input. This is
|
||||
* a read-side flag, not an additional virtual-controller button. */
|
||||
#define C4F_INPUT_BUTTON_INTERCEPTED UINT32_C(0x80000000)
|
||||
|
||||
@@ -31,6 +31,11 @@ enum { C4F_DO_NOTHING, C4F_DO_START, C4F_DO_STOP, C4F_DO_SET_UP, C4F_DO_AUTORUN_
|
||||
static pthread_mutex_t c4fMutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
static C4fLauncherScreen c4fScreen;
|
||||
static int c4fCommand, c4fQuit;
|
||||
static int c4fCloseRequested;
|
||||
static uint64_t c4fRenderAt, c4fInputAt;
|
||||
static C4fServiceStatus c4fConnectionStatus;
|
||||
static int c4fConnectionService = C4F_SERVICE_UNKNOWN;
|
||||
static uint64_t c4fConnectionCheckedAt;
|
||||
|
||||
/* Set up: auto-start on, then start it now if PayLoader takes it. */
|
||||
static int c4fSetUp(char *message, size_t size)
|
||||
@@ -89,6 +94,7 @@ static void *c4fWorker(void *)
|
||||
if (command == C4F_DO_AUTORUN_OFF) result = c4fAutorunDisable(message, sizeof(message));
|
||||
if (first || command >= C4F_DO_SET_UP) autorun = c4fAutorunCheck(autorunNote, sizeof(autorunNote));
|
||||
C4fServiceStatus status;
|
||||
uint64_t probeAt = c4fLauncherTimeMs();
|
||||
int running = c4fLauncherProbe(&status);
|
||||
const char *problem = c4fLauncherProblem();
|
||||
pthread_mutex_lock(&c4fMutex);
|
||||
@@ -103,6 +109,9 @@ static void *c4fWorker(void *)
|
||||
snprintf(shownProblem, sizeof(shownProblem), "%s", problem);
|
||||
}
|
||||
c4fScreen.running = running;
|
||||
c4fConnectionStatus = status;
|
||||
c4fConnectionService = running;
|
||||
c4fConnectionCheckedAt = probeAt;
|
||||
c4fScreen.controllers = status.controllers;
|
||||
snprintf(c4fScreen.runningVersion, sizeof(c4fScreen.runningVersion), "%s", running == 1 ? status.version : "");
|
||||
c4fScreen.autorun = autorun;
|
||||
@@ -268,6 +277,119 @@ static int c4fNativeRead(void *context, int handle, uint32_t *buttons, int *conn
|
||||
|
||||
static void c4fNativeClose(void *, int handle) { scePadClose(handle); }
|
||||
|
||||
/* ---- the controller test's native side ---- */
|
||||
|
||||
/* The pinned header names the two motion vectors vel and acell. Sony's order
|
||||
* is orientation, acceleration (G), angular velocity (rad/s). */
|
||||
static_assert(offsetof(OrbisPadData, quat) == 12, "orientation");
|
||||
static_assert(offsetof(OrbisPadData, vel) == 28, "acceleration");
|
||||
static_assert(offsetof(OrbisPadData, acell) == 40, "angular velocity");
|
||||
static_assert(offsetof(OrbisPadData, touch) == 52, "touch data");
|
||||
static_assert(C4F_INPUT_BUTTON_L3 == ORBIS_PAD_BUTTON_L3, "L3 button");
|
||||
static_assert(C4F_INPUT_BUTTON_R3 == ORBIS_PAD_BUTTON_R3, "R3 button");
|
||||
static_assert(C4F_INPUT_BUTTON_L2 == ORBIS_PAD_BUTTON_L2, "L2 button");
|
||||
static_assert(C4F_INPUT_BUTTON_R2 == ORBIS_PAD_BUTTON_R2, "R2 button");
|
||||
static_assert(C4F_INPUT_BUTTON_L1 == ORBIS_PAD_BUTTON_L1, "L1 button");
|
||||
static_assert(C4F_INPUT_BUTTON_R1 == ORBIS_PAD_BUTTON_R1, "R1 button");
|
||||
static_assert(C4F_INPUT_BUTTON_TRIANGLE == ORBIS_PAD_BUTTON_TRIANGLE, "Triangle button");
|
||||
static_assert(C4F_INPUT_BUTTON_SQUARE == ORBIS_PAD_BUTTON_SQUARE, "Square button");
|
||||
static_assert(C4F_INPUT_BUTTON_TOUCHPAD == ORBIS_PAD_BUTTON_TOUCH_PAD, "touchpad button");
|
||||
|
||||
static int c4fTesterLoginUsers(void *, int32_t *users)
|
||||
{
|
||||
OrbisUserServiceLoginUserIdList list;
|
||||
memset(&list, 0, sizeof(list));
|
||||
int result = sceUserServiceGetLoginUserIdList(&list);
|
||||
if (result) return result < 0 ? result : -1;
|
||||
int count = 0;
|
||||
for (int i = 0; i < ORBIS_USER_SERVICE_MAX_LOGIN_USERS && count < C4F_TESTER_USERS; i++)
|
||||
if (list.userId[i] != ORBIS_USER_SERVICE_USER_ID_INVALID) users[count++] = list.userId[i];
|
||||
return count;
|
||||
}
|
||||
|
||||
static void c4fTesterUserName(void *, int32_t user, char *name, int size)
|
||||
{
|
||||
if (sceUserServiceGetUserName(user, name, (size_t)size)) name[0] = 0;
|
||||
}
|
||||
|
||||
static int c4fTesterOpenPad(void *context, int32_t user, int *owned)
|
||||
{
|
||||
C4fNativeInput *input = (C4fNativeInput *)context;
|
||||
*owned = 0;
|
||||
if (!input->initialized) {
|
||||
int result = scePadInit();
|
||||
if (result) return result < 0 ? result : -1;
|
||||
input->initialized = 1;
|
||||
}
|
||||
/* The launcher's own reader may hold this user's handle: borrow it. */
|
||||
int handle = scePadOpen(user, ORBIS_PAD_PORT_TYPE_STANDARD, 0, NULL);
|
||||
if ((uint32_t)handle == C4F_PAD_ERROR_ALREADY_OPENED)
|
||||
handle = scePadGetHandle(user, ORBIS_PAD_PORT_TYPE_STANDARD, 0);
|
||||
else if (handle >= 0)
|
||||
*owned = 1;
|
||||
return handle;
|
||||
}
|
||||
|
||||
static int c4fTesterRead(void *, int handle, C4fTesterSample *out)
|
||||
{
|
||||
OrbisPadData data;
|
||||
memset(&data, 0, sizeof(data));
|
||||
int result = scePadReadState(handle, &data);
|
||||
if (result) return result;
|
||||
out->buttons = data.buttons;
|
||||
out->lx = data.leftStick.x; out->ly = data.leftStick.y;
|
||||
out->rx = data.rightStick.x; out->ry = data.rightStick.y;
|
||||
out->l2 = data.analogButtons.l2; out->r2 = data.analogButtons.r2;
|
||||
out->touches = data.touch.fingers > 2 ? 2 : data.touch.fingers;
|
||||
for (int i = 0; i < out->touches; i++) {
|
||||
out->touch[i].x = data.touch.touch[i].x;
|
||||
out->touch[i].y = data.touch.touch[i].y;
|
||||
out->touch[i].id = data.touch.touch[i].finger;
|
||||
}
|
||||
out->quat[0] = data.quat.x; out->quat[1] = data.quat.y; out->quat[2] = data.quat.z; out->quat[3] = data.quat.w;
|
||||
out->accel[0] = data.vel.x; out->accel[1] = data.vel.y; out->accel[2] = data.vel.z;
|
||||
out->gyro[0] = data.acell.x; out->gyro[1] = data.acell.y; out->gyro[2] = data.acell.z;
|
||||
out->connected = data.connected;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void c4fTesterClosePad(void *, int handle) { scePadClose(handle); }
|
||||
|
||||
static int c4fTesterVibrate(void *, int handle, uint8_t large, uint8_t small)
|
||||
{
|
||||
OrbisPadVibeParam param = { large, small };
|
||||
int result = scePadSetVibration(handle, ¶m);
|
||||
if (result) printf("[c4f-launcher] controller test: scePadSetVibration = 0x%08x\n", (unsigned)result);
|
||||
return result;
|
||||
}
|
||||
|
||||
static int c4fTesterLight(void *, int handle, uint8_t r, uint8_t g, uint8_t b)
|
||||
{
|
||||
OrbisPadColor color = { r, g, b, 255 };
|
||||
int result = scePadSetLightBar(handle, &color);
|
||||
if (result) printf("[c4f-launcher] controller test: scePadSetLightBar = 0x%08x\n", (unsigned)result);
|
||||
return result;
|
||||
}
|
||||
|
||||
static int c4fTesterResetLight(void *, int handle) { return scePadResetLightBar(handle); }
|
||||
static int c4fTesterResetOrientation(void *, int handle) { return scePadResetOrientation(handle); }
|
||||
|
||||
static int c4fTesterMotion(void *, int handle, int on)
|
||||
{
|
||||
/* OpenOrbis declares this without parameters; Sony's call (as shadPS4
|
||||
* documents it) takes the handle and an on/off flag. */
|
||||
int result = reinterpret_cast<int32_t (*)(int32_t, bool)>(&scePadSetMotionSensorState)(handle, on != 0);
|
||||
printf("[c4f-launcher] controller test: scePadSetMotionSensorState = 0x%08x\n", (unsigned)result);
|
||||
return result;
|
||||
}
|
||||
|
||||
static void c4fTesterLogState(void *, int slot, int state, uint32_t error)
|
||||
{
|
||||
static const char *const names[] = { "opening", "ready", "no controller", "failed", "signed out" };
|
||||
printf("[c4f-launcher] controller test reader %d: %s (0x%08x)\n", slot + 1,
|
||||
state >= 0 && state <= C4F_TESTER_SIGNED_OUT ? names[state] : "?", (unsigned)error);
|
||||
}
|
||||
|
||||
static void c4fNativeInputNote(const C4fNativeInput *input, char *note, size_t size)
|
||||
{
|
||||
snprintf(note, size, "%s: %s (0x%08x); %s: %s (0x%08x), last key 0x%02x / raw 0x%08x",
|
||||
@@ -278,6 +400,202 @@ static void c4fNativeInputNote(const C4fNativeInput *input, char *note, size_t s
|
||||
(unsigned)input->remoteLastKey, input->remoteLastRawButtons);
|
||||
}
|
||||
|
||||
/* Own every native input/tester handle on this thread. Rendering and flip waits
|
||||
* must not postpone button edges or the one-second rumble safety timer. */
|
||||
static void *c4fInputWorker(void *)
|
||||
{
|
||||
C4fNativeInput nativeInput = {};
|
||||
nativeInput.handles[0] = nativeInput.handles[1] = -1;
|
||||
C4fInputOps inputOps = { &nativeInput, c4fNativeInitialUser, c4fNativeOpen, c4fNativeRead, c4fNativeClose };
|
||||
C4fInput inputs;
|
||||
c4fInputInit(&inputs, &inputOps);
|
||||
C4fTesterOps testerOps = { &nativeInput, c4fTesterLoginUsers, c4fTesterUserName, c4fTesterOpenPad, c4fTesterRead,
|
||||
c4fTesterClosePad, c4fTesterVibrate, c4fTesterLight, c4fTesterResetLight,
|
||||
c4fTesterResetOrientation, c4fTesterMotion, c4fTesterLogState };
|
||||
C4fTester tester;
|
||||
c4fTesterInit(&tester, &testerOps);
|
||||
C4fTesterHold launcherHold = {};
|
||||
int waitRelease = 0, paused = 0;
|
||||
uint64_t lastInputAt = c4fLauncherTimeMs();
|
||||
#ifdef C4F_DIAG
|
||||
uint64_t polls = 0, gapMax = 0, pollMax = 0, reportAt = lastInputAt + 5000;
|
||||
#endif
|
||||
for (;;) {
|
||||
uint64_t now = c4fLauncherTimeMs();
|
||||
pthread_mutex_lock(&c4fMutex);
|
||||
int quit = c4fQuit || c4fCloseRequested;
|
||||
uint64_t renderedAt = c4fRenderAt;
|
||||
pthread_mutex_unlock(&c4fMutex);
|
||||
if (quit) break;
|
||||
/* A suspended or stalled display must not leave test output running.
|
||||
* The first fresh frame reopens readers with their normal neutral gate. */
|
||||
if (now > renderedAt && now - renderedAt > 1000) {
|
||||
if (!paused) {
|
||||
c4fTesterSuspend(&tester, now);
|
||||
c4fInputReset(&inputs, now);
|
||||
C4fTesterView view;
|
||||
c4fTesterView(&tester, &view);
|
||||
pthread_mutex_lock(&c4fMutex);
|
||||
c4fScreen.tester = view;
|
||||
pthread_mutex_unlock(&c4fMutex);
|
||||
}
|
||||
paused = 1;
|
||||
usleep(10000);
|
||||
continue;
|
||||
}
|
||||
int resumed = paused || (now > lastInputAt && now - lastInputAt > 1000);
|
||||
if (resumed) {
|
||||
c4fTesterSuspend(&tester, now);
|
||||
c4fInputReset(&inputs, now);
|
||||
launcherHold = {};
|
||||
}
|
||||
paused = 0;
|
||||
#ifdef C4F_DIAG
|
||||
uint64_t gap = now - lastInputAt;
|
||||
if (!resumed && gap > gapMax) gapMax = gap;
|
||||
#endif
|
||||
lastInputAt = now;
|
||||
C4fInputFrame input;
|
||||
c4fInputPoll(&inputs, now, &input);
|
||||
int wasTesting = tester.active;
|
||||
if (wasTesting && !c4fTesterFrame(&tester, now, input.remote.pressed)) waitRelease = 1;
|
||||
int held = c4fTesterHoldUpdate(&launcherHold, input.standard.buttons, now);
|
||||
C4fTesterView view;
|
||||
c4fTesterView(&tester, &view);
|
||||
char inputNote[sizeof(c4fScreen.inputNote)];
|
||||
c4fNativeInputNote(&nativeInput, inputNote, sizeof(inputNote));
|
||||
int openTester = 0;
|
||||
#ifdef C4F_DIAG
|
||||
const int diagnostic = 1;
|
||||
int capture = 0;
|
||||
#else
|
||||
const int diagnostic = 0;
|
||||
#endif
|
||||
/* No native pad/user calls or drawing while this publication lock is
|
||||
* held. The service worker owns its own status fields in the same view. */
|
||||
pthread_mutex_lock(&c4fMutex);
|
||||
snprintf(c4fScreen.inputNote, sizeof(c4fScreen.inputNote), "%s", inputNote);
|
||||
c4fScreen.tester = view;
|
||||
c4fInputAt = c4fLauncherTimeMs();
|
||||
if (resumed) {
|
||||
c4fScreen.actionMenu = c4fScreen.confirmStop = c4fScreen.confirmMenu = 0;
|
||||
c4fScreen.actionFocus = C4F_ACTION_NONE;
|
||||
}
|
||||
uint32_t standardUnhandled = 0, remoteUnhandled = 0;
|
||||
C4fLauncherAction action = C4F_ACTION_NONE;
|
||||
if (!wasTesting) {
|
||||
uint32_t standardPressed = input.standard.pressed, remotePressed = input.remote.pressed;
|
||||
if (waitRelease) {
|
||||
if (!input.standard.buttons && !input.remote.buttons) waitRelease = 0;
|
||||
standardPressed = remotePressed = 0;
|
||||
}
|
||||
action = c4fNavigate(&c4fScreen, standardPressed, remotePressed,
|
||||
diagnostic, &standardUnhandled, &remoteUnhandled);
|
||||
if (action == C4F_ACTION_TESTER || (action == C4F_ACTION_NONE && held && !c4fScreen.actionMenu &&
|
||||
!c4fScreen.confirmStop && !c4fScreen.locked)) {
|
||||
openTester = 1;
|
||||
action = C4F_ACTION_NONE;
|
||||
}
|
||||
}
|
||||
if (action == C4F_ACTION_CLOSE) c4fCloseRequested = 1;
|
||||
else if (action == C4F_ACTION_START) {
|
||||
int setUp = c4fScreen.autorun != C4F_AUTORUN_ON;
|
||||
c4fCommand = setUp ? C4F_DO_SET_UP : C4F_DO_START; c4fScreen.busy = 1;
|
||||
snprintf(c4fScreen.message, sizeof(c4fScreen.message), "%s",
|
||||
setUp ? "Setting up Control4Free. Please wait..." : "Starting Control4Free. Please wait...");
|
||||
} else if (action == C4F_ACTION_STOP) {
|
||||
c4fCommand = C4F_DO_STOP; c4fScreen.busy = 1;
|
||||
snprintf(c4fScreen.message, sizeof(c4fScreen.message), "Stopping and releasing controllers...");
|
||||
} else if (action == C4F_ACTION_AUTORUN) {
|
||||
int off = c4fScreen.autorun == C4F_AUTORUN_ON;
|
||||
c4fCommand = off ? C4F_DO_AUTORUN_OFF : C4F_DO_AUTORUN_ON; c4fScreen.busy = 1;
|
||||
snprintf(c4fScreen.message, sizeof(c4fScreen.message), "%s",
|
||||
off ? "Turning auto-start off..." : "Turning auto-start on...");
|
||||
}
|
||||
#ifdef C4F_DIAG
|
||||
uint32_t pressed = wasTesting ? 0 : standardUnhandled | remoteUnhandled;
|
||||
int page = c4fScreen.diagPage;
|
||||
if (pressed & (ORBIS_PAD_BUTTON_R1 | ORBIS_PAD_BUTTON_RIGHT)) c4fScreen.diagPage = (page + 1) % C4F_DIAG_PAGES;
|
||||
if (pressed & (ORBIS_PAD_BUTTON_L1 | ORBIS_PAD_BUTTON_LEFT)) c4fScreen.diagPage = (page + C4F_DIAG_PAGES - 1) % C4F_DIAG_PAGES;
|
||||
if (pressed & (ORBIS_PAD_BUTTON_UP | ORBIS_PAD_BUTTON_DOWN)) {
|
||||
int max = c4fDiagMaxScroll(page), at = c4fScreen.diagScroll[page];
|
||||
if (at >= C4F_DIAG_END) at = max;
|
||||
at += pressed & ORBIS_PAD_BUTTON_UP ? -10 : 10;
|
||||
c4fScreen.diagScroll[page] = at >= max ? C4F_DIAG_END : at < 0 ? 0 : at;
|
||||
}
|
||||
capture = action == C4F_ACTION_CAPTURE || (pressed & ORBIS_PAD_BUTTON_OPTIONS);
|
||||
#endif
|
||||
pthread_mutex_unlock(&c4fMutex);
|
||||
if (openTester) {
|
||||
c4fTesterOpen(&tester, now);
|
||||
c4fTesterView(&tester, &view);
|
||||
pthread_mutex_lock(&c4fMutex);
|
||||
c4fScreen.tester = view;
|
||||
pthread_mutex_unlock(&c4fMutex);
|
||||
}
|
||||
#ifdef C4F_DIAG
|
||||
if (capture) c4fDiagCapture(4000);
|
||||
uint64_t took = c4fLauncherTimeMs() - now;
|
||||
if (took > pollMax) pollMax = took;
|
||||
polls++;
|
||||
if (now >= reportAt) {
|
||||
printf("[c4f-launcher] input sampler: %llu polls, largest gap %llu ms, slowest poll %llu ms\n",
|
||||
(unsigned long long)polls, (unsigned long long)gapMax, (unsigned long long)pollMax);
|
||||
polls = gapMax = pollMax = 0; reportAt = now + 5000;
|
||||
}
|
||||
#endif
|
||||
uint64_t finished = c4fLauncherTimeMs();
|
||||
unsigned delay = finished < now + 4 ? (unsigned)(now + 4 - finished) : 1;
|
||||
usleep(delay * 1000);
|
||||
}
|
||||
c4fTesterClose(&tester);
|
||||
c4fInputClose(&inputs);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* RTT is reported by the controller page for its own input-channel probe.
|
||||
* Match all virtual slots for the selected user; never call a local pad 0 ms. */
|
||||
static C4fTesterConnection c4fTesterConnectionView(const C4fServiceStatus *status, int running,
|
||||
uint64_t checkedAt, uint64_t now, const C4fTesterView *tester)
|
||||
{
|
||||
C4fTesterConnection result = {};
|
||||
for (int p = 0; p < C4F_SERVICE_PADS; p++) result.rttMs[p] = -1;
|
||||
if (running != C4F_SERVICE_RUNNING || !status->latencyAvailable || now < checkedAt ||
|
||||
now - checkedAt > C4F_SERVICE_LATENCY_STALE_MS) return result;
|
||||
result.available = 1;
|
||||
if (tester->selected < 0 || tester->selected >= C4F_TESTER_USERS ||
|
||||
!tester->user[tester->selected].used || tester->user[tester->selected].state != C4F_TESTER_READY) return result;
|
||||
uint32_t user = (uint32_t)tester->user[tester->selected].id;
|
||||
for (int p = 0; p < C4F_SERVICE_PADS; p++) {
|
||||
const C4fServiceLatency *link = &status->latency[p];
|
||||
if (!link->userKnown || link->userId != user) continue;
|
||||
result.pads |= 1U << p;
|
||||
if (link->rttMs >= 0 && link->ageMs >= 0 &&
|
||||
(uint64_t)link->ageMs + now - checkedAt <= C4F_SERVICE_LATENCY_STALE_MS) result.rttMs[p] = link->rttMs;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Background service fields and hidden live readings cannot change the tester
|
||||
* tools view. Compare only what is visible before spending a frame on it. */
|
||||
static C4fLauncherScreen c4fVisibleScreen(const C4fLauncherScreen *screen)
|
||||
{
|
||||
C4fLauncherScreen visible = *screen;
|
||||
if (!screen->tester.active) return visible;
|
||||
memset(&visible, 0, sizeof(visible));
|
||||
visible.tester = screen->tester;
|
||||
visible.connection = screen->connection;
|
||||
if (visible.tester.menu) {
|
||||
C4fTesterView *v = &visible.tester;
|
||||
v->buttons = 0;
|
||||
v->lx = v->ly = v->rx = v->ry = v->l2 = v->r2 = v->touches = 0;
|
||||
memset(v->touchX, 0, sizeof(v->touchX)); memset(v->touchY, 0, sizeof(v->touchY));
|
||||
memset(v->quat, 0, sizeof(v->quat)); memset(v->accel, 0, sizeof(v->accel));
|
||||
memset(v->gyro, 0, sizeof(v->gyro));
|
||||
}
|
||||
return visible;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
const size_t frameBytes = (size_t)C4F_SCREEN_WIDTH * C4F_SCREEN_HEIGHT * sizeof(uint32_t);
|
||||
@@ -296,11 +614,6 @@ int main(void)
|
||||
OrbisUserServiceInitializeParams params = {};
|
||||
params.priority = ORBIS_KERNEL_PRIO_FIFO_LOWEST;
|
||||
sceUserServiceInitialize(¶ms);
|
||||
C4fNativeInput nativeInput = {};
|
||||
nativeInput.handles[0] = nativeInput.handles[1] = -1;
|
||||
C4fInputOps inputOps = { &nativeInput, c4fNativeInitialUser, c4fNativeOpen, c4fNativeRead, c4fNativeClose };
|
||||
C4fInput inputs;
|
||||
c4fInputInit(&inputs, &inputOps);
|
||||
c4fScreen.running = -1; c4fScreen.busy = 1; c4fScreen.autorun = C4F_AUTORUN_UNKNOWN;
|
||||
snprintf(c4fScreen.message, sizeof(c4fScreen.message), "Checking Control4Free...");
|
||||
/* Read before leaving the sandbox: /app0 is only visible from inside it. */
|
||||
@@ -321,77 +634,75 @@ int main(void)
|
||||
snprintf(c4fScreen.message, sizeof(c4fScreen.message), "Could not initialize launcher. Close with PS and retry.");
|
||||
pthread_mutex_unlock(&c4fMutex);
|
||||
}
|
||||
c4fRenderAt = c4fLauncherTimeMs();
|
||||
pthread_t inputWorker;
|
||||
int inputStarted = pthread_create(&inputWorker, NULL, c4fInputWorker, NULL) == 0;
|
||||
if (!inputStarted) {
|
||||
pthread_mutex_lock(&c4fMutex);
|
||||
c4fScreen.locked = 1;
|
||||
snprintf(c4fScreen.message, sizeof(c4fScreen.message), "Could not initialize input. Close with PS and retry.");
|
||||
pthread_mutex_unlock(&c4fMutex);
|
||||
}
|
||||
sceSystemServiceHideSplashScreen();
|
||||
uint64_t lastInputAt = c4fLauncherTimeMs();
|
||||
int buffer = 0, done = 0, drawn = 0, shown[2] = { 0, 0 };
|
||||
int buffer = 0, done = 0, drawn = 0, shown[2] = { 0, 0 }, requestedRate = 1;
|
||||
int64_t frame = 1;
|
||||
C4fLauncherScreen drawnState;
|
||||
memset(&drawnState, 0, sizeof(drawnState));
|
||||
#ifdef C4F_DIAG
|
||||
uint64_t testFrames = 0, testDraws = 0, testDrawMax = 0, testCopyMax = 0;
|
||||
uint64_t testPresentMax = 0, testFrameMax = 0, testInputAgeMax = 0, testDrawReportAt = 0;
|
||||
#endif
|
||||
while (!done) {
|
||||
uint64_t now = c4fLauncherTimeMs();
|
||||
/* A suspended app must not resume a pending confirmation or reuse a
|
||||
* held navigation button. Do not guess unreversed focus API signatures. */
|
||||
int resumed = now > lastInputAt && now - lastInputAt > 1000;
|
||||
if (resumed) c4fInputReset(&inputs, now);
|
||||
lastInputAt = now;
|
||||
C4fInputFrame input;
|
||||
c4fInputPoll(&inputs, now, &input);
|
||||
pthread_mutex_lock(&c4fMutex);
|
||||
c4fNativeInputNote(&nativeInput, c4fScreen.inputNote, sizeof(c4fScreen.inputNote));
|
||||
if (resumed) {
|
||||
c4fScreen.actionMenu = c4fScreen.confirmStop = c4fScreen.confirmMenu = 0;
|
||||
c4fScreen.actionFocus = C4F_ACTION_NONE;
|
||||
}
|
||||
uint32_t standardUnhandled = 0, remoteUnhandled = 0;
|
||||
#ifdef C4F_DIAG
|
||||
const int diagnostic = 1;
|
||||
#else
|
||||
const int diagnostic = 0;
|
||||
#endif
|
||||
C4fLauncherAction action = c4fNavigate(&c4fScreen, input.standard.pressed, input.remote.pressed,
|
||||
diagnostic, &standardUnhandled, &remoteUnhandled);
|
||||
if (action == C4F_ACTION_CLOSE) done = 1;
|
||||
else if (action == C4F_ACTION_START) {
|
||||
/* Until auto-start is on, Start sets it up and starts it. */
|
||||
int setUp = c4fScreen.autorun != C4F_AUTORUN_ON;
|
||||
c4fCommand = setUp ? C4F_DO_SET_UP : C4F_DO_START; c4fScreen.busy = 1;
|
||||
snprintf(c4fScreen.message, sizeof(c4fScreen.message), "%s",
|
||||
setUp ? "Setting up Control4Free. Please wait..." : "Starting Control4Free. Please wait...");
|
||||
} else if (action == C4F_ACTION_STOP) {
|
||||
c4fCommand = C4F_DO_STOP; c4fScreen.busy = 1;
|
||||
snprintf(c4fScreen.message, sizeof(c4fScreen.message), "Stopping and releasing controllers...");
|
||||
} else if (action == C4F_ACTION_AUTORUN) {
|
||||
int off = c4fScreen.autorun == C4F_AUTORUN_ON;
|
||||
c4fCommand = off ? C4F_DO_AUTORUN_OFF : C4F_DO_AUTORUN_ON; c4fScreen.busy = 1;
|
||||
snprintf(c4fScreen.message, sizeof(c4fScreen.message), "%s",
|
||||
off ? "Turning auto-start off..." : "Turning auto-start on...");
|
||||
}
|
||||
#ifdef C4F_DIAG
|
||||
/* Pages and scrolling work while busy too. */
|
||||
uint32_t pressed = standardUnhandled | remoteUnhandled;
|
||||
int page = c4fScreen.diagPage;
|
||||
if (pressed & (ORBIS_PAD_BUTTON_R1 | ORBIS_PAD_BUTTON_RIGHT)) c4fScreen.diagPage = (page + 1) % C4F_DIAG_PAGES;
|
||||
if (pressed & (ORBIS_PAD_BUTTON_L1 | ORBIS_PAD_BUTTON_LEFT)) c4fScreen.diagPage = (page + C4F_DIAG_PAGES - 1) % C4F_DIAG_PAGES;
|
||||
if (pressed & (ORBIS_PAD_BUTTON_UP | ORBIS_PAD_BUTTON_DOWN)) {
|
||||
int max = c4fDiagMaxScroll(page), at = c4fScreen.diagScroll[page];
|
||||
if (at >= C4F_DIAG_END) at = max;
|
||||
at += pressed & ORBIS_PAD_BUTTON_UP ? -10 : 10;
|
||||
c4fScreen.diagScroll[page] = at >= max ? C4F_DIAG_END : at < 0 ? 0 : at;
|
||||
}
|
||||
if (action == C4F_ACTION_CAPTURE || (pressed & ORBIS_PAD_BUTTON_OPTIONS)) c4fDiagCapture(4000);
|
||||
#endif
|
||||
c4fRenderAt = now;
|
||||
done = c4fCloseRequested;
|
||||
C4fLauncherScreen snapshot = c4fScreen;
|
||||
pthread_mutex_unlock(&c4fMutex);
|
||||
/* Redraw only when something changed, then copy it to each buffer once. */
|
||||
if (!drawn || memcmp(&snapshot, &drawnState, sizeof(snapshot))) {
|
||||
C4fServiceStatus connectionStatus = c4fConnectionStatus;
|
||||
int connectionService = c4fConnectionService;
|
||||
uint64_t connectionCheckedAt = c4fConnectionCheckedAt;
|
||||
#ifdef C4F_DIAG
|
||||
c4fDrawDiag(canvas, &snapshot);
|
||||
#else
|
||||
c4fDrawLauncher(canvas, &snapshot);
|
||||
uint64_t sampledAt = c4fInputAt;
|
||||
#endif
|
||||
drawnState = snapshot; drawn = 1; shown[0] = shown[1] = 0;
|
||||
pthread_mutex_unlock(&c4fMutex);
|
||||
if (done) break;
|
||||
snapshot.connection = c4fTesterConnectionView(&connectionStatus, connectionService, connectionCheckedAt, now, &snapshot.tester);
|
||||
int wantedRate = snapshot.tester.active ? 0 : 1;
|
||||
if (wantedRate != requestedRate) {
|
||||
int result = sceVideoOutSetFlipRate(c4fVideo, wantedRate);
|
||||
if (result < 0) printf("[c4f-launcher] display rate change failed: 0x%08x\n", (unsigned)result);
|
||||
requestedRate = wantedRate;
|
||||
}
|
||||
C4fLauncherScreen visible = c4fVisibleScreen(&snapshot);
|
||||
/* Redraw only when something visible changed, then copy each buffer once. */
|
||||
if (!drawn || memcmp(&visible, &drawnState, sizeof(visible))) {
|
||||
if (snapshot.tester.active) {
|
||||
#ifdef C4F_DIAG
|
||||
uint64_t drawStart = c4fLauncherTimeMs();
|
||||
c4fDrawTester(canvas, &snapshot);
|
||||
uint64_t took = c4fLauncherTimeMs() - drawStart;
|
||||
if (took > testDrawMax) testDrawMax = took;
|
||||
testDraws++;
|
||||
#else
|
||||
c4fDrawTester(canvas, &snapshot);
|
||||
#endif
|
||||
} else {
|
||||
#ifdef C4F_DIAG
|
||||
c4fDrawDiag(canvas, &snapshot);
|
||||
#else
|
||||
c4fDrawLauncher(canvas, &snapshot);
|
||||
#endif
|
||||
}
|
||||
drawnState = visible; drawn = 1; shown[0] = shown[1] = 0;
|
||||
}
|
||||
#ifdef C4F_DIAG
|
||||
uint64_t copiedAt = c4fLauncherTimeMs();
|
||||
#endif
|
||||
if (!shown[buffer]) { memcpy(c4fBuffers[buffer], canvas, frameBytes); shown[buffer] = 1; }
|
||||
#ifdef C4F_DIAG
|
||||
uint64_t presentAt = c4fLauncherTimeMs();
|
||||
uint64_t copyMs = presentAt - copiedAt;
|
||||
#endif
|
||||
if (sceVideoOutSubmitFlip(c4fVideo, buffer, ORBIS_VIDEO_OUT_FLIP_VSYNC, frame) < 0) break;
|
||||
for (;;) {
|
||||
OrbisVideoOutFlipStatus status;
|
||||
@@ -403,14 +714,35 @@ int main(void)
|
||||
OrbisKernelUseconds timeout = 100000;
|
||||
sceKernelWaitEqueue(c4fFlipQueue, &event, 1, &count, &timeout);
|
||||
}
|
||||
#ifdef C4F_DIAG
|
||||
if (snapshot.tester.active) {
|
||||
uint64_t completedAt = c4fLauncherTimeMs();
|
||||
uint64_t presentMs = completedAt - presentAt, frameMs = completedAt - now;
|
||||
uint64_t inputAge = sampledAt && now >= sampledAt ? now - sampledAt : 0;
|
||||
if (copyMs > testCopyMax) testCopyMax = copyMs;
|
||||
if (presentMs > testPresentMax) testPresentMax = presentMs;
|
||||
if (frameMs > testFrameMax) testFrameMax = frameMs;
|
||||
if (inputAge > testInputAgeMax) testInputAgeMax = inputAge;
|
||||
testFrames++;
|
||||
}
|
||||
if (testFrames && now >= testDrawReportAt) {
|
||||
printf("[c4f-launcher] controller test: %llu frames, %llu redraws; max draw %llu, copy %llu, present %llu, frame %llu, sample age %llu ms\n",
|
||||
(unsigned long long)testFrames, (unsigned long long)testDraws,
|
||||
(unsigned long long)testDrawMax, (unsigned long long)testCopyMax,
|
||||
(unsigned long long)testPresentMax, (unsigned long long)testFrameMax,
|
||||
(unsigned long long)testInputAgeMax);
|
||||
testFrames = testDraws = testDrawMax = testCopyMax = testPresentMax = testFrameMax = testInputAgeMax = 0;
|
||||
testDrawReportAt = now + 5000;
|
||||
}
|
||||
#endif
|
||||
buffer ^= 1; frame++;
|
||||
}
|
||||
pthread_mutex_lock(&c4fMutex); c4fQuit = 1; pthread_mutex_unlock(&c4fMutex);
|
||||
if (inputStarted) pthread_join(inputWorker, NULL);
|
||||
if (workerStarted) pthread_join(worker, NULL);
|
||||
#ifdef C4F_DIAG
|
||||
c4fDiagStop();
|
||||
#endif
|
||||
c4fInputClose(&inputs);
|
||||
sceNetCtlTerm();
|
||||
c4fCloseVideo();
|
||||
free(canvas);
|
||||
|
||||
@@ -17,6 +17,7 @@ int c4fActionAvailable(const C4fLauncherScreen *screen, C4fLauncherAction action
|
||||
return !screen->busy && !screen->locked && screen->autorun != C4F_AUTORUN_UNKNOWN;
|
||||
case C4F_ACTION_STOP: return !screen->busy && !screen->locked && screen->running == 1;
|
||||
case C4F_ACTION_CAPTURE: return !!diagnostic;
|
||||
case C4F_ACTION_TESTER: return !screen->locked;
|
||||
case C4F_ACTION_CLOSE: return !screen->busy;
|
||||
default: return 0;
|
||||
}
|
||||
@@ -26,7 +27,7 @@ int c4fActionList(const C4fLauncherScreen *screen, int diagnostic,
|
||||
C4fLauncherAction *actions, int capacity)
|
||||
{
|
||||
const C4fLauncherAction order[] = { C4F_ACTION_RESUME, C4F_ACTION_START,
|
||||
C4F_ACTION_AUTORUN, C4F_ACTION_STOP, C4F_ACTION_CAPTURE, C4F_ACTION_CLOSE };
|
||||
C4F_ACTION_AUTORUN, C4F_ACTION_STOP, C4F_ACTION_TESTER, C4F_ACTION_CAPTURE, C4F_ACTION_CLOSE };
|
||||
int count = 0;
|
||||
for (unsigned i = 0; i < sizeof(order) / sizeof(order[0]); i++) {
|
||||
if (!c4fActionAvailable(screen, order[i], diagnostic)) continue;
|
||||
@@ -48,6 +49,7 @@ const char *c4fActionLabel(const C4fLauncherScreen *screen, C4fLauncherAction ac
|
||||
case C4F_ACTION_STOP: return "Stop Control4Free";
|
||||
case C4F_ACTION_CLOSE: return "Close app";
|
||||
case C4F_ACTION_CAPTURE: return "Capture kernel log";
|
||||
case C4F_ACTION_TESTER: return "Test controllers";
|
||||
default: return "";
|
||||
}
|
||||
}
|
||||
@@ -130,8 +132,8 @@ C4fLauncherAction c4fNavigate(C4fLauncherScreen *screen, uint32_t standardPresse
|
||||
if (pressed & C4F_NAV_DIRECTIONS) {
|
||||
int direction = c4fDirection(pressed);
|
||||
if (direction) {
|
||||
C4fLauncherAction actions[6];
|
||||
int count = c4fActionList(screen, diagnostic, actions, 6), at = 0;
|
||||
C4fLauncherAction actions[8];
|
||||
int count = c4fActionList(screen, diagnostic, actions, 8), at = 0;
|
||||
while (at < count && (int)actions[at] != screen->actionFocus) at++;
|
||||
screen->actionFocus = actions[(at + count + direction) % count];
|
||||
}
|
||||
@@ -159,6 +161,13 @@ C4fLauncherAction c4fNavigate(C4fLauncherScreen *screen, uint32_t standardPresse
|
||||
screen->actionFocus = c4fDefaultAction(screen, diagnostic);
|
||||
return C4F_ACTION_NONE;
|
||||
}
|
||||
/* A DS4's Options opens the controller test. The diagnostic build keeps
|
||||
* Options for capturing the kernel log. */
|
||||
if (!diagnostic && (standardPressed & C4F_INPUT_BUTTON_OPTIONS) &&
|
||||
c4fActionAvailable(screen, C4F_ACTION_TESTER, diagnostic)) {
|
||||
c4fConsume(standardUnhandled, remoteUnhandled);
|
||||
return C4F_ACTION_TESTER;
|
||||
}
|
||||
if (screen->busy) return C4F_ACTION_NONE;
|
||||
if (pressed & C4F_NAV_BACK) {
|
||||
c4fConsume(standardUnhandled, remoteUnhandled);
|
||||
|
||||
@@ -11,9 +11,7 @@
|
||||
#include "logo.h"
|
||||
#include "navigation.h"
|
||||
#include "qrcodegen.h"
|
||||
|
||||
/* Page CSS pixels to TV pixels. */
|
||||
#define C4F_S 1.5f
|
||||
#include "style.h"
|
||||
|
||||
/* Layout, in TV pixels. */
|
||||
#define C4F_LEFT 150.0f
|
||||
@@ -27,25 +25,6 @@
|
||||
#define C4F_PANEL_TOP (C4F_ROW_TOP + C4F_ROW_HEIGHT + 28)
|
||||
#define C4F_PANEL_HEIGHT 128.0f
|
||||
|
||||
/* The page's palette (:root). */
|
||||
#define C4F_TEXT C4F_RGBA(244, 247, 255, 1)
|
||||
#define C4F_MUTED C4F_RGBA(226, 236, 255, .72f)
|
||||
#define C4F_FAINT C4F_RGBA(226, 236, 255, .46f)
|
||||
#define C4F_LINE C4F_RGBA(255, 255, 255, .16f)
|
||||
#define C4F_LINE_SOFT C4F_RGBA(255, 255, 255, .09f)
|
||||
#define C4F_PANEL C4F_RGBA(3, 16, 44, .55f)
|
||||
#define C4F_PANEL_STRONG C4F_RGBA(2, 11, 31, .86f)
|
||||
#define C4F_TILE C4F_RGBA(12, 40, 98, .5f)
|
||||
#define C4F_ACCENT C4F_RGBA(58, 140, 255, 1)
|
||||
#define C4F_OK C4F_RGBA(69, 224, 138, 1)
|
||||
#define C4F_WARN C4F_RGBA(255, 208, 77, 1)
|
||||
#define C4F_BAD C4F_RGBA(255, 107, 125, 1)
|
||||
#define C4F_CIRCLE_KEY C4F_RGBA(255, 102, 128, 1)
|
||||
#define C4F_CROSS_KEY C4F_RGBA(134, 182, 255, 1)
|
||||
#define C4F_SQUARE_KEY C4F_RGBA(255, 146, 220, 1)
|
||||
#define C4F_TRIANGLE_KEY C4F_RGBA(63, 224, 184, 1)
|
||||
#define C4F_WHITE C4F_RGBA(255, 255, 255, 1)
|
||||
|
||||
static uint32_t *c4fBackdrop;
|
||||
|
||||
static float c4fClamp(float v) { return v < 0 ? 0 : v > 1 ? 1 : v; }
|
||||
@@ -172,7 +151,7 @@ static void c4fPaintWave(C4fCanvas *c, const float path[25], float offset, float
|
||||
}
|
||||
}
|
||||
|
||||
static void c4fPaintBackdrop(C4fCanvas *c, float scale)
|
||||
void c4fPaintBackdrop(C4fCanvas *c, float scale)
|
||||
{
|
||||
const float w = (float)c->w, h = (float)c->h, sx = 2 * w / 3200;
|
||||
c4fPaintGradient(c);
|
||||
@@ -226,7 +205,7 @@ static void c4fBoxPass(const uint8_t *src, uint8_t *dst, int w, int h, int radiu
|
||||
|
||||
/* backdrop-filter: blur(sigma) under a tile. The backdrop never changes, so
|
||||
* this is done once, clipped to the tile's rounded shape. */
|
||||
static void c4fBlurUnder(C4fCanvas *c, float x, float y, float w, float h, float r, float sigma)
|
||||
void c4fBlurUnder(C4fCanvas *c, float x, float y, float w, float h, float r, float sigma)
|
||||
{
|
||||
/* Three box passes of this radius approximate a Gaussian of that sigma. */
|
||||
const int radius = (int)((sqrtf(4 * sigma * sigma + 1) - 1) / 2 + 0.5f), margin = 3 * radius;
|
||||
@@ -270,7 +249,7 @@ static void c4fBlurUnder(C4fCanvas *c, float x, float y, float w, float h, float
|
||||
/* ---- pieces of the page ---- */
|
||||
|
||||
/* The DualShock 4 mark, its cut-outs showing what is behind. */
|
||||
static void c4fMark(C4fCanvas *c, float x, float y, float width, C4fColor color)
|
||||
void c4fMark(C4fCanvas *c, float x, float y, float width, C4fColor color)
|
||||
{
|
||||
C4fPath path = { 0 };
|
||||
c4fLogoPath(&path, 1);
|
||||
@@ -280,8 +259,8 @@ static void c4fMark(C4fCanvas *c, float x, float y, float width, C4fColor color)
|
||||
|
||||
/* A DualShock 4 with its light bar lit: the bar glows over the top edge, and
|
||||
* the cut-outs read as dark recesses. */
|
||||
static void c4fLitController(C4fCanvas *c, float x, float y, float width, float angle,
|
||||
C4fColor body, C4fColor light, int shadow)
|
||||
void c4fLitController(C4fCanvas *c, float x, float y, float width, float angle,
|
||||
C4fColor body, C4fColor light, int shadow)
|
||||
{
|
||||
C4fPath outline = { 0 }, detail = { 0 }, bar = { 0 };
|
||||
C4fPlacement at = c4fLogoAt(x, y, width, angle);
|
||||
@@ -310,13 +289,13 @@ static void c4fLitController(C4fCanvas *c, float x, float y, float width, float
|
||||
c4fPathFree(&bar);
|
||||
}
|
||||
|
||||
static void c4fTextCentered(C4fCanvas *c, int font, float size, float cx, float y, C4fColor color, const char *text)
|
||||
void c4fTextCentered(C4fCanvas *c, int font, float size, float cx, float y, C4fColor color, const char *text)
|
||||
{
|
||||
c4fText(c, font, size, cx - c4fTextWidth(font, size, text) / 2, y, color, text);
|
||||
}
|
||||
|
||||
/* .tile: --tile with a light sheen, a 1px line, a drop shadow and the colour bar. */
|
||||
static void c4fTile(C4fCanvas *c, float x, float y, float w, float h, C4fColor bar)
|
||||
void c4fTile(C4fCanvas *c, float x, float y, float w, float h, C4fColor bar)
|
||||
{
|
||||
const float r = 6 * C4F_S;
|
||||
c4fShadowRoundRect(c, x, y, w, h, r, 0, 16 * C4F_S, 34 * C4F_S, -18 * C4F_S, C4F_RGBA(0, 0, 0, .9f));
|
||||
@@ -328,7 +307,7 @@ static void c4fTile(C4fCanvas *c, float x, float y, float w, float h, C4fColor b
|
||||
}
|
||||
|
||||
/* .panel */
|
||||
static void c4fPanel(C4fCanvas *c, float x, float y, float w, float h, C4fColor fill, C4fColor line)
|
||||
void c4fPanel(C4fCanvas *c, float x, float y, float w, float h, C4fColor fill, C4fColor line)
|
||||
{
|
||||
c4fFillRoundRect(c, x, y, w, h, 6 * C4F_S, fill);
|
||||
c4fInsetRoundRect(c, x, y, w, h, 6 * C4F_S, 1 * C4F_S, line);
|
||||
@@ -352,21 +331,18 @@ static void c4fRingBadge(C4fCanvas *c, float cx, float cy, C4fColor color, const
|
||||
}
|
||||
|
||||
/* .dot, with the glow the page gives a live connection. */
|
||||
static void c4fDot(C4fCanvas *c, float cx, float cy, C4fColor color, int glow)
|
||||
void c4fDot(C4fCanvas *c, float cx, float cy, C4fColor color, int glow)
|
||||
{
|
||||
if (glow) c4fGlowCircle(c, cx, cy, 4 * C4F_S, 10 * C4F_S, 0, color);
|
||||
c4fFillCircle(c, cx, cy, 4 * C4F_S, color);
|
||||
}
|
||||
|
||||
/* Button guide in the PS4's manner, with the page's face-button colours. */
|
||||
enum { C4F_KEY_CROSS, C4F_KEY_CIRCLE, C4F_KEY_SQUARE, C4F_KEY_TRIANGLE };
|
||||
typedef struct { int key; const char *label; } C4fHint;
|
||||
#define C4F_HINT_RADIUS 22.0f
|
||||
#define C4F_HINT_GAP 14.0f
|
||||
#define C4F_HINT_SPACING 40.0f
|
||||
#define C4F_HINT_SIZE 26.0f
|
||||
|
||||
static void c4fHints(C4fCanvas *c, float right, float cy, const C4fHint *hints, int count)
|
||||
void c4fHints(C4fCanvas *c, float right, float cy, const C4fHint *hints, int count)
|
||||
{
|
||||
float x = right;
|
||||
for (int i = 0; i < count; i++)
|
||||
@@ -383,6 +359,10 @@ static void c4fHints(C4fCanvas *c, float right, float cy, const C4fHint *hints,
|
||||
c4fRing(c, cx, cy, 10.5f, 3.6f, C4F_CIRCLE_KEY);
|
||||
} else if (hints[i].key == C4F_KEY_TRIANGLE) {
|
||||
c4fTriangleOutline(c, cx, cy + 1.5f, 11.5f, 3.4f, C4F_TRIANGLE_KEY);
|
||||
} else if (hints[i].key == C4F_KEY_OPTIONS) {
|
||||
/* The page's menu mark, three lines, for the Options button. */
|
||||
for (int line = -1; line <= 1; line++)
|
||||
c4fSegment(c, cx - 9, cy + line * 6.5f, cx + 9, cy + line * 6.5f, 3, C4F_TEXT);
|
||||
} else {
|
||||
c4fSquareOutline(c, cx, cy, 8.5f, 3.4f, C4F_SQUARE_KEY);
|
||||
}
|
||||
@@ -468,8 +448,8 @@ void c4fDrawActionOverlay(uint32_t *pixels, const C4fLauncherScreen *s)
|
||||
#ifdef C4F_DIAG
|
||||
diagnostic = 1;
|
||||
#endif
|
||||
C4fLauncherAction actions[7];
|
||||
int count = s->confirmStop ? 0 : c4fActionList(s, diagnostic, actions, 7);
|
||||
C4fLauncherAction actions[8];
|
||||
int count = s->confirmStop ? 0 : c4fActionList(s, diagnostic, actions, 8);
|
||||
const float dh = s->confirmStop ? (s->confirmMenu ? 410 : 330) : 220 + count * 72;
|
||||
const float dy = (c.h - dh) / 2;
|
||||
c4fFillRect(&c, 0, 0, c.w, c.h, C4F_RGBA(2, 11, 31, .65f));
|
||||
@@ -633,7 +613,7 @@ void c4fDrawLauncher(uint32_t *pixels, const C4fLauncherScreen *s)
|
||||
c4fText(&c, C4F_FONT_LIGHT, 23, left, 973, C4F_MUTED, "TV remote: OK opens actions / arrows select / Back returns");
|
||||
c4fText(&c, C4F_FONT_LIGHT, 20, left, 1030, C4F_FAINT, "Control4Free " C4F_LAUNCHER_VERSION);
|
||||
if (!s->confirmStop && !s->busy) {
|
||||
C4fHint hints[4];
|
||||
C4fHint hints[5];
|
||||
int n = 0;
|
||||
if (!s->locked && !live) {
|
||||
hints[n].key = C4F_KEY_CROSS;
|
||||
@@ -648,6 +628,10 @@ void c4fDrawLauncher(uint32_t *pixels, const C4fLauncherScreen *s)
|
||||
hints[n].key = C4F_KEY_SQUARE;
|
||||
hints[n++].label = "Stop";
|
||||
}
|
||||
if (!s->locked) {
|
||||
hints[n].key = C4F_KEY_OPTIONS;
|
||||
hints[n++].label = "Test controllers";
|
||||
}
|
||||
hints[n].key = C4F_KEY_CIRCLE;
|
||||
hints[n++].label = "Close";
|
||||
c4fHints(&c, right, 1022, hints, n);
|
||||
@@ -668,18 +652,35 @@ void c4fDrawIcon(uint32_t *pixels, int size)
|
||||
c4fTextCentered(&c, C4F_FONT_LIGHT, s * 0.1f, s / 2.0f, s * 0.89f, C4F_TEXT, "Control4Free");
|
||||
}
|
||||
|
||||
/* The backdrop before this screen's blurs, for the controller test to build its
|
||||
* own layer on. Taken once; NULL if memory was short. */
|
||||
static uint32_t *c4fCleanBackdrop;
|
||||
|
||||
uint32_t *c4fTakeCleanBackdrop(void)
|
||||
{
|
||||
uint32_t *taken = c4fCleanBackdrop;
|
||||
c4fCleanBackdrop = NULL;
|
||||
return taken;
|
||||
}
|
||||
|
||||
int c4fScreenInit(void)
|
||||
{
|
||||
if (c4fDrawInit()) return -1;
|
||||
if (!c4fBackdrop) {
|
||||
c4fBackdrop = malloc((size_t)C4F_SCREEN_WIDTH * C4F_SCREEN_HEIGHT * sizeof(uint32_t));
|
||||
const size_t bytes = (size_t)C4F_SCREEN_WIDTH * C4F_SCREEN_HEIGHT * sizeof(uint32_t);
|
||||
c4fBackdrop = malloc(bytes);
|
||||
if (!c4fBackdrop) return -1;
|
||||
C4fCanvas c = { c4fBackdrop, C4F_SCREEN_WIDTH, C4F_SCREEN_HEIGHT };
|
||||
c4fPaintBackdrop(&c, C4F_S);
|
||||
c4fCleanBackdrop = malloc(bytes);
|
||||
if (c4fCleanBackdrop) memcpy(c4fCleanBackdrop, c4fBackdrop, bytes);
|
||||
/* .tile has backdrop-filter: blur(6px), .panel blur(10px). */
|
||||
c4fBlurUnder(&c, C4F_LEFT, C4F_ROW_TOP, C4F_MAIN_WIDTH, C4F_ROW_HEIGHT, 6 * C4F_S, 6 * C4F_S);
|
||||
c4fBlurUnder(&c, C4F_QR_LEFT, C4F_ROW_TOP, C4F_QR_WIDTH, C4F_ROW_HEIGHT, 6 * C4F_S, 6 * C4F_S);
|
||||
c4fBlurUnder(&c, C4F_LEFT, C4F_PANEL_TOP, C4F_RIGHT - C4F_LEFT, C4F_PANEL_HEIGHT, 6 * C4F_S, 10 * C4F_S);
|
||||
#ifndef C4F_DIAG
|
||||
c4fDrawSolidarityBadge(&c, C4F_SCREEN_WIDTH - 64, 40);
|
||||
#endif
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef C4F_LAUNCHER_SCREEN_H
|
||||
#define C4F_LAUNCHER_SCREEN_H
|
||||
#include <stdint.h>
|
||||
#include "tester.h"
|
||||
/* Set by the Makefile from the VERSION file. */
|
||||
#ifndef C4F_LAUNCHER_VERSION
|
||||
#define C4F_LAUNCHER_VERSION "dev"
|
||||
@@ -14,14 +15,24 @@ typedef enum {
|
||||
C4F_ACTION_AUTORUN,
|
||||
C4F_ACTION_STOP,
|
||||
C4F_ACTION_CLOSE,
|
||||
C4F_ACTION_CAPTURE
|
||||
C4F_ACTION_CAPTURE,
|
||||
C4F_ACTION_TESTER
|
||||
} C4fLauncherAction;
|
||||
/* Connection round trips for the selected user's virtual-controller slots.
|
||||
* A negative RTT means that no fresh measurement is available yet. */
|
||||
typedef struct {
|
||||
int available;
|
||||
unsigned pads;
|
||||
int rttMs[4];
|
||||
} C4fTesterConnection;
|
||||
typedef struct {
|
||||
int running, busy, controllers, confirmStop, locked;
|
||||
int actionMenu, actionFocus;
|
||||
int confirmChoice, confirmMenu; /* 0 Cancel / 1 Stop; menu-origin confirmation. */
|
||||
int autorun; /* C4F_AUTORUN_* from autorun.h */
|
||||
char address[64], message[160], autorunNote[96], runningVersion[32], inputNote[160];
|
||||
C4fTesterView tester; /* the controller test, when active */
|
||||
C4fTesterConnection connection;
|
||||
#ifdef C4F_DIAG
|
||||
unsigned diagGeneration; /* bumped when a diagnostic page changes */
|
||||
int diagPage, diagScroll[8];
|
||||
@@ -37,6 +48,8 @@ int c4fScreenInit(void);
|
||||
void c4fDrawLauncher(uint32_t *pixels, const C4fLauncherScreen *state);
|
||||
/* Shared actions and stop-confirmation overlay for both package variants. */
|
||||
void c4fDrawActionOverlay(uint32_t *pixels, const C4fLauncherScreen *state);
|
||||
/* The controller test, in both package variants (tester_draw.c). */
|
||||
void c4fDrawTester(uint32_t *pixels, const C4fLauncherScreen *state);
|
||||
/* The package icon (size x size), in the same style. */
|
||||
void c4fDrawIcon(uint32_t *pixels, int size);
|
||||
#ifdef __cplusplus
|
||||
|
||||
@@ -192,17 +192,35 @@ static int c4fEquals(const char *json, const jsmntok_t *token, const char *value
|
||||
return token->end - token->start == (int)strlen(value) && !memcmp(json + token->start, value, strlen(value));
|
||||
}
|
||||
|
||||
static int c4fStatusInteger(const char *json, const jsmntok_t *token, int64_t maximum, int64_t *out)
|
||||
{
|
||||
if (token->type != JSMN_PRIMITIVE) return -1;
|
||||
if (c4fEquals(json, token, "-1")) { *out = -1; return 0; }
|
||||
int length = token->end - token->start;
|
||||
if (length < 1 || length > 10 || (length > 1 && json[token->start] == '0')) return -1;
|
||||
int64_t value = 0;
|
||||
for (int i = token->start; i < token->end; i++) {
|
||||
if (json[i] < '0' || json[i] > '9') return -1;
|
||||
value = value * 10 + json[i] - '0';
|
||||
}
|
||||
if (value > maximum) return -1;
|
||||
*out = value;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int c4fLauncherProbe(C4fServiceStatus *status)
|
||||
{
|
||||
char body[512]; jsmntok_t tokens[24]; jsmn_parser parser;
|
||||
char body[1024]; jsmntok_t tokens[64]; jsmn_parser parser;
|
||||
memset(status, 0, sizeof(*status));
|
||||
for (int i = 0; i < C4F_SERVICE_PADS; i++) status->latency[i].rttMs = status->latency[i].ageMs = -1;
|
||||
int result = c4fHttp("GET", "/api/status", body, sizeof(body));
|
||||
if (result <= 0) return result;
|
||||
jsmn_init(&parser);
|
||||
int count = jsmn_parse(&parser, body, strlen(body), tokens, 24);
|
||||
int count = jsmn_parse(&parser, body, strlen(body), tokens, 64);
|
||||
snprintf(c4fProblem, sizeof(c4fProblem), "the status reply is not from Control4Free");
|
||||
if (count < 1 || tokens[0].type != JSMN_OBJECT || tokens[0].end != (int)strlen(body)) return -1;
|
||||
int app = 0, api = 0, controllers = 0, stopping = 0;
|
||||
unsigned latencyFields[C4F_SERVICE_PADS] = { 0 };
|
||||
for (int i = 1; i + 1 < count; i += 2) {
|
||||
jsmntok_t *key = &tokens[i], *value = &tokens[i+1];
|
||||
if (key->type != JSMN_STRING || (value->type != JSMN_STRING && value->type != JSMN_PRIMITIVE)) return -1;
|
||||
@@ -218,9 +236,28 @@ int c4fLauncherProbe(C4fServiceStatus *status)
|
||||
int size = value->end - value->start;
|
||||
if (value->type != JSMN_STRING || size >= (int)sizeof(status->version)) return -1;
|
||||
memcpy(status->version, body + value->start, (size_t)size);
|
||||
} else if (c4fEquals(body, key, "latency")) {
|
||||
if (value->type != JSMN_PRIMITIVE || (!c4fEquals(body, value, "true") && !c4fEquals(body, value, "false"))) return -1;
|
||||
status->latencyAvailable = c4fEquals(body, value, "true");
|
||||
} else {
|
||||
static const char *const fields[] = { "User", "Rtt", "Age" };
|
||||
for (int p = 0; p < C4F_SERVICE_PADS; p++) for (int f = 0; f < 3; f++) {
|
||||
char name[16];
|
||||
snprintf(name, sizeof(name), "pad%d%s", p, fields[f]);
|
||||
if (!c4fEquals(body, key, name)) continue;
|
||||
int64_t number;
|
||||
if ((latencyFields[p] & (1U << f)) ||
|
||||
c4fStatusInteger(body, value, f == 0 ? UINT32_MAX : f == 1 ? 60000 : INT32_MAX, &number)) return -1;
|
||||
latencyFields[p] |= 1U << f;
|
||||
C4fServiceLatency *link = &status->latency[p];
|
||||
if (f == 0) { link->userKnown = number >= 0; link->userId = number >= 0 ? (uint32_t)number : 0; }
|
||||
else if (f == 1) link->rttMs = (int)number;
|
||||
else link->ageMs = (int)number;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!(app && api && controllers && stopping)) return -1;
|
||||
for (int p = 0; p < C4F_SERVICE_PADS; p++) if (latencyFields[p] != 7) status->latencyAvailable = 0;
|
||||
c4fProblem[0] = 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,19 @@ extern "C" {
|
||||
#endif
|
||||
|
||||
enum { C4F_SERVICE_UNKNOWN = -1, C4F_SERVICE_DOWN = 0, C4F_SERVICE_RUNNING = 1 };
|
||||
typedef struct { int controllers, stopping; char version[24]; } C4fServiceStatus;
|
||||
#define C4F_SERVICE_PADS 4
|
||||
#define C4F_SERVICE_LATENCY_STALE_MS 6000
|
||||
typedef struct {
|
||||
int userKnown;
|
||||
uint32_t userId;
|
||||
int rttMs, ageMs;
|
||||
} C4fServiceLatency;
|
||||
typedef struct {
|
||||
int controllers, stopping;
|
||||
char version[24];
|
||||
int latencyAvailable;
|
||||
C4fServiceLatency latency[C4F_SERVICE_PADS];
|
||||
} C4fServiceStatus;
|
||||
uint64_t c4fLauncherTimeMs(void);
|
||||
/* The console's own IPv4 address, when known. The app's sandbox may not reach
|
||||
* 127.0.0.1, so it is the second way to reach Control4Free and PayLoader. */
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
#ifndef C4F_LAUNCHER_STYLE_H
|
||||
#define C4F_LAUNCHER_STYLE_H
|
||||
/* The controller page's look, shared by the launcher's screens (screen.c,
|
||||
* tester_draw.c): palette, sizes and the drawn pieces. */
|
||||
#include "draw.h"
|
||||
|
||||
/* Page CSS pixels to TV pixels. */
|
||||
#define C4F_S 1.5f
|
||||
|
||||
/* The page's palette (:root). */
|
||||
#define C4F_TEXT C4F_RGBA(244, 247, 255, 1)
|
||||
#define C4F_MUTED C4F_RGBA(226, 236, 255, .72f)
|
||||
#define C4F_FAINT C4F_RGBA(226, 236, 255, .46f)
|
||||
#define C4F_LINE C4F_RGBA(255, 255, 255, .16f)
|
||||
#define C4F_LINE_SOFT C4F_RGBA(255, 255, 255, .09f)
|
||||
#define C4F_PANEL C4F_RGBA(3, 16, 44, .55f)
|
||||
#define C4F_PANEL_STRONG C4F_RGBA(2, 11, 31, .86f)
|
||||
#define C4F_TILE C4F_RGBA(12, 40, 98, .5f)
|
||||
#define C4F_ACCENT C4F_RGBA(58, 140, 255, 1)
|
||||
#define C4F_OK C4F_RGBA(69, 224, 138, 1)
|
||||
#define C4F_WARN C4F_RGBA(255, 208, 77, 1)
|
||||
#define C4F_BAD C4F_RGBA(255, 107, 125, 1)
|
||||
#define C4F_CIRCLE_KEY C4F_RGBA(255, 102, 128, 1)
|
||||
#define C4F_CROSS_KEY C4F_RGBA(134, 182, 255, 1)
|
||||
#define C4F_SQUARE_KEY C4F_RGBA(255, 146, 220, 1)
|
||||
#define C4F_TRIANGLE_KEY C4F_RGBA(63, 224, 184, 1)
|
||||
#define C4F_WHITE C4F_RGBA(255, 255, 255, 1)
|
||||
|
||||
/* Button guide in the PS4's manner, with the page's face-button colours. */
|
||||
enum { C4F_KEY_CROSS, C4F_KEY_CIRCLE, C4F_KEY_SQUARE, C4F_KEY_TRIANGLE, C4F_KEY_OPTIONS };
|
||||
typedef struct { int key; const char *label; } C4fHint;
|
||||
|
||||
void c4fPaintBackdrop(C4fCanvas *c, float scale);
|
||||
/* The full-screen backdrop without the launcher's blurs, handed over once
|
||||
* (the caller frees it); NULL when it is gone or memory was short. */
|
||||
uint32_t *c4fTakeCleanBackdrop(void);
|
||||
/* backdrop-filter: blur(sigma) under a rounded box, done once on a still backdrop. */
|
||||
void c4fBlurUnder(C4fCanvas *c, float x, float y, float w, float h, float r, float sigma);
|
||||
/* The DualShock 4 mark, its cut-outs showing what is behind. */
|
||||
void c4fMark(C4fCanvas *c, float x, float y, float width, C4fColor color);
|
||||
/* A DualShock 4 with its light bar lit. */
|
||||
void c4fLitController(C4fCanvas *c, float x, float y, float width, float angle,
|
||||
C4fColor body, C4fColor light, int shadow);
|
||||
void c4fTextCentered(C4fCanvas *c, int font, float size, float cx, float y, C4fColor color, const char *text);
|
||||
/* .tile, .panel, .dot */
|
||||
void c4fTile(C4fCanvas *c, float x, float y, float w, float h, C4fColor bar);
|
||||
void c4fPanel(C4fCanvas *c, float x, float y, float w, float h, C4fColor fill, C4fColor line);
|
||||
void c4fDot(C4fCanvas *c, float cx, float cy, C4fColor color, int glow);
|
||||
/* Right-aligned at `right`, centred on cy. */
|
||||
void c4fHints(C4fCanvas *c, float right, float cy, const C4fHint *hints, int count);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,561 @@
|
||||
#include "tester.h"
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "input.h"
|
||||
|
||||
#define C4F_TESTER_STICKS (C4F_INPUT_BUTTON_L3 | C4F_INPUT_BUTTON_R3)
|
||||
#define C4F_TESTER_DIRECTIONS (C4F_INPUT_BUTTON_UP | C4F_INPUT_BUTTON_DOWN | C4F_INPUT_BUTTON_LEFT | C4F_INPUT_BUTTON_RIGHT)
|
||||
#define C4F_TESTER_ACTIVATE (C4F_INPUT_BUTTON_CROSS | C4F_INPUT_BUTTON_CIRCLE)
|
||||
|
||||
int c4fTesterHoldUpdate(C4fTesterHold *hold, uint32_t buttons, uint64_t now)
|
||||
{
|
||||
if ((buttons & C4F_TESTER_STICKS) != C4F_TESTER_STICKS) {
|
||||
hold->holding = hold->fired = 0;
|
||||
return 0;
|
||||
}
|
||||
if (!hold->holding) {
|
||||
hold->holding = 1;
|
||||
hold->since = now;
|
||||
}
|
||||
if (hold->fired || now - hold->since < C4F_TESTER_HOLD_MS) return 0;
|
||||
hold->fired = 1;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void c4fReaderState(C4fTester *t, int slot, int state, uint32_t error)
|
||||
{
|
||||
C4fTesterReader *r = &t->readers[slot];
|
||||
if ((r->state != state || r->error != error) && t->ops.state) t->ops.state(t->ops.context, slot, state, error);
|
||||
r->state = state;
|
||||
r->error = error;
|
||||
}
|
||||
|
||||
/* Undo this test's output on a reader whose handle still works. */
|
||||
static void c4fReaderQuiet(C4fTester *t, int slot)
|
||||
{
|
||||
C4fTesterReader *r = &t->readers[slot];
|
||||
if (r->handle >= 0 && t->rumble && t->rumbleReader == slot && t->ops.vibrate)
|
||||
t->ops.vibrate(t->ops.context, r->handle, 0, 0);
|
||||
if (r->handle >= 0 && r->lightSet && t->ops.resetLightBar)
|
||||
t->ops.resetLightBar(t->ops.context, r->handle);
|
||||
if (t->rumble && t->rumbleReader == slot) t->rumble = 0;
|
||||
r->lightSet = 0;
|
||||
}
|
||||
|
||||
static void c4fReaderRelease(C4fTester *t, int slot)
|
||||
{
|
||||
C4fTesterReader *r = &t->readers[slot];
|
||||
if (r->handle >= 0 && r->owned && t->ops.close) t->ops.close(t->ops.context, r->handle);
|
||||
if (t->rumble && t->rumbleReader == slot) t->rumble = 0;
|
||||
r->handle = -1;
|
||||
r->owned = r->lightSet = r->seen = 0;
|
||||
r->previous = r->pressed = 0;
|
||||
r->menuBlocked = r->repeatDirection = 0;
|
||||
r->repeatAt = 0;
|
||||
memset(&r->hold, 0, sizeof(r->hold));
|
||||
memset(&r->sample, 0, sizeof(r->sample));
|
||||
}
|
||||
|
||||
static void c4fReaderEmpty(C4fTester *t, int slot)
|
||||
{
|
||||
c4fReaderRelease(t, slot);
|
||||
C4fTesterReader *r = &t->readers[slot];
|
||||
r->user = -1;
|
||||
r->state = C4F_TESTER_OPENING;
|
||||
r->error = 0;
|
||||
r->name[0] = 0;
|
||||
}
|
||||
|
||||
/* Signed-out readers stay only while they are the one on screen. */
|
||||
static void c4fTesterForgetSignedOut(C4fTester *t)
|
||||
{
|
||||
for (int s = 0; s < C4F_TESTER_USERS; s++)
|
||||
if (s != t->selected && t->readers[s].user >= 0 && t->readers[s].state == C4F_TESTER_SIGNED_OUT)
|
||||
c4fReaderEmpty(t, s);
|
||||
}
|
||||
|
||||
static void c4fTesterSelectFirst(C4fTester *t)
|
||||
{
|
||||
if (t->selected >= 0 && t->readers[t->selected].user >= 0) return;
|
||||
t->selected = -1;
|
||||
for (int s = 0; s < C4F_TESTER_USERS && t->selected < 0; s++)
|
||||
if (t->readers[s].user >= 0) t->selected = s;
|
||||
}
|
||||
|
||||
static void c4fTesterUsers(C4fTester *t, uint64_t now)
|
||||
{
|
||||
int32_t users[C4F_TESTER_USERS];
|
||||
t->usersAt = now + C4F_TESTER_RETRY_MS;
|
||||
if (!t->ops.loginUsers) return;
|
||||
for (int i = 0; i < C4F_TESTER_USERS; i++) users[i] = -1;
|
||||
int count = t->ops.loginUsers(t->ops.context, users);
|
||||
if (count < 0) return; /* keep what is known until the next try */
|
||||
if (count > C4F_TESTER_USERS) count = C4F_TESTER_USERS;
|
||||
for (int s = 0; s < C4F_TESTER_USERS; s++) {
|
||||
C4fTesterReader *r = &t->readers[s];
|
||||
if (r->user < 0) continue;
|
||||
int present = 0;
|
||||
for (int i = 0; i < count; i++) present |= users[i] == r->user;
|
||||
if (present) {
|
||||
if (r->state == C4F_TESTER_SIGNED_OUT) {
|
||||
c4fReaderState(t, s, C4F_TESTER_OPENING, 0);
|
||||
r->retryAt = now;
|
||||
}
|
||||
} else if (r->state != C4F_TESTER_SIGNED_OUT) {
|
||||
c4fReaderQuiet(t, s);
|
||||
if (s == t->selected) {
|
||||
c4fReaderRelease(t, s);
|
||||
c4fReaderState(t, s, C4F_TESTER_SIGNED_OUT, 0);
|
||||
} else {
|
||||
c4fReaderEmpty(t, s);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < count; i++) {
|
||||
int known = users[i] < 0;
|
||||
for (int s = 0; s < C4F_TESTER_USERS && !known; s++) known = t->readers[s].user == users[i];
|
||||
for (int s = 0; s < C4F_TESTER_USERS && !known; s++) {
|
||||
C4fTesterReader *r = &t->readers[s];
|
||||
if (r->user >= 0) continue;
|
||||
r->user = users[i];
|
||||
r->name[0] = 0;
|
||||
if (t->ops.userName) t->ops.userName(t->ops.context, r->user, r->name, (int)sizeof(r->name));
|
||||
r->name[sizeof(r->name) - 1] = 0;
|
||||
/* Names are drawn in plain ASCII. */
|
||||
for (char *p = r->name; *p; p++) if ((unsigned char)*p < 32 || (unsigned char)*p > 126) *p = '?';
|
||||
if (!r->name[0]) snprintf(r->name, sizeof(r->name), "User %d", s + 1);
|
||||
r->retryAt = now;
|
||||
c4fReaderState(t, s, C4F_TESTER_OPENING, 0);
|
||||
known = 1;
|
||||
}
|
||||
}
|
||||
c4fTesterSelectFirst(t);
|
||||
}
|
||||
|
||||
static void c4fReaderPoll(C4fTester *t, int slot, uint64_t now)
|
||||
{
|
||||
C4fTesterReader *r = &t->readers[slot];
|
||||
r->pressed = 0;
|
||||
if (r->user < 0 || r->state == C4F_TESTER_SIGNED_OUT) return;
|
||||
if (r->handle < 0) {
|
||||
if (now < r->retryAt || !t->ops.open) return;
|
||||
int owned = 0, handle = t->ops.open(t->ops.context, r->user, &owned);
|
||||
if (handle < 0) {
|
||||
c4fReaderState(t, slot, C4F_TESTER_FAILED, (uint32_t)handle);
|
||||
r->retryAt = now + C4F_TESTER_RETRY_MS;
|
||||
return;
|
||||
}
|
||||
r->handle = handle;
|
||||
r->owned = !!owned;
|
||||
r->seen = 0;
|
||||
/* Games turn the motion sensor on for their own handles; so does this. */
|
||||
if (t->ops.motionSensor) t->ops.motionSensor(t->ops.context, handle, 1);
|
||||
}
|
||||
C4fTesterSample sample;
|
||||
memset(&sample, 0, sizeof(sample));
|
||||
int result = t->ops.read ? t->ops.read(t->ops.context, r->handle, &sample) : -1;
|
||||
if (result != 0) {
|
||||
c4fReaderQuiet(t, slot);
|
||||
c4fReaderRelease(t, slot);
|
||||
c4fReaderState(t, slot, C4F_TESTER_FAILED, (uint32_t)result);
|
||||
r->retryAt = now + C4F_TESTER_RETRY_MS;
|
||||
return;
|
||||
}
|
||||
if (!sample.connected) {
|
||||
c4fReaderQuiet(t, slot);
|
||||
memset(&r->sample, 0, sizeof(r->sample));
|
||||
r->previous = 0;
|
||||
r->seen = 0;
|
||||
c4fReaderState(t, slot, C4F_TESTER_NO_CONTROLLER, 0);
|
||||
return;
|
||||
}
|
||||
if (sample.touches > 2) sample.touches = 2;
|
||||
if (!r->seen) {
|
||||
/* A hold already in progress when a reader starts cannot open a menu. */
|
||||
r->hold.holding = r->hold.fired = 1;
|
||||
r->hold.since = now;
|
||||
}
|
||||
if (!r->seen && t->menu) r->menuBlocked |= sample.buttons & (C4F_TESTER_DIRECTIONS | C4F_TESTER_ACTIVATE);
|
||||
/* Buttons already held when reading starts are shown, but are not presses. */
|
||||
r->pressed = r->seen ? sample.buttons & ~r->previous : 0;
|
||||
r->previous = sample.buttons;
|
||||
r->seen = 1;
|
||||
r->sample = sample;
|
||||
c4fReaderState(t, slot, C4F_TESTER_READY, 0);
|
||||
}
|
||||
|
||||
static void c4fTesterStopRumble(C4fTester *t)
|
||||
{
|
||||
if (!t->rumble) return;
|
||||
C4fTesterReader *r = &t->readers[t->rumbleReader];
|
||||
if (r->handle >= 0 && t->ops.vibrate) t->ops.vibrate(t->ops.context, r->handle, 0, 0);
|
||||
t->rumble = 0;
|
||||
}
|
||||
|
||||
static void c4fTesterNextUser(C4fTester *t, int direction)
|
||||
{
|
||||
int start = t->selected >= 0 ? t->selected : 0;
|
||||
for (int step = 1; step <= C4F_TESTER_USERS; step++) {
|
||||
int s = (start + direction * step + 2 * C4F_TESTER_USERS) % C4F_TESTER_USERS;
|
||||
if (t->readers[s].user >= 0 && t->readers[s].state != C4F_TESTER_SIGNED_OUT) {
|
||||
t->selected = s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
c4fTesterForgetSignedOut(t);
|
||||
t->note[0] = 0;
|
||||
}
|
||||
|
||||
static void c4fTesterResult(C4fTester *t, int result, const char *done)
|
||||
{
|
||||
if (result) snprintf(t->note, sizeof(t->note), "The PS4 refused that (0x%08x).", (unsigned)result);
|
||||
else snprintf(t->note, sizeof(t->note), "%s", done);
|
||||
}
|
||||
|
||||
static void c4fTesterAct(C4fTester *t, int item, uint64_t now)
|
||||
{
|
||||
C4fTesterView view;
|
||||
c4fTesterView(t, &view);
|
||||
if (!c4fTesterItemAvailable(&view, item)) {
|
||||
snprintf(t->note, sizeof(t->note), "%s", item == C4F_TESTER_USER ?
|
||||
"No other user is signed in." : "This user has no controller to test.");
|
||||
return;
|
||||
}
|
||||
C4fTesterReader *r = t->selected >= 0 ? &t->readers[t->selected] : NULL;
|
||||
if (item == C4F_TESTER_RESUME) {
|
||||
t->menu = 0;
|
||||
return;
|
||||
}
|
||||
if (item == C4F_TESTER_USER) {
|
||||
c4fTesterNextUser(t, 1);
|
||||
return;
|
||||
}
|
||||
if (!r || r->user < 0 || r->handle < 0 || r->state != C4F_TESTER_READY) {
|
||||
snprintf(t->note, sizeof(t->note), "This user has no controller to test.");
|
||||
return;
|
||||
}
|
||||
switch (item) {
|
||||
case C4F_TESTER_RUMBLE_LARGE:
|
||||
case C4F_TESTER_RUMBLE_SMALL: {
|
||||
int large = item == C4F_TESTER_RUMBLE_LARGE;
|
||||
c4fTesterStopRumble(t);
|
||||
int result = t->ops.vibrate ? t->ops.vibrate(t->ops.context, r->handle, large ? 255 : 0, large ? 0 : 255) : -1;
|
||||
if (!result) {
|
||||
t->rumble = large ? 1 : 2;
|
||||
t->rumbleReader = t->selected;
|
||||
t->rumbleUntil = now + C4F_TESTER_RUMBLE_MS;
|
||||
}
|
||||
c4fTesterResult(t, result, large ? "Large motor: rumbling for a second." : "Small motor: rumbling for a second.");
|
||||
break;
|
||||
}
|
||||
case C4F_TESTER_LIGHT_COLOR: {
|
||||
uint8_t rgb[3] = { 255, 255, 255 };
|
||||
if (r->lightSet) memcpy(rgb, r->light, sizeof(rgb));
|
||||
int result = t->ops.lightBar ? t->ops.lightBar(t->ops.context, r->handle, rgb[0], rgb[1], rgb[2]) : -1;
|
||||
if (!result) {
|
||||
r->lightSet = 1;
|
||||
memcpy(r->light, rgb, 3);
|
||||
t->colorMenu = 1;
|
||||
t->note[0] = 0;
|
||||
} else {
|
||||
c4fTesterResult(t, result, "");
|
||||
}
|
||||
break;
|
||||
}
|
||||
case C4F_TESTER_LIGHT_RESET: {
|
||||
int result = t->ops.resetLightBar ? t->ops.resetLightBar(t->ops.context, r->handle) : -1;
|
||||
if (!result) r->lightSet = 0;
|
||||
c4fTesterResult(t, result, "Light bar reset.");
|
||||
break;
|
||||
}
|
||||
case C4F_TESTER_ORIENTATION: {
|
||||
int result = t->ops.resetOrientation ? t->ops.resetOrientation(t->ops.context, r->handle) : -1;
|
||||
c4fTesterResult(t, result, "Orientation reset.");
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void c4fTesterInit(C4fTester *t, const C4fTesterOps *ops)
|
||||
{
|
||||
memset(t, 0, sizeof(*t));
|
||||
if (ops) t->ops = *ops;
|
||||
t->selected = -1;
|
||||
for (int s = 0; s < C4F_TESTER_USERS; s++) {
|
||||
t->readers[s].user = -1;
|
||||
t->readers[s].handle = -1;
|
||||
}
|
||||
}
|
||||
|
||||
void c4fTesterOpen(C4fTester *t, uint64_t now)
|
||||
{
|
||||
if (t->active) return;
|
||||
C4fTesterOps ops = t->ops;
|
||||
c4fTesterInit(t, &ops);
|
||||
t->active = 1;
|
||||
c4fTesterUsers(t, now);
|
||||
}
|
||||
|
||||
static void c4fTesterQuietAll(C4fTester *t)
|
||||
{
|
||||
c4fTesterStopRumble(t);
|
||||
for (int s = 0; s < C4F_TESTER_USERS; s++) c4fReaderQuiet(t, s);
|
||||
}
|
||||
|
||||
void c4fTesterSuspend(C4fTester *t, uint64_t now)
|
||||
{
|
||||
if (!t->active) return;
|
||||
c4fTesterQuietAll(t);
|
||||
for (int s = 0; s < C4F_TESTER_USERS; s++) {
|
||||
C4fTesterReader *r = &t->readers[s];
|
||||
c4fReaderRelease(t, s);
|
||||
if (r->user >= 0 && r->state != C4F_TESTER_SIGNED_OUT) {
|
||||
c4fReaderState(t, s, C4F_TESTER_OPENING, 0);
|
||||
r->retryAt = now;
|
||||
}
|
||||
}
|
||||
t->menu = 0;
|
||||
t->colorMenu = 0;
|
||||
t->menuOpeners = 0;
|
||||
t->armed = 0;
|
||||
}
|
||||
|
||||
void c4fTesterClose(C4fTester *t)
|
||||
{
|
||||
if (!t->active) return;
|
||||
c4fTesterQuietAll(t);
|
||||
for (int s = 0; s < C4F_TESTER_USERS; s++) c4fReaderRelease(t, s);
|
||||
C4fTesterOps ops = t->ops;
|
||||
c4fTesterInit(t, &ops);
|
||||
}
|
||||
|
||||
static void c4fTesterMenuOpen(C4fTester *t, int opener)
|
||||
{
|
||||
t->menu = 1;
|
||||
t->colorMenu = 0;
|
||||
t->focus = C4F_TESTER_RESUME;
|
||||
t->menuOpeners = opener >= 0 ? 1u << opener : 0;
|
||||
for (int s = 0; s < C4F_TESTER_USERS; s++) {
|
||||
C4fTesterReader *r = &t->readers[s];
|
||||
uint32_t buttons = r->state == C4F_TESTER_READY ? r->sample.buttons : 0;
|
||||
r->menuBlocked = buttons & (C4F_TESTER_DIRECTIONS | C4F_TESTER_ACTIVATE);
|
||||
r->repeatDirection = 0;
|
||||
r->repeatAt = 0;
|
||||
}
|
||||
t->armed = !t->menuOpeners;
|
||||
}
|
||||
|
||||
/* A held direction repeats, but activation always needs its own fresh edge.
|
||||
* Track each controller separately so a key held on another pad cannot mask a
|
||||
* new press, or create a repeat from input held before the menu opened. */
|
||||
static uint32_t c4fTesterMenuDirection(C4fTesterReader *r, uint64_t now)
|
||||
{
|
||||
uint32_t held = r->sample.buttons & ~r->menuBlocked & C4F_TESTER_DIRECTIONS;
|
||||
if ((held & (C4F_INPUT_BUTTON_UP | C4F_INPUT_BUTTON_DOWN)) == (C4F_INPUT_BUTTON_UP | C4F_INPUT_BUTTON_DOWN))
|
||||
held &= ~(C4F_INPUT_BUTTON_UP | C4F_INPUT_BUTTON_DOWN);
|
||||
if ((held & (C4F_INPUT_BUTTON_LEFT | C4F_INPUT_BUTTON_RIGHT)) == (C4F_INPUT_BUTTON_LEFT | C4F_INPUT_BUTTON_RIGHT))
|
||||
held &= ~(C4F_INPUT_BUTTON_LEFT | C4F_INPUT_BUTTON_RIGHT);
|
||||
uint32_t fresh = r->pressed & held;
|
||||
uint32_t candidates = fresh ? fresh : held, direction = 0;
|
||||
static const uint32_t directions[] = { C4F_INPUT_BUTTON_UP, C4F_INPUT_BUTTON_DOWN, C4F_INPUT_BUTTON_LEFT, C4F_INPUT_BUTTON_RIGHT };
|
||||
for (unsigned i = 0; i < sizeof(directions) / sizeof(directions[0]); i++)
|
||||
if (candidates & directions[i]) { direction = directions[i]; break; }
|
||||
if (!direction || (!fresh && direction != r->repeatDirection)) {
|
||||
r->repeatDirection = 0; r->repeatAt = 0;
|
||||
return 0;
|
||||
}
|
||||
if (fresh) {
|
||||
r->repeatDirection = direction;
|
||||
r->repeatAt = now + C4F_TESTER_REPEAT_DELAY_MS;
|
||||
return direction;
|
||||
}
|
||||
if (now < r->repeatAt) return 0;
|
||||
r->repeatAt = now + C4F_TESTER_REPEAT_MS;
|
||||
return direction;
|
||||
}
|
||||
|
||||
static void c4fTesterMenuMove(C4fTester *t, uint32_t direction)
|
||||
{
|
||||
int row = t->focus / C4F_TESTER_MENU_COLUMNS;
|
||||
int column = t->focus % C4F_TESTER_MENU_COLUMNS;
|
||||
if (direction & C4F_INPUT_BUTTON_UP) {
|
||||
row = (row + C4F_TESTER_MENU_ROWS - 1) % C4F_TESTER_MENU_ROWS;
|
||||
} else if (direction & C4F_INPUT_BUTTON_DOWN) {
|
||||
row = (row + 1) % C4F_TESTER_MENU_ROWS;
|
||||
} else if (direction & C4F_INPUT_BUTTON_LEFT) {
|
||||
column = (column + C4F_TESTER_MENU_COLUMNS - 1) % C4F_TESTER_MENU_COLUMNS;
|
||||
} else if (direction & C4F_INPUT_BUTTON_RIGHT) {
|
||||
column = (column + 1) % C4F_TESTER_MENU_COLUMNS;
|
||||
}
|
||||
t->focus = row * C4F_TESTER_MENU_COLUMNS + column;
|
||||
}
|
||||
|
||||
static void c4fTesterColorInput(C4fTester *t, uint32_t navigation, uint32_t pressed)
|
||||
{
|
||||
if (pressed & C4F_INPUT_BUTTON_CIRCLE) {
|
||||
t->colorMenu = 0;
|
||||
snprintf(t->note, sizeof(t->note), "Light bar set. It goes back to normal when you leave.");
|
||||
return;
|
||||
}
|
||||
if (navigation & C4F_INPUT_BUTTON_UP) t->colorChannel = (t->colorChannel + 2) % 3;
|
||||
else if (navigation & C4F_INPUT_BUTTON_DOWN) t->colorChannel = (t->colorChannel + 1) % 3;
|
||||
else if (navigation & (C4F_INPUT_BUTTON_LEFT | C4F_INPUT_BUTTON_RIGHT)) {
|
||||
C4fTesterReader *r = &t->readers[t->selected];
|
||||
uint8_t rgb[3];
|
||||
memcpy(rgb, r->light, sizeof(rgb));
|
||||
const int direction = navigation & C4F_INPUT_BUTTON_LEFT ? -1 : 1;
|
||||
int value = rgb[t->colorChannel] + direction * (t->colorFine ? 1 : 16);
|
||||
if (value < 0) value = 0;
|
||||
if (value > 255) value = 255;
|
||||
if (value == rgb[t->colorChannel]) return;
|
||||
rgb[t->colorChannel] = (uint8_t)value;
|
||||
int result = t->ops.lightBar ? t->ops.lightBar(t->ops.context, r->handle, rgb[0], rgb[1], rgb[2]) : -1;
|
||||
if (!result) {
|
||||
r->lightSet = 1;
|
||||
memcpy(r->light, rgb, sizeof(rgb));
|
||||
t->note[0] = 0;
|
||||
} else c4fTesterResult(t, result, "");
|
||||
} else if (pressed & C4F_INPUT_BUTTON_CROSS) t->colorFine = !t->colorFine;
|
||||
}
|
||||
|
||||
int c4fTesterFrame(C4fTester *t, uint64_t now, uint32_t remotePressed)
|
||||
{
|
||||
if (!t->active) return 0;
|
||||
if (now >= t->usersAt) c4fTesterUsers(t, now);
|
||||
int menuOpener = -1;
|
||||
for (int s = 0; s < C4F_TESTER_USERS; s++) {
|
||||
c4fReaderPoll(t, s, now);
|
||||
C4fTesterReader *r = &t->readers[s];
|
||||
if (c4fTesterHoldUpdate(&r->hold, r->state == C4F_TESTER_READY ? r->sample.buttons : 0, now) && menuOpener < 0)
|
||||
menuOpener = s;
|
||||
}
|
||||
if (t->rumble && now >= t->rumbleUntil) c4fTesterStopRumble(t);
|
||||
if (t->colorMenu && (t->selected < 0 || t->readers[t->selected].state != C4F_TESTER_READY)) {
|
||||
t->colorMenu = 0;
|
||||
snprintf(t->note, sizeof(t->note), "This user's controller is unavailable.");
|
||||
}
|
||||
if (menuOpener >= 0 && !t->menu) {
|
||||
c4fTesterMenuOpen(t, menuOpener);
|
||||
return 1;
|
||||
}
|
||||
if (!t->menu) {
|
||||
/* The remote is not under test, so it may leave and open the menu. */
|
||||
if (remotePressed & C4F_INPUT_BUTTON_CIRCLE) {
|
||||
c4fTesterClose(t);
|
||||
return 0;
|
||||
}
|
||||
if (remotePressed & (C4F_INPUT_BUTTON_CROSS | C4F_INPUT_BUTTON_OPTIONS)) {
|
||||
c4fTesterMenuOpen(t, -1);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
for (int s = 0; s < C4F_TESTER_USERS; s++) {
|
||||
const C4fTesterReader *r = &t->readers[s];
|
||||
if (r->state != C4F_TESTER_READY || !(r->sample.buttons & C4F_TESTER_STICKS)) t->menuOpeners &= ~(1u << s);
|
||||
}
|
||||
t->armed = !t->menuOpeners;
|
||||
uint32_t navigation = remotePressed & C4F_TESTER_DIRECTIONS;
|
||||
uint32_t pressed = t->armed ? remotePressed & C4F_TESTER_ACTIVATE : 0;
|
||||
for (int s = 0; s < C4F_TESTER_USERS; s++) {
|
||||
C4fTesterReader *r = &t->readers[s];
|
||||
if (r->state != C4F_TESTER_READY) continue;
|
||||
r->menuBlocked &= r->sample.buttons;
|
||||
if (!t->armed) r->menuBlocked |= r->sample.buttons & C4F_TESTER_ACTIVATE;
|
||||
navigation |= c4fTesterMenuDirection(r, now);
|
||||
if (t->armed) pressed |= r->pressed & ~r->menuBlocked & C4F_TESTER_ACTIVATE;
|
||||
}
|
||||
if (t->colorMenu) {
|
||||
c4fTesterColorInput(t, navigation, pressed);
|
||||
} else if (pressed & C4F_INPUT_BUTTON_CIRCLE) {
|
||||
t->menu = 0;
|
||||
} else if (navigation) {
|
||||
c4fTesterMenuMove(t, navigation);
|
||||
} else if (pressed & C4F_INPUT_BUTTON_CROSS) {
|
||||
if (t->focus == C4F_TESTER_EXIT) {
|
||||
c4fTesterClose(t);
|
||||
return 0;
|
||||
}
|
||||
c4fTesterAct(t, t->focus, now);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int c4fTesterItemAvailable(const C4fTesterView *v, int item)
|
||||
{
|
||||
if (item < 0 || item >= C4F_TESTER_ITEMS) return 0;
|
||||
if (item == C4F_TESTER_RESUME || item == C4F_TESTER_EXIT) return 1;
|
||||
if (item == C4F_TESTER_USER) {
|
||||
for (int s = 0; s < C4F_TESTER_USERS; s++)
|
||||
if (s != v->selected && v->user[s].used && v->user[s].state != C4F_TESTER_SIGNED_OUT) return 1;
|
||||
return 0;
|
||||
}
|
||||
return v->selected >= 0 && v->selected < C4F_TESTER_USERS && v->user[v->selected].state == C4F_TESTER_READY;
|
||||
}
|
||||
|
||||
static int16_t c4fScaled(float value, float scale)
|
||||
{
|
||||
if (isnan(value)) return 0;
|
||||
float v = roundf(value * scale);
|
||||
return (int16_t)(v > 32767 ? 32767 : v < -32767 ? -32767 : v);
|
||||
}
|
||||
|
||||
void c4fTesterView(const C4fTester *t, C4fTesterView *v)
|
||||
{
|
||||
memset(v, 0, sizeof(*v));
|
||||
for (int s = 0; s < C4F_TESTER_USERS; s++) v->user[s].id = -1;
|
||||
if (!t->active) return;
|
||||
v->active = 1;
|
||||
v->menu = t->menu;
|
||||
v->focus = t->focus;
|
||||
v->colorMenu = t->colorMenu;
|
||||
v->colorChannel = t->colorChannel;
|
||||
v->colorFine = t->colorFine;
|
||||
v->selected = t->selected;
|
||||
for (int s = 0; s < C4F_TESTER_USERS; s++) {
|
||||
const C4fTesterReader *r = &t->readers[s];
|
||||
v->user[s].used = r->user >= 0;
|
||||
if (!v->user[s].used) continue;
|
||||
v->user[s].id = r->user;
|
||||
snprintf(v->user[s].name, sizeof(v->user[s].name), "%s", r->name);
|
||||
v->user[s].state = r->state;
|
||||
v->user[s].error = r->error;
|
||||
}
|
||||
snprintf(v->note, sizeof(v->note), "%s", t->note);
|
||||
if (t->selected < 0) return;
|
||||
const C4fTesterReader *r = &t->readers[t->selected];
|
||||
v->rumble = t->rumble && t->rumbleReader == t->selected ? t->rumble : 0;
|
||||
v->lightSet = r->lightSet;
|
||||
memcpy(v->light, r->light, 3);
|
||||
if (r->state != C4F_TESTER_READY) return;
|
||||
const C4fTesterSample *s = &r->sample;
|
||||
v->buttons = s->buttons;
|
||||
v->lx = s->lx; v->ly = s->ly; v->rx = s->rx; v->ry = s->ry; v->l2 = s->l2; v->r2 = s->r2;
|
||||
v->touches = s->touches;
|
||||
for (int i = 0; i < v->touches; i++) {
|
||||
v->touchX[i] = s->touch[i].x;
|
||||
v->touchY[i] = s->touch[i].y;
|
||||
}
|
||||
for (int i = 0; i < 4; i++) v->quat[i] = c4fScaled(s->quat[i], 1000);
|
||||
for (int i = 0; i < 3; i++) {
|
||||
v->accel[i] = c4fScaled(s->accel[i], 100);
|
||||
v->gyro[i] = c4fScaled(s->gyro[i], 100);
|
||||
}
|
||||
}
|
||||
|
||||
const char *c4fTesterItemLabel(int item)
|
||||
{
|
||||
switch (item) {
|
||||
case C4F_TESTER_RESUME: return "Resume";
|
||||
case C4F_TESTER_USER: return "Switch user";
|
||||
case C4F_TESTER_RUMBLE_LARGE: return "Rumble the large motor";
|
||||
case C4F_TESTER_RUMBLE_SMALL: return "Rumble the small motor";
|
||||
case C4F_TESTER_LIGHT_COLOR: return "Choose the light-bar color";
|
||||
case C4F_TESTER_LIGHT_RESET: return "Reset the light bar";
|
||||
case C4F_TESTER_ORIENTATION: return "Reset orientation";
|
||||
case C4F_TESTER_EXIT: return "Back to the launcher";
|
||||
default: return "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
#ifndef C4F_LAUNCHER_TESTER_H
|
||||
#define C4F_LAUNCHER_TESTER_H
|
||||
/* Controller test: what each signed-in user's controller sends, read the way a
|
||||
* game reads it, with rumble and light-bar test actions. The native pad and
|
||||
* user calls are injected, so this logic also runs in host tests. */
|
||||
#include <stdint.h>
|
||||
|
||||
#define C4F_TESTER_USERS 4
|
||||
#define C4F_TESTER_HOLD_MS 2000
|
||||
#define C4F_TESTER_RUMBLE_MS 1000
|
||||
#define C4F_TESTER_RETRY_MS 1000
|
||||
#define C4F_TESTER_NAME 24
|
||||
#define C4F_TESTER_MENU_COLUMNS 4
|
||||
#define C4F_TESTER_REPEAT_DELAY_MS 350
|
||||
#define C4F_TESTER_REPEAT_MS 100
|
||||
|
||||
/* One sample as the read side reports it. */
|
||||
typedef struct {
|
||||
uint32_t buttons;
|
||||
uint8_t lx, ly, rx, ry, l2, r2, touches;
|
||||
struct { uint16_t x, y; uint8_t id; } touch[2];
|
||||
float quat[4]; /* orientation x y z w */
|
||||
float accel[3]; /* G */
|
||||
float gyro[3]; /* rad/s */
|
||||
int connected;
|
||||
} C4fTesterSample;
|
||||
|
||||
typedef struct {
|
||||
void *context;
|
||||
/* Fills up to C4F_TESTER_USERS signed-in user IDs; returns how many, or a
|
||||
* negative error. */
|
||||
int (*loginUsers)(void *context, int32_t *users);
|
||||
/* The name the PS4 shows. Only ever drawn, never logged. */
|
||||
void (*userName)(void *context, int32_t user, char *name, int size);
|
||||
/* A handle, or a negative error. owned as in C4fInputOps: a handle borrowed
|
||||
* from the launcher's own reader must not be closed here. */
|
||||
int (*open)(void *context, int32_t user, int *owned);
|
||||
int (*read)(void *context, int handle, C4fTesterSample *sample);
|
||||
void (*close)(void *context, int handle);
|
||||
int (*vibrate)(void *context, int handle, uint8_t large, uint8_t small);
|
||||
int (*lightBar)(void *context, int handle, uint8_t r, uint8_t g, uint8_t b);
|
||||
int (*resetLightBar)(void *context, int handle);
|
||||
int (*resetOrientation)(void *context, int handle);
|
||||
int (*motionSensor)(void *context, int handle, int on);
|
||||
/* Optional: a reader changed state (for the log; slot, never the user). */
|
||||
void (*state)(void *context, int slot, int state, uint32_t error);
|
||||
} C4fTesterOps;
|
||||
|
||||
/* Per user. */
|
||||
enum { C4F_TESTER_OPENING, C4F_TESTER_READY, C4F_TESTER_NO_CONTROLLER, C4F_TESTER_FAILED, C4F_TESTER_SIGNED_OUT };
|
||||
|
||||
/* The menu, in order. */
|
||||
typedef enum {
|
||||
C4F_TESTER_RESUME, C4F_TESTER_USER, C4F_TESTER_ORIENTATION, C4F_TESTER_LIGHT_COLOR,
|
||||
C4F_TESTER_RUMBLE_LARGE, C4F_TESTER_RUMBLE_SMALL, C4F_TESTER_LIGHT_RESET,
|
||||
C4F_TESTER_EXIT, C4F_TESTER_ITEMS
|
||||
} C4fTesterItem;
|
||||
#define C4F_TESTER_MENU_ROWS ((C4F_TESTER_ITEMS + C4F_TESTER_MENU_COLUMNS - 1) / C4F_TESTER_MENU_COLUMNS)
|
||||
|
||||
/* L3 and R3 held together for C4F_TESTER_HOLD_MS. */
|
||||
typedef struct {
|
||||
int holding, fired;
|
||||
uint64_t since;
|
||||
} C4fTesterHold;
|
||||
|
||||
typedef struct {
|
||||
int32_t user; /* -1: empty slot */
|
||||
int handle, owned, state, lightSet, seen;
|
||||
uint32_t error, previous, pressed, menuBlocked, repeatDirection;
|
||||
uint64_t retryAt, repeatAt;
|
||||
C4fTesterHold hold;
|
||||
uint8_t light[3];
|
||||
char name[C4F_TESTER_NAME];
|
||||
C4fTesterSample sample;
|
||||
} C4fTesterReader;
|
||||
|
||||
typedef struct {
|
||||
C4fTesterOps ops;
|
||||
int active, menu, focus, armed, selected, rumble, rumbleReader;
|
||||
int colorMenu, colorChannel, colorFine;
|
||||
unsigned menuOpeners;
|
||||
uint64_t usersAt, rumbleUntil;
|
||||
C4fTesterReader readers[C4F_TESTER_USERS];
|
||||
char note[96];
|
||||
} C4fTester;
|
||||
|
||||
/* What the screen shows, in whole display units so that an unchanged reading
|
||||
* compares equal and causes no redraw. */
|
||||
typedef struct {
|
||||
int active, menu, focus, selected, rumble, lightSet;
|
||||
int colorMenu, colorChannel, colorFine;
|
||||
uint8_t light[3];
|
||||
struct { int32_t id; char name[C4F_TESTER_NAME]; int state, used; uint32_t error; } user[C4F_TESTER_USERS];
|
||||
uint32_t buttons;
|
||||
uint8_t lx, ly, rx, ry, l2, r2, touches;
|
||||
uint16_t touchX[2], touchY[2];
|
||||
int16_t quat[4]; /* x 1000 */
|
||||
int16_t accel[3]; /* x 100, G */
|
||||
int16_t gyro[3]; /* x 100, rad/s */
|
||||
char note[96];
|
||||
} C4fTesterView;
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
/* True once per hold; the buttons must be released before it fires again. */
|
||||
int c4fTesterHoldUpdate(C4fTesterHold *hold, uint32_t buttons, uint64_t now);
|
||||
void c4fTesterInit(C4fTester *tester, const C4fTesterOps *ops);
|
||||
void c4fTesterOpen(C4fTester *tester, uint64_t now);
|
||||
/* One frame. remotePressed is the TV remote's new presses: OK and Options open
|
||||
* the menu, Back closes it or leaves. Returns 0 once the tester has closed. */
|
||||
int c4fTesterFrame(C4fTester *tester, uint64_t now, uint32_t remotePressed);
|
||||
/* The app lost focus: stop test output and reopen every reader afterwards. */
|
||||
void c4fTesterSuspend(C4fTester *tester, uint64_t now);
|
||||
/* Stops rumble, resets light bars this test set, closes owned handles. */
|
||||
void c4fTesterClose(C4fTester *tester);
|
||||
void c4fTesterView(const C4fTester *tester, C4fTesterView *view);
|
||||
const char *c4fTesterItemLabel(int item);
|
||||
/* Shared by menu drawing and activation. Unavailable tiles may take focus. */
|
||||
int c4fTesterItemAvailable(const C4fTesterView *view, int item);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
@@ -10,14 +10,16 @@
|
||||
|
||||
#include "c4f_vda.h"
|
||||
|
||||
/* scePadVirtualDeviceGetRemoteSetting fills a buffer laid out like the DualShock
|
||||
* 4's HID output report without its report ID. Read on firmware 10.01
|
||||
* (2026-10-05) while games rumbled and users signed in:
|
||||
/* scePadVirtualDeviceGetRemoteSetting fills a buffer much like the DualShock 4's
|
||||
* HID output report without its report ID. Read on firmware 10.01 while games
|
||||
* rumbled and users signed in (2026-10-05), and with the PS4 app's controller
|
||||
* test driving each motor on its own (2026-10-16):
|
||||
*
|
||||
* [0] update flags: bit 0 rumble, bit 1 light bar, as in the DS4 report.
|
||||
* Pulses for a few milliseconds, so it is not relied on here.
|
||||
* [3] right motor, the small fast one: 0xff for ~300 ms on a game's hit.
|
||||
* [4] left motor, the large slow one.
|
||||
* [3] the large, slow motor: 0xff for ~300 ms on a game's hit. Not the
|
||||
* DS4 report's order: scePadSetVibration's large motor lands here.
|
||||
* [4] the small, fast motor.
|
||||
* [5..7] light bar red, green, blue: 0x40 at full, as the PS4 sets them
|
||||
* (40 00 00 for player 2, 00 40 00 for player 3).
|
||||
* [16] 1 once a user is assigned.
|
||||
@@ -26,8 +28,8 @@ void c4fPadFeedbackParse(const uint8_t *buf, size_t size, C4fPadFeedback *out)
|
||||
{
|
||||
(void)memset(out, 0, sizeof(*out));
|
||||
if (size < 8) return;
|
||||
out->small = buf[3];
|
||||
out->large = buf[4];
|
||||
out->large = buf[3];
|
||||
out->small = buf[4];
|
||||
out->r = buf[5];
|
||||
out->g = buf[6];
|
||||
out->b = buf[7];
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
/* Bounded HTTP/WebSocket transport for the embedded controller page. */
|
||||
/* Bounded HTTP transport for the embedded controller page: the page itself,
|
||||
* the launcher's requests, and the WebRTC offer (POST /rtc). The page's
|
||||
* connection then runs over UDP (rtc.c). The WebSocket path stays for the
|
||||
* host tests. */
|
||||
#include <arpa/inet.h>
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
@@ -6,16 +9,17 @@
|
||||
#include <netinet/in.h>
|
||||
#include <netinet/tcp.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <sys/select.h>
|
||||
#include <sys/socket.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#include "c4f_log.h"
|
||||
#include "c4f_net.h"
|
||||
|
||||
#ifdef C4F_DIAG
|
||||
#include "c4f_log.h"
|
||||
/* Diagnostic build: each connection from another device, step by step. The
|
||||
* launcher's status checks over loopback come every 2 seconds and stay out. */
|
||||
#define C4F_NET_LOG(c, fmt, ...) do { if (strcmp((c)->peer, "127.0.0.1")) \
|
||||
@@ -51,6 +55,7 @@ uint64_t c4fTimeMs(void)
|
||||
return (uint64_t)t.tv_sec * 1000 + (uint64_t)t.tv_nsec / 1000000;
|
||||
}
|
||||
|
||||
#ifdef C4F_TEST_WEBSOCKET
|
||||
static uint32_t c4fRol(uint32_t v, unsigned n) { return (v << n) | (v >> (32 - n)); }
|
||||
|
||||
/* RFC 6455's SHA-1 + base64 handshake. Keys are fixed at 24 ASCII bytes; this
|
||||
@@ -94,6 +99,7 @@ void c4fWebSocketAccept(const char *key, char out[29])
|
||||
}
|
||||
out[j] = 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
static int c4fQueue(C4fNetClient *c, const void *data, size_t n)
|
||||
{
|
||||
@@ -108,6 +114,7 @@ static int c4fQueue(C4fNetClient *c, const void *data, size_t n)
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifdef C4F_TEST_WEBSOCKET
|
||||
static int c4fFrame(C4fNetClient *c, unsigned op, const void *data, size_t n)
|
||||
{
|
||||
unsigned char head[4] = { (unsigned char)(0x80 | op), (unsigned char)n, 0, 0 };
|
||||
@@ -118,9 +125,12 @@ static int c4fFrame(C4fNetClient *c, unsigned op, const void *data, size_t n)
|
||||
}
|
||||
|
||||
int c4fNetText(C4fNetClient *c, const char *text) { return c4fFrame(c, 1, text, strlen(text)); }
|
||||
static int c4fNetConnText(C4fConn *conn, const char *text) { return c4fNetText((C4fNetClient *)conn, text); }
|
||||
#endif
|
||||
|
||||
void c4fNetHttpJson(C4fNetClient *c, const char *json)
|
||||
void c4fNetHttpJson(C4fConn *conn, const char *json)
|
||||
{
|
||||
C4fNetClient *c = (C4fNetClient *)conn;
|
||||
char header[256];
|
||||
int n = snprintf(header, sizeof(header), "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: %zu\r\nCache-Control: no-store\r\nConnection: close\r\n\r\n", strlen(json));
|
||||
if (c4fQueue(c, header, (size_t)n) || c4fQueue(c, json, strlen(json))) return;
|
||||
@@ -129,7 +139,7 @@ void c4fNetHttpJson(C4fNetClient *c, const char *json)
|
||||
|
||||
static void c4fDrop(C4fNet *net, C4fNetClient *c)
|
||||
{
|
||||
if (c->websocket) net->handler(c, C4F_NET_CLOSE, NULL, 0, net->context);
|
||||
if (c->websocket) net->handler(&c->conn, C4F_NET_CLOSE, NULL, 0, net->context);
|
||||
close(c->fd);
|
||||
memset(c, 0, sizeof(*c)); c->fd = -1;
|
||||
}
|
||||
@@ -161,14 +171,62 @@ static int c4fHeader(const char *request, const char *key, char *value, size_t s
|
||||
static void c4fHttpError(C4fNetClient *c, int code, const char *message)
|
||||
{
|
||||
char buf[512];
|
||||
int n = snprintf(buf, sizeof(buf), "HTTP/1.1 %d Error\r\nConnection: close\r\nContent-Type: text/plain\r\nContent-Length: %zu\r\n\r\n%s", code, strlen(message), message);
|
||||
int n = snprintf(buf, sizeof(buf), "HTTP/1.1 %d Error\r\nConnection: close\r\nContent-Type: text/plain\r\nContent-Length: %zu\r\n%s\r\n%s", code, strlen(message),
|
||||
c->savedOrigin ? "Access-Control-Allow-Origin: null\r\n" : "", message);
|
||||
C4F_NET_LOG(c, "answered %d %s", code, message);
|
||||
c4fQueue(c, buf, (size_t)n); c->closing = 2;
|
||||
}
|
||||
|
||||
/* POST /rtc: the page's WebRTC offer in, our answer out, as plain text (a
|
||||
* "simple" request, so a saved copy of the page needs no preflight). The same
|
||||
* Origin rule as the page's own connection: none, this page, or a saved file
|
||||
* ("null"). A saved file is also answered on a preflight, for browsers that ask
|
||||
* before reaching into a local network. */
|
||||
static void c4fHttpOffer(C4fNet *net, C4fNetClient *c, const char *request, size_t used,
|
||||
const char *host, const char *origin, int preflight)
|
||||
{
|
||||
char expected[160], length[16], transfer[32], reply[512];
|
||||
snprintf(expected, sizeof(expected), "http://%s", host);
|
||||
int saved = !strcmp(origin, "null");
|
||||
if (origin[0] && !saved && strcmp(origin, expected)) { c4fHttpError(c, 403, "Origin not allowed"); return; }
|
||||
c->savedOrigin = saved;
|
||||
const char *allow = saved ? "Access-Control-Allow-Origin: null\r\n" : "";
|
||||
if (preflight) {
|
||||
if (!saved) { c4fHttpError(c, 403, "Origin not allowed"); return; }
|
||||
int n = snprintf(reply, sizeof(reply), "HTTP/1.1 204 No Content\r\n%sAccess-Control-Allow-Methods: POST\r\n"
|
||||
"Access-Control-Allow-Headers: Content-Type\r\nAccess-Control-Allow-Private-Network: true\r\n"
|
||||
"Access-Control-Max-Age: 600\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", allow);
|
||||
c4fQueue(c, reply, (size_t)n); c->closing = 2;
|
||||
return;
|
||||
}
|
||||
if (c4fHeader(request, "Content-Length", length, sizeof(length)) != 1 ||
|
||||
c4fHeader(request, "Transfer-Encoding", transfer, sizeof(transfer)) != 0 ||
|
||||
!length[0] || strspn(length, "0123456789") != strlen(length) || strlen(length) > 5) {
|
||||
c4fHttpError(c, 400, "An offer needs a Content-Length"); return;
|
||||
}
|
||||
size_t body = (size_t)atoi(length);
|
||||
if (!body || used + body > sizeof(c->rx)) { c4fHttpError(c, 413, "Offer too large"); return; }
|
||||
if (c->rxUsed < used + body) return; /* the rest of it is still coming */
|
||||
struct sockaddr_in local;
|
||||
socklen_t localSize = sizeof(local);
|
||||
if (getsockname(c->fd, (struct sockaddr *)&local, &localSize)) { c4fHttpError(c, 500, "No local address"); return; }
|
||||
static char answer[2048];
|
||||
int status = c4fRtcOffer(&net->rtc, (const char *)c->rx + used, body, local.sin_addr.s_addr, answer, sizeof(answer));
|
||||
c->rxUsed = 0;
|
||||
if (status == 503) { c4fHttpError(c, 503, "Controller connections full; close an unused page"); return; }
|
||||
if (status) { c4fHttpError(c, 400, "That is not a WebRTC data-channel offer"); return; }
|
||||
int n = snprintf(reply, sizeof(reply), "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: %zu\r\n"
|
||||
"Cache-Control: no-store\r\n%sConnection: close\r\n\r\n", strlen(answer), allow);
|
||||
C4F_NET_LOG(c, "answered a WebRTC offer%s", "");
|
||||
if (c4fQueue(c, reply, (size_t)n) == 0 && c4fQueue(c, answer, strlen(answer)) == 0) c->closing = 2;
|
||||
}
|
||||
|
||||
static void c4fHttp(C4fNet *net, C4fNetClient *c)
|
||||
{
|
||||
char request[8193], host[128], origin[160], key[40], upgrade[32], version[12], connection[128];
|
||||
char request[8193], host[128], origin[160], upgrade[32];
|
||||
#ifdef C4F_TEST_WEBSOCKET
|
||||
char key[40], version[12], connection[128];
|
||||
#endif
|
||||
char *end;
|
||||
size_t used;
|
||||
memcpy(request, c->rx, c->rxUsed); request[c->rxUsed] = 0;
|
||||
@@ -179,6 +237,12 @@ static void c4fHttp(C4fNet *net, C4fNetClient *c)
|
||||
C4F_NET_LOG(c, "asked: %.*s", (int)strcspn(request, "\r\n") > 100 ? 100 : (int)strcspn(request, "\r\n"), request);
|
||||
int statusRequest = !strncmp(request, "GET /api/status HTTP/1.1\r\n", 26);
|
||||
int stopRequest = !strncmp(request, "POST /api/stop HTTP/1.1\r\n", 25);
|
||||
int offerRequest = !strncmp(request, "POST /rtc HTTP/1.1\r\n", 20);
|
||||
int preflightRequest = !strncmp(request, "OPTIONS /rtc HTTP/1.1\r\n", 23);
|
||||
int websocketRequest = 0;
|
||||
#ifdef C4F_TEST_WEBSOCKET
|
||||
websocketRequest = !strncmp(request, "GET /ws HTTP/1.1\r\n", 18);
|
||||
#endif
|
||||
/* The two files a phone needs to keep this page on its home screen. */
|
||||
const unsigned char *asset = NULL;
|
||||
size_t assetSize = 0;
|
||||
@@ -188,9 +252,10 @@ static void c4fHttp(C4fNet *net, C4fNetClient *c)
|
||||
} else if (!strncmp(request, "GET /icon-192.png HTTP/1.1\r\n", 28)) {
|
||||
asset = c4fIcon; assetSize = c4fIconSize; assetType = "image/png";
|
||||
}
|
||||
if (!statusRequest && !stopRequest && !asset && strncmp(request, "GET / HTTP/1.1\r\n", 16) &&
|
||||
if (!statusRequest && !stopRequest && !offerRequest && !preflightRequest && !asset &&
|
||||
strncmp(request, "GET / HTTP/1.1\r\n", 16) &&
|
||||
strncmp(request, "GET /index.html HTTP/1.1\r\n", 26) &&
|
||||
strncmp(request, "GET /ws HTTP/1.1\r\n", 18) &&
|
||||
!websocketRequest &&
|
||||
strncmp(request, "GET /?", 6)) {
|
||||
c4fHttpError(c, 404, "Not found"); return;
|
||||
}
|
||||
@@ -208,7 +273,9 @@ static void c4fHttp(C4fNet *net, C4fNetClient *c)
|
||||
c4fHttpError(c, 403, "Open the PS4 IP address"); return;
|
||||
}
|
||||
}
|
||||
if (statusRequest || stopRequest) {
|
||||
if (offerRequest || preflightRequest) {
|
||||
c4fHttpOffer(net, c, request, used, host, origin, preflightRequest);
|
||||
} else if (statusRequest || stopRequest) {
|
||||
char launcher[8], length[16], transfer[32];
|
||||
/* For the launcher app. A website cannot reach this: it would need the
|
||||
* custom header, which a browser only sends cross-origin after a CORS
|
||||
@@ -223,8 +290,10 @@ static void c4fHttp(C4fNet *net, C4fNetClient *c)
|
||||
c4fHttpError(c, 403, "Launcher only"); return;
|
||||
}
|
||||
c->rxUsed = 0;
|
||||
net->handler(c, stopRequest ? C4F_NET_HTTP_STOP : C4F_NET_HTTP_STATUS, NULL, 0, net->context);
|
||||
net->handler(&c->conn, stopRequest ? C4F_NET_HTTP_STOP : C4F_NET_HTTP_STATUS, NULL, 0, net->context);
|
||||
} else if (upgrade[0]) {
|
||||
#ifdef C4F_TEST_WEBSOCKET
|
||||
if (!websocketRequest) { c4fHttpError(c, 404, "Not found"); return; }
|
||||
char accept[29], reply[256], expected[160];
|
||||
int n;
|
||||
snprintf(expected, sizeof(expected), "http://%s", host);
|
||||
@@ -250,7 +319,10 @@ static void c4fHttp(C4fNet *net, C4fNetClient *c)
|
||||
c4fQueue(c, reply, (size_t)n); c->websocket = 1;
|
||||
c->pingAt = c4fTimeMs() + C4F_NET_PING_MS;
|
||||
memmove(c->rx, c->rx+used, c->rxUsed-used); c->rxUsed -= used;
|
||||
net->handler(c, C4F_NET_OPEN, NULL, 0, net->context);
|
||||
net->handler(&c->conn, C4F_NET_OPEN, NULL, 0, net->context);
|
||||
#else
|
||||
c4fHttpError(c, 400, "Use the controller page to connect over WebRTC");
|
||||
#endif
|
||||
} else if (asset) {
|
||||
char reply[256];
|
||||
int n = snprintf(reply, sizeof(reply), "HTTP/1.1 200 OK\r\nContent-Type: %s\r\nContent-Length: %zu\r\nConnection: close\r\nCache-Control: max-age=86400\r\nX-Content-Type-Options: nosniff\r\n\r\n", assetType, assetSize);
|
||||
@@ -259,13 +331,14 @@ static void c4fHttp(C4fNet *net, C4fNetClient *c)
|
||||
c->body = asset; c->bodySize = assetSize; c->rxUsed = 0;
|
||||
} else {
|
||||
char reply[768];
|
||||
int n = snprintf(reply, sizeof(reply), "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nContent-Encoding: gzip\r\nContent-Length: %zu\r\nConnection: close\r\nCache-Control: no-store\r\nX-Content-Type-Options: nosniff\r\nReferrer-Policy: no-referrer\r\nContent-Security-Policy: default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src 'self' data:; media-src data:; connect-src ws:; manifest-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'\r\n\r\n", c4fPageSize);
|
||||
int n = snprintf(reply, sizeof(reply), "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nContent-Encoding: gzip\r\nContent-Length: %zu\r\nConnection: close\r\nCache-Control: no-store\r\nX-Content-Type-Options: nosniff\r\nReferrer-Policy: no-referrer\r\nContent-Security-Policy: default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src 'self' data:; media-src data:; connect-src 'self'; manifest-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'\r\n\r\n", c4fPageSize);
|
||||
C4F_NET_LOG(c, "sending the page, %zu bytes", c4fPageSize);
|
||||
c4fQueue(c, reply, (size_t)n);
|
||||
c->body = c4fPage; c->bodySize = c4fPageSize; c->rxUsed = 0;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef C4F_TEST_WEBSOCKET
|
||||
static void c4fWs(C4fNet *net, C4fNetClient *c)
|
||||
{
|
||||
while (c->rxUsed >= 2 && !c->closing) {
|
||||
@@ -299,7 +372,7 @@ static void c4fWs(C4fNet *net, C4fNetClient *c)
|
||||
c->fragmented = !fin;
|
||||
if (fin) {
|
||||
c->message[c->messageUsed] = 0;
|
||||
net->handler(c, C4F_NET_MESSAGE_EVENT, c->message, c->messageUsed, net->context);
|
||||
net->handler(&c->conn, C4F_NET_MESSAGE_EVENT, c->message, c->messageUsed, net->context);
|
||||
c->messageUsed = 0;
|
||||
}
|
||||
} else { c->closing = 1; break; }
|
||||
@@ -307,12 +380,20 @@ static void c4fWs(C4fNet *net, C4fNetClient *c)
|
||||
memmove(c->rx, c->rx+taken, c->rxUsed-taken); c->rxUsed -= taken;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
int c4fNetOpen(C4fNet *net, int port, C4fNetHandler handler, void *context)
|
||||
{
|
||||
struct sockaddr_in addr;
|
||||
int one = 1;
|
||||
/* The certificate outlives a reopen: making one takes a moment. */
|
||||
C4fRtcCrypto *crypto = net->rtc.crypto;
|
||||
char fingerprint[sizeof(net->rtc.fingerprint)];
|
||||
unsigned nextId = net->rtc.nextId;
|
||||
memcpy(fingerprint, net->rtc.fingerprint, sizeof(fingerprint));
|
||||
memset(net, 0, sizeof(*net)); net->fd = -1;
|
||||
net->rtc.crypto = crypto; net->rtc.nextId = nextId; net->rtc.fd = -1;
|
||||
memcpy(net->rtc.fingerprint, fingerprint, sizeof(fingerprint));
|
||||
for (int i = 0; i < C4F_NET_CLIENTS; i++) net->clients[i].fd = -1;
|
||||
net->handler = handler; net->context = context;
|
||||
net->fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
@@ -327,15 +408,22 @@ int c4fNetOpen(C4fNet *net, int port, C4fNetHandler handler, void *context)
|
||||
bind(net->fd, (struct sockaddr *)&addr, sizeof(addr)) || listen(net->fd, 8)) {
|
||||
close(net->fd); net->fd = -1; return -1;
|
||||
}
|
||||
/* A listener without the input transport is not a running service. */
|
||||
if (c4fRtcOpen(&net->rtc, port, handler, context)) {
|
||||
c4fLog("rtc: not available on port %d\n", port);
|
||||
close(net->fd); net->fd = -1;
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int c4fNetPoll(C4fNet *net, int timeoutMs)
|
||||
{
|
||||
fd_set rd, wr;
|
||||
struct timeval timeout = { .tv_sec = 0, .tv_usec = timeoutMs * 1000 };
|
||||
int maxFd = net->fd, ready;
|
||||
uint64_t now = c4fTimeMs();
|
||||
timeoutMs = c4fRtcWait(&net->rtc, now, timeoutMs);
|
||||
struct timeval timeout = { .tv_sec = timeoutMs / 1000, .tv_usec = (timeoutMs % 1000) * 1000 };
|
||||
if (now >= net->checkedMs) {
|
||||
int error = 0, listening = 0; socklen_t size = sizeof(error);
|
||||
if (getsockopt(net->fd, SOL_SOCKET, SO_ERROR, &error, &size)) return -1;
|
||||
@@ -346,9 +434,15 @@ int c4fNetPoll(C4fNet *net, int timeoutMs)
|
||||
net->checkedMs = now + 1000;
|
||||
}
|
||||
FD_ZERO(&rd); FD_ZERO(&wr); FD_SET(net->fd, &rd);
|
||||
if (net->rtc.fd >= 0 && net->rtc.fd < (int)FD_SETSIZE) {
|
||||
FD_SET(net->rtc.fd, &rd);
|
||||
if (c4fRtcWantsWrite(&net->rtc)) FD_SET(net->rtc.fd, &wr);
|
||||
if (net->rtc.fd > maxFd) maxFd = net->rtc.fd;
|
||||
}
|
||||
for (int i = 0; i < C4F_NET_CLIENTS; i++) {
|
||||
C4fNetClient *c = &net->clients[i];
|
||||
if (c->fd < 0) continue;
|
||||
#ifdef C4F_TEST_WEBSOCKET
|
||||
if (c->websocket && !c->closing) {
|
||||
if (c->pongDeadline && now >= c->pongDeadline) c->closing = 1;
|
||||
else if (!c->pongDeadline && now >= c->pingAt) {
|
||||
@@ -357,6 +451,7 @@ int c4fNetPoll(C4fNet *net, int timeoutMs)
|
||||
c->pongDeadline = now + C4F_NET_PONG_MS;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (c->closing == 1) { c4fDrop(net, c); continue; }
|
||||
if (!c->closing && !c->body) FD_SET(c->fd, &rd);
|
||||
if (c->txSent < c->txUsed || c->body) FD_SET(c->fd, &wr);
|
||||
@@ -369,6 +464,9 @@ int c4fNetPoll(C4fNet *net, int timeoutMs)
|
||||
if (waitedTo < waitedFrom || waitedTo - waitedFrom > 5000 || time(NULL) - wallFrom > 5)
|
||||
return -2;
|
||||
if (ready < 0) return errno == EINTR ? 0 : -1;
|
||||
if (net->rtc.fd >= 0 && FD_ISSET(net->rtc.fd, &rd)) c4fRtcReadable(&net->rtc);
|
||||
if (net->rtc.fd >= 0 && FD_ISSET(net->rtc.fd, &wr)) c4fRtcWritable(&net->rtc);
|
||||
c4fRtcTick(&net->rtc, c4fTimeMs());
|
||||
if (FD_ISSET(net->fd, &rd)) {
|
||||
struct sockaddr_in peer;
|
||||
socklen_t peerSize = sizeof(peer);
|
||||
@@ -391,6 +489,9 @@ int c4fNetPoll(C4fNet *net, int timeoutMs)
|
||||
setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &one, sizeof(one));
|
||||
#endif
|
||||
net->clients[index].fd = fd; net->clients[index].openedMs = c4fTimeMs();
|
||||
#ifdef C4F_TEST_WEBSOCKET
|
||||
net->clients[index].conn.text = c4fNetConnText;
|
||||
#endif
|
||||
#ifdef C4F_DIAG
|
||||
memcpy(net->clients[index].peer, peerName, sizeof(peerName));
|
||||
C4F_NET_LOG(&net->clients[index], "connected: slot %d, fd %d", index, fd);
|
||||
@@ -409,7 +510,9 @@ int c4fNetPoll(C4fNet *net, int timeoutMs)
|
||||
} else if (n > 0) {
|
||||
c->rxUsed += (size_t)n;
|
||||
if (!c->websocket) c4fHttp(net, c);
|
||||
#ifdef C4F_TEST_WEBSOCKET
|
||||
if (c->websocket) c4fWs(net, c);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
if (c->closing != 1 && FD_ISSET(c->fd, &wr)) {
|
||||
@@ -446,6 +549,7 @@ int c4fNetPoll(C4fNet *net, int timeoutMs)
|
||||
|
||||
void c4fNetClose(C4fNet *net)
|
||||
{
|
||||
c4fRtcClose(&net->rtc);
|
||||
for (int i = 0; i < C4F_NET_CLIENTS; i++) if (net->clients[i].fd >= 0) c4fDrop(net, &net->clients[i]);
|
||||
if (net->fd >= 0) close(net->fd);
|
||||
net->fd = -1;
|
||||
|
||||
@@ -0,0 +1,631 @@
|
||||
/* WebRTC for the controller page: ICE-lite, DTLS through Mbed TLS, and the
|
||||
* SCTP in sctp.c. See c4f_rtc.h. */
|
||||
#include <arpa/inet.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <netinet/in.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <sys/select.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <mbedtls/ctr_drbg.h>
|
||||
#include <mbedtls/ecp.h>
|
||||
#include <mbedtls/entropy.h>
|
||||
#include <mbedtls/pk.h>
|
||||
#include <mbedtls/sha256.h>
|
||||
#include <mbedtls/ssl.h>
|
||||
#include <mbedtls/x509_crt.h>
|
||||
|
||||
#include "c4f_log.h"
|
||||
#include "c4f_rtc.h"
|
||||
#include "c4f_sctp.h"
|
||||
#include "c4f_stun.h"
|
||||
|
||||
#define C4F_RTC_MTU 1200
|
||||
#define C4F_RTC_TEXT 4096 /* the biggest message the service takes (C4F_NET_MESSAGE) */
|
||||
#define C4F_RTC_OUT_BYTES (64u * 1024u)
|
||||
#define C4F_RTC_OUT_PACKETS 64u
|
||||
#define C4F_RTC_WRITE_BURST 8u
|
||||
|
||||
typedef struct C4fRtcDatagram C4fRtcDatagram;
|
||||
struct C4fRtcDatagram {
|
||||
C4fRtcDatagram *next;
|
||||
struct sockaddr_in to;
|
||||
size_t len;
|
||||
uint8_t data[];
|
||||
};
|
||||
|
||||
struct C4fRtcCrypto {
|
||||
mbedtls_entropy_context entropy;
|
||||
mbedtls_ctr_drbg_context drbg;
|
||||
mbedtls_pk_context key;
|
||||
mbedtls_x509_crt cert;
|
||||
mbedtls_ssl_config conf;
|
||||
};
|
||||
|
||||
struct C4fRtcPeer {
|
||||
C4fConn conn; /* first: the service holds a peer by it */
|
||||
C4fRtc *rtc;
|
||||
unsigned id;
|
||||
char localUfrag[9], localPwd[25], remoteUfrag[257];
|
||||
uint8_t remoteFingerprint[32];
|
||||
struct sockaddr_in addr;
|
||||
int addrKnown;
|
||||
mbedtls_ssl_context ssl;
|
||||
int dtlsUp;
|
||||
const uint8_t *in; /* the datagram Mbed TLS is reading, until it has */
|
||||
size_t inLen;
|
||||
uint64_t timerAt;
|
||||
uint32_t timerInt, timerFin;
|
||||
C4fSctp sctp;
|
||||
int opened, ending, failed;
|
||||
const char *failure;
|
||||
uint64_t createdAt, heardAt;
|
||||
uint64_t datagramsIn, datagramsOut;
|
||||
C4fRtcDatagram *out, *outTail;
|
||||
size_t outBytes;
|
||||
unsigned outPackets;
|
||||
uint64_t retryWriteAt;
|
||||
};
|
||||
|
||||
/* ------------------------------------------------------------ certificate */
|
||||
|
||||
static int c4fRtcEntropy(void *data, unsigned char *out, size_t len, size_t *olen)
|
||||
{
|
||||
(void)data;
|
||||
arc4random_buf(out, len);
|
||||
*olen = len;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void c4fRtcFreeCrypto(C4fRtcCrypto *k)
|
||||
{
|
||||
mbedtls_ssl_config_free(&k->conf);
|
||||
mbedtls_x509_crt_free(&k->cert);
|
||||
mbedtls_pk_free(&k->key);
|
||||
mbedtls_ctr_drbg_free(&k->drbg);
|
||||
mbedtls_entropy_free(&k->entropy);
|
||||
free(k);
|
||||
}
|
||||
|
||||
/* A fresh P-256 key and a self-signed certificate for it, once per run: the
|
||||
* browser checks it by the fingerprint in the answer, not by who signed it. */
|
||||
static int c4fRtcMakeCrypto(C4fRtc *rtc)
|
||||
{
|
||||
C4fRtcCrypto *k = calloc(1, sizeof(*k));
|
||||
if (!k) return -1;
|
||||
uint64_t started = c4fTimeMs();
|
||||
mbedtls_entropy_init(&k->entropy);
|
||||
mbedtls_ctr_drbg_init(&k->drbg);
|
||||
mbedtls_pk_init(&k->key);
|
||||
mbedtls_x509_crt_init(&k->cert);
|
||||
mbedtls_ssl_config_init(&k->conf);
|
||||
unsigned char der[1024];
|
||||
int derLen = 0;
|
||||
int ret = mbedtls_entropy_add_source(&k->entropy, c4fRtcEntropy, NULL, 32, MBEDTLS_ENTROPY_SOURCE_STRONG);
|
||||
if (!ret) ret = mbedtls_ctr_drbg_seed(&k->drbg, mbedtls_entropy_func, &k->entropy, (const unsigned char *)"Control4Free", 12);
|
||||
if (!ret) ret = mbedtls_pk_setup(&k->key, mbedtls_pk_info_from_type(MBEDTLS_PK_ECKEY));
|
||||
if (!ret) ret = mbedtls_ecp_gen_key(MBEDTLS_ECP_DP_SECP256R1, mbedtls_pk_ec(k->key), mbedtls_ctr_drbg_random, &k->drbg);
|
||||
if (!ret) {
|
||||
mbedtls_x509write_cert w;
|
||||
unsigned char serial[8];
|
||||
mbedtls_x509write_crt_init(&w);
|
||||
mbedtls_x509write_crt_set_version(&w, MBEDTLS_X509_CRT_VERSION_3);
|
||||
mbedtls_x509write_crt_set_md_alg(&w, MBEDTLS_MD_SHA256);
|
||||
mbedtls_x509write_crt_set_subject_key(&w, &k->key);
|
||||
mbedtls_x509write_crt_set_issuer_key(&w, &k->key);
|
||||
ret = mbedtls_x509write_crt_set_subject_name(&w, "CN=Control4Free");
|
||||
if (!ret) ret = mbedtls_x509write_crt_set_issuer_name(&w, "CN=Control4Free");
|
||||
if (!ret) ret = mbedtls_ctr_drbg_random(&k->drbg, serial, sizeof(serial));
|
||||
serial[0] = (serial[0] & 0x7F) | 0x01; /* positive, no leading zero */
|
||||
if (!ret) ret = mbedtls_x509write_crt_set_serial_raw(&w, serial, sizeof(serial));
|
||||
if (!ret) ret = mbedtls_x509write_crt_set_validity(&w, "20250101000000", "20450101000000");
|
||||
if (!ret) {
|
||||
derLen = mbedtls_x509write_crt_der(&w, der, sizeof(der), mbedtls_ctr_drbg_random, &k->drbg);
|
||||
if (derLen < 0) ret = derLen;
|
||||
}
|
||||
mbedtls_x509write_crt_free(&w);
|
||||
}
|
||||
const unsigned char *cert = der + sizeof(der) - derLen; /* written at the end of the buffer */
|
||||
if (!ret) ret = mbedtls_x509_crt_parse_der(&k->cert, cert, (size_t)derLen);
|
||||
if (!ret) {
|
||||
unsigned char hash[32];
|
||||
ret = mbedtls_sha256(cert, (size_t)derLen, hash, 0);
|
||||
for (int i = 0; !ret && i < 32; i++) snprintf(rtc->fingerprint + 3 * i, 4, i < 31 ? "%02X:" : "%02X", hash[i]);
|
||||
}
|
||||
if (!ret) ret = mbedtls_ssl_config_defaults(&k->conf, MBEDTLS_SSL_IS_SERVER, MBEDTLS_SSL_TRANSPORT_DATAGRAM,
|
||||
MBEDTLS_SSL_PRESET_DEFAULT);
|
||||
if (!ret) {
|
||||
mbedtls_ssl_conf_rng(&k->conf, mbedtls_ctr_drbg_random, &k->drbg);
|
||||
/* The browser's certificate is self-signed too: asked for, then checked
|
||||
* against the fingerprint in its offer. */
|
||||
mbedtls_ssl_conf_authmode(&k->conf, MBEDTLS_SSL_VERIFY_OPTIONAL);
|
||||
mbedtls_ssl_conf_handshake_timeout(&k->conf, 400, 6000);
|
||||
ret = mbedtls_ssl_conf_own_cert(&k->conf, &k->cert, &k->key);
|
||||
}
|
||||
if (ret) {
|
||||
c4fLog("rtc: making the certificate failed: -0x%04x\n", (unsigned)-ret);
|
||||
c4fRtcFreeCrypto(k);
|
||||
return -1;
|
||||
}
|
||||
rtc->crypto = k;
|
||||
c4fLog("rtc: certificate ready in %llu ms\n", (unsigned long long)(c4fTimeMs() - started));
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ peers */
|
||||
|
||||
static int c4fRtcPeerTimer(void *ctx)
|
||||
{
|
||||
C4fRtcPeer *p = ctx;
|
||||
if (!p->timerFin) return -1;
|
||||
uint64_t elapsed = c4fTimeMs() - p->timerAt;
|
||||
return elapsed >= p->timerFin ? 2 : elapsed >= p->timerInt ? 1 : 0;
|
||||
}
|
||||
|
||||
static void c4fRtcPeerSetTimer(void *ctx, uint32_t intMs, uint32_t finMs)
|
||||
{
|
||||
C4fRtcPeer *p = ctx;
|
||||
p->timerAt = c4fTimeMs();
|
||||
p->timerInt = intMs;
|
||||
p->timerFin = finMs;
|
||||
}
|
||||
|
||||
static int c4fRtcSendDatagram(C4fRtcPeer *p, const unsigned char *buf, size_t len, const struct sockaddr_in *to)
|
||||
{
|
||||
if (p->failed || len > C4F_RTC_MTU) return -1;
|
||||
if (!p->out) {
|
||||
ssize_t n = sendto(p->rtc->fd, buf, len, 0, (const struct sockaddr *)to, sizeof(*to));
|
||||
if (n == (ssize_t)len) { p->datagramsOut++; return 0; }
|
||||
if (n >= 0 || (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR && errno != ENOBUFS)) {
|
||||
p->failed = 1; p->failure = "UDP send failed"; return -1;
|
||||
}
|
||||
if (errno == ENOBUFS) p->retryWriteAt = c4fTimeMs() + 2;
|
||||
}
|
||||
if (p->outPackets >= C4F_RTC_OUT_PACKETS || p->outBytes + len > C4F_RTC_OUT_BYTES) {
|
||||
p->failed = 1; p->failure = "UDP output queue full"; return -1;
|
||||
}
|
||||
C4fRtcDatagram *d = malloc(sizeof(*d) + len);
|
||||
if (!d) { p->failed = 1; p->failure = "UDP output allocation failed"; return -1; }
|
||||
d->next = NULL; d->to = *to; d->len = len; memcpy(d->data, buf, len);
|
||||
if (p->outTail) p->outTail->next = d; else p->out = d;
|
||||
p->outTail = d; p->outBytes += len; p->outPackets++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void c4fRtcDrainPeer(C4fRtcPeer *p, uint64_t now)
|
||||
{
|
||||
if (p->ending || p->failed || now < p->retryWriteAt) return;
|
||||
for (unsigned i = 0; p->out && i < C4F_RTC_WRITE_BURST; i++) {
|
||||
C4fRtcDatagram *d = p->out;
|
||||
ssize_t n = sendto(p->rtc->fd, d->data, d->len, 0, (const struct sockaddr *)&d->to, sizeof(d->to));
|
||||
if (n != (ssize_t)d->len) {
|
||||
if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR || errno == ENOBUFS)) {
|
||||
if (errno == ENOBUFS) p->retryWriteAt = now + 2;
|
||||
} else { p->failed = 1; p->failure = "UDP queued send failed"; }
|
||||
return;
|
||||
}
|
||||
p->datagramsOut++; p->out = d->next;
|
||||
p->outBytes -= d->len; p->outPackets--; free(d);
|
||||
if (!p->out) p->outTail = NULL;
|
||||
}
|
||||
p->retryWriteAt = 0;
|
||||
}
|
||||
|
||||
int c4fRtcWantsWrite(const C4fRtc *rtc)
|
||||
{
|
||||
uint64_t now = c4fTimeMs();
|
||||
for (int i = 0; i < C4F_RTC_PEERS; i++) {
|
||||
const C4fRtcPeer *p = rtc->peers[i];
|
||||
if (p && !p->ending && !p->failed && p->out && now >= p->retryWriteAt) return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void c4fRtcWritable(C4fRtc *rtc)
|
||||
{
|
||||
uint64_t now = c4fTimeMs();
|
||||
unsigned first = rtc->writeCursor++ % C4F_RTC_PEERS;
|
||||
for (unsigned i = 0; i < C4F_RTC_PEERS; i++) {
|
||||
C4fRtcPeer *p = rtc->peers[(first + i) % C4F_RTC_PEERS];
|
||||
if (p) c4fRtcDrainPeer(p, now);
|
||||
}
|
||||
}
|
||||
|
||||
static int c4fRtcBioSend(void *ctx, const unsigned char *buf, size_t len)
|
||||
{
|
||||
C4fRtcPeer *p = ctx;
|
||||
/* Accept a complete owned ciphertext datagram before telling DTLS that
|
||||
* its write succeeded. A later TLS write can never overwrite a blocked one. */
|
||||
if (!p->addrKnown || c4fRtcSendDatagram(p, buf, len, &p->addr)) return MBEDTLS_ERR_SSL_INTERNAL_ERROR;
|
||||
return (int)len;
|
||||
}
|
||||
|
||||
static int c4fRtcBioRecv(void *ctx, unsigned char *buf, size_t len)
|
||||
{
|
||||
C4fRtcPeer *p = ctx;
|
||||
if (!p->inLen) return MBEDTLS_ERR_SSL_WANT_READ;
|
||||
size_t n = p->inLen < len ? p->inLen : len;
|
||||
memcpy(buf, p->in, n);
|
||||
p->inLen = 0;
|
||||
return (int)n;
|
||||
}
|
||||
|
||||
static int c4fRtcSctpSend(void *user, const uint8_t *packet, size_t len)
|
||||
{
|
||||
C4fRtcPeer *p = user;
|
||||
if (p->ending || p->failed || !p->dtlsUp) return -1;
|
||||
int ret = mbedtls_ssl_write(&p->ssl, packet, len);
|
||||
if (ret != (int)len) {
|
||||
if (!p->failed) { p->failed = 1; p->failure = "DTLS send failed"; }
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void c4fRtcSctpRandom(void *user, void *out, size_t len)
|
||||
{
|
||||
C4fRtcPeer *p = user;
|
||||
if (mbedtls_ctr_drbg_random(&p->rtc->crypto->drbg, out, len)) arc4random_buf(out, len);
|
||||
}
|
||||
|
||||
static void c4fRtcOpened(C4fRtcPeer *p)
|
||||
{
|
||||
p->opened = 1;
|
||||
c4fLog("rtc: peer %u open in %llu ms\n", p->id, (unsigned long long)(c4fTimeMs() - p->createdAt));
|
||||
p->rtc->handler(&p->conn, C4F_NET_OPEN, NULL, 0, p->rtc->context);
|
||||
}
|
||||
|
||||
static void c4fRtcSctpDeliver(void *user, uint16_t stream, uint32_t ppid, const uint8_t *data, size_t len)
|
||||
{
|
||||
static char text[C4F_RTC_TEXT + 1];
|
||||
C4fRtcPeer *p = user;
|
||||
if (p->ending) return;
|
||||
if (!p->opened) c4fRtcOpened(p);
|
||||
if (ppid != C4F_SCTP_PPID_STRING || len > C4F_RTC_TEXT) return;
|
||||
memcpy(text, data, len);
|
||||
text[len] = 0;
|
||||
p->rtc->handler(&p->conn, stream == 1 ? C4F_NET_INPUT_EVENT : C4F_NET_MESSAGE_EVENT, text, len, p->rtc->context);
|
||||
}
|
||||
|
||||
static int c4fRtcSend(C4fConn *c, const char *text, uint16_t stream)
|
||||
{
|
||||
C4fRtcPeer *p = (C4fRtcPeer *)c;
|
||||
if (p->ending || p->failed || !p->opened) return -1;
|
||||
if (!c4fSctpSend(&p->sctp, stream, C4F_SCTP_PPID_STRING, (const uint8_t *)text, strlen(text), c4fTimeMs())) return 0;
|
||||
/* A reply that can't be queued means the page lost too much: let it reconnect. */
|
||||
if (stream == 0) { p->failed = 1; p->failure = "too much waiting to be sent"; }
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int c4fRtcText(C4fConn *c, const char *text) { return c4fRtcSend(c, text, 0); }
|
||||
static int c4fRtcInput(C4fConn *c, const char *text) { return c4fRtcSend(c, text, 1); }
|
||||
|
||||
/* Ends a peer: the service hears of it now, the memory goes on the next tick. */
|
||||
static void c4fRtcEnd(C4fRtcPeer *p, const char *why)
|
||||
{
|
||||
if (p->ending) return;
|
||||
p->ending = 1;
|
||||
c4fLog("rtc: peer %u closed (%s) after %llu s; datagrams %llu in, %llu out; sctp resent %llu, gave up %llu, "
|
||||
"duplicates %llu, bad checksums %llu\n", p->id, why, (unsigned long long)((c4fTimeMs() - p->createdAt) / 1000),
|
||||
(unsigned long long)p->datagramsIn, (unsigned long long)p->datagramsOut,
|
||||
(unsigned long long)p->sctp.resent, (unsigned long long)p->sctp.abandoned,
|
||||
(unsigned long long)p->sctp.duplicates, (unsigned long long)p->sctp.badChecksums);
|
||||
if (p->opened) p->rtc->handler(&p->conn, C4F_NET_CLOSE, NULL, 0, p->rtc->context);
|
||||
}
|
||||
|
||||
static void c4fRtcFreePeer(C4fRtc *rtc, int slot)
|
||||
{
|
||||
C4fRtcPeer *p = rtc->peers[slot];
|
||||
rtc->peers[slot] = NULL;
|
||||
if (p->dtlsUp && rtc->fd >= 0) (void)mbedtls_ssl_close_notify(&p->ssl);
|
||||
mbedtls_ssl_free(&p->ssl);
|
||||
c4fSctpFree(&p->sctp);
|
||||
while (p->out) { C4fRtcDatagram *d = p->out; p->out = d->next; free(d); }
|
||||
free(p);
|
||||
}
|
||||
|
||||
/* After SCTP had a packet or a tick: open, or ended by the browser. */
|
||||
static void c4fRtcAfterSctp(C4fRtcPeer *p)
|
||||
{
|
||||
if (p->ending) return;
|
||||
if (!p->opened && p->sctp.state == C4F_SCTP_ESTABLISHED) c4fRtcOpened(p);
|
||||
if (p->sctp.state == C4F_SCTP_ENDED) c4fRtcEnd(p, "the page closed it");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ datagrams */
|
||||
|
||||
static void c4fRtcStun(C4fRtc *rtc, const uint8_t *msg, size_t len, const struct sockaddr_in *from, uint64_t now)
|
||||
{
|
||||
size_t size;
|
||||
const uint8_t *user = c4fStunUsername(msg, len, &size);
|
||||
if (!user) return;
|
||||
const uint8_t *colon = memchr(user, ':', size);
|
||||
if (!colon) return;
|
||||
size_t ours = (size_t)(colon - user), theirs = size - ours - 1;
|
||||
for (int i = 0; i < C4F_RTC_PEERS; i++) {
|
||||
C4fRtcPeer *p = rtc->peers[i];
|
||||
if (!p || p->ending || strlen(p->localUfrag) != ours || memcmp(p->localUfrag, user, ours) ||
|
||||
strlen(p->remoteUfrag) != theirs || memcmp(p->remoteUfrag, colon + 1, theirs)) continue;
|
||||
uint8_t answer[C4F_STUN_ANSWER_SIZE];
|
||||
int nominate;
|
||||
size_t n = c4fStunAnswer(msg, len, p->localPwd, from->sin_addr.s_addr, from->sin_port, answer, &nominate);
|
||||
if (!n) return;
|
||||
p->datagramsIn++;
|
||||
(void)c4fRtcSendDatagram(p, answer, n, from);
|
||||
p->heardAt = now;
|
||||
/* Only a check signed with our password may move where the peer is. */
|
||||
if (!p->addrKnown || nominate) {
|
||||
p->addr = *from;
|
||||
p->addrKnown = 1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static void c4fRtcDtls(C4fRtcPeer *p, const uint8_t *data, size_t len, uint64_t now)
|
||||
{
|
||||
static unsigned char plain[2048];
|
||||
p->heardAt = now;
|
||||
p->datagramsIn++;
|
||||
p->in = data;
|
||||
p->inLen = len;
|
||||
if (!p->dtlsUp) {
|
||||
int ret = mbedtls_ssl_handshake(&p->ssl);
|
||||
if (ret == 0) {
|
||||
/* Is this the browser that sent the offer? */
|
||||
const mbedtls_x509_crt *peer = mbedtls_ssl_get_peer_cert(&p->ssl);
|
||||
unsigned char hash[32];
|
||||
if (!peer || mbedtls_sha256(peer->raw.p, peer->raw.len, hash, 0) ||
|
||||
memcmp(hash, p->remoteFingerprint, sizeof(hash))) {
|
||||
c4fRtcEnd(p, "its certificate doesn't match its offer");
|
||||
p->inLen = 0;
|
||||
return;
|
||||
}
|
||||
p->dtlsUp = 1;
|
||||
c4fLog("rtc: peer %u secured in %llu ms\n", p->id, (unsigned long long)(now - p->createdAt));
|
||||
} else if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) {
|
||||
char why[48];
|
||||
snprintf(why, sizeof(why), "DTLS handshake failed: -0x%04x", (unsigned)-ret);
|
||||
c4fRtcEnd(p, why);
|
||||
p->inLen = 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
while (p->dtlsUp && !p->ending) {
|
||||
int ret = mbedtls_ssl_read(&p->ssl, plain, sizeof(plain));
|
||||
if (ret > 0) {
|
||||
c4fSctpReceive(&p->sctp, plain, (size_t)ret, now);
|
||||
c4fRtcAfterSctp(p);
|
||||
continue;
|
||||
}
|
||||
if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE) break;
|
||||
if (ret == 0 || ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY) { c4fRtcEnd(p, "the page closed it"); break; }
|
||||
char why[40];
|
||||
snprintf(why, sizeof(why), "DTLS read failed: -0x%04x", (unsigned)-ret);
|
||||
c4fRtcEnd(p, why);
|
||||
break;
|
||||
}
|
||||
p->inLen = 0;
|
||||
}
|
||||
|
||||
void c4fRtcReadable(C4fRtc *rtc)
|
||||
{
|
||||
static uint8_t buf[2048];
|
||||
for (int i = 0; i < 64 && rtc->fd >= 0; i++) {
|
||||
struct sockaddr_in from;
|
||||
socklen_t fromLen = sizeof(from);
|
||||
ssize_t n = recvfrom(rtc->fd, buf, sizeof(buf), 0, (struct sockaddr *)&from, &fromLen);
|
||||
if (n <= 0) break;
|
||||
uint64_t now = c4fTimeMs();
|
||||
if (c4fIsStun(buf, (size_t)n)) { c4fRtcStun(rtc, buf, (size_t)n, &from, now); continue; }
|
||||
if (!c4fIsDtls(buf, (size_t)n)) continue;
|
||||
for (int k = 0; k < C4F_RTC_PEERS; k++) {
|
||||
C4fRtcPeer *p = rtc->peers[k];
|
||||
if (p && !p->ending && p->addrKnown && p->addr.sin_addr.s_addr == from.sin_addr.s_addr &&
|
||||
p->addr.sin_port == from.sin_port) {
|
||||
c4fRtcDtls(p, buf, (size_t)n, now);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void c4fRtcTick(C4fRtc *rtc, uint64_t now)
|
||||
{
|
||||
for (int i = 0; i < C4F_RTC_PEERS; i++) {
|
||||
C4fRtcPeer *p = rtc->peers[i];
|
||||
if (!p) continue;
|
||||
if (!p->ending && p->failed) c4fRtcEnd(p, p->failure ? p->failure : "it failed");
|
||||
if (!p->ending && !p->dtlsUp && c4fRtcPeerTimer(p) == 2) {
|
||||
/* A handshake flight went unanswered: Mbed TLS sends it again. */
|
||||
int ret = mbedtls_ssl_handshake(&p->ssl);
|
||||
if (ret && ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) c4fRtcEnd(p, "DTLS handshake timed out");
|
||||
}
|
||||
if (!p->ending && p->dtlsUp) { c4fSctpTick(&p->sctp, now); c4fRtcAfterSctp(p); }
|
||||
if (!p->ending && now - p->heardAt > C4F_RTC_SILENT_MS) c4fRtcEnd(p, "nothing heard for 10 s");
|
||||
if (!p->ending && !p->opened && now - p->createdAt > C4F_RTC_SETUP_MS) c4fRtcEnd(p, "it never connected");
|
||||
if (p->ending) c4fRtcFreePeer(rtc, i);
|
||||
}
|
||||
}
|
||||
|
||||
int c4fRtcWait(const C4fRtc *rtc, uint64_t now, int limit)
|
||||
{
|
||||
uint64_t soonest = now + (uint64_t)limit;
|
||||
for (int i = 0; i < C4F_RTC_PEERS; i++) {
|
||||
const C4fRtcPeer *p = rtc->peers[i];
|
||||
if (!p) continue;
|
||||
if (p->ending || p->failed) return 0;
|
||||
uint64_t at = p->heardAt + C4F_RTC_SILENT_MS + 1;
|
||||
if (at < soonest) soonest = at;
|
||||
if (p->out && p->retryWriteAt > now && p->retryWriteAt < soonest) soonest = p->retryWriteAt;
|
||||
if (!p->dtlsUp && p->timerFin && p->timerAt + p->timerFin < soonest) soonest = p->timerAt + p->timerFin;
|
||||
uint64_t sctp = c4fSctpDeadline(&p->sctp);
|
||||
if (sctp && sctp < soonest) soonest = sctp;
|
||||
}
|
||||
return soonest <= now ? 0 : (int)(soonest - now);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ offer and answer */
|
||||
|
||||
/* Copies an "a=name:value" line's value. */
|
||||
static int c4fSdpValue(const char *line, size_t len, const char *name, char *out, size_t size)
|
||||
{
|
||||
size_t n = strlen(name);
|
||||
if (len < n || memcmp(line, name, n)) return 0;
|
||||
size_t v = len - n;
|
||||
if (v >= size) return -1;
|
||||
memcpy(out, line + n, v);
|
||||
out[v] = 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int c4fContains(const char *text, size_t len, const char *word)
|
||||
{
|
||||
size_t n = strlen(word);
|
||||
for (size_t i = 0; i + n <= len; i++) if (!memcmp(text + i, word, n)) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int c4fHex(char c) { return c >= '0' && c <= '9' ? c - '0' : c >= 'a' && c <= 'f' ? c - 'a' + 10 : c >= 'A' && c <= 'F' ? c - 'A' + 10 : -1; }
|
||||
|
||||
static void c4fRtcToken(C4fRtc *rtc, char *out, size_t chars)
|
||||
{
|
||||
static const char alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
unsigned char raw[32];
|
||||
if (mbedtls_ctr_drbg_random(&rtc->crypto->drbg, raw, chars)) arc4random_buf(raw, chars);
|
||||
for (size_t i = 0; i < chars; i++) out[i] = alphabet[raw[i] % 62];
|
||||
out[chars] = 0;
|
||||
}
|
||||
|
||||
int c4fRtcOffer(C4fRtc *rtc, const char *offer, size_t len, uint32_t local, char *answer, size_t size)
|
||||
{
|
||||
char ufrag[257] = "", mid[33] = "0", setup[16] = "", fingerprint[128] = "", value[128];
|
||||
int application = 0, other = 0, bundle = 0;
|
||||
if (!rtc->crypto || rtc->fd < 0) return 503;
|
||||
for (size_t at = 0; at < len; ) {
|
||||
size_t end = at;
|
||||
while (end < len && offer[end] != '\n') end++;
|
||||
size_t n = end - at;
|
||||
const char *line = offer + at;
|
||||
if (n && line[n - 1] == '\r') n--;
|
||||
if (n > 2 && line[0] == 'm' && line[1] == '=') {
|
||||
if (n >= 14 && !memcmp(line, "m=application ", 14) && c4fContains(line, n, "webrtc-datachannel")) application++;
|
||||
else other++;
|
||||
}
|
||||
else if (c4fSdpValue(line, n, "a=ice-ufrag:", ufrag, sizeof(ufrag)) < 0) return 400;
|
||||
else if (c4fSdpValue(line, n, "a=mid:", mid, sizeof(mid)) < 0) return 400;
|
||||
else if (c4fSdpValue(line, n, "a=setup:", setup, sizeof(setup)) < 0) return 400;
|
||||
else if (c4fSdpValue(line, n, "a=fingerprint:", value, sizeof(value)) > 0) {
|
||||
if (!strncasecmp(value, "sha-256 ", 8) && !fingerprint[0]) snprintf(fingerprint, sizeof(fingerprint), "%s", value + 8);
|
||||
}
|
||||
else if (n >= 15 && !memcmp(line, "a=group:BUNDLE ", 15)) bundle = 1;
|
||||
at = end + 1;
|
||||
}
|
||||
/* One data-channel section, credentials, and a SHA-256 fingerprint. */
|
||||
if (application != 1 || other || strlen(ufrag) < 4 || !strcmp(setup, "passive") || strlen(fingerprint) != 95) return 400;
|
||||
for (size_t i = 0; mid[i]; i++) if (mid[i] <= ' ' || mid[i] > '~') return 400;
|
||||
for (size_t i = 0; ufrag[i]; i++) if (ufrag[i] <= ' ' || ufrag[i] > '~' || ufrag[i] == ':') return 400;
|
||||
uint8_t remote[32];
|
||||
for (int i = 0; i < 32; i++) {
|
||||
int hi = c4fHex(fingerprint[3 * i]), lo = c4fHex(fingerprint[3 * i + 1]);
|
||||
if (hi < 0 || lo < 0 || (i < 31 && fingerprint[3 * i + 2] != ':')) return 400;
|
||||
remote[i] = (uint8_t)(hi << 4 | lo);
|
||||
}
|
||||
int slot = -1;
|
||||
for (int i = 0; i < C4F_RTC_PEERS && slot < 0; i++) if (!rtc->peers[i]) slot = i;
|
||||
if (slot < 0) return 503;
|
||||
|
||||
C4fRtcPeer *p = calloc(1, sizeof(*p));
|
||||
if (!p) return 503;
|
||||
p->conn.text = c4fRtcText;
|
||||
p->conn.input = c4fRtcInput;
|
||||
p->rtc = rtc;
|
||||
p->id = ++rtc->nextId;
|
||||
c4fRtcToken(rtc, p->localUfrag, 8);
|
||||
c4fRtcToken(rtc, p->localPwd, 24);
|
||||
snprintf(p->remoteUfrag, sizeof(p->remoteUfrag), "%s", ufrag);
|
||||
memcpy(p->remoteFingerprint, remote, sizeof(remote));
|
||||
p->createdAt = p->heardAt = c4fTimeMs();
|
||||
mbedtls_ssl_init(&p->ssl);
|
||||
if (mbedtls_ssl_setup(&p->ssl, &rtc->crypto->conf)) { mbedtls_ssl_free(&p->ssl); free(p); return 503; }
|
||||
mbedtls_ssl_set_bio(&p->ssl, p, c4fRtcBioSend, c4fRtcBioRecv, NULL);
|
||||
mbedtls_ssl_set_timer_cb(&p->ssl, p, c4fRtcPeerSetTimer, c4fRtcPeerTimer);
|
||||
mbedtls_ssl_set_mtu(&p->ssl, C4F_RTC_MTU);
|
||||
C4fSctpOps ops = { p, c4fRtcSctpSend, c4fRtcSctpDeliver, c4fRtcSctpRandom };
|
||||
c4fSctpInit(&p->sctp, &ops);
|
||||
|
||||
char ip[INET_ADDRSTRLEN];
|
||||
struct in_addr address = { .s_addr = local };
|
||||
inet_ntop(AF_INET, &address, ip, sizeof(ip));
|
||||
unsigned long long session;
|
||||
c4fRtcSctpRandom(p, &session, sizeof(session));
|
||||
int n = snprintf(answer, size,
|
||||
"v=0\r\n"
|
||||
"o=- %llu 2 IN IP4 0.0.0.0\r\n"
|
||||
"s=-\r\n"
|
||||
"t=0 0\r\n"
|
||||
"%s%s%s"
|
||||
"a=ice-lite\r\n"
|
||||
"m=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\n"
|
||||
"c=IN IP4 0.0.0.0\r\n"
|
||||
"a=mid:%s\r\n"
|
||||
"a=ice-ufrag:%s\r\n"
|
||||
"a=ice-pwd:%s\r\n"
|
||||
"a=fingerprint:sha-256 %s\r\n"
|
||||
"a=setup:passive\r\n"
|
||||
"a=sctp-port:5000\r\n"
|
||||
"a=max-message-size:%d\r\n"
|
||||
"a=candidate:1 1 udp 2130706431 %s %d typ host\r\n"
|
||||
"a=end-of-candidates\r\n",
|
||||
session & 0x7FFFFFFFFFFFFFFFull, bundle ? "a=group:BUNDLE " : "", bundle ? mid : "", bundle ? "\r\n" : "",
|
||||
mid, p->localUfrag, p->localPwd, rtc->fingerprint, C4F_SCTP_MAX_MESSAGE, ip, rtc->port);
|
||||
if (n < 0 || (size_t)n >= size) { mbedtls_ssl_free(&p->ssl); c4fSctpFree(&p->sctp); free(p); return 503; }
|
||||
rtc->peers[slot] = p;
|
||||
c4fLog("rtc: peer %u offered, answering with %s:%d\n", p->id, ip, rtc->port);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ socket */
|
||||
|
||||
int c4fRtcOpen(C4fRtc *rtc, int port, C4fNetHandler handler, void *context)
|
||||
{
|
||||
rtc->handler = handler;
|
||||
rtc->context = context;
|
||||
rtc->port = port;
|
||||
rtc->fd = -1;
|
||||
if (!rtc->crypto && c4fRtcMakeCrypto(rtc)) return -1;
|
||||
int fd = socket(AF_INET, SOCK_DGRAM, 0), one = 1;
|
||||
if (fd < 0) return -1;
|
||||
struct sockaddr_in addr = { .sin_family = AF_INET, .sin_port = htons((uint16_t)port), .sin_addr.s_addr = htonl(INADDR_ANY) };
|
||||
(void)setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
|
||||
if (fd >= (int)FD_SETSIZE || bind(fd, (struct sockaddr *)&addr, sizeof(addr)) || fcntl(fd, F_SETFL, O_NONBLOCK)) {
|
||||
c4fLog("rtc: UDP port %d unavailable, errno %d\n", port, errno);
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
rtc->fd = fd;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void c4fRtcClose(C4fRtc *rtc)
|
||||
{
|
||||
for (int i = 0; i < C4F_RTC_PEERS; i++) {
|
||||
if (!rtc->peers[i]) continue;
|
||||
c4fRtcEnd(rtc->peers[i], "the service reset its connections");
|
||||
c4fRtcFreePeer(rtc, i);
|
||||
}
|
||||
if (rtc->fd >= 0) close(rtc->fd);
|
||||
rtc->fd = -1;
|
||||
}
|
||||
|
||||
void c4fRtcDestroy(C4fRtc *rtc)
|
||||
{
|
||||
c4fRtcClose(rtc);
|
||||
if (rtc->crypto) { c4fRtcFreeCrypto(rtc->crypto); rtc->crypto = NULL; }
|
||||
memset(rtc->fingerprint, 0, sizeof(rtc->fingerprint));
|
||||
}
|
||||
@@ -0,0 +1,742 @@
|
||||
/* A minimal SCTP association for WebRTC data channels. See c4f_sctp.h.
|
||||
*
|
||||
* What it does, and no more:
|
||||
* - answers the browser's INIT (INIT-ACK with a cookie, COOKIE-ECHO, COOKIE-ACK);
|
||||
* - receives DATA on streams 0 (reliable, in order) and 1 (unordered), puts
|
||||
* fragments back together, and acknowledges every packet straight away;
|
||||
* - sends on stream 0 reliably (resent after a timeout, or sooner when three
|
||||
* acknowledgements in a row show it missing) and on stream 1 once (given up
|
||||
* on instead, and skipped over with FORWARD-TSN);
|
||||
* - answers HEARTBEAT, stream resets (RE-CONFIG) and FORWARD-TSN, and ends on
|
||||
* ABORT or SHUTDOWN.
|
||||
* New DATA observes a shared congestion window and the peer's receive window.
|
||||
* Reliable messages wait in a bounded queue; blocked unreliable state is
|
||||
* rejected for the caller to replace. No message is delayed to batch it. */
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "c4f_sctp.h"
|
||||
|
||||
enum {
|
||||
CHUNK_DATA = 0, CHUNK_INIT = 1, CHUNK_INIT_ACK = 2, CHUNK_SACK = 3, CHUNK_HEARTBEAT = 4,
|
||||
CHUNK_HEARTBEAT_ACK = 5, CHUNK_ABORT = 6, CHUNK_SHUTDOWN = 7, CHUNK_SHUTDOWN_ACK = 8,
|
||||
CHUNK_ERROR = 9, CHUNK_COOKIE_ECHO = 10, CHUNK_COOKIE_ACK = 11, CHUNK_SHUTDOWN_COMPLETE = 14,
|
||||
CHUNK_RECONFIG = 130, CHUNK_FORWARD_TSN = 192,
|
||||
};
|
||||
enum { DATA_END = 1, DATA_BEGIN = 2, DATA_UNORDERED = 4 };
|
||||
enum { PARAM_STATE_COOKIE = 7, PARAM_OUTGOING_RESET = 13, PARAM_INCOMING_RESET = 14,
|
||||
PARAM_RECONFIG_RESPONSE = 16, PARAM_SUPPORTED_EXTENSIONS = 0x8008, PARAM_FORWARD_TSN = 0xC000 };
|
||||
enum { RECONFIG_PERFORMED = 1, RECONFIG_DENIED = 2 };
|
||||
|
||||
#define C4F_SCTP_RWND (128u * 1024u) /* what we take in before delivering */
|
||||
#define C4F_SCTP_OUT_LIMIT (256u * 1024u) /* what may wait for acknowledgement */
|
||||
#define C4F_SCTP_RTO_START 200
|
||||
#define C4F_SCTP_RTO_MIN 100
|
||||
#define C4F_SCTP_RTO_MAX 1000
|
||||
#define C4F_SCTP_WINDOW 1024 /* TSNs tracked beyond the cumulative one */
|
||||
#define C4F_SCTP_MAX_GAPS 32
|
||||
#define C4F_SCTP_OUT_CHUNKS 512
|
||||
#define C4F_SCTP_MAX_BURST 4
|
||||
#define C4F_SCTP_MAX_TIMEOUTS 8
|
||||
#define C4F_SCTP_DATA_MAX ((C4F_SCTP_MTU - 28u) & ~3u)
|
||||
#define C4F_SCTP_INITIAL_CWND 4380u
|
||||
|
||||
struct C4fSctpOut {
|
||||
C4fSctpOut *next;
|
||||
uint32_t tsn, ppid;
|
||||
uint16_t stream, ssn, len;
|
||||
uint8_t flags, reliable, acked, abandoned, fastDone, inFlight;
|
||||
int missing;
|
||||
unsigned sends;
|
||||
uint64_t sentAt;
|
||||
uint8_t data[]; /* retained until cumulative acknowledgement */
|
||||
};
|
||||
|
||||
struct C4fSctpIn {
|
||||
C4fSctpIn *next;
|
||||
uint32_t tsn, ppid;
|
||||
uint16_t stream, ssn, len;
|
||||
uint8_t flags;
|
||||
uint8_t data[];
|
||||
};
|
||||
|
||||
static uint16_t get16(const uint8_t *p) { return (uint16_t)(p[0] << 8 | p[1]); }
|
||||
static uint32_t get32(const uint8_t *p) { return (uint32_t)p[0] << 24 | (uint32_t)p[1] << 16 | (uint32_t)p[2] << 8 | p[3]; }
|
||||
static void put16(uint8_t *p, uint16_t v) { p[0] = v >> 8; p[1] = (uint8_t)v; }
|
||||
static void put32(uint8_t *p, uint32_t v) { p[0] = v >> 24; p[1] = (uint8_t)(v >> 16); p[2] = (uint8_t)(v >> 8); p[3] = (uint8_t)v; }
|
||||
/* Serial number arithmetic (RFC 1982), as TSNs wrap. */
|
||||
static int tsnLt(uint32_t a, uint32_t b) { return (int32_t)(a - b) < 0; }
|
||||
static int tsnLe(uint32_t a, uint32_t b) { return (int32_t)(a - b) <= 0; }
|
||||
|
||||
static uint32_t c4fCrc32cUpdate(uint32_t c, const uint8_t *data, size_t len)
|
||||
{
|
||||
static uint32_t table[256];
|
||||
static int ready;
|
||||
if (!ready) {
|
||||
for (uint32_t i = 0; i < 256; i++) {
|
||||
uint32_t v = i;
|
||||
for (int k = 0; k < 8; k++) v = v & 1 ? 0x82F63B78u ^ (v >> 1) : v >> 1;
|
||||
table[i] = v;
|
||||
}
|
||||
ready = 1;
|
||||
}
|
||||
for (size_t i = 0; i < len; i++) c = table[(c ^ data[i]) & 0xFF] ^ (c >> 8);
|
||||
return c;
|
||||
}
|
||||
|
||||
uint32_t c4fCrc32c(const uint8_t *data, size_t len)
|
||||
{
|
||||
return c4fCrc32cUpdate(0xFFFFFFFFu, data, len) ^ 0xFFFFFFFFu;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ packets */
|
||||
|
||||
typedef struct { uint8_t buf[C4F_SCTP_MTU]; size_t len; } C4fPacket;
|
||||
|
||||
static void pktStart(const C4fSctp *s, C4fPacket *p, uint32_t tag)
|
||||
{
|
||||
put16(p->buf, s->localPort); put16(p->buf + 2, s->remotePort);
|
||||
put32(p->buf + 4, tag); put32(p->buf + 8, 0);
|
||||
p->len = 12;
|
||||
}
|
||||
|
||||
static size_t pktRoom(const C4fPacket *p) { return C4F_SCTP_MTU - p->len; }
|
||||
|
||||
/* Adds a chunk header; the value goes right after it. NULL when it doesn't fit. */
|
||||
static uint8_t *pktChunk(C4fPacket *p, uint8_t type, uint8_t flags, size_t valueLen)
|
||||
{
|
||||
size_t total = (4 + valueLen + 3) & ~(size_t)3;
|
||||
if (total > pktRoom(p)) return NULL;
|
||||
uint8_t *c = p->buf + p->len;
|
||||
memset(c, 0, total);
|
||||
c[0] = type; c[1] = flags; put16(c + 2, (uint16_t)(4 + valueLen));
|
||||
p->len += total;
|
||||
return c + 4;
|
||||
}
|
||||
|
||||
static int pktSend(C4fSctp *s, C4fPacket *p)
|
||||
{
|
||||
if (p->len <= 12) return 0;
|
||||
uint32_t crc = c4fCrc32c(p->buf, p->len);
|
||||
/* The checksum goes in least significant byte first (RFC 9260, appendix A). */
|
||||
p->buf[8] = (uint8_t)crc; p->buf[9] = (uint8_t)(crc >> 8); p->buf[10] = (uint8_t)(crc >> 16); p->buf[11] = (uint8_t)(crc >> 24);
|
||||
if (s->ops.send(s->ops.user, p->buf, p->len)) { s->state = C4F_SCTP_ENDED; return -1; }
|
||||
s->packetsOut++;
|
||||
pktStart(s, p, get32(p->buf + 4));
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ receiving */
|
||||
|
||||
static int seenGet(const C4fSctp *s, uint32_t tsn) { uint32_t i = tsn % C4F_SCTP_WINDOW; return s->seen[i >> 3] >> (i & 7) & 1; }
|
||||
static void seenSet(C4fSctp *s, uint32_t tsn, int on)
|
||||
{
|
||||
uint32_t i = tsn % C4F_SCTP_WINDOW;
|
||||
if (on) s->seen[i >> 3] |= (uint8_t)(1u << (i & 7)); else s->seen[i >> 3] &= (uint8_t)~(1u << (i & 7));
|
||||
}
|
||||
|
||||
static void advanceCum(C4fSctp *s)
|
||||
{
|
||||
while (seenGet(s, s->cumTsn + 1)) { seenSet(s, s->cumTsn + 1, 0); s->cumTsn++; }
|
||||
if (tsnLt(s->highTsn, s->cumTsn)) s->highTsn = s->cumTsn;
|
||||
}
|
||||
|
||||
/* Adds a SACK for what has arrived so far. */
|
||||
static int addSack(C4fSctp *s, C4fPacket *p)
|
||||
{
|
||||
uint16_t gaps[C4F_SCTP_MAX_GAPS][2];
|
||||
int count = 0;
|
||||
uint32_t t = s->cumTsn + 1;
|
||||
while (tsnLe(t, s->highTsn) && count < C4F_SCTP_MAX_GAPS) {
|
||||
while (tsnLe(t, s->highTsn) && !seenGet(s, t)) t++;
|
||||
if (tsnLt(s->highTsn, t)) break;
|
||||
uint32_t start = t;
|
||||
while (tsnLe(t, s->highTsn) && seenGet(s, t)) t++;
|
||||
gaps[count][0] = (uint16_t)(start - s->cumTsn);
|
||||
gaps[count][1] = (uint16_t)(t - 1 - s->cumTsn);
|
||||
count++;
|
||||
}
|
||||
uint8_t *v = pktChunk(p, CHUNK_SACK, 0, 12 + 4 * (size_t)count);
|
||||
if (!v) return -1;
|
||||
put32(v, s->cumTsn);
|
||||
put32(v + 4, s->inBytes < C4F_SCTP_RWND ? C4F_SCTP_RWND - (uint32_t)s->inBytes : 0);
|
||||
put16(v + 8, (uint16_t)count); put16(v + 10, 0);
|
||||
for (int i = 0; i < count; i++) { put16(v + 12 + 4 * i, gaps[i][0]); put16(v + 14 + 4 * i, gaps[i][1]); }
|
||||
s->sackDue = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void freeIn(C4fSctp *s, C4fSctpIn *e)
|
||||
{
|
||||
s->inBytes -= e->len;
|
||||
free(e);
|
||||
}
|
||||
|
||||
/* The message starting at `first` (a BEGIN chunk), if all of it is here: its
|
||||
* last chunk, else NULL. Chunks are kept in TSN order. */
|
||||
static C4fSctpIn *messageEnd(C4fSctpIn *first)
|
||||
{
|
||||
C4fSctpIn *e = first;
|
||||
for (;;) {
|
||||
if (e->flags & DATA_END) return e;
|
||||
C4fSctpIn *n = e->next;
|
||||
if (!n || n->tsn != e->tsn + 1 || n->stream != first->stream ||
|
||||
(n->flags & (DATA_BEGIN | DATA_UNORDERED)) != (first->flags & DATA_UNORDERED) ||
|
||||
(!(first->flags & DATA_UNORDERED) && n->ssn != first->ssn)) return NULL;
|
||||
e = n;
|
||||
}
|
||||
}
|
||||
|
||||
/* Takes a whole message out of the list and hands it over. */
|
||||
static void deliverRun(C4fSctp *s, C4fSctpIn **link, C4fSctpIn *last)
|
||||
{
|
||||
C4fSctpIn *first = *link;
|
||||
size_t total = 0;
|
||||
for (C4fSctpIn *e = first; ; e = e->next) { total += e->len; if (e == last) break; }
|
||||
*link = last->next;
|
||||
uint8_t *message = total <= C4F_SCTP_MAX_MESSAGE ? malloc(total ? total : 1) : NULL;
|
||||
size_t at = 0;
|
||||
uint16_t stream = first->stream;
|
||||
uint32_t ppid = first->ppid;
|
||||
for (C4fSctpIn *e = first, *next; ; e = next) {
|
||||
next = e->next;
|
||||
int end = e == last;
|
||||
if (message) memcpy(message + at, e->data, e->len);
|
||||
at += e->len;
|
||||
freeIn(s, e);
|
||||
if (end) break;
|
||||
}
|
||||
if (message) {
|
||||
s->ops.deliver(s->ops.user, stream, ppid, message, total);
|
||||
free(message);
|
||||
} else s->state = C4F_SCTP_ENDED; /* acknowledged reliable data must not disappear silently */
|
||||
}
|
||||
|
||||
/* Hands over every message that is complete and due. */
|
||||
static void deliverReady(C4fSctp *s)
|
||||
{
|
||||
for (int progressed = 1; progressed; ) {
|
||||
progressed = 0;
|
||||
for (C4fSctpIn **link = &s->in; *link; link = &(*link)->next) {
|
||||
C4fSctpIn *e = *link;
|
||||
if (!(e->flags & DATA_BEGIN)) continue;
|
||||
if (!(e->flags & DATA_UNORDERED) && (e->stream > 1 || e->ssn != s->nextSsn[e->stream])) continue;
|
||||
C4fSctpIn *last = messageEnd(e);
|
||||
if (!last) continue;
|
||||
if (!(e->flags & DATA_UNORDERED)) s->nextSsn[e->stream]++;
|
||||
deliverRun(s, link, last);
|
||||
progressed = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void onData(C4fSctp *s, uint8_t flags, const uint8_t *v, size_t vlen)
|
||||
{
|
||||
if (s->state != C4F_SCTP_ESTABLISHED || vlen < 12) return;
|
||||
uint32_t tsn = get32(v), ppid = get32(v + 8);
|
||||
uint16_t stream = get16(v + 4), ssn = get16(v + 6);
|
||||
const uint8_t *payload = v + 12;
|
||||
size_t plen = vlen - 12;
|
||||
s->sackDue = 1;
|
||||
if (tsnLe(tsn, s->cumTsn)) { s->duplicates++; return; }
|
||||
if (tsn - s->cumTsn > C4F_SCTP_WINDOW) return; /* beyond what we track: it comes again */
|
||||
if (seenGet(s, tsn)) { s->duplicates++; return; }
|
||||
int unordered = flags & DATA_UNORDERED, whole = (flags & (DATA_BEGIN | DATA_END)) == (DATA_BEGIN | DATA_END);
|
||||
int stale = !unordered && stream <= 1 && (int16_t)(ssn - s->nextSsn[stream]) < 0;
|
||||
if (stream > 1 || plen == 0 || plen > C4F_SCTP_MAX_MESSAGE || stale) {
|
||||
/* Not one of ours, or a message already delivered: acknowledged and dropped. */
|
||||
seenSet(s, tsn, 1);
|
||||
} else if (whole && (unordered || ssn == s->nextSsn[stream])) {
|
||||
seenSet(s, tsn, 1);
|
||||
if (tsnLt(s->highTsn, tsn)) s->highTsn = tsn;
|
||||
advanceCum(s);
|
||||
if (!unordered) s->nextSsn[stream]++;
|
||||
s->ops.deliver(s->ops.user, stream, ppid, payload, plen);
|
||||
deliverReady(s);
|
||||
return;
|
||||
} else {
|
||||
if (s->inBytes + plen > C4F_SCTP_RWND) return; /* full: not acknowledged, so it comes again */
|
||||
C4fSctpIn *e = malloc(sizeof(*e) + plen);
|
||||
if (!e) return;
|
||||
e->tsn = tsn; e->ppid = ppid; e->stream = stream; e->ssn = ssn; e->len = (uint16_t)plen; e->flags = flags;
|
||||
memcpy(e->data, payload, plen);
|
||||
C4fSctpIn **link = &s->in;
|
||||
while (*link && tsnLt((*link)->tsn, tsn)) link = &(*link)->next;
|
||||
e->next = *link; *link = e;
|
||||
s->inBytes += plen;
|
||||
seenSet(s, tsn, 1);
|
||||
}
|
||||
if (tsnLt(s->highTsn, tsn)) s->highTsn = tsn;
|
||||
advanceCum(s);
|
||||
deliverReady(s);
|
||||
}
|
||||
|
||||
static void onForwardTsn(C4fSctp *s, const uint8_t *v, size_t vlen)
|
||||
{
|
||||
if (s->state != C4F_SCTP_ESTABLISHED || vlen < 4) return;
|
||||
uint32_t target = get32(v);
|
||||
s->sackDue = 1;
|
||||
if (tsnLe(target, s->cumTsn)) return;
|
||||
if (target - s->cumTsn >= C4F_SCTP_WINDOW) memset(s->seen, 0, sizeof(s->seen));
|
||||
else for (uint32_t t = s->cumTsn + 1; tsnLe(t, target); t++) seenSet(s, t, 0);
|
||||
s->cumTsn = target;
|
||||
if (tsnLt(s->highTsn, target)) s->highTsn = target;
|
||||
/* What it skipped can never be completed now. */
|
||||
for (C4fSctpIn **link = &s->in; *link; ) {
|
||||
if (tsnLe((*link)->tsn, target)) { C4fSctpIn *e = *link; *link = e->next; freeIn(s, e); }
|
||||
else link = &(*link)->next;
|
||||
}
|
||||
for (size_t at = 4; at + 4 <= vlen; at += 4) {
|
||||
uint16_t stream = get16(v + at), ssn = get16(v + at + 2);
|
||||
if (stream <= 1 && (int16_t)(ssn + 1 - s->nextSsn[stream]) > 0) s->nextSsn[stream] = ssn + 1;
|
||||
}
|
||||
advanceCum(s);
|
||||
deliverReady(s);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ sending */
|
||||
|
||||
static void addData(C4fPacket *p, const C4fSctpOut *o, const uint8_t *data)
|
||||
{
|
||||
uint8_t *v = pktChunk(p, CHUNK_DATA, o->flags, 12 + (size_t)o->len);
|
||||
put32(v, o->tsn); put16(v + 4, o->stream); put16(v + 6, o->ssn); put32(v + 8, o->ppid);
|
||||
memcpy(v + 12, data, o->len);
|
||||
}
|
||||
|
||||
static uint32_t dataSize(const C4fSctpOut *o) { return (16u + o->len + 3u) & ~3u; }
|
||||
|
||||
static void releaseFlight(C4fSctp *s, C4fSctpOut *o)
|
||||
{
|
||||
if (!o->inFlight) return;
|
||||
s->flightBytes -= dataSize(o); s->flightData -= o->len; o->inFlight = 0;
|
||||
}
|
||||
|
||||
static void addFlight(C4fSctp *s, C4fSctpOut *o)
|
||||
{
|
||||
if (o->inFlight) return;
|
||||
s->flightBytes += dataSize(o); s->flightData += o->len; o->inFlight = 1;
|
||||
}
|
||||
|
||||
static void retransmit(C4fSctp *s, C4fSctpOut *o, C4fPacket *p, uint64_t now)
|
||||
{
|
||||
if (pktRoom(p) < dataSize(o) && pktSend(s, p)) return;
|
||||
addData(p, o, o->data);
|
||||
o->sends++; o->sentAt = now; o->missing = 0;
|
||||
addFlight(s, o);
|
||||
s->resent++;
|
||||
for (C4fSctpOut *first = s->out; first; first = first->next) {
|
||||
if (!first->sends || first->acked || first->abandoned) continue;
|
||||
if (first == o) s->t3 = now + s->rto;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void removeOut(C4fSctp *s, C4fSctpOut **link)
|
||||
{
|
||||
C4fSctpOut *o = *link;
|
||||
*link = o->next;
|
||||
if (s->outTail == o) {
|
||||
s->outTail = NULL;
|
||||
for (C4fSctpOut *e = s->out; e; e = e->next) s->outTail = e;
|
||||
}
|
||||
releaseFlight(s, o);
|
||||
s->outBytes -= o->len; s->outChunks--;
|
||||
free(o);
|
||||
}
|
||||
|
||||
/* Moves the forward point over everything acknowledged or given up on. */
|
||||
static int advanceForward(C4fSctp *s)
|
||||
{
|
||||
int skipped = 0;
|
||||
if (tsnLt(s->forwardTsn, s->peerCumTsn)) s->forwardTsn = s->peerCumTsn;
|
||||
for (C4fSctpOut *o = s->out; o; o = o->next) {
|
||||
if (tsnLe(o->tsn, s->forwardTsn)) { skipped |= o->abandoned; continue; }
|
||||
if (!o->sends || o->tsn != s->forwardTsn + 1 || !(o->acked || o->abandoned)) break;
|
||||
s->forwardTsn = o->tsn;
|
||||
skipped |= o->abandoned;
|
||||
}
|
||||
return skipped && tsnLt(s->peerCumTsn, s->forwardTsn);
|
||||
}
|
||||
|
||||
static void addForwardTsn(C4fSctp *s, C4fPacket *p)
|
||||
{
|
||||
uint8_t *v = pktChunk(p, CHUNK_FORWARD_TSN, 0, 4);
|
||||
if (!v) { if (pktSend(s, p)) return; v = pktChunk(p, CHUNK_FORWARD_TSN, 0, 4); }
|
||||
put32(v, s->forwardTsn);
|
||||
}
|
||||
|
||||
static int anyOutstanding(const C4fSctp *s)
|
||||
{
|
||||
for (const C4fSctpOut *o = s->out; o; o = o->next) if (o->sends && !o->acked && !o->abandoned) return 1;
|
||||
return tsnLt(s->peerCumTsn, s->forwardTsn);
|
||||
}
|
||||
|
||||
static int hasQueued(const C4fSctp *s)
|
||||
{
|
||||
for (const C4fSctpOut *o = s->out; o; o = o->next) if (!o->sends) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Release up to Max.Burst packets without delaying the first for batching.
|
||||
* A closed (or too-small) peer window gets one DATA probe after an RTO. */
|
||||
static void pump(C4fSctp *s, uint64_t now)
|
||||
{
|
||||
unsigned burst = 0;
|
||||
s->sendAt = 0;
|
||||
for (C4fSctpOut *o = s->out; o && burst < C4F_SCTP_MAX_BURST && s->state == C4F_SCTP_ESTABLISHED; o = o->next) {
|
||||
if (o->sends) continue;
|
||||
uint32_t room = s->peerRwnd > s->flightData ? s->peerRwnd - s->flightData : 0;
|
||||
if (s->flightBytes + dataSize(o) > s->cwnd) break;
|
||||
int probe = o->len > room;
|
||||
if (probe) {
|
||||
if (s->flightBytes) break;
|
||||
if (!s->probeAt) s->probeAt = now + s->rto;
|
||||
if (now < s->probeAt) break;
|
||||
} else s->probeAt = 0;
|
||||
C4fPacket p;
|
||||
pktStart(s, &p, s->remoteTag);
|
||||
if (s->sackDue) addSack(s, &p);
|
||||
if (pktRoom(&p) < dataSize(o) && pktSend(s, &p)) return;
|
||||
addData(&p, o, o->data);
|
||||
if (pktSend(s, &p)) return;
|
||||
o->sends = 1; o->sentAt = now;
|
||||
s->highestSent = o->tsn;
|
||||
addFlight(s, o);
|
||||
if (!s->t3) s->t3 = now + s->rto;
|
||||
burst++;
|
||||
if (probe) { s->probeAt = 0; break; }
|
||||
}
|
||||
if (burst == C4F_SCTP_MAX_BURST) {
|
||||
for (C4fSctpOut *o = s->out; o; o = o->next) if (!o->sends) {
|
||||
uint32_t room = s->peerRwnd > s->flightData ? s->peerRwnd - s->flightData : 0;
|
||||
if (o->len <= room && s->flightBytes + dataSize(o) <= s->cwnd) s->sendAt = now + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void reduceWindow(C4fSctp *s, int timeout)
|
||||
{
|
||||
s->ssthresh = s->cwnd / 2;
|
||||
if (s->ssthresh < 4u * C4F_SCTP_MTU) s->ssthresh = 4u * C4F_SCTP_MTU;
|
||||
s->cwnd = timeout ? C4F_SCTP_MTU : s->ssthresh;
|
||||
s->partialBytesAcked = 0;
|
||||
}
|
||||
|
||||
static int sackHas(const uint8_t *v, size_t gaps, uint32_t cum, uint32_t tsn)
|
||||
{
|
||||
if (tsnLe(tsn, cum)) return 1;
|
||||
for (size_t i = 0; i < gaps; i++) {
|
||||
uint32_t from = cum + get16(v + 12 + 4 * i), to = cum + get16(v + 14 + 4 * i);
|
||||
if (tsnLe(from, tsn) && tsnLe(tsn, to)) return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void onSack(C4fSctp *s, const uint8_t *v, size_t vlen, uint64_t now)
|
||||
{
|
||||
if (s->state != C4F_SCTP_ESTABLISHED || vlen < 12) return;
|
||||
uint32_t cum = get32(v);
|
||||
size_t gaps = get16(v + 8);
|
||||
if (12 + 4 * gaps > vlen || tsnLt(cum, s->peerCumTsn) || tsnLt(s->highestSent, cum)) return;
|
||||
uint16_t last = 0;
|
||||
for (size_t i = 0; i < gaps; i++) {
|
||||
uint16_t from = get16(v + 12 + 4 * i), to = get16(v + 14 + 4 * i);
|
||||
if (!from || from <= last || to < from || tsnLt(s->highestSent, cum + to)) return;
|
||||
last = to;
|
||||
}
|
||||
int advanced = tsnLt(s->peerCumTsn, cum);
|
||||
uint32_t flightBefore = s->flightBytes, ackBytes = 0, highestNew = cum;
|
||||
C4fSctpOut *sample = NULL;
|
||||
for (C4fSctpOut *o = s->out; o && o->sends; o = o->next) {
|
||||
int acked = sackHas(v, gaps, cum, o->tsn);
|
||||
if (acked && !o->acked && !o->abandoned) {
|
||||
ackBytes += dataSize(o);
|
||||
if (tsnLt(highestNew, o->tsn)) highestNew = o->tsn;
|
||||
if (o->sends == 1 && tsnLe(o->tsn, cum)) sample = o;
|
||||
}
|
||||
if (acked) releaseFlight(s, o);
|
||||
else if (o->acked && !o->abandoned) addFlight(s, o); /* receiver reneged a gap ACK */
|
||||
o->acked = (uint8_t)acked;
|
||||
}
|
||||
if (sample && advanced) {
|
||||
uint64_t r = now >= sample->sentAt ? now - sample->sentAt : 0;
|
||||
if (!s->rttKnown) { s->srtt = r; s->rttvar = r / 2; s->rttKnown = 1; }
|
||||
else {
|
||||
uint64_t d = s->srtt > r ? s->srtt - r : r - s->srtt;
|
||||
s->rttvar = (3 * s->rttvar + d) / 4;
|
||||
s->srtt = (7 * s->srtt + r) / 8;
|
||||
}
|
||||
uint64_t rto = s->srtt + 4 * s->rttvar;
|
||||
s->rto = rto < C4F_SCTP_RTO_MIN ? C4F_SCTP_RTO_MIN : rto > C4F_SCTP_RTO_MAX ? C4F_SCTP_RTO_MAX : rto;
|
||||
}
|
||||
if (s->fastRecovery && tsnLe(s->recoveryTsn, cum)) s->fastRecovery = 0;
|
||||
if (advanced && !s->fastRecovery && flightBefore + C4F_SCTP_MTU >= s->cwnd) {
|
||||
if (s->cwnd <= s->ssthresh) s->cwnd += ackBytes < C4F_SCTP_MTU ? ackBytes : C4F_SCTP_MTU;
|
||||
else {
|
||||
s->partialBytesAcked += ackBytes;
|
||||
if (s->partialBytesAcked >= s->cwnd) { s->partialBytesAcked -= s->cwnd; s->cwnd += C4F_SCTP_MTU; }
|
||||
}
|
||||
if (s->cwnd > C4F_SCTP_OUT_LIMIT) s->cwnd = C4F_SCTP_OUT_LIMIT;
|
||||
}
|
||||
while (s->out && tsnLe(s->out->tsn, cum)) removeOut(s, &s->out);
|
||||
s->peerCumTsn = cum;
|
||||
s->peerRwnd = get32(v + 4);
|
||||
s->timeouts = 0;
|
||||
if (s->peerRwnd) s->probeAt = 0;
|
||||
C4fPacket p;
|
||||
pktStart(s, &p, s->remoteTag);
|
||||
int fastSent = 0;
|
||||
for (C4fSctpOut *o = s->out; o && o->sends && tsnLt(o->tsn, highestNew); o = o->next) {
|
||||
if (o->acked || o->abandoned || o->fastDone) continue;
|
||||
/* Only newly acknowledged TSNs report a new hole (HTNA): a repeated
|
||||
* copy of the same SACK must not trigger retransmission. */
|
||||
if (++o->missing < 3) continue;
|
||||
if (!s->fastRecovery) {
|
||||
reduceWindow(s, 0); s->fastRecovery = 1; s->recoveryTsn = s->highestSent;
|
||||
}
|
||||
if (!o->reliable) { releaseFlight(s, o); o->abandoned = 1; o->fastDone = 1; s->abandoned++; }
|
||||
else if (!fastSent) {
|
||||
o->fastDone = 1; retransmit(s, o, &p, now); fastSent = 1;
|
||||
}
|
||||
}
|
||||
if (advanceForward(s)) addForwardTsn(s, &p);
|
||||
if (pktSend(s, &p)) return;
|
||||
if (!anyOutstanding(s)) s->t3 = 0;
|
||||
else if (advanced || !s->t3) s->t3 = now + s->rto;
|
||||
pump(s, now);
|
||||
}
|
||||
|
||||
int c4fSctpSend(C4fSctp *s, uint16_t stream, uint32_t ppid, const uint8_t *data, size_t len, uint64_t now)
|
||||
{
|
||||
if (s->state != C4F_SCTP_ESTABLISHED || stream > 1 || len == 0 || len > C4F_SCTP_MAX_MESSAGE) return -1;
|
||||
int reliable = stream == 0;
|
||||
unsigned chunks = (unsigned)((len + C4F_SCTP_DATA_MAX - 1) / C4F_SCTP_DATA_MAX);
|
||||
if (s->outBytes + len > C4F_SCTP_OUT_LIMIT || s->outChunks + chunks > C4F_SCTP_OUT_CHUNKS) return -1;
|
||||
/* Never queue stale unreliable state behind a window or another message. */
|
||||
uint32_t room = s->peerRwnd > s->flightData ? s->peerRwnd - s->flightData : 0;
|
||||
if (!reliable && (chunks != 1 || hasQueued(s) || len > room ||
|
||||
s->flightBytes + ((16u + len + 3u) & ~3u) > s->cwnd)) return -1;
|
||||
C4fSctpOut *head = NULL, *tail = NULL;
|
||||
size_t at = 0;
|
||||
while (at < len) {
|
||||
size_t part = len - at < C4F_SCTP_DATA_MAX ? len - at : C4F_SCTP_DATA_MAX;
|
||||
C4fSctpOut *o = calloc(1, sizeof(*o) + part);
|
||||
if (!o) {
|
||||
while (head) { C4fSctpOut *next = head->next; free(head); head = next; }
|
||||
return -1; /* no partial message or consumed sequence numbers */
|
||||
}
|
||||
o->ppid = ppid; o->stream = stream; o->ssn = reliable ? s->ssn[stream] : 0;
|
||||
o->len = (uint16_t)part; o->reliable = (uint8_t)reliable;
|
||||
o->flags = (uint8_t)((reliable ? 0 : DATA_UNORDERED) | (at == 0 ? DATA_BEGIN : 0) |
|
||||
(at + part == len ? DATA_END : 0));
|
||||
memcpy(o->data, data + at, part);
|
||||
if (tail) tail->next = o; else head = o;
|
||||
tail = o; at += part;
|
||||
}
|
||||
for (C4fSctpOut *o = head; o; o = o->next) o->tsn = s->nextTsn++;
|
||||
if (s->outTail) s->outTail->next = head; else s->out = head;
|
||||
s->outTail = tail; s->outBytes += len; s->outChunks += chunks;
|
||||
if (reliable) s->ssn[stream]++;
|
||||
pump(s, now);
|
||||
return s->state == C4F_SCTP_ESTABLISHED ? 0 : -1;
|
||||
}
|
||||
|
||||
void c4fSctpTick(C4fSctp *s, uint64_t now)
|
||||
{
|
||||
if (s->state != C4F_SCTP_ESTABLISHED) return;
|
||||
if (s->t3 && now >= s->t3) {
|
||||
if (++s->timeouts > C4F_SCTP_MAX_TIMEOUTS) { s->state = C4F_SCTP_ENDED; return; }
|
||||
C4fPacket p;
|
||||
pktStart(s, &p, s->remoteTag);
|
||||
int retransmitted = 0;
|
||||
if (s->peerRwnd) reduceWindow(s, 1);
|
||||
s->fastRecovery = 0;
|
||||
for (C4fSctpOut *o = s->out; o && o->sends; o = o->next) {
|
||||
if (o->acked || o->abandoned || now - o->sentAt < s->rto) continue;
|
||||
if (!o->reliable) { releaseFlight(s, o); o->abandoned = 1; s->abandoned++; }
|
||||
else if (!retransmitted) {
|
||||
o->fastDone = 0; retransmit(s, o, &p, now); retransmitted = 1;
|
||||
}
|
||||
}
|
||||
s->rto = s->rto * 2 > C4F_SCTP_RTO_MAX ? C4F_SCTP_RTO_MAX : s->rto * 2;
|
||||
if (advanceForward(s) || tsnLt(s->peerCumTsn, s->forwardTsn)) addForwardTsn(s, &p);
|
||||
if (pktSend(s, &p)) return;
|
||||
s->t3 = anyOutstanding(s) ? now + s->rto : 0;
|
||||
}
|
||||
pump(s, now);
|
||||
}
|
||||
|
||||
uint64_t c4fSctpDeadline(const C4fSctp *s)
|
||||
{
|
||||
if (s->state != C4F_SCTP_ESTABLISHED) return 0;
|
||||
uint64_t due = s->t3;
|
||||
if (s->probeAt && (!due || s->probeAt < due)) due = s->probeAt;
|
||||
if (s->sendAt && (!due || s->sendAt < due)) due = s->sendAt;
|
||||
return due;
|
||||
}
|
||||
static void freeQueues(C4fSctp *s)
|
||||
{
|
||||
while (s->out) { C4fSctpOut *o = s->out; s->out = o->next; free(o); }
|
||||
while (s->in) { C4fSctpIn *e = s->in; s->in = e->next; free(e); }
|
||||
s->outTail = NULL; s->outBytes = s->inBytes = 0; s->outChunks = 0;
|
||||
s->flightBytes = s->flightData = 0;
|
||||
}
|
||||
|
||||
void c4fSctpInit(C4fSctp *s, const C4fSctpOps *ops)
|
||||
{
|
||||
memset(s, 0, sizeof(*s));
|
||||
s->ops = *ops;
|
||||
s->state = C4F_SCTP_CLOSED;
|
||||
s->rto = C4F_SCTP_RTO_START;
|
||||
}
|
||||
|
||||
void c4fSctpFree(C4fSctp *s)
|
||||
{
|
||||
freeQueues(s);
|
||||
s->state = C4F_SCTP_ENDED;
|
||||
}
|
||||
|
||||
static void onInit(C4fSctp *s, const uint8_t *packet, const uint8_t *v, size_t vlen)
|
||||
{
|
||||
if (vlen < 16) return;
|
||||
uint32_t tag = get32(v), tsn = get32(v + 12);
|
||||
uint16_t os = get16(v + 8), mis = get16(v + 10);
|
||||
if (!tag || os < 2 || mis < 2 || get32(v + 4) < 1500) return;
|
||||
int duplicate = (s->state == C4F_SCTP_COOKIE_WAIT || s->state == C4F_SCTP_ESTABLISHED) &&
|
||||
s->remoteTag == tag && s->remoteInitialTsn == tsn;
|
||||
if (!duplicate) {
|
||||
/* A restart needs a fresh DTLS/page session too, so ownership and
|
||||
* application sequence floors cannot survive an association reset. */
|
||||
if (s->state == C4F_SCTP_ESTABLISHED) { s->state = C4F_SCTP_ENDED; return; }
|
||||
freeQueues(s);
|
||||
memset(s->seen, 0, sizeof(s->seen));
|
||||
s->remotePort = get16(packet); s->localPort = get16(packet + 2);
|
||||
s->remoteTag = tag; s->remoteInitialTsn = tsn;
|
||||
do s->ops.random(s->ops.user, &s->localTag, sizeof(s->localTag)); while (!s->localTag);
|
||||
s->ops.random(s->ops.user, &s->nextTsn, sizeof(s->nextTsn));
|
||||
s->localInitialTsn = s->nextTsn;
|
||||
s->ops.random(s->ops.user, s->cookie, sizeof(s->cookie));
|
||||
s->cumTsn = s->highTsn = tsn - 1;
|
||||
s->nextSsn[0] = s->nextSsn[1] = s->ssn[0] = s->ssn[1] = 0;
|
||||
s->peerCumTsn = s->forwardTsn = s->nextTsn - 1;
|
||||
s->highestSent = s->nextTsn - 1;
|
||||
s->peerRwnd = get32(v + 4); s->cwnd = C4F_SCTP_INITIAL_CWND; s->ssthresh = s->peerRwnd;
|
||||
s->partialBytesAcked = 0; s->fastRecovery = 0; s->probeAt = s->sendAt = 0; s->timeouts = 0;
|
||||
s->rto = C4F_SCTP_RTO_START; s->rttKnown = 0; s->t3 = 0; s->sackDue = 0;
|
||||
s->state = C4F_SCTP_COOKIE_WAIT;
|
||||
}
|
||||
|
||||
C4fPacket p;
|
||||
pktStart(s, &p, tag);
|
||||
uint8_t *a = pktChunk(&p, CHUNK_INIT_ACK, 0, 16 + 20 + 8 + 4);
|
||||
put32(a, s->localTag);
|
||||
put32(a + 4, C4F_SCTP_RWND);
|
||||
put16(a + 8, mis); /* outbound streams: as many as it takes in */
|
||||
put16(a + 10, 65535); /* inbound */
|
||||
put32(a + 12, s->localInitialTsn);
|
||||
put16(a + 16, PARAM_STATE_COOKIE); put16(a + 18, 4 + sizeof(s->cookie));
|
||||
memcpy(a + 20, s->cookie, sizeof(s->cookie));
|
||||
put16(a + 36, PARAM_SUPPORTED_EXTENSIONS); put16(a + 38, 6);
|
||||
a[40] = CHUNK_FORWARD_TSN; a[41] = CHUNK_RECONFIG; /* a[42..43]: padding */
|
||||
put16(a + 44, PARAM_FORWARD_TSN); put16(a + 46, 4);
|
||||
pktSend(s, &p);
|
||||
}
|
||||
|
||||
static void onCookieEcho(C4fSctp *s, const uint8_t *v, size_t vlen)
|
||||
{
|
||||
if (s->state != C4F_SCTP_COOKIE_WAIT && s->state != C4F_SCTP_ESTABLISHED) return;
|
||||
if (vlen != sizeof(s->cookie) || memcmp(v, s->cookie, sizeof(s->cookie))) return;
|
||||
s->state = C4F_SCTP_ESTABLISHED;
|
||||
C4fPacket p;
|
||||
pktStart(s, &p, s->remoteTag);
|
||||
pktChunk(&p, CHUNK_COOKIE_ACK, 0, 0);
|
||||
pktSend(s, &p);
|
||||
}
|
||||
|
||||
/* Stream resets (RFC 6525): the browser closing a channel resets its side of
|
||||
* the stream; the next message on it starts again at 0. */
|
||||
static void onReconfig(C4fSctp *s, C4fPacket *reply, const uint8_t *v, size_t vlen)
|
||||
{
|
||||
for (size_t at = 0; at + 4 <= vlen; ) {
|
||||
uint16_t type = get16(v + at), size = get16(v + at + 2);
|
||||
if (size < 4 || at + size > vlen) break;
|
||||
if ((type == PARAM_OUTGOING_RESET && size >= 16) || (type == PARAM_INCOMING_RESET && size >= 8)) {
|
||||
uint32_t seq = get32(v + at + 4);
|
||||
int result = RECONFIG_DENIED;
|
||||
if (type == PARAM_OUTGOING_RESET) {
|
||||
size_t first = at + 16, count = (size - 16) / 2;
|
||||
if (!count) s->nextSsn[0] = s->nextSsn[1] = 0;
|
||||
for (size_t i = 0; i < count; i++) { uint16_t st = get16(v + first + 2 * i); if (st <= 1) s->nextSsn[st] = 0; }
|
||||
result = RECONFIG_PERFORMED;
|
||||
}
|
||||
uint8_t *r = pktChunk(reply, CHUNK_RECONFIG, 0, 12);
|
||||
if (!r) { pktSend(s, reply); r = pktChunk(reply, CHUNK_RECONFIG, 0, 12); }
|
||||
put16(r, PARAM_RECONFIG_RESPONSE); put16(r + 2, 12);
|
||||
put32(r + 4, seq); put32(r + 8, (uint32_t)result);
|
||||
}
|
||||
at += (size + 3) & ~3u;
|
||||
}
|
||||
}
|
||||
|
||||
void c4fSctpReceive(C4fSctp *s, const uint8_t *packet, size_t len, uint64_t now)
|
||||
{
|
||||
if (len < 16 || s->state == C4F_SCTP_ENDED) return;
|
||||
/* The checksum is taken over the packet with its own field as zero. */
|
||||
static const uint8_t zero[4];
|
||||
uint32_t c = c4fCrc32cUpdate(0xFFFFFFFFu, packet, 8);
|
||||
c = c4fCrc32cUpdate(c, zero, 4);
|
||||
c = c4fCrc32cUpdate(c, packet + 12, len - 12) ^ 0xFFFFFFFFu;
|
||||
uint32_t sent = (uint32_t)packet[8] | (uint32_t)packet[9] << 8 | (uint32_t)packet[10] << 16 | (uint32_t)packet[11] << 24;
|
||||
if (c != sent) { s->badChecksums++; return; }
|
||||
s->packetsIn++;
|
||||
uint32_t tag = get32(packet + 4);
|
||||
if (packet[12] == CHUNK_INIT) {
|
||||
size_t size = get16(packet + 14);
|
||||
if (tag == 0 && size >= 20 && size <= len - 12) onInit(s, packet, packet + 16, size - 4);
|
||||
return;
|
||||
}
|
||||
if (s->state == C4F_SCTP_CLOSED || tag != s->localTag) return;
|
||||
|
||||
C4fPacket reply;
|
||||
pktStart(s, &reply, s->remoteTag);
|
||||
for (size_t at = 12; at + 4 <= len; ) {
|
||||
uint8_t type = packet[at], flags = packet[at + 1];
|
||||
size_t size = get16(packet + at + 2);
|
||||
if (size < 4 || at + size > len) break;
|
||||
const uint8_t *v = packet + at + 4;
|
||||
size_t vlen = size - 4;
|
||||
switch (type) {
|
||||
case CHUNK_DATA: onData(s, flags, v, vlen); break;
|
||||
case CHUNK_SACK: onSack(s, v, vlen, now); break;
|
||||
case CHUNK_HEARTBEAT: {
|
||||
uint8_t *r = pktChunk(&reply, CHUNK_HEARTBEAT_ACK, 0, vlen);
|
||||
if (!r) { pktSend(s, &reply); r = pktChunk(&reply, CHUNK_HEARTBEAT_ACK, 0, vlen); }
|
||||
if (r) memcpy(r, v, vlen);
|
||||
break;
|
||||
}
|
||||
case CHUNK_COOKIE_ECHO: onCookieEcho(s, v, vlen); break;
|
||||
case CHUNK_FORWARD_TSN: onForwardTsn(s, v, vlen); break;
|
||||
case CHUNK_RECONFIG: if (s->state == C4F_SCTP_ESTABLISHED) onReconfig(s, &reply, v, vlen); break;
|
||||
case CHUNK_SHUTDOWN:
|
||||
pktChunk(&reply, CHUNK_SHUTDOWN_ACK, 0, 0);
|
||||
pktSend(s, &reply);
|
||||
s->state = C4F_SCTP_ENDED;
|
||||
return;
|
||||
case CHUNK_ABORT: case CHUNK_SHUTDOWN_COMPLETE:
|
||||
s->state = C4F_SCTP_ENDED;
|
||||
return;
|
||||
case CHUNK_INIT_ACK: case CHUNK_COOKIE_ACK: case CHUNK_ERROR: case CHUNK_HEARTBEAT_ACK: case CHUNK_SHUTDOWN_ACK:
|
||||
break;
|
||||
default:
|
||||
/* Unknown: the top two bits say whether to skip it or stop here. */
|
||||
if (!(type & 0x80)) { at = len; continue; }
|
||||
break;
|
||||
}
|
||||
if (s->state == C4F_SCTP_ENDED) return;
|
||||
at += (size + 3) & ~(size_t)3;
|
||||
}
|
||||
if (s->sackDue && s->state == C4F_SCTP_ESTABLISHED && addSack(s, &reply) != 0) { pktSend(s, &reply); addSack(s, &reply); }
|
||||
pktSend(s, &reply);
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
/* STUN for an ICE-lite WebRTC endpoint: checks the browser's connectivity checks
|
||||
* and answers them. See c4f_stun.h. */
|
||||
#include <string.h>
|
||||
|
||||
#include <mbedtls/md.h>
|
||||
|
||||
#include "c4f_stun.h"
|
||||
|
||||
enum {
|
||||
STUN_BINDING_REQUEST = 0x0001,
|
||||
STUN_BINDING_SUCCESS = 0x0101,
|
||||
ATTR_USERNAME = 0x0006,
|
||||
ATTR_MESSAGE_INTEGRITY = 0x0008,
|
||||
ATTR_XOR_MAPPED_ADDRESS = 0x0020,
|
||||
ATTR_USE_CANDIDATE = 0x0025,
|
||||
ATTR_FINGERPRINT = 0x8028,
|
||||
};
|
||||
|
||||
static uint16_t c4fGet16(const uint8_t *p) { return (uint16_t)(p[0] << 8 | p[1]); }
|
||||
static uint32_t c4fGet32(const uint8_t *p) { return (uint32_t)p[0] << 24 | (uint32_t)p[1] << 16 | (uint32_t)p[2] << 8 | p[3]; }
|
||||
static void c4fPut16(uint8_t *p, uint16_t v) { p[0] = v >> 8; p[1] = (uint8_t)v; }
|
||||
static void c4fPut32(uint8_t *p, uint32_t v) { p[0] = v >> 24; p[1] = (uint8_t)(v >> 16); p[2] = (uint8_t)(v >> 8); p[3] = (uint8_t)v; }
|
||||
|
||||
/* CRC-32 in pieces: start from 0xFFFFFFFF, invert at the end. */
|
||||
static uint32_t c4fCrc32Update(uint32_t c, const uint8_t *data, size_t len)
|
||||
{
|
||||
static uint32_t table[256];
|
||||
static int ready;
|
||||
if (!ready) {
|
||||
for (uint32_t i = 0; i < 256; i++) {
|
||||
uint32_t v = i;
|
||||
for (int k = 0; k < 8; k++) v = v & 1 ? 0xEDB88320u ^ (v >> 1) : v >> 1;
|
||||
table[i] = v;
|
||||
}
|
||||
ready = 1;
|
||||
}
|
||||
for (size_t i = 0; i < len; i++) c = table[(c ^ data[i]) & 0xFF] ^ (c >> 8);
|
||||
return c;
|
||||
}
|
||||
|
||||
uint32_t c4fCrc32(const uint8_t *data, size_t len)
|
||||
{
|
||||
return c4fCrc32Update(0xFFFFFFFFu, data, len) ^ 0xFFFFFFFFu;
|
||||
}
|
||||
|
||||
/* A STUN message filling the whole datagram, or 0. */
|
||||
static int c4fStunWellFormed(const uint8_t *msg, size_t len)
|
||||
{
|
||||
if (len < 20 || len > 1500 || (msg[0] & 0xC0)) return 0;
|
||||
if (c4fGet32(msg + 4) != C4F_STUN_MAGIC) return 0;
|
||||
size_t body = c4fGet16(msg + 2);
|
||||
return (body & 3) == 0 && body + 20 == len;
|
||||
}
|
||||
|
||||
/* Finds an attribute: the offset of its header, or 0. */
|
||||
static size_t c4fStunFind(const uint8_t *msg, size_t len, uint16_t type)
|
||||
{
|
||||
for (size_t at = 20; at + 4 <= len; ) {
|
||||
size_t size = c4fGet16(msg + at + 2);
|
||||
if (at + 4 + size > len) return 0;
|
||||
if (c4fGet16(msg + at) == type) return at;
|
||||
at += 4 + ((size + 3) & ~(size_t)3);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* HMAC-SHA1 over the message up to an attribute at `at`, with the header's
|
||||
* length set as if a MESSAGE-INTEGRITY there were the last attribute. */
|
||||
static void c4fStunHmac(const uint8_t *msg, size_t at, const char *key, uint8_t out[20])
|
||||
{
|
||||
uint8_t head[20];
|
||||
memcpy(head, msg, 20);
|
||||
c4fPut16(head + 2, (uint16_t)(at - 20 + 24));
|
||||
const mbedtls_md_info_t *sha1 = mbedtls_md_info_from_type(MBEDTLS_MD_SHA1);
|
||||
mbedtls_md_context_t ctx;
|
||||
mbedtls_md_init(&ctx);
|
||||
if (mbedtls_md_setup(&ctx, sha1, 1) == 0 &&
|
||||
mbedtls_md_hmac_starts(&ctx, (const unsigned char *)key, strlen(key)) == 0 &&
|
||||
mbedtls_md_hmac_update(&ctx, head, 20) == 0 &&
|
||||
mbedtls_md_hmac_update(&ctx, msg + 20, at - 20) == 0 &&
|
||||
mbedtls_md_hmac_finish(&ctx, out) == 0) {
|
||||
mbedtls_md_free(&ctx);
|
||||
return;
|
||||
}
|
||||
mbedtls_md_free(&ctx);
|
||||
memset(out, 0, 20);
|
||||
}
|
||||
|
||||
/* CRC-32 over the message up to a FINGERPRINT at `at`, the length counting it. */
|
||||
static uint32_t c4fStunFingerprint(const uint8_t *msg, size_t at)
|
||||
{
|
||||
uint8_t head[20];
|
||||
memcpy(head, msg, 20);
|
||||
c4fPut16(head + 2, (uint16_t)(at - 20 + 8));
|
||||
uint32_t c = c4fCrc32Update(0xFFFFFFFFu, head, 20);
|
||||
c = c4fCrc32Update(c, msg + 20, at - 20);
|
||||
return (c ^ 0xFFFFFFFFu) ^ 0x5354554Eu;
|
||||
}
|
||||
|
||||
const uint8_t *c4fStunUsername(const uint8_t *msg, size_t len, size_t *size)
|
||||
{
|
||||
if (!c4fStunWellFormed(msg, len) || c4fGet16(msg) != STUN_BINDING_REQUEST) return NULL;
|
||||
size_t at = c4fStunFind(msg, len, ATTR_USERNAME);
|
||||
if (!at) return NULL;
|
||||
*size = c4fGet16(msg + at + 2);
|
||||
return msg + at + 4;
|
||||
}
|
||||
|
||||
int c4fStunVerify(const uint8_t *msg, size_t len, const char *key)
|
||||
{
|
||||
if (!c4fStunWellFormed(msg, len)) return 0;
|
||||
size_t mi = c4fStunFind(msg, len, ATTR_MESSAGE_INTEGRITY);
|
||||
if (!mi || c4fGet16(msg + mi + 2) != 20) return 0;
|
||||
uint8_t mac[20];
|
||||
c4fStunHmac(msg, mi, key, mac);
|
||||
/* Constant time is not needed against a LAN guesser, but costs nothing. */
|
||||
uint8_t diff = 0;
|
||||
for (int i = 0; i < 20; i++) diff |= mac[i] ^ msg[mi + 4 + i];
|
||||
if (diff) return 0;
|
||||
/* Only FINGERPRINT may follow MESSAGE-INTEGRITY. */
|
||||
size_t after = mi + 24;
|
||||
if (after == len) return 1;
|
||||
if (after + 8 != len || c4fGet16(msg + after) != ATTR_FINGERPRINT || c4fGet16(msg + after + 2) != 4) return 0;
|
||||
return c4fGet32(msg + after + 4) == c4fStunFingerprint(msg, after);
|
||||
}
|
||||
|
||||
size_t c4fStunAnswer(const uint8_t *msg, size_t len, const char *password,
|
||||
uint32_t address, uint16_t port, uint8_t out[C4F_STUN_ANSWER_SIZE], int *nominate)
|
||||
{
|
||||
*nominate = 0;
|
||||
if (!c4fStunWellFormed(msg, len) || c4fGet16(msg) != STUN_BINDING_REQUEST) return 0;
|
||||
if (!c4fStunVerify(msg, len, password)) return 0;
|
||||
size_t mi = c4fStunFind(msg, len, ATTR_MESSAGE_INTEGRITY);
|
||||
size_t use = c4fStunFind(msg, len, ATTR_USE_CANDIDATE);
|
||||
*nominate = use && use < mi;
|
||||
|
||||
memset(out, 0, C4F_STUN_ANSWER_SIZE);
|
||||
c4fPut16(out, STUN_BINDING_SUCCESS);
|
||||
c4fPut16(out + 2, 44); /* 12 + 24 + 8 */
|
||||
memcpy(out + 4, msg + 4, 16); /* magic cookie and transaction ID */
|
||||
/* XOR-MAPPED-ADDRESS: the address and port as the PS4 saw them. */
|
||||
uint8_t *a = out + 20;
|
||||
c4fPut16(a, ATTR_XOR_MAPPED_ADDRESS); c4fPut16(a + 2, 8);
|
||||
a[5] = 0x01; /* IPv4 */
|
||||
uint16_t hostPort = (uint16_t)(((const uint8_t *)&port)[0] << 8 | ((const uint8_t *)&port)[1]);
|
||||
c4fPut16(a + 6, hostPort ^ (uint16_t)(C4F_STUN_MAGIC >> 16));
|
||||
uint32_t hostAddress = c4fGet32((const uint8_t *)&address);
|
||||
c4fPut32(a + 8, hostAddress ^ C4F_STUN_MAGIC);
|
||||
/* MESSAGE-INTEGRITY with our password, then FINGERPRINT. */
|
||||
uint8_t *m = out + 32;
|
||||
c4fPut16(m, ATTR_MESSAGE_INTEGRITY); c4fPut16(m + 2, 20);
|
||||
c4fStunHmac(out, 32, password, m + 4);
|
||||
uint8_t *f = out + 56;
|
||||
c4fPut16(f, ATTR_FINGERPRINT); c4fPut16(f + 2, 4);
|
||||
c4fPut32(f + 4, c4fStunFingerprint(out, 56));
|
||||
return C4F_STUN_ANSWER_SIZE;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
/* Browser-controlled VDA lifecycle. No automatic button presses or user binds.
|
||||
* The page talks JSON over a WebSocket: info, status, claim, u (input), leave, stop. */
|
||||
* WebRTC control carries requests and status; its unordered input channel
|
||||
* carries sequenced controller state and motion. */
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdio.h>
|
||||
@@ -34,11 +35,17 @@
|
||||
/* How often each controller's rumble and light bar are read back. Games pulse
|
||||
* rumble for a few tens of milliseconds, so this has to stay well under 100. */
|
||||
#define C4F_FEEDBACK_MS 16
|
||||
/* Rumble goes where a lost message isn't resent, so while it's on it is sent
|
||||
* again this often. */
|
||||
#define C4F_RUMBLE_REPEAT_MS 200
|
||||
#define C4F_USER_POLL_MS 250
|
||||
#ifndef C4F_LATENCY_MS
|
||||
#define C4F_LATENCY_MS 6000
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
C4fVirtualPad device;
|
||||
C4fNetClient *owner;
|
||||
C4fConn *owner;
|
||||
ScePadData current, queue[C4F_QUEUE_SIZE];
|
||||
unsigned head, count;
|
||||
uint64_t lastInput, lastReport, nextReport, detachedAt, reports;
|
||||
@@ -48,15 +55,34 @@ typedef struct {
|
||||
C4fPadFeedback feedback;
|
||||
int feedbackKnown, rumbleSentValid, feedbackFailed;
|
||||
uint8_t rumbleSent[2];
|
||||
uint64_t feedbackAt, colorLogAt;
|
||||
uint64_t feedbackAt, colorLogAt, rumbleSentAt;
|
||||
uint8_t colorLogged[3];
|
||||
int colorLogKnown;
|
||||
/* The signed-in user's name, once the PS4 will tell it. */
|
||||
char userName[72];
|
||||
int nameTries;
|
||||
uint64_t nameAt;
|
||||
/* Motion its owner sends ('m'), put into every sample. Until then, and after
|
||||
* any neutral reset, samples carry the still values c4fPadDataNeutral sets. */
|
||||
int motion;
|
||||
float quat[4], accel[3], gyro[3];
|
||||
/* The newest input ('i') and motion ('m') applied: over WebRTC they can
|
||||
* arrive late or out of order, and an older one must not undo a newer. */
|
||||
int64_t inputSeq, motionSeq;
|
||||
int inputSeqKnown, motionSeqKnown;
|
||||
uint64_t latencyAfter; /* no sample from before the last ownership/stale reset */
|
||||
} C4fWebPad;
|
||||
|
||||
/* Informational, browser-measured input-channel RTT. The service accepts one
|
||||
* bounded report for a probe it just echoed on this connection; it never uses
|
||||
* the browser's clock or this report to renew a controller's input lease. */
|
||||
typedef struct {
|
||||
C4fConn *conn;
|
||||
int64_t probeSeq;
|
||||
uint64_t probeAt, measuredProbeAt;
|
||||
int probeKnown, pending, known, rttMs;
|
||||
} C4fWebConn;
|
||||
|
||||
/* Creating a controller means issuing AddDevice and then finding its DeviceId in
|
||||
* the kernel log, which takes a moment. None of that may stop the service from
|
||||
* answering the other players, so it runs as a state machine driven from the main
|
||||
@@ -74,7 +100,7 @@ typedef struct {
|
||||
unsigned wanted; /* the whole claim this request asked for */
|
||||
unsigned pending; /* the slots still to create */
|
||||
unsigned created; /* the slots created so far, to undo on failure */
|
||||
C4fNetClient *client; /* who asked, or NULL once they left */
|
||||
C4fConn *client; /* who asked, or NULL once they left */
|
||||
int64_t id;
|
||||
int hasId;
|
||||
int slot; /* the slot AddDevice was issued for, or -1 */
|
||||
@@ -98,11 +124,18 @@ typedef struct {
|
||||
C4fAdd add;
|
||||
uint64_t broadcastAt, stopAt, heartbeatAt, usersAt;
|
||||
int usersFailed;
|
||||
C4fWebConn conns[C4F_NET_CLIENTS + C4F_RTC_PEERS]; /* open peers and their optional RTT */
|
||||
int connCount;
|
||||
/* Loop iterations that took long, since the last heartbeat: while the loop
|
||||
* is held up, every page's input waits with it. */
|
||||
unsigned slowLoops;
|
||||
uint64_t slowestLoop, slowLogAt;
|
||||
} C4fWeb;
|
||||
|
||||
#define C4F_MAX_ARGS 32
|
||||
typedef struct {
|
||||
char method[24];
|
||||
int64_t id, args[16];
|
||||
int64_t id, args[C4F_MAX_ARGS];
|
||||
unsigned argc;
|
||||
int hasId;
|
||||
} C4fRequest;
|
||||
@@ -143,7 +176,7 @@ static int c4fParseRequest(const char *json, size_t len, C4fRequest *r)
|
||||
if (method++ || value->type != JSMN_STRING || n < 1 || n >= (int)sizeof(r->method)) return -1;
|
||||
memcpy(r->method, json+value->start, (size_t)n); r->method[n] = 0;
|
||||
} else if (c4fTokenEquals(json, key, "params")) {
|
||||
if (params++ || value->type != JSMN_ARRAY || value->size > 16) return -1;
|
||||
if (params++ || value->type != JSMN_ARRAY || value->size > C4F_MAX_ARGS) return -1;
|
||||
r->argc = (unsigned)value->size;
|
||||
for (unsigned a = 0; a < r->argc; a++) {
|
||||
if (i >= count || c4fInteger(json, &tok[i++], &r->args[a])) return -1;
|
||||
@@ -155,13 +188,13 @@ static int c4fParseRequest(const char *json, size_t len, C4fRequest *r)
|
||||
return method == 1 && params == 1 ? 0 : -1;
|
||||
}
|
||||
|
||||
static void c4fError(C4fNetClient *c, const C4fRequest *r, int code, const char *message)
|
||||
static void c4fError(C4fConn *c, const C4fRequest *r, int code, const char *message)
|
||||
{
|
||||
char reply[384];
|
||||
/* Messages are constant ASCII strings under our control. */
|
||||
if (r && r->hasId) snprintf(reply, sizeof(reply), "{\"id\":%lld,\"error\":{\"code\":%d,\"message\":\"%s\"}}", (long long)r->id, code, message);
|
||||
else snprintf(reply, sizeof(reply), "{\"method\":\"error\",\"params\":{\"message\":\"%s\"}}", message);
|
||||
c4fNetText(c, reply);
|
||||
c4fConnText(c, reply);
|
||||
}
|
||||
|
||||
/* `in` as the inside of a JSON string. Quotes and backslashes are escaped, and
|
||||
@@ -189,13 +222,13 @@ static void c4fJsonText(char *out, size_t size, const char *in)
|
||||
out[o] = 0;
|
||||
}
|
||||
|
||||
static void c4fStatus(C4fWeb *app, C4fNetClient *c, const C4fRequest *request)
|
||||
static void c4fStatus(C4fWeb *app, C4fConn *c, const C4fRequest *request)
|
||||
{
|
||||
char json[4096];
|
||||
size_t pos;
|
||||
if (request && request->hasId) pos = (size_t)snprintf(json, sizeof(json), "{\"id\":%lld,\"result\":", (long long)request->id);
|
||||
else pos = (size_t)snprintf(json, sizeof(json), "{\"method\":\"s\",\"params\":");
|
||||
pos += (size_t)snprintf(json+pos, sizeof(json)-pos, "{\"version\":\"%s\",\"protocol\":2,\"pads\":[", C4F_VERSION);
|
||||
pos += (size_t)snprintf(json+pos, sizeof(json)-pos, "{\"version\":\"%s\",\"protocol\":3,\"pads\":[", C4F_VERSION);
|
||||
for (int i = 0; i < C4F_MAX_PADS; i++) {
|
||||
C4fWebPad *p = &app->pads[i];
|
||||
const char *state = p->error ? "error" : app->add.state && (app->add.pending & (1u << i)) ? "connecting"
|
||||
@@ -219,7 +252,7 @@ static void c4fStatus(C4fWeb *app, C4fNetClient *c, const C4fRequest *request)
|
||||
p->owner == c ? "true" : "false", state, p->assigned ? "" : "unassigned-", p->userId,
|
||||
rgb[0], rgb[1], rgb[2], (unsigned long long)p->reports, p->error);
|
||||
}
|
||||
snprintf(json+pos, sizeof(json)-pos, "]}}"); c4fNetText(c, json);
|
||||
snprintf(json+pos, sizeof(json)-pos, "]}}"); c4fConnText(c, json);
|
||||
}
|
||||
|
||||
/* How long ago `then` was, and never more than that. These are unsigned
|
||||
@@ -233,12 +266,31 @@ static uint64_t c4fSince(uint64_t now, uint64_t then)
|
||||
return now > then ? now - then : 0;
|
||||
}
|
||||
|
||||
static C4fWebConn *c4fWebConn(C4fWeb *app, const C4fConn *conn)
|
||||
{
|
||||
for (int i = 0; i < app->connCount; i++) if (app->conns[i].conn == conn) return &app->conns[i];
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void c4fNeutralize(C4fWebPad *p)
|
||||
{
|
||||
p->head = p->count = 0;
|
||||
p->motion = 0;
|
||||
p->latencyAfter = c4fTimeMs();
|
||||
/* Preserve sequence floors across a stale/error reset: delayed packets
|
||||
* must not restore a state that a newer packet already superseded. A new
|
||||
* owner starts a new sequence space in c4fApplyClaim. */
|
||||
c4fPadDataNeutral(&p->current);
|
||||
}
|
||||
|
||||
static void c4fApplyMotion(const C4fWebPad *p, ScePadData *data)
|
||||
{
|
||||
if (!p->motion) return;
|
||||
memcpy(data->quat, p->quat, sizeof(data->quat));
|
||||
memcpy(data->accel, p->accel, sizeof(data->accel));
|
||||
memcpy(data->gyro, p->gyro, sizeof(data->gyro));
|
||||
}
|
||||
|
||||
static void c4fRemove(C4fWeb *app, C4fWebPad *p)
|
||||
{
|
||||
if (p->active) {
|
||||
@@ -251,11 +303,11 @@ static void c4fRemove(C4fWeb *app, C4fWebPad *p)
|
||||
|
||||
static void c4fLogoutPad(C4fWeb *app, C4fWebPad *p, int index)
|
||||
{
|
||||
C4fNetClient *owner = p->owner;
|
||||
C4fConn *owner = p->owner;
|
||||
if (owner) {
|
||||
char message[64];
|
||||
snprintf(message, sizeof(message), "{\"method\":\"v\",\"params\":[%d,0,0]}", index);
|
||||
c4fNetText(owner, message);
|
||||
c4fConnInput(owner, message);
|
||||
}
|
||||
c4fLog("web controller %d disconnected after PS4 user logout\n", index + 1);
|
||||
c4fRemove(app, p);
|
||||
@@ -404,11 +456,13 @@ static void c4fPollFeedback(C4fWeb *app, uint64_t now)
|
||||
app->rumbleChanges++;
|
||||
p->feedback = f;
|
||||
p->feedbackKnown = 1;
|
||||
if (p->owner && (!p->rumbleSentValid || f.large != p->rumbleSent[0] || f.small != p->rumbleSent[1])) {
|
||||
int changed = !p->rumbleSentValid || f.large != p->rumbleSent[0] || f.small != p->rumbleSent[1];
|
||||
int again = p->owner && p->owner->input && (f.large || f.small) && now - p->rumbleSentAt >= C4F_RUMBLE_REPEAT_MS;
|
||||
if (p->owner && (changed || again)) {
|
||||
char message[64];
|
||||
snprintf(message, sizeof(message), "{\"method\":\"v\",\"params\":[%d,%u,%u]}", i, f.large, f.small);
|
||||
if (c4fNetText(p->owner, message) == 0) {
|
||||
p->rumbleSent[0] = f.large; p->rumbleSent[1] = f.small; p->rumbleSentValid = 1;
|
||||
if (c4fConnInput(p->owner, message) == 0) {
|
||||
p->rumbleSent[0] = f.large; p->rumbleSent[1] = f.small; p->rumbleSentValid = 1; p->rumbleSentAt = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -561,12 +615,15 @@ static void c4fReleaseIdleKlog(C4fWeb *app)
|
||||
}
|
||||
|
||||
/* Hands the claim its slots once every device it needed exists. */
|
||||
static void c4fApplyClaim(C4fWeb *app, C4fNetClient *c, unsigned wanted, C4fRequest *r)
|
||||
static void c4fApplyClaim(C4fWeb *app, C4fConn *c, unsigned wanted, C4fRequest *r)
|
||||
{
|
||||
for (int i = 0; i < C4F_MAX_PADS; i++) {
|
||||
C4fWebPad *p = &app->pads[i];
|
||||
if (wanted & (1u << i)) {
|
||||
if (p->owner != c) { c4fNeutralize(p); p->rumbleSentValid = 0; }
|
||||
if (p->owner != c) {
|
||||
c4fNeutralize(p); p->rumbleSentValid = 0;
|
||||
p->inputSeqKnown = p->motionSeqKnown = 0;
|
||||
}
|
||||
p->owner = c; p->detachedAt = 0; p->lastInput = c4fTimeMs(); p->stale = 0;
|
||||
} else if (p->owner == c) c4fRemove(app, p);
|
||||
}
|
||||
@@ -583,7 +640,7 @@ static void c4fAddReset(C4fWeb *app)
|
||||
* run, so a device we can no longer address may exist. */
|
||||
static void c4fAddFail(C4fWeb *app, int code, const char *message, int blocked)
|
||||
{
|
||||
C4fNetClient *client = app->add.client;
|
||||
C4fConn *client = app->add.client;
|
||||
C4fRequest reply;
|
||||
|
||||
if (blocked) app->creationBlocked = 1;
|
||||
@@ -678,7 +735,7 @@ static void c4fAdvanceAdd(C4fWeb *app, uint64_t now, long klogBytes)
|
||||
}
|
||||
if (app->add.pending) { c4fAddNextDevice(app, now); return; }
|
||||
{
|
||||
C4fNetClient *client = app->add.client;
|
||||
C4fConn *client = app->add.client;
|
||||
unsigned wanted = app->add.wanted;
|
||||
C4fRequest reply;
|
||||
memset(&reply, 0, sizeof(reply));
|
||||
@@ -707,7 +764,7 @@ static void c4fAdvanceAdd(C4fWeb *app, uint64_t now, long klogBytes)
|
||||
}
|
||||
}
|
||||
|
||||
static void c4fClaim(C4fWeb *app, C4fNetClient *c, C4fRequest *r)
|
||||
static void c4fClaim(C4fWeb *app, C4fConn *c, C4fRequest *r)
|
||||
{
|
||||
unsigned wanted = 0, needed = 0;
|
||||
int needsAssignment = 0;
|
||||
@@ -754,7 +811,7 @@ static void c4fClaim(C4fWeb *app, C4fNetClient *c, C4fRequest *r)
|
||||
* that works from this network; without it, the console's own address is used.
|
||||
* The answer is the module grid, one hex string per row, most significant bit
|
||||
* first: the page draws it, so it needs no QR library of its own. */
|
||||
static void c4fInvite(C4fNetClient *c, const C4fRequest *r)
|
||||
static void c4fInvite(C4fConn *c, const C4fRequest *r)
|
||||
{
|
||||
char text[64], address[64], reply[1024];
|
||||
uint8_t qr[qrcodegen_BUFFER_LEN_FOR_VERSION(5)], scratch[qrcodegen_BUFFER_LEN_FOR_VERSION(5)];
|
||||
@@ -791,42 +848,186 @@ static void c4fInvite(C4fNetClient *c, const C4fRequest *r)
|
||||
reply[pos++] = '"';
|
||||
}
|
||||
snprintf(reply + pos, sizeof(reply) - pos, "]}}");
|
||||
c4fNetText(c, reply);
|
||||
c4fConnText(c, reply);
|
||||
}
|
||||
|
||||
static void c4fUpdate(C4fWeb *app, C4fNetClient *c, C4fRequest *r)
|
||||
/* Buttons, sticks, triggers and touches from args[at..]: [buttons, lx, ly, rx,
|
||||
* ry, l2, r2, n, (id, x, y) x n]. The number of args it took, or 0 after
|
||||
* answering an error. */
|
||||
static unsigned c4fParseState(C4fConn *c, C4fRequest *r, unsigned at, ScePadData *data)
|
||||
{
|
||||
const int64_t *a = r->args + at;
|
||||
if (r->argc < at + 8) { c4fError(c, r, 400, "That input was not in a form Control4Free understands"); return 0; }
|
||||
if (a[0] < 0 || a[0] > C4F_INPUT_MASK || (a[0] & ~C4F_INPUT_MASK)) { c4fError(c, r, 400, "Those are not buttons a DualShock 4 has"); return 0; }
|
||||
for (unsigned i = 1; i < 7; i++) if (a[i] < 0 || a[i] > 255) { c4fError(c, r, 400, "A stick or trigger was outside its range"); return 0; }
|
||||
if (a[7] < 0 || a[7] > 2 || r->argc < at + 8 + 3 * (unsigned)a[7]) { c4fError(c, r, 400, "That touch was not in a form Control4Free understands"); return 0; }
|
||||
c4fPadDataNeutral(data); data->buttons = (uint32_t)a[0];
|
||||
data->lx = a[1]; data->ly = a[2]; data->rx = a[3]; data->ry = a[4]; data->l2 = a[5]; data->r2 = a[6];
|
||||
data->touchData.fingers = a[7];
|
||||
for (unsigned i = 0; i < data->touchData.fingers; i++) {
|
||||
const int64_t *t = &a[8 + 3 * i];
|
||||
if (t[0] < 0 || t[0] > 127 || t[1] < 0 || t[1] > 1919 || t[2] < 0 || t[2] > 941) { c4fError(c, r, 400, "A touch landed outside the touchpad"); return 0; }
|
||||
data->touchData.touch[i].finger = t[0]; data->touchData.touch[i].x = t[1]; data->touchData.touch[i].y = t[2];
|
||||
}
|
||||
return 8 + 3 * data->touchData.fingers;
|
||||
}
|
||||
|
||||
/* A new state for the pad: queued, and out at once unless a report has only
|
||||
* just gone (holding it for the next tick would add lag for nothing). */
|
||||
static void c4fTakeInput(C4fWeb *app, C4fWebPad *p, int index, ScePadData *data)
|
||||
{
|
||||
c4fApplyMotion(p, data);
|
||||
if (p->stale) { p->stale = 0; app->changed = 1; }
|
||||
uint64_t now = c4fTimeMs();
|
||||
if (c4fSince(now, p->lastInput) >= C4F_STALE_MS) p->latencyAfter = now;
|
||||
p->lastInput = now; c4fEnqueue(p, data);
|
||||
if (c4fSince(now, p->lastReport) >= C4F_REPORT_FAST_MS) c4fReportPad(app, p, index, now);
|
||||
}
|
||||
|
||||
/* u: [pad, buttons, lx, ly, rx, ry, l2, r2, n, (id, x, y) x n], in order on a
|
||||
* reliable connection (the host tests' WebSocket). */
|
||||
#ifdef C4F_TEST_WEBSOCKET
|
||||
static void c4fUpdate(C4fWeb *app, C4fConn *c, C4fRequest *r)
|
||||
{
|
||||
if (r->argc < 9 || r->args[0] < 0 || r->args[0] >= C4F_MAX_PADS) { c4fError(c, r, 400, "That input was not in a form Control4Free understands"); return; }
|
||||
C4fWebPad *p = &app->pads[r->args[0]];
|
||||
if (p->owner != c || !p->active) return; /* input never implicitly claims a pad */
|
||||
if (r->args[1] < 0 || r->args[1] > C4F_INPUT_MASK || (r->args[1] & ~C4F_INPUT_MASK)) { c4fError(c, r, 400, "Those are not buttons a DualShock 4 has"); return; }
|
||||
for (unsigned i = 2; i < 8; i++) if (r->args[i] < 0 || r->args[i] > 255) { c4fError(c, r, 400, "A stick or trigger was outside its range"); return; }
|
||||
if (r->args[8] < 0 || r->args[8] > 2 || r->argc != 9+3*r->args[8]) { c4fError(c, r, 400, "That touch was not in a form Control4Free understands"); return; }
|
||||
ScePadData data;
|
||||
c4fPadDataNeutral(&data); data.buttons = (uint32_t)r->args[1];
|
||||
data.lx = r->args[2]; data.ly = r->args[3]; data.rx = r->args[4]; data.ry = r->args[5]; data.l2 = r->args[6]; data.r2 = r->args[7];
|
||||
data.touchData.fingers = r->args[8];
|
||||
for (unsigned i = 0; i < data.touchData.fingers; i++) {
|
||||
int64_t *t = &r->args[9+3*i];
|
||||
if (t[0] < 0 || t[0] > 127 || t[1] < 0 || t[1] > 1919 || t[2] < 0 || t[2] > 941) { c4fError(c, r, 400, "A touch landed outside the touchpad"); return; }
|
||||
data.touchData.touch[i].finger = t[0]; data.touchData.touch[i].x = t[1]; data.touchData.touch[i].y = t[2];
|
||||
unsigned used = c4fParseState(c, r, 1, &data);
|
||||
if (!used) return;
|
||||
if (r->argc != 1 + used) { c4fError(c, r, 400, "That touch was not in a form Control4Free understands"); return; }
|
||||
c4fTakeInput(app, p, (int)r->args[0], &data);
|
||||
}
|
||||
#endif
|
||||
|
||||
/* i: [pad, seq, buttons, lx, ly, rx, ry, l2, r2, n, (id, x, y) x n, k,
|
||||
* (seq, buttons) x k], the page's input over WebRTC, where a message can be
|
||||
* lost or overtaken. Anything older than the newest applied is dropped. The k
|
||||
* pairs are the page's latest button changes: one newer than what arrived last
|
||||
* means messages were lost in between, so that button state is queued first,
|
||||
* and a quick tap still happens. */
|
||||
static void c4fInput(C4fWeb *app, C4fConn *c, C4fRequest *r)
|
||||
{
|
||||
if (r->argc < 11 || r->args[0] < 0 || r->args[0] >= C4F_MAX_PADS ||
|
||||
r->args[1] < 0 || r->args[1] > 9007199254740991LL) { c4fError(c, r, 400, "That input was not in a form Control4Free understands"); return; }
|
||||
C4fWebPad *p = &app->pads[r->args[0]];
|
||||
if (p->owner != c || !p->active) return;
|
||||
int64_t seq = r->args[1];
|
||||
ScePadData data;
|
||||
unsigned used = c4fParseState(c, r, 2, &data);
|
||||
if (!used) return;
|
||||
unsigned k = 2 + used;
|
||||
if (k >= r->argc || r->args[k] < 0 || r->args[k] > 4 || r->argc != k + 1 + 2 * (unsigned)r->args[k]) { c4fError(c, r, 400, "That input was not in a form Control4Free understands"); return; }
|
||||
unsigned edges = (unsigned)r->args[k];
|
||||
for (unsigned e = 0; e < edges; e++) {
|
||||
int64_t at = r->args[k + 1 + 2 * e];
|
||||
int64_t buttons = r->args[k + 2 + 2 * e];
|
||||
if (at < 0 || at > seq || (e && at <= r->args[k - 1 + 2 * e])) {
|
||||
c4fError(c, r, 400, "Input history is not in sequence"); return;
|
||||
}
|
||||
if (buttons < 0 || buttons > C4F_INPUT_MASK || (buttons & ~C4F_INPUT_MASK)) { c4fError(c, r, 400, "Those are not buttons a DualShock 4 has"); return; }
|
||||
}
|
||||
if (p->stale) { p->stale = 0; app->changed = 1; }
|
||||
if (p->inputSeqKnown && seq <= p->inputSeq) return; /* late: a newer one is in already */
|
||||
if (p->inputSeqKnown) {
|
||||
for (unsigned e = 0; e < edges; e++) {
|
||||
int64_t at = r->args[k + 1 + 2 * e];
|
||||
if (at <= p->inputSeq || at >= seq) continue;
|
||||
ScePadData missed = data;
|
||||
missed.buttons = (uint32_t)r->args[k + 2 + 2 * e];
|
||||
c4fApplyMotion(p, &missed);
|
||||
c4fEnqueue(p, &missed);
|
||||
}
|
||||
}
|
||||
p->inputSeq = seq; p->inputSeqKnown = 1;
|
||||
c4fTakeInput(app, p, (int)r->args[0], &data);
|
||||
}
|
||||
|
||||
/* e: [seq, time]: answered at once on the same path, for the page's link
|
||||
* quality line. */
|
||||
static void c4fEcho(C4fWeb *app, C4fConn *c, C4fRequest *r)
|
||||
{
|
||||
if (r->argc != 2) { c4fError(c, r, 400, "Invalid request"); return; }
|
||||
char reply[96];
|
||||
snprintf(reply, sizeof(reply), "{\"method\":\"e\",\"params\":[%lld,%lld]}", (long long)r->args[0], (long long)r->args[1]);
|
||||
C4fWebConn *peer = c4fWebConn(app, c);
|
||||
if (c4fConnInput(c, reply) == 0 && peer && r->args[0] >= 0 && r->args[0] <= 9007199254740991LL &&
|
||||
(!peer->probeKnown || r->args[0] > peer->probeSeq)) {
|
||||
peer->probeSeq = r->args[0]; peer->probeAt = c4fTimeMs();
|
||||
peer->probeKnown = peer->pending = 1;
|
||||
}
|
||||
}
|
||||
|
||||
static void c4fLatency(C4fWeb *app, C4fConn *c, const C4fRequest *r)
|
||||
{
|
||||
if (r->argc != 2 || r->args[0] < 0 || r->args[0] > 9007199254740991LL || r->args[1] < 0 || r->args[1] > 60000)
|
||||
return;
|
||||
C4fWebConn *peer = c4fWebConn(app, c);
|
||||
uint64_t now = c4fTimeMs();
|
||||
p->lastInput = now; c4fEnqueue(p, &data);
|
||||
/* Out at once, unless a report has only just gone. Holding a new sample back
|
||||
* for the next tick would add lag for nothing. */
|
||||
if (!peer || !peer->pending || r->args[0] != peer->probeSeq || c4fSince(now, peer->probeAt) > C4F_LATENCY_MS) return;
|
||||
peer->pending = 0; peer->known = 1;
|
||||
peer->rttMs = (int)r->args[1]; peer->measuredProbeAt = peer->probeAt;
|
||||
}
|
||||
|
||||
/* m: [pad, seq, qx, qy, qz, qw, ax, ay, az, gx, gy, gz] in whole numbers: the
|
||||
* orientation x 10000, acceleration in thousandths of a G, angular velocity in
|
||||
* thousandths of a radian per second. Sequence numbers reject older motion.
|
||||
* The buttons, sticks and touches stay as
|
||||
* the last input left them. */
|
||||
static void c4fMotion(C4fWeb *app, C4fConn *c, C4fRequest *r)
|
||||
{
|
||||
static const int64_t limits[3] = { 10000, 16000, 35000 };
|
||||
unsigned offset = 2;
|
||||
#ifdef C4F_TEST_WEBSOCKET
|
||||
if (r->argc == 11) offset = 1; /* legacy application test adapter only */
|
||||
#endif
|
||||
if (r->argc != offset + 10 || r->args[0] < 0 || r->args[0] >= C4F_MAX_PADS ||
|
||||
(offset == 2 && (r->args[1] < 0 || r->args[1] > 9007199254740991LL))) { c4fError(c, r, 400, "That motion was not in a form Control4Free understands"); return; }
|
||||
C4fWebPad *p = &app->pads[r->args[0]];
|
||||
if (p->owner != c || !p->active) return;
|
||||
for (unsigned i = 0; i < 10; i++) {
|
||||
int64_t limit = limits[i < 4 ? 0 : i < 7 ? 1 : 2];
|
||||
if (r->args[offset+i] < -limit || r->args[offset+i] > limit) { c4fError(c, r, 400, "Motion was outside its range"); return; }
|
||||
}
|
||||
if (offset == 2) {
|
||||
if (p->motionSeqKnown && r->args[1] <= p->motionSeq) return;
|
||||
p->motionSeq = r->args[1]; p->motionSeqKnown = 1;
|
||||
}
|
||||
for (int i = 0; i < 4; i++) p->quat[i] = (float)r->args[offset+i] / 10000.0f;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
p->accel[i] = (float)r->args[offset+4+i] / 1000.0f;
|
||||
p->gyro[i] = (float)r->args[offset+7+i] / 1000.0f;
|
||||
}
|
||||
p->motion = 1;
|
||||
uint64_t now = c4fTimeMs();
|
||||
/* Motion alone cannot renew an old held button's input lease. */
|
||||
/* The newest state, moved: coalesces with a queued sample of the same buttons. */
|
||||
ScePadData data = p->count ? p->queue[(p->head+p->count-1)%C4F_QUEUE_SIZE] : p->current;
|
||||
c4fApplyMotion(p, &data);
|
||||
c4fEnqueue(p, &data);
|
||||
if (c4fSince(now, p->lastReport) >= C4F_REPORT_FAST_MS) c4fReportPad(app, p, (int)r->args[0], now);
|
||||
}
|
||||
|
||||
static void c4fWebEvent(C4fNetClient *c, int event, const char *text, size_t len, void *context)
|
||||
static void c4fWebEvent(C4fConn *c, int event, const char *text, size_t len, void *context)
|
||||
{
|
||||
C4fWeb *app = context;
|
||||
if (event == C4F_NET_HTTP_STATUS) {
|
||||
int active = 0;
|
||||
char reply[192];
|
||||
char reply[1024];
|
||||
for (int i = 0; i < C4F_MAX_PADS; i++) active += app->pads[i].active != 0;
|
||||
snprintf(reply, sizeof(reply), "{\"application\":\"Control4Free\",\"api\":1,\"version\":\"%s\",\"controllers\":%d,\"stopping\":%s}", C4F_VERSION, active, app->stopAt ? "true" : "false");
|
||||
size_t pos = (size_t)snprintf(reply, sizeof(reply), "{\"application\":\"Control4Free\",\"api\":1,\"version\":\"%s\",\"controllers\":%d,\"stopping\":%s,\"latency\":true", C4F_VERSION, active, app->stopAt ? "true" : "false");
|
||||
uint64_t now = c4fTimeMs();
|
||||
for (int i = 0; i < C4F_MAX_PADS; i++) {
|
||||
const C4fWebPad *p = &app->pads[i];
|
||||
int eligible = p->active && p->assigned && p->owner && !p->stale && c4fSince(now, p->lastInput) < C4F_STALE_MS;
|
||||
C4fWebConn *peer = eligible ? c4fWebConn(app, p->owner) : NULL;
|
||||
int known = peer && peer->known && peer->measuredProbeAt > p->latencyAfter &&
|
||||
c4fSince(now, peer->measuredProbeAt) <= C4F_LATENCY_MS;
|
||||
pos += (size_t)snprintf(reply + pos, sizeof(reply) - pos,
|
||||
",\"pad%dUser\":%lld,\"pad%dRtt\":%d,\"pad%dAge\":%lld", i,
|
||||
eligible ? (long long)p->userId : -1LL, i, known ? peer->rttMs : -1, i,
|
||||
known ? (long long)c4fSince(now, peer->measuredProbeAt) : -1LL);
|
||||
}
|
||||
snprintf(reply + pos, sizeof(reply) - pos, "}");
|
||||
c4fNetHttpJson(c, reply); return;
|
||||
}
|
||||
if (event == C4F_NET_HTTP_STOP) {
|
||||
@@ -836,8 +1037,16 @@ static void c4fWebEvent(C4fNetClient *c, int event, const char *text, size_t len
|
||||
if (!app->stopAt) app->stopAt = c4fTimeMs() + 250;
|
||||
return;
|
||||
}
|
||||
if (event == C4F_NET_OPEN) { c4fStatus(app, c, NULL); return; }
|
||||
if (event == C4F_NET_OPEN) {
|
||||
if (app->connCount < (int)(sizeof(app->conns) / sizeof(app->conns[0]))) {
|
||||
C4fWebConn *peer = &app->conns[app->connCount++];
|
||||
memset(peer, 0, sizeof(*peer)); peer->conn = c;
|
||||
}
|
||||
c4fStatus(app, c, NULL);
|
||||
return;
|
||||
}
|
||||
if (event == C4F_NET_CLOSE) {
|
||||
for (int i = 0; i < app->connCount; i++) if (app->conns[i].conn == c) { app->conns[i] = app->conns[--app->connCount]; break; }
|
||||
/* Before the slot can be reused by someone else. */
|
||||
if (app->add.client == c) c4fCancelAdd(app);
|
||||
for (int i = 0; i < C4F_MAX_PADS; i++) if (app->pads[i].owner == c) {
|
||||
@@ -848,18 +1057,28 @@ static void c4fWebEvent(C4fNetClient *c, int event, const char *text, size_t len
|
||||
}
|
||||
C4fRequest r;
|
||||
if (c4fParseRequest(text, len, &r)) { c4fError(c, NULL, 400, "Invalid request"); return; }
|
||||
int input = !strcmp(r.method, "i") || !strcmp(r.method, "m") || !strcmp(r.method, "e") || !strcmp(r.method, "latency");
|
||||
if (c->input && input != (event == C4F_NET_INPUT_EVENT)) {
|
||||
c4fError(c, &r, 400, "Message sent on the wrong data channel"); return;
|
||||
}
|
||||
if (app->stopAt) { c4fError(c, &r, 503, "Control4Free is shutting down"); return; }
|
||||
if (!strcmp(r.method, "info")) {
|
||||
char reply[160];
|
||||
if (r.hasId) { snprintf(reply, sizeof(reply), "{\"id\":%lld,\"result\":{\"version\":\"%s\",\"protocol\":2,\"pads\":%d}}", (long long)r.id, C4F_VERSION, C4F_MAX_PADS); c4fNetText(c, reply); }
|
||||
char reply[192];
|
||||
if (r.hasId) { snprintf(reply, sizeof(reply), "{\"id\":%lld,\"result\":{\"version\":\"%s\",\"protocol\":3,\"pads\":%d,\"motion\":true,\"input\":true,\"latency\":true}}", (long long)r.id, C4F_VERSION, C4F_MAX_PADS); c4fConnText(c, reply); }
|
||||
} else if (!strcmp(r.method, "ping")) {
|
||||
char reply[64];
|
||||
if (r.hasId) { snprintf(reply, sizeof(reply), "{\"id\":%lld,\"result\":{}}", (long long)r.id); c4fNetText(c, reply); }
|
||||
if (r.hasId) { snprintf(reply, sizeof(reply), "{\"id\":%lld,\"result\":{}}", (long long)r.id); c4fConnText(c, reply); }
|
||||
} else if (!strcmp(r.method, "invite")) {
|
||||
if (r.hasId) c4fInvite(c, &r);
|
||||
} else if (!strcmp(r.method, "status")) c4fStatus(app, c, &r);
|
||||
else if (!strcmp(r.method, "claim")) c4fClaim(app, c, &r);
|
||||
#ifdef C4F_TEST_WEBSOCKET
|
||||
else if (!strcmp(r.method, "u")) c4fUpdate(app, c, &r);
|
||||
#endif
|
||||
else if (!strcmp(r.method, "i")) c4fInput(app, c, &r);
|
||||
else if (!strcmp(r.method, "e")) c4fEcho(app, c, &r);
|
||||
else if (!strcmp(r.method, "latency")) c4fLatency(app, c, &r);
|
||||
else if (!strcmp(r.method, "m")) c4fMotion(app, c, &r);
|
||||
else if (!strcmp(r.method, "leave")) {
|
||||
if (app->add.client == c) { c4fError(c, &r, 409, "Wait for the pending controller claim"); return; }
|
||||
if (r.argc != 1 || r.args[0] < 0 || r.args[0] >= C4F_MAX_PADS || app->pads[r.args[0]].owner != c) { c4fError(c, &r, 409, "That controller is not yours to disconnect"); return; }
|
||||
@@ -882,6 +1101,7 @@ int c4fWebRun(int klogFd)
|
||||
if (c4fNetOpen(&app->net, C4F_WEB_PORT, c4fWebEvent, app)) {
|
||||
c4fLog("web listen failed errno=%d\n", errno);
|
||||
if (klogFd >= 0) close(klogFd);
|
||||
c4fRtcDestroy(&app->net.rtc);
|
||||
free(app); return -1;
|
||||
}
|
||||
c4fLog("Control4Free %s: browser controller on port %d\n", C4F_VERSION, C4F_WEB_PORT);
|
||||
@@ -921,11 +1141,17 @@ int c4fWebRun(int klogFd)
|
||||
retryAt = now + 1000;
|
||||
} else c4fLog("listener recovered on port %d\n", C4F_WEB_PORT);
|
||||
}
|
||||
uint64_t loopStart = now, mark = c4fTimeMs();
|
||||
long klogBytes = c4fReadKlog(app);
|
||||
uint64_t klogMs = c4fTimeMs() - mark; mark += klogMs;
|
||||
c4fPollUsers(app, now);
|
||||
uint64_t usersMs = c4fTimeMs() - mark; mark += usersMs;
|
||||
c4fReportPads(app);
|
||||
uint64_t reportMs = c4fTimeMs() - mark; mark += reportMs;
|
||||
c4fPollFeedback(app, now);
|
||||
uint64_t feedbackMs = c4fTimeMs() - mark; mark += feedbackMs;
|
||||
c4fLookUpNames(app, now);
|
||||
uint64_t namesMs = c4fTimeMs() - mark;
|
||||
#ifdef C4F_PROBE_SETTING
|
||||
c4fProbeSetting(app, now);
|
||||
#endif
|
||||
@@ -936,7 +1162,7 @@ int c4fWebRun(int klogFd)
|
||||
if (app->add.state && (app->add.wanted & (1u << i))) continue;
|
||||
if (p->active && ((!p->owner && c4fSince(now, p->detachedAt) > C4F_RELEASE_MS) ||
|
||||
(p->owner && c4fSince(now, p->lastInput) > C4F_RELEASE_MS))) {
|
||||
C4fNetClient *owner = p->owner;
|
||||
C4fConn *owner = p->owner;
|
||||
c4fLog("web controller %d removed after %llu ms %s\n", i + 1,
|
||||
(unsigned long long)c4fSince(now, owner ? p->lastInput : p->detachedAt),
|
||||
owner ? "without input" : "with nobody connected");
|
||||
@@ -949,20 +1175,18 @@ int c4fWebRun(int klogFd)
|
||||
int active = 0;
|
||||
for (int i = 0; i < C4F_MAX_PADS; i++) active += app->pads[i].active;
|
||||
c4fLog("heartbeat: listener=%d controllers=%d klog=%d connecting=%d rumble-changes=%u "
|
||||
"feedback-reads=%llu avg=%lluus max=%lluus\n",
|
||||
"feedback-reads=%llu avg=%lluus max=%lluus slow-loops=%u slowest=%llums\n",
|
||||
app->net.fd >= 0, active, app->klogFd >= 0, app->add.state, app->rumbleChanges,
|
||||
(unsigned long long)app->feedbackCalls,
|
||||
(unsigned long long)(app->feedbackCalls ? app->feedbackUs / app->feedbackCalls : 0),
|
||||
(unsigned long long)app->feedbackMaxUs);
|
||||
(unsigned long long)app->feedbackMaxUs, app->slowLoops, (unsigned long long)app->slowestLoop);
|
||||
app->slowLoops = 0; app->slowestLoop = 0;
|
||||
app->rumbleChanges = 0;
|
||||
app->feedbackCalls = app->feedbackUs = app->feedbackMaxUs = 0;
|
||||
app->heartbeatAt = now + 60000;
|
||||
}
|
||||
if (app->changed || now >= app->broadcastAt) {
|
||||
for (int i = 0; i < C4F_NET_CLIENTS; i++) {
|
||||
C4fNetClient *c = &app->net.clients[i];
|
||||
if (c->fd >= 0 && c->websocket && !c->closing) c4fStatus(app, c, NULL);
|
||||
}
|
||||
for (int i = 0; i < app->connCount; i++) c4fStatus(app, app->conns[i].conn, NULL);
|
||||
app->changed = 0; app->broadcastAt = now+1000;
|
||||
}
|
||||
/* Wake often enough to keep a moving controller at its report rate, and
|
||||
@@ -975,9 +1199,22 @@ int c4fWebRun(int klogFd)
|
||||
if (c4fSince(now, p->lastInput) < C4F_ACTIVE_MS) moving = 1;
|
||||
}
|
||||
int wait = moving ? 2 : anyActive || app->add.state ? 8 : 50;
|
||||
uint64_t netStart = c4fTimeMs();
|
||||
if (app->net.fd < 0) usleep(8000);
|
||||
else {
|
||||
int result = c4fNetPoll(&app->net, wait);
|
||||
uint64_t netMs = c4fTimeMs() - netStart, total = c4fTimeMs() - loopStart;
|
||||
if (total > 150) {
|
||||
app->slowLoops++;
|
||||
if (total > app->slowestLoop) app->slowestLoop = total;
|
||||
if (netStart >= app->slowLogAt) {
|
||||
c4fLog("slow loop: %llu ms (klog %llu, users %llu, reports %llu, feedback %llu, names %llu, "
|
||||
"network %llu for a %d ms wait)\n", (unsigned long long)total, (unsigned long long)klogMs,
|
||||
(unsigned long long)usersMs, (unsigned long long)reportMs, (unsigned long long)feedbackMs,
|
||||
(unsigned long long)namesMs, (unsigned long long)netMs, wait);
|
||||
app->slowLogAt = netStart + 1000;
|
||||
}
|
||||
}
|
||||
if (result) {
|
||||
if (result == -2) c4fLog("service resumed after a gap in select; resetting connections\n");
|
||||
else c4fLog("network failed errno=%d; resetting connections\n", errno);
|
||||
@@ -995,6 +1232,7 @@ int c4fWebRun(int klogFd)
|
||||
}
|
||||
for (int i = 0; i < C4F_MAX_PADS; i++) c4fRemove(app, &app->pads[i]);
|
||||
c4fNetClose(&app->net);
|
||||
c4fRtcDestroy(&app->net.rtc);
|
||||
if (app->klogFd >= 0) close(app->klogFd);
|
||||
free(app);
|
||||
c4fLog("Control4Free web controller stopped\n");
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
// Test instrumentation is inserted into the page in memory, never shipped.
|
||||
const { chromium } = require('playwright');
|
||||
const fs = require('node:fs'), assert = require('node:assert/strict'), { spawn } = require('node:child_process');
|
||||
const hook = '\nwindow.c4fTest = { ControllerMapping, mapper, profileStore, gamepadSources, pollGamepads, setGamepadPad, mergedState, renderGamepads, renderPads, openSettings, closeSettings, openPad, profileFor, loadProfiles, persistProfiles, clearDevice, applyStatus, status, getKeys: () => keymap };\n';
|
||||
const { relayOffer, recordRtcMessages, copyService } = require('./rtc_browser_helpers.cjs');
|
||||
const hook = '\nwindow.c4fTest = { ControllerMapping, mapper, profileStore, gamepadSources, pollGamepads, setGamepadPad, mergedState, renderGamepads, renderPads, openSettings, closeSettings, openPad, profileFor, loadProfiles, persistProfiles, clearDevice, applyStatus, status, conn, sendInput, sendMotion, getKeys: () => keymap };\n';
|
||||
const html = fs.readFileSync('/src/client/index.html', 'utf8').replace('\n })();', hook + ' })();');
|
||||
fs.copyFileSync('/src/build/web-host-test', '/tmp/c4f-browser-service');
|
||||
copyService('/tmp/c4f-browser-service');
|
||||
const server = spawn('/tmp/c4f-browser-service', [], { env: { ...process.env, C4F_TEST_KLOG_FD: '0' }, stdio: ['pipe', 'pipe', 'pipe'] });
|
||||
server.on('error', error => console.error('Service harness:', error));
|
||||
server.stderr.on('data', data => console.error(data.toString()));
|
||||
@@ -16,33 +17,117 @@ const listening = page => page.waitForFunction(() => c4fTest.mapper.capture?.rea
|
||||
const heard = page => page.waitForFunction(() => !c4fTest.mapper.capture);
|
||||
const bindings = (page, output) => page.evaluate(o => c4fTest.mapper.draft.bindings.filter(b => b.output === o).map(b => ({ ...b.input, invert: b.invert })), output);
|
||||
|
||||
// Focused connection failures need deterministic peers, independently of the
|
||||
// real RTC/service integration below. No console or other network destination.
|
||||
async function checkConnectionLifecycle(browser) {
|
||||
const context = await browser.newContext();
|
||||
await context.addInitScript(() => {
|
||||
window.mockPeers = []; window.mockSends = [];
|
||||
window.mockPad = { index: 0, id: 'Synthetic standard controller', mapping: 'standard', connected: true,
|
||||
buttons: Array.from({ length: 17 }, () => ({ value: 0, pressed: false })), axes: [0, 0, 0, 0] };
|
||||
Object.defineProperty(navigator, 'getGamepads', { value: () => [mockPad] });
|
||||
window.WebSocket = class { constructor() { throw Error('The controller must use RTC only'); } };
|
||||
class Channel {
|
||||
constructor(peer, label, options) { Object.assign(this, options, { peer, label, readyState: 'connecting' }); }
|
||||
send(data) {
|
||||
if (this.readyState !== 'open') throw Error('closed');
|
||||
const message = JSON.parse(data);
|
||||
mockSends.push({ peer: this.peer.index, channel: this.id, message });
|
||||
if (!message.id) return;
|
||||
let result = {};
|
||||
if (message.method === 'info') result = { protocol: 3, input: true, motion: true };
|
||||
if (message.method === 'claim' || message.method === 'status') result = { pads: Array.from({ length: 4 }, (_, pad) => ({
|
||||
pad, mine: message.params.includes(pad), open: message.params.includes(pad), connected: message.params.includes(pad),
|
||||
clients: message.params.includes(pad) ? 1 : 0, state: message.params.includes(pad) ? 'ready' : 'free',
|
||||
color: [32, 96, 255], name: 'Controller ' + (pad + 1),
|
||||
})) };
|
||||
queueMicrotask(() => this.onmessage?.({ data: JSON.stringify({ id: message.id, result }) }));
|
||||
}
|
||||
}
|
||||
window.RTCPeerConnection = class {
|
||||
constructor(options) { this.options = options; this.channels = []; this.index = mockPeers.length; this.connectionState = 'new'; mockPeers.push(this); }
|
||||
createDataChannel(label, options) { const ch = new Channel(this, label, options); this.channels.push(ch); return ch; }
|
||||
async createOffer() { return { type: 'offer', sdp: 'local test offer' }; }
|
||||
async setLocalDescription(offer) { this.localDescription = offer; }
|
||||
async setRemoteDescription(answer) {
|
||||
if (answer.type !== 'answer') throw Error('answer required');
|
||||
this.connectionState = 'connected';
|
||||
for (const ch of this.channels) { ch.readyState = 'open'; ch.onopen?.(); }
|
||||
this.onconnectionstatechange?.();
|
||||
}
|
||||
close() {
|
||||
this.connectionState = 'closed';
|
||||
for (const ch of this.channels) { ch.readyState = 'closed'; ch.onclose?.(); }
|
||||
this.onconnectionstatechange?.();
|
||||
}
|
||||
};
|
||||
window.fetch = async (url, options) => {
|
||||
if (url !== 'http://127.0.0.1:4279/rtc' || options.method !== 'POST' || options.body !== 'local test offer') throw Error('Unexpected signaling request');
|
||||
return { ok: true, text: async () => 'local test answer' };
|
||||
};
|
||||
});
|
||||
const page = await context.newPage(), errors = [];
|
||||
page.on('pageerror', e => errors.push(e.message));
|
||||
await page.route('http://127.0.0.1:4279/', r => r.fulfill({ contentType: 'text/html', body: html }));
|
||||
try {
|
||||
await page.goto('http://127.0.0.1:4279/');
|
||||
await page.waitForFunction(() => window.c4fTest?.conn.state === 'open' && c4fTest.gamepadSources.size === 1);
|
||||
assert.deepEqual(await page.evaluate(() => mockPeers[0].channels.map(ch => ({ id: ch.id, ordered: ch.ordered, maxRetransmits: ch.maxRetransmits ?? null }))),
|
||||
[{ id: 0, ordered: true, maxRetransmits: null }, { id: 1, ordered: false, maxRetransmits: 0 }]);
|
||||
await page.evaluate(() => { c4fTest.setGamepadPad(0, 0); c4fTest.openPad(0); });
|
||||
await page.waitForFunction(() => c4fTest.status[0].mine && !c4fTest.gamepadSources.get(0).waitNeutral);
|
||||
const burst = await page.evaluate(() => {
|
||||
const before = mockSends.length;
|
||||
for (const buttons of [0x4000, 0, 0x1000, 0, 0x2000, 0]) c4fTest.sendInput([0, buttons, 128, 128, 128, 128, 0, 0, 0]);
|
||||
c4fTest.sendMotion([0, 0, 0, 0, 10000, 0, 0, 0, 0, 0, 0]);
|
||||
return mockSends.slice(before);
|
||||
});
|
||||
assert.equal(burst.length, 7, 'all changes send in the same task, without a new pacing cap');
|
||||
for (const record of burst) assert.equal(record.channel, 1);
|
||||
const packets = burst.filter(r => r.message.method === 'i').map(r => r.message.params);
|
||||
assert.deepEqual(packets.map(p => p[2]), [0x4000, 0, 0x1000, 0, 0x2000, 0]);
|
||||
for (let i = 1; i < packets.length; i++) assert.equal(packets[i][1], packets[i - 1][1] + 1);
|
||||
const tail = packets.at(-1), edges = Array.from({ length: tail[10] }, (_, i) => tail.slice(11 + 2 * i, 13 + 2 * i));
|
||||
assert.equal(edges.length, 4);
|
||||
assert.deepEqual(edges.map(e => e[1]), [0, 0x1000, 0, 0x2000]);
|
||||
assert(edges.every((e, i) => e[0] < tail[1] && (!i || e[0] > edges[i - 1][0])));
|
||||
assert.deepEqual(burst.at(-1).message.params, [0, 1, 0, 0, 0, 10000, 0, 0, 0, 0, 0, 0]);
|
||||
await page.evaluate(() => { mockPad.buttons[0].value = 1; });
|
||||
await page.waitForFunction(() => !!(c4fTest.mergedState(0).buttons & 0x4000));
|
||||
await page.evaluate(() => { window.oldPeer = c4fTest.conn.pc; oldPeer.close(); });
|
||||
await page.waitForFunction(() => mockPeers.length === 2 && c4fTest.conn.state === 'open' && c4fTest.status[0].mine);
|
||||
assert.equal(await page.evaluate(() => c4fTest.gamepadSources.get(0).pad), 0, 'reconnect retains the gamepad selection');
|
||||
assert.equal(await page.locator('#padView').isVisible(), true, 'reconnect retains the controller view');
|
||||
assert.equal(await page.evaluate(() => c4fTest.mergedState(0).buttons & 0x4000), 0, 'a held button is not replayed');
|
||||
await page.evaluate(() => oldPeer.channels[0].onmessage({ data: JSON.stringify({ method: 's', params: { pads: [{ pad: 0, mine: false }] } }) }));
|
||||
assert.equal(await page.evaluate(() => c4fTest.status[0].mine), true, 'late messages from the old peer are ignored');
|
||||
await page.evaluate(() => { mockPad.buttons[0].value = 0; });
|
||||
await page.waitForFunction(() => !c4fTest.gamepadSources.get(0).waitNeutral);
|
||||
await page.evaluate(() => { mockPad.buttons[0].value = 1; });
|
||||
await page.waitForFunction(() => !!(c4fTest.mergedState(0).buttons & 0x4000));
|
||||
assert.deepEqual(errors, []);
|
||||
console.log('PASS RTC page: channel modes, immediate changes, four preceding edges, motion sequence, automatic reclaim and held-input recovery');
|
||||
} finally { await context.close(); }
|
||||
}
|
||||
|
||||
(async () => {
|
||||
let browser;
|
||||
try {
|
||||
browser = await chromium.launch({ headless: true });
|
||||
await checkConnectionLifecycle(browser);
|
||||
const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } });
|
||||
// A fulfilled test document has no network address-space metadata. Allow
|
||||
// its local test service explicitly, as a user would allow local access.
|
||||
await context.grantPermissions(['local-network-access']);
|
||||
await context.addInitScript(recordRtcMessages);
|
||||
await context.addInitScript(() => {
|
||||
window.testClaims = [];
|
||||
window.testInputs = [];
|
||||
const socketSend = WebSocket.prototype.send;
|
||||
WebSocket.prototype.send = function(data) {
|
||||
try {
|
||||
const message = JSON.parse(data);
|
||||
if (message.method === 'claim') testClaims.push(message.params);
|
||||
if (message.method === 'u') testInputs.push(message.params);
|
||||
} catch {}
|
||||
return socketSend.call(this, data);
|
||||
};
|
||||
window.testPads = [0, 1, 2].map(index => ({ index, id: index < 2 ? 'Synthetic standard controller' : 'Synthetic wheel / pedals',
|
||||
mapping: index < 2 ? 'standard' : '', connected: true, buttons: Array.from({ length: 24 }, () => ({ value: 0, pressed: false })), axes: Array(6).fill(0) }));
|
||||
Object.defineProperty(navigator, 'getGamepads', { value: () => { if (window.failPolling) throw Error('SecurityError'); return window.testPads; } });
|
||||
});
|
||||
const page = await context.newPage(), errors = [];
|
||||
page.on('websocket', ws => ws.on('socketerror', error => console.error('WebSocket:', error)));
|
||||
page.on('pageerror', e => { errors.push(e.message); console.error('Page error:', e.stack); });
|
||||
await relayOffer(page);
|
||||
await page.route(/^http:\/\/127\.0\.0\.1:4264\/(?:\?.*)?$/, async r => r.fulfill({ response: await r.fetch(), body: html }));
|
||||
await page.goto('http://127.0.0.1:4264/');
|
||||
await page.waitForFunction(() => window.c4fTest?.gamepadSources.size === 3);
|
||||
@@ -99,7 +184,7 @@ const bindings = (page, output) => page.evaluate(o => c4fTest.mapper.draft.bindi
|
||||
assert.equal(await page.evaluate(other => c4fTest.mergedState(other).buttons & 0x10000, other), 0, 'PS leaked into the other virtual controller');
|
||||
await page.waitForFunction(target => document.querySelector('#psBtn').classList.contains('on') === (target === 1), target);
|
||||
await page.waitForFunction(target => !!(testInputs.findLast(p => p[0] === target)?.[1] & 0x10000), target);
|
||||
assert.equal(await page.evaluate(other => testInputs.findLast(p => p[0] === other)?.[1] & 0x10000, other), 0, 'PS went to the wrong WebSocket slot');
|
||||
assert.equal(await page.evaluate(other => testInputs.findLast(p => p[0] === other)?.[1] & 0x10000, other), 0, 'PS went to the wrong input slot');
|
||||
await page.evaluate(index => { testPads[index].buttons[16].value = 0; }, index);
|
||||
await page.waitForFunction(() => !(c4fTest.mergedState(0).buttons & 0x10000) && !(c4fTest.mergedState(1).buttons & 0x10000));
|
||||
await page.waitForFunction(() => !document.querySelector('#psBtn').classList.contains('on'));
|
||||
@@ -116,7 +201,7 @@ const bindings = (page, output) => page.evaluate(o => c4fTest.mapper.draft.bindi
|
||||
}
|
||||
await page.evaluate(() => { c4fTest.setGamepadPad(0, 0); c4fTest.setGamepadPad(1, 1); c4fTest.setGamepadPad(2, 0); });
|
||||
await page.click('#backBtn');
|
||||
console.log('PASS real browser: independent PS buttons on identical gamepads, reversed assignments, simultaneous holds, both release orders, display and WebSocket routing');
|
||||
console.log('PASS real browser: independent PS buttons on identical gamepads, reversed assignments, simultaneous holds, both release orders, display and RTC routing');
|
||||
|
||||
// The two mapping tabs must read only the selected physical source, even
|
||||
// when identical pads arrive in a different enumeration order. Guide can
|
||||
@@ -428,11 +513,13 @@ const bindings = (page, output) => page.evaluate(o => c4fTest.mapper.draft.bindi
|
||||
|
||||
// Actual served HTTP document works without instrumentation or grants.
|
||||
const plain = await browser.newContext(), smoke = await plain.newPage();
|
||||
await relayOffer(smoke);
|
||||
await smoke.goto('http://127.0.0.1:4264/');
|
||||
await smoke.waitForFunction(() => document.querySelector('#conn').dataset.conn === 'open');
|
||||
await plain.close();
|
||||
const saved = await browser.newContext(), filePage = await saved.newPage();
|
||||
await saved.grantPermissions(['local-network-access']);
|
||||
await relayOffer(filePage);
|
||||
await filePage.goto('file:///src/client/index.html');
|
||||
assert.equal(await filePage.locator('#addressForm').isVisible(), true, 'a saved copy asks for the address');
|
||||
await filePage.fill('#address', '127.0.0.1');
|
||||
|
||||
@@ -0,0 +1,515 @@
|
||||
// Settings → Experimental in a real browser, against the real service with the PS4
|
||||
// stubbed (build/web-rtc-host-test). WebHID devices, gamepads and motion sensors are
|
||||
// fakes that record what the page does. Test instrumentation is inserted into
|
||||
// the page in memory, never shipped. Run in the Playwright Docker image:
|
||||
// NODE_PATH=… node tests/experimental_test.cjs
|
||||
const { chromium } = require('playwright');
|
||||
const fs = require('node:fs'), assert = require('node:assert/strict'), { spawn } = require('node:child_process');
|
||||
const { relayOffer, recordRtcMessages, copyService } = require('./rtc_browser_helpers.cjs');
|
||||
|
||||
const hook = '\nwindow.c4fTest = { experiments, hidLinks, setExperiment, hidReport, triggerEffect, phoneMotion, status, ' +
|
||||
'gamepadSources, setGamepadPad, openPad, closePad, openSettings, showPage, conn, rumble };\n';
|
||||
const html = fs.readFileSync('/src/client/index.html', 'utf8').replace('\n })();', hook + ' })();');
|
||||
copyService('/tmp/c4f-experimental-service');
|
||||
|
||||
// An independent CRC-32 (zlib's), to check the Bluetooth reports' last four bytes.
|
||||
function crc32(bytes) {
|
||||
let c = ~0;
|
||||
for (const b of bytes) {
|
||||
c ^= b;
|
||||
for (let k = 0; k < 8; k++) c = c & 1 ? (c >>> 1) ^ 0xedb88320 : c >>> 1;
|
||||
}
|
||||
return ~c >>> 0;
|
||||
}
|
||||
const le32 = (bytes, at) => (bytes[at] | bytes[at + 1] << 8 | bytes[at + 2] << 16 | bytes[at + 3] << 24) >>> 0;
|
||||
|
||||
// In the page before its own script: fake gamepads, WebHID and motion, and a
|
||||
// record of the input messages and the page's sensor listeners.
|
||||
function fakes({ insecure = false } = {}) {
|
||||
Object.defineProperty(window, 'sentMessages', { get: () => controllerMessages });
|
||||
window.motionListeners = 0;
|
||||
const addListener = window.addEventListener;
|
||||
window.addEventListener = function(type, ...rest) {
|
||||
if (type === 'devicemotion') motionListeners++;
|
||||
return addListener.call(this, type, ...rest);
|
||||
};
|
||||
window.effects = [];
|
||||
const pad = (index, id, actuator) => ({ index, id, mapping: 'standard', connected: true, timestamp: 0,
|
||||
buttons: Array.from({ length: 17 }, () => ({ value: 0, pressed: false })), axes: [0, 0, 0, 0], vibrationActuator: actuator });
|
||||
const xbox = { type: 'dual-rumble', effects: ['dual-rumble', 'trigger-rumble'],
|
||||
playEffect: async (type, params) => { effects.push([type, params]); return 'complete'; }, reset: async () => 'complete' };
|
||||
window.testPads = [
|
||||
pad(0, 'Wireless Controller (STANDARD GAMEPAD Vendor: 054c Product: 09cc)'),
|
||||
pad(1, 'Xbox 360 Controller (XInput STANDARD GAMEPAD)', xbox),
|
||||
pad(2, 'DualSense Wireless Controller (STANDARD GAMEPAD Vendor: 054c Product: 0ce6)'),
|
||||
];
|
||||
Object.defineProperty(navigator, 'getGamepads', { value: () => testPads });
|
||||
if (insecure) {
|
||||
Object.defineProperty(window, 'isSecureContext', { value: false });
|
||||
delete Navigator.prototype.hid;
|
||||
delete window.DeviceMotionEvent;
|
||||
return;
|
||||
}
|
||||
class FakeDevice extends EventTarget {
|
||||
constructor(productId, outputs, features) {
|
||||
super();
|
||||
Object.assign(this, { vendorId: 0x054c, productId, productName: 'Fake', opened: false, closes: 0, sent: [], features,
|
||||
collections: [{ outputReports: outputs.map(reportId => ({ reportId })) }] });
|
||||
}
|
||||
async open() { this.opened = true; }
|
||||
async close() { this.opened = false; this.closes++; }
|
||||
async sendReport(id, data) { this.sent.push({ id, data: Array.from(data) }); }
|
||||
async receiveFeatureReport(id) {
|
||||
if (!this.features[id]) throw new DOMException('no such report', 'NotAllowedError');
|
||||
return new DataView(Uint8Array.from(this.features[id]).buffer);
|
||||
}
|
||||
input(reportId, bytes) {
|
||||
const ev = new Event('inputreport');
|
||||
Object.assign(ev, { reportId, data: new DataView(Uint8Array.from(bytes).buffer) });
|
||||
this.dispatchEvent(ev);
|
||||
}
|
||||
}
|
||||
// DS4 calibration (report 0x02 over USB, ID included): biases 0, every gyro axis
|
||||
// ±8192 for a speed of 1024 + 1024 (so 1/8 degree per second per step), and
|
||||
// the accelerometer ±8192 for ±1 G.
|
||||
const calibration = new Uint8Array(37);
|
||||
const put = (at, v) => { calibration[at] = v & 0xff; calibration[at + 1] = (v >> 8) & 0xff; };
|
||||
calibration[0] = 0x02;
|
||||
for (const at of [7, 11, 15]) put(at, 8192);
|
||||
for (const at of [9, 13, 17]) put(at, -8192);
|
||||
put(19, 1024); put(21, 1024);
|
||||
for (const at of [23, 27, 31]) put(at, 8192);
|
||||
for (const at of [25, 29, 33]) put(at, -8192);
|
||||
// DualSense firmware 0x20, update version 2.32 at byte 44.
|
||||
const firmware = new Uint8Array(64);
|
||||
firmware[0] = 0x20; firmware[44] = 0x32; firmware[45] = 0x02;
|
||||
window.fakeHid = {
|
||||
ds4: new FakeDevice(0x09cc, [0x05], { 0x02: Array.from(calibration) }),
|
||||
ds4bt: new FakeDevice(0x09cc, [0x11], {}),
|
||||
dualsense: new FakeDevice(0x0ce6, [0x02], { 0x20: Array.from(firmware) }),
|
||||
dualsensebt: new FakeDevice(0x0ce6, [0x31], { 0x20: Array.from(firmware) }),
|
||||
granted: [], next: null, calls: [],
|
||||
};
|
||||
Object.defineProperty(navigator, 'hid', { configurable: true, value: {
|
||||
requestDevice: async options => { fakeHid.calls.push(['requestDevice', options]); return fakeHid.next ? [fakeHid.next] : []; },
|
||||
getDevices: async () => { fakeHid.calls.push(['getDevices']); return fakeHid.granted; },
|
||||
addEventListener: type => { fakeHid.calls.push(['addEventListener', type]); },
|
||||
} });
|
||||
}
|
||||
|
||||
// A fresh service for each scenario, so its controllers' handles start over:
|
||||
// the first controller created is 11030d, the second 12030d.
|
||||
let server = null;
|
||||
async function startService() {
|
||||
if (server) {
|
||||
const old = server;
|
||||
old.kill();
|
||||
await new Promise(done => old.exitCode !== null || old.signalCode !== null ? done() : old.once('exit', done));
|
||||
}
|
||||
server = spawn('/tmp/c4f-experimental-service', [], { env: { ...process.env, C4F_TEST_KLOG_FD: '0' }, stdio: ['pipe', 'pipe', 'pipe'] });
|
||||
server.on('error', error => console.error('Service harness:', error));
|
||||
server.stderr.on('data', data => console.error(data.toString()));
|
||||
server.stdout.on('data', () => {});
|
||||
}
|
||||
const feedback = (handle, large, small, r = 0, g = 0, b = 0) => server.stdin.write(`C4F-TEST-FEEDBACK ${handle} ${large} ${small} ${r} ${g} ${b}\n`);
|
||||
|
||||
async function open(browser, options) {
|
||||
await startService();
|
||||
const context = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
||||
await context.grantPermissions(['local-network-access']);
|
||||
await context.addInitScript(recordRtcMessages);
|
||||
await context.addInitScript(fakes, options);
|
||||
const page = await context.newPage();
|
||||
page.on('pageerror', e => { console.error('Page error:', e.stack); process.exitCode = 1; });
|
||||
await relayOffer(page);
|
||||
await page.route(/^http:\/\/127\.0\.0\.1:4264\/(?:\?.*)?$/, async r => r.fulfill({ response: await r.fetch(), body: html }));
|
||||
for (let tries = 0; ; tries++) {
|
||||
try { await page.goto('http://127.0.0.1:4264/'); break; } catch (e) {
|
||||
if (tries > 50) throw e;
|
||||
await new Promise(done => setTimeout(done, 100)); // the service is still starting
|
||||
}
|
||||
}
|
||||
await page.waitForFunction(() => window.c4fTest?.gamepadSources.size === 3);
|
||||
await page.waitForFunction(() => document.querySelector('#conn').dataset.conn === 'open' && c4fTest.conn.motion);
|
||||
return { context, page };
|
||||
}
|
||||
|
||||
const lastReport = (page, device) => page.evaluate(d => fakeHid[d].sent.at(-1), device);
|
||||
|
||||
// Sony reports carry a digital trigger flag alongside the independent analog
|
||||
// pressure byte. A partial pull must survive both fields being present.
|
||||
function triggerInput(device, l2, r2) {
|
||||
const ds4 = device.startsWith('ds4'), bt = device.endsWith('bt');
|
||||
const bytes = new Array(bt ? 77 : 63).fill(0);
|
||||
const sticksAt = ds4 ? (bt ? 2 : 0) : (bt ? 1 : 0);
|
||||
const buttonsAt = ds4 ? sticksAt + 4 : (bt ? 8 : 7);
|
||||
const triggersAt = ds4 ? sticksAt + 7 : (bt ? 5 : 4);
|
||||
bytes.fill(128, sticksAt, sticksAt + 4);
|
||||
bytes[buttonsAt] = 0x08; // D-pad released
|
||||
bytes[buttonsAt + 1] = (l2 ? 0x04 : 0) | (r2 ? 0x08 : 0);
|
||||
bytes[triggersAt] = l2; bytes[triggersAt + 1] = r2;
|
||||
if (!ds4) { const touchAt = bt ? 33 : 32; bytes[touchAt] = bytes[touchAt + 4] = 0x80; }
|
||||
return { device, reportId: ds4 ? (bt ? 0x11 : 0x01) : (bt ? 0x31 : 0x01), bytes };
|
||||
}
|
||||
|
||||
async function inputAndRead(page, input, pad) {
|
||||
return page.evaluate(async ({ input, pad }) => {
|
||||
fakeHid[input.device].input(input.reportId, input.bytes);
|
||||
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
||||
return sentMessages.findLast(m => m.method === 'u' && m.params[0] === pad)?.params;
|
||||
}, { input, pad });
|
||||
}
|
||||
|
||||
async function checkTriggerPressure(page, device, pad) {
|
||||
for (const [l2, r2] of [[0, 0], [32, 96], [96, 160], [160, 224], [224, 32], [0, 0]]) {
|
||||
const state = await inputAndRead(page, triggerInput(device, l2, r2), pad);
|
||||
assert(state, device + ': no controller state sent');
|
||||
assert.deepEqual(state.slice(6, 8), [l2, r2], device + ': digital trigger flags must preserve partial analog pressure');
|
||||
assert.equal(state[1] & 0x300, (l2 > 20 ? 0x100 : 0) | (r2 > 20 ? 0x200 : 0), device + ': trigger button flags stay independent of analog pressure');
|
||||
}
|
||||
}
|
||||
|
||||
(async () => {
|
||||
let browser;
|
||||
try {
|
||||
browser = await chromium.launch({ headless: true });
|
||||
|
||||
// Off by default: the tab is there, nothing new runs.
|
||||
let { context, page } = await open(browser);
|
||||
await page.evaluate(() => { c4fTest.openSettings('experimental'); });
|
||||
assert.deepEqual(await page.$$eval('#expRows .check', inputs => inputs.map(i => [i.dataset.experiment, i.checked, i.disabled])),
|
||||
[['hid', false, false], ['triggers', false, false], ['motion', false, false]]);
|
||||
assert.equal(await page.locator('#expSony').isHidden(), true);
|
||||
assert.equal(await page.locator('#expMotion').isHidden(), true);
|
||||
await page.evaluate(() => { c4fTest.setGamepadPad(0, 0); c4fTest.setGamepadPad(1, 0); c4fTest.openPad(0); });
|
||||
await page.waitForFunction(() => c4fTest.status[0].mine);
|
||||
feedback('11030d', 200, 40);
|
||||
await page.waitForFunction(() => effects.length > 0);
|
||||
await page.waitForTimeout(300);
|
||||
assert.equal(await page.evaluate(() => fakeHid.calls.length), 0, 'WebHID was used while switched off');
|
||||
assert.equal(await page.evaluate(() => motionListeners), 0, 'motion sensors were listened to while switched off');
|
||||
assert.equal(await page.evaluate(() => sentMessages.some(m => m.method === 'm')), false, 'motion was sent while switched off');
|
||||
assert.equal(await page.evaluate(() => effects.every(([type]) => type === 'dual-rumble')), true);
|
||||
feedback('11030d', 0, 0);
|
||||
console.log('PASS experimental: three switches, all off; nothing of them runs while off');
|
||||
await context.close();
|
||||
|
||||
// A page the browser treats as insecure: the WebHID and motion switches explain why they can't be used.
|
||||
({ context, page } = await open(browser, { insecure: true }));
|
||||
await page.evaluate(() => c4fTest.openSettings('experimental'));
|
||||
assert.deepEqual(await page.$$eval('#expRows .check', inputs => inputs.map(i => i.disabled)), [true, false, true]);
|
||||
assert.equal(await page.locator('#expRows .blocked').count(), 2);
|
||||
assert.match(await page.locator('#expRows .blocked').first().textContent(), /insecure.*How to turn it on/);
|
||||
assert.match(await page.locator('#expRows .blocked a').first().getAttribute('href'), /troubleshooting\.md#experimental-features$/);
|
||||
console.log('PASS experimental: insecure page: WebHID and motion off with the reason and a link; trigger rumble available');
|
||||
await context.close();
|
||||
|
||||
// Lights and rumble on a DualShock 4 by USB, then by Bluetooth.
|
||||
({ context, page } = await open(browser));
|
||||
await page.evaluate(() => { c4fTest.setGamepadPad(0, 0); c4fTest.setGamepadPad(1, 0); c4fTest.setGamepadPad(2, 1); c4fTest.openSettings('experimental'); });
|
||||
await page.waitForFunction(() => c4fTest.status[0].mine && c4fTest.status[1].mine);
|
||||
await page.check('#expRows [data-experiment="hid"]');
|
||||
await page.waitForFunction(() => fakeHid.calls.some(([call]) => call === 'getDevices'));
|
||||
assert.equal(await page.locator('#expSony').isVisible(), true);
|
||||
assert.equal(await page.locator('#expPads [data-link]').count(), 2, 'both Sony gamepads listed, the Xbox one not');
|
||||
await page.evaluate(() => { fakeHid.next = fakeHid.ds4; });
|
||||
await page.click('#expPads [data-link="0"]');
|
||||
await page.waitForFunction(() => fakeHid.ds4.sent.length > 0);
|
||||
assert.deepEqual(await page.evaluate(() => fakeHid.calls.find(([c]) => c === 'requestDevice')[1]),
|
||||
{ filters: [{ vendorId: 0x054c, productId: 0x09cc }] });
|
||||
let report = await lastReport(page, 'ds4');
|
||||
assert.equal(report.id, 0x05);
|
||||
assert.equal(report.data.length, 31);
|
||||
assert.deepEqual(report.data.slice(0, 8), [0x03, 0, 0, 0, 0, 32, 96, 255], 'still, in controller 1\'s own colour');
|
||||
await checkTriggerPressure(page, 'ds4', 0);
|
||||
// A linked DS4 and ordinary Gamepad API source share this controller. The
|
||||
// strongest pressure wins independently for each trigger, without a digital
|
||||
// flag turning either partial pull into a full pull.
|
||||
await page.evaluate(() => { testPads[1].buttons[6].value = 160 / 255; testPads[1].buttons[7].value = 32 / 255; });
|
||||
const mixedTriggers = await inputAndRead(page, triggerInput('ds4', 32, 96), 0);
|
||||
assert.deepEqual(mixedTriggers.slice(6, 8), [160, 96], 'linked HID and Gamepad API sources merge partial trigger pressure');
|
||||
await page.evaluate(() => { testPads[1].buttons[6].value = testPads[1].buttons[7].value = 0; });
|
||||
assert.deepEqual((await inputAndRead(page, triggerInput('ds4', 0, 0), 0)).slice(6, 8), [0, 0], 'both trigger sources release');
|
||||
const effectsBefore = await page.evaluate(() => effects.length);
|
||||
feedback('11030d', 200, 40, 64, 0, 0);
|
||||
await page.waitForFunction(() => { const r = fakeHid.ds4.sent.at(-1).data; return r[3] === 40 && r[4] === 200 && r[5] === 255; });
|
||||
report = await lastReport(page, 'ds4');
|
||||
assert.deepEqual(report.data.slice(0, 8), [0x03, 0, 0, 40, 200, 255, 0, 0], 'small motor, large motor, the game\'s red');
|
||||
await page.waitForFunction(n => effects.length > n, effectsBefore);
|
||||
assert.equal(await page.evaluate(() => effects.every(([type]) => type === 'dual-rumble')), true, 'the Xbox pad keeps its own rumble');
|
||||
// Unchanged state is not sent again every frame.
|
||||
const count = await page.evaluate(() => fakeHid.ds4.sent.length);
|
||||
await page.waitForTimeout(200);
|
||||
assert.ok(await page.evaluate(() => fakeHid.ds4.sent.length) - count <= 1, 'unchanged reports were repeated');
|
||||
// Lose the final stop on the unreliable channel: the local lease must
|
||||
// still stop WebHID motors, as well as bounded Gamepad API effects.
|
||||
await page.evaluate(() => {
|
||||
const input = c4fTest.conn.input, received = input.onmessage;
|
||||
input.onmessage = event => {
|
||||
const m = JSON.parse(event.data);
|
||||
if (m.method === 'v' && m.params[0] === 0 && !m.params[1] && !m.params[2]) return;
|
||||
received(event);
|
||||
};
|
||||
window.restoreRumbleMessages = () => { input.onmessage = received; };
|
||||
});
|
||||
feedback('11030d', 0, 0, 64, 0, 0);
|
||||
await page.waitForFunction(() => fakeHid.ds4.sent.at(-1).data[4] === 0);
|
||||
await page.evaluate(() => restoreRumbleMessages());
|
||||
// Unlinking leaves it still and dim, and closes it.
|
||||
await page.click('#expPads [data-link="0"]');
|
||||
await page.waitForFunction(() => fakeHid.ds4.closes === 1);
|
||||
assert.deepEqual((await lastReport(page, 'ds4')).data.slice(0, 8), [0x03, 0, 0, 0, 0, 0, 0, 64]);
|
||||
assert.equal(await page.evaluate(() => c4fTest.hidLinks.size), 0);
|
||||
// Bluetooth: report 0x11, the CRC over 0xa2, the ID and the rest.
|
||||
await page.evaluate(() => { fakeHid.next = fakeHid.ds4bt; });
|
||||
await page.click('#expPads [data-link="0"]');
|
||||
await page.waitForFunction(() => fakeHid.ds4bt.sent.length > 0);
|
||||
report = await lastReport(page, 'ds4bt');
|
||||
assert.equal(report.id, 0x11);
|
||||
assert.equal(report.data.length, 77);
|
||||
assert.deepEqual(report.data.slice(0, 10), [0xc4, 0, 0x03, 0, 0, 0, 0, 255, 0, 0]);
|
||||
assert.equal(le32(report.data, 73), crc32([0xa2, 0x11, ...report.data.slice(0, 73)]), 'Bluetooth CRC');
|
||||
await checkTriggerPressure(page, 'ds4bt', 0);
|
||||
console.log('PASS experimental: DualShock 4 by USB and Bluetooth: colour, both motors, unchanged state not resent, CRC, unlink');
|
||||
|
||||
// DualSense by USB with trigger rumble: start-up light faded once, then the
|
||||
// newer rumble (firmware 2.32), the colour, and trigger effects only on change.
|
||||
await page.check('#expRows [data-experiment="triggers"]');
|
||||
await page.evaluate(() => { fakeHid.next = fakeHid.dualsense; });
|
||||
await page.click('#expPads [data-link="2"]');
|
||||
await page.waitForFunction(() => fakeHid.dualsense.sent.length >= 2);
|
||||
let sent = await page.evaluate(() => fakeHid.dualsense.sent);
|
||||
assert.equal(sent[0].id, 0x02);
|
||||
assert.equal(sent[0].data.length, 62);
|
||||
assert.deepEqual([sent[0].data[0], sent[0].data[1], sent[0].data[38], sent[0].data[41]], [0, 0, 0x02, 0x02], 'light bar setup alone first');
|
||||
report = sent[1];
|
||||
assert.equal(report.data[0] & 0x03, 0x02, 'firmware 2.32: no old-style rumble flag');
|
||||
assert.equal(report.data[38] & 0x04, 0x04, 'newer rumble flag');
|
||||
assert.equal(report.data[1], 0x04);
|
||||
assert.deepEqual(report.data.slice(44, 47), [255, 48, 64], 'controller 2\'s own colour');
|
||||
await checkTriggerPressure(page, 'dualsense', 1);
|
||||
// Its touchpad's fingers reach the PS4, its 1080-high pad scaled to the PS4's 942.
|
||||
await page.evaluate(() => {
|
||||
const bytes = new Array(63).fill(0);
|
||||
bytes.fill(128, 0, 4); bytes[7] = 0x08; // sticks centred, D-pad released
|
||||
bytes[32] = 7; bytes[33] = 960 & 0xff; bytes[34] = (960 >> 8) | (1079 & 0x0f) << 4; bytes[35] = 1079 >> 4;
|
||||
bytes[36] = 0x80;
|
||||
fakeHid.dualsense.input(0x01, bytes);
|
||||
});
|
||||
await page.waitForFunction(() => sentMessages.findLast(m => m.method === 'u' && m.params[0] === 1)?.params[8] === 1);
|
||||
assert.deepEqual((await page.evaluate(() => sentMessages.findLast(m => m.method === 'u' && m.params[0] === 1).params)).slice(8),
|
||||
[1, 7, 960, 941]);
|
||||
await page.evaluate(() => {
|
||||
const bytes = new Array(63).fill(0);
|
||||
bytes.fill(128, 0, 4); bytes[7] = 0x08; bytes[32] = 0x87; bytes[36] = 0x80;
|
||||
fakeHid.dualsense.input(0x01, bytes);
|
||||
});
|
||||
await page.waitForFunction(() => sentMessages.findLast(m => m.method === 'u' && m.params[0] === 1)?.params[8] === 0);
|
||||
// From here it reports its triggers all the time, as a real one does.
|
||||
await page.evaluate(() => {
|
||||
window.dsTriggers = [0, 0];
|
||||
window.dsTimer = setInterval(() => {
|
||||
const bytes = new Array(63).fill(0);
|
||||
bytes.fill(128, 0, 4); bytes[4] = dsTriggers[0]; bytes[5] = dsTriggers[1]; bytes[7] = 0x08;
|
||||
bytes[8] = (dsTriggers[0] ? 0x04 : 0) | (dsTriggers[1] ? 0x08 : 0); bytes[32] = 0x80; bytes[36] = 0x80;
|
||||
fakeHid.dualsense.input(0x01, bytes);
|
||||
}, 10);
|
||||
});
|
||||
feedback('12030d', 255, 128);
|
||||
await page.waitForFunction(() => fakeHid.dualsense.sent.at(-1).data[3] === 255);
|
||||
report = await lastReport(page, 'dualsense');
|
||||
assert.deepEqual([report.data[2], report.data[3]], [128, 255], 'small motor, large motor');
|
||||
assert.equal(report.data[0] & 0x0c, 0, 'no trigger effect while neither trigger is pulled');
|
||||
// The trigger being pulled vibrates, as strongly as the stronger motor (8),
|
||||
// slowly for the large one (30 Hz); the other is off.
|
||||
const lastEffects = () => page.evaluate(() => fakeHid.dualsense.sent.findLast(r => r.data[0] & 0x0c).data);
|
||||
const VIBRATE = [0x26, 0xff, 0x03, 0xff, 0xff, 0xff, 0x3f, 0, 0, 30, 0], OFF = [0x05, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
||||
await page.evaluate(() => { dsTriggers = [0, 200]; });
|
||||
await page.waitForFunction(() => fakeHid.dualsense.sent.findLast(r => r.data[0] & 0x0c)?.data[10] === 0x26);
|
||||
report = { data: await lastEffects() };
|
||||
assert.deepEqual([report.data.slice(10, 21), report.data.slice(21, 32)], [VIBRATE, OFF], 'R2 pulled: right trigger');
|
||||
// Aiming with L2 alone moves it there ...
|
||||
await page.evaluate(() => { dsTriggers = [200, 0]; });
|
||||
await page.waitForFunction(() => fakeHid.dualsense.sent.findLast(r => r.data[0] & 0x0c)?.data[21] === 0x26);
|
||||
report = { data: await lastEffects() };
|
||||
assert.deepEqual([report.data.slice(10, 21), report.data.slice(21, 32)], [OFF, VIBRATE], 'L2 only: left trigger');
|
||||
// ... and firing with R2 while aiming moves it to R2, the last one pulled, where it stays.
|
||||
await page.evaluate(() => { dsTriggers = [200, 200]; });
|
||||
await page.waitForFunction(() => fakeHid.dualsense.sent.findLast(r => r.data[0] & 0x0c)?.data[10] === 0x26);
|
||||
report = { data: await lastEffects() };
|
||||
assert.deepEqual([report.data.slice(10, 21), report.data.slice(21, 32)], [VIBRATE, OFF], 'R2 pulled while aiming: right trigger');
|
||||
const effectReports = await page.evaluate(() => fakeHid.dualsense.sent.filter(r => r.data[0] & 0x0c).length);
|
||||
await page.waitForTimeout(150);
|
||||
assert.equal(await page.evaluate(() => fakeHid.dualsense.sent.filter(r => r.data[0] & 0x0c).length), effectReports, 'unchanged effects re-sent');
|
||||
// A stronger small motor makes it quick (60 Hz).
|
||||
feedback('12030d', 64, 255);
|
||||
await page.waitForFunction(() => fakeHid.dualsense.sent.findLast(r => r.data[0] & 0x0c)?.data[19] === 60);
|
||||
// The light bar alone changing does not repeat the trigger effects.
|
||||
feedback('12030d', 64, 255, 0, 64, 0);
|
||||
await page.waitForFunction(() => fakeHid.dualsense.sent.at(-1).data[45] === 255);
|
||||
assert.equal((await lastReport(page, 'dualsense')).data[0] & 0x0c, 0, 'trigger effects re-sent without a change');
|
||||
feedback('12030d', 0, 0, 0, 64, 0);
|
||||
await page.waitForFunction(() => fakeHid.dualsense.sent.at(-1).data[3] === 0);
|
||||
report = await lastReport(page, 'dualsense');
|
||||
assert.deepEqual([report.data[0] & 0x0c, report.data[10], report.data[21]], [0x0c, 0x05, 0x05], 'trigger effects off when the rumble stops');
|
||||
await page.evaluate(() => { clearInterval(dsTimer); });
|
||||
// The Xbox pad rumbles in the trigger being pulled too, while the switch is on.
|
||||
const xboxTriggers = () => page.evaluate(() => effects.findLast(([type]) => type === 'trigger-rumble')?.[1]);
|
||||
feedback('11030d', 200, 100);
|
||||
await page.waitForFunction(() => effects.some(([type]) => type === 'trigger-rumble'));
|
||||
let params = await xboxTriggers();
|
||||
assert.deepEqual([params.leftTrigger, params.rightTrigger], [0, 0], 'neither trigger pulled: ' + JSON.stringify(params));
|
||||
await page.evaluate(() => { testPads[1].buttons[7].value = .9; });
|
||||
await page.waitForFunction(() => effects.findLast(([type]) => type === 'trigger-rumble')[1].rightTrigger > 0);
|
||||
params = await xboxTriggers();
|
||||
assert.ok(params.leftTrigger === 0 && Math.abs(params.rightTrigger - 200 / 255 * .6) < 1e-6, JSON.stringify(params));
|
||||
await page.evaluate(() => { testPads[1].buttons[7].value = 0; });
|
||||
feedback('11030d', 0, 0);
|
||||
console.log('PASS experimental: DualSense setup, newer rumble, colour, effects in the pulled trigger on change; Xbox trigger rumble');
|
||||
|
||||
// DualSense over Bluetooth: report 0x31 with its sequence, tag and CRC. Once
|
||||
// linked the browser's gamepad goes quiet (here: frozen at rest), so the
|
||||
// controller's own reports carry its buttons, sticks and triggers.
|
||||
await page.click('#expPads [data-link="2"]');
|
||||
await page.waitForFunction(() => !c4fTest.hidLinks.has(2) && fakeHid.dualsense.closes === 1);
|
||||
await page.evaluate(() => { fakeHid.next = fakeHid.dualsensebt; });
|
||||
await page.click('#expPads [data-link="2"]');
|
||||
await page.waitForFunction(() => fakeHid.dualsensebt.sent.length >= 2);
|
||||
sent = await page.evaluate(() => fakeHid.dualsensebt.sent);
|
||||
for (const [i, r] of sent.slice(0, 2).entries()) {
|
||||
assert.equal(r.id, 0x31);
|
||||
assert.equal(r.data.length, 77);
|
||||
assert.deepEqual([r.data[0], r.data[1]], [i << 4, 0x10], 'sequence in the high nibble, then the tag');
|
||||
assert.equal(le32(r.data, 73), crc32([0xa2, 0x31, ...r.data.slice(0, 73)]), 'Bluetooth CRC');
|
||||
}
|
||||
assert.deepEqual([sent[0].data[2 + 38], sent[0].data[2 + 41]], [0x02, 0x02], 'light bar setup first');
|
||||
assert.deepEqual(sent[1].data.slice(2 + 44, 2 + 47), [0, 255, 0], 'the green the game set earlier');
|
||||
await checkTriggerPressure(page, 'dualsensebt', 1);
|
||||
const btInput = (b0, b1, b2, x, r2) => {
|
||||
const bytes = new Array(77).fill(0);
|
||||
bytes.fill(128, 1, 5);
|
||||
bytes[1] = x; bytes[6] = r2; bytes[8] = b0; bytes[9] = b1 | (r2 ? 0x08 : 0); bytes[10] = b2;
|
||||
return bytes;
|
||||
};
|
||||
const pad1 = () => page.evaluate(() => sentMessages.findLast(m => m.method === 'u' && m.params[0] === 1)?.params);
|
||||
// Cross, D-pad up (hat 0), L1, PS; left stick fully left; R2 at 200.
|
||||
await page.evaluate(bytes => fakeHid.dualsensebt.input(0x31, bytes), btInput(0x20, 0x01, 0x01, 0, 200));
|
||||
await page.waitForFunction(() => (sentMessages.findLast(m => m.method === 'u' && m.params[0] === 1)?.params[1] & 0x4000) !== 0);
|
||||
let u = await pad1();
|
||||
assert.equal(u[1], 0x4000 | 0x0010 | 0x0400 | 0x10000 | 0x0200, 'Cross, up, L1, PS and R2 pulled: ' + u[1].toString(16));
|
||||
assert.deepEqual([u[2], u[3], u[4], u[5], u[6], u[7]], [0, 128, 128, 128, 0, 200]);
|
||||
await page.evaluate(bytes => fakeHid.dualsensebt.input(0x31, bytes), btInput(0x08, 0, 0, 128, 0));
|
||||
await page.waitForFunction(() => sentMessages.findLast(m => m.method === 'u' && m.params[0] === 1)?.params[1] === 0);
|
||||
// Unlinked, the browser's gamepad drives it again.
|
||||
await page.click('#expPads [data-link="2"]');
|
||||
await page.waitForFunction(() => !c4fTest.hidLinks.has(2));
|
||||
await page.evaluate(() => { testPads[2].buttons[0].value = 1; testPads[2].buttons[0].pressed = true; });
|
||||
await page.waitForFunction(() => (sentMessages.findLast(m => m.method === 'u' && m.params[0] === 1)?.params[1] & 0x4000) !== 0);
|
||||
await page.evaluate(() => { testPads[2].buttons[0].value = 0; testPads[2].buttons[0].pressed = false; });
|
||||
console.log('PASS experimental: DualSense by Bluetooth: reports and CRC; buttons, sticks and triggers from its own reports');
|
||||
|
||||
// Motion: a phone held sideways, then a linked DualShock 4's own sensors.
|
||||
await page.check('#expRows [data-experiment="motion"]');
|
||||
await page.waitForFunction(() => motionListeners === 1, null, { timeout: 3000 }).catch(async () => {
|
||||
throw new Error('no devicemotion listener: ' + await page.evaluate(() => JSON.stringify({
|
||||
secure: isSecureContext, motion: 'DeviceMotionEvent' in window, on: c4fTest.experiments.motion,
|
||||
permission: typeof (window.DeviceMotionEvent && DeviceMotionEvent.requestPermission) })));
|
||||
});
|
||||
await page.evaluate(() => { c4fTest.openPad(0); });
|
||||
const motions = () => page.evaluate(() => sentMessages.filter(m => m.method === 'm').map(m => m.params));
|
||||
const shake = (beta, at) => page.evaluate(([beta, at]) => {
|
||||
const ev = new DeviceMotionEvent('devicemotion', { accelerationIncludingGravity: { x: 0, y: 9.80665, z: 0 },
|
||||
rotationRate: { alpha: 0, beta, gamma: 0 }, interval: 16 });
|
||||
Object.defineProperty(ev, 'timeStamp', { value: at });
|
||||
window.dispatchEvent(ev);
|
||||
}, [beta, at]);
|
||||
for (let i = 0; i < 6; i++) { await shake(0, 1000 + i * 20); await page.waitForTimeout(25); }
|
||||
await page.waitForFunction(() => sentMessages.some(m => m.method === 'm' && m.params[0] === 0));
|
||||
let m = (await motions()).filter(p => p[0] === 0).at(-1);
|
||||
assert.deepEqual(m.slice(1, 5), [0, 0, 0, 10000], 'held still and sideways: level');
|
||||
assert.deepEqual(m.slice(5, 8), [0, 1000, 0], 'gravity along y, like a DualShock 4 lying flat');
|
||||
await shake(90, 1200);
|
||||
await page.waitForFunction(() => sentMessages.findLast(m => m.method === 'm')?.params[8] === 1571);
|
||||
m = (await motions()).at(-1);
|
||||
assert.ok(m[1] > 0 && m[4] < 10000, 'turning about x tilts the orientation: ' + m);
|
||||
// Leaving the controller view tells the PS4 the controller is still again.
|
||||
await page.evaluate(() => c4fTest.closePad());
|
||||
await page.waitForFunction(() => JSON.stringify(sentMessages.findLast(m => m.method === 'm')?.params) === '[0,0,0,0,10000,0,0,0,0,0,0]');
|
||||
// A linked DualShock 4's own gyro and accelerometer, calibrated. The
|
||||
// Bluetooth one is unlinked first; the USB one has calibration to read.
|
||||
await page.evaluate(() => c4fTest.openSettings('experimental'));
|
||||
assert.equal(await page.textContent('#expPads [data-link="0"]'), 'Unlink');
|
||||
await page.click('#expPads [data-link="0"]');
|
||||
await page.waitForFunction(() => !c4fTest.hidLinks.has(0) && fakeHid.ds4bt.closes === 1);
|
||||
await page.evaluate(() => { fakeHid.next = fakeHid.ds4; });
|
||||
await page.click('#expPads [data-link="0"]');
|
||||
await page.waitForFunction(() => c4fTest.hidLinks.get(0)?.calib);
|
||||
const report1 = (gyro, accel) => {
|
||||
const bytes = new Array(63).fill(0);
|
||||
bytes.fill(128, 0, 4); bytes[4] = 0x08; // sticks centred, D-pad released
|
||||
[...gyro, ...accel].forEach((v, i) => { bytes[12 + 2 * i] = v & 0xff; bytes[13 + 2 * i] = (v >> 8) & 0xff; });
|
||||
return bytes;
|
||||
};
|
||||
await page.evaluate(bytes => { for (let i = 0; i < 3; i++) fakeHid.ds4.input(0x01, bytes); }, report1([8 * 90, 0, 0], [0, 8192, 0]));
|
||||
await page.waitForFunction(() => sentMessages.findLast(m => m.method === 'm' && m.params[0] === 0)?.params[8] === 1571);
|
||||
m = (await motions()).filter(p => p[0] === 0).at(-1);
|
||||
assert.deepEqual(m.slice(5, 11), [0, 1000, 0, 1571, 0, 0], '90 degrees per second about x, 1 G on y');
|
||||
// Its touchpad's fingers, which browsers' gamepads leave out: the newest of a
|
||||
// report's touch reports, after its timestamp byte.
|
||||
const touchReport = (contact, x, y) => {
|
||||
const bytes = new Array(63).fill(0);
|
||||
bytes.fill(128, 0, 4); bytes[4] = 0x08;
|
||||
bytes[32] = 1;
|
||||
bytes[34] = contact; bytes[35] = x & 0xff; bytes[36] = (x >> 8) | (y & 0x0f) << 4; bytes[37] = y >> 4;
|
||||
bytes[38] = 0x80;
|
||||
return bytes;
|
||||
};
|
||||
const lastInput = () => page.evaluate(() => sentMessages.findLast(m => m.method === 'u' && m.params[0] === 0)?.params);
|
||||
await page.evaluate(bytes => fakeHid.ds4.input(0x01, bytes), touchReport(5, 1000, 500));
|
||||
await page.waitForFunction(() => sentMessages.findLast(m => m.method === 'u' && m.params[0] === 0)?.params[8] === 1);
|
||||
assert.deepEqual((await lastInput()).slice(8), [1, 5, 1000, 500]);
|
||||
await page.evaluate(bytes => fakeHid.ds4.input(0x01, bytes), touchReport(0x85, 1000, 500));
|
||||
await page.waitForFunction(() => sentMessages.findLast(m => m.method === 'u' && m.params[0] === 0)?.params[8] === 0);
|
||||
// Switching motion off: still again, and no more listening.
|
||||
await page.uncheck('#expRows [data-experiment="motion"]');
|
||||
await page.waitForFunction(() => JSON.stringify(sentMessages.findLast(m => m.method === 'm')?.params) === '[0,0,0,0,10000,0,0,0,0,0,0]');
|
||||
console.log('PASS experimental: phone motion (level, gravity on y, gyro), still on leaving; DualShock 4 sensors calibrated and touches');
|
||||
|
||||
// A linked controller stays linked through a lost connection, but holding
|
||||
// Cross across the reconnect must not start a new press on the PS4.
|
||||
const heldReport = report1([0, 0, 0], [0, 8192, 0]);
|
||||
heldReport[4] = 0x28; heldReport[5] = 0x0c; heldReport[7] = 96; heldReport[8] = 160;
|
||||
await page.evaluate(bytes => fakeHid.ds4.input(0x01, bytes), heldReport);
|
||||
await page.waitForFunction(() => !!(sentMessages.findLast(m => m.method === 'u' && m.params[0] === 0)?.params[1] & 0x4000));
|
||||
assert.deepEqual((await lastInput()).slice(6, 8), [96, 160], 'partial trigger pulls before reconnect');
|
||||
await page.evaluate(() => { window.previousPeer = c4fTest.conn.pc; previousPeer.close(); });
|
||||
await page.waitForFunction(() => c4fTest.conn.pc !== previousPeer && c4fTest.conn.state === 'open' && c4fTest.status[0].mine);
|
||||
await page.waitForFunction(() => sentMessages.findLast(m => m.method === 'u' && m.params[0] === 0)?.params[1] === 0);
|
||||
assert.equal(await page.evaluate(() => c4fTest.hidLinks.get(0).device === fakeHid.ds4 && fakeHid.ds4.opened), true, 'the HID link survives reconnect');
|
||||
await page.evaluate(bytes => fakeHid.ds4.input(0x01, bytes), heldReport);
|
||||
await page.waitForFunction(() => c4fTest.gamepadSources.get(0).waitNeutral);
|
||||
assert.equal((await lastInput())[1], 0, 'the held HID button stays gated');
|
||||
assert.deepEqual((await lastInput()).slice(6, 8), [0, 0], 'held analog triggers stay gated after reconnect');
|
||||
await page.evaluate(bytes => fakeHid.ds4.input(0x01, bytes), report1([0, 0, 0], [0, 8192, 0]));
|
||||
await page.waitForFunction(() => !c4fTest.gamepadSources.get(0).waitNeutral);
|
||||
await page.evaluate(bytes => fakeHid.ds4.input(0x01, bytes), heldReport);
|
||||
await page.waitForFunction(() => !!(sentMessages.findLast(m => m.method === 'u' && m.params[0] === 0)?.params[1] & 0x4000));
|
||||
assert.deepEqual((await lastInput()).slice(6, 8), [96, 160], 'partial trigger pressure returns after release and re-press');
|
||||
|
||||
// Leaving the visible page explicitly releases both input and motion.
|
||||
await page.check('#expRows [data-experiment="motion"]');
|
||||
await page.evaluate(bytes => fakeHid.ds4.input(0x01, bytes), report1([8 * 90, 0, 0], [0, 8192, 0]));
|
||||
await page.waitForFunction(() => sentMessages.findLast(m => m.method === 'm' && m.params[0] === 0)?.params[8] === 1571);
|
||||
await page.evaluate(() => {
|
||||
Object.defineProperty(document, 'hidden', { configurable: true, value: true });
|
||||
document.dispatchEvent(new Event('visibilitychange'));
|
||||
});
|
||||
assert.equal((await lastInput())[1], 0, 'hiding the page releases buttons');
|
||||
assert.deepEqual((await motions()).filter(p => p[0] === 0).at(-1), [0, 0, 0, 0, 10000, 0, 0, 0, 0, 0, 0], 'hiding the page stops motion');
|
||||
console.log('PASS experimental: linked HID reconnect keeps the link, gates held input until release, and hides safely with neutral motion');
|
||||
await context.close();
|
||||
} finally {
|
||||
if (browser) await browser.close();
|
||||
if (server) server.kill();
|
||||
}
|
||||
})().catch(error => { console.error(error); process.exitCode = 1; });
|
||||
@@ -125,12 +125,12 @@
|
||||
profiles = data; index = null;
|
||||
$('profile').textContent = '';
|
||||
for (const profile of profiles) $('profile').add(new Option(profile.label + ' — ' + profile.name, profile.name));
|
||||
const relay = location.protocol === 'http:' && location.hostname === '127.0.0.1' ? 'ws://' + location.host + '/local-service' : null;
|
||||
const relay = location.protocol === 'http:' && location.hostname === '127.0.0.1' ? 'http://' + location.host + '/local-service' : null;
|
||||
const bootstrap = script + '\nwindow.fakePads.loadProfiles(' + JSON.stringify(profiles).replace(/</g, '\\u003c') + ');\n' +
|
||||
'try { localStorage.setItem("c4f.address", JSON.stringify("127.0.0.1:4264")); } catch(e) {}\n' +
|
||||
'window.WebSocket = class LocalTestSocket extends WebSocket { constructor(address, protocols) {\n' +
|
||||
'const url = new URL(address); if (url.protocol !== "ws:" || url.hostname !== "127.0.0.1" || url.port !== "4264") throw new Error("The gamepad lab only connects to the local test service");\n' +
|
||||
'super(' + JSON.stringify(relay) + ' || address, protocols); } };';
|
||||
'const localFetch = window.fetch.bind(window); window.fetch = async (address, options) => {\n' +
|
||||
'const url = new URL(address); if (url.protocol !== "http:" || url.hostname !== "127.0.0.1" || url.port !== "4264" || url.pathname !== "/rtc") throw new Error("The gamepad lab only connects to the local test service");\n' +
|
||||
'return localFetch(' + JSON.stringify(relay) + ' || address, options); };';
|
||||
frame.srcdoc = html.replace(/<head>/i, '<head><script>' + bootstrap.replace(/<\/script/gi, '<\\/script') + '<\/script>');
|
||||
frame.onload = () => { $('profile').disabled = false; render(); message('Choose a profile, then Connect.'); };
|
||||
}
|
||||
|
||||
@@ -1,11 +1,36 @@
|
||||
// Local-only manual lab: static files plus a fixed loopback service relay.
|
||||
// Run in Docker after tests/run.py has built build/web-host-test.
|
||||
const http = require('node:http'), net = require('node:net'), fs = require('node:fs'), path = require('node:path');
|
||||
// Local-only manual lab: static files plus a fixed host-stub SDP relay.
|
||||
// Run in Docker after tests/run.py has built build/web-rtc-host-test.
|
||||
const http = require('node:http'), fs = require('node:fs'), path = require('node:path');
|
||||
const { spawn } = require('node:child_process');
|
||||
const { address, copyService } = require('./rtc_browser_helpers.cjs');
|
||||
const root = path.resolve(__dirname, '..');
|
||||
function createLabServer() {
|
||||
const server = http.createServer((request, response) => {
|
||||
const url = new URL(request.url, 'http://127.0.0.1:8000');
|
||||
if (request.method === 'POST' && url.pathname === '/local-service') {
|
||||
const expected = 'http://127.0.0.1:' + server.address().port;
|
||||
if (request.headers.origin !== expected || !address) { response.writeHead(403).end(); return; }
|
||||
const chunks = []; let size = 0;
|
||||
request.on('data', chunk => {
|
||||
size += chunk.length;
|
||||
if (size > 16384) { response.writeHead(413).end(); request.destroy(); }
|
||||
else chunks.push(chunk);
|
||||
});
|
||||
request.on('end', () => {
|
||||
if (size > 16384) return;
|
||||
const body = Buffer.concat(chunks), host = address + ':4264';
|
||||
// A fixed destination, never a caller-provided URL. Using the real
|
||||
// interface makes the SDP answer usable by Chromium's RTC stack.
|
||||
const upstream = http.request({ hostname: address, port: 4264, path: '/rtc', method: 'POST',
|
||||
headers: { Host: host, Origin: 'http://' + host, 'Content-Type': 'text/plain', 'Content-Length': body.length } }, reply => {
|
||||
response.writeHead(reply.statusCode, { 'Content-Type': 'text/plain', 'Cache-Control': 'no-store' });
|
||||
reply.pipe(response);
|
||||
});
|
||||
upstream.on('error', () => { if (!response.headersSent) response.writeHead(502); response.end(); });
|
||||
upstream.end(body);
|
||||
});
|
||||
return;
|
||||
}
|
||||
let pathname = url.pathname === '/' ? '/tests/fake_gamepads.html' : url.pathname;
|
||||
if (['/manifest.webmanifest', '/icon-192.png'].includes(pathname)) pathname = '/client' + pathname;
|
||||
const allowed = pathname === '/client/index.html' || ['/client/manifest.webmanifest', '/client/icon-192.png',
|
||||
@@ -20,34 +45,15 @@ function createLabServer() {
|
||||
});
|
||||
const sockets = new Set();
|
||||
server.on('connection', socket => { sockets.add(socket); socket.on('close', () => sockets.delete(socket)); });
|
||||
server.on('upgrade', (request, socket, head) => {
|
||||
const expected = 'http://127.0.0.1:' + server.address().port;
|
||||
if (request.url !== '/local-service' || request.headers.origin !== expected || request.headers.upgrade?.toLowerCase() !== 'websocket') {
|
||||
socket.end('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n'); return;
|
||||
}
|
||||
// No caller-specified destination: the host stub is the only upstream.
|
||||
const upstream = net.connect(4264, '127.0.0.1'); sockets.add(upstream);
|
||||
upstream.on('close', () => { sockets.delete(upstream); socket.destroy(); });
|
||||
upstream.on('error', () => socket.destroy()); socket.on('error', () => upstream.destroy()); socket.on('close', () => upstream.destroy());
|
||||
upstream.on('connect', () => {
|
||||
const headers = ['GET /ws HTTP/1.1', 'Host: 127.0.0.1:4264', 'Origin: http://127.0.0.1:4264'];
|
||||
for (const name of ['upgrade', 'connection', 'sec-websocket-key', 'sec-websocket-version', 'sec-websocket-protocol']) {
|
||||
if (request.headers[name]) headers.push(name + ': ' + request.headers[name]);
|
||||
}
|
||||
upstream.write(headers.join('\r\n') + '\r\n\r\n');
|
||||
if (head.length) upstream.write(head);
|
||||
socket.pipe(upstream); upstream.pipe(socket);
|
||||
});
|
||||
});
|
||||
server.stop = () => { for (const socket of sockets) socket.destroy(); return new Promise(resolve => server.close(resolve)); };
|
||||
return server;
|
||||
}
|
||||
module.exports = { createLabServer };
|
||||
if (require.main === module) {
|
||||
const binary = path.join(root, 'build', 'web-host-test');
|
||||
const binary = path.join(root, 'build', 'web-rtc-host-test');
|
||||
if (!fs.existsSync(binary)) { console.error('Run tests/run.py in Docker first.'); process.exitCode = 1; }
|
||||
else {
|
||||
fs.copyFileSync(binary, '/tmp/c4f-gamepad-lab-service');
|
||||
copyService('/tmp/c4f-gamepad-lab-service');
|
||||
const service = spawn('/tmp/c4f-gamepad-lab-service', [], { stdio: ['ignore', 'ignore', 'inherit'] });
|
||||
const server = createLabServer();
|
||||
server.listen(8000, process.env.C4F_LAB_BIND || '127.0.0.1', () => console.log('Local gamepad lab: http://127.0.0.1:8000/'));
|
||||
|
||||
@@ -6,6 +6,7 @@ const path = require('node:path');
|
||||
const assert = require('node:assert/strict');
|
||||
const { spawn } = require('node:child_process');
|
||||
const { createLabServer } = require('./fake_gamepads_server.cjs');
|
||||
const { relayOffer, copyService } = require('./rtc_browser_helpers.cjs');
|
||||
|
||||
const root = '/src', profileDir = path.join(root, 'tests/fake_gamepads');
|
||||
const profiles = fs.readdirSync(profileDir).filter(file => file.endsWith('.json') && file !== 'manifest.json')
|
||||
@@ -27,7 +28,7 @@ const outputs = {
|
||||
rx: ['Right stick, left and right'], ry: ['Right stick, up and down']
|
||||
};
|
||||
const lab = createLabServer();
|
||||
fs.copyFileSync(path.join(root, 'build/web-host-test'), '/tmp/c4f-fake-gamepad-service');
|
||||
copyService('/tmp/c4f-fake-gamepad-service');
|
||||
const service = spawn('/tmp/c4f-fake-gamepad-service', [], { stdio: ['ignore', 'ignore', 'pipe'] });
|
||||
let serviceError;
|
||||
service.on('error', error => { serviceError = error; });
|
||||
@@ -216,6 +217,7 @@ async function runProfile(browser, profile) {
|
||||
const page = await context.newPage(), errors = [];
|
||||
page.on('pageerror', error => errors.push(error.message));
|
||||
await page.route(/^http:\/\/127\.0\.0\.1:4264\/(?:\?.*)?$/, async route => route.fulfill({ response: await route.fetch(), body: html }));
|
||||
await relayOffer(page);
|
||||
try {
|
||||
await page.goto('http://127.0.0.1:4264/');
|
||||
await sourceReady(page, profile);
|
||||
@@ -407,10 +409,10 @@ async function runManualHarness(browser) {
|
||||
await frame.waitForFunction(() => window.c4fTest?.gamepadSources.size === 1);
|
||||
await frame.waitForFunction(() => document.querySelector('#conn').dataset.conn === 'open');
|
||||
assert.equal(await frame.evaluate(() => navigator.getGamepads()[0].id), chosen.gamepad.id.value);
|
||||
const refused = await frame.evaluate(() => {
|
||||
try { new WebSocket('wss://example.invalid/ws'); return ''; } catch (error) { return error.message; }
|
||||
const refused = await frame.evaluate(async () => {
|
||||
try { await fetch('https://example.invalid/rtc', { method: 'POST' }); return ''; } catch (error) { return error.message; }
|
||||
});
|
||||
assert.match(refused, /only connects to the local test service/, 'the local manual harness rejects external WebSocket destinations');
|
||||
assert.match(refused, /only connects to the local test service/, 'the local manual harness rejects external signaling destinations');
|
||||
await frame.click('.gp-row [data-focus="configure"]');
|
||||
await changeControl(frame, 'CROSS');
|
||||
await frame.waitForFunction(() => c4fTest.mapper.capture?.ready);
|
||||
|
||||
@@ -25,15 +25,16 @@ int main(void)
|
||||
f = parse(green, sizeof(green));
|
||||
assert(f.r == 0 && f.g == 0x40 && f.b == 0);
|
||||
|
||||
/* A game's hit: the small motor at full for ~300 ms. */
|
||||
/* A game's hit: the large motor at full for ~300 ms. */
|
||||
static const uint8_t hit[] = { 0, 0, 0, 0xff, 0, 0, 0x40, 0 };
|
||||
f = parse(hit, sizeof(hit));
|
||||
assert(f.small == 0xff && f.large == 0 && f.g == 0x40);
|
||||
assert(f.large == 0xff && f.small == 0 && f.g == 0x40);
|
||||
|
||||
/* Both motors, by the DualShock 4 layout: [3] small, [4] large. */
|
||||
static const uint8_t both[] = { 0x01, 0, 0, 0x30, 0xc0, 0x20, 0, 0x20 };
|
||||
/* Each motor on its own from the PS4 app's controller test: the large motor
|
||||
* in [3], the small one in [4], the reverse of the DS4 report's order. */
|
||||
static const uint8_t both[] = { 0x01, 0, 0, 0xc0, 0x30, 0x20, 0, 0x20 };
|
||||
f = parse(both, sizeof(both));
|
||||
assert(f.small == 0x30 && f.large == 0xc0 && f.r == 0x20 && f.b == 0x20);
|
||||
assert(f.large == 0xc0 && f.small == 0x30 && f.r == 0x20 && f.b == 0x20);
|
||||
|
||||
/* Too short to hold the fields: nothing, rather than a misread. */
|
||||
uint8_t shortBuf[4] = { 1, 2, 3, 4 };
|
||||
|
||||
@@ -19,6 +19,31 @@ function padStatus(fields) {
|
||||
}
|
||||
|
||||
(async () => {
|
||||
// Echo timing remains the page's own measurement, with optional telemetry
|
||||
// only for a recognized probe and an advertised supporting service.
|
||||
const sentRtt = [], pingCtx = {
|
||||
conn: { echoes: new Map([[7, 100]]), pending: new Map(), latency: true },
|
||||
now: () => 116.6, pingMs: 0, send: message => sentRtt.push(message), showVersion() {},
|
||||
};
|
||||
vm.createContext(pingCtx);
|
||||
vm.runInContext(extract(' function onMessage(data)', ' function applyStatus('), pingCtx);
|
||||
vm.runInContext('onMessage(JSON.stringify({ method: "e", params: [7, 100] }))', pingCtx);
|
||||
assert.equal(pingCtx.pingMs, 17);
|
||||
assert.deepEqual(JSON.parse(JSON.stringify(sentRtt)), [{ method: 'latency', params: [7, 17] }]);
|
||||
vm.runInContext('onMessage(JSON.stringify({ method: "e", params: [7, 100] }))', pingCtx);
|
||||
assert.equal(sentRtt.length, 1, 'duplicate and unrecognized probes produce no report');
|
||||
pingCtx.conn.latency = false; pingCtx.conn.echoes.set(8, 100);
|
||||
vm.runInContext('onMessage(JSON.stringify({ method: "e", params: [8, 100] }))', pingCtx);
|
||||
assert.equal(sentRtt.length, 1, 'older services get no new method');
|
||||
const routed = [], routeCtx = { conn: { state: 'open',
|
||||
input: { readyState: 'open', bufferedAmount: 0, send: data => routed.push(['input', JSON.parse(data)]) },
|
||||
control: { readyState: 'open', bufferedAmount: 0, send: data => routed.push(['control', JSON.parse(data)]) } } };
|
||||
vm.createContext(routeCtx);
|
||||
vm.runInContext(extract(' function send(obj)', ' function call('), routeCtx);
|
||||
vm.runInContext('send({ method: "latency", params: [7, 17] })', routeCtx);
|
||||
assert.equal(routed[0][0], 'input');
|
||||
console.log('PASS exact input-channel RTT telemetry, optional capability and duplicate suppression');
|
||||
|
||||
// A claim is answered as a whole. One controller taken on another device must
|
||||
// not disconnect the gamepad that was playing as a different one.
|
||||
const claims = [];
|
||||
|
||||
@@ -30,6 +30,15 @@ int main(int argc, char **argv)
|
||||
printf("%d %d %d %s|%s\n", result, s.controllers, s.stopping, s.version, c4fLauncherProblem());
|
||||
return 0;
|
||||
}
|
||||
if (!strcmp(argv[1], "latency")) {
|
||||
C4fServiceStatus s;
|
||||
int result = c4fLauncherProbe(&s);
|
||||
printf("%d %d", result, s.latencyAvailable);
|
||||
for (int i = 0; i < C4F_SERVICE_PADS; i++)
|
||||
printf(" %d:%u:%d:%d", s.latency[i].userKnown, s.latency[i].userId, s.latency[i].rttMs, s.latency[i].ageMs);
|
||||
printf("\n");
|
||||
return 0;
|
||||
}
|
||||
if (!strcmp(argv[1], "start") && argc == 3) {
|
||||
size_t size = 0;
|
||||
const unsigned char *payload = c4fAutorunLoadBundled(argv[2]) ? NULL : c4fAutorunBundled(&size);
|
||||
|
||||
@@ -374,6 +374,9 @@ static void heldRemoteConfirmation(void)
|
||||
remoteFrame(&fake, &input, &screen, 40, C4F_INPUT_BUTTON_UP);
|
||||
remoteFrame(&fake, &input, &screen, 50, 0);
|
||||
remoteFrame(&fake, &input, &screen, 60, C4F_INPUT_BUTTON_UP);
|
||||
assert(screen.actionFocus == C4F_ACTION_TESTER);
|
||||
remoteFrame(&fake, &input, &screen, 62, 0);
|
||||
remoteFrame(&fake, &input, &screen, 64, C4F_INPUT_BUTTON_UP);
|
||||
assert(screen.actionFocus == C4F_ACTION_STOP);
|
||||
remoteFrame(&fake, &input, &screen, 70, 0);
|
||||
assert(remoteFrame(&fake, &input, &screen, 80, C4F_INPUT_BUTTON_CROSS) == C4F_ACTION_NONE);
|
||||
@@ -432,6 +435,8 @@ static void stopConfirmation(void)
|
||||
navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 0);
|
||||
navigate(&screen, 0, C4F_INPUT_BUTTON_UP, 0);
|
||||
navigate(&screen, 0, C4F_INPUT_BUTTON_UP, 0);
|
||||
assert(screen.actionFocus == C4F_ACTION_TESTER);
|
||||
navigate(&screen, 0, C4F_INPUT_BUTTON_UP, 0);
|
||||
assert(screen.actionFocus == C4F_ACTION_STOP);
|
||||
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 0) == C4F_ACTION_NONE);
|
||||
assert(screen.confirmStop && screen.confirmMenu && !screen.confirmChoice && !screen.actionMenu);
|
||||
@@ -441,6 +446,7 @@ static void stopConfirmation(void)
|
||||
navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 0);
|
||||
navigate(&screen, 0, C4F_INPUT_BUTTON_UP, 0);
|
||||
navigate(&screen, 0, C4F_INPUT_BUTTON_UP, 0);
|
||||
navigate(&screen, 0, C4F_INPUT_BUTTON_UP, 0);
|
||||
navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 0);
|
||||
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_RIGHT, 0) == C4F_ACTION_NONE);
|
||||
assert(screen.confirmChoice);
|
||||
|
||||
@@ -0,0 +1,310 @@
|
||||
/* The real sampler and visible-state comparison from main.cpp, with only native
|
||||
* APIs replaced. The display thread never polls input in these scenarios. */
|
||||
#include <assert.h>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <pthread.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <thread>
|
||||
#include <unistd.h>
|
||||
#include "autorun.h"
|
||||
#include "input.h"
|
||||
#include "navigation.h"
|
||||
#include "screen.h"
|
||||
#include "service.h"
|
||||
|
||||
static pthread_mutex_t c4fMutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
static C4fLauncherScreen c4fScreen;
|
||||
static int c4fCommand, c4fQuit, c4fCloseRequested;
|
||||
static uint64_t c4fRenderAt, c4fInputAt;
|
||||
/* Native API context is opaque to this test; its handles are owned by sampler. */
|
||||
typedef struct { int handles[2]; } C4fNativeInput;
|
||||
static std::atomic<uint32_t> standardButtons{0}, remoteButtons{0};
|
||||
static std::atomic<int> polls{0}, closes{0}, largeMotor{0};
|
||||
static std::atomic<bool> standardOpen{false}, lightOn{false};
|
||||
static std::atomic<bool> blockRead{false}, readBlocked{false}, releaseRead{false};
|
||||
|
||||
uint64_t c4fLauncherTimeMs(void)
|
||||
{
|
||||
return (uint64_t)std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch()).count();
|
||||
}
|
||||
|
||||
static int c4fNativeInitialUser(void *, int32_t *user) { *user = 1; return 0; }
|
||||
static int c4fNativeOpen(void *, int32_t, int port, int *owned)
|
||||
{
|
||||
*owned = 1;
|
||||
if (port != 16) standardOpen = true;
|
||||
return port == 16 ? 11 : 10;
|
||||
}
|
||||
static int c4fNativeRead(void *, int handle, uint32_t *buttons, int *connected)
|
||||
{
|
||||
polls++;
|
||||
*buttons = handle == 10 ? standardButtons.load() : remoteButtons.load();
|
||||
*connected = 1;
|
||||
return 0;
|
||||
}
|
||||
static void c4fNativeClose(void *, int handle)
|
||||
{
|
||||
if (handle == 10) {
|
||||
assert(largeMotor.load() == 0 && !lightOn.load());
|
||||
standardOpen = false;
|
||||
}
|
||||
closes++;
|
||||
}
|
||||
static int c4fTesterLoginUsers(void *, int32_t *users) { users[0] = 1; return 1; }
|
||||
static void c4fTesterUserName(void *, int32_t, char *name, int size) { snprintf(name, size, "Alex"); }
|
||||
static int c4fTesterOpenPad(void *, int32_t, int *owned) { *owned = 0; return 10; }
|
||||
static int c4fTesterRead(void *, int, C4fTesterSample *sample)
|
||||
{
|
||||
if (blockRead.exchange(false)) {
|
||||
readBlocked = true;
|
||||
while (!releaseRead) usleep(1000);
|
||||
readBlocked = false;
|
||||
}
|
||||
memset(sample, 0, sizeof(*sample));
|
||||
sample->buttons = standardButtons.load(); sample->connected = 1;
|
||||
sample->lx = sample->ly = sample->rx = sample->ry = 128;
|
||||
return 0;
|
||||
}
|
||||
static void c4fTesterClosePad(void *, int) { assert(!"borrowed handle closed"); }
|
||||
static int c4fTesterVibrate(void *, int, uint8_t large, uint8_t)
|
||||
{
|
||||
assert(standardOpen.load());
|
||||
largeMotor = large;
|
||||
return 0;
|
||||
}
|
||||
static int c4fTesterLight(void *, int, uint8_t, uint8_t, uint8_t) { assert(standardOpen.load()); lightOn = true; return 0; }
|
||||
static int c4fTesterResetLight(void *, int) { assert(standardOpen.load()); lightOn = false; return 0; }
|
||||
static int c4fTesterResetOrientation(void *, int) { return 0; }
|
||||
static int c4fTesterMotion(void *, int, int) { return 0; }
|
||||
static void c4fTesterLogState(void *, int, int, uint32_t) {}
|
||||
static void c4fNativeInputNote(const C4fNativeInput *, char *note, size_t size) { snprintf(note, size, "test input"); }
|
||||
|
||||
#include "launcher-sampling-source.inc"
|
||||
|
||||
template <typename Predicate> static void until(Predicate predicate)
|
||||
{
|
||||
uint64_t deadline = c4fLauncherTimeMs() + 1000;
|
||||
while (!predicate()) { assert(c4fLauncherTimeMs() < deadline); usleep(1000); }
|
||||
}
|
||||
|
||||
static C4fTesterView view()
|
||||
{
|
||||
pthread_mutex_lock(&c4fMutex);
|
||||
C4fTesterView result = c4fScreen.tester;
|
||||
pthread_mutex_unlock(&c4fMutex);
|
||||
return result;
|
||||
}
|
||||
|
||||
static void tap(std::atomic<uint32_t> &buttons, uint32_t mask)
|
||||
{
|
||||
buttons = mask; usleep(16000); buttons = 0; usleep(16000);
|
||||
}
|
||||
|
||||
static void noConnectionPads(C4fTesterConnection connection, int available)
|
||||
{
|
||||
assert(connection.available == available && connection.pads == 0);
|
||||
for (int p = 0; p < C4F_SERVICE_PADS; p++) assert(connection.rttMs[p] == -1);
|
||||
}
|
||||
|
||||
static void connectionView()
|
||||
{
|
||||
C4fServiceStatus status = {};
|
||||
status.latencyAvailable = 1;
|
||||
C4fTesterView tester = {};
|
||||
tester.active = 1; tester.selected = 2;
|
||||
tester.user[0].used = tester.user[2].used = 1;
|
||||
tester.user[0].state = tester.user[2].state = C4F_TESTER_READY;
|
||||
tester.user[0].id = 1; tester.user[2].id = 0x1A2B3C4D;
|
||||
snprintf(tester.user[0].name, sizeof(tester.user[0].name), "Alex");
|
||||
snprintf(tester.user[2].name, sizeof(tester.user[2].name), "Alex");
|
||||
status.latency[0] = { 1, 1, 4, 0 };
|
||||
status.latency[1] = { 1, 0x1A2B3C4D, 8, 100 };
|
||||
status.latency[2] = { 0, 0x1A2B3C4D, 0, 0 };
|
||||
status.latency[3] = { 1, 0x1A2B3C4D, 12, 200 };
|
||||
const uint64_t checkedAt = 10000;
|
||||
C4fTesterConnection connection = c4fTesterConnectionView(&status, C4F_SERVICE_RUNNING, checkedAt, checkedAt, &tester);
|
||||
assert(connection.available && connection.pads == ((1U << 1) | (1U << 3)));
|
||||
assert(connection.rttMs[0] == -1 && connection.rttMs[1] == 8 && connection.rttMs[2] == -1 && connection.rttMs[3] == 12);
|
||||
/* Equal display names and equal slot indexes never establish ownership. */
|
||||
tester.selected = 0;
|
||||
connection = c4fTesterConnectionView(&status, C4F_SERVICE_RUNNING, checkedAt, checkedAt, &tester);
|
||||
assert(connection.pads == 1 && connection.rttMs[0] == 4 && connection.rttMs[1] == -1 && connection.rttMs[3] == -1);
|
||||
tester.selected = 2;
|
||||
tester.user[2].id = 99;
|
||||
noConnectionPads(c4fTesterConnectionView(&status, C4F_SERVICE_RUNNING, checkedAt, checkedAt, &tester), 1);
|
||||
tester.user[2].id = 0x1A2B3C4D;
|
||||
printf("PASS connection view: selected user ID matches every owned virtual slot, independently of names and indexes\n");
|
||||
|
||||
/* The cached service reply also ages while the launcher renders it. */
|
||||
status.latency[1].ageMs = C4F_SERVICE_LATENCY_STALE_MS - 1000;
|
||||
status.latency[3].ageMs = C4F_SERVICE_LATENCY_STALE_MS - 999;
|
||||
connection = c4fTesterConnectionView(&status, C4F_SERVICE_RUNNING, checkedAt, checkedAt + 1000, &tester);
|
||||
assert(connection.pads == ((1U << 1) | (1U << 3)) && connection.rttMs[1] == 8 && connection.rttMs[3] == -1);
|
||||
connection = c4fTesterConnectionView(&status, C4F_SERVICE_RUNNING, checkedAt, checkedAt + 1001, &tester);
|
||||
assert(connection.pads == ((1U << 1) | (1U << 3)) && connection.rttMs[1] == -1 && connection.rttMs[3] == -1);
|
||||
status.latency[1].ageMs = status.latency[3].ageMs = 0;
|
||||
connection = c4fTesterConnectionView(&status, C4F_SERVICE_RUNNING, checkedAt, checkedAt + C4F_SERVICE_LATENCY_STALE_MS, &tester);
|
||||
assert(connection.available && connection.rttMs[1] == 8 && connection.rttMs[3] == 12);
|
||||
noConnectionPads(c4fTesterConnectionView(&status, C4F_SERVICE_RUNNING, checkedAt,
|
||||
checkedAt + C4F_SERVICE_LATENCY_STALE_MS + 1, &tester), 0);
|
||||
noConnectionPads(c4fTesterConnectionView(&status, C4F_SERVICE_RUNNING, checkedAt, checkedAt - 1, &tester), 0);
|
||||
printf("PASS connection view: each sample expires at its own age plus local cache age; stale or future cache is unavailable\n");
|
||||
|
||||
status.latency[1].rttMs = -1;
|
||||
status.latency[3].ageMs = -1;
|
||||
connection = c4fTesterConnectionView(&status, C4F_SERVICE_RUNNING, checkedAt, checkedAt, &tester);
|
||||
assert(connection.available && connection.pads == ((1U << 1) | (1U << 3)));
|
||||
for (int p = 0; p < C4F_SERVICE_PADS; p++) assert(connection.rttMs[p] == -1);
|
||||
status.latency[1].rttMs = 0;
|
||||
connection = c4fTesterConnectionView(&status, C4F_SERVICE_RUNNING, checkedAt, checkedAt, &tester);
|
||||
assert(connection.rttMs[1] == 0 && connection.rttMs[3] == -1);
|
||||
const int serviceStates[] = { C4F_SERVICE_UNKNOWN, C4F_SERVICE_DOWN };
|
||||
for (int running : serviceStates)
|
||||
noConnectionPads(c4fTesterConnectionView(&status, running, checkedAt, checkedAt, &tester), 0);
|
||||
status.latencyAvailable = 0; /* A service without telemetry support. */
|
||||
noConnectionPads(c4fTesterConnectionView(&status, C4F_SERVICE_RUNNING, checkedAt, checkedAt, &tester), 0);
|
||||
status.latencyAvailable = 1;
|
||||
printf("PASS connection view: unknown RTT stays -1, measured zero stays zero, unknown/stopped/older service has no readings\n");
|
||||
|
||||
tester.selected = -1;
|
||||
noConnectionPads(c4fTesterConnectionView(&status, C4F_SERVICE_RUNNING, checkedAt, checkedAt, &tester), 1);
|
||||
tester.selected = C4F_TESTER_USERS;
|
||||
noConnectionPads(c4fTesterConnectionView(&status, C4F_SERVICE_RUNNING, checkedAt, checkedAt, &tester), 1);
|
||||
tester.selected = 2; tester.user[2].used = 0;
|
||||
noConnectionPads(c4fTesterConnectionView(&status, C4F_SERVICE_RUNNING, checkedAt, checkedAt, &tester), 1);
|
||||
tester.user[2].used = 1;
|
||||
const int readerStates[] = { C4F_TESTER_OPENING, C4F_TESTER_NO_CONTROLLER, C4F_TESTER_FAILED, C4F_TESTER_SIGNED_OUT };
|
||||
for (int state : readerStates) {
|
||||
tester.user[2].state = state;
|
||||
noConnectionPads(c4fTesterConnectionView(&status, C4F_SERVICE_RUNNING, checkedAt, checkedAt, &tester), 1);
|
||||
}
|
||||
printf("PASS connection view: no selected ready user means no virtual-controller association\n");
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
connectionView();
|
||||
c4fScreen.running = 1; c4fScreen.autorun = C4F_AUTORUN_ON;
|
||||
c4fRenderAt = c4fLauncherTimeMs();
|
||||
pthread_t worker;
|
||||
assert(!pthread_create(&worker, NULL, c4fInputWorker, NULL));
|
||||
until([] { return polls.load() >= 4; });
|
||||
tap(standardButtons, C4F_INPUT_BUTTON_OPTIONS);
|
||||
until([] { C4fTesterView v = view(); return v.active && v.user[0].state == C4F_TESTER_READY; });
|
||||
tap(remoteButtons, C4F_INPUT_BUTTON_CROSS);
|
||||
until([] { return view().menu; });
|
||||
std::thread press([] {
|
||||
usleep(20000); standardButtons = C4F_INPUT_BUTTON_DOWN;
|
||||
usleep(16000); standardButtons = 0;
|
||||
});
|
||||
int before = polls;
|
||||
usleep(100000); /* A display stall cannot swallow this complete short tap. */
|
||||
press.join();
|
||||
assert(view().focus == C4F_TESTER_RUMBLE_LARGE);
|
||||
assert(polls.load() - before >= 8);
|
||||
printf("PASS sampler: a short navigation tap survives a 100 ms display pause\n");
|
||||
|
||||
assert(view().focus == C4F_TESTER_RUMBLE_LARGE);
|
||||
tap(standardButtons, C4F_INPUT_BUTTON_CROSS);
|
||||
until([] { return largeMotor.load() == 255; });
|
||||
blockRead = true;
|
||||
until([] { return readBlocked.load(); });
|
||||
assert(!pthread_mutex_trylock(&c4fMutex));
|
||||
c4fScreen.controllers = 3;
|
||||
pthread_mutex_unlock(&c4fMutex);
|
||||
releaseRead = true;
|
||||
until([] { return !readBlocked.load(); });
|
||||
printf("PASS sampler: blocked native reads leave the display/status lock available\n");
|
||||
|
||||
// Keep the display alive while the input thread stalls. This must exercise
|
||||
// the input-gap resume branch, not the stale-display pause branch. Both
|
||||
// outputs use a handle borrowed from the ordinary launcher input reader.
|
||||
tap(standardButtons, C4F_INPUT_BUTTON_UP);
|
||||
tap(standardButtons, C4F_INPUT_BUTTON_LEFT);
|
||||
assert(view().focus == C4F_TESTER_LIGHT_COLOR);
|
||||
tap(standardButtons, C4F_INPUT_BUTTON_CROSS);
|
||||
until([] { return lightOn.load(); });
|
||||
tap(standardButtons, C4F_INPUT_BUTTON_CIRCLE);
|
||||
tap(standardButtons, C4F_INPUT_BUTTON_DOWN);
|
||||
tap(standardButtons, C4F_INPUT_BUTTON_RIGHT);
|
||||
assert(view().focus == C4F_TESTER_RUMBLE_LARGE);
|
||||
tap(standardButtons, C4F_INPUT_BUTTON_CROSS);
|
||||
until([] { return largeMotor.load() == 255; });
|
||||
releaseRead = false;
|
||||
blockRead = true;
|
||||
until([] { return readBlocked.load(); });
|
||||
int beforeResume = closes.load();
|
||||
for (int i = 0; i < 12; i++) {
|
||||
pthread_mutex_lock(&c4fMutex);
|
||||
c4fRenderAt = c4fLauncherTimeMs();
|
||||
pthread_mutex_unlock(&c4fMutex);
|
||||
usleep(100000);
|
||||
}
|
||||
pthread_mutex_lock(&c4fMutex);
|
||||
c4fRenderAt = c4fLauncherTimeMs();
|
||||
pthread_mutex_unlock(&c4fMutex);
|
||||
releaseRead = true;
|
||||
until([beforeResume] { return closes.load() >= beforeResume + 2 && !view().menu &&
|
||||
view().user[0].state == C4F_TESTER_READY; });
|
||||
assert(!largeMotor.load() && !lightOn.load());
|
||||
printf("PASS sampler: fresh-display input-gap resume quiets borrowed-handle outputs before closing their owner\n");
|
||||
|
||||
// Restore active output so the independent stale-display safety check
|
||||
// below cannot pass merely because the resume case already stopped it.
|
||||
tap(remoteButtons, C4F_INPUT_BUTTON_CROSS);
|
||||
until([] { return view().menu; });
|
||||
tap(standardButtons, C4F_INPUT_BUTTON_DOWN);
|
||||
assert(view().focus == C4F_TESTER_RUMBLE_LARGE);
|
||||
tap(standardButtons, C4F_INPUT_BUTTON_CROSS);
|
||||
until([] { return largeMotor.load() == 255; });
|
||||
int beforeDisplayPause = closes.load();
|
||||
|
||||
pthread_mutex_lock(&c4fMutex);
|
||||
c4fRenderAt = c4fLauncherTimeMs() - 1100;
|
||||
pthread_mutex_unlock(&c4fMutex);
|
||||
until([] { return largeMotor.load() == 0 && view().menu == 0; });
|
||||
assert(closes.load() >= beforeDisplayPause + 2);
|
||||
pthread_mutex_lock(&c4fMutex);
|
||||
assert(c4fScreen.controllers == 3);
|
||||
c4fRenderAt = c4fLauncherTimeMs();
|
||||
pthread_mutex_unlock(&c4fMutex);
|
||||
until([] { return view().user[0].state == C4F_TESTER_READY; });
|
||||
pthread_mutex_lock(&c4fMutex); c4fQuit = 1; pthread_mutex_unlock(&c4fMutex);
|
||||
pthread_join(worker, NULL);
|
||||
assert(largeMotor.load() == 0 && closes.load() >= 4);
|
||||
printf("PASS sampler: stalled display stops output; recovery and owned-handle cleanup are safe\n");
|
||||
|
||||
C4fLauncherScreen a = {}, b;
|
||||
a.tester.active = a.tester.menu = 1; a.tester.selected = 0;
|
||||
b = a; b.controllers = 4; b.tester.l2 = 170; b.tester.gyro[2] = 230;
|
||||
C4fLauncherScreen va = c4fVisibleScreen(&a), vb = c4fVisibleScreen(&b);
|
||||
assert(!memcmp(&va, &vb, sizeof(va)));
|
||||
b.tester.focus = 1; vb = c4fVisibleScreen(&b); assert(memcmp(&va, &vb, sizeof(va)));
|
||||
b = a; b.tester.rumble = 1; vb = c4fVisibleScreen(&b); assert(memcmp(&va, &vb, sizeof(va)));
|
||||
a.connection.available = 1; a.connection.pads = 1U << 1;
|
||||
for (int p = 0; p < C4F_SERVICE_PADS; p++) a.connection.rttMs[p] = -1;
|
||||
a.connection.rttMs[1] = 8;
|
||||
b = a; b.connection.rttMs[1] = 9;
|
||||
va = c4fVisibleScreen(&a); vb = c4fVisibleScreen(&b); assert(memcmp(&va, &vb, sizeof(va)));
|
||||
b = a; b.connection.rttMs[1] = -1;
|
||||
vb = c4fVisibleScreen(&b); assert(memcmp(&va, &vb, sizeof(va)));
|
||||
printf("PASS tester redraw key: fresh and expired connection RTT changes remain visible in tools\n");
|
||||
a.tester.colorMenu = 1;
|
||||
va = c4fVisibleScreen(&a);
|
||||
b = a; b.tester.gyro[2] = 190;
|
||||
vb = c4fVisibleScreen(&b); assert(!memcmp(&va, &vb, sizeof(va)));
|
||||
b.tester.colorChannel = 1;
|
||||
vb = c4fVisibleScreen(&b); assert(memcmp(&va, &vb, sizeof(va)));
|
||||
b = a; b.tester.colorFine = 1;
|
||||
vb = c4fVisibleScreen(&b); assert(memcmp(&va, &vb, sizeof(va)));
|
||||
b = a; b.tester.light[0] = 128;
|
||||
vb = c4fVisibleScreen(&b); assert(memcmp(&va, &vb, sizeof(va)));
|
||||
printf("PASS RGB picker redraw key: selection, precision and color visible; hidden live motion ignored\n");
|
||||
b = a;
|
||||
a.tester.menu = b.tester.menu = 0; b.tester.gyro[2] = 200;
|
||||
va = c4fVisibleScreen(&a); vb = c4fVisibleScreen(&b); assert(memcmp(&va, &vb, sizeof(va)));
|
||||
printf("PASS tester redraw key: hidden readings ignored by tools; live view and output stay visible\n");
|
||||
}
|
||||
@@ -0,0 +1,759 @@
|
||||
/* The controller test's logic with simulated users and pads. No console, no
|
||||
* screen: the real tester.c and navigation.c against fake native calls. */
|
||||
#include <assert.h>
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "autorun.h"
|
||||
#include "input.h"
|
||||
#include "navigation.h"
|
||||
#include "screen.h"
|
||||
#include "tester.h"
|
||||
|
||||
#define STICKS (C4F_INPUT_BUTTON_L3 | C4F_INPUT_BUTTON_R3)
|
||||
|
||||
typedef struct {
|
||||
int32_t user;
|
||||
int handle, owned, openError, readError, connected;
|
||||
int opens, closes, reads, motionOn, lightResets, orientationResets, vibrations, lightCalls, lightError;
|
||||
int vibrating, large, small, lit;
|
||||
uint8_t rgb[3];
|
||||
C4fTesterSample sample;
|
||||
} FakePad;
|
||||
|
||||
typedef struct {
|
||||
int32_t users[4];
|
||||
int count, listError;
|
||||
FakePad pads[4];
|
||||
int stateCalls;
|
||||
} Fake;
|
||||
|
||||
static FakePad *byUser(Fake *f, int32_t user)
|
||||
{
|
||||
for (int i = 0; i < 4; i++) if (f->pads[i].user == user) return &f->pads[i];
|
||||
assert(!"unknown user");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static FakePad *byHandle(Fake *f, int handle)
|
||||
{
|
||||
for (int i = 0; i < 4; i++) if (f->pads[i].handle == handle) return &f->pads[i];
|
||||
assert(!"unknown handle");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int fakeUsers(void *context, int32_t *users)
|
||||
{
|
||||
Fake *f = context;
|
||||
if (f->listError) return f->listError;
|
||||
for (int i = 0; i < f->count; i++) users[i] = f->users[i];
|
||||
return f->count;
|
||||
}
|
||||
|
||||
static void fakeName(void *context, int32_t user, char *name, int size)
|
||||
{
|
||||
(void)context;
|
||||
/* User 3 has no name the app can draw: the tester falls back. */
|
||||
if (user == 3) { name[0] = 0; return; }
|
||||
snprintf(name, (size_t)size, user == 1 ? "Alex" : user == 2 ? "Sam\x01" : "Kim");
|
||||
}
|
||||
|
||||
static int fakeOpen(void *context, int32_t user, int *owned)
|
||||
{
|
||||
FakePad *p = byUser(context, user);
|
||||
p->opens++;
|
||||
*owned = p->owned;
|
||||
return p->openError ? p->openError : p->handle;
|
||||
}
|
||||
|
||||
static int fakeRead(void *context, int handle, C4fTesterSample *sample)
|
||||
{
|
||||
FakePad *p = byHandle(context, handle);
|
||||
p->reads++;
|
||||
*sample = p->sample;
|
||||
sample->connected = p->connected;
|
||||
return p->readError;
|
||||
}
|
||||
|
||||
static void fakeClose(void *context, int handle)
|
||||
{
|
||||
FakePad *p = byHandle(context, handle);
|
||||
assert(p->owned);
|
||||
p->closes++;
|
||||
assert(p->closes <= p->opens);
|
||||
}
|
||||
|
||||
static int fakeVibrate(void *context, int handle, uint8_t large, uint8_t small)
|
||||
{
|
||||
FakePad *p = byHandle(context, handle);
|
||||
p->vibrations++;
|
||||
p->large = large;
|
||||
p->small = small;
|
||||
p->vibrating = large || small;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int fakeLight(void *context, int handle, uint8_t r, uint8_t g, uint8_t b)
|
||||
{
|
||||
FakePad *p = byHandle(context, handle);
|
||||
p->lightCalls++;
|
||||
if (p->lightError) return p->lightError;
|
||||
p->lit = 1;
|
||||
p->rgb[0] = r; p->rgb[1] = g; p->rgb[2] = b;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int fakeResetLight(void *context, int handle)
|
||||
{
|
||||
FakePad *p = byHandle(context, handle);
|
||||
p->lightResets++;
|
||||
p->lit = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int fakeResetOrientation(void *context, int handle)
|
||||
{
|
||||
byHandle(context, handle)->orientationResets++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int fakeMotion(void *context, int handle, int on)
|
||||
{
|
||||
byHandle(context, handle)->motionOn = on;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void fakeState(void *context, int slot, int state, uint32_t error)
|
||||
{
|
||||
(void)slot; (void)state; (void)error;
|
||||
((Fake *)context)->stateCalls++;
|
||||
}
|
||||
|
||||
static void setup(Fake *f, C4fTester *t)
|
||||
{
|
||||
memset(f, 0, sizeof(*f));
|
||||
f->count = 2;
|
||||
f->users[0] = 1;
|
||||
f->users[1] = 2;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
f->pads[i] = (FakePad){ .user = i + 1, .handle = 100 + i, .owned = 1, .connected = 1 };
|
||||
f->pads[i].sample.lx = f->pads[i].sample.ly = f->pads[i].sample.rx = f->pads[i].sample.ry = 128;
|
||||
}
|
||||
/* The launcher's own reader already holds the first user's handle. */
|
||||
f->pads[0].owned = 0;
|
||||
C4fTesterOps ops = { f, fakeUsers, fakeName, fakeOpen, fakeRead, fakeClose, fakeVibrate, fakeLight,
|
||||
fakeResetLight, fakeResetOrientation, fakeMotion, fakeState };
|
||||
c4fTesterInit(t, &ops);
|
||||
}
|
||||
|
||||
/* A frame with the given buttons held on the first user's controller. */
|
||||
static int frame(Fake *f, C4fTester *t, uint64_t now, uint32_t held, uint32_t remote)
|
||||
{
|
||||
f->pads[0].sample.buttons = held;
|
||||
return c4fTesterFrame(t, now, remote);
|
||||
}
|
||||
|
||||
static int tap(Fake *f, C4fTester *t, uint64_t *now, uint32_t button)
|
||||
{
|
||||
int active = frame(f, t, *now += 10, button, 0);
|
||||
if (active) frame(f, t, *now += 10, 0, 0);
|
||||
return active;
|
||||
}
|
||||
|
||||
static void holdTiming(void)
|
||||
{
|
||||
C4fTesterHold hold = { 0 };
|
||||
assert(!c4fTesterHoldUpdate(&hold, C4F_INPUT_BUTTON_L3, 0));
|
||||
assert(!c4fTesterHoldUpdate(&hold, STICKS, 100));
|
||||
assert(!c4fTesterHoldUpdate(&hold, STICKS | C4F_INPUT_BUTTON_CROSS, 2099));
|
||||
assert(c4fTesterHoldUpdate(&hold, STICKS, 2100));
|
||||
assert(!c4fTesterHoldUpdate(&hold, STICKS, 5000));
|
||||
assert(!c4fTesterHoldUpdate(&hold, 0, 5001));
|
||||
assert(!c4fTesterHoldUpdate(&hold, STICKS, 6000));
|
||||
assert(c4fTesterHoldUpdate(&hold, STICKS, 8000));
|
||||
puts("PASS L3+R3 must be held together for two seconds, once per hold");
|
||||
}
|
||||
|
||||
static void readsUsers(void)
|
||||
{
|
||||
Fake f;
|
||||
static C4fTester t;
|
||||
setup(&f, &t);
|
||||
c4fTesterOpen(&t, 1000);
|
||||
assert(t.active && t.selected == 0);
|
||||
f.pads[0].sample.lx = 12; f.pads[0].sample.l2 = 200; f.pads[0].sample.touches = 3;
|
||||
f.pads[0].sample.touch[1].x = 1919; f.pads[0].sample.touch[1].y = 941;
|
||||
f.pads[0].sample.accel[1] = 0.987f; f.pads[0].sample.gyro[0] = -1.234f; f.pads[0].sample.quat[3] = 0.5f;
|
||||
f.pads[0].sample.quat[0] = NAN;
|
||||
assert(frame(&f, &t, 1000, C4F_INPUT_BUTTON_CROSS, 0));
|
||||
assert(f.pads[0].opens == 1 && f.pads[1].opens == 1 && f.pads[0].motionOn && f.pads[1].motionOn);
|
||||
C4fTesterView v;
|
||||
c4fTesterView(&t, &v);
|
||||
assert(v.active && !v.menu && v.selected == 0);
|
||||
assert(v.user[0].used && v.user[1].used && !v.user[2].used);
|
||||
assert(v.user[0].id == 1 && v.user[1].id == 2 && v.user[2].id == -1 && v.user[3].id == -1);
|
||||
assert(v.user[v.selected].id == 1);
|
||||
assert(!strcmp(v.user[0].name, "Alex") && !strcmp(v.user[1].name, "Sam?"));
|
||||
assert(v.user[0].state == C4F_TESTER_READY && v.user[1].state == C4F_TESTER_READY);
|
||||
assert(v.buttons == C4F_INPUT_BUTTON_CROSS && v.lx == 12 && v.l2 == 200);
|
||||
assert(v.touches == 2 && v.touchX[1] == 1919 && v.touchY[1] == 941);
|
||||
assert(v.accel[1] == 99 && v.gyro[0] == -123 && v.quat[3] == 500 && v.quat[0] == 0);
|
||||
/* The Cross held when reading started is shown, but is not a press. */
|
||||
assert(t.readers[0].pressed == 0);
|
||||
t.selected = 1;
|
||||
c4fTesterView(&t, &v);
|
||||
assert(v.selected == 1 && v.user[v.selected].id == 2);
|
||||
t.selected = 0;
|
||||
frame(&f, &t, 1010, C4F_INPUT_BUTTON_CROSS | C4F_INPUT_BUTTON_CIRCLE, 0);
|
||||
assert(t.readers[0].pressed == C4F_INPUT_BUTTON_CIRCLE && t.active && !t.menu);
|
||||
|
||||
/* A third user with no drawable name gets a stand-in. */
|
||||
f.count = 3; f.users[2] = 3;
|
||||
frame(&f, &t, 2010, 0, 0);
|
||||
c4fTesterView(&t, &v);
|
||||
assert(!strcmp(v.user[2].name, "User 3"));
|
||||
assert(v.user[2].id == 3);
|
||||
c4fTesterClose(&t);
|
||||
assert(!t.active && f.pads[0].closes == 0 && f.pads[1].closes == 1 && f.pads[2].closes == 1);
|
||||
c4fTesterView(&t, &v);
|
||||
assert(!v.active);
|
||||
for (int s = 0; s < C4F_TESTER_USERS; s++) assert(!v.user[s].used && v.user[s].id == -1);
|
||||
|
||||
setup(&f, &t);
|
||||
f.count = 0;
|
||||
c4fTesterOpen(&t, 0);
|
||||
frame(&f, &t, 0, 0, 0);
|
||||
c4fTesterView(&t, &v);
|
||||
assert(v.active && v.selected == -1);
|
||||
for (int s = 0; s < C4F_TESTER_USERS; s++) assert(!v.user[s].used && v.user[s].id == -1);
|
||||
c4fTesterClose(&t);
|
||||
puts("PASS readers for every signed-in user, borrowed handle kept, values in display units");
|
||||
}
|
||||
|
||||
/* Opens the test, lets the opening hold go, then holds again until the menu opens. */
|
||||
static uint64_t openMenu(Fake *f, C4fTester *t)
|
||||
{
|
||||
c4fTesterOpen(t, 0);
|
||||
/* Still holding the L3+R3 that opened it: no menu, however long. */
|
||||
for (uint64_t now = 0; now <= 4000; now += 500) assert(frame(f, t, now, STICKS, 0) && !t->menu);
|
||||
frame(f, t, 4100, 0, 0);
|
||||
frame(f, t, 4200, STICKS, 0);
|
||||
frame(f, t, 6199, STICKS, 0);
|
||||
assert(!t->menu);
|
||||
frame(f, t, 6200, STICKS, 0);
|
||||
assert(t->menu && !t->armed && t->focus == C4F_TESTER_RESUME);
|
||||
return 6200;
|
||||
}
|
||||
|
||||
static void menuActions(void)
|
||||
{
|
||||
Fake f;
|
||||
static C4fTester t;
|
||||
setup(&f, &t);
|
||||
uint64_t now = openMenu(&f, &t);
|
||||
frame(&f, &t, now += 10, 0, 0);
|
||||
assert(t.armed);
|
||||
tap(&f, &t, &now, C4F_INPUT_BUTTON_DOWN);
|
||||
assert(t.focus == C4F_TESTER_RUMBLE_LARGE);
|
||||
tap(&f, &t, &now, C4F_INPUT_BUTTON_CROSS);
|
||||
assert(f.pads[0].vibrating && f.pads[0].large == 255 && f.pads[0].small == 0);
|
||||
C4fTesterView v;
|
||||
c4fTesterView(&t, &v);
|
||||
assert(v.rumble == 1 && strstr(v.note, "Large motor"));
|
||||
now = t.rumbleUntil - 1;
|
||||
frame(&f, &t, now, 0, 0);
|
||||
assert(f.pads[0].vibrating);
|
||||
frame(&f, &t, ++now, 0, 0);
|
||||
assert(!f.pads[0].vibrating && !t.rumble);
|
||||
|
||||
/* Small motor, then the light bar and orientation. */
|
||||
tap(&f, &t, &now, C4F_INPUT_BUTTON_RIGHT);
|
||||
assert(t.focus == C4F_TESTER_RUMBLE_SMALL);
|
||||
tap(&f, &t, &now, C4F_INPUT_BUTTON_CROSS);
|
||||
assert(f.pads[0].vibrating && f.pads[0].large == 0 && f.pads[0].small == 255);
|
||||
/* A move and a select arriving together only move. */
|
||||
frame(&f, &t, now += 10, C4F_INPUT_BUTTON_DOWN | C4F_INPUT_BUTTON_CROSS, 0);
|
||||
assert(t.active && t.focus == C4F_TESTER_USER && !f.pads[0].lit);
|
||||
frame(&f, &t, now += 10, 0, 0);
|
||||
tap(&f, &t, &now, C4F_INPUT_BUTTON_RIGHT);
|
||||
tap(&f, &t, &now, C4F_INPUT_BUTTON_RIGHT);
|
||||
assert(t.focus == C4F_TESTER_LIGHT_COLOR);
|
||||
tap(&f, &t, &now, C4F_INPUT_BUTTON_CROSS);
|
||||
assert(t.colorMenu && f.pads[0].lit && f.pads[0].rgb[0] == 255 && f.pads[0].rgb[1] == 255);
|
||||
c4fTesterView(&t, &v);
|
||||
assert(v.lightSet && v.light[0] == 255);
|
||||
tap(&f, &t, &now, C4F_INPUT_BUTTON_CIRCLE);
|
||||
tap(&f, &t, &now, C4F_INPUT_BUTTON_LEFT);
|
||||
assert(t.focus == C4F_TESTER_ORIENTATION);
|
||||
tap(&f, &t, &now, C4F_INPUT_BUTTON_CROSS);
|
||||
assert(f.pads[0].orientationResets == 1);
|
||||
|
||||
/* Back to the launcher leaves and cleans up. */
|
||||
tap(&f, &t, &now, C4F_INPUT_BUTTON_RIGHT);
|
||||
tap(&f, &t, &now, C4F_INPUT_BUTTON_DOWN);
|
||||
assert(t.focus == C4F_TESTER_EXIT);
|
||||
assert(!tap(&f, &t, &now, C4F_INPUT_BUTTON_CROSS));
|
||||
assert(!t.active);
|
||||
assert(f.pads[0].lightResets == 1 && !f.pads[0].lit && !f.pads[0].vibrating);
|
||||
assert(f.pads[0].closes == 0 && f.pads[1].closes == 1);
|
||||
puts("PASS menu after a fresh hold and release; rumble stops after a second; light bar reset on leaving");
|
||||
}
|
||||
|
||||
static void switchUsers(void)
|
||||
{
|
||||
Fake f;
|
||||
static C4fTester t;
|
||||
setup(&f, &t);
|
||||
uint64_t now = openMenu(&f, &t);
|
||||
frame(&f, &t, now += 10, 0, 0);
|
||||
tap(&f, &t, &now, C4F_INPUT_BUTTON_RIGHT);
|
||||
assert(t.focus == C4F_TESTER_USER);
|
||||
tap(&f, &t, &now, C4F_INPUT_BUTTON_CROSS);
|
||||
assert(t.selected == 1);
|
||||
tap(&f, &t, &now, C4F_INPUT_BUTTON_CROSS);
|
||||
assert(t.selected == 0);
|
||||
/* The second user's own controller drives the menu as well. */
|
||||
f.pads[1].sample.buttons = C4F_INPUT_BUTTON_CROSS;
|
||||
frame(&f, &t, now += 10, 0, 0);
|
||||
assert(t.selected == 1);
|
||||
f.pads[1].sample.buttons = 0;
|
||||
frame(&f, &t, now += 10, 0, 0);
|
||||
/* Rumble goes to the selected user's controller. */
|
||||
tap(&f, &t, &now, C4F_INPUT_BUTTON_DOWN);
|
||||
tap(&f, &t, &now, C4F_INPUT_BUTTON_LEFT);
|
||||
tap(&f, &t, &now, C4F_INPUT_BUTTON_CROSS);
|
||||
assert(f.pads[1].vibrating && !f.pads[0].vibrating);
|
||||
/* Back closes the menu; the test stays open. */
|
||||
tap(&f, &t, &now, C4F_INPUT_BUTTON_CIRCLE);
|
||||
assert(t.active && !t.menu);
|
||||
c4fTesterClose(&t);
|
||||
assert(!f.pads[1].vibrating && f.pads[1].closes == 1);
|
||||
puts("PASS user choice from either controller, output to the chosen user, Back resumes testing");
|
||||
}
|
||||
|
||||
static void menuOpeningSafety(void)
|
||||
{
|
||||
Fake f;
|
||||
static C4fTester t;
|
||||
setup(&f, &t);
|
||||
/* Another controller held Down before the menu opened. It must neither
|
||||
* scroll by itself nor prevent fresh input on the opening controller. */
|
||||
f.pads[1].sample.buttons = C4F_INPUT_BUTTON_DOWN;
|
||||
uint64_t now = openMenu(&f, &t);
|
||||
frame(&f, &t, now + 10, STICKS | C4F_INPUT_BUTTON_RIGHT, 0);
|
||||
assert(!t.armed && t.focus == C4F_TESTER_USER);
|
||||
frame(&f, &t, now + 20, STICKS | C4F_INPUT_BUTTON_RIGHT | C4F_INPUT_BUTTON_CROSS, 0);
|
||||
assert(t.selected == 0 && t.menu);
|
||||
/* Releasing the opener arms activation even though Cross and another
|
||||
* controller's Down remain held. The old Cross still is not a fresh click. */
|
||||
frame(&f, &t, now + 30, C4F_INPUT_BUTTON_CROSS, 0);
|
||||
assert(t.armed && t.selected == 0);
|
||||
frame(&f, &t, now + 40, C4F_INPUT_BUTTON_CROSS, 0);
|
||||
assert(t.selected == 0);
|
||||
frame(&f, &t, now + 50, 0, 0);
|
||||
frame(&f, &t, now + 60, C4F_INPUT_BUTTON_CROSS, 0);
|
||||
assert(t.selected == 1 && t.focus == C4F_TESTER_USER);
|
||||
frame(&f, &t, now + 1000, C4F_INPUT_BUTTON_CROSS, 0);
|
||||
assert(t.selected == 1 && t.focus == C4F_TESTER_USER);
|
||||
c4fTesterClose(&t);
|
||||
|
||||
setup(&f, &t);
|
||||
now = openMenu(&f, &t);
|
||||
/* No neutral frame is required between the opening gesture and a new
|
||||
* direction: release L3/R3 and press Down in the same sample. */
|
||||
frame(&f, &t, now + 10, C4F_INPUT_BUTTON_DOWN, 0);
|
||||
assert(t.armed && t.focus == C4F_TESTER_RUMBLE_LARGE);
|
||||
frame(&f, &t, now + 20, 0, 0);
|
||||
frame(&f, &t, now + 30, C4F_INPUT_BUTTON_CROSS, 0);
|
||||
assert(f.pads[0].vibrating);
|
||||
c4fTesterClose(&t);
|
||||
|
||||
setup(&f, &t);
|
||||
f.pads[1].sample.buttons = STICKS;
|
||||
now = openMenu(&f, &t);
|
||||
frame(&f, &t, now + 10, C4F_INPUT_BUTTON_RIGHT, 0);
|
||||
assert(t.armed && t.focus == C4F_TESTER_USER); /* unrelated held sticks do not block */
|
||||
frame(&f, &t, now + 20, C4F_INPUT_BUTTON_CROSS, 0);
|
||||
assert(t.selected == 1);
|
||||
c4fTesterClose(&t);
|
||||
|
||||
setup(&f, &t);
|
||||
c4fTesterOpen(&t, 0);
|
||||
frame(&f, &t, 0, 0, 0);
|
||||
frame(&f, &t, 100, STICKS, 0);
|
||||
f.pads[1].sample.buttons = STICKS;
|
||||
frame(&f, &t, 1000, 0, 0);
|
||||
frame(&f, &t, 2100, 0, 0);
|
||||
assert(!t.menu); /* a different reader cannot inherit elapsed hold time */
|
||||
frame(&f, &t, 2999, 0, 0);
|
||||
assert(!t.menu);
|
||||
frame(&f, &t, 3000, 0, 0);
|
||||
assert(t.menu && t.menuOpeners == (1u << 1));
|
||||
c4fTesterClose(&t);
|
||||
puts("PASS menu release gate belongs only to its opener; fresh navigation survives; held activation never replays");
|
||||
}
|
||||
|
||||
static void gridNavigationAndRepeat(void)
|
||||
{
|
||||
Fake f;
|
||||
static C4fTester t;
|
||||
setup(&f, &t);
|
||||
uint64_t now = openMenu(&f, &t);
|
||||
assert(C4F_TESTER_MENU_COLUMNS == 4 && C4F_TESTER_MENU_ROWS == 2);
|
||||
frame(&f, &t, now + 10, 0, 0);
|
||||
frame(&f, &t, now + 20, C4F_INPUT_BUTTON_RIGHT, 0);
|
||||
assert(t.focus == C4F_TESTER_USER);
|
||||
frame(&f, &t, now + 369, C4F_INPUT_BUTTON_RIGHT, 0);
|
||||
assert(t.focus == C4F_TESTER_USER);
|
||||
frame(&f, &t, now + 370, C4F_INPUT_BUTTON_RIGHT, 0);
|
||||
assert(t.focus == C4F_TESTER_ORIENTATION);
|
||||
frame(&f, &t, now + 469, C4F_INPUT_BUTTON_RIGHT, 0);
|
||||
assert(t.focus == C4F_TESTER_ORIENTATION);
|
||||
frame(&f, &t, now + 470, C4F_INPUT_BUTTON_RIGHT, 0);
|
||||
assert(t.focus == C4F_TESTER_LIGHT_COLOR);
|
||||
frame(&f, &t, now + 570, C4F_INPUT_BUTTON_RIGHT, 0);
|
||||
assert(t.focus == C4F_TESTER_RESUME);
|
||||
frame(&f, &t, now + 670, C4F_INPUT_BUTTON_RIGHT, 0);
|
||||
assert(t.focus == C4F_TESTER_USER); /* held Right wraps within its row */
|
||||
frame(&f, &t, now + 680, 0, 0);
|
||||
frame(&f, &t, now + 690, C4F_INPUT_BUTTON_DOWN, 0);
|
||||
assert(t.focus == C4F_TESTER_RUMBLE_SMALL);
|
||||
frame(&f, &t, now + 1040, C4F_INPUT_BUTTON_DOWN, 0);
|
||||
assert(t.focus == C4F_TESTER_USER); /* held Down wraps within its column */
|
||||
frame(&f, &t, now + 1050, C4F_INPUT_BUTTON_LEFT, 0);
|
||||
assert(t.focus == C4F_TESTER_RESUME); /* direction change moves immediately */
|
||||
frame(&f, &t, now + 1060, C4F_INPUT_BUTTON_UP | C4F_INPUT_BUTTON_DOWN, 0);
|
||||
assert(t.focus == C4F_TESTER_RESUME);
|
||||
frame(&f, &t, now + 1070, C4F_INPUT_BUTTON_UP, 0);
|
||||
assert(t.focus == C4F_TESTER_RESUME); /* old held half is not a fresh press */
|
||||
frame(&f, &t, now + 1080, 0, 0);
|
||||
frame(&f, &t, now + 1090, C4F_INPUT_BUTTON_UP, 0);
|
||||
assert(t.focus == C4F_TESTER_RUMBLE_LARGE); /* top wraps to the bottom of its column */
|
||||
frame(&f, &t, now + 1100, 0, 0);
|
||||
frame(&f, &t, now + 1110, C4F_INPUT_BUTTON_UP, 0);
|
||||
assert(t.focus == C4F_TESTER_RESUME);
|
||||
now += 1120;
|
||||
frame(&f, &t, now, 0, 0);
|
||||
for (int i = 0; i < 4; i++) tap(&f, &t, &now, C4F_INPUT_BUTTON_LEFT);
|
||||
assert(t.focus == C4F_TESTER_RESUME);
|
||||
tap(&f, &t, &now, C4F_INPUT_BUTTON_LEFT);
|
||||
assert(t.focus == C4F_TESTER_LIGHT_COLOR); /* first column wraps to last */
|
||||
|
||||
/* Every tile has the same spatial neighbors for a controller tap and a
|
||||
* fresh HDMI-CEC direction. Row ends wrap within the row, never snake. */
|
||||
static const uint32_t directions[] = { C4F_INPUT_BUTTON_UP, C4F_INPUT_BUTTON_DOWN,
|
||||
C4F_INPUT_BUTTON_LEFT, C4F_INPUT_BUTTON_RIGHT };
|
||||
static const int neighbors[C4F_TESTER_ITEMS][4] = {
|
||||
{ 4, 4, 3, 1 }, { 5, 5, 0, 2 }, { 6, 6, 1, 3 }, { 7, 7, 2, 0 },
|
||||
{ 0, 0, 7, 5 }, { 1, 1, 4, 6 }, { 2, 2, 5, 7 }, { 3, 3, 6, 4 }
|
||||
};
|
||||
for (int item = 0; item < C4F_TESTER_ITEMS; item++) {
|
||||
for (int direction = 0; direction < 4; direction++) {
|
||||
t.focus = item;
|
||||
tap(&f, &t, &now, directions[direction]);
|
||||
assert(t.focus == neighbors[item][direction]);
|
||||
t.focus = item;
|
||||
frame(&f, &t, now += 10, 0, directions[direction]);
|
||||
assert(t.focus == neighbors[item][direction]);
|
||||
frame(&f, &t, now += 10, 0, 0);
|
||||
assert(t.focus == neighbors[item][direction]);
|
||||
}
|
||||
}
|
||||
c4fTesterSuspend(&t, now + 1000);
|
||||
assert(!t.menu && !t.menuOpeners);
|
||||
for (int s = 0; s < C4F_TESTER_USERS; s++) assert(!t.readers[s].repeatDirection && !t.readers[s].repeatAt);
|
||||
c4fTesterClose(&t);
|
||||
puts("PASS 4-by-2 spatial wrap for controller/TV remote, immediate taps, 350/100 ms hold repeat and reset on focus loss");
|
||||
}
|
||||
|
||||
static void colorPicker(void)
|
||||
{
|
||||
Fake f;
|
||||
static C4fTester t;
|
||||
setup(&f, &t);
|
||||
uint64_t now = openMenu(&f, &t);
|
||||
frame(&f, &t, now += 10, 0, 0);
|
||||
t.focus = C4F_TESTER_LIGHT_COLOR;
|
||||
frame(&f, &t, now += 10, C4F_INPUT_BUTTON_CROSS, 0);
|
||||
assert(t.colorMenu && !t.colorFine && f.pads[0].lit);
|
||||
assert(f.pads[0].rgb[0] == 255 && f.pads[0].rgb[1] == 255 && f.pads[0].rgb[2] == 255);
|
||||
frame(&f, &t, now += 500, C4F_INPUT_BUTTON_CROSS, 0);
|
||||
assert(!t.colorFine); /* The opening Cross is not also a precision toggle. */
|
||||
frame(&f, &t, now += 10, 0, 0);
|
||||
for (int i = 0; i < 18; i++) frame(&f, &t, now += 10, 0, C4F_INPUT_BUTTON_LEFT);
|
||||
assert(f.pads[0].rgb[0] == 0 && f.pads[0].rgb[1] == 255 && f.pads[0].rgb[2] == 255);
|
||||
frame(&f, &t, now += 10, 0, C4F_INPUT_BUTTON_CROSS);
|
||||
assert(t.colorFine);
|
||||
/* Every exact byte value can be selected by the TV remote, without a mouse. */
|
||||
for (int value = 1; value <= 255; value++) {
|
||||
frame(&f, &t, now += 10, 0, C4F_INPUT_BUTTON_RIGHT);
|
||||
assert(f.pads[0].rgb[0] == value);
|
||||
}
|
||||
frame(&f, &t, now += 10, 0, C4F_INPUT_BUTTON_RIGHT);
|
||||
assert(f.pads[0].rgb[0] == 255); /* clamp, never wrap */
|
||||
frame(&f, &t, now += 10, 0, C4F_INPUT_BUTTON_DOWN);
|
||||
assert(t.colorChannel == 1);
|
||||
for (int i = 0; i < 10; i++) frame(&f, &t, now += 10, 0, C4F_INPUT_BUTTON_LEFT);
|
||||
assert(f.pads[0].rgb[1] == 245 && f.pads[0].rgb[0] == 255);
|
||||
frame(&f, &t, now += 10, 0, C4F_INPUT_BUTTON_DOWN);
|
||||
assert(t.colorChannel == 2);
|
||||
for (int i = 0; i < 255; i++) frame(&f, &t, now += 10, 0, C4F_INPUT_BUTTON_LEFT);
|
||||
assert(f.pads[0].rgb[2] == 0 && !f.pads[1].lit);
|
||||
C4fTesterView v;
|
||||
c4fTesterView(&t, &v);
|
||||
assert(v.colorMenu && v.colorChannel == 2 && v.colorFine && v.lightSet);
|
||||
assert(v.light[0] == 255 && v.light[1] == 245 && v.light[2] == 0);
|
||||
frame(&f, &t, now += 10, 0, C4F_INPUT_BUTTON_CIRCLE);
|
||||
assert(t.menu && !t.colorMenu && f.pads[0].lit);
|
||||
frame(&f, &t, now += 10, 0, C4F_INPUT_BUTTON_CROSS);
|
||||
assert(t.colorMenu && t.colorChannel == 2 && f.pads[0].rgb[1] == 245);
|
||||
/* Hardware refusal never fabricates a successful preview or stored value. */
|
||||
f.pads[0].lightError = -5;
|
||||
frame(&f, &t, now += 10, 0, C4F_INPUT_BUTTON_RIGHT);
|
||||
assert(f.pads[0].rgb[2] == 0 && t.readers[0].light[2] == 0 && strstr(t.note, "refused"));
|
||||
f.pads[0].lightError = 0;
|
||||
frame(&f, &t, now += 10, 0, C4F_INPUT_BUTTON_RIGHT);
|
||||
assert(f.pads[0].rgb[2] == 1 && !t.note[0]);
|
||||
/* Held controller input keeps its normal repeat timing in the picker. */
|
||||
frame(&f, &t, now += 10, C4F_INPUT_BUTTON_RIGHT, 0);
|
||||
assert(f.pads[0].rgb[2] == 2);
|
||||
frame(&f, &t, now += 349, C4F_INPUT_BUTTON_RIGHT, 0);
|
||||
assert(f.pads[0].rgb[2] == 2);
|
||||
frame(&f, &t, ++now, C4F_INPUT_BUTTON_RIGHT, 0);
|
||||
assert(f.pads[0].rgb[2] == 3);
|
||||
frame(&f, &t, now += 10, 0, 0);
|
||||
frame(&f, &t, now += 10, 0, C4F_INPUT_BUTTON_CIRCLE);
|
||||
t.focus = C4F_TESTER_LIGHT_RESET;
|
||||
frame(&f, &t, now += 10, 0, C4F_INPUT_BUTTON_CROSS);
|
||||
assert(!t.readers[0].lightSet && !f.pads[0].lit);
|
||||
t.selected = 1;
|
||||
t.focus = C4F_TESTER_LIGHT_COLOR;
|
||||
frame(&f, &t, now += 10, 0, C4F_INPUT_BUTTON_CROSS);
|
||||
assert(t.colorMenu && f.pads[1].lit && !f.pads[0].lit);
|
||||
f.count = 1;
|
||||
frame(&f, &t, now = t.usersAt, 0, 0);
|
||||
assert(!t.colorMenu && !f.pads[1].lit && t.readers[1].state == C4F_TESTER_SIGNED_OUT);
|
||||
c4fTesterClose(&t);
|
||||
|
||||
setup(&f, &t);
|
||||
now = openMenu(&f, &t);
|
||||
frame(&f, &t, now += 10, 0, 0);
|
||||
t.focus = C4F_TESTER_LIGHT_COLOR;
|
||||
f.pads[0].lightError = -5;
|
||||
tap(&f, &t, &now, C4F_INPUT_BUTTON_CROSS);
|
||||
assert(!t.colorMenu && !t.readers[0].lightSet && !f.pads[0].lit);
|
||||
f.pads[0].lightError = 0;
|
||||
tap(&f, &t, &now, C4F_INPUT_BUTTON_CROSS);
|
||||
f.pads[0].connected = 0;
|
||||
frame(&f, &t, now += 10, 0, 0);
|
||||
assert(!t.colorMenu && !f.pads[0].lit && !t.readers[0].lightSet);
|
||||
c4fTesterClose(&t);
|
||||
puts("PASS RGB picker: all byte strengths, coarse/fine, controller repeat, remote control, selected user, refusal and cleanup");
|
||||
}
|
||||
|
||||
static void menuAvailability(void)
|
||||
{
|
||||
Fake f;
|
||||
static C4fTester t;
|
||||
setup(&f, &t);
|
||||
f.count = 1;
|
||||
c4fTesterOpen(&t, 0);
|
||||
frame(&f, &t, 0, 0, 0);
|
||||
C4fTesterView view;
|
||||
c4fTesterView(&t, &view);
|
||||
assert(c4fTesterItemAvailable(&view, C4F_TESTER_RESUME));
|
||||
assert(c4fTesterItemAvailable(&view, C4F_TESTER_EXIT));
|
||||
assert(!c4fTesterItemAvailable(&view, C4F_TESTER_USER));
|
||||
assert(c4fTesterItemAvailable(&view, C4F_TESTER_ORIENTATION));
|
||||
t.menu = t.armed = 1; t.focus = C4F_TESTER_USER;
|
||||
frame(&f, &t, 10, C4F_INPUT_BUTTON_CROSS, 0);
|
||||
assert(t.selected == 0 && strstr(t.note, "No other user"));
|
||||
frame(&f, &t, 20, 0, 0);
|
||||
f.pads[0].connected = 0;
|
||||
frame(&f, &t, 30, 0, 0);
|
||||
c4fTesterView(&t, &view);
|
||||
assert(!c4fTesterItemAvailable(&view, C4F_TESTER_RUMBLE_LARGE));
|
||||
assert(!c4fTesterItemAvailable(&view, C4F_TESTER_LIGHT_COLOR));
|
||||
assert(c4fTesterItemAvailable(&view, C4F_TESTER_RESUME));
|
||||
assert(c4fTesterItemAvailable(&view, C4F_TESTER_EXIT));
|
||||
/* A signed-out user may stay selected for its status screen. The one
|
||||
* remaining signed-in user must still be available as a switch target. */
|
||||
view.user[0].state = C4F_TESTER_SIGNED_OUT;
|
||||
view.user[1].used = 1;
|
||||
view.user[1].state = C4F_TESTER_READY;
|
||||
assert(c4fTesterItemAvailable(&view, C4F_TESTER_USER));
|
||||
c4fTesterClose(&t);
|
||||
puts("PASS menu availability matches activation: user count, disconnected hardware tools, Resume and Exit");
|
||||
}
|
||||
|
||||
static void failures(void)
|
||||
{
|
||||
Fake f;
|
||||
static C4fTester t;
|
||||
setup(&f, &t);
|
||||
c4fTesterOpen(&t, 0);
|
||||
frame(&f, &t, 0, 0, 0);
|
||||
/* A read failure releases the handle and retries a second later. */
|
||||
f.pads[1].readError = (int)0x80920003u;
|
||||
frame(&f, &t, 10, 0, 0);
|
||||
assert(t.readers[1].state == C4F_TESTER_FAILED && t.readers[1].handle < 0 && f.pads[1].closes == 1);
|
||||
f.pads[1].readError = 0;
|
||||
frame(&f, &t, 1009, 0, 0);
|
||||
assert(f.pads[1].opens == 1);
|
||||
frame(&f, &t, 1010, 0, 0);
|
||||
assert(f.pads[1].opens == 2 && t.readers[1].state == C4F_TESTER_READY);
|
||||
/* An open failure is shown with its code and retried. */
|
||||
c4fTesterClose(&t);
|
||||
setup(&f, &t);
|
||||
f.pads[1].openError = (int)0x80920001u;
|
||||
c4fTesterOpen(&t, 0);
|
||||
frame(&f, &t, 0, 0, 0);
|
||||
C4fTesterView v;
|
||||
c4fTesterView(&t, &v);
|
||||
assert(v.user[1].state == C4F_TESTER_FAILED && v.user[1].error == 0x80920001u);
|
||||
f.pads[1].openError = 0;
|
||||
frame(&f, &t, 1000, 0, 0);
|
||||
assert(t.readers[1].state == C4F_TESTER_READY);
|
||||
/* No controller: shown as such, the handle kept, nothing pressed. */
|
||||
f.pads[0].connected = 0;
|
||||
frame(&f, &t, 1010, C4F_INPUT_BUTTON_CROSS, 0);
|
||||
c4fTesterView(&t, &v);
|
||||
assert(v.user[0].state == C4F_TESTER_NO_CONTROLLER && !v.buttons && t.readers[0].handle >= 0);
|
||||
/* An action needs a controller to act on. */
|
||||
f.pads[0].connected = 1;
|
||||
frame(&f, &t, 1020, 0, 0);
|
||||
f.pads[0].connected = 0;
|
||||
t.menu = t.armed = 1;
|
||||
t.focus = C4F_TESTER_RUMBLE_LARGE;
|
||||
frame(&f, &t, 1030, 0, 0);
|
||||
f.pads[1].sample.buttons = C4F_INPUT_BUTTON_CROSS;
|
||||
frame(&f, &t, 1040, 0, 0);
|
||||
assert(!f.pads[0].vibrating && strstr(t.note, "no controller"));
|
||||
f.pads[1].sample.buttons = 0;
|
||||
c4fTesterClose(&t);
|
||||
puts("PASS read/open failures retried after a second, no-controller state, actions need a controller");
|
||||
}
|
||||
|
||||
static void logout(void)
|
||||
{
|
||||
Fake f;
|
||||
static C4fTester t;
|
||||
setup(&f, &t);
|
||||
f.count = 3; f.users[2] = 3;
|
||||
c4fTesterOpen(&t, 0);
|
||||
frame(&f, &t, 0, 0, 0);
|
||||
t.selected = 1;
|
||||
/* The user on screen signs out: kept, marked, its handle closed. */
|
||||
f.count = 2; f.users[1] = 3;
|
||||
frame(&f, &t, 1000, 0, 0);
|
||||
assert(t.selected == 1 && t.readers[1].state == C4F_TESTER_SIGNED_OUT && f.pads[1].closes == 1);
|
||||
C4fTesterView v;
|
||||
c4fTesterView(&t, &v);
|
||||
assert(v.user[1].used && v.user[1].state == C4F_TESTER_SIGNED_OUT);
|
||||
assert(v.user[v.selected].id == 2);
|
||||
/* Signs in again: read again. */
|
||||
f.count = 3; f.users[1] = 2; f.users[2] = 3;
|
||||
frame(&f, &t, 2000, 0, 0);
|
||||
frame(&f, &t, 2001, 0, 0);
|
||||
assert(t.readers[1].state == C4F_TESTER_READY && f.pads[1].opens == 2);
|
||||
c4fTesterView(&t, &v);
|
||||
assert(v.user[v.selected].id == 2);
|
||||
/* A user not on screen signs out: dropped at once. */
|
||||
f.count = 2;
|
||||
frame(&f, &t, 3000, 0, 0);
|
||||
assert(t.readers[2].user < 0 && f.pads[2].closes == 1);
|
||||
c4fTesterView(&t, &v);
|
||||
assert(!v.user[2].used && v.user[2].id == -1);
|
||||
/* A failed user list keeps what is known. */
|
||||
f.listError = -1;
|
||||
frame(&f, &t, 4000, 0, 0);
|
||||
assert(t.readers[0].user == 1 && t.readers[1].user == 2);
|
||||
c4fTesterClose(&t);
|
||||
puts("PASS sign-out keeps the user on screen as signed out; others dropped; sign-in reads again");
|
||||
}
|
||||
|
||||
static void remoteAndSuspend(void)
|
||||
{
|
||||
Fake f;
|
||||
static C4fTester t;
|
||||
setup(&f, &t);
|
||||
c4fTesterOpen(&t, 0);
|
||||
frame(&f, &t, 0, 0, 0);
|
||||
/* The remote's OK opens the menu, its Back closes it, and Back again leaves. */
|
||||
frame(&f, &t, 10, 0, C4F_INPUT_BUTTON_CROSS);
|
||||
assert(t.menu);
|
||||
frame(&f, &t, 20, 0, 0);
|
||||
assert(t.armed);
|
||||
frame(&f, &t, 30, 0, C4F_INPUT_BUTTON_RIGHT);
|
||||
assert(t.focus == C4F_TESTER_USER);
|
||||
frame(&f, &t, 40, 0, C4F_INPUT_BUTTON_CIRCLE);
|
||||
assert(t.active && !t.menu);
|
||||
/* A DS4's Circle is under test and does not leave. */
|
||||
frame(&f, &t, 50, C4F_INPUT_BUTTON_CIRCLE, 0);
|
||||
assert(t.active && !t.menu);
|
||||
/* Losing focus stops output and reopens every reader. */
|
||||
t.menu = t.armed = 1;
|
||||
t.focus = C4F_TESTER_RUMBLE_LARGE;
|
||||
frame(&f, &t, 60, 0, 0);
|
||||
frame(&f, &t, 70, C4F_INPUT_BUTTON_CROSS, 0);
|
||||
t.focus = C4F_TESTER_LIGHT_COLOR;
|
||||
frame(&f, &t, 80, 0, 0);
|
||||
frame(&f, &t, 90, C4F_INPUT_BUTTON_CROSS, 0);
|
||||
assert(f.pads[0].vibrating && f.pads[0].lit && f.pads[0].rgb[2] == 255);
|
||||
c4fTesterSuspend(&t, 5000);
|
||||
assert(!f.pads[0].vibrating && !f.pads[0].lit && !t.menu && t.active);
|
||||
assert(t.readers[0].handle < 0 && t.readers[1].handle < 0 && f.pads[1].closes == 1);
|
||||
frame(&f, &t, 5000, 0, 0);
|
||||
assert(t.readers[0].state == C4F_TESTER_READY && t.readers[1].state == C4F_TESTER_READY);
|
||||
assert(!frame(&f, &t, 5010, 0, C4F_INPUT_BUTTON_CIRCLE));
|
||||
assert(!t.active && f.pads[1].closes == 2);
|
||||
puts("PASS TV remote opens/closes the menu and leaves; focus loss stops output and reopens readers");
|
||||
}
|
||||
|
||||
static void launcherEntry(void)
|
||||
{
|
||||
C4fLauncherScreen screen;
|
||||
memset(&screen, 0, sizeof(screen));
|
||||
screen.autorun = C4F_AUTORUN_ON;
|
||||
assert(c4fActionAvailable(&screen, C4F_ACTION_TESTER, 0));
|
||||
assert(!strcmp(c4fActionLabel(&screen, C4F_ACTION_TESTER), "Test controllers"));
|
||||
C4fLauncherAction actions[8];
|
||||
int count = c4fActionList(&screen, 0, actions, 8), found = 0;
|
||||
for (int i = 0; i < count; i++) found |= actions[i] == C4F_ACTION_TESTER;
|
||||
assert(found && actions[count - 1] == C4F_ACTION_CLOSE);
|
||||
screen.busy = 1;
|
||||
assert(c4fActionAvailable(&screen, C4F_ACTION_TESTER, 0));
|
||||
screen.locked = 1;
|
||||
assert(!c4fActionAvailable(&screen, C4F_ACTION_TESTER, 0));
|
||||
for (int i = 0; i < C4F_TESTER_ITEMS; i++) assert(c4fTesterItemLabel(i)[0]);
|
||||
/* A DS4's Options opens it straight from the launcher, even while busy; the
|
||||
* diagnostic build keeps Options for its kernel-log capture. */
|
||||
memset(&screen, 0, sizeof(screen));
|
||||
screen.autorun = C4F_AUTORUN_ON;
|
||||
uint32_t standard, remote;
|
||||
assert(c4fNavigate(&screen, C4F_INPUT_BUTTON_OPTIONS, 0, 0, &standard, &remote) == C4F_ACTION_TESTER && !standard);
|
||||
assert(c4fNavigate(&screen, C4F_INPUT_BUTTON_OPTIONS, 0, 1, &standard, &remote) == C4F_ACTION_NONE &&
|
||||
standard == C4F_INPUT_BUTTON_OPTIONS);
|
||||
screen.busy = 1;
|
||||
assert(c4fNavigate(&screen, C4F_INPUT_BUTTON_OPTIONS, 0, 0, &standard, &remote) == C4F_ACTION_TESTER);
|
||||
screen.locked = 1;
|
||||
assert(c4fNavigate(&screen, C4F_INPUT_BUTTON_OPTIONS, 0, 0, &standard, &remote) == C4F_ACTION_NONE);
|
||||
puts("PASS Test controllers: in Actions unless locked, Options opens it, every menu item has a label");
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
holdTiming();
|
||||
readsUsers();
|
||||
menuActions();
|
||||
switchUsers();
|
||||
menuOpeningSafety();
|
||||
gridNavigationAndRepeat();
|
||||
colorPicker();
|
||||
menuAvailability();
|
||||
failures();
|
||||
logout();
|
||||
remoteAndSuspend();
|
||||
launcherEntry();
|
||||
return 0;
|
||||
}
|
||||
@@ -48,6 +48,26 @@ for (let byte = 0; byte <= 255; byte++) {
|
||||
const result = M.toSource(M.fromSource(original));
|
||||
assert.equal(result.lx, byte); assert.equal(result.l2, byte);
|
||||
}
|
||||
// HID reports carry independent digital trigger flags and pressure bytes. A
|
||||
// pressed flag must not turn a partial analog pull into full pressure.
|
||||
for (let byte = 0; byte <= 255; byte++) {
|
||||
const hid = { buttons: B.L2 | B.R2, lx: 128, ly: 128, rx: 128, ry: 128, l2: byte, r2: 255 - byte };
|
||||
const result = M.toSource(M.fromSource(hid, true));
|
||||
assert.equal(result.l2, byte, 'HID L2 pressure with its digital flag set');
|
||||
assert.equal(result.r2, 255 - byte, 'HID R2 pressure with its digital flag set');
|
||||
assert.equal(result.buttons, B.L2 | B.R2, 'HID digital flags remain independent');
|
||||
}
|
||||
const triggerSource = { buttons: B.L2 | B.R2, lx: 128, ly: 128, rx: 128, ry: 128, l2: 64, r2: 160 };
|
||||
const digitalTriggers = M.toSource(M.fromSource({ ...triggerSource, l2: 0, r2: 0 }));
|
||||
assert.equal(digitalTriggers.l2, 255, 'touch and keyboard L2 stay digital');
|
||||
assert.equal(digitalTriggers.r2, 255, 'touch and keyboard R2 stay digital');
|
||||
const triggerMerge = M.toSource(M.merge([M.fromSource(triggerSource, true),
|
||||
M.fromSource({ ...triggerSource, l2: 192, r2: 32 }, true)]));
|
||||
assert.equal(triggerMerge.l2, 192); assert.equal(triggerMerge.r2, 160);
|
||||
const mixedTrigger = M.toSource(M.merge([M.fromSource(triggerSource, true),
|
||||
M.fromSource({ ...triggerSource, buttons: B.L2, l2: 0, r2: 0 })]));
|
||||
assert.equal(mixedTrigger.l2, 255); assert.equal(mixedTrigger.r2, 160);
|
||||
console.log('PASS HID trigger pressure with digital flags, digital touch/keys and mixed-source maximum');
|
||||
|
||||
p = fresh(); p.calibration = { 0: { type: 'center', min: -.8, center: .2, max: .9 },
|
||||
1: { type: 'pedal', released: -1, full: 1 }, 2: { type: 'pedal', released: 1, full: -1 } };
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
"""The vendored Mbed TLS, built once for the host tests: build/mbedtls-host/libmbedtls.a.
|
||||
It is rebuilt only when a source or the configuration is newer than the library."""
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
LIBRARY = ROOT / 'build/mbedtls-host/libmbedtls.a'
|
||||
FLAGS = ['-Iinclude', '-Ivendor/mbedtls/include', '-DMBEDTLS_CONFIG_FILE=<c4f_mbedtls_config.h>']
|
||||
|
||||
|
||||
def library():
|
||||
sources = sorted((ROOT / 'vendor/mbedtls/library').glob('*.c'))
|
||||
inputs = sources + list((ROOT / 'vendor/mbedtls').rglob('*.h')) + [ROOT / 'include/c4f_mbedtls_config.h']
|
||||
newest = max(p.stat().st_mtime for p in inputs)
|
||||
if LIBRARY.exists() and LIBRARY.stat().st_mtime >= newest:
|
||||
return str(LIBRARY)
|
||||
LIBRARY.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def compile_one(source):
|
||||
target = LIBRARY.parent / (source.stem + '.o')
|
||||
subprocess.run(['clang-18', '-std=gnu11', '-O2', '-g', *FLAGS, '-c', str(source), '-o', str(target)],
|
||||
check=True, cwd=ROOT)
|
||||
return str(target)
|
||||
|
||||
with ThreadPoolExecutor() as pool:
|
||||
objects = list(pool.map(compile_one, sources))
|
||||
LIBRARY.unlink(missing_ok=True)
|
||||
subprocess.run(['ar', 'rcs', str(LIBRARY), *objects], check=True)
|
||||
return str(LIBRARY)
|
||||
@@ -0,0 +1,53 @@
|
||||
// Browser integration stays on loopback for secure-context APIs (WebHID and
|
||||
// motion). Only the SDP exchange goes through the container's real interface:
|
||||
// Chromium excludes remote loopback candidates from WebRTC connectivity.
|
||||
const os = require('node:os');
|
||||
const fs = require('node:fs');
|
||||
const address = Object.values(os.networkInterfaces()).flat().find(i => i.family === 'IPv4' && !i.internal)?.address;
|
||||
const serviceBinary = '/src/build/web-rtc-host-test';
|
||||
|
||||
async function relayOffer(page) {
|
||||
if (!address) throw new Error('The RTC browser test needs a non-loopback container interface');
|
||||
await page.route('http://127.0.0.1:4264/rtc', async route => {
|
||||
const host = address + ':4264';
|
||||
const original = route.request().headers();
|
||||
const headers = { ...original, host, origin: 'http://' + host };
|
||||
const response = await route.fetch({ url: 'http://' + host + '/rtc', headers });
|
||||
await route.fulfill({ response, headers: { ...response.headers(), 'access-control-allow-origin': original.origin || '*' } });
|
||||
});
|
||||
}
|
||||
|
||||
// Keep the existing controller-behavior assertions readable while recording the
|
||||
// actual wire messages separately for sequence/history/channel assertions.
|
||||
function recordRtcMessages() {
|
||||
window.rtcMessages = [];
|
||||
window.controllerMessages = [];
|
||||
window.testClaims = [];
|
||||
window.testInputs = [];
|
||||
const dataSend = RTCDataChannel.prototype.send;
|
||||
RTCDataChannel.prototype.send = function(data) {
|
||||
try {
|
||||
const message = JSON.parse(data);
|
||||
rtcMessages.push({ channel: this.id, message });
|
||||
let normalized = message;
|
||||
if (message.method === 'i') {
|
||||
const p = message.params;
|
||||
normalized = { ...message, method: 'u', params: [p[0], ...p.slice(2, 10 + 3 * p[9])] };
|
||||
} else if (message.method === 'm') {
|
||||
normalized = { ...message, params: [message.params[0], ...message.params.slice(2)] };
|
||||
}
|
||||
controllerMessages.push(normalized);
|
||||
if (normalized.method === 'claim') testClaims.push(normalized.params);
|
||||
if (normalized.method === 'u') testInputs.push(normalized.params);
|
||||
} catch {}
|
||||
return dataSend.call(this, data);
|
||||
};
|
||||
}
|
||||
|
||||
function copyService(destination) {
|
||||
if (!fs.existsSync(serviceBinary)) throw new Error('Build the production RTC host service with tests/run.py first');
|
||||
fs.copyFileSync(serviceBinary, destination);
|
||||
fs.chmodSync(destination, 0o755);
|
||||
}
|
||||
|
||||
module.exports = { address, relayOffer, recordRtcMessages, copyService };
|
||||
@@ -0,0 +1,101 @@
|
||||
// The WebRTC transport against real browsers: Chromium and Firefox open the two
|
||||
// data channels to the service (build/web-rtc-host-test, the PS4 stubbed) and talk
|
||||
// to it. The production-only HTTP surface refuses WebSocket. The page is
|
||||
// opened by the container's own address, not 127.0.0.1:
|
||||
// browsers leave loopback out of WebRTC. Run in the Playwright Docker image:
|
||||
// NODE_PATH=… node tests/rtc_e2e.cjs
|
||||
const pw = require('playwright');
|
||||
const fs = require('node:fs'), os = require('node:os'), assert = require('node:assert/strict'), { spawn } = require('node:child_process');
|
||||
|
||||
const address = Object.values(os.networkInterfaces()).flat().find(i => i.family === 'IPv4' && !i.internal).address;
|
||||
fs.copyFileSync('/src/build/web-rtc-host-test', '/tmp/c4f-rtc-service');
|
||||
fs.chmodSync('/tmp/c4f-rtc-service', 0o755);
|
||||
|
||||
let server, log = '';
|
||||
function startService(env = {}) {
|
||||
server = spawn('/tmp/c4f-rtc-service', [], { env: { ...process.env, C4F_TEST_KLOG_FD: '0', ...env }, stdio: ['pipe', 'pipe', 'pipe'] });
|
||||
server.stderr.on('data', d => { log += d; });
|
||||
server.stdout.on('data', d => { log += d; });
|
||||
}
|
||||
async function stopService() {
|
||||
const old = server;
|
||||
if (!old) return;
|
||||
server = null;
|
||||
old.kill();
|
||||
await new Promise(done => old.exitCode !== null || old.signalCode !== null ? done() : old.once('exit', done));
|
||||
}
|
||||
|
||||
// In the page: an offer to POST /rtc, the two negotiated channels, then a few
|
||||
// round trips on each. Returns what happened.
|
||||
async function talk(page, { echoes = 20 } = {}) {
|
||||
return page.evaluate(async ({ echoes }) => {
|
||||
const pc = new RTCPeerConnection();
|
||||
try {
|
||||
const control = pc.createDataChannel('control', { negotiated: true, id: 0 });
|
||||
const input = pc.createDataChannel('input', { negotiated: true, id: 1, ordered: false, maxRetransmits: 0 });
|
||||
const pushed = [];
|
||||
const pending = new Map();
|
||||
control.onmessage = ev => {
|
||||
const m = JSON.parse(ev.data);
|
||||
if (m.id && pending.has(m.id)) { pending.get(m.id)(m); pending.delete(m.id); } else pushed.push(m);
|
||||
};
|
||||
const started = performance.now();
|
||||
await pc.setLocalDescription(await pc.createOffer());
|
||||
const reply = await fetch('/rtc', { method: 'POST', headers: { 'Content-Type': 'text/plain' }, body: pc.localDescription.sdp });
|
||||
if (!reply.ok) return { error: 'offer refused: ' + reply.status + ' ' + await reply.text() };
|
||||
const answer = await reply.text();
|
||||
await pc.setRemoteDescription({ type: 'answer', sdp: answer });
|
||||
const open = ch => new Promise((done, fail) => {
|
||||
if (ch.readyState === 'open') return done();
|
||||
ch.onopen = done; ch.onerror = fail;
|
||||
setTimeout(() => fail(new Error(ch.label + ' did not open: ' + pc.connectionState + '/' + pc.iceConnectionState)), 10000);
|
||||
});
|
||||
try { await Promise.all([open(control), open(input)]); } catch (e) { return { error: e.message, answer }; }
|
||||
const openedMs = performance.now() - started;
|
||||
const call = (id, method, params = []) => new Promise((done, fail) => {
|
||||
const timer = setTimeout(() => { pending.delete(id); fail(new Error(method + ' reply timed out')); }, 5000);
|
||||
pending.set(id, value => { clearTimeout(timer); done(value); });
|
||||
control.send(JSON.stringify({ id, method, params }));
|
||||
});
|
||||
const info = await call(1, 'info');
|
||||
// The service's first push is the status, sent the moment the channel opened.
|
||||
await new Promise(done => setTimeout(done, 200));
|
||||
const status = pushed.find(m => m.method === 's');
|
||||
// Echoes on the unordered channel.
|
||||
const back = new Map();
|
||||
input.onmessage = ev => { const m = JSON.parse(ev.data); if (m.method === 'e') back.set(m.params[0], performance.now() - m.params[1]); };
|
||||
for (let i = 1; i <= echoes; i++) { input.send(JSON.stringify({ method: 'e', params: [i, Math.round(performance.now())] })); await new Promise(d => setTimeout(d, 20)); }
|
||||
await new Promise(done => setTimeout(done, 500));
|
||||
// A big reply (the invite QR code) comes in pieces over the reliable channel.
|
||||
const invite = await call(2, 'invite', []);
|
||||
return { openedMs, info: info.result, status: status && status.params, echoes: back.size, invite: invite.result || invite.error };
|
||||
} finally { pc.close(); }
|
||||
}, { echoes });
|
||||
}
|
||||
|
||||
(async () => {
|
||||
startService();
|
||||
try {
|
||||
const results = {};
|
||||
for (const name of ['chromium', 'firefox']) {
|
||||
const browser = await pw[name].launch(name === 'firefox' ? { firefoxUserPrefs: { 'media.peerconnection.ice.loopback': true } } : {});
|
||||
try {
|
||||
const page = await browser.newPage();
|
||||
page.on('pageerror', e => console.error(name, 'page error:', e.message));
|
||||
for (let tries = 0; ; tries++) {
|
||||
try { await page.goto(`http://${address}:4264/`); break; } catch (e) { if (tries > 50) throw e; await new Promise(d => setTimeout(d, 100)); }
|
||||
}
|
||||
const r = await talk(page);
|
||||
results[name] = r;
|
||||
if (r.error) { console.error(log); throw new Error(name + ': ' + r.error + '\n' + (r.answer || '')); }
|
||||
assert.equal(r.info.protocol, 3, name);
|
||||
assert.equal(r.info.input, true, name);
|
||||
assert.ok(r.status && r.status.pads.length === 4, name + ': status pushed on open');
|
||||
assert.equal(r.echoes, 20, name + ': healthy-link echoes');
|
||||
assert.ok(r.invite && r.invite.size > 20, name + ': invite ' + JSON.stringify(r.invite));
|
||||
console.log(`PASS rtc ${name}: open in ${r.openedMs.toFixed(0)} ms, info, status push, ${r.echoes}/20 echoes, big reply in pieces`);
|
||||
} finally { await browser.close(); }
|
||||
}
|
||||
if (!/rtc: peer \d+ open/.test(log)) { console.error(log); throw new Error('the service did not log an open peer'); }
|
||||
} finally { await stopService(); }
|
||||
})().catch(e => { console.error(e); process.exit(1); });
|
||||
@@ -0,0 +1,199 @@
|
||||
// Real production RTC application behavior through a controllable local UDP
|
||||
// relay. All sockets stay inside the Docker test container; no console access.
|
||||
const { chromium } = require('playwright');
|
||||
const assert = require('node:assert/strict');
|
||||
const dgram = require('node:dgram');
|
||||
const { spawn } = require('node:child_process');
|
||||
const { address, copyService, relayOffer } = require('./rtc_browser_helpers.cjs');
|
||||
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
async function relay() {
|
||||
const down = dgram.createSocket('udp4'), up = dgram.createSocket('udp4');
|
||||
await Promise.all([new Promise(resolve => down.bind(0, '0.0.0.0', resolve)),
|
||||
new Promise(resolve => up.bind(0, '0.0.0.0', resolve))]);
|
||||
let client, mode = 'pass';
|
||||
const held = [];
|
||||
function forward(socket, data, port, host) {
|
||||
if (mode === 'drop') return;
|
||||
const deliver = () => socket.send(data, port, host);
|
||||
if (mode === 'hold') held.push(deliver); else deliver();
|
||||
}
|
||||
down.on('message', (data, from) => {
|
||||
client = from;
|
||||
forward(up, Buffer.from(data), 4264, address);
|
||||
});
|
||||
up.on('message', data => { if (client) forward(down, Buffer.from(data), client.port, client.address); });
|
||||
return { port: down.address().port,
|
||||
mode(value) { mode = value; },
|
||||
release() { for (const deliver of held.splice(0)) deliver(); },
|
||||
async close() { held.length = 0; await Promise.all([new Promise(r => down.close(r)), new Promise(r => up.close(r))]); }
|
||||
};
|
||||
}
|
||||
|
||||
async function connect(page, port) {
|
||||
return page.evaluate(async port => {
|
||||
const pc = new RTCPeerConnection({ iceServers: [] });
|
||||
const control = pc.createDataChannel('control', { negotiated: true, id: 0 });
|
||||
const input = pc.createDataChannel('input', { negotiated: true, id: 1, ordered: false, maxRetransmits: 0 });
|
||||
const pending = new Map(), pushes = [];
|
||||
let nextId = 1;
|
||||
control.onmessage = event => {
|
||||
const msg = JSON.parse(event.data), request = pending.get(msg.id);
|
||||
if (request) { clearTimeout(request.timer); pending.delete(msg.id); request.resolve(msg); }
|
||||
else pushes.push(msg);
|
||||
};
|
||||
input.onmessage = event => pushes.push(JSON.parse(event.data));
|
||||
function request(method, params = [], onInput = false) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const id = nextId++;
|
||||
const timer = setTimeout(() => { pending.delete(id); reject(new Error('Request timed out: ' + method)); }, 5000);
|
||||
pending.set(id, { resolve, timer });
|
||||
(onInput ? input : control).send(JSON.stringify({ id, method, params }));
|
||||
});
|
||||
}
|
||||
await pc.setLocalDescription(await pc.createOffer());
|
||||
const response = await fetch('/rtc', { method: 'POST', headers: { 'Content-Type': 'text/plain' }, body: pc.localDescription.sdp });
|
||||
if (!response.ok) throw new Error('Offer refused: ' + response.status);
|
||||
let answer = await response.text();
|
||||
if (port) answer = answer.replace(/(a=candidate:\S+ \d+ \S+ \d+ \S+ )\d+/g, (_match, prefix) => prefix + port);
|
||||
await pc.setRemoteDescription({ type: 'answer', sdp: answer });
|
||||
await Promise.all([control, input].map(channel => new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => reject(new Error('Channel did not open')), 10000);
|
||||
if (channel.readyState === 'open') { clearTimeout(timer); resolve(); }
|
||||
else channel.onopen = () => { clearTimeout(timer); resolve(); };
|
||||
})));
|
||||
window.rtcInputTest = { pc, control, input, pushes, request,
|
||||
state(seq, buttons = 0, lx = 128, history = []) {
|
||||
input.send(JSON.stringify({ method: 'i', params: [0, seq, buttons, lx, 128, 128, 128, 0, 0, 0, history.length, ...history.flat()] }));
|
||||
},
|
||||
motion(seq, acceleration) {
|
||||
input.send(JSON.stringify({ method: 'm', params: [0, seq, 0, 0, 0, 10000, acceleration, 0, 0, 0, 0, 0] }));
|
||||
}
|
||||
};
|
||||
return request('info');
|
||||
}, port);
|
||||
}
|
||||
|
||||
(async () => {
|
||||
let service, browser, proxy;
|
||||
let trace = '', partial = '';
|
||||
const frames = [];
|
||||
const frameAt = (buttons, lx = undefined) => rows => rows.some(row => +row[3] === buttons && (lx === undefined || +row[4] === lx));
|
||||
async function waitFrames(start, condition, timeout = 2500) {
|
||||
const deadline = Date.now() + timeout;
|
||||
while (!condition(frames.slice(start))) {
|
||||
if (Date.now() > deadline) throw new Error('Expected controller report not seen\n' + trace.slice(-5000));
|
||||
await sleep(10);
|
||||
}
|
||||
}
|
||||
try {
|
||||
if (!address) throw new Error('Run the browser checks with a Docker network interface; --network none has only loopback');
|
||||
copyService('/tmp/c4f-rtc-input-service');
|
||||
service = spawn('/tmp/c4f-rtc-input-service', [], { env: { ...process.env, C4F_TEST_KLOG_FD: '0' }, stdio: ['pipe', 'pipe', 'pipe'] });
|
||||
service.stdout.on('data', data => {
|
||||
trace += data; partial += data;
|
||||
const lines = partial.split('\n'); partial = lines.pop();
|
||||
for (const line of lines) if (line.startsWith('FRAME ')) frames.push(line.split(' '));
|
||||
});
|
||||
service.stderr.on('data', data => { trace += data; });
|
||||
browser = await chromium.launch();
|
||||
const context = await browser.newContext();
|
||||
await context.grantPermissions(['local-network-access']);
|
||||
const page = await context.newPage();
|
||||
await relayOffer(page);
|
||||
// Supply an empty same-origin document: this test owns the RTC peers.
|
||||
await page.route('http://127.0.0.1:4264/', async route => route.fulfill({ response: await route.fetch(), body: '<!doctype html><title>RTC input test</title>' }));
|
||||
await page.goto('http://127.0.0.1:4264/');
|
||||
for (let tries = 0; ; tries++) {
|
||||
try { if ((await page.request.get(`http://${address}:4264/`)).ok()) break; }
|
||||
catch (error) { if (tries > 50) throw error; }
|
||||
await sleep(100);
|
||||
}
|
||||
proxy = await relay();
|
||||
const info = await connect(page, proxy.port);
|
||||
assert.equal(info.result.protocol, 3);
|
||||
const claim = await page.evaluate(() => rtcInputTest.request('claim', [0]));
|
||||
assert.equal(claim.result.pads[0].mine, true);
|
||||
let start = frames.length;
|
||||
await page.evaluate(() => rtcInputTest.state(1));
|
||||
await waitFrames(start, frameAt(0));
|
||||
|
||||
// The press and release packets were absent, but their bounded history
|
||||
// repairs a quick tap. Analog state is current, not reconstructed history.
|
||||
start = frames.length;
|
||||
await page.evaluate(() => rtcInputTest.state(4, 0, 17, [[2, 0x4000], [3, 0]]));
|
||||
await waitFrames(start, rows => frameAt(0x4000, 17)(rows) && frameAt(0, 17)(rows));
|
||||
await page.evaluate(() => { rtcInputTest.state(3, 0x2000); rtcInputTest.state(4, 0x2000); });
|
||||
await sleep(80);
|
||||
assert.ok(frames.slice(start).every(row => +row[3] !== 0x2000), 'late or duplicate state restored an old button');
|
||||
|
||||
const wrongControl = await page.evaluate(() => rtcInputTest.request('ping', [], true));
|
||||
assert.equal(wrongControl.error.code, 400);
|
||||
const wrongInput = await page.evaluate(() => rtcInputTest.request('i', [0, 5, 0, 128, 128, 128, 128, 0, 0, 0, 0]));
|
||||
assert.equal(wrongInput.error.code, 400);
|
||||
const malformedHistory = await page.evaluate(() => rtcInputTest.request('i', [0, 5, 0, 128, 128, 128, 128, 0, 0, 0, 2, 4, 0, 3, 0], true));
|
||||
assert.equal(malformedHistory.error.code, 400);
|
||||
console.log('PASS RTC input: explicit claim, recovered tap, ordered history, duplicate/late rejection, channel separation');
|
||||
|
||||
start = frames.length;
|
||||
await page.evaluate(() => { rtcInputTest.motion(10, 1000); rtcInputTest.motion(9, -1000); rtcInputTest.state(5, 0, 33); });
|
||||
await waitFrames(start, rows => rows.some(row => +row[4] === 33 && +row[15] === 1));
|
||||
assert.ok(frames.slice(start).every(row => +row[15] !== -1), 'old motion overtook new motion');
|
||||
|
||||
// A genuinely held datagram can arrive after a newer one. No retransmit
|
||||
// setting can remove it from this queue; receiver sequences must reject it.
|
||||
proxy.mode('hold');
|
||||
await page.evaluate(() => rtcInputTest.state(6, 0x2000));
|
||||
await sleep(100);
|
||||
proxy.mode('pass');
|
||||
start = frames.length;
|
||||
await page.evaluate(() => rtcInputTest.state(7, 0, 44));
|
||||
await waitFrames(start, frameAt(0, 44));
|
||||
proxy.release();
|
||||
await sleep(100);
|
||||
assert.ok(frames.slice(start).every(row => +row[3] !== 0x2000), 'held old datagram regressed input');
|
||||
|
||||
// Drop both directions for 900ms, including several full-state messages.
|
||||
// Afterward repeat recent events and exact final state. Timing lost during
|
||||
// the outage is not claimed to be recoverable.
|
||||
proxy.mode('drop');
|
||||
start = frames.length;
|
||||
await page.evaluate(() => { rtcInputTest.state(8, 0x4000); rtcInputTest.state(9, 0); });
|
||||
await sleep(900);
|
||||
assert.ok(frames.slice(start).every(row => +row[3] === 0), 'dropped traffic reached the service');
|
||||
proxy.mode('pass');
|
||||
await page.evaluate(() => rtcInputTest.state(10, 0, 55, [[8, 0x4000], [9, 0]]));
|
||||
await waitFrames(start, rows => frameAt(0x4000, 55)(rows) && frameAt(0, 55)(rows));
|
||||
console.log('PASS RTC impairment: stale held datagram rejected, 900ms bidirectional loss, bounded tap recovery and exact final state');
|
||||
|
||||
// A stale reset must not forget its sequence floor. Motion by itself must
|
||||
// not keep an old held button alive.
|
||||
await page.evaluate(() => rtcInputTest.state(11, 0x20));
|
||||
await waitFrames(frames.length, frameAt(0x20));
|
||||
start = frames.length;
|
||||
for (let i = 11; i < 24; i++) {
|
||||
await page.evaluate(seq => rtcInputTest.motion(seq, 0), i);
|
||||
await sleep(250);
|
||||
}
|
||||
await waitFrames(start, frameAt(0));
|
||||
const status = await page.evaluate(() => rtcInputTest.request('status'));
|
||||
assert.equal(status.result.pads[0].state, 'paused');
|
||||
start = frames.length;
|
||||
await page.evaluate(() => rtcInputTest.state(10, 0x4000));
|
||||
await sleep(100);
|
||||
assert.ok(frames.slice(start).every(row => +row[3] === 0), 'stale reset discarded sequence floor');
|
||||
await page.evaluate(() => rtcInputTest.state(12, 0));
|
||||
await waitFrames(start, frameAt(0));
|
||||
assert.equal((await page.evaluate(() => rtcInputTest.request('status'))).result.pads[0].state, 'select');
|
||||
console.log('PASS RTC freshness: motion cannot renew held buttons; stale reset preserves sequence floor; newer input resumes');
|
||||
|
||||
await page.evaluate(() => rtcInputTest.pc.close());
|
||||
} finally {
|
||||
if (browser) await browser.close();
|
||||
if (proxy) await proxy.close();
|
||||
if (service) {
|
||||
service.kill();
|
||||
await new Promise(resolve => service.exitCode !== null || service.signalCode !== null ? resolve() : service.once('exit', resolve));
|
||||
}
|
||||
}
|
||||
})().catch(error => { console.error(error); process.exitCode = 1; });
|
||||
@@ -0,0 +1,116 @@
|
||||
/* Deterministic tests of the real RTC datagram boundary. No sockets or PS4:
|
||||
* force the sendto outcomes that are difficult to reproduce on a healthy LAN. */
|
||||
#include <errno.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
|
||||
static ssize_t c4fTestSendto(int, const void *, size_t, int, const struct sockaddr *, socklen_t);
|
||||
#define sendto c4fTestSendto
|
||||
#include "../src/rtc.c"
|
||||
#undef sendto
|
||||
|
||||
#define CHECK(cond) do { if (!(cond)) { fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); exit(1); } } while (0)
|
||||
|
||||
static uint64_t fakeNow = 1000;
|
||||
static int sendError;
|
||||
static unsigned writes;
|
||||
static uint8_t wire[256][C4F_RTC_MTU];
|
||||
static size_t wireLen[256];
|
||||
static uint16_t wirePort[256];
|
||||
|
||||
uint64_t c4fTimeMs(void) { return fakeNow; }
|
||||
void c4fLog(const char *fmt, ...) { (void)fmt; }
|
||||
|
||||
static ssize_t c4fTestSendto(int fd, const void *data, size_t len, int flags,
|
||||
const struct sockaddr *to, socklen_t tolen)
|
||||
{
|
||||
(void)fd; (void)flags; (void)tolen;
|
||||
if (sendError) { errno = sendError; return -1; }
|
||||
CHECK(writes < 256 && len <= C4F_RTC_MTU);
|
||||
memcpy(wire[writes], data, len); wireLen[writes] = len;
|
||||
wirePort[writes] = ((const struct sockaddr_in *)to)->sin_port;
|
||||
writes++;
|
||||
return (ssize_t)len;
|
||||
}
|
||||
|
||||
static C4fRtcPeer *peer(C4fRtc *rtc, unsigned slot)
|
||||
{
|
||||
C4fRtcPeer *p = calloc(1, sizeof(*p));
|
||||
CHECK(p);
|
||||
p->rtc = rtc; p->addrKnown = 1; p->addr.sin_family = AF_INET; p->addr.sin_port = htons((uint16_t)(6000 + slot));
|
||||
rtc->peers[slot] = p;
|
||||
return p;
|
||||
}
|
||||
|
||||
static void dispose(C4fRtc *rtc)
|
||||
{
|
||||
for (unsigned i = 0; i < C4F_RTC_PEERS; i++) {
|
||||
C4fRtcPeer *p = rtc->peers[i];
|
||||
if (!p) continue;
|
||||
while (p->out) { C4fRtcDatagram *d = p->out; p->out = d->next; free(d); }
|
||||
free(p); rtc->peers[i] = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
C4fRtc rtc = { .fd = 77 };
|
||||
C4fRtcPeer *p = peer(&rtc, 0);
|
||||
uint8_t a[] = { 1, 2, 3 }, b[] = { 4, 5, 6, 7 };
|
||||
sendError = EAGAIN;
|
||||
CHECK(c4fRtcBioSend(p, a, sizeof(a)) == sizeof(a));
|
||||
CHECK(c4fRtcBioSend(p, b, sizeof(b)) == sizeof(b));
|
||||
CHECK(p->outPackets == 2 && !p->datagramsOut && c4fRtcWantsWrite(&rtc));
|
||||
memset(a, 99, sizeof(a)); memset(b, 99, sizeof(b)); /* caller's buffers can be reused immediately */
|
||||
c4fRtcWritable(&rtc);
|
||||
CHECK(p->outPackets == 2 && !writes);
|
||||
sendError = 0;
|
||||
c4fRtcWritable(&rtc);
|
||||
CHECK(writes == 2 && wireLen[0] == 3 && wire[0][0] == 1 && wire[1][0] == 4);
|
||||
CHECK(!p->outPackets && p->datagramsOut == 2 && !c4fRtcWantsWrite(&rtc));
|
||||
CHECK(wirePort[0] == htons(6000));
|
||||
CHECK(c4fRtcBioSend(p, a, sizeof(a)) == sizeof(a) && writes == 3 && wire[2][0] == 99);
|
||||
|
||||
/* ENOBUFS can occur on a writable UDP socket. Back off briefly instead of
|
||||
* spinning select continuously or pretending that the datagram was sent. */
|
||||
sendError = ENOBUFS;
|
||||
CHECK(c4fRtcBioSend(p, a, sizeof(a)) == sizeof(a) && !c4fRtcWantsWrite(&rtc));
|
||||
CHECK(c4fRtcWait(&rtc, fakeNow, 50) <= 2);
|
||||
fakeNow += 2;
|
||||
CHECK(c4fRtcWantsWrite(&rtc));
|
||||
CHECK(c4fRtcWait(&rtc, fakeNow, 50) > 0); /* expired backoff must not force a busy select loop */
|
||||
sendError = 0; c4fRtcWritable(&rtc);
|
||||
CHECK(!p->out && writes == 4);
|
||||
|
||||
/* Bounded queues fail explicitly, and permanent socket failures are not
|
||||
* counted as successful datagrams. */
|
||||
sendError = EAGAIN;
|
||||
for (unsigned i = 0; i < C4F_RTC_OUT_PACKETS; i++) CHECK(c4fRtcBioSend(p, a, sizeof(a)) == sizeof(a));
|
||||
CHECK(c4fRtcBioSend(p, a, sizeof(a)) < 0 && p->failed && p->outPackets == C4F_RTC_OUT_PACKETS);
|
||||
CHECK(p->datagramsOut == 4);
|
||||
dispose(&rtc);
|
||||
|
||||
p = peer(&rtc, 0);
|
||||
sendError = ECONNREFUSED;
|
||||
CHECK(c4fRtcBioSend(p, a, sizeof(a)) < 0 && p->failed && !p->out && !p->datagramsOut);
|
||||
dispose(&rtc);
|
||||
|
||||
/* Each peer gets a bounded drain and the first served peer rotates. */
|
||||
p = peer(&rtc, 0);
|
||||
C4fRtcPeer *other = peer(&rtc, 1);
|
||||
sendError = EAGAIN;
|
||||
for (unsigned i = 0; i < 12; i++) {
|
||||
CHECK(c4fRtcBioSend(p, a, sizeof(a)) == sizeof(a));
|
||||
CHECK(c4fRtcBioSend(other, b, sizeof(b)) == sizeof(b));
|
||||
}
|
||||
sendError = 0; c4fRtcWritable(&rtc);
|
||||
CHECK(p->outPackets == 4 && other->outPackets == 4);
|
||||
c4fRtcWritable(&rtc);
|
||||
CHECK(!p->out && !other->out);
|
||||
dispose(&rtc);
|
||||
puts("PASS rtc I/O: owned ciphertext retry/order, EAGAIN/ENOBUFS, bounded queues, fatal errors and fair peer drains");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,433 @@
|
||||
/* STUN and SCTP, as the WebRTC transport uses them, on the host. STUN against the
|
||||
* RFC 5769 test vectors; SCTP driven packet by packet the way a browser would. */
|
||||
#include <arpa/inet.h>
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "c4f_sctp.h"
|
||||
#include "c4f_stun.h"
|
||||
|
||||
#define CHECK(cond) do { if (!(cond)) { fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); exit(1); } } while (0)
|
||||
|
||||
/* ------------------------------------------------------------ STUN */
|
||||
|
||||
static const uint8_t rfcRequest[] = {
|
||||
0x00,0x01,0x00,0x58, 0x21,0x12,0xa4,0x42, 0xb7,0xe7,0xa7,0x01, 0xbc,0x34,0xd6,0x86, 0xfa,0x87,0xdf,0xae,
|
||||
0x80,0x22,0x00,0x10, 0x53,0x54,0x55,0x4e, 0x20,0x74,0x65,0x73, 0x74,0x20,0x63,0x6c, 0x69,0x65,0x6e,0x74,
|
||||
0x00,0x24,0x00,0x04, 0x6e,0x00,0x01,0xff,
|
||||
0x80,0x29,0x00,0x08, 0x93,0x2f,0xf9,0xb1, 0x51,0x26,0x3b,0x36,
|
||||
0x00,0x06,0x00,0x09, 0x65,0x76,0x74,0x6a, 0x3a,0x68,0x36,0x76, 0x59,0x20,0x20,0x20,
|
||||
0x00,0x08,0x00,0x14, 0x9a,0xea,0xa7,0x0c, 0xbf,0xd8,0xcb,0x56, 0x78,0x1e,0xf2,0xb5, 0xb2,0xd3,0xf2,0x49, 0xc1,0xb5,0x71,0xa2,
|
||||
0x80,0x28,0x00,0x04, 0xe5,0x7a,0x3b,0xcf,
|
||||
};
|
||||
static const uint8_t rfcResponse[] = {
|
||||
0x01,0x01,0x00,0x3c, 0x21,0x12,0xa4,0x42, 0xb7,0xe7,0xa7,0x01, 0xbc,0x34,0xd6,0x86, 0xfa,0x87,0xdf,0xae,
|
||||
0x80,0x22,0x00,0x0b, 0x74,0x65,0x73,0x74, 0x20,0x76,0x65,0x63, 0x74,0x6f,0x72,0x20,
|
||||
0x00,0x20,0x00,0x08, 0x00,0x01,0xa1,0x47, 0xe1,0x12,0xa6,0x43,
|
||||
0x00,0x08,0x00,0x14, 0x2b,0x91,0xf5,0x99, 0xfd,0x9e,0x90,0xc3, 0x8c,0x74,0x89,0xf9, 0x2a,0xf9,0xba,0x53, 0xf0,0x6b,0xe7,0xd7,
|
||||
0x80,0x28,0x00,0x04, 0xc0,0x7d,0x4c,0x96,
|
||||
};
|
||||
static const char rfcPassword[] = "VOkJxbRl1RmTxUk/WvJxBt";
|
||||
|
||||
static void testStun(void)
|
||||
{
|
||||
CHECK(c4fCrc32((const uint8_t *)"123456789", 9) == 0xCBF43926u);
|
||||
CHECK(c4fCrc32c((const uint8_t *)"123456789", 9) == 0xE3069283u);
|
||||
CHECK(c4fIsStun(rfcRequest, sizeof(rfcRequest)) && !c4fIsDtls(rfcRequest, sizeof(rfcRequest)));
|
||||
size_t size;
|
||||
const uint8_t *user = c4fStunUsername(rfcRequest, sizeof(rfcRequest), &size);
|
||||
CHECK(user && size == 9 && !memcmp(user, "evtj:h6vY", 9));
|
||||
CHECK(c4fStunVerify(rfcRequest, sizeof(rfcRequest), rfcPassword));
|
||||
CHECK(!c4fStunVerify(rfcRequest, sizeof(rfcRequest), "wrong password"));
|
||||
CHECK(c4fStunVerify(rfcResponse, sizeof(rfcResponse), rfcPassword));
|
||||
/* A changed byte breaks the integrity check. */
|
||||
uint8_t bent[sizeof(rfcRequest)];
|
||||
memcpy(bent, rfcRequest, sizeof(bent));
|
||||
bent[47] ^= 1;
|
||||
CHECK(!c4fStunVerify(bent, sizeof(bent), rfcPassword));
|
||||
CHECK(!c4fStunUsername(rfcResponse, sizeof(rfcResponse), &size)); /* a response, not a request */
|
||||
|
||||
/* Our answer to the RFC's request, from 192.0.2.1:32853, carries the same
|
||||
* XOR-MAPPED-ADDRESS as the RFC's sample response and checks out. */
|
||||
uint8_t answer[C4F_STUN_ANSWER_SIZE];
|
||||
int nominate = -1;
|
||||
uint32_t address = inet_addr("192.0.2.1");
|
||||
size_t n = c4fStunAnswer(rfcRequest, sizeof(rfcRequest), rfcPassword, address, htons(32853), answer, &nominate);
|
||||
CHECK(n == C4F_STUN_ANSWER_SIZE && nominate == 0);
|
||||
CHECK(answer[0] == 0x01 && answer[1] == 0x01 && !memcmp(answer + 4, rfcRequest + 4, 16));
|
||||
CHECK(!memcmp(answer + 20, rfcResponse + 36, 12));
|
||||
CHECK(c4fStunVerify(answer, n, rfcPassword));
|
||||
CHECK(!c4fStunAnswer(rfcRequest, sizeof(rfcRequest), "another", address, htons(32853), answer, &nominate));
|
||||
puts("PASS stun: RFC 5769 request and response, integrity, fingerprint, our XOR-MAPPED-ADDRESS answer");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ SCTP */
|
||||
|
||||
typedef struct { uint8_t data[2048]; size_t len; } Blob;
|
||||
static Blob sent[512], delivered[64];
|
||||
static uint16_t deliveredStream[64];
|
||||
static int sentCount, deliveredCount;
|
||||
static int rejectSend;
|
||||
static uint32_t randomState = 0x12345678;
|
||||
|
||||
static int opSend(void *user, const uint8_t *p, size_t len)
|
||||
{
|
||||
(void)user;
|
||||
if (rejectSend) return -1;
|
||||
CHECK(sentCount < 512 && len <= C4F_SCTP_MTU);
|
||||
/* Every packet we send carries a correct CRC32c, least significant byte first. */
|
||||
uint8_t copy[2048];
|
||||
memcpy(copy, p, len);
|
||||
memset(copy + 8, 0, 4);
|
||||
uint32_t crc = c4fCrc32c(copy, len);
|
||||
CHECK(p[8] == (uint8_t)crc && p[9] == (uint8_t)(crc >> 8) && p[10] == (uint8_t)(crc >> 16) && p[11] == (uint8_t)(crc >> 24));
|
||||
memcpy(sent[sentCount].data, p, len); sent[sentCount++].len = len;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void opDeliver(void *user, uint16_t stream, uint32_t ppid, const uint8_t *d, size_t len)
|
||||
{
|
||||
(void)user;
|
||||
CHECK(deliveredCount < 64 && ppid == C4F_SCTP_PPID_STRING);
|
||||
memcpy(delivered[deliveredCount].data, d, len); delivered[deliveredCount].len = len;
|
||||
deliveredStream[deliveredCount++] = stream;
|
||||
}
|
||||
|
||||
static void opRandom(void *user, void *out, size_t len)
|
||||
{
|
||||
(void)user;
|
||||
for (size_t i = 0; i < len; i++) { randomState = randomState * 1103515245u + 12345u; ((uint8_t *)out)[i] = (uint8_t)(randomState >> 16); }
|
||||
}
|
||||
|
||||
static uint16_t g16(const uint8_t *p) { return (uint16_t)(p[0] << 8 | p[1]); }
|
||||
static uint32_t g32(const uint8_t *p) { return (uint32_t)p[0] << 24 | (uint32_t)p[1] << 16 | (uint32_t)p[2] << 8 | p[3]; }
|
||||
static void p16(uint8_t *p, uint16_t v) { p[0] = v >> 8; p[1] = (uint8_t)v; }
|
||||
static void p32(uint8_t *p, uint32_t v) { p[0] = v >> 24; p[1] = (uint8_t)(v >> 16); p[2] = (uint8_t)(v >> 8); p[3] = (uint8_t)v; }
|
||||
|
||||
/* A browser-side packet under construction. */
|
||||
typedef struct { uint8_t b[2048]; size_t len; } Pkt;
|
||||
static void pkStart(Pkt *k, uint32_t tag) { memset(k, 0, sizeof(*k)); p16(k->b, 5000); p16(k->b + 2, 5000); p32(k->b + 4, tag); k->len = 12; }
|
||||
static uint8_t *pkChunk(Pkt *k, uint8_t type, uint8_t flags, size_t vlen)
|
||||
{
|
||||
uint8_t *c = k->b + k->len;
|
||||
c[0] = type; c[1] = flags; p16(c + 2, (uint16_t)(4 + vlen));
|
||||
k->len += (4 + vlen + 3) & ~(size_t)3;
|
||||
return c + 4;
|
||||
}
|
||||
static void pkData(Pkt *k, uint8_t flags, uint32_t tsn, uint16_t stream, uint16_t ssn, const char *text)
|
||||
{
|
||||
size_t n = strlen(text);
|
||||
uint8_t *v = pkChunk(k, 0, flags, 12 + n);
|
||||
p32(v, tsn); p16(v + 4, stream); p16(v + 6, ssn); p32(v + 8, C4F_SCTP_PPID_STRING); memcpy(v + 12, text, n);
|
||||
}
|
||||
static void pkSend(C4fSctp *s, Pkt *k, uint64_t now)
|
||||
{
|
||||
uint32_t crc = c4fCrc32c(k->b, k->len);
|
||||
k->b[8] = (uint8_t)crc; k->b[9] = (uint8_t)(crc >> 8); k->b[10] = (uint8_t)(crc >> 16); k->b[11] = (uint8_t)(crc >> 24);
|
||||
c4fSctpReceive(s, k->b, k->len, now);
|
||||
}
|
||||
|
||||
/* The first chunk of type `type` in a sent packet, or NULL. */
|
||||
static const uint8_t *findChunk(const Blob *b, uint8_t type)
|
||||
{
|
||||
for (size_t at = 12; at + 4 <= b->len; at += (g16(b->data + at + 2) + 3) & ~3u)
|
||||
if (b->data[at] == type) return b->data + at;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int isDelivered(int i, uint16_t stream, const char *text)
|
||||
{
|
||||
return i < deliveredCount && deliveredStream[i] == stream && delivered[i].len == strlen(text) &&
|
||||
!memcmp(delivered[i].data, text, delivered[i].len);
|
||||
}
|
||||
|
||||
static void testSctp(void)
|
||||
{
|
||||
C4fSctp s;
|
||||
C4fSctpOps ops = { NULL, opSend, opDeliver, opRandom };
|
||||
c4fSctpInit(&s, &ops);
|
||||
uint64_t now = 1000;
|
||||
const uint32_t browserTag = 0x11223344, browserTsn = 1000;
|
||||
|
||||
/* INIT -> INIT-ACK with a cookie, partial reliability and stream resets. */
|
||||
Pkt k;
|
||||
pkStart(&k, 0);
|
||||
uint8_t *v = pkChunk(&k, 1, 0, 16);
|
||||
p32(v, browserTag); p32(v + 4, 131072); p16(v + 8, 65535); p16(v + 10, 65535); p32(v + 12, browserTsn);
|
||||
pkSend(&s, &k, now);
|
||||
CHECK(sentCount == 1 && g32(sent[0].data + 4) == browserTag);
|
||||
const uint8_t *ack = findChunk(&sent[0], 2);
|
||||
CHECK(ack && s.state == C4F_SCTP_COOKIE_WAIT);
|
||||
uint32_t ourTag = g32(ack + 4), ourTsn = g32(ack + 16);
|
||||
CHECK(ourTag == s.localTag && g16(ack + 20) == 7 && g16(ack + 22) == 20);
|
||||
uint8_t cookie[16];
|
||||
memcpy(cookie, ack + 24, 16);
|
||||
CHECK(g16(ack + 40) == 0x8008 && ack[44] == 0xC0 && ack[45] == 0x82 && g16(ack + 48) == 0xC000);
|
||||
|
||||
/* A wrong cookie is ignored; the right one establishes. */
|
||||
pkStart(&k, ourTag); v = pkChunk(&k, 10, 0, 16); memset(v, 0xAA, 16); pkSend(&s, &k, now);
|
||||
CHECK(s.state == C4F_SCTP_COOKIE_WAIT && sentCount == 1);
|
||||
pkStart(&k, ourTag); v = pkChunk(&k, 10, 0, 16); memcpy(v, cookie, 16); pkSend(&s, &k, now);
|
||||
CHECK(s.state == C4F_SCTP_ESTABLISHED && sentCount == 2 && findChunk(&sent[1], 11));
|
||||
|
||||
/* A packet under another tag, or with a bad checksum, is dropped. */
|
||||
pkStart(&k, 0x99999999); pkData(&k, 3, browserTsn, 0, 0, "nope"); pkSend(&s, &k, now);
|
||||
pkStart(&k, ourTag); pkData(&k, 3, browserTsn, 0, 0, "nope"); k.b[k.len - 1] ^= 0x55;
|
||||
c4fSctpReceive(&s, k.b, k.len, now);
|
||||
CHECK(deliveredCount == 0 && s.badChecksums == 1);
|
||||
|
||||
/* In order on stream 0, then a gap, then unordered on stream 1 delivered at once. */
|
||||
pkStart(&k, ourTag); pkData(&k, 3, browserTsn, 0, 0, "hello"); pkSend(&s, &k, now);
|
||||
CHECK(isDelivered(0, 0, "hello") && s.cumTsn == browserTsn);
|
||||
const uint8_t *sack = findChunk(&sent[sentCount - 1], 3);
|
||||
CHECK(sack && g32(sack + 4) == browserTsn && g16(sack + 12) == 0);
|
||||
pkStart(&k, ourTag); pkData(&k, 7, browserTsn + 2, 1, 0, "fast"); pkSend(&s, &k, now);
|
||||
CHECK(isDelivered(1, 1, "fast") && s.cumTsn == browserTsn);
|
||||
sack = findChunk(&sent[sentCount - 1], 3);
|
||||
CHECK(g32(sack + 4) == browserTsn && g16(sack + 12) == 1 && g16(sack + 16) == 2 && g16(sack + 18) == 2);
|
||||
pkStart(&k, ourTag); pkData(&k, 3, browserTsn + 1, 0, 1, "second"); pkSend(&s, &k, now);
|
||||
CHECK(isDelivered(2, 0, "second") && s.cumTsn == browserTsn + 2);
|
||||
/* The same TSN again: acknowledged, not delivered twice. */
|
||||
pkStart(&k, ourTag); pkData(&k, 3, browserTsn + 1, 0, 1, "second"); pkSend(&s, &k, now);
|
||||
CHECK(deliveredCount == 3 && s.duplicates == 1);
|
||||
|
||||
/* Out of order on the ordered stream: held until its turn. */
|
||||
pkStart(&k, ourTag); pkData(&k, 3, browserTsn + 4, 0, 3, "four"); pkSend(&s, &k, now);
|
||||
CHECK(deliveredCount == 3);
|
||||
pkStart(&k, ourTag); pkData(&k, 3, browserTsn + 3, 0, 2, "three"); pkSend(&s, &k, now);
|
||||
CHECK(isDelivered(3, 0, "three") && isDelivered(4, 0, "four") && s.cumTsn == browserTsn + 4);
|
||||
|
||||
/* A message in three pieces, arriving last, first, middle. */
|
||||
pkStart(&k, ourTag); pkData(&k, 1, browserTsn + 7, 0, 4, "ghi"); pkSend(&s, &k, now);
|
||||
pkStart(&k, ourTag); pkData(&k, 2, browserTsn + 5, 0, 4, "abc"); pkSend(&s, &k, now);
|
||||
CHECK(deliveredCount == 5);
|
||||
pkStart(&k, ourTag); pkData(&k, 0, browserTsn + 6, 0, 4, "def"); pkSend(&s, &k, now);
|
||||
CHECK(isDelivered(5, 0, "abcdefghi") && s.inBytes == 0);
|
||||
|
||||
/* FORWARD-TSN skips an unordered piece that will never be finished. */
|
||||
pkStart(&k, ourTag); pkData(&k, 6, browserTsn + 8, 1, 0, "lost-fir"); pkSend(&s, &k, now);
|
||||
pkStart(&k, ourTag); v = pkChunk(&k, 192, 0, 4); p32(v, browserTsn + 9); pkSend(&s, &k, now);
|
||||
CHECK(s.cumTsn == browserTsn + 9 && s.inBytes == 0 && deliveredCount == 6);
|
||||
pkStart(&k, ourTag); pkData(&k, 7, browserTsn + 10, 1, 0, "after"); pkSend(&s, &k, now);
|
||||
CHECK(isDelivered(6, 1, "after"));
|
||||
|
||||
/* Sending: reliable and in order on stream 0, acknowledged by a SACK. */
|
||||
int before = sentCount;
|
||||
CHECK(c4fSctpSend(&s, 0, C4F_SCTP_PPID_STRING, (const uint8_t *)"reply", 5, now) == 0);
|
||||
CHECK(sentCount == before + 1);
|
||||
const uint8_t *data = findChunk(&sent[before], 0);
|
||||
CHECK(data && data[1] == 3 && g32(data + 4) == ourTsn && g16(data + 8) == 0 && g16(data + 10) == 0 &&
|
||||
g32(data + 12) == C4F_SCTP_PPID_STRING && !memcmp(data + 16, "reply", 5));
|
||||
CHECK(c4fSctpDeadline(&s) == now + 200);
|
||||
pkStart(&k, ourTag); v = pkChunk(&k, 3, 0, 12); p32(v, ourTsn); p32(v + 4, 131072); pkSend(&s, &k, now + 5);
|
||||
CHECK(s.out == NULL && c4fSctpDeadline(&s) == 0);
|
||||
|
||||
/* Not acknowledged: resent, same TSN, after the timeout. */
|
||||
now += 1000;
|
||||
before = sentCount;
|
||||
CHECK(c4fSctpSend(&s, 0, C4F_SCTP_PPID_STRING, (const uint8_t *)"again", 5, now) == 0);
|
||||
uint64_t due = c4fSctpDeadline(&s);
|
||||
CHECK(due > now && due <= now + 200);
|
||||
c4fSctpTick(&s, due - 1);
|
||||
CHECK(sentCount == before + 1);
|
||||
c4fSctpTick(&s, due);
|
||||
CHECK(sentCount == before + 2 && s.resent == 1);
|
||||
data = findChunk(&sent[before + 1], 0);
|
||||
CHECK(data && g32(data + 4) == ourTsn + 1 && !memcmp(data + 16, "again", 5));
|
||||
pkStart(&k, ourTag); v = pkChunk(&k, 3, 0, 12); p32(v, ourTsn + 1); p32(v + 4, 131072); pkSend(&s, &k, due + 1);
|
||||
CHECK(s.out == NULL);
|
||||
|
||||
/* Unordered on stream 1 is never resent: given up on, then skipped with FORWARD-TSN. */
|
||||
now += 1000;
|
||||
before = sentCount;
|
||||
CHECK(c4fSctpSend(&s, 1, C4F_SCTP_PPID_STRING, (const uint8_t *)"rumble", 6, now) == 0);
|
||||
data = findChunk(&sent[before], 0);
|
||||
CHECK(data && data[1] == 7);
|
||||
due = c4fSctpDeadline(&s);
|
||||
c4fSctpTick(&s, due);
|
||||
const uint8_t *fwd = findChunk(&sent[sentCount - 1], 192);
|
||||
CHECK(s.abandoned == 1 && fwd && g32(fwd + 4) == ourTsn + 2 && !findChunk(&sent[sentCount - 1], 0));
|
||||
pkStart(&k, ourTag); v = pkChunk(&k, 3, 0, 12); p32(v, ourTsn + 2); p32(v + 4, 131072); pkSend(&s, &k, due + 1);
|
||||
CHECK(s.out == NULL && c4fSctpDeadline(&s) == 0);
|
||||
|
||||
/* Three newly acknowledged later TSNs trigger fast retransmit. Repeated
|
||||
* copies of an old SACK do not manufacture more evidence of a hole. */
|
||||
now += 1000;
|
||||
c4fSctpSend(&s, 0, C4F_SCTP_PPID_STRING, (const uint8_t *)"A", 1, now);
|
||||
c4fSctpSend(&s, 0, C4F_SCTP_PPID_STRING, (const uint8_t *)"B", 1, now);
|
||||
c4fSctpSend(&s, 0, C4F_SCTP_PPID_STRING, (const uint8_t *)"C", 1, now);
|
||||
c4fSctpSend(&s, 0, C4F_SCTP_PPID_STRING, (const uint8_t *)"D", 1, now);
|
||||
before = sentCount;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
pkStart(&k, ourTag); v = pkChunk(&k, 3, 0, 16); p32(v, ourTsn + 2); p32(v + 4, 131072); p16(v + 8, 1); p16(v + 12, 2); p16(v + 14, 2);
|
||||
pkSend(&s, &k, now + 10);
|
||||
}
|
||||
CHECK(s.resent == 1 && sentCount == before);
|
||||
for (int i = 3; i <= 4; i++) {
|
||||
pkStart(&k, ourTag); v = pkChunk(&k, 3, 0, 16); p32(v, ourTsn + 2); p32(v + 4, 131072);
|
||||
p16(v + 8, 1); p16(v + 12, 2); p16(v + 14, (uint16_t)i); pkSend(&s, &k, now + 10);
|
||||
}
|
||||
CHECK(s.resent == 2 && sentCount == before + 1);
|
||||
data = findChunk(&sent[before], 0);
|
||||
CHECK(data && g32(data + 4) == ourTsn + 3 && data[16] == 'A');
|
||||
pkStart(&k, ourTag); v = pkChunk(&k, 3, 0, 12); p32(v, ourTsn + 6); p32(v + 4, 131072); pkSend(&s, &k, now + 20);
|
||||
CHECK(s.out == NULL);
|
||||
|
||||
/* A message bigger than a packet goes in pieces, the same SSN on each. */
|
||||
char big[3000];
|
||||
for (size_t i = 0; i < sizeof(big); i++) big[i] = (char)('a' + i % 26);
|
||||
before = sentCount;
|
||||
CHECK(c4fSctpSend(&s, 0, C4F_SCTP_PPID_STRING, (const uint8_t *)big, sizeof(big), now) == 0);
|
||||
CHECK(sentCount == before + 3);
|
||||
size_t total = 0;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
data = findChunk(&sent[before + i], 0);
|
||||
CHECK(data && g16(data + 10) == 6);
|
||||
CHECK((data[1] & 3) == (i == 0 ? 2 : i == 2 ? 1 : 0));
|
||||
total += g16(data + 2) - 16u;
|
||||
}
|
||||
CHECK(total == sizeof(big));
|
||||
pkStart(&k, ourTag); v = pkChunk(&k, 3, 0, 12); p32(v, ourTsn + 9); p32(v + 4, 131072); pkSend(&s, &k, now + 30);
|
||||
CHECK(s.out == NULL);
|
||||
|
||||
/* HEARTBEAT is echoed; a stream reset is performed and answered. */
|
||||
pkStart(&k, ourTag); v = pkChunk(&k, 4, 0, 8); p16(v, 1); p16(v + 2, 8); memcpy(v + 4, "beat", 4); pkSend(&s, &k, now);
|
||||
const uint8_t *hb = findChunk(&sent[sentCount - 1], 5);
|
||||
CHECK(hb && !memcmp(hb + 8, "beat", 4));
|
||||
pkStart(&k, ourTag); v = pkChunk(&k, 130, 0, 18);
|
||||
p16(v, 13); p16(v + 2, 18); p32(v + 4, 777); p32(v + 8, 0); p32(v + 12, browserTsn + 10); p16(v + 16, 0);
|
||||
pkSend(&s, &k, now);
|
||||
const uint8_t *rc = findChunk(&sent[sentCount - 1], 130);
|
||||
CHECK(rc && g16(rc + 4) == 16 && g32(rc + 8) == 777 && g32(rc + 12) == 1 && s.nextSsn[0] == 0);
|
||||
|
||||
/* ABORT ends it. */
|
||||
pkStart(&k, ourTag); pkChunk(&k, 6, 0, 0); pkSend(&s, &k, now);
|
||||
CHECK(s.state == C4F_SCTP_ENDED && c4fSctpSend(&s, 0, 51, (const uint8_t *)"x", 1, now) == -1);
|
||||
c4fSctpFree(&s);
|
||||
puts("PASS sctp: INIT/cookie, tags, checksums, ordering, gaps, reassembly, FORWARD-TSN, resend, give-up, fast resend, fragments, heartbeat, reset, abort");
|
||||
}
|
||||
|
||||
static void openAssociation(C4fSctp *s, uint64_t now)
|
||||
{
|
||||
C4fSctpOps ops = { NULL, opSend, opDeliver, opRandom };
|
||||
sentCount = deliveredCount = rejectSend = 0;
|
||||
c4fSctpInit(s, &ops);
|
||||
Pkt k;
|
||||
pkStart(&k, 0);
|
||||
uint8_t *v = pkChunk(&k, 1, 0, 16);
|
||||
p32(v, 0xabcdef01); p32(v + 4, 131072); p16(v + 8, 2); p16(v + 10, 2); p32(v + 12, 1000);
|
||||
pkSend(s, &k, now);
|
||||
pkStart(&k, s->localTag); v = pkChunk(&k, 10, 0, 16); memcpy(v, s->cookie, 16); pkSend(s, &k, now);
|
||||
CHECK(s->state == C4F_SCTP_ESTABLISHED);
|
||||
sentCount = 0;
|
||||
}
|
||||
|
||||
static void acknowledge(C4fSctp *s, uint32_t cum, uint32_t window, uint64_t now)
|
||||
{
|
||||
Pkt k;
|
||||
pkStart(&k, s->localTag);
|
||||
uint8_t *v = pkChunk(&k, 3, 0, 12);
|
||||
p32(v, cum); p32(v + 4, window); pkSend(s, &k, now);
|
||||
}
|
||||
|
||||
static void testWindows(void)
|
||||
{
|
||||
C4fSctp s;
|
||||
uint64_t now = 1000;
|
||||
openAssociation(&s, now);
|
||||
uint32_t first = s.nextTsn;
|
||||
char big[C4F_SCTP_MAX_MESSAGE];
|
||||
memset(big, 'q', sizeof(big));
|
||||
CHECK(c4fSctpSend(&s, 0, 51, (const uint8_t *)big, sizeof(big), now) == 0);
|
||||
CHECK(sentCount > 0 && sentCount <= 4 && s.flightBytes <= s.cwnd && s.outBytes == sizeof(big));
|
||||
CHECK(s.highestSent < s.nextTsn - 1); /* remaining fragments queued, not falsely sent */
|
||||
uint32_t beforeSeq = s.nextTsn;
|
||||
CHECK(c4fSctpSend(&s, 1, 51, (const uint8_t *)"stale", 5, now) == -1 && s.nextTsn == beforeSeq);
|
||||
int before = sentCount;
|
||||
acknowledge(&s, s.nextTsn - 1, 131072, now + 1); /* cannot ACK unsent queued data */
|
||||
CHECK(sentCount == before && s.peerCumTsn == first - 1);
|
||||
size_t bytes = 0;
|
||||
for (unsigned tries = 0; s.out && tries < 32; tries++) {
|
||||
acknowledge(&s, s.highestSent, 131072, ++now);
|
||||
CHECK(s.flightBytes <= s.cwnd);
|
||||
}
|
||||
CHECK(!s.out && s.outBytes == 0 && s.flightBytes == 0 && !c4fSctpDeadline(&s));
|
||||
for (int i = 0; i < sentCount; i++) {
|
||||
const uint8_t *data = findChunk(&sent[i], 0);
|
||||
CHECK(data); bytes += g16(data + 2) - 16u;
|
||||
}
|
||||
CHECK(bytes == sizeof(big));
|
||||
c4fSctpFree(&s);
|
||||
|
||||
/* A large already-open window still emits a bounded burst, but schedules
|
||||
* its continuation promptly instead of waiting for the retransmit timer. */
|
||||
openAssociation(&s, now);
|
||||
s.cwnd = 30000;
|
||||
CHECK(c4fSctpSend(&s, 0, 51, (const uint8_t *)big, sizeof(big), now) == 0 && sentCount == 4);
|
||||
CHECK(c4fSctpDeadline(&s) == now + 1);
|
||||
c4fSctpTick(&s, now + 1); CHECK(sentCount == 8);
|
||||
c4fSctpFree(&s);
|
||||
|
||||
/* Receive-window closure buffers reliable control and rejects replaceable
|
||||
* state. A timed probe prevents a lost window update from deadlocking it. */
|
||||
openAssociation(&s, now);
|
||||
acknowledge(&s, s.highestSent, 0, now);
|
||||
first = s.nextTsn;
|
||||
CHECK(c4fSctpSend(&s, 0, 51, (const uint8_t *)big, 2000, now) == 0 && sentCount == 0);
|
||||
CHECK(c4fSctpSend(&s, 1, 51, (const uint8_t *)"stale", 5, now) == -1);
|
||||
uint64_t due = c4fSctpDeadline(&s);
|
||||
CHECK(due == now + s.rto);
|
||||
c4fSctpTick(&s, due - 1); CHECK(sentCount == 0);
|
||||
c4fSctpTick(&s, due); CHECK(sentCount == 1 && s.highestSent == first);
|
||||
acknowledge(&s, first - 1, 0, due + 1); CHECK(sentCount == 1);
|
||||
acknowledge(&s, first, 131072, due + 2); CHECK(sentCount == 2);
|
||||
acknowledge(&s, s.highestSent, 131072, due + 3); CHECK(!s.out && !s.flightBytes);
|
||||
|
||||
/* A too-small positive receive window is also respected before the probe. */
|
||||
acknowledge(&s, s.highestSent, 20, due + 4);
|
||||
before = sentCount;
|
||||
CHECK(c4fSctpSend(&s, 0, 51, (const uint8_t *)big, 500, due + 4) == 0 && sentCount == before);
|
||||
acknowledge(&s, s.highestSent, 131072, due + 5); CHECK(sentCount == before + 1);
|
||||
acknowledge(&s, s.highestSent, 131072, due + 6);
|
||||
c4fSctpFree(&s);
|
||||
|
||||
openAssociation(&s, now);
|
||||
CHECK(c4fSctpSend(&s, 0, 51, (const uint8_t *)big, 3000, now) == 0);
|
||||
before = sentCount; due = c4fSctpDeadline(&s);
|
||||
c4fSctpTick(&s, due);
|
||||
CHECK(s.cwnd == C4F_SCTP_MTU && sentCount == before + 1 && s.resent == 1);
|
||||
c4fSctpFree(&s);
|
||||
|
||||
/* The output callback cannot silently reject reliable DATA. */
|
||||
openAssociation(&s, now);
|
||||
CHECK(c4fSctpSend(&s, 0, 51, (const uint8_t *)"keep", 4, now) == 0);
|
||||
uint32_t savedTag = s.localTag, savedNext = s.nextTsn;
|
||||
size_t savedBytes = s.outBytes;
|
||||
Pkt retry;
|
||||
pkStart(&retry, 0);
|
||||
uint8_t *init = pkChunk(&retry, 1, 0, 16);
|
||||
p32(init, 0xabcdef01); p32(init + 4, 131072); p16(init + 8, 2); p16(init + 10, 2); p32(init + 12, 1000);
|
||||
pkSend(&s, &retry, now + 1);
|
||||
const uint8_t *initAck = findChunk(&sent[sentCount - 1], 2);
|
||||
CHECK(initAck && s.state == C4F_SCTP_ESTABLISHED && s.localTag == savedTag && s.nextTsn == savedNext &&
|
||||
s.outBytes == savedBytes && g32(initAck + 16) == s.localInitialTsn);
|
||||
/* A declared chunk beyond the datagram boundary cannot reset the peer. */
|
||||
pkStart(&retry, 0); init = pkChunk(&retry, 1, 0, 16); p16(init - 2, 65535);
|
||||
pkSend(&s, &retry, now + 2);
|
||||
CHECK(s.state == C4F_SCTP_ESTABLISHED && s.localTag == savedTag && s.outBytes == savedBytes);
|
||||
rejectSend = 1;
|
||||
CHECK(c4fSctpSend(&s, 0, 51, (const uint8_t *)"fatal", 5, now) == -1 && s.state == C4F_SCTP_ENDED);
|
||||
rejectSend = 0;
|
||||
c4fSctpFree(&s);
|
||||
puts("PASS sctp windows: bounded congestion, queued fragments, receive credit/probes, stale-send rejection, timeout burst and send failures");
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
testStun();
|
||||
testSctp();
|
||||
testWindows();
|
||||
return 0;
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SUITES = ['test_logging', 'test_frontend', 'test_web', 'test_recovery', 'test_launcher']
|
||||
SUITES = ['test_logging', 'test_frontend', 'test_web', 'test_recovery', 'test_launcher', 'test_rtc']
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -45,8 +45,40 @@ def one_reply(address, reply, check=None):
|
||||
return thread
|
||||
|
||||
|
||||
def check_latency_status():
|
||||
def probe(body):
|
||||
raw = body if isinstance(body, bytes) else json.dumps(body, separators=(',', ':')).encode()
|
||||
thread = one_reply('127.0.0.1', b'HTTP/1.1 200 OK\r\nContent-Length: %d\r\nConnection: close\r\n\r\n' % len(raw) + raw)
|
||||
result = command('latency')
|
||||
thread.join(timeout=3)
|
||||
assert not thread.is_alive()
|
||||
return result
|
||||
|
||||
assert probe(STATUS).startswith('1 0 '), 'old services must still be recognized'
|
||||
state = dict(json.loads(STATUS), latency=True)
|
||||
for slot, (user, rtt, age) in enumerate(((0x1A2B3C4D, 8, 12), (0x1A2B3C4D, 11, 0),
|
||||
(0x7FFFFFFE, -1, -1), (-1, -1, -1))):
|
||||
state.update({f'pad{slot}User': user, f'pad{slot}Rtt': rtt, f'pad{slot}Age': age})
|
||||
answer = probe(state).split()
|
||||
assert answer[:2] == ['1', '1'], answer
|
||||
assert answer[2:] == [f'1:{0x1A2B3C4D}:8:12', f'1:{0x1A2B3C4D}:11:0',
|
||||
f'1:{0x7FFFFFFE}:-1:-1', '0:0:-1:-1'], answer
|
||||
incomplete = dict(state); del incomplete['pad3Age']
|
||||
assert probe(incomplete).startswith('1 0 '), 'incomplete optional telemetry must not break service control'
|
||||
for key, value in (('pad0User', 4294967296), ('pad0Rtt', -2), ('pad0Rtt', 60001),
|
||||
('pad0Rtt', '8'), ('pad0Age', 2147483648), ('latency', 1)):
|
||||
invalid = dict(state); invalid[key] = value
|
||||
assert probe(invalid).startswith('-1 '), (key, value)
|
||||
duplicate = json.dumps(state, separators=(',', ':')).encode()[:-1] + b',"pad0Rtt":9}'
|
||||
assert probe(duplicate).startswith('-1 ')
|
||||
zero = dict(state, pad0Rtt=0, pad0Age=0)
|
||||
assert f'1:{0x1A2B3C4D}:0:0' in probe(zero), 'a measured sub-ms RTT can round to zero'
|
||||
print('PASS launcher connection RTT: optional old API, four slots, identity, unknown values and malformed telemetry', flush=True)
|
||||
|
||||
|
||||
def main():
|
||||
web.build()
|
||||
subprocess.run([sys.executable, '-B', 'tests/test_launcher_sampling.py'], check=True)
|
||||
# The launcher's own input reader and action navigation, with only the
|
||||
# native PS4 pad calls replaced. This also exercises a remote-only launch.
|
||||
for diagnostic in (False, True):
|
||||
@@ -55,6 +87,12 @@ def main():
|
||||
*(['-DC4F_DIAG'] if diagnostic else []), '-Ilauncher', 'launcher/input.c',
|
||||
'launcher/navigation.c', 'tests/launcher_input_test.c', '-o', binary], check=True)
|
||||
subprocess.run([binary], check=True)
|
||||
# The controller test's logic, against simulated users and pads.
|
||||
binary = 'build/launcher-tester-diag-host-test' if diagnostic else 'build/launcher-tester-host-test'
|
||||
subprocess.run(['clang-18', '-std=gnu11', '-D_DEFAULT_SOURCE', '-Wall', '-Wextra', '-Werror',
|
||||
*(['-DC4F_DIAG'] if diagnostic else []), '-Ilauncher', 'launcher/tester.c',
|
||||
'launcher/navigation.c', 'tests/launcher_tester_test.c', '-lm', '-o', binary], check=True)
|
||||
subprocess.run([binary], check=True)
|
||||
subprocess.run(['clang-18', '-std=gnu11', '-D_DEFAULT_SOURCE', '-Wall', '-Wextra', '-Werror',
|
||||
'-ffunction-sections', '-fdata-sections', '-Ilauncher', '-Itests/include',
|
||||
'tests/diag_test.c', '-Wl,--gc-sections,--wrap=write,--wrap=fsync', '-pthread',
|
||||
@@ -117,6 +155,7 @@ def main():
|
||||
assert out.startswith('-1 ') and '127.0.0.1 refused; 127.0.0.2 HTTP 403' in out, out
|
||||
assert command('probe', C4F_TEST_HOST='127.0.0.2').startswith('0 ')
|
||||
print('PASS console-address fallback, with the reason when neither path answers', flush=True)
|
||||
check_latency_status()
|
||||
|
||||
received = bytearray()
|
||||
child = []
|
||||
@@ -185,9 +224,16 @@ def main():
|
||||
sys.path.insert(0, str(ROOT / 'tools'))
|
||||
import package_launcher as package
|
||||
package.build_art_tool()
|
||||
# A fresh renderer is the reference for every warm cache transition; no
|
||||
# timing limit here depends on the speed of the CI machine.
|
||||
import test_tester_render
|
||||
test_tester_render.check(test_tester_render.build())
|
||||
for state in ('setup', 'running', 'stopped', 'payloader-unavailable', 'outdated', 'mismatch', 'confirm', 'busy', 'locked', 'unknown',
|
||||
'remote-actions-stopped', 'remote-actions-running', 'remote-confirm-cancel',
|
||||
'remote-confirm-stop', 'remote-actions-busy', 'remote-actions-unavailable'):
|
||||
'remote-confirm-stop', 'remote-actions-busy', 'remote-actions-unavailable',
|
||||
'tester', 'tester-motion', 'tester-menu', 'tester-remote', 'tester-disconnected', 'tester-error',
|
||||
'tester-menu-user', 'tester-menu-unavailable', 'tester-menu-error', 'tester-menu-output', 'tester-menu-no-users',
|
||||
'tester-color', 'tester-color-green', 'tester-color-fine', 'tester-color-black', 'tester-color-white', 'tester-color-error'):
|
||||
png = ROOT / f'build/launcher-preview-{state}.png'
|
||||
package.render(['screen', state], png, 1920, 1080)
|
||||
assert png.stat().st_size > 50000, png
|
||||
@@ -198,6 +244,7 @@ def main():
|
||||
assert png.stat().st_size > 50000, png
|
||||
package.render(['icon', '512'], ROOT / 'build/launcher-icon.png', 512, 512)
|
||||
assert subprocess.run([str(package.ART), 'screen', 'nonsense', '/dev/null']).returncode != 0
|
||||
assert subprocess.run([str(package.ART), 'screen', 'tester-nonsense', '/dev/null']).returncode != 0
|
||||
# The icon the payload serves is committed, so the payload build needs no
|
||||
# renderer. Catch it drifting away from the app's own icon.
|
||||
fresh = ROOT / 'build/icon-192.png'
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
"""Exercise the shipped sampler independently of the native display APIs."""
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def main():
|
||||
source = (ROOT / 'launcher/main.cpp').read_text()
|
||||
commands_at = source.index('enum { C4F_DO_NOTHING')
|
||||
commands = source[commands_at:source.index('};', commands_at) + 2]
|
||||
start = source.index('static void *c4fInputWorker(')
|
||||
stop = source.index('\nint main(void)', start)
|
||||
out = ROOT / 'build/launcher-sampling-source.inc'
|
||||
out.parent.mkdir(exist_ok=True)
|
||||
out.write_text(commands + '\n' + source[start:stop])
|
||||
objects = []
|
||||
for name in ('input', 'tester', 'navigation'):
|
||||
obj = f'build/sampling-{name}.o'
|
||||
subprocess.run(['clang-18', '-std=gnu11', '-D_DEFAULT_SOURCE', '-Wall', '-Wextra', '-Werror',
|
||||
'-Ilauncher', '-c', f'launcher/{name}.c', '-o', obj], cwd=ROOT, check=True)
|
||||
objects.append(obj)
|
||||
subprocess.run(['clang++-18', '-std=c++17', '-D_DEFAULT_SOURCE', '-Wall', '-Wextra', '-Werror',
|
||||
'-Ilauncher', '-Ibuild', 'tests/launcher_sampling_test.cpp', *objects,
|
||||
'-pthread', '-lm', '-o', 'build/launcher-sampling-test'], cwd=ROOT, check=True)
|
||||
subprocess.run(['build/launcher-sampling-test'], cwd=ROOT, check=True, timeout=10)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -7,6 +7,7 @@ import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
import test_web as web
|
||||
import mbedtls_host
|
||||
|
||||
|
||||
def connect_again():
|
||||
@@ -21,8 +22,9 @@ def connect_again():
|
||||
|
||||
def main():
|
||||
subprocess.run(['clang-18', '-std=gnu11', '-Wall', '-Wextra', '-Werror', '-g',
|
||||
'-Iinclude', '-Ivendor/jsmn', '-Ivendor/qrcodegen', 'src/net.c', 'src/web.c', 'src/klog_line.c', 'vendor/qrcodegen/qrcodegen.c',
|
||||
'tests/web_stub.c', 'tests/recovery_faults.c', 'build/client.c', 'build/assets.c', '-pthread',
|
||||
'-Iinclude', '-Ivendor/jsmn', '-Ivendor/qrcodegen', '-DC4F_TEST_WEBSOCKET', *mbedtls_host.FLAGS,
|
||||
'src/net.c', 'src/web.c', 'src/rtc.c', 'src/stun.c', 'src/sctp.c', 'src/klog_line.c', 'vendor/qrcodegen/qrcodegen.c',
|
||||
'tests/web_stub.c', 'tests/recovery_faults.c', 'build/client.c', 'build/assets.c', mbedtls_host.library(), '-pthread',
|
||||
'-Wl,--wrap=select,--wrap=accept,--wrap=getsockopt', '-o', str(web.BINARY)], check=True)
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
fault = Path(directory) / 'fault'
|
||||
@@ -48,7 +50,8 @@ def main():
|
||||
assert server.c.request('claim', [0])['result']['pads'][0]['mine']
|
||||
rows = server.rows()
|
||||
split = next(i for i, row in enumerate(rows) if 'network failed' in row)
|
||||
assert all(row.split()[3] == '0' for row in rows[split:] if row.startswith('FRAME'))
|
||||
held = [row for row in rows[split:] if row.startswith('FRAME') and row.split()[3] != '0']
|
||||
assert not held, f'{name}: non-neutral frames after network reset:\n' + '\n'.join(held)
|
||||
assert len([r for r in rows if r.startswith('ADD')]) == 1
|
||||
assert any('listener recovered' in row for row in rows)
|
||||
print(f'PASS {name} failure: listener returns, old buttons cleared, no duplicate pad', flush=True)
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
"""RTC codecs and the production HTTP surface, built and run inside Docker."""
|
||||
import os
|
||||
from pathlib import Path
|
||||
import socket
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
import mbedtls_host
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
BINARY = ROOT / 'build/web-rtc-host-test'
|
||||
FLAGS = ['clang-18', '-std=gnu11', '-Wall', '-Wextra', '-Werror', '-g',
|
||||
'-Iinclude', '-Ivendor/jsmn', '-Ivendor/qrcodegen', *mbedtls_host.FLAGS]
|
||||
SOURCES = ['src/net.c', 'src/web.c', 'src/rtc.c', 'src/stun.c', 'src/sctp.c',
|
||||
'src/klog_line.c', 'vendor/qrcodegen/qrcodegen.c', 'tests/web_stub.c',
|
||||
'build/client.c', 'build/assets.c']
|
||||
|
||||
|
||||
def build():
|
||||
subprocess.run(['python3', 'tools/embed_client.py', 'client/index.html', 'build/client.c'], check=True)
|
||||
subprocess.run(['python3', 'tools/embed_file.py', 'build/assets.c',
|
||||
'c4fManifest=client/manifest.webmanifest', 'c4fIcon=client/icon-192.png'], check=True)
|
||||
library = mbedtls_host.library()
|
||||
subprocess.run([*FLAGS, 'src/stun.c', 'src/sctp.c', 'tests/rtc_test.c', library,
|
||||
'-o', 'build/rtc-test'], check=True)
|
||||
subprocess.run(['build/rtc-test'], check=True, timeout=30)
|
||||
subprocess.run([*FLAGS, 'src/stun.c', 'src/sctp.c', 'tests/rtc_io_test.c', library,
|
||||
'-o', 'build/rtc-io-test'], check=True)
|
||||
subprocess.run(['build/rtc-io-test'], check=True, timeout=30)
|
||||
# No C4F_TEST_WEBSOCKET: browser checks exercise the production protocol.
|
||||
subprocess.run([*FLAGS, *SOURCES, library, '-pthread', '-o', str(BINARY)], check=True)
|
||||
|
||||
|
||||
def http(request):
|
||||
with socket.create_connection(('127.0.0.1', 4264), timeout=3) as connection:
|
||||
connection.sendall(request)
|
||||
result = bytearray()
|
||||
while chunk := connection.recv(65536):
|
||||
result.extend(chunk)
|
||||
return bytes(result)
|
||||
|
||||
|
||||
def main():
|
||||
build()
|
||||
with tempfile.TemporaryFile() as trace:
|
||||
service = subprocess.Popen([str(BINARY)], stdin=subprocess.PIPE, stdout=trace, stderr=trace,
|
||||
env={**os.environ, 'C4F_TEST_KLOG_FD': '0'})
|
||||
try:
|
||||
deadline = time.monotonic() + 5
|
||||
while True:
|
||||
try:
|
||||
page = http(b'GET / HTTP/1.1\r\nHost: 127.0.0.1:4264\r\n\r\n')
|
||||
break
|
||||
except OSError:
|
||||
if time.monotonic() > deadline:
|
||||
trace.seek(0)
|
||||
raise AssertionError(trace.read().decode(errors='replace'))
|
||||
time.sleep(.03)
|
||||
assert page.startswith(b'HTTP/1.1 200')
|
||||
headers = page.split(b'\r\n\r\n', 1)[0]
|
||||
assert b"connect-src 'self';" in headers and b'ws:' not in headers
|
||||
upgrade = (b'GET /ws HTTP/1.1\r\nHost: 127.0.0.1:4264\r\nUpgrade: websocket\r\n'
|
||||
b'Connection: Upgrade\r\nSec-WebSocket-Version: 13\r\n'
|
||||
b'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n')
|
||||
assert http(upgrade).startswith(b'HTTP/1.1 404'), 'production accepted a WebSocket'
|
||||
request = b'POST /rtc HTTP/1.1\r\nHost: 127.0.0.1:4264\r\n'
|
||||
assert b'403' in http(request + b'Origin: https://evil.example\r\nContent-Length: 3\r\n\r\nv=0')
|
||||
assert b'400' in http(request + b'Content-Length: 3\r\n\r\nv=0')
|
||||
saved_error = http(request + b'Origin: null\r\nContent-Length: 3\r\n\r\nv=0')
|
||||
assert b'400' in saved_error and b'Access-Control-Allow-Origin: null' in saved_error
|
||||
assert b'400' in http(request + b'Content-Length: 3\r\nContent-Length: 3\r\n\r\nv=0')
|
||||
assert b'400' in http(request + b'Transfer-Encoding: chunked\r\n\r\n0\r\n\r\n')
|
||||
assert b'413' in http(request + b'Content-Length: 65535\r\n\r\n')
|
||||
assert b'400' in http(request + b'Origin: null\r\nOrigin: null\r\nContent-Length: 3\r\n\r\nv=0')
|
||||
preflight = http(b'OPTIONS /rtc HTTP/1.1\r\nHost: 127.0.0.1:4264\r\nOrigin: null\r\n\r\n')
|
||||
assert b'204 No Content' in preflight and b'Access-Control-Allow-Origin: null' in preflight
|
||||
assert b'Access-Control-Allow-Private-Network: true' in preflight
|
||||
assert b'403' in http(b'OPTIONS /rtc HTTP/1.1\r\nHost: 127.0.0.1:4264\r\nOrigin: https://evil.example\r\n\r\n')
|
||||
status = http(b'GET /api/status HTTP/1.1\r\nHost: 127.0.0.1:4264\r\nX-Control4Free-Launcher: 1\r\n\r\n')
|
||||
assert b'200 OK' in status and b'"application":"Control4Free"' in status
|
||||
print('PASS production transport: HTTP page/API, WebSocket refused, RTC origin/body bounds and saved-page preflight', flush=True)
|
||||
finally:
|
||||
service.terminate()
|
||||
service.wait(timeout=5)
|
||||
service.stdin.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,92 @@
|
||||
"""Cache regression and optional host timings for the shipped tester renderer.
|
||||
|
||||
Run in the build Docker image. Timings measure CPU drawing on this host, exclude
|
||||
display/copy/input scheduling, and must not be reported as PS4 frame timings.
|
||||
"""
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import statistics
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def build(renderer=None, output=None):
|
||||
sys.path.insert(0, str(ROOT / 'tools'))
|
||||
import package_launcher as package
|
||||
package.embed_fonts()
|
||||
output = Path(output or ROOT / 'build/launcher/tester-render-host-test')
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
version = (ROOT / 'VERSION').read_text().strip()
|
||||
flags = ['clang-18', '-std=gnu11', '-O2', '-D_DEFAULT_SOURCE', '-Ilauncher',
|
||||
'-Ivendor/stb', '-Ivendor/qrcodegen', f'-DC4F_LAUNCHER_VERSION="{version}"']
|
||||
stb = output.with_suffix('.stb.o')
|
||||
subprocess.run([*flags, '-w', '-c', 'launcher/stb_truetype.c', '-o', str(stb)], cwd=ROOT, check=True)
|
||||
subprocess.run([*flags, '-Wall', '-Wextra', '-Werror', 'tests/tester_render_test.c',
|
||||
'launcher/screen.c', 'launcher/navigation.c', 'launcher/tester.c',
|
||||
str(renderer or 'launcher/tester_draw.c'), 'launcher/draw.c', 'launcher/logo.c',
|
||||
'vendor/qrcodegen/qrcodegen.c', str(package.FONTS), str(stb), '-lm', '-o', str(output)],
|
||||
cwd=ROOT, check=True)
|
||||
return output
|
||||
|
||||
|
||||
def capture(binary, *args):
|
||||
return subprocess.check_output([str(binary), *map(str, args)], cwd=ROOT, text=True, timeout=120).splitlines()
|
||||
|
||||
|
||||
def check(binary):
|
||||
names = capture(binary, '--list')
|
||||
expected = dict(capture(binary, '--hash', i)[0].split() for i in range(len(names)))
|
||||
sequence = capture(binary)
|
||||
assert len(sequence) == 3 * len(names)
|
||||
for row in sequence:
|
||||
name, actual = row.split()
|
||||
assert actual == expected[name], f'render cache retained a previous state for {name}: {actual} != {expected[name]}'
|
||||
for name in names[1:]:
|
||||
assert expected[name] != expected['idle'], f'{name} did not change the idle display'
|
||||
focus = [expected[name] for name in ('menu', 'menu-user', 'menu-orientation', 'menu-color',
|
||||
'menu-large', 'menu-small', 'menu-reset', 'menu-exit')]
|
||||
assert len(set(focus)) == len(focus), 'menu focus is not visibly distinct'
|
||||
picker = [expected[name] for name in names if name.startswith('color-')]
|
||||
assert len(set(picker)) == len(picker), 'RGB selection, precision, color, error or user is not visibly distinct'
|
||||
for a, b in (('menu-unavailable', 'menu-large'), ('menu-error', 'menu-unavailable'),
|
||||
('menu-output', 'menu-large'), ('menu-no-users', 'menu-unavailable'),
|
||||
('menu-other-user', 'menu-large'), ('menu-renamed-user', 'menu-large')):
|
||||
assert expected[a] != expected[b], f'menu state is not visibly distinct: {a} and {b}'
|
||||
for prefix, base in (('', 'idle'), ('menu-', 'menu-large')):
|
||||
states = [base, *(prefix + 'rtt-' + suffix for suffix in (
|
||||
'known', 'updated', 'measuring', 'no-page', 'unavailable', 'four', 'four-measuring', 'other-slot'))]
|
||||
assert len({expected[name] for name in states}) == len(states), f'{prefix}RTT states are not visibly distinct'
|
||||
assert expected[prefix + 'rtt-unavailable-stale'] == expected[prefix + 'rtt-unavailable'], \
|
||||
'unavailable connection displayed stale RTT values'
|
||||
no_user = 'menu-no-users' if prefix else 'no-users'
|
||||
assert expected[prefix + 'rtt-no-user'] == expected[no_user], 'RTT displayed without a selected user'
|
||||
print(f'PASS tester renderer: {len(names)} fresh references and {len(sequence)} warm transitions match', flush=True)
|
||||
|
||||
|
||||
def benchmark(binary, samples=120):
|
||||
names = capture(binary, '--list')
|
||||
for label, name, mode in (('cold-idle', 'idle', '--cold'), ('cold-menu', 'menu', '--cold'),
|
||||
('first-menu-after-idle', 'menu', '--entry')):
|
||||
times = sorted(float(capture(binary, mode, names.index(name))[0].split('ms=')[1]) for _ in range(12))
|
||||
print(f'bench {label} median_ms={statistics.median(times):.3f} '
|
||||
f'p95_ms={times[int((len(times) - 1) * .95)]:.3f} max_ms={times[-1]:.3f} samples={len(times)}', flush=True)
|
||||
for row in capture(binary, '--benchmark', samples):
|
||||
print(row, flush=True)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--benchmark', action='store_true')
|
||||
parser.add_argument('--renderer', type=Path)
|
||||
parser.add_argument('--output', type=Path)
|
||||
args = parser.parse_args()
|
||||
binary = build(args.renderer, args.output)
|
||||
check(binary)
|
||||
if args.benchmark:
|
||||
benchmark(binary)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -12,6 +12,8 @@ import tempfile
|
||||
import threading
|
||||
import time
|
||||
|
||||
import mbedtls_host
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
BINARY = ROOT / 'build/web-host-test'
|
||||
|
||||
@@ -21,9 +23,11 @@ def build():
|
||||
subprocess.run(['python3', 'tools/embed_file.py', 'build/assets.c',
|
||||
'c4fManifest=client/manifest.webmanifest', 'c4fIcon=client/icon-192.png'], check=True)
|
||||
subprocess.run(['clang-18', '-std=gnu11', '-Wall', '-Wextra', '-Werror', '-g',
|
||||
'-Iinclude', '-Ivendor/jsmn', '-Ivendor/qrcodegen', '-DC4F_STALE_MS=400', '-DC4F_RELEASE_MS=1800',
|
||||
'src/net.c', 'src/web.c', 'src/klog_line.c', 'vendor/qrcodegen/qrcodegen.c', 'tests/web_stub.c', 'build/client.c',
|
||||
'build/assets.c', '-pthread', '-o', str(BINARY)], check=True)
|
||||
'-Iinclude', '-Ivendor/jsmn', '-Ivendor/qrcodegen', '-DC4F_TEST_WEBSOCKET', '-DC4F_STALE_MS=400', '-DC4F_RELEASE_MS=1800', '-DC4F_LATENCY_MS=700',
|
||||
*mbedtls_host.FLAGS,
|
||||
'src/net.c', 'src/web.c', 'src/rtc.c', 'src/stun.c', 'src/sctp.c', 'src/klog_line.c',
|
||||
'vendor/qrcodegen/qrcodegen.c', 'tests/web_stub.c', 'build/client.c',
|
||||
'build/assets.c', mbedtls_host.library(), '-pthread', '-o', str(BINARY)], check=True)
|
||||
|
||||
|
||||
class Client:
|
||||
@@ -129,8 +133,9 @@ class Server:
|
||||
raise AssertionError('server not listening')
|
||||
|
||||
def rows(self):
|
||||
self.trace.seek(0)
|
||||
return self.trace.read().splitlines()
|
||||
# Child stdout shares this file offset, so reading must never seek it.
|
||||
fd = self.trace.fileno()
|
||||
return os.pread(fd, os.fstat(fd).st_size, 0).decode(self.trace.encoding).splitlines()
|
||||
|
||||
def close(self):
|
||||
self.c.close()
|
||||
@@ -151,8 +156,119 @@ def http(request):
|
||||
return b''.join(parts)
|
||||
|
||||
|
||||
def check_latency():
|
||||
"""Exact page RTT is optional telemetry, bound to a recent owner probe."""
|
||||
server = Server()
|
||||
other = None
|
||||
try:
|
||||
owner = server.c
|
||||
keep_input = True
|
||||
|
||||
def keepalive():
|
||||
if keep_input:
|
||||
owner.input(0); owner.input(1)
|
||||
|
||||
def state():
|
||||
keepalive()
|
||||
response = http(b'GET /api/status HTTP/1.1\r\nHost: 127.0.0.1:4264\r\nX-Control4Free-Launcher: 1\r\n\r\n')
|
||||
return json.loads(response.split(b'\r\n\r\n', 1)[1])
|
||||
|
||||
def echo(client, seq):
|
||||
keepalive()
|
||||
client.frame(json.dumps(dict(method='e', params=[seq, 123])))
|
||||
return client.wait_for(lambda m: m.get('method') == 'e' and m.get('params') == [seq, 123])
|
||||
|
||||
def report(client, seq, rtt):
|
||||
keepalive()
|
||||
client.frame(json.dumps(dict(method='latency', params=[seq, rtt])))
|
||||
client.request('ping') # a barrier behind the unacknowledged report
|
||||
|
||||
assert owner.request('info')['result']['latency'] is True
|
||||
assert state()['latency'] is True
|
||||
assert all(state()[f'pad{i}User'] == -1 for i in range(4))
|
||||
owner.request('claim', [0, 1])
|
||||
os.write(server.log, b'DEVICE_OWNER_CHANGED [DeviceId:0x11030d][UserId:0x1a2b3c4d]\n'
|
||||
b'DEVICE_OWNER_CHANGED [DeviceId:0x12030d][UserId:0x1a2b3c4d]\n')
|
||||
end = time.monotonic() + 1
|
||||
while state()['pad0User'] != 0x1a2b3c4d or state()['pad1User'] != 0x1a2b3c4d:
|
||||
assert time.monotonic() < end
|
||||
owner.input(0); owner.input(1); time.sleep(.01)
|
||||
time.sleep(.005) # the first accepted probe must follow ownership
|
||||
echo(owner, 1); report(owner, 1, 17)
|
||||
snapshot = state()
|
||||
assert snapshot['pad0Rtt'] == snapshot['pad1Rtt'] == 17, snapshot
|
||||
assert 0 <= snapshot['pad0Age'] < 200
|
||||
report(owner, 1, 400) # one measurement per probe
|
||||
assert state()['pad0Rtt'] == 17
|
||||
other = Client()
|
||||
report(other, 1, 123) # no probe on this connection
|
||||
echo(other, 1); report(other, 1, 321)
|
||||
assert state()['pad0Rtt'] == 17, 'another connection replaced the owner RTT'
|
||||
for seq, invalid in ((2, -1), (3, 60001)):
|
||||
echo(owner, seq)
|
||||
report(owner, seq, invalid)
|
||||
assert state()['pad0Rtt'] == 17
|
||||
echo(owner, 4); report(owner, 99, 88)
|
||||
assert state()['pad0Rtt'] == 17
|
||||
# Zero is a valid rounded measurement on a fresh increasing probe.
|
||||
echo(owner, 70000); report(owner, 70000, 0)
|
||||
assert state()['pad0Rtt'] == 0
|
||||
echo(owner, 70000); report(owner, 70000, 55)
|
||||
assert state()['pad0Rtt'] == 0, 'a replay reopened a consumed probe'
|
||||
# A delayed report inherits its probe's age; arrival cannot give an
|
||||
# already old measurement a second complete freshness window.
|
||||
echo(owner, 70001)
|
||||
until = time.monotonic() + .4
|
||||
while time.monotonic() < until:
|
||||
owner.input(0); owner.input(1); time.sleep(.05)
|
||||
report(owner, 70001, 19)
|
||||
snapshot = state()
|
||||
assert snapshot['pad0Rtt'] == 19 and snapshot['pad0Age'] >= 350, snapshot
|
||||
until = time.monotonic() + .35
|
||||
while time.monotonic() < until:
|
||||
owner.input(0); owner.input(1); time.sleep(.05)
|
||||
assert state()['pad0Rtt'] == state()['pad0Age'] == -1, 'delayed report renewed an old probe'
|
||||
echo(owner, 70002)
|
||||
until = time.monotonic() + .8
|
||||
while time.monotonic() < until:
|
||||
owner.input(0); owner.input(1); time.sleep(.08)
|
||||
report(owner, 70002, 55)
|
||||
assert state()['pad0User'] == 0x1a2b3c4d
|
||||
assert state()['pad0Rtt'] == state()['pad0Age'] == -1, 'RTT or pending probe did not expire'
|
||||
echo(owner, 70003); report(owner, 70003, 23)
|
||||
assert state()['pad0Rtt'] == 23
|
||||
# RTT messages cannot keep a pad's held input alive.
|
||||
keep_input = False
|
||||
owner.input(0, 0x4000); owner.input(1)
|
||||
until = time.monotonic() + .5
|
||||
seq = 70004
|
||||
while time.monotonic() < until:
|
||||
echo(owner, seq); report(owner, seq, 23); seq += 1
|
||||
time.sleep(.07)
|
||||
assert state()['pad0User'] == state()['pad0Rtt'] == -1
|
||||
owner.input(0); owner.input(1); owner.request('ping')
|
||||
assert state()['pad0Rtt'] == -1, 'pre-stale measurement revived after fresh input'
|
||||
time.sleep(.005)
|
||||
echo(owner, seq); report(owner, seq, 31); seq += 1
|
||||
assert state()['pad0Rtt'] == 31
|
||||
owner.close()
|
||||
owner = server.c = Client()
|
||||
owner.request('claim', [0, 1]); owner.input(0); owner.input(1); owner.request('ping')
|
||||
assert state()['pad0Rtt'] == -1, 'reclaimed owner inherited the previous connection RTT'
|
||||
# The new connection may restart its sequence at one.
|
||||
time.sleep(.005)
|
||||
echo(owner, 1); report(owner, 1, 42)
|
||||
assert state()['pad0Rtt'] == 42
|
||||
print('PASS controller-page RTT: capability, exact values, owner isolation, duplicate/range checks, expiry, stale/reclaim reset and unchanged input lease', flush=True)
|
||||
finally:
|
||||
if other:
|
||||
other.close()
|
||||
server.close()
|
||||
|
||||
|
||||
def main():
|
||||
build()
|
||||
check_latency()
|
||||
server = Server()
|
||||
try:
|
||||
response = http(b'GET / HTTP/1.1\r\nHost: 127.0.0.1:4264\r\n\r\n')
|
||||
@@ -419,6 +535,79 @@ def main():
|
||||
finally:
|
||||
server.close()
|
||||
|
||||
# Motion: the owner's only, ranges checked, put into every later sample, and
|
||||
# back to still whenever the controller is reset.
|
||||
def motion_rows():
|
||||
return [r.split() for r in server.rows() if r.startswith('FRAME') and r.split()[2] == '11030d']
|
||||
|
||||
def motion(c, params):
|
||||
c.frame(json.dumps(dict(method='m', params=params)))
|
||||
|
||||
# A small message right after another can wait for the first one's ACK
|
||||
# (Nagle's algorithm on this test client), so wait for the frame itself.
|
||||
def motion_row(match, timeout=.3):
|
||||
end = time.monotonic() + timeout
|
||||
while True:
|
||||
rows = motion_rows()
|
||||
if rows and match(rows[-1]):
|
||||
return rows[-1]
|
||||
assert time.monotonic() < end, rows[-1] if rows else 'no frames'
|
||||
time.sleep(.01)
|
||||
server = Server()
|
||||
try:
|
||||
c = server.c
|
||||
info = c.request('info')['result']
|
||||
assert info['motion'] is True and info['input'] is True and info['protocol'] == 3, info
|
||||
assert c.request('claim', [0])['result']['pads'][0]['mine']
|
||||
c.input(0)
|
||||
time.sleep(.05)
|
||||
still = motion_rows()[-1][11:21]
|
||||
assert still == ['0.0000', '0.0000', '0.0000', '1.0000'] + ['0.000'] * 6, still
|
||||
for params in ([0, 0, 0, 0, 10000, 0, 1000, 0, 0, 0], [0, 0, 0, 0, 10001, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 10000, 16001, 0, 0, 0, 0, 0], [0, 0, 0, 0, 10000, 0, 0, 0, 0, 0, -35001],
|
||||
[4, 0, 0, 0, 10000, 0, 0, 0, 0, 0, 0]):
|
||||
assert c.request('m', params)['error']['code'] == 400, params
|
||||
other = Client()
|
||||
motion(other, [0, 0, 0, 0, 10000, 500, 0, 0, 0, 0, 0])
|
||||
other.request('ping') # answered only after the motion before it was handled
|
||||
time.sleep(.05)
|
||||
assert all(r[15] == '0.000' for r in motion_rows()), 'a non-owner moved the controller'
|
||||
c.input(0, 0x4000)
|
||||
motion(c, [0, 1000, -2000, 3000, 9000, -500, 1000, 250, 1500, -35000, 12])
|
||||
motion_row(lambda r: r[3] == '16384' and r[11:21] == ['0.1000', '-0.2000', '0.3000', '0.9000', '-0.500',
|
||||
'1.000', '0.250', '1.500', '-35.000', '0.012'])
|
||||
c.input(0, 0x2000, lx=10)
|
||||
row = motion_row(lambda r: r[3] == '8192')
|
||||
assert row[4] == '10' and row[16] == '1.000', 'motion dropped by new input'
|
||||
# A burst of motion alone coalesces instead of queueing up behind itself.
|
||||
start = len(motion_rows())
|
||||
for i in range(200):
|
||||
motion(c, [0, 0, 0, 0, 10000, i, 1000, 0, 0, 0, 0])
|
||||
row = motion_row(lambda r: r[15] == '0.199')
|
||||
assert row[3] == '8192', row
|
||||
assert len(motion_rows()) - start < 100, f'{len(motion_rows()) - start} reports for 200 motion messages'
|
||||
# Idle: neutral and still. Motion cannot revive an old button lease.
|
||||
time.sleep(.6)
|
||||
assert c.request('status')['result']['pads'][0]['state'] == 'paused'
|
||||
assert motion_rows()[-1][11:21] == still, motion_rows()[-1]
|
||||
motion(c, [0, 0, 0, 0, 10000, 0, 1000, 0, 0, 0, 0])
|
||||
time.sleep(.05)
|
||||
assert c.request('status')['result']['pads'][0]['state'] == 'paused'
|
||||
c.input(0)
|
||||
time.sleep(.03)
|
||||
assert c.request('status')['result']['pads'][0]['state'] != 'paused'
|
||||
# A new owner starts still.
|
||||
c.close()
|
||||
server.c = c = Client()
|
||||
assert c.request('claim', [0])['result']['pads'][0]['mine']
|
||||
c.input(0)
|
||||
time.sleep(.05)
|
||||
assert motion_rows()[-1][11:21] == still, motion_rows()[-1]
|
||||
other.close()
|
||||
print('PASS motion: owner only, ranges checked, merged into samples, coalesced, reset to still', flush=True)
|
||||
finally:
|
||||
server.close()
|
||||
|
||||
# Without the call, controllers work as before.
|
||||
server = Server(C4F_TEST_NO_FEEDBACK='1')
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
/* Render the real tester through changing states. The Python runner compares
|
||||
* each warm result with a new process rendering the same state from scratch. */
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include "screen.h"
|
||||
|
||||
static const char *c4fCases[] = {
|
||||
"idle", "sticks", "buttons", "motion", "touch", "red", "blue", "rumble", "note",
|
||||
"other-user", "renamed-user", "one-user", "disconnected", "error", "opening", "signed-out", "no-users",
|
||||
"menu", "menu-user", "menu-orientation", "menu-color", "menu-large", "menu-small",
|
||||
"menu-reset", "menu-exit", "menu-unavailable", "menu-error", "menu-output", "menu-no-users",
|
||||
"menu-other-user", "menu-renamed-user", "menu-one-user", "buttons-red", "buttons-blue", "touch-red",
|
||||
"rtt-known", "rtt-updated", "rtt-measuring", "rtt-no-page", "rtt-unavailable", "rtt-four", "rtt-four-measuring",
|
||||
"rtt-other-slot", "rtt-no-user", "rtt-unavailable-stale",
|
||||
"menu-rtt-known", "menu-rtt-updated", "menu-rtt-measuring", "menu-rtt-no-page", "menu-rtt-unavailable",
|
||||
"menu-rtt-four", "menu-rtt-four-measuring", "menu-rtt-other-slot", "menu-rtt-no-user", "menu-rtt-unavailable-stale",
|
||||
"color-red", "color-green", "color-blue", "color-fine", "color-black", "color-white", "color-error", "color-other-user", "color-rtt-known"
|
||||
};
|
||||
#define C4F_CASES ((int)(sizeof(c4fCases) / sizeof(c4fCases[0])))
|
||||
|
||||
static void c4fFixture(C4fLauncherScreen *s, int id)
|
||||
{
|
||||
memset(s, 0, sizeof(*s));
|
||||
C4fTesterView *v = &s->tester;
|
||||
s->running = 1;
|
||||
v->active = 1;
|
||||
v->selected = 0;
|
||||
v->user[0].used = v->user[1].used = 1;
|
||||
v->user[0].state = v->user[1].state = C4F_TESTER_READY;
|
||||
snprintf(v->user[0].name, sizeof(v->user[0].name), "Alex");
|
||||
snprintf(v->user[1].name, sizeof(v->user[1].name), "Sam");
|
||||
v->lx = v->ly = v->rx = v->ry = 128;
|
||||
v->quat[3] = 1000;
|
||||
s->connection.available = 1;
|
||||
s->connection.pads = 1;
|
||||
for (int i = 0; i < 4; i++) s->connection.rttMs[i] = -1;
|
||||
s->connection.rttMs[0] = 8;
|
||||
const char *name = c4fCases[id];
|
||||
if (!strcmp(name, "sticks")) {
|
||||
v->lx = 12; v->ly = 230; v->rx = 211; v->ry = 60;
|
||||
} else if (!strcmp(name, "buttons") || !strcmp(name, "buttons-red") || !strcmp(name, "buttons-blue")) {
|
||||
v->buttons = 0x10fffe; /* face, direction, shoulder and stick buttons */
|
||||
v->l2 = 93; v->r2 = 207; v->lx = 35; v->rx = 224;
|
||||
if (strcmp(name, "buttons")) {
|
||||
v->lightSet = 1; v->light[!strcmp(name, "buttons-red") ? 0 : 2] = 255;
|
||||
}
|
||||
} else if (!strcmp(name, "motion")) {
|
||||
v->accel[0] = -12; v->accel[1] = 98; v->accel[2] = 21;
|
||||
v->gyro[0] = 150; v->gyro[1] = -32; v->gyro[2] = 4;
|
||||
v->quat[0] = 120; v->quat[1] = -40; v->quat[2] = 10; v->quat[3] = 991;
|
||||
} else if (!strcmp(name, "touch") || !strcmp(name, "touch-red")) {
|
||||
v->touches = 2;
|
||||
v->touchX[0] = 420; v->touchY[0] = 300;
|
||||
v->touchX[1] = 1500; v->touchY[1] = 700;
|
||||
if (!strcmp(name, "touch-red")) { v->lightSet = 1; v->light[0] = 255; }
|
||||
} else if (!strcmp(name, "red") || !strcmp(name, "blue")) {
|
||||
v->lightSet = 1;
|
||||
v->light[!strcmp(name, "red") ? 0 : 2] = 255;
|
||||
} else if (!strcmp(name, "rumble")) v->rumble = 1;
|
||||
else if (!strcmp(name, "note")) snprintf(v->note, sizeof(v->note), "Orientation reset.");
|
||||
else if (!strcmp(name, "other-user")) v->selected = 1;
|
||||
else if (!strcmp(name, "renamed-user")) snprintf(v->user[0].name, sizeof(v->user[0].name), "Alex second controller");
|
||||
else if (!strcmp(name, "one-user")) v->user[1].used = 0;
|
||||
else if (!strcmp(name, "disconnected")) v->user[0].state = C4F_TESTER_NO_CONTROLLER;
|
||||
else if (!strcmp(name, "error")) {
|
||||
v->user[0].state = C4F_TESTER_FAILED; v->user[0].error = 0x80920001u;
|
||||
} else if (!strcmp(name, "opening")) v->user[0].state = C4F_TESTER_OPENING;
|
||||
else if (!strcmp(name, "signed-out")) v->user[0].state = C4F_TESTER_SIGNED_OUT;
|
||||
else if (!strcmp(name, "no-users")) {
|
||||
v->selected = -1; v->user[0].used = v->user[1].used = 0;
|
||||
}
|
||||
if (!strncmp(name, "menu", 4)) {
|
||||
static const int focus[] = {
|
||||
C4F_TESTER_RESUME, C4F_TESTER_USER, C4F_TESTER_ORIENTATION, C4F_TESTER_LIGHT_COLOR,
|
||||
C4F_TESTER_RUMBLE_LARGE, C4F_TESTER_RUMBLE_SMALL, C4F_TESTER_LIGHT_RESET, C4F_TESTER_EXIT
|
||||
};
|
||||
v->menu = 1;
|
||||
v->focus = id >= 17 && id <= 24 ? focus[id - 17] : C4F_TESTER_RUMBLE_LARGE;
|
||||
if (!strcmp(name, "menu-unavailable")) v->user[0].state = C4F_TESTER_NO_CONTROLLER;
|
||||
else if (!strcmp(name, "menu-error")) {
|
||||
v->user[0].state = C4F_TESTER_FAILED; v->user[0].error = 0x80920001u;
|
||||
} else if (!strcmp(name, "menu-output")) {
|
||||
v->lightSet = 1; v->light[1] = 255; v->rumble = 2;
|
||||
snprintf(v->note, sizeof(v->note), "Small motor: rumbling for a second.");
|
||||
} else if (!strcmp(name, "menu-no-users")) {
|
||||
v->selected = -1; v->user[0].used = v->user[1].used = 0;
|
||||
} else if (!strcmp(name, "menu-other-user")) v->selected = 1;
|
||||
else if (!strcmp(name, "menu-renamed-user")) snprintf(v->user[0].name, sizeof(v->user[0].name), "Alex second controller");
|
||||
else if (!strcmp(name, "menu-one-user")) v->user[1].used = 0;
|
||||
}
|
||||
if (!strncmp(name, "color-", 6)) {
|
||||
v->menu = v->colorMenu = v->lightSet = 1;
|
||||
v->focus = C4F_TESTER_LIGHT_COLOR;
|
||||
v->light[0] = 51; v->light[1] = 128; v->light[2] = 230;
|
||||
if (!strcmp(name, "color-green")) v->colorChannel = 1;
|
||||
else if (!strcmp(name, "color-blue")) v->colorChannel = 2;
|
||||
else if (!strcmp(name, "color-fine")) v->colorFine = 1;
|
||||
else if (!strcmp(name, "color-black")) memset(v->light, 0, sizeof(v->light));
|
||||
else if (!strcmp(name, "color-white")) memset(v->light, 255, sizeof(v->light));
|
||||
else if (!strcmp(name, "color-error")) snprintf(v->note, sizeof(v->note), "The PS4 refused that (0xfffffffb).");
|
||||
else if (!strcmp(name, "color-other-user")) v->selected = 1;
|
||||
}
|
||||
const char *rtt = strstr(name, "rtt-");
|
||||
if (rtt) {
|
||||
rtt += 4;
|
||||
if (!strcmp(rtt, "known")) s->connection.rttMs[0] = 12;
|
||||
else if (!strcmp(rtt, "updated")) s->connection.rttMs[0] = 63;
|
||||
else if (!strcmp(rtt, "measuring")) s->connection.rttMs[0] = -1;
|
||||
else if (!strcmp(rtt, "no-page")) s->connection.pads = 0;
|
||||
else if (!strcmp(rtt, "unavailable") || !strcmp(rtt, "unavailable-stale")) {
|
||||
s->connection.available = 0;
|
||||
if (!strcmp(rtt, "unavailable-stale")) {
|
||||
s->connection.pads = 15;
|
||||
for (int i = 0; i < 4; i++) s->connection.rttMs[i] = 999;
|
||||
}
|
||||
} else if (!strcmp(rtt, "four") || !strcmp(rtt, "four-measuring")) {
|
||||
s->connection.pads = 15;
|
||||
s->connection.rttMs[0] = 0; s->connection.rttMs[1] = 8;
|
||||
s->connection.rttMs[2] = 60000; s->connection.rttMs[3] = -1;
|
||||
if (!strcmp(rtt, "four-measuring")) for (int i = 0; i < 4; i++) s->connection.rttMs[i] = -1;
|
||||
} else if (!strcmp(rtt, "other-slot")) {
|
||||
s->connection.pads = 8; s->connection.rttMs[3] = 12;
|
||||
} else if (!strcmp(rtt, "no-user")) {
|
||||
v->selected = -1; v->user[0].used = v->user[1].used = 0;
|
||||
s->connection.pads = 15;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static uint64_t c4fHash(const uint32_t *pixels)
|
||||
{
|
||||
uint64_t hash = UINT64_C(14695981039346656037);
|
||||
const unsigned char *p = (const unsigned char *)pixels;
|
||||
const size_t bytes = (size_t)C4F_SCREEN_WIDTH * C4F_SCREEN_HEIGHT * sizeof(*pixels);
|
||||
for (size_t i = 0; i < bytes; i++) { hash ^= p[i]; hash *= UINT64_C(1099511628211); }
|
||||
return hash;
|
||||
}
|
||||
|
||||
static int c4fCaseId(const char *name)
|
||||
{
|
||||
for (int i = 0; i < C4F_CASES; i++) if (!strcmp(c4fCases[i], name)) return i;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static double c4fClockMs(void)
|
||||
{
|
||||
struct timespec now;
|
||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
return now.tv_sec * 1000.0 + now.tv_nsec / 1000000.0;
|
||||
}
|
||||
|
||||
static int c4fCompareDouble(const void *a, const void *b)
|
||||
{
|
||||
double x = *(const double *)a, y = *(const double *)b;
|
||||
return x < y ? -1 : x > y;
|
||||
}
|
||||
|
||||
static void c4fMeasure(uint32_t *pixels, const char *name, int a, int b, int transition, int samples)
|
||||
{
|
||||
double *times = calloc((size_t)samples, sizeof(*times));
|
||||
if (!times) exit(2);
|
||||
C4fLauncherScreen state;
|
||||
for (int i = 0; i < samples + 8; i++) {
|
||||
if (transition) {
|
||||
c4fFixture(&state, b);
|
||||
c4fDrawTester(pixels, &state);
|
||||
}
|
||||
c4fFixture(&state, transition || (i & 1) ? a : b);
|
||||
double start = c4fClockMs();
|
||||
c4fDrawTester(pixels, &state);
|
||||
double elapsed = c4fClockMs() - start;
|
||||
if (i >= 8) times[i - 8] = elapsed;
|
||||
}
|
||||
qsort(times, (size_t)samples, sizeof(*times), c4fCompareDouble);
|
||||
printf("bench %s median_ms=%.3f p95_ms=%.3f max_ms=%.3f samples=%d\n", name,
|
||||
times[samples / 2], times[(samples - 1) * 95 / 100], times[samples - 1], samples);
|
||||
free(times);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
if (argc == 2 && !strcmp(argv[1], "--list")) {
|
||||
for (int i = 0; i < C4F_CASES; i++) puts(c4fCases[i]);
|
||||
return 0;
|
||||
}
|
||||
if (c4fScreenInit()) return 2;
|
||||
uint32_t *pixels = calloc((size_t)C4F_SCREEN_WIDTH * C4F_SCREEN_HEIGHT, sizeof(*pixels));
|
||||
if (!pixels) return 2;
|
||||
C4fLauncherScreen state;
|
||||
if (argc == 3 && (!strcmp(argv[1], "--hash") || !strcmp(argv[1], "--cold") || !strcmp(argv[1], "--entry"))) {
|
||||
int id = atoi(argv[2]);
|
||||
if (id < 0 || id >= C4F_CASES) return 2;
|
||||
if (!strcmp(argv[1], "--entry")) {
|
||||
c4fFixture(&state, 0);
|
||||
c4fDrawTester(pixels, &state);
|
||||
}
|
||||
c4fFixture(&state, id);
|
||||
double start = c4fClockMs();
|
||||
c4fDrawTester(pixels, &state);
|
||||
double elapsed = c4fClockMs() - start;
|
||||
if (strcmp(argv[1], "--hash")) printf("cold %s ms=%.3f\n", c4fCases[id], elapsed);
|
||||
else printf("%s %016llx\n", c4fCases[id], (unsigned long long)c4fHash(pixels));
|
||||
} else if (argc >= 2 && !strcmp(argv[1], "--benchmark")) {
|
||||
int samples = argc == 3 ? atoi(argv[2]) : 120;
|
||||
if (samples < 10 || samples > 10000) return 2;
|
||||
c4fMeasure(pixels, "idle", 0, 0, 0, samples);
|
||||
c4fMeasure(pixels, "sticks-buttons", 1, 2, 0, samples);
|
||||
c4fMeasure(pixels, "motion", 0, 3, 0, samples);
|
||||
c4fMeasure(pixels, "menu-still", 17, 17, 0, samples);
|
||||
c4fMeasure(pixels, "menu-focus", 18, 22, 0, samples);
|
||||
c4fMeasure(pixels, "menu-actions", 17, 29, 0, samples);
|
||||
c4fMeasure(pixels, "menu-enter", 17, 0, 1, samples);
|
||||
c4fMeasure(pixels, "menu-leave", 0, 17, 1, samples);
|
||||
c4fMeasure(pixels, "color-channel", c4fCaseId("color-red"), c4fCaseId("color-green"), 0, samples);
|
||||
c4fMeasure(pixels, "color-values", c4fCaseId("color-red"), c4fCaseId("color-black"), 0, samples);
|
||||
c4fMeasure(pixels, "color-enter", c4fCaseId("color-red"), c4fCaseId("menu-color"), 1, samples);
|
||||
} else if (argc == 1) {
|
||||
/* Both directions expose caches which retain the most recent input,
|
||||
* user, colour, menu focus, connection state or output-test feedback. */
|
||||
for (int pass = 0; pass < 3; pass++) {
|
||||
/* The same canvas is reused by the launcher's home screen when
|
||||
* leaving/reopening the tester; renderer caches must survive it. */
|
||||
C4fLauncherScreen home = { 0 };
|
||||
snprintf(home.address, sizeof(home.address), "http://192.168.1.20:4264");
|
||||
c4fDrawLauncher(pixels, &home);
|
||||
for (int step = 0; step < C4F_CASES; step++) {
|
||||
int id = pass & 1 ? C4F_CASES - step - 1 : step;
|
||||
c4fFixture(&state, id);
|
||||
c4fDrawTester(pixels, &state);
|
||||
printf("%s %016llx\n", c4fCases[id], (unsigned long long)c4fHash(pixels));
|
||||
}
|
||||
}
|
||||
} else return 2;
|
||||
free(pixels);
|
||||
return 0;
|
||||
}
|
||||
@@ -264,8 +264,11 @@ void c4fPadDataNeutral(ScePadData *p)
|
||||
}
|
||||
int32_t c4fVirtualPadInsert(const C4fVirtualPad *p, const ScePadData *d)
|
||||
{
|
||||
printf("FRAME %llu %x %u %u %u %u %u %u %u %u\n", (unsigned long long)c4fTimeMs(),
|
||||
p->handle, d->buttons, d->lx, d->ly, d->rx, d->ry, d->l2, d->r2, d->touchData.fingers);
|
||||
/* Motion last, so the earlier columns keep their places. */
|
||||
printf("FRAME %llu %x %u %u %u %u %u %u %u %u %.4f %.4f %.4f %.4f %.3f %.3f %.3f %.3f %.3f %.3f\n",
|
||||
(unsigned long long)c4fTimeMs(), p->handle, d->buttons, d->lx, d->ly, d->rx, d->ry, d->l2, d->r2,
|
||||
d->touchData.fingers, d->quat[0], d->quat[1], d->quat[2], d->quat[3],
|
||||
d->accel[0], d->accel[1], d->accel[2], d->gyro[0], d->gyro[1], d->gyro[2]);
|
||||
return getenv("C4F_FAIL_INPUT") && d->buttons ? -5 : 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,100 @@ static int c4fPrintSvg(void)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Controller test states, with example readings: 1 when state is one, 0 when it
|
||||
* is not a test state, -1 for an unknown one. */
|
||||
static int c4fTesterPreview(const char *state, C4fLauncherScreen *s)
|
||||
{
|
||||
C4fTesterView *v = &s->tester;
|
||||
if (strncmp(state, "tester", 6)) return 0;
|
||||
v->active = 1;
|
||||
v->selected = 0;
|
||||
snprintf(v->user[0].name, sizeof(v->user[0].name), "Alex");
|
||||
snprintf(v->user[1].name, sizeof(v->user[1].name), "Sam");
|
||||
v->user[0].used = v->user[1].used = 1;
|
||||
v->user[0].state = v->user[1].state = C4F_TESTER_READY;
|
||||
v->lx = v->ly = v->rx = v->ry = 128;
|
||||
v->quat[3] = 1000;
|
||||
s->connection.available = 1;
|
||||
s->connection.pads = 1;
|
||||
for (int i = 0; i < 4; i++) s->connection.rttMs[i] = -1;
|
||||
s->connection.rttMs[0] = 8;
|
||||
if (!strcmp(state, "tester")) {
|
||||
v->buttons = 0x4000 | 0x0400 | 0x0010 | 0x0200; /* Cross, L1, Up, R2 */
|
||||
v->lx = 40; v->ly = 90; v->rx = 210; v->r2 = 180;
|
||||
v->touches = 1; v->touchX[0] = 1300; v->touchY[0] = 420;
|
||||
v->accel[1] = 100;
|
||||
} else if (!strcmp(state, "tester-motion")) {
|
||||
v->buttons = 0x0002 | 0x100000; /* L3, touchpad click */
|
||||
v->l2 = 60;
|
||||
v->touches = 2; v->touchX[0] = 420; v->touchY[0] = 300; v->touchX[1] = 1500; v->touchY[1] = 700;
|
||||
v->accel[0] = -12; v->accel[1] = 98; v->accel[2] = 21;
|
||||
v->gyro[0] = 150; v->gyro[1] = -32; v->gyro[2] = 4;
|
||||
v->quat[0] = 120; v->quat[1] = -40; v->quat[2] = 10; v->quat[3] = 991;
|
||||
v->lightSet = 1; v->light[0] = 255;
|
||||
v->rumble = 1;
|
||||
snprintf(v->note, sizeof(v->note), "Large motor: rumbling for a second.");
|
||||
} else if (!strcmp(state, "tester-menu") || !strcmp(state, "tester-remote") ||
|
||||
!strcmp(state, "tester-menu-user") || !strcmp(state, "tester-menu-unavailable") ||
|
||||
!strcmp(state, "tester-menu-error") || !strcmp(state, "tester-menu-output") ||
|
||||
!strcmp(state, "tester-menu-no-users") || !strncmp(state, "tester-menu-rtt-", 16)) {
|
||||
v->menu = 1;
|
||||
v->focus = !strcmp(state, "tester-menu") ? C4F_TESTER_RUMBLE_LARGE : C4F_TESTER_USER;
|
||||
if (!strcmp(state, "tester-menu-user")) {
|
||||
v->selected = 1; s->connection.pads = 2; s->connection.rttMs[1] = 6;
|
||||
}
|
||||
else if (!strcmp(state, "tester-menu-unavailable")) {
|
||||
v->user[0].state = C4F_TESTER_NO_CONTROLLER;
|
||||
v->focus = C4F_TESTER_RUMBLE_LARGE;
|
||||
} else if (!strcmp(state, "tester-menu-error")) {
|
||||
v->user[0].state = C4F_TESTER_FAILED;
|
||||
v->user[0].error = 0x80920001u;
|
||||
v->focus = C4F_TESTER_ORIENTATION;
|
||||
} else if (!strcmp(state, "tester-menu-output")) {
|
||||
v->lightSet = 1; v->light[1] = 255; v->rumble = 2;
|
||||
v->focus = C4F_TESTER_RUMBLE_SMALL;
|
||||
snprintf(v->note, sizeof(v->note), "Small motor: rumbling for a second.");
|
||||
} else if (!strcmp(state, "tester-menu-no-users")) {
|
||||
v->selected = -1;
|
||||
v->user[0].used = v->user[1].used = 0;
|
||||
}
|
||||
} else if (!strcmp(state, "tester-color") || !strcmp(state, "tester-color-green") ||
|
||||
!strcmp(state, "tester-color-fine") || !strcmp(state, "tester-color-black") ||
|
||||
!strcmp(state, "tester-color-white") || !strcmp(state, "tester-color-error")) {
|
||||
v->menu = v->colorMenu = v->lightSet = 1;
|
||||
v->focus = C4F_TESTER_LIGHT_COLOR;
|
||||
v->light[0] = 51; v->light[1] = 128; v->light[2] = 230;
|
||||
if (!strcmp(state, "tester-color-green")) v->colorChannel = 1;
|
||||
else if (!strcmp(state, "tester-color-fine")) v->colorFine = 1;
|
||||
else if (!strcmp(state, "tester-color-black")) memset(v->light, 0, sizeof(v->light));
|
||||
else if (!strcmp(state, "tester-color-white")) memset(v->light, 255, sizeof(v->light));
|
||||
else if (!strcmp(state, "tester-color-error")) snprintf(v->note, sizeof(v->note), "The PS4 refused that (0xfffffffb).");
|
||||
} else if (!strcmp(state, "tester-disconnected")) {
|
||||
v->user[0].state = C4F_TESTER_NO_CONTROLLER;
|
||||
} else if (!strcmp(state, "tester-error")) {
|
||||
v->selected = 1;
|
||||
v->user[1].state = C4F_TESTER_FAILED;
|
||||
v->user[1].error = 0x80920001u;
|
||||
} else if (strncmp(state, "tester-rtt-", 11)) {
|
||||
return -1;
|
||||
}
|
||||
const char *rtt = strstr(state, "rtt-");
|
||||
if (rtt) {
|
||||
rtt += 4;
|
||||
if (!strcmp(rtt, "four")) {
|
||||
s->connection.pads = 15;
|
||||
s->connection.rttMs[0] = 0; s->connection.rttMs[1] = 8;
|
||||
s->connection.rttMs[2] = 60000; s->connection.rttMs[3] = -1;
|
||||
} else if (!strcmp(rtt, "measuring")) {
|
||||
s->connection.pads = 15;
|
||||
for (int i = 0; i < 4; i++) s->connection.rttMs[i] = -1;
|
||||
} else if (!strcmp(rtt, "no-page")) s->connection.pads = 0;
|
||||
else if (!strcmp(rtt, "unavailable")) s->connection.available = 0;
|
||||
else return -1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
if (argc == 2 && !strcmp(argv[1], "svg")) return c4fPrintSvg();
|
||||
@@ -60,7 +154,11 @@ int main(int argc, char **argv)
|
||||
snprintf(s.address, sizeof(s.address), "http://192.168.1.20:4264");
|
||||
const char *state = argv[2], *message = "";
|
||||
s.autorun = C4F_AUTORUN_ON;
|
||||
if (!strcmp(state, "running") || !strcmp(state, "confirm") || !strcmp(state, "outdated") || !strcmp(state, "mismatch") ||
|
||||
int tester = c4fTesterPreview(state, &s);
|
||||
if (tester < 0) return 2;
|
||||
if (tester) {
|
||||
s.running = 1; s.controllers = 2;
|
||||
} else if (!strcmp(state, "running") || !strcmp(state, "confirm") || !strcmp(state, "outdated") || !strcmp(state, "mismatch") ||
|
||||
!strcmp(state, "remote-actions-running") || !strcmp(state, "remote-confirm-cancel") ||
|
||||
!strcmp(state, "remote-confirm-stop") || !strcmp(state, "remote-confirm-diag")) {
|
||||
s.running = 1; s.controllers = 2; s.confirmStop = !strcmp(state, "confirm");
|
||||
@@ -98,6 +196,10 @@ int main(int argc, char **argv)
|
||||
snprintf(s.message, sizeof(s.message), "%s", message);
|
||||
uint32_t *pixels = calloc((size_t)C4F_SCREEN_WIDTH * C4F_SCREEN_HEIGHT, sizeof(uint32_t));
|
||||
if (!pixels) return 2;
|
||||
if (s.tester.active) {
|
||||
c4fDrawTester(pixels, &s);
|
||||
return c4fWrite(argv[3], pixels, (size_t)C4F_SCREEN_WIDTH * C4F_SCREEN_HEIGHT);
|
||||
}
|
||||
#ifdef C4F_DIAG
|
||||
if (!strcmp(state, "remote-input-diag") || !strcmp(state, "payloader-unavailable-diag")) {
|
||||
snprintf(s.inputNote, sizeof(s.inputNote),
|
||||
|
||||
@@ -53,6 +53,7 @@ def build_art_tool(diagnostic=False):
|
||||
subprocess.run([*flags, '-w', '-c', str(ROOT / 'launcher/stb_truetype.c'), '-o', str(stb)], check=True)
|
||||
subprocess.run([*flags, '-Wall', '-Wextra', '-Werror', str(ROOT / 'tools/launcher_art.c'),
|
||||
str(ROOT / 'launcher/screen.c'), str(ROOT / 'launcher/navigation.c'),
|
||||
str(ROOT / 'launcher/tester.c'), str(ROOT / 'launcher/tester_draw.c'),
|
||||
str(ROOT / 'launcher/draw.c'), str(ROOT / 'launcher/logo.c'), *extra_sources,
|
||||
str(ROOT / 'vendor/qrcodegen/qrcodegen.c'), str(FONTS), str(stb), '-lm', *extra_flags, '-o', str(art)],
|
||||
check=True)
|
||||
@@ -139,6 +140,7 @@ def main():
|
||||
shutil.copyfile(ROOT / 'THIRD_PARTY.md', licenses / 'THIRD_PARTY.txt')
|
||||
shutil.copyfile(ROOT / 'vendor/jsmn/LICENSE', licenses / 'jsmn-MIT.txt')
|
||||
shutil.copyfile(ROOT / 'vendor/roboto/LICENSE', licenses / 'Roboto-Apache-2.0.txt')
|
||||
shutil.copyfile(ROOT / 'vendor/mbedtls/LICENSE', licenses / 'Mbed-TLS-LICENSE.txt')
|
||||
qrcodegen = (ROOT / 'vendor/qrcodegen/qrcodegen.h').read_text()
|
||||
(licenses / 'qrcodegen-MIT.txt').write_text(qrcodegen.split('*/', 1)[0] + '*/\n')
|
||||
stb = (ROOT / 'vendor/stb/stb_truetype.h').read_text()
|
||||
|
||||
@@ -0,0 +1,553 @@
|
||||
Mbed TLS files are provided under a dual [Apache-2.0](https://spdx.org/licenses/Apache-2.0.html)
|
||||
OR [GPL-2.0-or-later](https://spdx.org/licenses/GPL-2.0-or-later.html) license.
|
||||
This means that users may choose which of these licenses they take the code
|
||||
under.
|
||||
|
||||
The full text of each of these licenses is given below.
|
||||
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
|
||||
===============================================================================
|
||||
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Lesser General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's
|
||||
source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate
|
||||
copyright notice and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
REPAIR OR CORRECTION.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, the commands you use may
|
||||
be called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License.
|
||||
@@ -0,0 +1,4 @@
|
||||
Makefile
|
||||
*.sln
|
||||
*.vcxproj
|
||||
mbedtls/check_config
|
||||
@@ -0,0 +1,631 @@
|
||||
/**
|
||||
* \file aes.h
|
||||
*
|
||||
* \brief This file contains AES definitions and functions.
|
||||
*
|
||||
* The Advanced Encryption Standard (AES) specifies a FIPS-approved
|
||||
* cryptographic algorithm that can be used to protect electronic
|
||||
* data.
|
||||
*
|
||||
* The AES algorithm is a symmetric block cipher that can
|
||||
* encrypt and decrypt information. For more information, see
|
||||
* <em>FIPS Publication 197: Advanced Encryption Standard</em> and
|
||||
* <em>ISO/IEC 18033-2:2006: Information technology -- Security
|
||||
* techniques -- Encryption algorithms -- Part 2: Asymmetric
|
||||
* ciphers</em>.
|
||||
*
|
||||
* The AES-XTS block mode is standardized by NIST SP 800-38E
|
||||
* <https://nvlpubs.nist.gov/nistpubs/legacy/sp/nistspecialpublication800-38e.pdf>
|
||||
* and described in detail by IEEE P1619
|
||||
* <https://ieeexplore.ieee.org/servlet/opac?punumber=4375278>.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Copyright The Mbed TLS Contributors
|
||||
* SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#ifndef MBEDTLS_AES_H
|
||||
#define MBEDTLS_AES_H
|
||||
#include "mbedtls/private_access.h"
|
||||
|
||||
#include "mbedtls/build_info.h"
|
||||
#include "mbedtls/platform_util.h"
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* padlock.c and aesni.c rely on these values! */
|
||||
#define MBEDTLS_AES_ENCRYPT 1 /**< AES encryption. */
|
||||
#define MBEDTLS_AES_DECRYPT 0 /**< AES decryption. */
|
||||
|
||||
/* Error codes in range 0x0020-0x0022 */
|
||||
/** Invalid key length. */
|
||||
#define MBEDTLS_ERR_AES_INVALID_KEY_LENGTH -0x0020
|
||||
/** Invalid data input length. */
|
||||
#define MBEDTLS_ERR_AES_INVALID_INPUT_LENGTH -0x0022
|
||||
|
||||
/* Error codes in range 0x0021-0x0025 */
|
||||
/** Invalid input data. */
|
||||
#define MBEDTLS_ERR_AES_BAD_INPUT_DATA -0x0021
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#if !defined(MBEDTLS_AES_ALT)
|
||||
// Regular implementation
|
||||
//
|
||||
|
||||
/**
|
||||
* \brief The AES context-type definition.
|
||||
*/
|
||||
typedef struct mbedtls_aes_context {
|
||||
int MBEDTLS_PRIVATE(nr); /*!< The number of rounds. */
|
||||
size_t MBEDTLS_PRIVATE(rk_offset); /*!< The offset in array elements to AES
|
||||
round keys in the buffer. */
|
||||
#if defined(MBEDTLS_AES_ONLY_128_BIT_KEY_LENGTH) && !defined(MBEDTLS_PADLOCK_C)
|
||||
uint32_t MBEDTLS_PRIVATE(buf)[44]; /*!< Aligned data buffer to hold
|
||||
10 round keys for 128-bit case. */
|
||||
#else
|
||||
uint32_t MBEDTLS_PRIVATE(buf)[68]; /*!< Unaligned data buffer. This buffer can
|
||||
hold 32 extra Bytes, which can be used for
|
||||
one of the following purposes:
|
||||
<ul><li>Alignment if VIA padlock is
|
||||
used.</li>
|
||||
<li>Simplifying key expansion in the 256-bit
|
||||
case by generating an extra round key.
|
||||
</li></ul> */
|
||||
#endif /* MBEDTLS_AES_ONLY_128_BIT_KEY_LENGTH && !MBEDTLS_PADLOCK_C */
|
||||
}
|
||||
mbedtls_aes_context;
|
||||
|
||||
#if defined(MBEDTLS_CIPHER_MODE_XTS)
|
||||
/**
|
||||
* \brief The AES XTS context-type definition.
|
||||
*/
|
||||
typedef struct mbedtls_aes_xts_context {
|
||||
mbedtls_aes_context MBEDTLS_PRIVATE(crypt); /*!< The AES context to use for AES block
|
||||
encryption or decryption. */
|
||||
mbedtls_aes_context MBEDTLS_PRIVATE(tweak); /*!< The AES context used for tweak
|
||||
computation. */
|
||||
} mbedtls_aes_xts_context;
|
||||
#endif /* MBEDTLS_CIPHER_MODE_XTS */
|
||||
|
||||
#else /* MBEDTLS_AES_ALT */
|
||||
#include "aes_alt.h"
|
||||
#endif /* MBEDTLS_AES_ALT */
|
||||
|
||||
/**
|
||||
* \brief This function initializes the specified AES context.
|
||||
*
|
||||
* It must be the first API called before using
|
||||
* the context.
|
||||
*
|
||||
* \param ctx The AES context to initialize. This must not be \c NULL.
|
||||
*/
|
||||
void mbedtls_aes_init(mbedtls_aes_context *ctx);
|
||||
|
||||
/**
|
||||
* \brief This function releases and clears the specified AES context.
|
||||
*
|
||||
* \param ctx The AES context to clear.
|
||||
* If this is \c NULL, this function does nothing.
|
||||
* Otherwise, the context must have been at least initialized.
|
||||
*/
|
||||
void mbedtls_aes_free(mbedtls_aes_context *ctx);
|
||||
|
||||
#if defined(MBEDTLS_CIPHER_MODE_XTS)
|
||||
/**
|
||||
* \brief This function initializes the specified AES XTS context.
|
||||
*
|
||||
* It must be the first API called before using
|
||||
* the context.
|
||||
*
|
||||
* \param ctx The AES XTS context to initialize. This must not be \c NULL.
|
||||
*/
|
||||
void mbedtls_aes_xts_init(mbedtls_aes_xts_context *ctx);
|
||||
|
||||
/**
|
||||
* \brief This function releases and clears the specified AES XTS context.
|
||||
*
|
||||
* \param ctx The AES XTS context to clear.
|
||||
* If this is \c NULL, this function does nothing.
|
||||
* Otherwise, the context must have been at least initialized.
|
||||
*/
|
||||
void mbedtls_aes_xts_free(mbedtls_aes_xts_context *ctx);
|
||||
#endif /* MBEDTLS_CIPHER_MODE_XTS */
|
||||
|
||||
/**
|
||||
* \brief This function sets the encryption key.
|
||||
*
|
||||
* \param ctx The AES context to which the key should be bound.
|
||||
* It must be initialized.
|
||||
* \param key The encryption key.
|
||||
* This must be a readable buffer of size \p keybits bits.
|
||||
* \param keybits The size of data passed in bits. Valid options are:
|
||||
* <ul><li>128 bits</li>
|
||||
* <li>192 bits</li>
|
||||
* <li>256 bits</li></ul>
|
||||
*
|
||||
* \return \c 0 on success.
|
||||
* \return #MBEDTLS_ERR_AES_INVALID_KEY_LENGTH on failure.
|
||||
*/
|
||||
MBEDTLS_CHECK_RETURN_TYPICAL
|
||||
int mbedtls_aes_setkey_enc(mbedtls_aes_context *ctx, const unsigned char *key,
|
||||
unsigned int keybits);
|
||||
|
||||
#if !defined(MBEDTLS_BLOCK_CIPHER_NO_DECRYPT)
|
||||
/**
|
||||
* \brief This function sets the decryption key.
|
||||
*
|
||||
* \param ctx The AES context to which the key should be bound.
|
||||
* It must be initialized.
|
||||
* \param key The decryption key.
|
||||
* This must be a readable buffer of size \p keybits bits.
|
||||
* \param keybits The size of data passed. Valid options are:
|
||||
* <ul><li>128 bits</li>
|
||||
* <li>192 bits</li>
|
||||
* <li>256 bits</li></ul>
|
||||
*
|
||||
* \return \c 0 on success.
|
||||
* \return #MBEDTLS_ERR_AES_INVALID_KEY_LENGTH on failure.
|
||||
*/
|
||||
MBEDTLS_CHECK_RETURN_TYPICAL
|
||||
int mbedtls_aes_setkey_dec(mbedtls_aes_context *ctx, const unsigned char *key,
|
||||
unsigned int keybits);
|
||||
#endif /* !MBEDTLS_BLOCK_CIPHER_NO_DECRYPT */
|
||||
|
||||
#if defined(MBEDTLS_CIPHER_MODE_XTS)
|
||||
/**
|
||||
* \brief This function prepares an XTS context for encryption and
|
||||
* sets the encryption key.
|
||||
*
|
||||
* \param ctx The AES XTS context to which the key should be bound.
|
||||
* It must be initialized.
|
||||
* \param key The encryption key. This is comprised of the XTS key1
|
||||
* concatenated with the XTS key2.
|
||||
* This must be a readable buffer of size \p keybits bits.
|
||||
* \param keybits The size of \p key passed in bits. Valid options are:
|
||||
* <ul><li>256 bits (each of key1 and key2 is a 128-bit key)</li>
|
||||
* <li>512 bits (each of key1 and key2 is a 256-bit key)</li></ul>
|
||||
*
|
||||
* \return \c 0 on success.
|
||||
* \return #MBEDTLS_ERR_AES_INVALID_KEY_LENGTH on failure.
|
||||
*/
|
||||
MBEDTLS_CHECK_RETURN_TYPICAL
|
||||
int mbedtls_aes_xts_setkey_enc(mbedtls_aes_xts_context *ctx,
|
||||
const unsigned char *key,
|
||||
unsigned int keybits);
|
||||
|
||||
/**
|
||||
* \brief This function prepares an XTS context for decryption and
|
||||
* sets the decryption key.
|
||||
*
|
||||
* \param ctx The AES XTS context to which the key should be bound.
|
||||
* It must be initialized.
|
||||
* \param key The decryption key. This is comprised of the XTS key1
|
||||
* concatenated with the XTS key2.
|
||||
* This must be a readable buffer of size \p keybits bits.
|
||||
* \param keybits The size of \p key passed in bits. Valid options are:
|
||||
* <ul><li>256 bits (each of key1 and key2 is a 128-bit key)</li>
|
||||
* <li>512 bits (each of key1 and key2 is a 256-bit key)</li></ul>
|
||||
*
|
||||
* \return \c 0 on success.
|
||||
* \return #MBEDTLS_ERR_AES_INVALID_KEY_LENGTH on failure.
|
||||
*/
|
||||
MBEDTLS_CHECK_RETURN_TYPICAL
|
||||
int mbedtls_aes_xts_setkey_dec(mbedtls_aes_xts_context *ctx,
|
||||
const unsigned char *key,
|
||||
unsigned int keybits);
|
||||
#endif /* MBEDTLS_CIPHER_MODE_XTS */
|
||||
|
||||
/**
|
||||
* \brief This function performs an AES single-block encryption or
|
||||
* decryption operation.
|
||||
*
|
||||
* It performs the operation defined in the \p mode parameter
|
||||
* (encrypt or decrypt), on the input data buffer defined in
|
||||
* the \p input parameter.
|
||||
*
|
||||
* mbedtls_aes_init(), and either mbedtls_aes_setkey_enc() or
|
||||
* mbedtls_aes_setkey_dec() must be called before the first
|
||||
* call to this API with the same context.
|
||||
*
|
||||
* \param ctx The AES context to use for encryption or decryption.
|
||||
* It must be initialized and bound to a key.
|
||||
* \param mode The AES operation: #MBEDTLS_AES_ENCRYPT or
|
||||
* #MBEDTLS_AES_DECRYPT.
|
||||
* \param input The buffer holding the input data.
|
||||
* It must be readable and at least \c 16 Bytes long.
|
||||
* \param output The buffer where the output data will be written.
|
||||
* It must be writeable and at least \c 16 Bytes long.
|
||||
|
||||
* \return \c 0 on success.
|
||||
*/
|
||||
MBEDTLS_CHECK_RETURN_TYPICAL
|
||||
int mbedtls_aes_crypt_ecb(mbedtls_aes_context *ctx,
|
||||
int mode,
|
||||
const unsigned char input[16],
|
||||
unsigned char output[16]);
|
||||
|
||||
#if defined(MBEDTLS_CIPHER_MODE_CBC)
|
||||
/**
|
||||
* \brief This function performs an AES-CBC encryption or decryption operation
|
||||
* on full blocks.
|
||||
*
|
||||
* It performs the operation defined in the \p mode
|
||||
* parameter (encrypt/decrypt), on the input data buffer defined in
|
||||
* the \p input parameter.
|
||||
*
|
||||
* It can be called as many times as needed, until all the input
|
||||
* data is processed. mbedtls_aes_init(), and either
|
||||
* mbedtls_aes_setkey_enc() or mbedtls_aes_setkey_dec() must be called
|
||||
* before the first call to this API with the same context.
|
||||
*
|
||||
* \note This function operates on full blocks, that is, the input size
|
||||
* must be a multiple of the AES block size of \c 16 Bytes.
|
||||
*
|
||||
* \note Upon exit, the content of the IV is updated so that you can
|
||||
* call the same function again on the next
|
||||
* block(s) of data and get the same result as if it was
|
||||
* encrypted in one call. This allows a "streaming" usage.
|
||||
* If you need to retain the contents of the IV, you should
|
||||
* either save it manually or use the cipher module instead.
|
||||
*
|
||||
*
|
||||
* \param ctx The AES context to use for encryption or decryption.
|
||||
* It must be initialized and bound to a key.
|
||||
* \param mode The AES operation: #MBEDTLS_AES_ENCRYPT or
|
||||
* #MBEDTLS_AES_DECRYPT.
|
||||
* \param length The length of the input data in Bytes. This must be a
|
||||
* multiple of the block size (\c 16 Bytes).
|
||||
* \param iv Initialization vector (updated after use).
|
||||
* It must be a readable and writeable buffer of \c 16 Bytes.
|
||||
* \param input The buffer holding the input data.
|
||||
* It must be readable and of size \p length Bytes.
|
||||
* \param output The buffer holding the output data.
|
||||
* It must be writeable and of size \p length Bytes.
|
||||
*
|
||||
* \return \c 0 on success.
|
||||
* \return #MBEDTLS_ERR_AES_INVALID_INPUT_LENGTH
|
||||
* on failure.
|
||||
*/
|
||||
MBEDTLS_CHECK_RETURN_TYPICAL
|
||||
int mbedtls_aes_crypt_cbc(mbedtls_aes_context *ctx,
|
||||
int mode,
|
||||
size_t length,
|
||||
unsigned char iv[16],
|
||||
const unsigned char *input,
|
||||
unsigned char *output);
|
||||
#endif /* MBEDTLS_CIPHER_MODE_CBC */
|
||||
|
||||
#if defined(MBEDTLS_CIPHER_MODE_XTS)
|
||||
/**
|
||||
* \brief This function performs an AES-XTS encryption or decryption
|
||||
* operation for an entire XTS data unit.
|
||||
*
|
||||
* AES-XTS encrypts or decrypts blocks based on their location as
|
||||
* defined by a data unit number. The data unit number must be
|
||||
* provided by \p data_unit.
|
||||
*
|
||||
* NIST SP 800-38E limits the maximum size of a data unit to 2^20
|
||||
* AES blocks. If the data unit is larger than this, this function
|
||||
* returns #MBEDTLS_ERR_AES_INVALID_INPUT_LENGTH.
|
||||
*
|
||||
* \param ctx The AES XTS context to use for AES XTS operations.
|
||||
* It must be initialized and bound to a key.
|
||||
* \param mode The AES operation: #MBEDTLS_AES_ENCRYPT or
|
||||
* #MBEDTLS_AES_DECRYPT.
|
||||
* \param length The length of a data unit in Bytes. This can be any
|
||||
* length between 16 bytes and 2^24 bytes inclusive
|
||||
* (between 1 and 2^20 block cipher blocks).
|
||||
* \param data_unit The address of the data unit encoded as an array of 16
|
||||
* bytes in little-endian format. For disk encryption, this
|
||||
* is typically the index of the block device sector that
|
||||
* contains the data.
|
||||
* \param input The buffer holding the input data (which is an entire
|
||||
* data unit). This function reads \p length Bytes from \p
|
||||
* input.
|
||||
* \param output The buffer holding the output data (which is an entire
|
||||
* data unit). This function writes \p length Bytes to \p
|
||||
* output.
|
||||
*
|
||||
* \return \c 0 on success.
|
||||
* \return #MBEDTLS_ERR_AES_INVALID_INPUT_LENGTH if \p length is
|
||||
* smaller than an AES block in size (16 Bytes) or if \p
|
||||
* length is larger than 2^20 blocks (16 MiB).
|
||||
*/
|
||||
MBEDTLS_CHECK_RETURN_TYPICAL
|
||||
int mbedtls_aes_crypt_xts(mbedtls_aes_xts_context *ctx,
|
||||
int mode,
|
||||
size_t length,
|
||||
const unsigned char data_unit[16],
|
||||
const unsigned char *input,
|
||||
unsigned char *output);
|
||||
#endif /* MBEDTLS_CIPHER_MODE_XTS */
|
||||
|
||||
#if defined(MBEDTLS_CIPHER_MODE_CFB)
|
||||
/**
|
||||
* \brief This function performs an AES-CFB128 encryption or decryption
|
||||
* operation.
|
||||
*
|
||||
* It performs the operation defined in the \p mode
|
||||
* parameter (encrypt or decrypt), on the input data buffer
|
||||
* defined in the \p input parameter.
|
||||
*
|
||||
* For CFB, you must set up the context with mbedtls_aes_setkey_enc(),
|
||||
* regardless of whether you are performing an encryption or decryption
|
||||
* operation, that is, regardless of the \p mode parameter. This is
|
||||
* because CFB mode uses the same key schedule for encryption and
|
||||
* decryption.
|
||||
*
|
||||
* \note Upon exit, the content of the IV is updated so that you can
|
||||
* call the same function again on the next
|
||||
* block(s) of data and get the same result as if it was
|
||||
* encrypted in one call. This allows a "streaming" usage.
|
||||
* If you need to retain the contents of the
|
||||
* IV, you must either save it manually or use the cipher
|
||||
* module instead.
|
||||
*
|
||||
*
|
||||
* \param ctx The AES context to use for encryption or decryption.
|
||||
* It must be initialized and bound to a key.
|
||||
* \param mode The AES operation: #MBEDTLS_AES_ENCRYPT or
|
||||
* #MBEDTLS_AES_DECRYPT.
|
||||
* \param length The length of the input data in Bytes.
|
||||
* \param iv_off The offset in IV (updated after use).
|
||||
* It must point to a valid \c size_t.
|
||||
* \param iv The initialization vector (updated after use).
|
||||
* It must be a readable and writeable buffer of \c 16 Bytes.
|
||||
* \param input The buffer holding the input data.
|
||||
* It must be readable and of size \p length Bytes.
|
||||
* \param output The buffer holding the output data.
|
||||
* It must be writeable and of size \p length Bytes.
|
||||
*
|
||||
* \return \c 0 on success.
|
||||
*/
|
||||
MBEDTLS_CHECK_RETURN_TYPICAL
|
||||
int mbedtls_aes_crypt_cfb128(mbedtls_aes_context *ctx,
|
||||
int mode,
|
||||
size_t length,
|
||||
size_t *iv_off,
|
||||
unsigned char iv[16],
|
||||
const unsigned char *input,
|
||||
unsigned char *output);
|
||||
|
||||
/**
|
||||
* \brief This function performs an AES-CFB8 encryption or decryption
|
||||
* operation.
|
||||
*
|
||||
* It performs the operation defined in the \p mode
|
||||
* parameter (encrypt/decrypt), on the input data buffer defined
|
||||
* in the \p input parameter.
|
||||
*
|
||||
* Due to the nature of CFB, you must use the same key schedule for
|
||||
* both encryption and decryption operations. Therefore, you must
|
||||
* use the context initialized with mbedtls_aes_setkey_enc() for
|
||||
* both #MBEDTLS_AES_ENCRYPT and #MBEDTLS_AES_DECRYPT.
|
||||
*
|
||||
* \note Upon exit, the content of the IV is updated so that you can
|
||||
* call the same function again on the next
|
||||
* block(s) of data and get the same result as if it was
|
||||
* encrypted in one call. This allows a "streaming" usage.
|
||||
* If you need to retain the contents of the
|
||||
* IV, you should either save it manually or use the cipher
|
||||
* module instead.
|
||||
*
|
||||
*
|
||||
* \param ctx The AES context to use for encryption or decryption.
|
||||
* It must be initialized and bound to a key.
|
||||
* \param mode The AES operation: #MBEDTLS_AES_ENCRYPT or
|
||||
* #MBEDTLS_AES_DECRYPT
|
||||
* \param length The length of the input data.
|
||||
* \param iv The initialization vector (updated after use).
|
||||
* It must be a readable and writeable buffer of \c 16 Bytes.
|
||||
* \param input The buffer holding the input data.
|
||||
* It must be readable and of size \p length Bytes.
|
||||
* \param output The buffer holding the output data.
|
||||
* It must be writeable and of size \p length Bytes.
|
||||
*
|
||||
* \return \c 0 on success.
|
||||
*/
|
||||
MBEDTLS_CHECK_RETURN_TYPICAL
|
||||
int mbedtls_aes_crypt_cfb8(mbedtls_aes_context *ctx,
|
||||
int mode,
|
||||
size_t length,
|
||||
unsigned char iv[16],
|
||||
const unsigned char *input,
|
||||
unsigned char *output);
|
||||
#endif /*MBEDTLS_CIPHER_MODE_CFB */
|
||||
|
||||
#if defined(MBEDTLS_CIPHER_MODE_OFB)
|
||||
/**
|
||||
* \brief This function performs an AES-OFB (Output Feedback Mode)
|
||||
* encryption or decryption operation.
|
||||
*
|
||||
* For OFB, you must set up the context with
|
||||
* mbedtls_aes_setkey_enc(), regardless of whether you are
|
||||
* performing an encryption or decryption operation. This is
|
||||
* because OFB mode uses the same key schedule for encryption and
|
||||
* decryption.
|
||||
*
|
||||
* The OFB operation is identical for encryption or decryption,
|
||||
* therefore no operation mode needs to be specified.
|
||||
*
|
||||
* \note Upon exit, the content of iv, the Initialisation Vector, is
|
||||
* updated so that you can call the same function again on the next
|
||||
* block(s) of data and get the same result as if it was encrypted
|
||||
* in one call. This allows a "streaming" usage, by initialising
|
||||
* iv_off to 0 before the first call, and preserving its value
|
||||
* between calls.
|
||||
*
|
||||
* For non-streaming use, the iv should be initialised on each call
|
||||
* to a unique value, and iv_off set to 0 on each call.
|
||||
*
|
||||
* If you need to retain the contents of the initialisation vector,
|
||||
* you must either save it manually or use the cipher module
|
||||
* instead.
|
||||
*
|
||||
* \warning For the OFB mode, the initialisation vector must be unique
|
||||
* every encryption operation. Reuse of an initialisation vector
|
||||
* will compromise security.
|
||||
*
|
||||
* \param ctx The AES context to use for encryption or decryption.
|
||||
* It must be initialized and bound to a key.
|
||||
* \param length The length of the input data.
|
||||
* \param iv_off The offset in IV (updated after use).
|
||||
* It must point to a valid \c size_t.
|
||||
* \param iv The initialization vector (updated after use).
|
||||
* It must be a readable and writeable buffer of \c 16 Bytes.
|
||||
* \param input The buffer holding the input data.
|
||||
* It must be readable and of size \p length Bytes.
|
||||
* \param output The buffer holding the output data.
|
||||
* It must be writeable and of size \p length Bytes.
|
||||
*
|
||||
* \return \c 0 on success.
|
||||
*/
|
||||
MBEDTLS_CHECK_RETURN_TYPICAL
|
||||
int mbedtls_aes_crypt_ofb(mbedtls_aes_context *ctx,
|
||||
size_t length,
|
||||
size_t *iv_off,
|
||||
unsigned char iv[16],
|
||||
const unsigned char *input,
|
||||
unsigned char *output);
|
||||
|
||||
#endif /* MBEDTLS_CIPHER_MODE_OFB */
|
||||
|
||||
#if defined(MBEDTLS_CIPHER_MODE_CTR)
|
||||
/**
|
||||
* \brief This function performs an AES-CTR encryption or decryption
|
||||
* operation.
|
||||
*
|
||||
* Due to the nature of CTR, you must use the same key schedule
|
||||
* for both encryption and decryption operations. Therefore, you
|
||||
* must use the context initialized with mbedtls_aes_setkey_enc()
|
||||
* for both #MBEDTLS_AES_ENCRYPT and #MBEDTLS_AES_DECRYPT.
|
||||
*
|
||||
* \warning You must never reuse a nonce value with the same key. Doing so
|
||||
* would void the encryption for the two messages encrypted with
|
||||
* the same nonce and key.
|
||||
*
|
||||
* There are two common strategies for managing nonces with CTR:
|
||||
*
|
||||
* 1. You can handle everything as a single message processed over
|
||||
* successive calls to this function. In that case, you want to
|
||||
* set \p nonce_counter and \p nc_off to 0 for the first call, and
|
||||
* then preserve the values of \p nonce_counter, \p nc_off and \p
|
||||
* stream_block across calls to this function as they will be
|
||||
* updated by this function.
|
||||
*
|
||||
* With this strategy, you must not encrypt more than 2**128
|
||||
* blocks of data with the same key.
|
||||
*
|
||||
* 2. You can encrypt separate messages by dividing the \p
|
||||
* nonce_counter buffer in two areas: the first one used for a
|
||||
* per-message nonce, handled by yourself, and the second one
|
||||
* updated by this function internally.
|
||||
*
|
||||
* For example, you might reserve the first 12 bytes for the
|
||||
* per-message nonce, and the last 4 bytes for internal use. In that
|
||||
* case, before calling this function on a new message you need to
|
||||
* set the first 12 bytes of \p nonce_counter to your chosen nonce
|
||||
* value, the last 4 to 0, and \p nc_off to 0 (which will cause \p
|
||||
* stream_block to be ignored). That way, you can encrypt at most
|
||||
* 2**96 messages of up to 2**32 blocks each with the same key.
|
||||
*
|
||||
* The per-message nonce (or information sufficient to reconstruct
|
||||
* it) needs to be communicated with the ciphertext and must be unique.
|
||||
* The recommended way to ensure uniqueness is to use a message
|
||||
* counter. An alternative is to generate random nonces, but this
|
||||
* limits the number of messages that can be securely encrypted:
|
||||
* for example, with 96-bit random nonces, you should not encrypt
|
||||
* more than 2**32 messages with the same key.
|
||||
*
|
||||
* Note that for both strategies, sizes are measured in blocks and
|
||||
* that an AES block is 16 bytes.
|
||||
*
|
||||
* \warning Upon return, \p stream_block contains sensitive data. Its
|
||||
* content must not be written to insecure storage and should be
|
||||
* securely discarded as soon as it's no longer needed.
|
||||
*
|
||||
* \param ctx The AES context to use for encryption or decryption.
|
||||
* It must be initialized and bound to a key.
|
||||
* \param length The length of the input data.
|
||||
* \param nc_off The offset in the current \p stream_block, for
|
||||
* resuming within the current cipher stream. The
|
||||
* offset pointer should be 0 at the start of a stream.
|
||||
* It must point to a valid \c size_t.
|
||||
* \param nonce_counter The 128-bit nonce and counter.
|
||||
* It must be a readable-writeable buffer of \c 16 Bytes.
|
||||
* \param stream_block The saved stream block for resuming. This is
|
||||
* overwritten by the function.
|
||||
* It must be a readable-writeable buffer of \c 16 Bytes.
|
||||
* \param input The buffer holding the input data.
|
||||
* It must be readable and of size \p length Bytes.
|
||||
* \param output The buffer holding the output data.
|
||||
* It must be writeable and of size \p length Bytes.
|
||||
*
|
||||
* \return \c 0 on success.
|
||||
*/
|
||||
MBEDTLS_CHECK_RETURN_TYPICAL
|
||||
int mbedtls_aes_crypt_ctr(mbedtls_aes_context *ctx,
|
||||
size_t length,
|
||||
size_t *nc_off,
|
||||
unsigned char nonce_counter[16],
|
||||
unsigned char stream_block[16],
|
||||
const unsigned char *input,
|
||||
unsigned char *output);
|
||||
#endif /* MBEDTLS_CIPHER_MODE_CTR */
|
||||
|
||||
/**
|
||||
* \brief Internal AES block encryption function. This is only
|
||||
* exposed to allow overriding it using
|
||||
* \c MBEDTLS_AES_ENCRYPT_ALT.
|
||||
*
|
||||
* \param ctx The AES context to use for encryption.
|
||||
* \param input The plaintext block.
|
||||
* \param output The output (ciphertext) block.
|
||||
*
|
||||
* \return \c 0 on success.
|
||||
*/
|
||||
MBEDTLS_CHECK_RETURN_TYPICAL
|
||||
int mbedtls_internal_aes_encrypt(mbedtls_aes_context *ctx,
|
||||
const unsigned char input[16],
|
||||
unsigned char output[16]);
|
||||
|
||||
#if !defined(MBEDTLS_BLOCK_CIPHER_NO_DECRYPT)
|
||||
/**
|
||||
* \brief Internal AES block decryption function. This is only
|
||||
* exposed to allow overriding it using see
|
||||
* \c MBEDTLS_AES_DECRYPT_ALT.
|
||||
*
|
||||
* \param ctx The AES context to use for decryption.
|
||||
* \param input The ciphertext block.
|
||||
* \param output The output (plaintext) block.
|
||||
*
|
||||
* \return \c 0 on success.
|
||||
*/
|
||||
MBEDTLS_CHECK_RETURN_TYPICAL
|
||||
int mbedtls_internal_aes_decrypt(mbedtls_aes_context *ctx,
|
||||
const unsigned char input[16],
|
||||
unsigned char output[16]);
|
||||
#endif /* !MBEDTLS_BLOCK_CIPHER_NO_DECRYPT */
|
||||
|
||||
#if defined(MBEDTLS_SELF_TEST)
|
||||
/**
|
||||
* \brief Checkup routine.
|
||||
*
|
||||
* \return \c 0 on success.
|
||||
* \return \c 1 on failure.
|
||||
*/
|
||||
MBEDTLS_CHECK_RETURN_CRITICAL
|
||||
int mbedtls_aes_self_test(int verbose);
|
||||
|
||||
#endif /* MBEDTLS_SELF_TEST */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* aes.h */
|
||||
@@ -0,0 +1,642 @@
|
||||
/**
|
||||
* \file asn1.h
|
||||
*
|
||||
* \brief Generic ASN.1 parsing
|
||||
*/
|
||||
/*
|
||||
* Copyright The Mbed TLS Contributors
|
||||
* SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
|
||||
*/
|
||||
#ifndef MBEDTLS_ASN1_H
|
||||
#define MBEDTLS_ASN1_H
|
||||
#include "mbedtls/private_access.h"
|
||||
|
||||
#include "mbedtls/build_info.h"
|
||||
#include "mbedtls/platform_util.h"
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#if defined(MBEDTLS_BIGNUM_C)
|
||||
#include "mbedtls/bignum.h"
|
||||
#endif
|
||||
|
||||
/**
|
||||
* \addtogroup asn1_module
|
||||
* \{
|
||||
*/
|
||||
|
||||
/**
|
||||
* \name ASN1 Error codes
|
||||
* These error codes are combined with other error codes for
|
||||
* higher error granularity.
|
||||
* e.g. X.509 and PKCS #7 error codes
|
||||
* ASN1 is a standard to specify data structures.
|
||||
* \{
|
||||
*/
|
||||
/** Out of data when parsing an ASN1 data structure. */
|
||||
#define MBEDTLS_ERR_ASN1_OUT_OF_DATA -0x0060
|
||||
/** ASN1 tag was of an unexpected value. */
|
||||
#define MBEDTLS_ERR_ASN1_UNEXPECTED_TAG -0x0062
|
||||
/** Error when trying to determine the length or invalid length. */
|
||||
#define MBEDTLS_ERR_ASN1_INVALID_LENGTH -0x0064
|
||||
/** Actual length differs from expected length. */
|
||||
#define MBEDTLS_ERR_ASN1_LENGTH_MISMATCH -0x0066
|
||||
/** Data is invalid. */
|
||||
#define MBEDTLS_ERR_ASN1_INVALID_DATA -0x0068
|
||||
/** Memory allocation failed */
|
||||
#define MBEDTLS_ERR_ASN1_ALLOC_FAILED -0x006A
|
||||
/** Buffer too small when writing ASN.1 data structure. */
|
||||
#define MBEDTLS_ERR_ASN1_BUF_TOO_SMALL -0x006C
|
||||
|
||||
/** \} name ASN1 Error codes */
|
||||
|
||||
/**
|
||||
* \name DER constants
|
||||
* These constants comply with the DER encoded ASN.1 type tags.
|
||||
* DER encoding uses hexadecimal representation.
|
||||
* An example DER sequence is:\n
|
||||
* - 0x02 -- tag indicating INTEGER
|
||||
* - 0x01 -- length in octets
|
||||
* - 0x05 -- value
|
||||
* Such sequences are typically read into \c ::mbedtls_x509_buf.
|
||||
* \{
|
||||
*/
|
||||
#define MBEDTLS_ASN1_BOOLEAN 0x01
|
||||
#define MBEDTLS_ASN1_INTEGER 0x02
|
||||
#define MBEDTLS_ASN1_BIT_STRING 0x03
|
||||
#define MBEDTLS_ASN1_OCTET_STRING 0x04
|
||||
#define MBEDTLS_ASN1_NULL 0x05
|
||||
#define MBEDTLS_ASN1_OID 0x06
|
||||
#define MBEDTLS_ASN1_ENUMERATED 0x0A
|
||||
#define MBEDTLS_ASN1_UTF8_STRING 0x0C
|
||||
#define MBEDTLS_ASN1_SEQUENCE 0x10
|
||||
#define MBEDTLS_ASN1_SET 0x11
|
||||
#define MBEDTLS_ASN1_PRINTABLE_STRING 0x13
|
||||
#define MBEDTLS_ASN1_T61_STRING 0x14
|
||||
#define MBEDTLS_ASN1_IA5_STRING 0x16
|
||||
#define MBEDTLS_ASN1_UTC_TIME 0x17
|
||||
#define MBEDTLS_ASN1_GENERALIZED_TIME 0x18
|
||||
#define MBEDTLS_ASN1_UNIVERSAL_STRING 0x1C
|
||||
#define MBEDTLS_ASN1_BMP_STRING 0x1E
|
||||
#define MBEDTLS_ASN1_PRIMITIVE 0x00
|
||||
#define MBEDTLS_ASN1_CONSTRUCTED 0x20
|
||||
#define MBEDTLS_ASN1_CONTEXT_SPECIFIC 0x80
|
||||
|
||||
/* Slightly smaller way to check if tag is a string tag
|
||||
* compared to canonical implementation. */
|
||||
#define MBEDTLS_ASN1_IS_STRING_TAG(tag) \
|
||||
((unsigned int) (tag) < 32u && ( \
|
||||
((1u << (tag)) & ((1u << MBEDTLS_ASN1_BMP_STRING) | \
|
||||
(1u << MBEDTLS_ASN1_UTF8_STRING) | \
|
||||
(1u << MBEDTLS_ASN1_T61_STRING) | \
|
||||
(1u << MBEDTLS_ASN1_IA5_STRING) | \
|
||||
(1u << MBEDTLS_ASN1_UNIVERSAL_STRING) | \
|
||||
(1u << MBEDTLS_ASN1_PRINTABLE_STRING))) != 0))
|
||||
|
||||
/*
|
||||
* Bit masks for each of the components of an ASN.1 tag as specified in
|
||||
* ITU X.690 (08/2015), section 8.1 "General rules for encoding",
|
||||
* paragraph 8.1.2.2:
|
||||
*
|
||||
* Bit 8 7 6 5 1
|
||||
* +-------+-----+------------+
|
||||
* | Class | P/C | Tag number |
|
||||
* +-------+-----+------------+
|
||||
*/
|
||||
#define MBEDTLS_ASN1_TAG_CLASS_MASK 0xC0
|
||||
#define MBEDTLS_ASN1_TAG_PC_MASK 0x20
|
||||
#define MBEDTLS_ASN1_TAG_VALUE_MASK 0x1F
|
||||
|
||||
/** \} name DER constants */
|
||||
|
||||
/** Returns the size of the binary string, without the trailing \\0 */
|
||||
#define MBEDTLS_OID_SIZE(x) (sizeof(x) - 1)
|
||||
|
||||
/**
|
||||
* Compares an mbedtls_asn1_buf structure to a reference OID.
|
||||
*
|
||||
* Only works for 'defined' oid_str values (MBEDTLS_OID_HMAC_SHA1), you cannot use a
|
||||
* 'unsigned char *oid' here!
|
||||
*/
|
||||
#define MBEDTLS_OID_CMP(oid_str, oid_buf) \
|
||||
((MBEDTLS_OID_SIZE(oid_str) != (oid_buf)->len) || \
|
||||
memcmp((oid_str), (oid_buf)->p, (oid_buf)->len) != 0)
|
||||
|
||||
#define MBEDTLS_OID_CMP_RAW(oid_str, oid_buf, oid_buf_len) \
|
||||
((MBEDTLS_OID_SIZE(oid_str) != (oid_buf_len)) || \
|
||||
memcmp((oid_str), (oid_buf), (oid_buf_len)) != 0)
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* \name Functions to parse ASN.1 data structures
|
||||
* \{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Type-length-value structure that allows for ASN1 using DER.
|
||||
*/
|
||||
typedef struct mbedtls_asn1_buf {
|
||||
int tag; /**< ASN1 type, e.g. MBEDTLS_ASN1_UTF8_STRING. */
|
||||
size_t len; /**< ASN1 length, in octets. */
|
||||
unsigned char *p; /**< ASN1 data, e.g. in ASCII. */
|
||||
}
|
||||
mbedtls_asn1_buf;
|
||||
|
||||
/**
|
||||
* Container for ASN1 bit strings.
|
||||
*/
|
||||
typedef struct mbedtls_asn1_bitstring {
|
||||
size_t len; /**< ASN1 length, in octets. */
|
||||
unsigned char unused_bits; /**< Number of unused bits at the end of the string */
|
||||
unsigned char *p; /**< Raw ASN1 data for the bit string */
|
||||
}
|
||||
mbedtls_asn1_bitstring;
|
||||
|
||||
/**
|
||||
* Container for a sequence of ASN.1 items
|
||||
*/
|
||||
typedef struct mbedtls_asn1_sequence {
|
||||
mbedtls_asn1_buf buf; /**< Buffer containing the given ASN.1 item. */
|
||||
|
||||
/** The next entry in the sequence.
|
||||
*
|
||||
* The details of memory management for sequences are not documented and
|
||||
* may change in future versions. Set this field to \p NULL when
|
||||
* initializing a structure, and do not modify it except via Mbed TLS
|
||||
* library functions.
|
||||
*/
|
||||
struct mbedtls_asn1_sequence *next;
|
||||
}
|
||||
mbedtls_asn1_sequence;
|
||||
|
||||
/**
|
||||
* Container for a sequence or list of 'named' ASN.1 data items
|
||||
*/
|
||||
typedef struct mbedtls_asn1_named_data {
|
||||
mbedtls_asn1_buf oid; /**< The object identifier. */
|
||||
mbedtls_asn1_buf val; /**< The named value. */
|
||||
|
||||
/** The next entry in the sequence.
|
||||
*
|
||||
* The details of memory management for named data sequences are not
|
||||
* documented and may change in future versions. Set this field to \p NULL
|
||||
* when initializing a structure, and do not modify it except via Mbed TLS
|
||||
* library functions.
|
||||
*/
|
||||
struct mbedtls_asn1_named_data *next;
|
||||
|
||||
/** Merge next item into the current one?
|
||||
*
|
||||
* This field exists for the sake of Mbed TLS's X.509 certificate parsing
|
||||
* code and may change in future versions of the library.
|
||||
*/
|
||||
unsigned char MBEDTLS_PRIVATE(next_merged);
|
||||
}
|
||||
mbedtls_asn1_named_data;
|
||||
|
||||
#if defined(MBEDTLS_ASN1_PARSE_C) || defined(MBEDTLS_X509_CREATE_C) || \
|
||||
defined(MBEDTLS_PSA_UTIL_HAVE_ECDSA)
|
||||
/**
|
||||
* \brief Get the length of an ASN.1 element.
|
||||
* Updates the pointer to immediately behind the length.
|
||||
*
|
||||
* \param p On entry, \c *p points to the first byte of the length,
|
||||
* i.e. immediately after the tag.
|
||||
* On successful completion, \c *p points to the first byte
|
||||
* after the length, i.e. the first byte of the content.
|
||||
* On error, the value of \c *p is undefined.
|
||||
* \param end End of data.
|
||||
* \param len On successful completion, \c *len contains the length
|
||||
* read from the ASN.1 input.
|
||||
*
|
||||
* \return 0 if successful.
|
||||
* \return #MBEDTLS_ERR_ASN1_OUT_OF_DATA if the ASN.1 element
|
||||
* would end beyond \p end.
|
||||
* \return #MBEDTLS_ERR_ASN1_INVALID_LENGTH if the length is unparsable.
|
||||
*/
|
||||
int mbedtls_asn1_get_len(unsigned char **p,
|
||||
const unsigned char *end,
|
||||
size_t *len);
|
||||
|
||||
/**
|
||||
* \brief Get the tag and length of the element.
|
||||
* Check for the requested tag.
|
||||
* Updates the pointer to immediately behind the tag and length.
|
||||
*
|
||||
* \param p On entry, \c *p points to the start of the ASN.1 element.
|
||||
* On successful completion, \c *p points to the first byte
|
||||
* after the length, i.e. the first byte of the content.
|
||||
* On error, the value of \c *p is undefined.
|
||||
* \param end End of data.
|
||||
* \param len On successful completion, \c *len contains the length
|
||||
* read from the ASN.1 input.
|
||||
* \param tag The expected tag.
|
||||
*
|
||||
* \return 0 if successful.
|
||||
* \return #MBEDTLS_ERR_ASN1_UNEXPECTED_TAG if the data does not start
|
||||
* with the requested tag.
|
||||
* \return #MBEDTLS_ERR_ASN1_OUT_OF_DATA if the ASN.1 element
|
||||
* would end beyond \p end.
|
||||
* \return #MBEDTLS_ERR_ASN1_INVALID_LENGTH if the length is unparsable.
|
||||
*/
|
||||
int mbedtls_asn1_get_tag(unsigned char **p,
|
||||
const unsigned char *end,
|
||||
size_t *len, int tag);
|
||||
#endif /* MBEDTLS_ASN1_PARSE_C || MBEDTLS_X509_CREATE_C || MBEDTLS_PSA_UTIL_HAVE_ECDSA */
|
||||
|
||||
#if defined(MBEDTLS_ASN1_PARSE_C)
|
||||
/**
|
||||
* \brief Retrieve a boolean ASN.1 tag and its value.
|
||||
* Updates the pointer to immediately behind the full tag.
|
||||
*
|
||||
* \param p On entry, \c *p points to the start of the ASN.1 element.
|
||||
* On successful completion, \c *p points to the first byte
|
||||
* beyond the ASN.1 element.
|
||||
* On error, the value of \c *p is undefined.
|
||||
* \param end End of data.
|
||||
* \param val On success, the parsed value (\c 0 or \c 1).
|
||||
*
|
||||
* \return 0 if successful.
|
||||
* \return An ASN.1 error code if the input does not start with
|
||||
* a valid ASN.1 BOOLEAN.
|
||||
*/
|
||||
int mbedtls_asn1_get_bool(unsigned char **p,
|
||||
const unsigned char *end,
|
||||
int *val);
|
||||
|
||||
/**
|
||||
* \brief Retrieve an integer ASN.1 tag and its value.
|
||||
* Updates the pointer to immediately behind the full tag.
|
||||
*
|
||||
* \param p On entry, \c *p points to the start of the ASN.1 element.
|
||||
* On successful completion, \c *p points to the first byte
|
||||
* beyond the ASN.1 element.
|
||||
* On error, the value of \c *p is undefined.
|
||||
* \param end End of data.
|
||||
* \param val On success, the parsed value.
|
||||
*
|
||||
* \return 0 if successful.
|
||||
* \return An ASN.1 error code if the input does not start with
|
||||
* a valid ASN.1 INTEGER.
|
||||
* \return #MBEDTLS_ERR_ASN1_INVALID_LENGTH if the parsed value does
|
||||
* not fit in an \c int.
|
||||
*/
|
||||
int mbedtls_asn1_get_int(unsigned char **p,
|
||||
const unsigned char *end,
|
||||
int *val);
|
||||
|
||||
/**
|
||||
* \brief Retrieve an enumerated ASN.1 tag and its value.
|
||||
* Updates the pointer to immediately behind the full tag.
|
||||
*
|
||||
* \param p On entry, \c *p points to the start of the ASN.1 element.
|
||||
* On successful completion, \c *p points to the first byte
|
||||
* beyond the ASN.1 element.
|
||||
* On error, the value of \c *p is undefined.
|
||||
* \param end End of data.
|
||||
* \param val On success, the parsed value.
|
||||
*
|
||||
* \return 0 if successful.
|
||||
* \return An ASN.1 error code if the input does not start with
|
||||
* a valid ASN.1 ENUMERATED.
|
||||
* \return #MBEDTLS_ERR_ASN1_INVALID_LENGTH if the parsed value does
|
||||
* not fit in an \c int.
|
||||
*/
|
||||
int mbedtls_asn1_get_enum(unsigned char **p,
|
||||
const unsigned char *end,
|
||||
int *val);
|
||||
|
||||
/**
|
||||
* \brief Retrieve a bitstring ASN.1 tag and its value.
|
||||
* Updates the pointer to immediately behind the full tag.
|
||||
*
|
||||
* \param p On entry, \c *p points to the start of the ASN.1 element.
|
||||
* On successful completion, \c *p is equal to \p end.
|
||||
* On error, the value of \c *p is undefined.
|
||||
* \param end End of data.
|
||||
* \param bs On success, ::mbedtls_asn1_bitstring information about
|
||||
* the parsed value.
|
||||
*
|
||||
* \return 0 if successful.
|
||||
* \return #MBEDTLS_ERR_ASN1_LENGTH_MISMATCH if the input contains
|
||||
* extra data after a valid BIT STRING.
|
||||
* \return An ASN.1 error code if the input does not start with
|
||||
* a valid ASN.1 BIT STRING.
|
||||
*/
|
||||
int mbedtls_asn1_get_bitstring(unsigned char **p, const unsigned char *end,
|
||||
mbedtls_asn1_bitstring *bs);
|
||||
|
||||
/**
|
||||
* \brief Retrieve a bitstring ASN.1 tag without unused bits and its
|
||||
* value.
|
||||
* Updates the pointer to the beginning of the bit/octet string.
|
||||
*
|
||||
* \param p On entry, \c *p points to the start of the ASN.1 element.
|
||||
* On successful completion, \c *p points to the first byte
|
||||
* of the content of the BIT STRING.
|
||||
* On error, the value of \c *p is undefined.
|
||||
* \param end End of data.
|
||||
* \param len On success, \c *len is the length of the content in bytes.
|
||||
*
|
||||
* \return 0 if successful.
|
||||
* \return #MBEDTLS_ERR_ASN1_INVALID_DATA if the input starts with
|
||||
* a valid BIT STRING with a nonzero number of unused bits.
|
||||
* \return An ASN.1 error code if the input does not start with
|
||||
* a valid ASN.1 BIT STRING.
|
||||
*/
|
||||
int mbedtls_asn1_get_bitstring_null(unsigned char **p,
|
||||
const unsigned char *end,
|
||||
size_t *len);
|
||||
|
||||
/**
|
||||
* \brief Parses and splits an ASN.1 "SEQUENCE OF <tag>".
|
||||
* Updates the pointer to immediately behind the full sequence tag.
|
||||
*
|
||||
* This function allocates memory for the sequence elements. You can free
|
||||
* the allocated memory with mbedtls_asn1_sequence_free().
|
||||
*
|
||||
* \note On error, this function may return a partial list in \p cur.
|
||||
* You must set `cur->next = NULL` before calling this function!
|
||||
* Otherwise it is impossible to distinguish a previously non-null
|
||||
* pointer from a pointer to an object allocated by this function.
|
||||
*
|
||||
* \note If the sequence is empty, this function does not modify
|
||||
* \c *cur. If the sequence is valid and non-empty, this
|
||||
* function sets `cur->buf.tag` to \p tag. This allows
|
||||
* callers to distinguish between an empty sequence and
|
||||
* a one-element sequence.
|
||||
*
|
||||
* \param p On entry, \c *p points to the start of the ASN.1 element.
|
||||
* On successful completion, \c *p is equal to \p end.
|
||||
* On error, the value of \c *p is undefined.
|
||||
* \param end End of data.
|
||||
* \param cur A ::mbedtls_asn1_sequence which this function fills.
|
||||
* When this function returns, \c *cur is the head of a linked
|
||||
* list. Each node in this list is allocated with
|
||||
* mbedtls_calloc() apart from \p cur itself, and should
|
||||
* therefore be freed with mbedtls_free().
|
||||
* The list describes the content of the sequence.
|
||||
* The head of the list (i.e. \c *cur itself) describes the
|
||||
* first element, `*cur->next` describes the second element, etc.
|
||||
* For each element, `buf.tag == tag`, `buf.len` is the length
|
||||
* of the content of the content of the element, and `buf.p`
|
||||
* points to the first byte of the content (i.e. immediately
|
||||
* past the length of the element).
|
||||
* Note that list elements may be allocated even on error.
|
||||
* \param tag Each element of the sequence must have this tag.
|
||||
*
|
||||
* \return 0 if successful.
|
||||
* \return #MBEDTLS_ERR_ASN1_LENGTH_MISMATCH if the input contains
|
||||
* extra data after a valid SEQUENCE OF \p tag.
|
||||
* \return #MBEDTLS_ERR_ASN1_UNEXPECTED_TAG if the input starts with
|
||||
* an ASN.1 SEQUENCE in which an element has a tag that
|
||||
* is different from \p tag.
|
||||
* \return #MBEDTLS_ERR_ASN1_ALLOC_FAILED if a memory allocation failed.
|
||||
* \return An ASN.1 error code if the input does not start with
|
||||
* a valid ASN.1 SEQUENCE.
|
||||
*/
|
||||
int mbedtls_asn1_get_sequence_of(unsigned char **p,
|
||||
const unsigned char *end,
|
||||
mbedtls_asn1_sequence *cur,
|
||||
int tag);
|
||||
/**
|
||||
* \brief Free a heap-allocated linked list presentation of
|
||||
* an ASN.1 sequence, including the first element.
|
||||
*
|
||||
* There are two common ways to manage the memory used for the representation
|
||||
* of a parsed ASN.1 sequence:
|
||||
* - Allocate a head node `mbedtls_asn1_sequence *head` with mbedtls_calloc().
|
||||
* Pass this node as the `cur` argument to mbedtls_asn1_get_sequence_of().
|
||||
* When you have finished processing the sequence,
|
||||
* call mbedtls_asn1_sequence_free() on `head`.
|
||||
* - Allocate a head node `mbedtls_asn1_sequence *head` in any manner,
|
||||
* for example on the stack. Make sure that `head->next == NULL`.
|
||||
* Pass `head` as the `cur` argument to mbedtls_asn1_get_sequence_of().
|
||||
* When you have finished processing the sequence,
|
||||
* call mbedtls_asn1_sequence_free() on `head->cur`,
|
||||
* then free `head` itself in the appropriate manner.
|
||||
*
|
||||
* \param seq The address of the first sequence component. This may
|
||||
* be \c NULL, in which case this functions returns
|
||||
* immediately.
|
||||
*/
|
||||
void mbedtls_asn1_sequence_free(mbedtls_asn1_sequence *seq);
|
||||
|
||||
/**
|
||||
* \brief Traverse an ASN.1 SEQUENCE container and
|
||||
* call a callback for each entry.
|
||||
*
|
||||
* This function checks that the input is a SEQUENCE of elements that
|
||||
* each have a "must" tag, and calls a callback function on the elements
|
||||
* that have a "may" tag.
|
||||
*
|
||||
* For example, to validate that the input is a SEQUENCE of `tag1` and call
|
||||
* `cb` on each element, use
|
||||
* ```
|
||||
* mbedtls_asn1_traverse_sequence_of(&p, end, 0xff, tag1, 0, 0, cb, ctx);
|
||||
* ```
|
||||
*
|
||||
* To validate that the input is a SEQUENCE of ANY and call `cb` on
|
||||
* each element, use
|
||||
* ```
|
||||
* mbedtls_asn1_traverse_sequence_of(&p, end, 0, 0, 0, 0, cb, ctx);
|
||||
* ```
|
||||
*
|
||||
* To validate that the input is a SEQUENCE of CHOICE {NULL, OCTET STRING}
|
||||
* and call `cb` on each element that is an OCTET STRING, use
|
||||
* ```
|
||||
* mbedtls_asn1_traverse_sequence_of(&p, end, 0xfe, 0x04, 0xff, 0x04, cb, ctx);
|
||||
* ```
|
||||
*
|
||||
* The callback is called on the elements with a "may" tag from left to
|
||||
* right. If the input is not a valid SEQUENCE of elements with a "must" tag,
|
||||
* the callback is called on the elements up to the leftmost point where
|
||||
* the input is invalid.
|
||||
*
|
||||
* \warning This function is still experimental and may change
|
||||
* at any time.
|
||||
*
|
||||
* \param p The address of the pointer to the beginning of
|
||||
* the ASN.1 SEQUENCE header. This is updated to
|
||||
* point to the end of the ASN.1 SEQUENCE container
|
||||
* on a successful invocation.
|
||||
* \param end The end of the ASN.1 SEQUENCE container.
|
||||
* \param tag_must_mask A mask to be applied to the ASN.1 tags found within
|
||||
* the SEQUENCE before comparing to \p tag_must_val.
|
||||
* \param tag_must_val The required value of each ASN.1 tag found in the
|
||||
* SEQUENCE, after masking with \p tag_must_mask.
|
||||
* Mismatching tags lead to an error.
|
||||
* For example, a value of \c 0 for both \p tag_must_mask
|
||||
* and \p tag_must_val means that every tag is allowed,
|
||||
* while a value of \c 0xFF for \p tag_must_mask means
|
||||
* that \p tag_must_val is the only allowed tag.
|
||||
* \param tag_may_mask A mask to be applied to the ASN.1 tags found within
|
||||
* the SEQUENCE before comparing to \p tag_may_val.
|
||||
* \param tag_may_val The desired value of each ASN.1 tag found in the
|
||||
* SEQUENCE, after masking with \p tag_may_mask.
|
||||
* Mismatching tags will be silently ignored.
|
||||
* For example, a value of \c 0 for \p tag_may_mask and
|
||||
* \p tag_may_val means that any tag will be considered,
|
||||
* while a value of \c 0xFF for \p tag_may_mask means
|
||||
* that all tags with value different from \p tag_may_val
|
||||
* will be ignored.
|
||||
* \param cb The callback to trigger for each component
|
||||
* in the ASN.1 SEQUENCE that matches \p tag_may_val.
|
||||
* The callback function is called with the following
|
||||
* parameters:
|
||||
* - \p ctx.
|
||||
* - The tag of the current element.
|
||||
* - A pointer to the start of the current element's
|
||||
* content inside the input.
|
||||
* - The length of the content of the current element.
|
||||
* If the callback returns a non-zero value,
|
||||
* the function stops immediately,
|
||||
* forwarding the callback's return value.
|
||||
* \param ctx The context to be passed to the callback \p cb.
|
||||
*
|
||||
* \return \c 0 if successful the entire ASN.1 SEQUENCE
|
||||
* was traversed without parsing or callback errors.
|
||||
* \return #MBEDTLS_ERR_ASN1_LENGTH_MISMATCH if the input
|
||||
* contains extra data after a valid SEQUENCE
|
||||
* of elements with an accepted tag.
|
||||
* \return #MBEDTLS_ERR_ASN1_UNEXPECTED_TAG if the input starts
|
||||
* with an ASN.1 SEQUENCE in which an element has a tag
|
||||
* that is not accepted.
|
||||
* \return An ASN.1 error code if the input does not start with
|
||||
* a valid ASN.1 SEQUENCE.
|
||||
* \return A non-zero error code forwarded from the callback
|
||||
* \p cb in case the latter returns a non-zero value.
|
||||
*/
|
||||
int mbedtls_asn1_traverse_sequence_of(
|
||||
unsigned char **p,
|
||||
const unsigned char *end,
|
||||
unsigned char tag_must_mask, unsigned char tag_must_val,
|
||||
unsigned char tag_may_mask, unsigned char tag_may_val,
|
||||
int (*cb)(void *ctx, int tag,
|
||||
unsigned char *start, size_t len),
|
||||
void *ctx);
|
||||
|
||||
#if defined(MBEDTLS_BIGNUM_C)
|
||||
/**
|
||||
* \brief Retrieve an integer ASN.1 tag and its value.
|
||||
* Updates the pointer to immediately behind the full tag.
|
||||
*
|
||||
* \param p On entry, \c *p points to the start of the ASN.1 element.
|
||||
* On successful completion, \c *p points to the first byte
|
||||
* beyond the ASN.1 element.
|
||||
* On error, the value of \c *p is undefined.
|
||||
* \param end End of data.
|
||||
* \param X On success, the parsed value.
|
||||
*
|
||||
* \return 0 if successful.
|
||||
* \return An ASN.1 error code if the input does not start with
|
||||
* a valid ASN.1 INTEGER.
|
||||
* \return #MBEDTLS_ERR_ASN1_INVALID_LENGTH if the parsed value does
|
||||
* not fit in an \c int.
|
||||
* \return An MPI error code if the parsed value is too large.
|
||||
*/
|
||||
int mbedtls_asn1_get_mpi(unsigned char **p,
|
||||
const unsigned char *end,
|
||||
mbedtls_mpi *X);
|
||||
#endif /* MBEDTLS_BIGNUM_C */
|
||||
|
||||
/**
|
||||
* \brief Retrieve an AlgorithmIdentifier ASN.1 sequence.
|
||||
* Updates the pointer to immediately behind the full
|
||||
* AlgorithmIdentifier.
|
||||
*
|
||||
* \param p On entry, \c *p points to the start of the ASN.1 element.
|
||||
* On successful completion, \c *p points to the first byte
|
||||
* beyond the AlgorithmIdentifier element.
|
||||
* On error, the value of \c *p is undefined.
|
||||
* \param end End of data.
|
||||
* \param alg The buffer to receive the OID.
|
||||
* \param params The buffer to receive the parameters.
|
||||
* This is zeroized if there are no parameters.
|
||||
*
|
||||
* \return 0 if successful or a specific ASN.1 or MPI error code.
|
||||
*/
|
||||
int mbedtls_asn1_get_alg(unsigned char **p,
|
||||
const unsigned char *end,
|
||||
mbedtls_asn1_buf *alg, mbedtls_asn1_buf *params);
|
||||
|
||||
/**
|
||||
* \brief Retrieve an AlgorithmIdentifier ASN.1 sequence with NULL or no
|
||||
* params.
|
||||
* Updates the pointer to immediately behind the full
|
||||
* AlgorithmIdentifier.
|
||||
*
|
||||
* \param p On entry, \c *p points to the start of the ASN.1 element.
|
||||
* On successful completion, \c *p points to the first byte
|
||||
* beyond the AlgorithmIdentifier element.
|
||||
* On error, the value of \c *p is undefined.
|
||||
* \param end End of data.
|
||||
* \param alg The buffer to receive the OID.
|
||||
*
|
||||
* \return 0 if successful or a specific ASN.1 or MPI error code.
|
||||
*/
|
||||
int mbedtls_asn1_get_alg_null(unsigned char **p,
|
||||
const unsigned char *end,
|
||||
mbedtls_asn1_buf *alg);
|
||||
|
||||
/**
|
||||
* \brief Find a specific named_data entry in a sequence or list based on
|
||||
* the OID.
|
||||
*
|
||||
* \param list The list to seek through
|
||||
* \param oid The OID to look for
|
||||
* \param len Size of the OID
|
||||
*
|
||||
* \return NULL if not found, or a pointer to the existing entry.
|
||||
*/
|
||||
const mbedtls_asn1_named_data *mbedtls_asn1_find_named_data(const mbedtls_asn1_named_data *list,
|
||||
const char *oid, size_t len);
|
||||
|
||||
#if !defined(MBEDTLS_DEPRECATED_REMOVED)
|
||||
/**
|
||||
* \brief Free a mbedtls_asn1_named_data entry
|
||||
*
|
||||
* \deprecated This function is deprecated and will be removed in a
|
||||
* future version of the library.
|
||||
* Please use mbedtls_asn1_free_named_data_list()
|
||||
* or mbedtls_asn1_free_named_data_list_shallow().
|
||||
*
|
||||
* \param entry The named data entry to free.
|
||||
* This function calls mbedtls_free() on
|
||||
* `entry->oid.p` and `entry->val.p`.
|
||||
*/
|
||||
void MBEDTLS_DEPRECATED mbedtls_asn1_free_named_data(mbedtls_asn1_named_data *entry);
|
||||
#endif /* MBEDTLS_DEPRECATED_REMOVED */
|
||||
|
||||
/**
|
||||
* \brief Free all entries in a mbedtls_asn1_named_data list.
|
||||
*
|
||||
* \param head Pointer to the head of the list of named data entries to free.
|
||||
* This function calls mbedtls_free() on
|
||||
* `entry->oid.p` and `entry->val.p` and then on `entry`
|
||||
* for each list entry, and sets \c *head to \c NULL.
|
||||
*/
|
||||
void mbedtls_asn1_free_named_data_list(mbedtls_asn1_named_data **head);
|
||||
|
||||
/**
|
||||
* \brief Free all shallow entries in a mbedtls_asn1_named_data list,
|
||||
* but do not free internal pointer targets.
|
||||
*
|
||||
* \param name Head of the list of named data entries to free.
|
||||
* This function calls mbedtls_free() on each list element.
|
||||
*/
|
||||
void mbedtls_asn1_free_named_data_list_shallow(mbedtls_asn1_named_data *name);
|
||||
|
||||
/** \} name Functions to parse ASN.1 data structures */
|
||||
/** \} addtogroup asn1_module */
|
||||
|
||||
#endif /* MBEDTLS_ASN1_PARSE_C */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* asn1.h */
|
||||
@@ -0,0 +1,390 @@
|
||||
/**
|
||||
* \file asn1write.h
|
||||
*
|
||||
* \brief ASN.1 buffer writing functionality
|
||||
*/
|
||||
/*
|
||||
* Copyright The Mbed TLS Contributors
|
||||
* SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
|
||||
*/
|
||||
#ifndef MBEDTLS_ASN1_WRITE_H
|
||||
#define MBEDTLS_ASN1_WRITE_H
|
||||
|
||||
#include "mbedtls/build_info.h"
|
||||
|
||||
#include "mbedtls/asn1.h"
|
||||
|
||||
#define MBEDTLS_ASN1_CHK_ADD(g, f) \
|
||||
do \
|
||||
{ \
|
||||
if ((ret = (f)) < 0) \
|
||||
return ret; \
|
||||
else \
|
||||
(g) += ret; \
|
||||
} while (0)
|
||||
|
||||
#define MBEDTLS_ASN1_CHK_CLEANUP_ADD(g, f) \
|
||||
do \
|
||||
{ \
|
||||
if ((ret = (f)) < 0) \
|
||||
goto cleanup; \
|
||||
else \
|
||||
(g) += ret; \
|
||||
} while (0)
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#if defined(MBEDTLS_ASN1_WRITE_C) || defined(MBEDTLS_X509_USE_C) || \
|
||||
defined(MBEDTLS_PSA_UTIL_HAVE_ECDSA)
|
||||
/**
|
||||
* \brief Write a length field in ASN.1 format.
|
||||
*
|
||||
* \note This function works backwards in data buffer.
|
||||
*
|
||||
* \param p The reference to the current position pointer.
|
||||
* \param start The start of the buffer, for bounds-checking.
|
||||
* \param len The length value to write.
|
||||
*
|
||||
* \return The number of bytes written to \p p on success.
|
||||
* \return A negative \c MBEDTLS_ERR_ASN1_XXX error code on failure.
|
||||
*/
|
||||
int mbedtls_asn1_write_len(unsigned char **p, const unsigned char *start,
|
||||
size_t len);
|
||||
/**
|
||||
* \brief Write an ASN.1 tag in ASN.1 format.
|
||||
*
|
||||
* \note This function works backwards in data buffer.
|
||||
*
|
||||
* \param p The reference to the current position pointer.
|
||||
* \param start The start of the buffer, for bounds-checking.
|
||||
* \param tag The tag to write.
|
||||
*
|
||||
* \return The number of bytes written to \p p on success.
|
||||
* \return A negative \c MBEDTLS_ERR_ASN1_XXX error code on failure.
|
||||
*/
|
||||
int mbedtls_asn1_write_tag(unsigned char **p, const unsigned char *start,
|
||||
unsigned char tag);
|
||||
#endif /* MBEDTLS_ASN1_WRITE_C || MBEDTLS_X509_USE_C || MBEDTLS_PSA_UTIL_HAVE_ECDSA*/
|
||||
|
||||
#if defined(MBEDTLS_ASN1_WRITE_C)
|
||||
/**
|
||||
* \brief Write raw buffer data.
|
||||
*
|
||||
* \note This function works backwards in data buffer.
|
||||
*
|
||||
* \param p The reference to the current position pointer.
|
||||
* \param start The start of the buffer, for bounds-checking.
|
||||
* \param buf The data buffer to write.
|
||||
* \param size The length of the data buffer.
|
||||
*
|
||||
* \return The number of bytes written to \p p on success.
|
||||
* \return A negative \c MBEDTLS_ERR_ASN1_XXX error code on failure.
|
||||
*/
|
||||
int mbedtls_asn1_write_raw_buffer(unsigned char **p, const unsigned char *start,
|
||||
const unsigned char *buf, size_t size);
|
||||
|
||||
#if defined(MBEDTLS_BIGNUM_C)
|
||||
/**
|
||||
* \brief Write an arbitrary-precision number (#MBEDTLS_ASN1_INTEGER)
|
||||
* in ASN.1 format.
|
||||
*
|
||||
* \note This function works backwards in data buffer.
|
||||
*
|
||||
* \param p The reference to the current position pointer.
|
||||
* \param start The start of the buffer, for bounds-checking.
|
||||
* \param X The MPI to write.
|
||||
* It must be non-negative.
|
||||
*
|
||||
* \return The number of bytes written to \p p on success.
|
||||
* \return A negative \c MBEDTLS_ERR_ASN1_XXX error code on failure.
|
||||
*/
|
||||
int mbedtls_asn1_write_mpi(unsigned char **p, const unsigned char *start,
|
||||
const mbedtls_mpi *X);
|
||||
#endif /* MBEDTLS_BIGNUM_C */
|
||||
|
||||
/**
|
||||
* \brief Write a NULL tag (#MBEDTLS_ASN1_NULL) with zero data
|
||||
* in ASN.1 format.
|
||||
*
|
||||
* \note This function works backwards in data buffer.
|
||||
*
|
||||
* \param p The reference to the current position pointer.
|
||||
* \param start The start of the buffer, for bounds-checking.
|
||||
*
|
||||
* \return The number of bytes written to \p p on success.
|
||||
* \return A negative \c MBEDTLS_ERR_ASN1_XXX error code on failure.
|
||||
*/
|
||||
int mbedtls_asn1_write_null(unsigned char **p, const unsigned char *start);
|
||||
|
||||
/**
|
||||
* \brief Write an OID tag (#MBEDTLS_ASN1_OID) and data
|
||||
* in ASN.1 format.
|
||||
*
|
||||
* \note This function works backwards in data buffer.
|
||||
*
|
||||
* \param p The reference to the current position pointer.
|
||||
* \param start The start of the buffer, for bounds-checking.
|
||||
* \param oid The OID to write.
|
||||
* \param oid_len The length of the OID.
|
||||
*
|
||||
* \return The number of bytes written to \p p on success.
|
||||
* \return A negative \c MBEDTLS_ERR_ASN1_XXX error code on failure.
|
||||
*/
|
||||
int mbedtls_asn1_write_oid(unsigned char **p, const unsigned char *start,
|
||||
const char *oid, size_t oid_len);
|
||||
|
||||
/**
|
||||
* \brief Write an AlgorithmIdentifier sequence in ASN.1 format.
|
||||
*
|
||||
* \note This function works backwards in data buffer.
|
||||
*
|
||||
* \param p The reference to the current position pointer.
|
||||
* \param start The start of the buffer, for bounds-checking.
|
||||
* \param oid The OID of the algorithm to write.
|
||||
* \param oid_len The length of the algorithm's OID.
|
||||
* \param par_len The length of the parameters, which must be already written.
|
||||
* If 0, NULL parameters are added
|
||||
*
|
||||
* \return The number of bytes written to \p p on success.
|
||||
* \return A negative \c MBEDTLS_ERR_ASN1_XXX error code on failure.
|
||||
*/
|
||||
int mbedtls_asn1_write_algorithm_identifier(unsigned char **p,
|
||||
const unsigned char *start,
|
||||
const char *oid, size_t oid_len,
|
||||
size_t par_len);
|
||||
|
||||
/**
|
||||
* \brief Write an AlgorithmIdentifier sequence in ASN.1 format.
|
||||
*
|
||||
* \note This function works backwards in data buffer.
|
||||
*
|
||||
* \param p The reference to the current position pointer.
|
||||
* \param start The start of the buffer, for bounds-checking.
|
||||
* \param oid The OID of the algorithm to write.
|
||||
* \param oid_len The length of the algorithm's OID.
|
||||
* \param par_len The length of the parameters, which must be already written.
|
||||
* \param has_par If there are any parameters. If 0, par_len must be 0. If 1
|
||||
* and \p par_len is 0, NULL parameters are added.
|
||||
*
|
||||
* \return The number of bytes written to \p p on success.
|
||||
* \return A negative \c MBEDTLS_ERR_ASN1_XXX error code on failure.
|
||||
*/
|
||||
int mbedtls_asn1_write_algorithm_identifier_ext(unsigned char **p,
|
||||
const unsigned char *start,
|
||||
const char *oid, size_t oid_len,
|
||||
size_t par_len, int has_par);
|
||||
|
||||
/**
|
||||
* \brief Write a boolean tag (#MBEDTLS_ASN1_BOOLEAN) and value
|
||||
* in ASN.1 format.
|
||||
*
|
||||
* \note This function works backwards in data buffer.
|
||||
*
|
||||
* \param p The reference to the current position pointer.
|
||||
* \param start The start of the buffer, for bounds-checking.
|
||||
* \param boolean The boolean value to write, either \c 0 or \c 1.
|
||||
*
|
||||
* \return The number of bytes written to \p p on success.
|
||||
* \return A negative \c MBEDTLS_ERR_ASN1_XXX error code on failure.
|
||||
*/
|
||||
int mbedtls_asn1_write_bool(unsigned char **p, const unsigned char *start,
|
||||
int boolean);
|
||||
|
||||
/**
|
||||
* \brief Write an int tag (#MBEDTLS_ASN1_INTEGER) and value
|
||||
* in ASN.1 format.
|
||||
*
|
||||
* \note This function works backwards in data buffer.
|
||||
*
|
||||
* \param p The reference to the current position pointer.
|
||||
* \param start The start of the buffer, for bounds-checking.
|
||||
* \param val The integer value to write.
|
||||
* It must be non-negative.
|
||||
*
|
||||
* \return The number of bytes written to \p p on success.
|
||||
* \return A negative \c MBEDTLS_ERR_ASN1_XXX error code on failure.
|
||||
*/
|
||||
int mbedtls_asn1_write_int(unsigned char **p, const unsigned char *start, int val);
|
||||
|
||||
/**
|
||||
* \brief Write an enum tag (#MBEDTLS_ASN1_ENUMERATED) and value
|
||||
* in ASN.1 format.
|
||||
*
|
||||
* \note This function works backwards in data buffer.
|
||||
*
|
||||
* \param p The reference to the current position pointer.
|
||||
* \param start The start of the buffer, for bounds-checking.
|
||||
* \param val The integer value to write.
|
||||
*
|
||||
* \return The number of bytes written to \p p on success.
|
||||
* \return A negative \c MBEDTLS_ERR_ASN1_XXX error code on failure.
|
||||
*/
|
||||
int mbedtls_asn1_write_enum(unsigned char **p, const unsigned char *start, int val);
|
||||
|
||||
/**
|
||||
* \brief Write a string in ASN.1 format using a specific
|
||||
* string encoding tag.
|
||||
|
||||
* \note This function works backwards in data buffer.
|
||||
*
|
||||
* \param p The reference to the current position pointer.
|
||||
* \param start The start of the buffer, for bounds-checking.
|
||||
* \param tag The string encoding tag to write, e.g.
|
||||
* #MBEDTLS_ASN1_UTF8_STRING.
|
||||
* \param text The string to write.
|
||||
* \param text_len The length of \p text in bytes (which might
|
||||
* be strictly larger than the number of characters).
|
||||
*
|
||||
* \return The number of bytes written to \p p on success.
|
||||
* \return A negative error code on failure.
|
||||
*/
|
||||
int mbedtls_asn1_write_tagged_string(unsigned char **p, const unsigned char *start,
|
||||
int tag, const char *text,
|
||||
size_t text_len);
|
||||
|
||||
/**
|
||||
* \brief Write a string in ASN.1 format using the PrintableString
|
||||
* string encoding tag (#MBEDTLS_ASN1_PRINTABLE_STRING).
|
||||
*
|
||||
* \note This function works backwards in data buffer.
|
||||
*
|
||||
* \param p The reference to the current position pointer.
|
||||
* \param start The start of the buffer, for bounds-checking.
|
||||
* \param text The string to write.
|
||||
* \param text_len The length of \p text in bytes (which might
|
||||
* be strictly larger than the number of characters).
|
||||
*
|
||||
* \return The number of bytes written to \p p on success.
|
||||
* \return A negative error code on failure.
|
||||
*/
|
||||
int mbedtls_asn1_write_printable_string(unsigned char **p,
|
||||
const unsigned char *start,
|
||||
const char *text, size_t text_len);
|
||||
|
||||
/**
|
||||
* \brief Write a UTF8 string in ASN.1 format using the UTF8String
|
||||
* string encoding tag (#MBEDTLS_ASN1_UTF8_STRING).
|
||||
*
|
||||
* \note This function works backwards in data buffer.
|
||||
*
|
||||
* \param p The reference to the current position pointer.
|
||||
* \param start The start of the buffer, for bounds-checking.
|
||||
* \param text The string to write.
|
||||
* \param text_len The length of \p text in bytes (which might
|
||||
* be strictly larger than the number of characters).
|
||||
*
|
||||
* \return The number of bytes written to \p p on success.
|
||||
* \return A negative error code on failure.
|
||||
*/
|
||||
int mbedtls_asn1_write_utf8_string(unsigned char **p, const unsigned char *start,
|
||||
const char *text, size_t text_len);
|
||||
|
||||
/**
|
||||
* \brief Write a string in ASN.1 format using the IA5String
|
||||
* string encoding tag (#MBEDTLS_ASN1_IA5_STRING).
|
||||
*
|
||||
* \note This function works backwards in data buffer.
|
||||
*
|
||||
* \param p The reference to the current position pointer.
|
||||
* \param start The start of the buffer, for bounds-checking.
|
||||
* \param text The string to write.
|
||||
* \param text_len The length of \p text in bytes (which might
|
||||
* be strictly larger than the number of characters).
|
||||
*
|
||||
* \return The number of bytes written to \p p on success.
|
||||
* \return A negative error code on failure.
|
||||
*/
|
||||
int mbedtls_asn1_write_ia5_string(unsigned char **p, const unsigned char *start,
|
||||
const char *text, size_t text_len);
|
||||
|
||||
/**
|
||||
* \brief Write a bitstring tag (#MBEDTLS_ASN1_BIT_STRING) and
|
||||
* value in ASN.1 format.
|
||||
*
|
||||
* \note This function works backwards in data buffer.
|
||||
*
|
||||
* \param p The reference to the current position pointer.
|
||||
* \param start The start of the buffer, for bounds-checking.
|
||||
* \param buf The bitstring to write.
|
||||
* \param bits The total number of bits in the bitstring.
|
||||
*
|
||||
* \return The number of bytes written to \p p on success.
|
||||
* \return A negative error code on failure.
|
||||
*/
|
||||
int mbedtls_asn1_write_bitstring(unsigned char **p, const unsigned char *start,
|
||||
const unsigned char *buf, size_t bits);
|
||||
|
||||
/**
|
||||
* \brief This function writes a named bitstring tag
|
||||
* (#MBEDTLS_ASN1_BIT_STRING) and value in ASN.1 format.
|
||||
*
|
||||
* As stated in RFC 5280 Appendix B, trailing zeroes are
|
||||
* omitted when encoding named bitstrings in DER.
|
||||
*
|
||||
* \note This function works backwards within the data buffer.
|
||||
*
|
||||
* \param p The reference to the current position pointer.
|
||||
* \param start The start of the buffer which is used for bounds-checking.
|
||||
* \param buf The bitstring to write.
|
||||
* \param bits The total number of bits in the bitstring.
|
||||
*
|
||||
* \return The number of bytes written to \p p on success.
|
||||
* \return A negative error code on failure.
|
||||
*/
|
||||
int mbedtls_asn1_write_named_bitstring(unsigned char **p,
|
||||
const unsigned char *start,
|
||||
const unsigned char *buf,
|
||||
size_t bits);
|
||||
|
||||
/**
|
||||
* \brief Write an octet string tag (#MBEDTLS_ASN1_OCTET_STRING)
|
||||
* and value in ASN.1 format.
|
||||
*
|
||||
* \note This function works backwards in data buffer.
|
||||
*
|
||||
* \param p The reference to the current position pointer.
|
||||
* \param start The start of the buffer, for bounds-checking.
|
||||
* \param buf The buffer holding the data to write.
|
||||
* \param size The length of the data buffer \p buf.
|
||||
*
|
||||
* \return The number of bytes written to \p p on success.
|
||||
* \return A negative error code on failure.
|
||||
*/
|
||||
int mbedtls_asn1_write_octet_string(unsigned char **p, const unsigned char *start,
|
||||
const unsigned char *buf, size_t size);
|
||||
|
||||
/**
|
||||
* \brief Create or find a specific named_data entry for writing in a
|
||||
* sequence or list based on the OID. If not already in there,
|
||||
* a new entry is added to the head of the list.
|
||||
* Warning: Destructive behaviour for the val data!
|
||||
*
|
||||
* \param list The pointer to the location of the head of the list to seek
|
||||
* through (will be updated in case of a new entry).
|
||||
* \param oid The OID to look for.
|
||||
* \param oid_len The size of the OID.
|
||||
* \param val The associated data to store. If this is \c NULL,
|
||||
* no data is copied to the new or existing buffer.
|
||||
* \param val_len The minimum length of the data buffer needed.
|
||||
* If this is 0, do not allocate a buffer for the associated
|
||||
* data.
|
||||
* If the OID was already present, enlarge, shrink or free
|
||||
* the existing buffer to fit \p val_len.
|
||||
*
|
||||
* \return A pointer to the new / existing entry on success.
|
||||
* \return \c NULL if there was a memory allocation error.
|
||||
*/
|
||||
mbedtls_asn1_named_data *mbedtls_asn1_store_named_data(mbedtls_asn1_named_data **list,
|
||||
const char *oid, size_t oid_len,
|
||||
const unsigned char *val,
|
||||
size_t val_len);
|
||||
|
||||
#endif /* MBEDTLS_ASN1_WRITE_C */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* MBEDTLS_ASN1_WRITE_H */
|
||||
@@ -0,0 +1,194 @@
|
||||
/**
|
||||
* \file mbedtls/build_info.h
|
||||
*
|
||||
* \brief Build-time configuration info
|
||||
*
|
||||
* Include this file if you need to depend on the
|
||||
* configuration options defined in mbedtls_config.h or MBEDTLS_CONFIG_FILE
|
||||
*/
|
||||
/*
|
||||
* Copyright The Mbed TLS Contributors
|
||||
* SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#ifndef MBEDTLS_BUILD_INFO_H
|
||||
#define MBEDTLS_BUILD_INFO_H
|
||||
|
||||
/*
|
||||
* This set of compile-time defines can be used to determine the version number
|
||||
* of the Mbed TLS library used. Run-time variables for the same can be found in
|
||||
* version.h
|
||||
*/
|
||||
|
||||
/**
|
||||
* The version number x.y.z is split into three parts.
|
||||
* Major, Minor, Patchlevel
|
||||
*/
|
||||
#define MBEDTLS_VERSION_MAJOR 3
|
||||
#define MBEDTLS_VERSION_MINOR 6
|
||||
#define MBEDTLS_VERSION_PATCH 7
|
||||
|
||||
/**
|
||||
* The single version number has the following structure:
|
||||
* MMNNPP00
|
||||
* Major version | Minor version | Patch version
|
||||
*/
|
||||
#define MBEDTLS_VERSION_NUMBER 0x03060700
|
||||
#define MBEDTLS_VERSION_STRING "3.6.7"
|
||||
#define MBEDTLS_VERSION_STRING_FULL "Mbed TLS 3.6.7"
|
||||
|
||||
/* Macros for build-time platform detection */
|
||||
|
||||
#if !defined(MBEDTLS_ARCH_IS_ARM64) && \
|
||||
(defined(__aarch64__) || defined(_M_ARM64) || defined(_M_ARM64EC))
|
||||
#define MBEDTLS_ARCH_IS_ARM64
|
||||
#endif
|
||||
|
||||
#if !defined(MBEDTLS_ARCH_IS_ARM32) && \
|
||||
(defined(__arm__) || defined(_M_ARM) || \
|
||||
defined(_M_ARMT) || defined(__thumb__) || defined(__thumb2__))
|
||||
#define MBEDTLS_ARCH_IS_ARM32
|
||||
#endif
|
||||
|
||||
#if !defined(MBEDTLS_ARCH_IS_X64) && \
|
||||
(defined(__amd64__) || defined(__x86_64__) || \
|
||||
((defined(_M_X64) || defined(_M_AMD64)) && !defined(_M_ARM64EC)))
|
||||
#define MBEDTLS_ARCH_IS_X64
|
||||
#endif
|
||||
|
||||
#if !defined(MBEDTLS_ARCH_IS_X86) && \
|
||||
(defined(__i386__) || defined(_X86_) || \
|
||||
(defined(_M_IX86) && !defined(_M_I86)))
|
||||
#define MBEDTLS_ARCH_IS_X86
|
||||
#endif
|
||||
|
||||
#if !defined(MBEDTLS_PLATFORM_IS_WINDOWS_ON_ARM64) && \
|
||||
(defined(_M_ARM64) || defined(_M_ARM64EC))
|
||||
#define MBEDTLS_PLATFORM_IS_WINDOWS_ON_ARM64
|
||||
#endif
|
||||
|
||||
/* This is defined if the architecture is Armv8-A, or higher */
|
||||
#if !defined(MBEDTLS_ARCH_IS_ARMV8_A)
|
||||
#if defined(__ARM_ARCH) && defined(__ARM_ARCH_PROFILE)
|
||||
#if (__ARM_ARCH >= 8) && (__ARM_ARCH_PROFILE == 'A')
|
||||
/* GCC, clang, armclang and IAR */
|
||||
#define MBEDTLS_ARCH_IS_ARMV8_A
|
||||
#endif
|
||||
#elif defined(__ARM_ARCH_8A)
|
||||
/* Alternative defined by clang */
|
||||
#define MBEDTLS_ARCH_IS_ARMV8_A
|
||||
#elif defined(_M_ARM64) || defined(_M_ARM64EC)
|
||||
/* MSVC ARM64 is at least Armv8.0-A */
|
||||
#define MBEDTLS_ARCH_IS_ARMV8_A
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if defined(__GNUC__) && !defined(__ARMCC_VERSION) && !defined(__clang__) \
|
||||
&& !defined(__llvm__) && !defined(__INTEL_COMPILER)
|
||||
/* Defined if the compiler really is gcc and not clang, etc */
|
||||
#define MBEDTLS_COMPILER_IS_GCC
|
||||
#define MBEDTLS_GCC_VERSION \
|
||||
(__GNUC__ * 10000 + __GNUC_MINOR__ * 100 + __GNUC_PATCHLEVEL__)
|
||||
#endif
|
||||
|
||||
#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_DEPRECATE)
|
||||
#define _CRT_SECURE_NO_DEPRECATE 1
|
||||
#endif
|
||||
|
||||
/* Define `inline` on some non-C99-compliant compilers. */
|
||||
#if (defined(__ARMCC_VERSION) || defined(_MSC_VER)) && \
|
||||
!defined(inline) && !defined(__cplusplus)
|
||||
#define inline __inline
|
||||
#endif
|
||||
|
||||
#if defined(MBEDTLS_CONFIG_FILES_READ)
|
||||
#error "Something went wrong: MBEDTLS_CONFIG_FILES_READ defined before reading the config files!"
|
||||
#endif
|
||||
#if defined(MBEDTLS_CONFIG_IS_FINALIZED)
|
||||
#error "Something went wrong: MBEDTLS_CONFIG_IS_FINALIZED defined before reading the config files!"
|
||||
#endif
|
||||
|
||||
/* X.509, TLS and non-PSA crypto configuration */
|
||||
#if !defined(MBEDTLS_CONFIG_FILE)
|
||||
#include "mbedtls/mbedtls_config.h"
|
||||
#else
|
||||
#include MBEDTLS_CONFIG_FILE
|
||||
#endif
|
||||
|
||||
#if defined(MBEDTLS_CONFIG_VERSION) && ( \
|
||||
MBEDTLS_CONFIG_VERSION < 0x03000000 || \
|
||||
MBEDTLS_CONFIG_VERSION > MBEDTLS_VERSION_NUMBER)
|
||||
#error "Invalid config version, defined value of MBEDTLS_CONFIG_VERSION is unsupported"
|
||||
#endif
|
||||
|
||||
/* Target and application specific configurations
|
||||
*
|
||||
* Allow user to override any previous default.
|
||||
*
|
||||
*/
|
||||
#if defined(MBEDTLS_USER_CONFIG_FILE)
|
||||
#include MBEDTLS_USER_CONFIG_FILE
|
||||
#endif
|
||||
|
||||
/* PSA crypto configuration */
|
||||
#if defined(MBEDTLS_PSA_CRYPTO_CONFIG)
|
||||
#if defined(MBEDTLS_PSA_CRYPTO_CONFIG_FILE)
|
||||
#include MBEDTLS_PSA_CRYPTO_CONFIG_FILE
|
||||
#else
|
||||
#include "psa/crypto_config.h"
|
||||
#endif
|
||||
#if defined(MBEDTLS_PSA_CRYPTO_USER_CONFIG_FILE)
|
||||
#include MBEDTLS_PSA_CRYPTO_USER_CONFIG_FILE
|
||||
#endif
|
||||
#endif /* defined(MBEDTLS_PSA_CRYPTO_CONFIG) */
|
||||
|
||||
/* Indicate that all configuration files have been read.
|
||||
* It is now time to adjust the configuration (follow through on dependencies,
|
||||
* make PSA and legacy crypto consistent, etc.).
|
||||
*/
|
||||
#define MBEDTLS_CONFIG_FILES_READ
|
||||
|
||||
/* Auto-enable MBEDTLS_CTR_DRBG_USE_128_BIT_KEY if
|
||||
* MBEDTLS_AES_ONLY_128_BIT_KEY_LENGTH and MBEDTLS_CTR_DRBG_C defined
|
||||
* to ensure a 128-bit key size in CTR_DRBG.
|
||||
*/
|
||||
#if defined(MBEDTLS_AES_ONLY_128_BIT_KEY_LENGTH) && defined(MBEDTLS_CTR_DRBG_C)
|
||||
#define MBEDTLS_CTR_DRBG_USE_128_BIT_KEY
|
||||
#endif
|
||||
|
||||
/* Auto-enable MBEDTLS_MD_C if needed by a module that didn't require it
|
||||
* in a previous release, to ensure backwards compatibility.
|
||||
*/
|
||||
#if defined(MBEDTLS_PKCS5_C)
|
||||
#define MBEDTLS_MD_C
|
||||
#endif
|
||||
|
||||
/* PSA crypto specific configuration options
|
||||
* - If config_psa.h reads a configuration option in preprocessor directive,
|
||||
* this symbol should be set before its inclusion. (e.g. MBEDTLS_MD_C)
|
||||
* - If config_psa.h writes a configuration option in conditional directive,
|
||||
* this symbol should be consulted after its inclusion.
|
||||
* (e.g. MBEDTLS_MD_LIGHT)
|
||||
*/
|
||||
#if defined(MBEDTLS_PSA_CRYPTO_CONFIG) /* PSA_WANT_xxx influences MBEDTLS_xxx */ || \
|
||||
defined(MBEDTLS_PSA_CRYPTO_C) /* MBEDTLS_xxx influences PSA_WANT_xxx */ || \
|
||||
defined(MBEDTLS_PSA_CRYPTO_CLIENT) /* The same as the previous, but with separation only */
|
||||
#include "mbedtls/config_psa.h"
|
||||
#endif
|
||||
|
||||
#include "mbedtls/config_adjust_legacy_crypto.h"
|
||||
|
||||
#include "mbedtls/config_adjust_x509.h"
|
||||
|
||||
#include "mbedtls/config_adjust_ssl.h"
|
||||
|
||||
/* Indicate that all configuration symbols are set,
|
||||
* even the ones that are calculated programmatically.
|
||||
* It is now safe to query the configuration (to check it, to size buffers,
|
||||
* etc.).
|
||||
*/
|
||||
#define MBEDTLS_CONFIG_IS_FINALIZED
|
||||
|
||||
#include "mbedtls/check_config.h"
|
||||
|
||||
#endif /* MBEDTLS_BUILD_INFO_H */
|
||||
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* \file chacha20.h
|
||||
*
|
||||
* \brief This file contains ChaCha20 definitions and functions.
|
||||
*
|
||||
* ChaCha20 is a stream cipher that can encrypt and decrypt
|
||||
* information. ChaCha was created by Daniel Bernstein as a variant of
|
||||
* its Salsa cipher https://cr.yp.to/chacha/chacha-20080128.pdf
|
||||
* ChaCha20 is the variant with 20 rounds, that was also standardized
|
||||
* in RFC 7539.
|
||||
*
|
||||
* \author Daniel King <damaki.gh@gmail.com>
|
||||
*/
|
||||
|
||||
/*
|
||||
* Copyright The Mbed TLS Contributors
|
||||
* SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#ifndef MBEDTLS_CHACHA20_H
|
||||
#define MBEDTLS_CHACHA20_H
|
||||
#include "mbedtls/private_access.h"
|
||||
|
||||
#include "mbedtls/build_info.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
/** Invalid input parameter(s). */
|
||||
#define MBEDTLS_ERR_CHACHA20_BAD_INPUT_DATA -0x0051
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#if !defined(MBEDTLS_CHACHA20_ALT)
|
||||
|
||||
typedef struct mbedtls_chacha20_context {
|
||||
uint32_t MBEDTLS_PRIVATE(state)[16]; /*! The state (before round operations). */
|
||||
uint8_t MBEDTLS_PRIVATE(keystream8)[64]; /*! Leftover keystream bytes. */
|
||||
size_t MBEDTLS_PRIVATE(keystream_bytes_used); /*! Number of keystream bytes already used,
|
||||
* or an internal sentinel value. */
|
||||
}
|
||||
mbedtls_chacha20_context;
|
||||
|
||||
#else /* MBEDTLS_CHACHA20_ALT */
|
||||
#include "chacha20_alt.h"
|
||||
#endif /* MBEDTLS_CHACHA20_ALT */
|
||||
|
||||
/**
|
||||
* \brief This function initializes the specified ChaCha20 context.
|
||||
*
|
||||
* It must be the first API called before using
|
||||
* the context.
|
||||
*
|
||||
* It is usually followed by calls to
|
||||
* \c mbedtls_chacha20_setkey() and
|
||||
* \c mbedtls_chacha20_starts(), then one or more calls to
|
||||
* to \c mbedtls_chacha20_update(), and finally to
|
||||
* \c mbedtls_chacha20_free().
|
||||
*
|
||||
* \param ctx The ChaCha20 context to initialize.
|
||||
* This must not be \c NULL.
|
||||
*/
|
||||
void mbedtls_chacha20_init(mbedtls_chacha20_context *ctx);
|
||||
|
||||
/**
|
||||
* \brief This function releases and clears the specified
|
||||
* ChaCha20 context.
|
||||
*
|
||||
* \param ctx The ChaCha20 context to clear. This may be \c NULL,
|
||||
* in which case this function is a no-op. If it is not
|
||||
* \c NULL, it must point to an initialized context.
|
||||
*
|
||||
*/
|
||||
void mbedtls_chacha20_free(mbedtls_chacha20_context *ctx);
|
||||
|
||||
/**
|
||||
* \brief This function sets the encryption/decryption key.
|
||||
*
|
||||
* \note After using this function, you must also call
|
||||
* \c mbedtls_chacha20_starts() to set a nonce before you
|
||||
* start encrypting/decrypting data with
|
||||
* \c mbedtls_chacha_update().
|
||||
*
|
||||
* \param ctx The ChaCha20 context to which the key should be bound.
|
||||
* It must be initialized.
|
||||
* \param key The encryption/decryption key. This must be \c 32 Bytes
|
||||
* in length.
|
||||
*
|
||||
* \return \c 0 on success.
|
||||
* \return #MBEDTLS_ERR_CHACHA20_BAD_INPUT_DATA if ctx or key is NULL.
|
||||
*/
|
||||
int mbedtls_chacha20_setkey(mbedtls_chacha20_context *ctx,
|
||||
const unsigned char key[32]);
|
||||
|
||||
/**
|
||||
* \brief This function sets the nonce and initial counter value.
|
||||
*
|
||||
* \note A ChaCha20 context can be re-used with the same key by
|
||||
* calling this function to change the nonce.
|
||||
*
|
||||
* \warning You must never use the same nonce twice with the same key.
|
||||
* This would void any confidentiality guarantees for the
|
||||
* messages encrypted with the same nonce and key.
|
||||
*
|
||||
* \param ctx The ChaCha20 context to which the nonce should be bound.
|
||||
* It must be initialized and bound to a key.
|
||||
* \param nonce The nonce. This must be \c 12 Bytes in size.
|
||||
* \param counter The initial counter value. This is usually \c 0.
|
||||
*
|
||||
* \return \c 0 on success.
|
||||
* \return #MBEDTLS_ERR_CHACHA20_BAD_INPUT_DATA if ctx or nonce is
|
||||
* NULL.
|
||||
*/
|
||||
int mbedtls_chacha20_starts(mbedtls_chacha20_context *ctx,
|
||||
const unsigned char nonce[12],
|
||||
uint32_t counter);
|
||||
|
||||
/**
|
||||
* \brief This function encrypts or decrypts data.
|
||||
*
|
||||
* Since ChaCha20 is a stream cipher, the same operation is
|
||||
* used for encrypting and decrypting data.
|
||||
*
|
||||
* \note The \p input and \p output pointers must either be equal or
|
||||
* point to non-overlapping buffers.
|
||||
*
|
||||
* \note \c mbedtls_chacha20_setkey() and
|
||||
* \c mbedtls_chacha20_starts() must be called at least once
|
||||
* to setup the context before this function can be called.
|
||||
*
|
||||
* \note This function can be called multiple times in a row in
|
||||
* order to encrypt of decrypt data piecewise with the same
|
||||
* key and nonce.
|
||||
*
|
||||
* \param ctx The ChaCha20 context to use for encryption or decryption.
|
||||
* It must be initialized and bound to a key and nonce.
|
||||
* \param size The length of the input data in Bytes.
|
||||
* \param input The buffer holding the input data.
|
||||
* This pointer can be \c NULL if `size == 0`.
|
||||
* \param output The buffer holding the output data.
|
||||
* This must be able to hold \p size Bytes.
|
||||
* This pointer can be \c NULL if `size == 0`.
|
||||
*
|
||||
* \return \c 0 on success.
|
||||
* \return #MBEDTLS_ERR_CHACHA20_BAD_INPUT_DATA
|
||||
* if processing \p size bytes would make the 32-bit block
|
||||
* counter wrap.
|
||||
* \return A negative error code on failure.
|
||||
*/
|
||||
int mbedtls_chacha20_update(mbedtls_chacha20_context *ctx,
|
||||
size_t size,
|
||||
const unsigned char *input,
|
||||
unsigned char *output);
|
||||
|
||||
/**
|
||||
* \brief This function encrypts or decrypts data with ChaCha20 and
|
||||
* the given key and nonce.
|
||||
*
|
||||
* Since ChaCha20 is a stream cipher, the same operation is
|
||||
* used for encrypting and decrypting data.
|
||||
*
|
||||
* \warning You must never use the same (key, nonce) pair more than
|
||||
* once. This would void any confidentiality guarantees for
|
||||
* the messages encrypted with the same nonce and key.
|
||||
*
|
||||
* \note The \p input and \p output pointers must either be equal or
|
||||
* point to non-overlapping buffers.
|
||||
*
|
||||
* \param key The encryption/decryption key.
|
||||
* This must be \c 32 Bytes in length.
|
||||
* \param nonce The nonce. This must be \c 12 Bytes in size.
|
||||
* \param counter The initial counter value. This is usually \c 0.
|
||||
* \param size The length of the input data in Bytes.
|
||||
* \param input The buffer holding the input data.
|
||||
* This pointer can be \c NULL if `size == 0`.
|
||||
* \param output The buffer holding the output data.
|
||||
* This must be able to hold \p size Bytes.
|
||||
* This pointer can be \c NULL if `size == 0`.
|
||||
*
|
||||
* \return \c 0 on success.
|
||||
* \return #MBEDTLS_ERR_CHACHA20_BAD_INPUT_DATA
|
||||
* if processing \p size bytes would make the 32-bit block
|
||||
* counter wrap.
|
||||
* \return A negative error code on failure.
|
||||
*/
|
||||
int mbedtls_chacha20_crypt(const unsigned char key[32],
|
||||
const unsigned char nonce[12],
|
||||
uint32_t counter,
|
||||
size_t size,
|
||||
const unsigned char *input,
|
||||
unsigned char *output);
|
||||
|
||||
#if defined(MBEDTLS_SELF_TEST)
|
||||
/**
|
||||
* \brief The ChaCha20 checkup routine.
|
||||
*
|
||||
* \return \c 0 on success.
|
||||
* \return \c 1 on failure.
|
||||
*/
|
||||
int mbedtls_chacha20_self_test(int verbose);
|
||||
#endif /* MBEDTLS_SELF_TEST */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* MBEDTLS_CHACHA20_H */
|
||||
@@ -0,0 +1,351 @@
|
||||
/**
|
||||
* \file chachapoly.h
|
||||
*
|
||||
* \brief This file contains the AEAD-ChaCha20-Poly1305 definitions and
|
||||
* functions.
|
||||
*
|
||||
* ChaCha20-Poly1305 is an algorithm for Authenticated Encryption
|
||||
* with Associated Data (AEAD) that can be used to encrypt and
|
||||
* authenticate data. It is based on ChaCha20 and Poly1305 by Daniel
|
||||
* Bernstein and was standardized in RFC 7539.
|
||||
*
|
||||
* \author Daniel King <damaki.gh@gmail.com>
|
||||
*/
|
||||
|
||||
/*
|
||||
* Copyright The Mbed TLS Contributors
|
||||
* SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#ifndef MBEDTLS_CHACHAPOLY_H
|
||||
#define MBEDTLS_CHACHAPOLY_H
|
||||
#include "mbedtls/private_access.h"
|
||||
|
||||
#include "mbedtls/build_info.h"
|
||||
|
||||
/* for shared error codes */
|
||||
#include "mbedtls/poly1305.h"
|
||||
|
||||
/** The requested operation is not permitted in the current state. */
|
||||
#define MBEDTLS_ERR_CHACHAPOLY_BAD_STATE -0x0054
|
||||
/** Authenticated decryption failed: data was not authentic. */
|
||||
#define MBEDTLS_ERR_CHACHAPOLY_AUTH_FAILED -0x0056
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
MBEDTLS_CHACHAPOLY_ENCRYPT, /**< The mode value for performing encryption. */
|
||||
MBEDTLS_CHACHAPOLY_DECRYPT /**< The mode value for performing decryption. */
|
||||
}
|
||||
mbedtls_chachapoly_mode_t;
|
||||
|
||||
#if !defined(MBEDTLS_CHACHAPOLY_ALT)
|
||||
|
||||
#include "mbedtls/chacha20.h"
|
||||
|
||||
typedef struct mbedtls_chachapoly_context {
|
||||
mbedtls_chacha20_context MBEDTLS_PRIVATE(chacha20_ctx); /**< The ChaCha20 context. */
|
||||
mbedtls_poly1305_context MBEDTLS_PRIVATE(poly1305_ctx); /**< The Poly1305 context. */
|
||||
uint64_t MBEDTLS_PRIVATE(aad_len); /**< The length (bytes) of the Additional Authenticated Data. */
|
||||
uint64_t MBEDTLS_PRIVATE(ciphertext_len); /**< The length (bytes) of the ciphertext. */
|
||||
int MBEDTLS_PRIVATE(state); /**< The current state of the context. */
|
||||
mbedtls_chachapoly_mode_t MBEDTLS_PRIVATE(mode); /**< Cipher mode (encrypt or decrypt). */
|
||||
}
|
||||
mbedtls_chachapoly_context;
|
||||
|
||||
#else /* !MBEDTLS_CHACHAPOLY_ALT */
|
||||
#include "chachapoly_alt.h"
|
||||
#endif /* !MBEDTLS_CHACHAPOLY_ALT */
|
||||
|
||||
/**
|
||||
* \brief This function initializes the specified ChaCha20-Poly1305 context.
|
||||
*
|
||||
* It must be the first API called before using
|
||||
* the context. It must be followed by a call to
|
||||
* \c mbedtls_chachapoly_setkey() before any operation can be
|
||||
* done, and to \c mbedtls_chachapoly_free() once all
|
||||
* operations with that context have been finished.
|
||||
*
|
||||
* In order to encrypt or decrypt full messages at once, for
|
||||
* each message you should make a single call to
|
||||
* \c mbedtls_chachapoly_crypt_and_tag() or
|
||||
* \c mbedtls_chachapoly_auth_decrypt().
|
||||
*
|
||||
* In order to encrypt messages piecewise, for each
|
||||
* message you should make a call to
|
||||
* \c mbedtls_chachapoly_starts(), then 0 or more calls to
|
||||
* \c mbedtls_chachapoly_update_aad(), then 0 or more calls to
|
||||
* \c mbedtls_chachapoly_update(), then one call to
|
||||
* \c mbedtls_chachapoly_finish().
|
||||
*
|
||||
* \warning Decryption with the piecewise API is discouraged! Always
|
||||
* use \c mbedtls_chachapoly_auth_decrypt() when possible!
|
||||
*
|
||||
* If however this is not possible because the data is too
|
||||
* large to fit in memory, you need to:
|
||||
*
|
||||
* - call \c mbedtls_chachapoly_starts() and (if needed)
|
||||
* \c mbedtls_chachapoly_update_aad() as above,
|
||||
* - call \c mbedtls_chachapoly_update() multiple times and
|
||||
* ensure its output (the plaintext) is NOT used in any other
|
||||
* way than placing it in temporary storage at this point,
|
||||
* - call \c mbedtls_chachapoly_finish() to compute the
|
||||
* authentication tag and compared it in constant time to the
|
||||
* tag received with the ciphertext.
|
||||
*
|
||||
* If the tags are not equal, you must immediately discard
|
||||
* all previous outputs of \c mbedtls_chachapoly_update(),
|
||||
* otherwise you can now safely use the plaintext.
|
||||
*
|
||||
* \param ctx The ChachaPoly context to initialize. Must not be \c NULL.
|
||||
*/
|
||||
void mbedtls_chachapoly_init(mbedtls_chachapoly_context *ctx);
|
||||
|
||||
/**
|
||||
* \brief This function releases and clears the specified
|
||||
* ChaCha20-Poly1305 context.
|
||||
*
|
||||
* \param ctx The ChachaPoly context to clear. This may be \c NULL, in which
|
||||
* case this function is a no-op.
|
||||
*/
|
||||
void mbedtls_chachapoly_free(mbedtls_chachapoly_context *ctx);
|
||||
|
||||
/**
|
||||
* \brief This function sets the ChaCha20-Poly1305
|
||||
* symmetric encryption key.
|
||||
*
|
||||
* \param ctx The ChaCha20-Poly1305 context to which the key should be
|
||||
* bound. This must be initialized.
|
||||
* \param key The \c 256 Bit (\c 32 Bytes) key.
|
||||
*
|
||||
* \return \c 0 on success.
|
||||
* \return A negative error code on failure.
|
||||
*/
|
||||
int mbedtls_chachapoly_setkey(mbedtls_chachapoly_context *ctx,
|
||||
const unsigned char key[32]);
|
||||
|
||||
/**
|
||||
* \brief This function starts a ChaCha20-Poly1305 encryption or
|
||||
* decryption operation.
|
||||
*
|
||||
* \warning You must never use the same nonce twice with the same key.
|
||||
* This would void any confidentiality and authenticity
|
||||
* guarantees for the messages encrypted with the same nonce
|
||||
* and key.
|
||||
*
|
||||
* \note If the context is being used for AAD only (no data to
|
||||
* encrypt or decrypt) then \p mode can be set to any value.
|
||||
*
|
||||
* \warning Decryption with the piecewise API is discouraged, see the
|
||||
* warning on \c mbedtls_chachapoly_init().
|
||||
*
|
||||
* \param ctx The ChaCha20-Poly1305 context. This must be initialized
|
||||
* and bound to a key.
|
||||
* \param nonce The nonce/IV to use for the message.
|
||||
* This must be a readable buffer of length \c 12 Bytes.
|
||||
* \param mode The operation to perform: #MBEDTLS_CHACHAPOLY_ENCRYPT or
|
||||
* #MBEDTLS_CHACHAPOLY_DECRYPT (discouraged, see warning).
|
||||
*
|
||||
* \return \c 0 on success.
|
||||
* \return A negative error code on failure.
|
||||
*/
|
||||
int mbedtls_chachapoly_starts(mbedtls_chachapoly_context *ctx,
|
||||
const unsigned char nonce[12],
|
||||
mbedtls_chachapoly_mode_t mode);
|
||||
|
||||
/**
|
||||
* \brief This function feeds additional data to be authenticated
|
||||
* into an ongoing ChaCha20-Poly1305 operation.
|
||||
*
|
||||
* The Additional Authenticated Data (AAD), also called
|
||||
* Associated Data (AD) is only authenticated but not
|
||||
* encrypted nor included in the encrypted output. It is
|
||||
* usually transmitted separately from the ciphertext or
|
||||
* computed locally by each party.
|
||||
*
|
||||
* \note This function is called before data is encrypted/decrypted.
|
||||
* I.e. call this function to process the AAD before calling
|
||||
* \c mbedtls_chachapoly_update().
|
||||
*
|
||||
* You may call this function multiple times to process
|
||||
* an arbitrary amount of AAD. It is permitted to call
|
||||
* this function 0 times, if no AAD is used.
|
||||
*
|
||||
* This function cannot be called any more if data has
|
||||
* been processed by \c mbedtls_chachapoly_update(),
|
||||
* or if the context has been finished.
|
||||
*
|
||||
* \warning Decryption with the piecewise API is discouraged, see the
|
||||
* warning on \c mbedtls_chachapoly_init().
|
||||
*
|
||||
* \param ctx The ChaCha20-Poly1305 context. This must be initialized
|
||||
* and bound to a key.
|
||||
* \param aad_len The length in Bytes of the AAD. The length has no
|
||||
* restrictions.
|
||||
* \param aad Buffer containing the AAD.
|
||||
* This pointer can be \c NULL if `aad_len == 0`.
|
||||
*
|
||||
* \return \c 0 on success.
|
||||
* \return #MBEDTLS_ERR_POLY1305_BAD_INPUT_DATA
|
||||
* if \p ctx or \p aad are NULL.
|
||||
* \return #MBEDTLS_ERR_CHACHAPOLY_BAD_STATE
|
||||
* if the operations has not been started or has been
|
||||
* finished, or if the AAD has been finished.
|
||||
*/
|
||||
int mbedtls_chachapoly_update_aad(mbedtls_chachapoly_context *ctx,
|
||||
const unsigned char *aad,
|
||||
size_t aad_len);
|
||||
|
||||
/**
|
||||
* \brief Thus function feeds data to be encrypted or decrypted
|
||||
* into an on-going ChaCha20-Poly1305
|
||||
* operation.
|
||||
*
|
||||
* The direction (encryption or decryption) depends on the
|
||||
* mode that was given when calling
|
||||
* \c mbedtls_chachapoly_starts().
|
||||
*
|
||||
* You may call this function multiple times to process
|
||||
* an arbitrary amount of data. It is permitted to call
|
||||
* this function 0 times, if no data is to be encrypted
|
||||
* or decrypted.
|
||||
*
|
||||
* \warning Decryption with the piecewise API is discouraged, see the
|
||||
* warning on \c mbedtls_chachapoly_init().
|
||||
*
|
||||
* \param ctx The ChaCha20-Poly1305 context to use. This must be initialized.
|
||||
* \param len The length (in bytes) of the data to encrypt or decrypt.
|
||||
* \param input The buffer containing the data to encrypt or decrypt.
|
||||
* This pointer can be \c NULL if `len == 0`.
|
||||
* \param output The buffer to where the encrypted or decrypted data is
|
||||
* written. This must be able to hold \p len bytes.
|
||||
* This pointer can be \c NULL if `len == 0`.
|
||||
*
|
||||
* \return \c 0 on success.
|
||||
* \return #MBEDTLS_ERR_CHACHAPOLY_BAD_STATE
|
||||
* if the operation has not been started or has been
|
||||
* finished.
|
||||
* \return #MBEDTLS_ERR_CHACHA20_BAD_INPUT_DATA
|
||||
* if processing \p len bytes would make the 32-bit block
|
||||
* counter wrap.
|
||||
* \return Another negative error code on other kinds of failure.
|
||||
*/
|
||||
int mbedtls_chachapoly_update(mbedtls_chachapoly_context *ctx,
|
||||
size_t len,
|
||||
const unsigned char *input,
|
||||
unsigned char *output);
|
||||
|
||||
/**
|
||||
* \brief This function finished the ChaCha20-Poly1305 operation and
|
||||
* generates the MAC (authentication tag).
|
||||
*
|
||||
* \param ctx The ChaCha20-Poly1305 context to use. This must be initialized.
|
||||
* \param mac The buffer to where the 128-bit (16 bytes) MAC is written.
|
||||
*
|
||||
* \warning Decryption with the piecewise API is discouraged, see the
|
||||
* warning on \c mbedtls_chachapoly_init().
|
||||
*
|
||||
* \return \c 0 on success.
|
||||
* \return #MBEDTLS_ERR_CHACHAPOLY_BAD_STATE
|
||||
* if the operation has not been started or has been
|
||||
* finished.
|
||||
* \return Another negative error code on other kinds of failure.
|
||||
*/
|
||||
int mbedtls_chachapoly_finish(mbedtls_chachapoly_context *ctx,
|
||||
unsigned char mac[16]);
|
||||
|
||||
/**
|
||||
* \brief This function performs a complete ChaCha20-Poly1305
|
||||
* authenticated encryption with the previously-set key.
|
||||
*
|
||||
* \note Before using this function, you must set the key with
|
||||
* \c mbedtls_chachapoly_setkey().
|
||||
*
|
||||
* \warning You must never use the same nonce twice with the same key.
|
||||
* This would void any confidentiality and authenticity
|
||||
* guarantees for the messages encrypted with the same nonce
|
||||
* and key.
|
||||
*
|
||||
* \param ctx The ChaCha20-Poly1305 context to use (holds the key).
|
||||
* This must be initialized.
|
||||
* \param length The length (in bytes) of the data to encrypt or decrypt.
|
||||
* \param nonce The 96-bit (12 bytes) nonce/IV to use.
|
||||
* \param aad The buffer containing the additional authenticated
|
||||
* data (AAD). This pointer can be \c NULL if `aad_len == 0`.
|
||||
* \param aad_len The length (in bytes) of the AAD data to process.
|
||||
* \param input The buffer containing the data to encrypt or decrypt.
|
||||
* This pointer can be \c NULL if `ilen == 0`.
|
||||
* \param output The buffer to where the encrypted or decrypted data
|
||||
* is written. This pointer can be \c NULL if `ilen == 0`.
|
||||
* \param tag The buffer to where the computed 128-bit (16 bytes) MAC
|
||||
* is written. This must not be \c NULL.
|
||||
*
|
||||
* \return \c 0 on success.
|
||||
* \return #MBEDTLS_ERR_CHACHA20_BAD_INPUT_DATA
|
||||
* if processing \p length bytes would make the 32-bit block
|
||||
* counter wrap.
|
||||
* \return A negative error code on failure.
|
||||
*/
|
||||
int mbedtls_chachapoly_encrypt_and_tag(mbedtls_chachapoly_context *ctx,
|
||||
size_t length,
|
||||
const unsigned char nonce[12],
|
||||
const unsigned char *aad,
|
||||
size_t aad_len,
|
||||
const unsigned char *input,
|
||||
unsigned char *output,
|
||||
unsigned char tag[16]);
|
||||
|
||||
/**
|
||||
* \brief This function performs a complete ChaCha20-Poly1305
|
||||
* authenticated decryption with the previously-set key.
|
||||
*
|
||||
* \note Before using this function, you must set the key with
|
||||
* \c mbedtls_chachapoly_setkey().
|
||||
*
|
||||
* \param ctx The ChaCha20-Poly1305 context to use (holds the key).
|
||||
* \param length The length (in Bytes) of the data to decrypt.
|
||||
* \param nonce The \c 96 Bit (\c 12 bytes) nonce/IV to use.
|
||||
* \param aad The buffer containing the additional authenticated data (AAD).
|
||||
* This pointer can be \c NULL if `aad_len == 0`.
|
||||
* \param aad_len The length (in bytes) of the AAD data to process.
|
||||
* \param tag The buffer holding the authentication tag.
|
||||
* This must be a readable buffer of length \c 16 Bytes.
|
||||
* \param input The buffer containing the data to decrypt.
|
||||
* This pointer can be \c NULL if `ilen == 0`.
|
||||
* \param output The buffer to where the decrypted data is written.
|
||||
* This pointer can be \c NULL if `ilen == 0`.
|
||||
*
|
||||
* \return \c 0 on success.
|
||||
* \return #MBEDTLS_ERR_CHACHAPOLY_AUTH_FAILED
|
||||
* if the data was not authentic.
|
||||
* \return #MBEDTLS_ERR_CHACHA20_BAD_INPUT_DATA
|
||||
* if processing \p length bytes would make the 32-bit block
|
||||
* counter wrap.
|
||||
* \return Another negative error code on other kinds of failure.
|
||||
*/
|
||||
int mbedtls_chachapoly_auth_decrypt(mbedtls_chachapoly_context *ctx,
|
||||
size_t length,
|
||||
const unsigned char nonce[12],
|
||||
const unsigned char *aad,
|
||||
size_t aad_len,
|
||||
const unsigned char tag[16],
|
||||
const unsigned char *input,
|
||||
unsigned char *output);
|
||||
|
||||
#if defined(MBEDTLS_SELF_TEST)
|
||||
/**
|
||||
* \brief The ChaCha20-Poly1305 checkup routine.
|
||||
*
|
||||
* \return \c 0 on success.
|
||||
* \return \c 1 on failure.
|
||||
*/
|
||||
int mbedtls_chachapoly_self_test(int verbose);
|
||||
#endif /* MBEDTLS_SELF_TEST */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* MBEDTLS_CHACHAPOLY_H */
|
||||
@@ -0,0 +1,246 @@
|
||||
/**
|
||||
* \file cmac.h
|
||||
*
|
||||
* \brief This file contains CMAC definitions and functions.
|
||||
*
|
||||
* The Cipher-based Message Authentication Code (CMAC) Mode for
|
||||
* Authentication is defined in <em>RFC-4493: The AES-CMAC Algorithm</em>.
|
||||
* It is supported with AES and DES.
|
||||
*/
|
||||
/*
|
||||
* Copyright The Mbed TLS Contributors
|
||||
* SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#ifndef MBEDTLS_CMAC_H
|
||||
#define MBEDTLS_CMAC_H
|
||||
#include "mbedtls/private_access.h"
|
||||
|
||||
#include "mbedtls/build_info.h"
|
||||
|
||||
#include "mbedtls/cipher.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define MBEDTLS_AES_BLOCK_SIZE 16
|
||||
#define MBEDTLS_DES3_BLOCK_SIZE 8
|
||||
|
||||
/* We don't support Camellia or ARIA in this module */
|
||||
#if defined(MBEDTLS_AES_C)
|
||||
#define MBEDTLS_CMAC_MAX_BLOCK_SIZE 16 /**< The longest block used by CMAC is that of AES. */
|
||||
#else
|
||||
#define MBEDTLS_CMAC_MAX_BLOCK_SIZE 8 /**< The longest block used by CMAC is that of 3DES. */
|
||||
#endif
|
||||
|
||||
#if !defined(MBEDTLS_DEPRECATED_REMOVED)
|
||||
/** The longest block supported by the cipher module.
|
||||
*
|
||||
* \deprecated
|
||||
* For the maximum block size of a cipher supported by the CMAC module,
|
||||
* use #MBEDTLS_CMAC_MAX_BLOCK_SIZE.
|
||||
* For the maximum block size of a cipher supported by the cipher module,
|
||||
* use #MBEDTLS_MAX_BLOCK_LENGTH.
|
||||
*/
|
||||
/* Before Mbed TLS 3.5, this was the maximum block size supported by the CMAC
|
||||
* module, so it didn't take Camellia or ARIA into account. Since the name
|
||||
* of the macro doesn't even convey "CMAC", this was misleading. Now the size
|
||||
* is sufficient for any cipher, but the name is defined in cmac.h for
|
||||
* backward compatibility. */
|
||||
#define MBEDTLS_CIPHER_BLKSIZE_MAX MBEDTLS_MAX_BLOCK_LENGTH
|
||||
#endif /* MBEDTLS_DEPRECATED_REMOVED */
|
||||
|
||||
#if !defined(MBEDTLS_CMAC_ALT)
|
||||
|
||||
/**
|
||||
* The CMAC context structure.
|
||||
*/
|
||||
struct mbedtls_cmac_context_t {
|
||||
/** The internal state of the CMAC algorithm. */
|
||||
unsigned char MBEDTLS_PRIVATE(state)[MBEDTLS_CMAC_MAX_BLOCK_SIZE];
|
||||
|
||||
/** Unprocessed data - either data that was not block aligned and is still
|
||||
* pending processing, or the final block. */
|
||||
unsigned char MBEDTLS_PRIVATE(unprocessed_block)[MBEDTLS_CMAC_MAX_BLOCK_SIZE];
|
||||
|
||||
/** The length of data pending processing. */
|
||||
size_t MBEDTLS_PRIVATE(unprocessed_len);
|
||||
};
|
||||
|
||||
#else /* !MBEDTLS_CMAC_ALT */
|
||||
#include "cmac_alt.h"
|
||||
#endif /* !MBEDTLS_CMAC_ALT */
|
||||
|
||||
/**
|
||||
* \brief This function starts a new CMAC computation
|
||||
* by setting the CMAC key, and preparing to authenticate
|
||||
* the input data.
|
||||
* It must be called with an initialized cipher context.
|
||||
*
|
||||
* Once this function has completed, data can be supplied
|
||||
* to the CMAC computation by calling
|
||||
* mbedtls_cipher_cmac_update().
|
||||
*
|
||||
* To start a CMAC computation using the same key as a previous
|
||||
* CMAC computation, use mbedtls_cipher_cmac_finish().
|
||||
*
|
||||
* \note When the CMAC implementation is supplied by an alternate
|
||||
* implementation (through #MBEDTLS_CMAC_ALT), some ciphers
|
||||
* may not be supported by that implementation, and thus
|
||||
* return an error. Alternate implementations must support
|
||||
* AES-128 and AES-256, and may support AES-192 and 3DES.
|
||||
*
|
||||
* \param ctx The cipher context used for the CMAC operation, initialized
|
||||
* as one of the following types: MBEDTLS_CIPHER_AES_128_ECB,
|
||||
* MBEDTLS_CIPHER_AES_192_ECB, MBEDTLS_CIPHER_AES_256_ECB,
|
||||
* or MBEDTLS_CIPHER_DES_EDE3_ECB.
|
||||
* \param key The CMAC key.
|
||||
* \param keybits The length of the CMAC key in bits.
|
||||
* Must be supported by the cipher.
|
||||
*
|
||||
* \return \c 0 on success.
|
||||
* \return A cipher-specific error code on failure.
|
||||
*/
|
||||
int mbedtls_cipher_cmac_starts(mbedtls_cipher_context_t *ctx,
|
||||
const unsigned char *key, size_t keybits);
|
||||
|
||||
/**
|
||||
* \brief This function feeds an input buffer into an ongoing CMAC
|
||||
* computation.
|
||||
*
|
||||
* The CMAC computation must have previously been started
|
||||
* by calling mbedtls_cipher_cmac_starts() or
|
||||
* mbedtls_cipher_cmac_reset().
|
||||
*
|
||||
* Call this function as many times as needed to input the
|
||||
* data to be authenticated.
|
||||
* Once all of the required data has been input,
|
||||
* call mbedtls_cipher_cmac_finish() to obtain the result
|
||||
* of the CMAC operation.
|
||||
*
|
||||
* \param ctx The cipher context used for the CMAC operation.
|
||||
* \param input The buffer holding the input data.
|
||||
* \param ilen The length of the input data.
|
||||
*
|
||||
* \return \c 0 on success.
|
||||
* \return #MBEDTLS_ERR_MD_BAD_INPUT_DATA
|
||||
* if parameter verification fails.
|
||||
*/
|
||||
int mbedtls_cipher_cmac_update(mbedtls_cipher_context_t *ctx,
|
||||
const unsigned char *input, size_t ilen);
|
||||
|
||||
/**
|
||||
* \brief This function finishes an ongoing CMAC operation, and
|
||||
* writes the result to the output buffer.
|
||||
*
|
||||
* It should be followed either by
|
||||
* mbedtls_cipher_cmac_reset(), which starts another CMAC
|
||||
* operation with the same key, or mbedtls_cipher_free(),
|
||||
* which clears the cipher context.
|
||||
*
|
||||
* \param ctx The cipher context used for the CMAC operation.
|
||||
* \param output The output buffer for the CMAC checksum result.
|
||||
*
|
||||
* \return \c 0 on success.
|
||||
* \return #MBEDTLS_ERR_MD_BAD_INPUT_DATA
|
||||
* if parameter verification fails.
|
||||
*/
|
||||
int mbedtls_cipher_cmac_finish(mbedtls_cipher_context_t *ctx,
|
||||
unsigned char *output);
|
||||
|
||||
/**
|
||||
* \brief This function starts a new CMAC operation with the same
|
||||
* key as the previous one.
|
||||
*
|
||||
* It should be called after finishing the previous CMAC
|
||||
* operation with mbedtls_cipher_cmac_finish().
|
||||
* After calling this function,
|
||||
* call mbedtls_cipher_cmac_update() to supply the new
|
||||
* CMAC operation with data.
|
||||
*
|
||||
* \param ctx The cipher context used for the CMAC operation.
|
||||
*
|
||||
* \return \c 0 on success.
|
||||
* \return #MBEDTLS_ERR_MD_BAD_INPUT_DATA
|
||||
* if parameter verification fails.
|
||||
*/
|
||||
int mbedtls_cipher_cmac_reset(mbedtls_cipher_context_t *ctx);
|
||||
|
||||
/**
|
||||
* \brief This function calculates the full generic CMAC
|
||||
* on the input buffer with the provided key.
|
||||
*
|
||||
* The function allocates the context, performs the
|
||||
* calculation, and frees the context.
|
||||
*
|
||||
* The CMAC result is calculated as
|
||||
* output = generic CMAC(cmac key, input buffer).
|
||||
*
|
||||
* \note When the CMAC implementation is supplied by an alternate
|
||||
* implementation (through #MBEDTLS_CMAC_ALT), some ciphers
|
||||
* may not be supported by that implementation, and thus
|
||||
* return an error. Alternate implementations must support
|
||||
* AES-128 and AES-256, and may support AES-192 and 3DES.
|
||||
*
|
||||
* \param cipher_info The cipher information.
|
||||
* \param key The CMAC key.
|
||||
* \param keylen The length of the CMAC key in bits.
|
||||
* \param input The buffer holding the input data.
|
||||
* \param ilen The length of the input data.
|
||||
* \param output The buffer for the generic CMAC result.
|
||||
*
|
||||
* \return \c 0 on success.
|
||||
* \return #MBEDTLS_ERR_MD_BAD_INPUT_DATA
|
||||
* if parameter verification fails.
|
||||
*/
|
||||
int mbedtls_cipher_cmac(const mbedtls_cipher_info_t *cipher_info,
|
||||
const unsigned char *key, size_t keylen,
|
||||
const unsigned char *input, size_t ilen,
|
||||
unsigned char *output);
|
||||
|
||||
#if defined(MBEDTLS_AES_C)
|
||||
/**
|
||||
* \brief This function implements the AES-CMAC-PRF-128 pseudorandom
|
||||
* function, as defined in
|
||||
* <em>RFC-4615: The Advanced Encryption Standard-Cipher-based
|
||||
* Message Authentication Code-Pseudo-Random Function-128
|
||||
* (AES-CMAC-PRF-128) Algorithm for the Internet Key
|
||||
* Exchange Protocol (IKE).</em>
|
||||
*
|
||||
* \param key The key to use.
|
||||
* \param key_len The key length in Bytes.
|
||||
* \param input The buffer holding the input data.
|
||||
* \param in_len The length of the input data in Bytes.
|
||||
* \param output The buffer holding the generated 16 Bytes of
|
||||
* pseudorandom output.
|
||||
*
|
||||
* \return \c 0 on success.
|
||||
*/
|
||||
int mbedtls_aes_cmac_prf_128(const unsigned char *key, size_t key_len,
|
||||
const unsigned char *input, size_t in_len,
|
||||
unsigned char output[16]);
|
||||
#endif /* MBEDTLS_AES_C */
|
||||
|
||||
#if defined(MBEDTLS_SELF_TEST) && (defined(MBEDTLS_AES_C) || defined(MBEDTLS_DES_C))
|
||||
/**
|
||||
* \brief The CMAC checkup routine.
|
||||
*
|
||||
* \note In case the CMAC routines are provided by an alternative
|
||||
* implementation (i.e. #MBEDTLS_CMAC_ALT is defined), the
|
||||
* checkup routine will succeed even if the implementation does
|
||||
* not support the less widely used AES-192 or 3DES primitives.
|
||||
* The self-test requires at least AES-128 and AES-256 to be
|
||||
* supported by the underlying implementation.
|
||||
*
|
||||
* \return \c 0 on success.
|
||||
* \return \c 1 on failure.
|
||||
*/
|
||||
int mbedtls_cmac_self_test(int verbose);
|
||||
#endif /* MBEDTLS_SELF_TEST && ( MBEDTLS_AES_C || MBEDTLS_DES_C ) */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* MBEDTLS_CMAC_H */
|
||||
@@ -0,0 +1,578 @@
|
||||
/**
|
||||
* \file mbedtls/config_adjust_legacy_crypto.h
|
||||
* \brief Adjust legacy configuration configuration
|
||||
*
|
||||
* This is an internal header. Do not include it directly.
|
||||
*
|
||||
* Automatically enable certain dependencies. Generally, MBEDTLS_xxx
|
||||
* configurations need to be explicitly enabled by the user: enabling
|
||||
* MBEDTLS_xxx_A but not MBEDTLS_xxx_B when A requires B results in a
|
||||
* compilation error. However, we do automatically enable certain options
|
||||
* in some circumstances. One case is if MBEDTLS_xxx_B is an internal option
|
||||
* used to identify parts of a module that are used by other module, and we
|
||||
* don't want to make the symbol MBEDTLS_xxx_B part of the public API.
|
||||
* Another case is if A didn't depend on B in earlier versions, and we
|
||||
* want to use B in A but we need to preserve backward compatibility with
|
||||
* configurations that explicitly activate MBEDTLS_xxx_A but not
|
||||
* MBEDTLS_xxx_B.
|
||||
*/
|
||||
/*
|
||||
* Copyright The Mbed TLS Contributors
|
||||
* SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#ifndef MBEDTLS_CONFIG_ADJUST_LEGACY_CRYPTO_H
|
||||
#define MBEDTLS_CONFIG_ADJUST_LEGACY_CRYPTO_H
|
||||
|
||||
#if !defined(MBEDTLS_CONFIG_FILES_READ)
|
||||
#error "Do not include mbedtls/config_adjust_*.h manually! This can lead to problems, " \
|
||||
"up to and including runtime errors such as buffer overflows. " \
|
||||
"If you're trying to fix a complaint from check_config.h, just remove " \
|
||||
"it from your configuration file: since Mbed TLS 3.0, it is included " \
|
||||
"automatically at the right point."
|
||||
#endif /* */
|
||||
|
||||
/* Ideally, we'd set those as defaults in mbedtls_config.h, but
|
||||
* putting an #ifdef _WIN32 in mbedtls_config.h would confuse config.py.
|
||||
*
|
||||
* So, adjust it here.
|
||||
* Not related to crypto, but this is the bottom of the stack. */
|
||||
#if defined(__MINGW32__) || (defined(_MSC_VER) && _MSC_VER <= 1900)
|
||||
#if !defined(MBEDTLS_PLATFORM_SNPRINTF_ALT) && \
|
||||
!defined(MBEDTLS_PLATFORM_SNPRINTF_MACRO)
|
||||
#define MBEDTLS_PLATFORM_SNPRINTF_ALT
|
||||
#endif
|
||||
#if !defined(MBEDTLS_PLATFORM_VSNPRINTF_ALT) && \
|
||||
!defined(MBEDTLS_PLATFORM_VSNPRINTF_MACRO)
|
||||
#define MBEDTLS_PLATFORM_VSNPRINTF_ALT
|
||||
#endif
|
||||
#endif /* _MINGW32__ || (_MSC_VER && (_MSC_VER <= 1900)) */
|
||||
|
||||
/* The number of "true" entropy sources (excluding NV seed).
|
||||
* This must be consistent with mbedtls_entropy_init() in entropy.c.
|
||||
*/
|
||||
/* Define auxiliary macros, because in standard C, defined(xxx) is only
|
||||
* allowed directly on an #if or #elif line, not in recursive expansion. */
|
||||
#if defined(MBEDTLS_NO_PLATFORM_ENTROPY)
|
||||
#define MBEDTLS_PLATFORM_ENTROPY_ENABLED 0
|
||||
#else
|
||||
#define MBEDTLS_PLATFORM_ENTROPY_ENABLED 1
|
||||
#endif
|
||||
#if defined(MBEDTLS_ENTROPY_HARDWARE_ALT)
|
||||
#define MBEDTLS_ENTROPY_HARDWARE_ALT_DEFINED 1
|
||||
#else
|
||||
#define MBEDTLS_ENTROPY_HARDWARE_ALT_DEFINED 0
|
||||
#endif
|
||||
|
||||
#define MBEDTLS_ENTROPY_TRUE_SOURCES ( \
|
||||
MBEDTLS_ENTROPY_HARDWARE_ALT_DEFINED + \
|
||||
MBEDTLS_PLATFORM_ENTROPY_ENABLED + \
|
||||
0)
|
||||
|
||||
/* Whether there is at least one entropy source for the entropy module.
|
||||
*
|
||||
* Note that when MBEDTLS_PSA_CRYPTO_EXTERNAL_RNG is enabled, the entropy
|
||||
* module is unused and the configuration will typically not include any
|
||||
* entropy source, so this macro will typically remain undefined.
|
||||
*/
|
||||
#if defined(MBEDTLS_ENTROPY_NV_SEED)
|
||||
#define MBEDTLS_ENTROPY_HAVE_SOURCES (MBEDTLS_ENTROPY_TRUE_SOURCES + 1)
|
||||
#elif MBEDTLS_ENTROPY_TRUE_SOURCES != 0
|
||||
#define MBEDTLS_ENTROPY_HAVE_SOURCES MBEDTLS_ENTROPY_TRUE_SOURCES
|
||||
#else
|
||||
#undef MBEDTLS_ENTROPY_HAVE_SOURCES
|
||||
#endif
|
||||
|
||||
/* Test function dependencies can only check with defined(),
|
||||
* not other preprocessor expressions. */
|
||||
#if MBEDTLS_ENTROPY_TRUE_SOURCES > 0
|
||||
#define MBEDTLS_ENTROPY_HAVE_TRUE_SOURCES
|
||||
#else
|
||||
#undef MBEDTLS_ENTROPY_HAVE_TRUE_SOURCES
|
||||
#endif
|
||||
|
||||
/* If MBEDTLS_PSA_CRYPTO_C is defined, make sure MBEDTLS_PSA_CRYPTO_CLIENT
|
||||
* is defined as well to include all PSA code.
|
||||
*/
|
||||
#if defined(MBEDTLS_PSA_CRYPTO_C)
|
||||
#define MBEDTLS_PSA_CRYPTO_CLIENT
|
||||
#endif /* MBEDTLS_PSA_CRYPTO_C */
|
||||
|
||||
/* Auto-enable CIPHER_C when any of the unauthenticated ciphers is builtin
|
||||
* in PSA. */
|
||||
#if defined(MBEDTLS_PSA_CRYPTO_C) && \
|
||||
(defined(MBEDTLS_PSA_BUILTIN_ALG_STREAM_CIPHER) || \
|
||||
defined(MBEDTLS_PSA_BUILTIN_ALG_CTR) || \
|
||||
defined(MBEDTLS_PSA_BUILTIN_ALG_CFB) || \
|
||||
defined(MBEDTLS_PSA_BUILTIN_ALG_OFB) || \
|
||||
defined(MBEDTLS_PSA_BUILTIN_ALG_ECB_NO_PADDING) || \
|
||||
defined(MBEDTLS_PSA_BUILTIN_ALG_CBC_NO_PADDING) || \
|
||||
defined(MBEDTLS_PSA_BUILTIN_ALG_CBC_PKCS7) || \
|
||||
defined(MBEDTLS_PSA_BUILTIN_ALG_CCM_STAR_NO_TAG) || \
|
||||
defined(MBEDTLS_PSA_BUILTIN_ALG_CMAC))
|
||||
#define MBEDTLS_CIPHER_C
|
||||
#endif
|
||||
|
||||
/* Auto-enable MBEDTLS_MD_LIGHT based on MBEDTLS_MD_C.
|
||||
* This allows checking for MD_LIGHT rather than MD_LIGHT || MD_C.
|
||||
*/
|
||||
#if defined(MBEDTLS_MD_C)
|
||||
#define MBEDTLS_MD_LIGHT
|
||||
#endif
|
||||
|
||||
/* Auto-enable MBEDTLS_MD_LIGHT if needed by a module that didn't require it
|
||||
* in a previous release, to ensure backwards compatibility.
|
||||
*/
|
||||
#if defined(MBEDTLS_ECJPAKE_C) || \
|
||||
defined(MBEDTLS_PEM_PARSE_C) || \
|
||||
defined(MBEDTLS_ENTROPY_C) || \
|
||||
defined(MBEDTLS_PK_C) || \
|
||||
defined(MBEDTLS_PKCS12_C) || \
|
||||
defined(MBEDTLS_RSA_C) || \
|
||||
defined(MBEDTLS_SSL_TLS_C) || \
|
||||
defined(MBEDTLS_X509_USE_C) || \
|
||||
defined(MBEDTLS_X509_CREATE_C)
|
||||
#define MBEDTLS_MD_LIGHT
|
||||
#endif
|
||||
|
||||
#if defined(MBEDTLS_MD_LIGHT)
|
||||
/*
|
||||
* - MBEDTLS_MD_CAN_xxx is defined if the md module can perform xxx.
|
||||
* - MBEDTLS_MD_xxx_VIA_PSA is defined if the md module may perform xxx via PSA
|
||||
* (see below).
|
||||
* - MBEDTLS_MD_SOME_PSA is defined if at least one algorithm may be performed
|
||||
* via PSA (see below).
|
||||
* - MBEDTLS_MD_SOME_LEGACY is defined if at least one algorithm may be performed
|
||||
* via a direct legacy call (see below).
|
||||
*
|
||||
* The md module performs an algorithm via PSA if there is a PSA hash
|
||||
* accelerator and the PSA driver subsytem is initialized at the time the
|
||||
* operation is started, and makes a direct legacy call otherwise.
|
||||
*/
|
||||
|
||||
/* PSA accelerated implementations */
|
||||
#if defined(MBEDTLS_PSA_CRYPTO_C)
|
||||
|
||||
#if defined(MBEDTLS_PSA_ACCEL_ALG_MD5)
|
||||
#define MBEDTLS_MD_CAN_MD5
|
||||
#define MBEDTLS_MD_MD5_VIA_PSA
|
||||
#define MBEDTLS_MD_SOME_PSA
|
||||
#endif
|
||||
#if defined(MBEDTLS_PSA_ACCEL_ALG_SHA_1)
|
||||
#define MBEDTLS_MD_CAN_SHA1
|
||||
#define MBEDTLS_MD_SHA1_VIA_PSA
|
||||
#define MBEDTLS_MD_SOME_PSA
|
||||
#endif
|
||||
#if defined(MBEDTLS_PSA_ACCEL_ALG_SHA_224)
|
||||
#define MBEDTLS_MD_CAN_SHA224
|
||||
#define MBEDTLS_MD_SHA224_VIA_PSA
|
||||
#define MBEDTLS_MD_SOME_PSA
|
||||
#endif
|
||||
#if defined(MBEDTLS_PSA_ACCEL_ALG_SHA_256)
|
||||
#define MBEDTLS_MD_CAN_SHA256
|
||||
#define MBEDTLS_MD_SHA256_VIA_PSA
|
||||
#define MBEDTLS_MD_SOME_PSA
|
||||
#endif
|
||||
#if defined(MBEDTLS_PSA_ACCEL_ALG_SHA_384)
|
||||
#define MBEDTLS_MD_CAN_SHA384
|
||||
#define MBEDTLS_MD_SHA384_VIA_PSA
|
||||
#define MBEDTLS_MD_SOME_PSA
|
||||
#endif
|
||||
#if defined(MBEDTLS_PSA_ACCEL_ALG_SHA_512)
|
||||
#define MBEDTLS_MD_CAN_SHA512
|
||||
#define MBEDTLS_MD_SHA512_VIA_PSA
|
||||
#define MBEDTLS_MD_SOME_PSA
|
||||
#endif
|
||||
#if defined(MBEDTLS_PSA_ACCEL_ALG_RIPEMD160)
|
||||
#define MBEDTLS_MD_CAN_RIPEMD160
|
||||
#define MBEDTLS_MD_RIPEMD160_VIA_PSA
|
||||
#define MBEDTLS_MD_SOME_PSA
|
||||
#endif
|
||||
#if defined(MBEDTLS_PSA_ACCEL_ALG_SHA3_224)
|
||||
#define MBEDTLS_MD_CAN_SHA3_224
|
||||
#define MBEDTLS_MD_SHA3_224_VIA_PSA
|
||||
#define MBEDTLS_MD_SOME_PSA
|
||||
#endif
|
||||
#if defined(MBEDTLS_PSA_ACCEL_ALG_SHA3_256)
|
||||
#define MBEDTLS_MD_CAN_SHA3_256
|
||||
#define MBEDTLS_MD_SHA3_256_VIA_PSA
|
||||
#define MBEDTLS_MD_SOME_PSA
|
||||
#endif
|
||||
#if defined(MBEDTLS_PSA_ACCEL_ALG_SHA3_384)
|
||||
#define MBEDTLS_MD_CAN_SHA3_384
|
||||
#define MBEDTLS_MD_SHA3_384_VIA_PSA
|
||||
#define MBEDTLS_MD_SOME_PSA
|
||||
#endif
|
||||
#if defined(MBEDTLS_PSA_ACCEL_ALG_SHA3_512)
|
||||
#define MBEDTLS_MD_CAN_SHA3_512
|
||||
#define MBEDTLS_MD_SHA3_512_VIA_PSA
|
||||
#define MBEDTLS_MD_SOME_PSA
|
||||
#endif
|
||||
|
||||
#elif defined(MBEDTLS_PSA_CRYPTO_CLIENT)
|
||||
|
||||
#if defined(PSA_WANT_ALG_MD5)
|
||||
#define MBEDTLS_MD_CAN_MD5
|
||||
#define MBEDTLS_MD_MD5_VIA_PSA
|
||||
#define MBEDTLS_MD_SOME_PSA
|
||||
#endif
|
||||
#if defined(PSA_WANT_ALG_SHA_1)
|
||||
#define MBEDTLS_MD_CAN_SHA1
|
||||
#define MBEDTLS_MD_SHA1_VIA_PSA
|
||||
#define MBEDTLS_MD_SOME_PSA
|
||||
#endif
|
||||
#if defined(PSA_WANT_ALG_SHA_224)
|
||||
#define MBEDTLS_MD_CAN_SHA224
|
||||
#define MBEDTLS_MD_SHA224_VIA_PSA
|
||||
#define MBEDTLS_MD_SOME_PSA
|
||||
#endif
|
||||
#if defined(PSA_WANT_ALG_SHA_256)
|
||||
#define MBEDTLS_MD_CAN_SHA256
|
||||
#define MBEDTLS_MD_SHA256_VIA_PSA
|
||||
#define MBEDTLS_MD_SOME_PSA
|
||||
#endif
|
||||
#if defined(PSA_WANT_ALG_SHA_384)
|
||||
#define MBEDTLS_MD_CAN_SHA384
|
||||
#define MBEDTLS_MD_SHA384_VIA_PSA
|
||||
#define MBEDTLS_MD_SOME_PSA
|
||||
#endif
|
||||
#if defined(PSA_WANT_ALG_SHA_512)
|
||||
#define MBEDTLS_MD_CAN_SHA512
|
||||
#define MBEDTLS_MD_SHA512_VIA_PSA
|
||||
#define MBEDTLS_MD_SOME_PSA
|
||||
#endif
|
||||
#if defined(PSA_WANT_ALG_RIPEMD160)
|
||||
#define MBEDTLS_MD_CAN_RIPEMD160
|
||||
#define MBEDTLS_MD_RIPEMD160_VIA_PSA
|
||||
#define MBEDTLS_MD_SOME_PSA
|
||||
#endif
|
||||
#if defined(PSA_WANT_ALG_SHA3_224)
|
||||
#define MBEDTLS_MD_CAN_SHA3_224
|
||||
#define MBEDTLS_MD_SHA3_224_VIA_PSA
|
||||
#define MBEDTLS_MD_SOME_PSA
|
||||
#endif
|
||||
#if defined(PSA_WANT_ALG_SHA3_256)
|
||||
#define MBEDTLS_MD_CAN_SHA3_256
|
||||
#define MBEDTLS_MD_SHA3_256_VIA_PSA
|
||||
#define MBEDTLS_MD_SOME_PSA
|
||||
#endif
|
||||
#if defined(PSA_WANT_ALG_SHA3_384)
|
||||
#define MBEDTLS_MD_CAN_SHA3_384
|
||||
#define MBEDTLS_MD_SHA3_384_VIA_PSA
|
||||
#define MBEDTLS_MD_SOME_PSA
|
||||
#endif
|
||||
#if defined(PSA_WANT_ALG_SHA3_512)
|
||||
#define MBEDTLS_MD_CAN_SHA3_512
|
||||
#define MBEDTLS_MD_SHA3_512_VIA_PSA
|
||||
#define MBEDTLS_MD_SOME_PSA
|
||||
#endif
|
||||
|
||||
#endif /* !MBEDTLS_PSA_CRYPTO_CLIENT && !MBEDTLS_PSA_CRYPTO_C */
|
||||
|
||||
/* Built-in implementations */
|
||||
#if defined(MBEDTLS_MD5_C)
|
||||
#define MBEDTLS_MD_CAN_MD5
|
||||
#define MBEDTLS_MD_SOME_LEGACY
|
||||
#endif
|
||||
#if defined(MBEDTLS_SHA1_C)
|
||||
#define MBEDTLS_MD_CAN_SHA1
|
||||
#define MBEDTLS_MD_SOME_LEGACY
|
||||
#endif
|
||||
#if defined(MBEDTLS_SHA224_C)
|
||||
#define MBEDTLS_MD_CAN_SHA224
|
||||
#define MBEDTLS_MD_SOME_LEGACY
|
||||
#endif
|
||||
#if defined(MBEDTLS_SHA256_C)
|
||||
#define MBEDTLS_MD_CAN_SHA256
|
||||
#define MBEDTLS_MD_SOME_LEGACY
|
||||
#endif
|
||||
#if defined(MBEDTLS_SHA384_C)
|
||||
#define MBEDTLS_MD_CAN_SHA384
|
||||
#define MBEDTLS_MD_SOME_LEGACY
|
||||
#endif
|
||||
#if defined(MBEDTLS_SHA512_C)
|
||||
#define MBEDTLS_MD_CAN_SHA512
|
||||
#define MBEDTLS_MD_SOME_LEGACY
|
||||
#endif
|
||||
#if defined(MBEDTLS_SHA3_C)
|
||||
#define MBEDTLS_MD_CAN_SHA3_224
|
||||
#define MBEDTLS_MD_CAN_SHA3_256
|
||||
#define MBEDTLS_MD_CAN_SHA3_384
|
||||
#define MBEDTLS_MD_CAN_SHA3_512
|
||||
#define MBEDTLS_MD_SOME_LEGACY
|
||||
#endif
|
||||
#if defined(MBEDTLS_RIPEMD160_C)
|
||||
#define MBEDTLS_MD_CAN_RIPEMD160
|
||||
#define MBEDTLS_MD_SOME_LEGACY
|
||||
#endif
|
||||
|
||||
#endif /* MBEDTLS_MD_LIGHT */
|
||||
|
||||
/* BLOCK_CIPHER module can dispatch to PSA when:
|
||||
* - PSA is enabled and drivers have been initialized
|
||||
* - desired key type is supported on the PSA side
|
||||
* If the above conditions are not met, but the legacy support is enabled, then
|
||||
* BLOCK_CIPHER will dynamically fallback to it.
|
||||
*
|
||||
* In case BLOCK_CIPHER is defined (see below) the following symbols/helpers
|
||||
* can be used to define its capabilities:
|
||||
* - MBEDTLS_BLOCK_CIPHER_SOME_PSA: there is at least 1 key type between AES,
|
||||
* ARIA and Camellia which is supported through a driver;
|
||||
* - MBEDTLS_BLOCK_CIPHER_xxx_VIA_PSA: xxx key type is supported through a
|
||||
* driver;
|
||||
* - MBEDTLS_BLOCK_CIPHER_xxx_VIA_LEGACY: xxx key type is supported through
|
||||
* a legacy module (i.e. MBEDTLS_xxx_C)
|
||||
*/
|
||||
#if defined(MBEDTLS_PSA_CRYPTO_C)
|
||||
#if defined(MBEDTLS_PSA_ACCEL_KEY_TYPE_AES)
|
||||
#define MBEDTLS_BLOCK_CIPHER_AES_VIA_PSA
|
||||
#define MBEDTLS_BLOCK_CIPHER_SOME_PSA
|
||||
#endif
|
||||
#if defined(MBEDTLS_PSA_ACCEL_KEY_TYPE_ARIA)
|
||||
#define MBEDTLS_BLOCK_CIPHER_ARIA_VIA_PSA
|
||||
#define MBEDTLS_BLOCK_CIPHER_SOME_PSA
|
||||
#endif
|
||||
#if defined(MBEDTLS_PSA_ACCEL_KEY_TYPE_CAMELLIA)
|
||||
#define MBEDTLS_BLOCK_CIPHER_CAMELLIA_VIA_PSA
|
||||
#define MBEDTLS_BLOCK_CIPHER_SOME_PSA
|
||||
#endif
|
||||
#endif /* MBEDTLS_PSA_CRYPTO_C */
|
||||
|
||||
#if defined(MBEDTLS_AES_C)
|
||||
#define MBEDTLS_BLOCK_CIPHER_AES_VIA_LEGACY
|
||||
#endif
|
||||
#if defined(MBEDTLS_ARIA_C)
|
||||
#define MBEDTLS_BLOCK_CIPHER_ARIA_VIA_LEGACY
|
||||
#endif
|
||||
#if defined(MBEDTLS_CAMELLIA_C)
|
||||
#define MBEDTLS_BLOCK_CIPHER_CAMELLIA_VIA_LEGACY
|
||||
#endif
|
||||
|
||||
/* Helpers to state that BLOCK_CIPHER module supports AES, ARIA and/or Camellia
|
||||
* block ciphers via either PSA or legacy. */
|
||||
#if defined(MBEDTLS_BLOCK_CIPHER_AES_VIA_PSA) || \
|
||||
defined(MBEDTLS_BLOCK_CIPHER_AES_VIA_LEGACY)
|
||||
#define MBEDTLS_BLOCK_CIPHER_CAN_AES
|
||||
#endif
|
||||
#if defined(MBEDTLS_BLOCK_CIPHER_ARIA_VIA_PSA) || \
|
||||
defined(MBEDTLS_BLOCK_CIPHER_ARIA_VIA_LEGACY)
|
||||
#define MBEDTLS_BLOCK_CIPHER_CAN_ARIA
|
||||
#endif
|
||||
#if defined(MBEDTLS_BLOCK_CIPHER_CAMELLIA_VIA_PSA) || \
|
||||
defined(MBEDTLS_BLOCK_CIPHER_CAMELLIA_VIA_LEGACY)
|
||||
#define MBEDTLS_BLOCK_CIPHER_CAN_CAMELLIA
|
||||
#endif
|
||||
|
||||
/* GCM_C and CCM_C can either depend on (in order of preference) BLOCK_CIPHER_C
|
||||
* or CIPHER_C. The former is auto-enabled when:
|
||||
* - CIPHER_C is not defined, which is also the legacy solution;
|
||||
* - BLOCK_CIPHER_SOME_PSA because in this case BLOCK_CIPHER can take advantage
|
||||
* of the driver's acceleration.
|
||||
*/
|
||||
#if (defined(MBEDTLS_GCM_C) || defined(MBEDTLS_CCM_C)) && \
|
||||
(!defined(MBEDTLS_CIPHER_C) || defined(MBEDTLS_BLOCK_CIPHER_SOME_PSA))
|
||||
#define MBEDTLS_BLOCK_CIPHER_C
|
||||
#endif
|
||||
|
||||
/* Helpers for GCM/CCM capabilities */
|
||||
#if (defined(MBEDTLS_CIPHER_C) && defined(MBEDTLS_AES_C)) || \
|
||||
(defined(MBEDTLS_BLOCK_CIPHER_C) && defined(MBEDTLS_BLOCK_CIPHER_CAN_AES))
|
||||
#define MBEDTLS_CCM_GCM_CAN_AES
|
||||
#endif
|
||||
|
||||
#if (defined(MBEDTLS_CIPHER_C) && defined(MBEDTLS_ARIA_C)) || \
|
||||
(defined(MBEDTLS_BLOCK_CIPHER_C) && defined(MBEDTLS_BLOCK_CIPHER_CAN_ARIA))
|
||||
#define MBEDTLS_CCM_GCM_CAN_ARIA
|
||||
#endif
|
||||
|
||||
#if (defined(MBEDTLS_CIPHER_C) && defined(MBEDTLS_CAMELLIA_C)) || \
|
||||
(defined(MBEDTLS_BLOCK_CIPHER_C) && defined(MBEDTLS_BLOCK_CIPHER_CAN_CAMELLIA))
|
||||
#define MBEDTLS_CCM_GCM_CAN_CAMELLIA
|
||||
#endif
|
||||
|
||||
/* MBEDTLS_ECP_LIGHT is auto-enabled by the following symbols:
|
||||
* - MBEDTLS_ECP_C because now it consists of MBEDTLS_ECP_LIGHT plus functions
|
||||
* for curve arithmetic. As a consequence if MBEDTLS_ECP_C is required for
|
||||
* some reason, then MBEDTLS_ECP_LIGHT should be enabled as well.
|
||||
* - MBEDTLS_PK_PARSE_EC_EXTENDED and MBEDTLS_PK_PARSE_EC_COMPRESSED because
|
||||
* these features are not supported in PSA so the only way to have them is
|
||||
* to enable the built-in solution.
|
||||
* Both of them are temporary dependencies:
|
||||
* - PK_PARSE_EC_EXTENDED will be removed after #7779 and #7789
|
||||
* - support for compressed points should also be added to PSA, but in this
|
||||
* case there is no associated issue to track it yet.
|
||||
* - PSA_WANT_KEY_TYPE_ECC_KEY_PAIR_DERIVE because Weierstrass key derivation
|
||||
* still depends on ECP_LIGHT.
|
||||
* - PK_C + USE_PSA + PSA_WANT_ALG_ECDSA is a temporary dependency which will
|
||||
* be fixed by #7453.
|
||||
*/
|
||||
#if defined(MBEDTLS_ECP_C) || \
|
||||
defined(MBEDTLS_PK_PARSE_EC_EXTENDED) || \
|
||||
defined(MBEDTLS_PK_PARSE_EC_COMPRESSED) || \
|
||||
defined(MBEDTLS_PSA_BUILTIN_KEY_TYPE_ECC_KEY_PAIR_DERIVE)
|
||||
#define MBEDTLS_ECP_LIGHT
|
||||
#endif
|
||||
|
||||
/* Backward compatibility: after #8740 the RSA module offers functions to parse
|
||||
* and write RSA private/public keys without relying on the PK one. Of course
|
||||
* this needs ASN1 support to do so, so we enable it here. */
|
||||
#if defined(MBEDTLS_RSA_C)
|
||||
#define MBEDTLS_ASN1_PARSE_C
|
||||
#define MBEDTLS_ASN1_WRITE_C
|
||||
#endif
|
||||
|
||||
/* MBEDTLS_PK_PARSE_EC_COMPRESSED is introduced in Mbed TLS version 3.5, while
|
||||
* in previous version compressed points were automatically supported as long
|
||||
* as PK_PARSE_C and ECP_C were enabled. As a consequence, for backward
|
||||
* compatibility, we auto-enable PK_PARSE_EC_COMPRESSED when these conditions
|
||||
* are met. */
|
||||
#if defined(MBEDTLS_PK_PARSE_C) && defined(MBEDTLS_ECP_C)
|
||||
#define MBEDTLS_PK_PARSE_EC_COMPRESSED
|
||||
#endif
|
||||
|
||||
/* Helper symbol to state that there is support for ECDH, either through
|
||||
* library implementation (ECDH_C) or through PSA. */
|
||||
#if (defined(MBEDTLS_USE_PSA_CRYPTO) && defined(PSA_WANT_ALG_ECDH)) || \
|
||||
(!defined(MBEDTLS_USE_PSA_CRYPTO) && defined(MBEDTLS_ECDH_C))
|
||||
#define MBEDTLS_CAN_ECDH
|
||||
#endif
|
||||
|
||||
/* PK module can achieve ECDSA functionalities by means of either software
|
||||
* implementations (ECDSA_C) or through a PSA driver. The following defines
|
||||
* are meant to list these capabilities in a general way which abstracts how
|
||||
* they are implemented under the hood. */
|
||||
#if !defined(MBEDTLS_USE_PSA_CRYPTO)
|
||||
#if defined(MBEDTLS_ECDSA_C)
|
||||
#define MBEDTLS_PK_CAN_ECDSA_SIGN
|
||||
#define MBEDTLS_PK_CAN_ECDSA_VERIFY
|
||||
#endif /* MBEDTLS_ECDSA_C */
|
||||
#else /* MBEDTLS_USE_PSA_CRYPTO */
|
||||
#if defined(PSA_WANT_ALG_ECDSA)
|
||||
#if defined(PSA_WANT_KEY_TYPE_ECC_KEY_PAIR_BASIC)
|
||||
#define MBEDTLS_PK_CAN_ECDSA_SIGN
|
||||
#endif /* PSA_WANT_KEY_TYPE_ECC_KEY_PAIR_BASIC */
|
||||
#if defined(PSA_WANT_KEY_TYPE_ECC_PUBLIC_KEY)
|
||||
#define MBEDTLS_PK_CAN_ECDSA_VERIFY
|
||||
#endif /* PSA_WANT_KEY_TYPE_ECC_PUBLIC_KEY */
|
||||
#endif /* PSA_WANT_ALG_ECDSA */
|
||||
#endif /* MBEDTLS_USE_PSA_CRYPTO */
|
||||
|
||||
#if defined(MBEDTLS_PK_CAN_ECDSA_VERIFY) || defined(MBEDTLS_PK_CAN_ECDSA_SIGN)
|
||||
#define MBEDTLS_PK_CAN_ECDSA_SOME
|
||||
#endif
|
||||
|
||||
/* Helpers to state that each key is supported either on the builtin or PSA side. */
|
||||
#if defined(MBEDTLS_ECP_DP_SECP521R1_ENABLED) || defined(PSA_WANT_ECC_SECP_R1_521)
|
||||
#define MBEDTLS_ECP_HAVE_SECP521R1
|
||||
#endif
|
||||
#if defined(MBEDTLS_ECP_DP_BP512R1_ENABLED) || defined(PSA_WANT_ECC_BRAINPOOL_P_R1_512)
|
||||
#define MBEDTLS_ECP_HAVE_BP512R1
|
||||
#endif
|
||||
#if defined(MBEDTLS_ECP_DP_CURVE448_ENABLED) || defined(PSA_WANT_ECC_MONTGOMERY_448)
|
||||
#define MBEDTLS_ECP_HAVE_CURVE448
|
||||
#endif
|
||||
#if defined(MBEDTLS_ECP_DP_BP384R1_ENABLED) || defined(PSA_WANT_ECC_BRAINPOOL_P_R1_384)
|
||||
#define MBEDTLS_ECP_HAVE_BP384R1
|
||||
#endif
|
||||
#if defined(MBEDTLS_ECP_DP_SECP384R1_ENABLED) || defined(PSA_WANT_ECC_SECP_R1_384)
|
||||
#define MBEDTLS_ECP_HAVE_SECP384R1
|
||||
#endif
|
||||
#if defined(MBEDTLS_ECP_DP_BP256R1_ENABLED) || defined(PSA_WANT_ECC_BRAINPOOL_P_R1_256)
|
||||
#define MBEDTLS_ECP_HAVE_BP256R1
|
||||
#endif
|
||||
#if defined(MBEDTLS_ECP_DP_SECP256K1_ENABLED) || defined(PSA_WANT_ECC_SECP_K1_256)
|
||||
#define MBEDTLS_ECP_HAVE_SECP256K1
|
||||
#endif
|
||||
#if defined(MBEDTLS_ECP_DP_SECP256R1_ENABLED) || defined(PSA_WANT_ECC_SECP_R1_256)
|
||||
#define MBEDTLS_ECP_HAVE_SECP256R1
|
||||
#endif
|
||||
#if defined(MBEDTLS_ECP_DP_CURVE25519_ENABLED) || defined(PSA_WANT_ECC_MONTGOMERY_255)
|
||||
#define MBEDTLS_ECP_HAVE_CURVE25519
|
||||
#endif
|
||||
#if defined(MBEDTLS_ECP_DP_SECP224K1_ENABLED) || defined(PSA_WANT_ECC_SECP_K1_224)
|
||||
#define MBEDTLS_ECP_HAVE_SECP224K1
|
||||
#endif
|
||||
#if defined(MBEDTLS_ECP_DP_SECP224R1_ENABLED) || defined(PSA_WANT_ECC_SECP_R1_224)
|
||||
#define MBEDTLS_ECP_HAVE_SECP224R1
|
||||
#endif
|
||||
#if defined(MBEDTLS_ECP_DP_SECP192K1_ENABLED) || defined(PSA_WANT_ECC_SECP_K1_192)
|
||||
#define MBEDTLS_ECP_HAVE_SECP192K1
|
||||
#endif
|
||||
#if defined(MBEDTLS_ECP_DP_SECP192R1_ENABLED) || defined(PSA_WANT_ECC_SECP_R1_192)
|
||||
#define MBEDTLS_ECP_HAVE_SECP192R1
|
||||
#endif
|
||||
|
||||
/* Helper symbol to state that the PK module has support for EC keys. This
|
||||
* can either be provided through the legacy ECP solution or through the
|
||||
* PSA friendly MBEDTLS_PK_USE_PSA_EC_DATA (see pk.h for its description). */
|
||||
#if defined(MBEDTLS_ECP_C) || \
|
||||
(defined(MBEDTLS_USE_PSA_CRYPTO) && defined(PSA_WANT_KEY_TYPE_ECC_PUBLIC_KEY))
|
||||
#define MBEDTLS_PK_HAVE_ECC_KEYS
|
||||
#endif /* MBEDTLS_PK_USE_PSA_EC_DATA || MBEDTLS_ECP_C */
|
||||
|
||||
/* Historically pkparse did not check the CBC padding when decrypting
|
||||
* a key. This was a bug, which is now fixed. As a consequence, pkparse
|
||||
* now needs PKCS7 padding support, but existing configurations might not
|
||||
* enable it, so we enable it here. */
|
||||
#if defined(MBEDTLS_PK_PARSE_C) && defined(MBEDTLS_PKCS5_C) && defined(MBEDTLS_CIPHER_MODE_CBC)
|
||||
#define MBEDTLS_CIPHER_PADDING_PKCS7
|
||||
#endif
|
||||
|
||||
/* Backwards compatibility for some macros which were renamed to reflect that
|
||||
* they are related to Armv8, not aarch64. */
|
||||
#if defined(MBEDTLS_SHA256_USE_A64_CRYPTO_IF_PRESENT) && \
|
||||
!defined(MBEDTLS_SHA256_USE_ARMV8_A_CRYPTO_IF_PRESENT)
|
||||
#define MBEDTLS_SHA256_USE_ARMV8_A_CRYPTO_IF_PRESENT
|
||||
#endif
|
||||
#if defined(MBEDTLS_SHA256_USE_A64_CRYPTO_ONLY) && !defined(MBEDTLS_SHA256_USE_ARMV8_A_CRYPTO_ONLY)
|
||||
#define MBEDTLS_SHA256_USE_ARMV8_A_CRYPTO_ONLY
|
||||
#endif
|
||||
|
||||
/* psa_util file features some ECDSA conversion functions, to convert between
|
||||
* legacy's ASN.1 DER format and PSA's raw one. */
|
||||
#if (defined(MBEDTLS_PSA_CRYPTO_CLIENT) && \
|
||||
(defined(PSA_WANT_ALG_ECDSA) || defined(PSA_WANT_ALG_DETERMINISTIC_ECDSA)))
|
||||
#define MBEDTLS_PSA_UTIL_HAVE_ECDSA
|
||||
#endif
|
||||
|
||||
/* Some internal helpers to determine which keys are available. */
|
||||
#if (!defined(MBEDTLS_USE_PSA_CRYPTO) && defined(MBEDTLS_AES_C)) || \
|
||||
(defined(MBEDTLS_USE_PSA_CRYPTO) && defined(PSA_WANT_KEY_TYPE_AES))
|
||||
#define MBEDTLS_SSL_HAVE_AES
|
||||
#endif
|
||||
#if (!defined(MBEDTLS_USE_PSA_CRYPTO) && defined(MBEDTLS_ARIA_C)) || \
|
||||
(defined(MBEDTLS_USE_PSA_CRYPTO) && defined(PSA_WANT_KEY_TYPE_ARIA))
|
||||
#define MBEDTLS_SSL_HAVE_ARIA
|
||||
#endif
|
||||
#if (!defined(MBEDTLS_USE_PSA_CRYPTO) && defined(MBEDTLS_CAMELLIA_C)) || \
|
||||
(defined(MBEDTLS_USE_PSA_CRYPTO) && defined(PSA_WANT_KEY_TYPE_CAMELLIA))
|
||||
#define MBEDTLS_SSL_HAVE_CAMELLIA
|
||||
#endif
|
||||
|
||||
/* Some internal helpers to determine which operation modes are available. */
|
||||
#if (!defined(MBEDTLS_USE_PSA_CRYPTO) && defined(MBEDTLS_CIPHER_MODE_CBC)) || \
|
||||
(defined(MBEDTLS_USE_PSA_CRYPTO) && defined(PSA_WANT_ALG_CBC_NO_PADDING))
|
||||
#define MBEDTLS_SSL_HAVE_CBC
|
||||
#endif
|
||||
|
||||
#if (!defined(MBEDTLS_USE_PSA_CRYPTO) && defined(MBEDTLS_GCM_C)) || \
|
||||
(defined(MBEDTLS_USE_PSA_CRYPTO) && defined(PSA_WANT_ALG_GCM))
|
||||
#define MBEDTLS_SSL_HAVE_GCM
|
||||
#endif
|
||||
|
||||
#if (!defined(MBEDTLS_USE_PSA_CRYPTO) && defined(MBEDTLS_CCM_C)) || \
|
||||
(defined(MBEDTLS_USE_PSA_CRYPTO) && defined(PSA_WANT_ALG_CCM))
|
||||
#define MBEDTLS_SSL_HAVE_CCM
|
||||
#endif
|
||||
|
||||
#if (!defined(MBEDTLS_USE_PSA_CRYPTO) && defined(MBEDTLS_CHACHAPOLY_C)) || \
|
||||
(defined(MBEDTLS_USE_PSA_CRYPTO) && defined(PSA_WANT_ALG_CHACHA20_POLY1305))
|
||||
#define MBEDTLS_SSL_HAVE_CHACHAPOLY
|
||||
#endif
|
||||
|
||||
#if defined(MBEDTLS_SSL_HAVE_GCM) || defined(MBEDTLS_SSL_HAVE_CCM) || \
|
||||
defined(MBEDTLS_SSL_HAVE_CHACHAPOLY)
|
||||
#define MBEDTLS_SSL_HAVE_AEAD
|
||||
#endif
|
||||
|
||||
#endif /* MBEDTLS_CONFIG_ADJUST_LEGACY_CRYPTO_H */
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* \file mbedtls/config_adjust_ssl.h
|
||||
* \brief Adjust TLS configuration
|
||||
*
|
||||
* This is an internal header. Do not include it directly.
|
||||
*
|
||||
* Automatically enable certain dependencies. Generally, MBEDTLS_xxx
|
||||
* configurations need to be explicitly enabled by the user: enabling
|
||||
* MBEDTLS_xxx_A but not MBEDTLS_xxx_B when A requires B results in a
|
||||
* compilation error. However, we do automatically enable certain options
|
||||
* in some circumstances. One case is if MBEDTLS_xxx_B is an internal option
|
||||
* used to identify parts of a module that are used by other module, and we
|
||||
* don't want to make the symbol MBEDTLS_xxx_B part of the public API.
|
||||
* Another case is if A didn't depend on B in earlier versions, and we
|
||||
* want to use B in A but we need to preserve backward compatibility with
|
||||
* configurations that explicitly activate MBEDTLS_xxx_A but not
|
||||
* MBEDTLS_xxx_B.
|
||||
*/
|
||||
/*
|
||||
* Copyright The Mbed TLS Contributors
|
||||
* SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#ifndef MBEDTLS_CONFIG_ADJUST_SSL_H
|
||||
#define MBEDTLS_CONFIG_ADJUST_SSL_H
|
||||
|
||||
#if !defined(MBEDTLS_CONFIG_FILES_READ)
|
||||
#error "Do not include mbedtls/config_adjust_*.h manually! This can lead to problems, " \
|
||||
"up to and including runtime errors such as buffer overflows. " \
|
||||
"If you're trying to fix a complaint from check_config.h, just remove " \
|
||||
"it from your configuration file: since Mbed TLS 3.0, it is included " \
|
||||
"automatically at the right point."
|
||||
#endif /* */
|
||||
|
||||
/* The following blocks make it easier to disable all of TLS,
|
||||
* or of TLS 1.2 or 1.3 or DTLS, without having to manually disable all
|
||||
* key exchanges, options and extensions related to them. */
|
||||
|
||||
#if !defined(MBEDTLS_SSL_TLS_C)
|
||||
#undef MBEDTLS_SSL_CLI_C
|
||||
#undef MBEDTLS_SSL_SRV_C
|
||||
#undef MBEDTLS_SSL_PROTO_TLS1_3
|
||||
#undef MBEDTLS_SSL_PROTO_TLS1_2
|
||||
#undef MBEDTLS_SSL_PROTO_DTLS
|
||||
#endif
|
||||
|
||||
#if !(defined(MBEDTLS_SSL_SRV_C) && defined(MBEDTLS_SSL_SESSION_TICKETS))
|
||||
#undef MBEDTLS_SSL_TICKET_C
|
||||
#endif
|
||||
|
||||
#if !defined(MBEDTLS_SSL_PROTO_DTLS)
|
||||
#undef MBEDTLS_SSL_DTLS_ANTI_REPLAY
|
||||
#undef MBEDTLS_SSL_DTLS_CONNECTION_ID
|
||||
#undef MBEDTLS_SSL_DTLS_CONNECTION_ID_COMPAT
|
||||
#undef MBEDTLS_SSL_DTLS_HELLO_VERIFY
|
||||
#undef MBEDTLS_SSL_DTLS_SRTP
|
||||
#undef MBEDTLS_SSL_DTLS_CLIENT_PORT_REUSE
|
||||
#endif
|
||||
|
||||
#if !defined(MBEDTLS_SSL_PROTO_TLS1_2)
|
||||
#undef MBEDTLS_SSL_ENCRYPT_THEN_MAC
|
||||
#undef MBEDTLS_SSL_EXTENDED_MASTER_SECRET
|
||||
#undef MBEDTLS_SSL_RENEGOTIATION
|
||||
#undef MBEDTLS_KEY_EXCHANGE_RSA_ENABLED
|
||||
#undef MBEDTLS_KEY_EXCHANGE_DHE_RSA_ENABLED
|
||||
#undef MBEDTLS_KEY_EXCHANGE_ECDHE_RSA_ENABLED
|
||||
#undef MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED
|
||||
#undef MBEDTLS_KEY_EXCHANGE_PSK_ENABLED
|
||||
#undef MBEDTLS_KEY_EXCHANGE_DHE_PSK_ENABLED
|
||||
#undef MBEDTLS_KEY_EXCHANGE_RSA_PSK_ENABLED
|
||||
#undef MBEDTLS_KEY_EXCHANGE_ECDHE_PSK_ENABLED
|
||||
#undef MBEDTLS_KEY_EXCHANGE_ECDH_RSA_ENABLED
|
||||
#undef MBEDTLS_KEY_EXCHANGE_ECDH_ECDSA_ENABLED
|
||||
#undef MBEDTLS_KEY_EXCHANGE_ECJPAKE_ENABLED
|
||||
#endif
|
||||
|
||||
#if !defined(MBEDTLS_SSL_PROTO_TLS1_3)
|
||||
#undef MBEDTLS_SSL_TLS1_3_KEY_EXCHANGE_MODE_PSK_ENABLED
|
||||
#undef MBEDTLS_SSL_TLS1_3_KEY_EXCHANGE_MODE_EPHEMERAL_ENABLED
|
||||
#undef MBEDTLS_SSL_TLS1_3_KEY_EXCHANGE_MODE_PSK_EPHEMERAL_ENABLED
|
||||
#undef MBEDTLS_SSL_EARLY_DATA
|
||||
#undef MBEDTLS_SSL_RECORD_SIZE_LIMIT
|
||||
#endif
|
||||
|
||||
#if defined(MBEDTLS_SSL_PROTO_TLS1_2) && \
|
||||
(defined(MBEDTLS_ECDH_C) || defined(MBEDTLS_ECDSA_C) || \
|
||||
defined(MBEDTLS_KEY_EXCHANGE_ECJPAKE_ENABLED))
|
||||
#define MBEDTLS_SSL_TLS1_2_SOME_ECC
|
||||
#endif
|
||||
|
||||
#endif /* MBEDTLS_CONFIG_ADJUST_SSL_H */
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* \file mbedtls/config_adjust_x509.h
|
||||
* \brief Adjust X.509 configuration
|
||||
*
|
||||
* This is an internal header. Do not include it directly.
|
||||
*
|
||||
* Automatically enable certain dependencies. Generally, MBEDTLS_xxx
|
||||
* configurations need to be explicitly enabled by the user: enabling
|
||||
* MBEDTLS_xxx_A but not MBEDTLS_xxx_B when A requires B results in a
|
||||
* compilation error. However, we do automatically enable certain options
|
||||
* in some circumstances. One case is if MBEDTLS_xxx_B is an internal option
|
||||
* used to identify parts of a module that are used by other module, and we
|
||||
* don't want to make the symbol MBEDTLS_xxx_B part of the public API.
|
||||
* Another case is if A didn't depend on B in earlier versions, and we
|
||||
* want to use B in A but we need to preserve backward compatibility with
|
||||
* configurations that explicitly activate MBEDTLS_xxx_A but not
|
||||
* MBEDTLS_xxx_B.
|
||||
*/
|
||||
/*
|
||||
* Copyright The Mbed TLS Contributors
|
||||
* SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#ifndef MBEDTLS_CONFIG_ADJUST_X509_H
|
||||
#define MBEDTLS_CONFIG_ADJUST_X509_H
|
||||
|
||||
#if !defined(MBEDTLS_CONFIG_FILES_READ)
|
||||
#error "Do not include mbedtls/config_adjust_*.h manually! This can lead to problems, " \
|
||||
"up to and including runtime errors such as buffer overflows. " \
|
||||
"If you're trying to fix a complaint from check_config.h, just remove " \
|
||||
"it from your configuration file: since Mbed TLS 3.0, it is included " \
|
||||
"automatically at the right point."
|
||||
#endif /* */
|
||||
|
||||
#endif /* MBEDTLS_CONFIG_ADJUST_X509_H */
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Constant-time functions
|
||||
*/
|
||||
/*
|
||||
* Copyright The Mbed TLS Contributors
|
||||
* SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#ifndef MBEDTLS_CONSTANT_TIME_H
|
||||
#define MBEDTLS_CONSTANT_TIME_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
/** Constant-time buffer comparison without branches.
|
||||
*
|
||||
* This is equivalent to the standard memcmp function, but is likely to be
|
||||
* compiled to code using bitwise operations rather than a branch, such that
|
||||
* the time taken is constant w.r.t. the data pointed to by \p a and \p b,
|
||||
* and w.r.t. whether \p a and \p b are equal or not. It is not constant-time
|
||||
* w.r.t. \p n .
|
||||
*
|
||||
* This function can be used to write constant-time code by replacing branches
|
||||
* with bit operations using masks.
|
||||
*
|
||||
* \param a Pointer to the first buffer, containing at least \p n bytes. May not be NULL.
|
||||
* \param b Pointer to the second buffer, containing at least \p n bytes. May not be NULL.
|
||||
* \param n The number of bytes to compare.
|
||||
*
|
||||
* \return Zero if the contents of the two buffers are the same,
|
||||
* otherwise non-zero.
|
||||
*/
|
||||
int mbedtls_ct_memcmp(const void *a,
|
||||
const void *b,
|
||||
size_t n);
|
||||
|
||||
#endif /* MBEDTLS_CONSTANT_TIME_H */
|
||||