Author SHA1 Message Date
MoHadiShibli 6a0e6925e5 Prepare Control4Free 1.3.0 with WebRTC and controller testing 2026-10-22 19:30:05 +03:00
200 changed files with 123536 additions and 709 deletions

No files matched your search

+15 -2
View File
@@ -49,11 +49,19 @@ 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/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
@@ -77,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
+22 -22
View File
@@ -1,31 +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 — Unreleased
A controller test on the PS4, and experimental extras on the page.
WebRTC connections, controller testing on the PS4, and new experimental controller features.
- **Test controllers** in the PS4 app shows what each signed-in user's controller sends, read the way a game
reads it: buttons, sticks, triggers, touchpad and motion sensors, on the page's controller picture. Press
**Options** in the app, or choose it under **Actions**. Hold **L3 + R3** for two seconds for its menu:
change user, rumble each motor, set the light bar, reset orientation, or go back. PS and Share go to the
PS4 itself and aren't shown. See [Testing controllers](docs/playing.md#testing-controllers-on-the-ps4).
- **Settings → Experimental** on the page. Every switch stays off until you turn it on:
- **Lights and rumble on Sony controllers**: a DualShock 4 or DualSense connected to a computer lights up
in its PS4 controller's colour, rumbles directly (a DualSense included), and passes on its touchpad's
fingers, which browsers' gamepads leave out. Chrome or Edge on a computer.
- **Trigger rumble**: Xbox controllers and a linked DualSense vibrate the trigger you're pulling (the last
one pulled, when both are), following the game's rumble.
- **Motion controls**: tilting a phone, or a linked DualShock 4 or DualSense, moves the controller's
motion sensors.
- **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.
Linking and motion need the page treated as secure: see
[Experimental features](docs/troubleshooting.md#experimental-features).
- The control protocol has a motion message, `m`. See [Protocol](docs/protocol.md).
- Fixed: a game's rumble reached gamepads on the controller page with the large and small motors
swapped, so heavy rumble buzzed lightly and light rumble shook hard.
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
+29 -9
View File
@@ -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.
+11 -3
View File
@@ -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 $< $@
+37 -13
View File
@@ -46,24 +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.
- **Experimental extras**, off until you turn them on in **Settings → Experimental**: a DualShock 4 or
DualSense connected to a computer lights up in the game's colour, rumbles directly (DualSense included) and
passes on its touchpad's fingers;
trigger rumble on Xbox controllers and a linked DualSense; and motion controls from a phone or a linked
DualShock 4 or DualSense. They need browser features that secure pages get, and the page is plain `http`:
see [Experimental features](docs/troubleshooting.md#experimental-features).
- **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, read the way a game reads it: buttons, sticks, triggers, touchpad and motion. Its menu rumbles each
motor and sets the light bar. See [Testing controllers](docs/playing.md#testing-controllers-on-the-ps4).
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).
@@ -82,6 +99,10 @@ New to GoldHEN packages? The **[installation guide](docs/installation.md)** walk
|---|---|---|
| ![Home screen on a phone](docs/images/home.png) | ![The page in a PC's browser](docs/images/desktop.png) | ![The Control4Free app on the PS4](docs/images/launcher.png) |
| Controller test | Controller tools |
|---|---|
| ![Controller test with example readings](docs/images/tester.png) | ![Controller tools](docs/images/tester-tools.png) |
## Documentation
- **[Installation](docs/installation.md)**: requirements, installing, auto-start, updating, uninstalling.
@@ -90,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)**.
@@ -100,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
@@ -115,7 +139,7 @@ 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.
- **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
@@ -127,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
+27 -13
View File
@@ -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
+14 -1
View File
@@ -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
@@ -44,4 +56,5 @@
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.
+247 -89
View File
@@ -37,6 +37,7 @@
--glow: 0 0 0 2px #fff, 0 0 22px rgba(255, 255, 255, .5);
--font: "SST", "SST W55 Regular", "SST Light", "Helvetica Neue", "Segoe UI", Roboto, Arial, sans-serif;
--ease: cubic-bezier(.2, .8, .2, 1);
--solidarity-space: 0px;
}
* { box-sizing: border-box; }
@@ -56,6 +57,32 @@
[hidden] { display: none !important; }
:focus-visible { outline: none; box-shadow: var(--glow); }
body:has(#home:not([hidden]), #settingsView:not([hidden])):not(.menu-open) {
--solidarity-space: 24px;
--solidarity-display: flex;
}
.solidarity {
position: fixed;
top: calc(env(safe-area-inset-top) + 5px);
right: max(12px, env(safe-area-inset-right));
z-index: 30;
display: var(--solidarity-display, none);
align-items: center;
gap: 8px;
color: var(--text);
font-size: 11.5px;
font-weight: 500;
line-height: 14px;
white-space: nowrap;
pointer-events: none;
user-select: none;
-webkit-user-select: none;
}
.solidarity-flags { display: flex; align-items: center; gap: 5px; }
.solidarity svg { display: block; width: 21px; height: 14px; box-shadow: 0 0 0 1px rgba(255, 255, 255, .18); }
/* ---------- Backdrop: deep blue with drifting light waves ---------- */
.backdrop {
@@ -128,6 +155,10 @@
}
.app-mark svg { width: 36px; height: 23px; }
a.app-mark { color: inherit; text-decoration: none; border-radius: 4px; }
a.app-mark:hover .brand-name { text-decoration: underline; text-underline-offset: 3px; }
.brand-name { display: flex; flex-direction: column; gap: 2px; line-height: 1.15; }
.brand-name small { color: var(--muted); font-size: 10px; font-weight: 400; letter-spacing: .01em; }
.clock {
font-size: 17px;
@@ -184,6 +215,7 @@
min-height: 100dvh;
display: flex;
flex-direction: column;
padding-top: var(--solidarity-space);
}
.home-main {
@@ -429,7 +461,7 @@
.pad-view {
position: fixed;
inset: 0;
inset: var(--solidarity-space) 0 0;
z-index: 1;
background:
radial-gradient(75% 95% at 10% -15%, color-mix(in srgb, var(--accent) 14%, transparent), transparent 62%),
@@ -792,7 +824,7 @@
.lb-btn.icon { width: 32px; padding: 0; display: grid; place-items: center; }
.lb-btn svg { width: 16px; height: 16px; }
.rotate { position: fixed; inset: 0; z-index: 5; display: none; place-content: center; justify-items: center; gap: 14px; padding: 24px; text-align: center; background: rgba(11, 14, 20, .96); }
.rotate { position: fixed; inset: var(--solidarity-space) 0 0; z-index: 5; display: none; place-content: center; justify-items: center; gap: 14px; padding: 24px; text-align: center; background: rgba(11, 14, 20, .96); }
.rotate svg { width: 56px; height: 56px; color: #fff; animation: tilt 2.4s var(--ease) infinite; }
.rotate strong { font-size: 20px; font-weight: 300; }
.rotate p { margin: 0; color: var(--muted); font-size: 14px; font-weight: 300; max-width: 30ch; line-height: 1.5; }
@@ -801,7 +833,7 @@
/* ---------- Settings ---------- */
.settings-view { position: fixed; inset: 0; z-index: 3; display: grid; grid-template-rows: auto 1fr; }
.settings-view { position: fixed; inset: var(--solidarity-space) 0 0; z-index: 3; display: grid; grid-template-rows: auto 1fr; }
.settings-view .backdrop { z-index: -1; }
.settings-body {
@@ -923,7 +955,7 @@
.key-btn.listening { background: #fff; color: #06215a; animation: pulse 1s ease-in-out infinite; }
/* ---------- Mapping: a DualShock 4 to map a gamepad or the keyboard onto ---------- */
.map-view { position: fixed; inset: 0; z-index: 4; display: flex; flex-direction: column; overflow-y: auto; overscroll-behavior: contain; }
.map-view { position: fixed; inset: var(--solidarity-space) 0 0; z-index: 4; display: flex; flex-direction: column; overflow-y: auto; overscroll-behavior: contain; }
.map-view .backdrop { z-index: -1; position: fixed; }
.map-top {
@@ -1152,13 +1184,13 @@
/* ---------- Options menu (controller) ---------- */
.scrim { position: fixed; inset: 0; z-index: 10; background: rgba(1, 6, 18, .5); opacity: 0; pointer-events: none; transition: opacity .25s; }
.scrim { position: fixed; inset: var(--solidarity-space) 0 0; z-index: 10; background: rgba(1, 6, 18, .5); opacity: 0; pointer-events: none; transition: opacity .25s; }
.menu-open .scrim { opacity: 1; pointer-events: auto; }
.options-menu {
position: fixed;
z-index: 11;
top: 0;
top: var(--solidarity-space);
right: 0;
bottom: 0;
width: min(380px, 92vw);
@@ -1194,7 +1226,7 @@
.toast {
position: fixed;
z-index: 20;
top: max(14px, env(safe-area-inset-top));
top: calc(var(--solidarity-space) + max(14px, env(safe-area-inset-top)));
left: max(14px, env(safe-area-inset-left));
display: flex;
align-items: center;
@@ -1222,6 +1254,18 @@
</style>
</head>
<body>
<aside class="solidarity" id="solidarity" aria-label="Free Palestine">
<span>Free Palestine</span>
<span class="solidarity-flags">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 300" preserveAspectRatio="none" role="img" aria-label="Palestinian flag">
<path fill="#000" d="M0 0h600v100H0z"/><path fill="#fff" d="M0 100h600v100H0z"/><path fill="#009736" d="M0 200h600v100H0z"/><path fill="#ed2e38" d="m0 0 300 150L0 300z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 600" role="img" aria-label="Syrian flag">
<path fill="#007a3d" d="M0 0h900v200H0z"/><path fill="#fff" d="M0 200h900v200H0z"/><path fill="#000" d="M0 400h900v200H0z"/>
<g fill="#ce1126"><path d="m225 236 14.4 44.2h46.5l-37.6 27.3 14.3 44.3-37.6-27.4-37.6 27.4 14.3-44.3-37.6-27.3h46.5z"/><path d="m450 236 14.4 44.2h46.5l-37.6 27.3 14.3 44.3-37.6-27.4-37.6 27.4 14.3-44.3-37.6-27.3h46.5z"/><path d="m675 236 14.4 44.2h46.5l-37.6 27.3 14.3 44.3-37.6-27.4-37.6 27.4 14.3-44.3-37.6-27.3h46.5z"/></g>
</svg>
</span>
</aside>
<div class="backdrop" aria-hidden="true">
<svg class="waves slow" viewBox="0 0 3200 400" preserveAspectRatio="none">
<defs>
@@ -1245,10 +1289,10 @@
<div class="home" id="home">
<header class="topbar">
<div class="tb-group">
<div class="app-mark">
<a class="app-mark" href="https://github.com/MoHadiShibli/Control4Free" target="_blank" rel="noopener noreferrer" aria-label="Control4Free by MoHadiShibli on GitHub">
<svg viewBox="56 38 888 566" aria-hidden="true"><path fill="#fff" d="M500 78C410 78 330 74 262 62C232 46 188 42 146 62C108 80 86 120 78 170C70 230 58 320 60 400C62 470 80 535 118 568C152 598 196 592 222 560C250 525 272 470 300 430C360 418 430 440 500 440C570 440 640 418 700 430C728 470 750 525 778 560C804 592 848 598 882 568C920 535 938 470 940 400C942 320 930 230 922 170C914 120 892 80 854 62C812 42 768 46 738 62C670 74 590 78 500 78ZM366 98L634 98C648.359 98 660 109.641 660 124L660 226C660 240.359 648.359 252 634 252L366 252C351.641 252 340 240.359 340 226L340 124C340 109.641 351.641 98 366 98ZM173 148L219 148L219 189L260 189L260 235L219 235L219 276L173 276L173 235L132 235L132 189L173 189L173 148ZM804 121C818.912 121 831 133.088 831 148C831 162.912 818.912 175 804 175C789.088 175 777 162.912 777 148C777 133.088 789.088 121 804 121ZM804 249C818.912 249 831 261.088 831 276C831 290.912 818.912 303 804 303C789.088 303 777 290.912 777 276C777 261.088 789.088 249 804 249ZM740 185C754.912 185 767 197.088 767 212C767 226.912 754.912 239 740 239C725.088 239 713 226.912 713 212C713 197.088 725.088 185 740 185ZM868 185C882.912 185 895 197.088 895 212C895 226.912 882.912 239 868 239C853.088 239 841 226.912 841 212C841 197.088 853.088 185 868 185ZM338 276C374.451 276 404 305.549 404 342C404 378.451 374.451 408 338 408C301.549 408 272 378.451 272 342C272 305.549 301.549 276 338 276ZM338 298C313.699 298 294 317.699 294 342C294 366.301 313.699 386 338 386C362.301 386 382 366.301 382 342C382 317.699 362.301 298 338 298ZM662 276C698.451 276 728 305.549 728 342C728 378.451 698.451 408 662 408C625.549 408 596 378.451 596 342C596 305.549 625.549 276 662 276ZM662 298C637.699 298 618 317.699 618 342C618 366.301 637.699 386 662 386C686.301 386 706 366.301 706 342C706 317.699 686.301 298 662 298ZM500 319C509.389 319 517 326.611 517 336C517 345.389 509.389 353 500 353C490.611 353 483 345.389 483 336C483 326.611 490.611 319 500 319Z"/></svg>
<span>Control4Free</span>
</div>
<span class="brand-name">Control4Free<small>by MoHadiShibli</small></span>
</a>
<span class="conn" id="conn" data-conn="connecting" role="status" aria-live="polite"><span class="dot"></span><span id="connText">Connecting</span><span class="conn-detail" id="connDetail"></span></span>
</div>
<div class="tb-group">
@@ -1287,7 +1331,7 @@
</main>
<footer class="home-foot">
<span id="version">Control4Free</span>
<a id="version" href="https://github.com/MoHadiShibli/Control4Free" target="_blank" rel="noopener noreferrer">Control4Free</a>
<button class="link-btn" type="button" id="keysLink">Keyboard keys</button>
</footer>
</div>
@@ -1583,9 +1627,12 @@
// ---------------------------------------------------------------- connection
const conn = {
ws: null, state: 'connecting', address: '', version: '', nextId: 1,
pc: null, control: null, input: null, abort: null, openTimer: 0, disconnectTimer: 0,
state: 'connecting', address: '', version: '', nextId: 1,
pending: new Map(), retry: 0, retryTimer: 0, everOpened: false,
motion: false, // the service takes 'm' (its info says so)
latency: false, // optional RTT telemetry for the native controller test
echoes: new Map(), echoSeq: 0,
};
const status = Array.from({ length: PADS }, (_, i) => ({
@@ -1616,17 +1663,30 @@
renderPadLabel();
}
function closeTransport() {
const pc = conn.pc;
conn.pc = conn.control = conn.input = null;
if (conn.abort) conn.abort.abort();
conn.abort = null;
clearTimeout(conn.openTimer); clearTimeout(conn.disconnectTimer);
if (pc) pc.close();
conn.motion = false;
conn.latency = false;
conn.echoes.clear();
pingMs = 0;
resetClaims();
for (const [, p] of conn.pending) p.reject(new Error('Connection changed'));
conn.pending.clear();
// Keep the selected controllers, but never replay held input after a
// reconnect. The new peer must claim them before it sends input again.
resetConnectionInput();
for (const st of status) { st.mine = false; st.known = false; st.connected = false; }
}
function connect(address) {
clearTimeout(conn.retryTimer);
if (conn.ws) {
conn.ws.onclose = null;
conn.ws.close();
resetClaims();
forgetSelections();
for (const [, p] of conn.pending) p.reject(new Error('Connection changed'));
conn.pending.clear();
}
conn.address = address.trim().replace(/^(?:https?|wss?):\/\//i, '').replace(/[/?#].*$/, '');
closeTransport();
conn.address = address.trim().replace(/^https?:\/\//i, '').replace(/[/?#].*$/, '');
if (!conn.address.includes(':') && conn.address) conn.address += ':4264';
if (!conn.address) {
setConnState('closed', 'Not connected');
@@ -1636,43 +1696,22 @@
if (!servedByConsole) store.set('address', conn.address);
if (conn.everOpened) setConnState('connecting', 'Waiting for Control4Free on the PS4');
else setConnState('connecting', 'Connecting', ' to ' + conn.address);
let ws;
if (typeof RTCPeerConnection !== 'function') {
setConnState('closed', 'This browser cannot connect with WebRTC');
return;
}
let pc;
try {
ws = new WebSocket('ws://' + conn.address);
pc = new RTCPeerConnection({ iceServers: [] });
} catch (e) {
setConnState('closed', 'Invalid address');
setConnState('closed', 'Could not open the connection');
$('#addressForm').hidden = !openedFromFile;
return;
}
conn.ws = ws;
ws.onopen = () => {
if (conn.ws !== ws) return;
conn.retry = 0;
conn.everOpened = true;
setConnState('open', 'Connected', ' to ' + conn.address);
$('#addressForm').hidden = true;
$('#addressHint').classList.remove('error');
call('info', []).then(info => {
conn.version = info.version || '';
conn.motion = info.motion === true;
showVersion();
sendClaim(true);
}).catch(() => {});
resendAll();
};
ws.onmessage = ev => { if (conn.ws === ws) onMessage(ev.data); };
ws.onclose = () => {
if (conn.ws !== ws) return;
conn.ws = null;
conn.motion = false;
motionLive.fill(false);
// Reconnecting the page must not silently recreate a controller or
// replay held buttons. The user selects a controller again.
resetClaims();
forgetSelections();
for (const [, p] of conn.pending) p.reject(new Error('closed'));
conn.pending.clear();
stopAllRumble();
conn.pc = pc;
const failed = () => {
if (conn.pc !== pc) return;
closeTransport();
const wait = Math.min(5000, 800 * Math.pow(1.6, conn.retry++));
if (!conn.everOpened && openedFromFile) {
setConnState('closed', 'Can\'t reach', ' ' + conn.address);
@@ -1684,12 +1723,65 @@
}
conn.retryTimer = setTimeout(() => connect(conn.address), wait);
};
try {
conn.control = pc.createDataChannel('control', { negotiated: true, id: 0, ordered: true });
conn.input = pc.createDataChannel('input', { negotiated: true, id: 1, ordered: false, maxRetransmits: 0 });
const opened = () => {
if (conn.pc !== pc || conn.state === 'open' || conn.control.readyState !== 'open' || conn.input.readyState !== 'open') return;
clearTimeout(conn.openTimer);
resetConnectionInput();
conn.everOpened = true;
setConnState('open', 'Connected', ' to ' + conn.address);
$('#addressForm').hidden = true;
$('#addressHint').classList.remove('error');
call('info', []).then(info => {
if (conn.pc !== pc) return;
if (info.protocol !== 3 || info.input !== true) throw new Error('Unsupported controller protocol');
conn.retry = 0;
conn.version = info.version || '';
conn.motion = info.motion === true;
conn.latency = info.latency === true;
showVersion();
sendClaim(true);
}).catch(failed);
};
for (const channel of [conn.control, conn.input]) {
channel.onopen = opened;
channel.onmessage = ev => { if (conn.pc === pc) onMessage(ev.data); };
channel.onclose = failed;
channel.onerror = failed;
}
pc.onconnectionstatechange = () => {
if (conn.pc !== pc) return;
clearTimeout(conn.disconnectTimer);
if (pc.connectionState === 'failed' || pc.connectionState === 'closed') failed();
else if (pc.connectionState === 'disconnected') {
conn.disconnectTimer = setTimeout(() => { if (pc.connectionState === 'disconnected') failed(); }, 5000);
}
};
conn.abort = new AbortController();
conn.openTimer = setTimeout(failed, 12000);
const signal = conn.abort.signal;
(async () => {
await pc.setLocalDescription(await pc.createOffer());
if (conn.pc !== pc) return;
const scheme = location.protocol === 'https:' ? 'https://' : 'http://';
const reply = await fetch(scheme + conn.address + '/rtc', {
method: 'POST', headers: { 'Content-Type': 'text/plain' }, body: pc.localDescription.sdp, signal,
});
if (!reply.ok) throw new Error('Connection refused');
const sdp = await reply.text();
if (conn.pc === pc) await pc.setRemoteDescription({ type: 'answer', sdp });
})().catch(failed);
} catch (e) { failed(); }
}
function send(obj) {
if (conn.ws && conn.ws.readyState === WebSocket.OPEN) {
conn.ws.send(JSON.stringify(obj));
return true;
const input = obj.method === 'i' || obj.method === 'm' || obj.method === 'e' || obj.method === 'latency';
const channel = input ? conn.input : conn.control;
if (channel && channel.readyState === 'open') {
try { channel.send(JSON.stringify(obj)); return true; }
catch (e) { /* A closing or full channel is retried by the next update. */ }
}
return false;
}
@@ -1722,6 +1814,14 @@
}
const params = msg.params;
switch (msg.method) {
case 'e':
if (Array.isArray(params) && conn.echoes.has(params[0])) {
pingMs = Math.round(now() - conn.echoes.get(params[0]));
conn.echoes.delete(params[0]); showVersion();
if (conn.latency && pingMs >= 0 && pingMs <= 60000)
send({ method: 'latency', params: [params[0], pingMs] });
}
break;
case 's': applyStatus(params); break;
case 'error': toast(params.message || 'Controller error'); break;
case 'v': onRumble(params[0], params[1], params[2]); break;
@@ -1782,17 +1882,25 @@
function allSources() {
return [screenSource, keySource, ...gamepadSources.values()];
}
function forgetSelections() {
function resetConnectionInput() {
stopAllRumble();
mapper.cancelCapture(); releaseScreen(); heldKeys.clear();
for (const src of gamepadSources.values()) { src.pad = -1; clearDevice(src); }
primaryPad = -1;
Object.assign(screenSource, blankSource(-1)); Object.assign(keySource, blankSource(-1));
padView.hidden = true;
if (mapView.hidden && $('#settingsView').hidden) $('#home').hidden = false;
document.body.classList.remove('in-pad');
document.title = 'Control4Free';
history.replaceState(null, '', updateQuery('pad', null));
for (const timer of holdTimers.values()) clearTimeout(timer);
holdTimers.clear();
Object.assign(screenSource, blankSource(screenSource.pad)); Object.assign(keySource, blankSource(keySource.pad));
for (const src of gamepadSources.values()) {
clearDevice(src); src.hidState = null; src.hidAt = 0;
src.hidHold = VALID_BUTTONS; src.hidHoldL2 = src.hidHoldR2 = true;
}
motionLive.fill(false); motionSentAt.fill(0); motionSeq.fill(0);
phoneMotion.at = 0; phoneMotion.fresh = false;
for (const link of hidLinks.values()) {
if (link.motion) { link.motion.at = 0; link.motion.fresh = false; }
hidUpdate(link);
}
inputSeq.fill(0); inputButtons.fill(0);
for (const edges of inputHistory) edges.length = 0;
resendAll();
renderGamepads();
}
@@ -1867,11 +1975,11 @@
function measurePing(t) {
if (conn.state !== 'open' || t - pingAt < 2000) return;
pingAt = t;
const started = now();
call('ping', []).then(() => {
pingMs = Math.round(now() - started);
showVersion();
}).catch(() => {});
const seq = ++conn.echoSeq, started = now();
// Use the input channel, so the displayed round trip follows gameplay.
// Lost probes need no retry, and never build up an unbounded history.
conn.echoes.clear();
if (send({ method: 'e', params: [seq, Math.round(started)] })) conn.echoes.set(seq, started);
}
function showVersion() {
@@ -1881,6 +1989,9 @@
const lastSent = Array(PADS).fill('');
const lastSentAt = Array(PADS).fill(0);
const inputSeq = Array(PADS).fill(0);
const inputButtons = Array(PADS).fill(0);
const inputHistory = Array.from({ length: PADS }, () => []);
function packState(pad, s) {
const params = [pad, s.buttons & VALID_BUTTONS, s.lx, s.ly, s.rx, s.ry, s.l2, s.r2, s.touches.length];
@@ -1890,11 +2001,12 @@
function flush(t) {
for (const pad of claimedPads()) {
if (!status[pad].mine) continue;
const params = packState(pad, mergedState(pad));
const key = params.join(',');
// Send on change, and once a second anyway so the console knows this device is still here
if (key !== lastSent[pad] || t - lastSentAt[pad] > KEEPALIVE_MS) {
if (send({ method: 'u', params })) {
if (sendInput(params)) {
if (key !== lastSent[pad]) pulseActivity(pad);
lastSent[pad] = key;
lastSentAt[pad] = t;
@@ -1903,6 +2015,17 @@
}
}
function sendInput(params) {
const pad = params[0], seq = inputSeq[pad] + 1, buttons = params[1];
const edges = inputHistory[pad].slice();
const message = [pad, seq, ...params.slice(1), edges.length, ...edges.flat()];
if (!send({ method: 'i', params: message })) return false;
if (buttons !== inputButtons[pad]) edges.push([seq, buttons]);
while (edges.length > 4) edges.shift();
inputSeq[pad] = seq; inputButtons[pad] = buttons; inputHistory[pad] = edges;
return true;
}
function resendAll() {
lastSent.fill('');
}
@@ -2153,10 +2276,10 @@
const view = padView.getBoundingClientRect();
const r = ctlNode(this.selected).getBoundingClientRect();
const pw = panel.offsetWidth, ph = panel.offsetHeight, gap = 10;
let top = r.bottom + gap;
if (top + ph > view.height - 4) top = r.top - gap - ph;
let top = r.bottom - view.top + gap;
if (top + ph > view.height - 4) top = r.top - view.top - gap - ph;
top = clamp(top, 4, view.height - ph - 4);
panel.style.left = clamp(r.left + r.width / 2 - pw / 2, 4, view.width - pw - 4) + 'px';
panel.style.left = clamp(r.left - view.left + r.width / 2 - pw / 2, 4, view.width - pw - 4) + 'px';
panel.style.top = top + 'px';
},
@@ -2170,8 +2293,8 @@
pointers.set(e.pointerId, {
move(ev) {
bar.classList.add('moved');
bar.style.left = clamp(ev.clientX - dx, 4, view.width - r.width - 4) + 'px';
bar.style.top = clamp(ev.clientY - dy, 4, view.height - r.height - 4) + 'px';
bar.style.left = clamp(ev.clientX - view.left - dx, 4, view.width - r.width - 4) + 'px';
bar.style.top = clamp(ev.clientY - view.top - dy, 4, view.height - r.height - 4) + 'px';
},
up() {},
});
@@ -2609,9 +2732,12 @@
}
return s;
}
function fromSource(src) {
function fromSource(src, analogTriggers = false) {
const out = empty(); out.buttons = src.buttons;
out.l2 = src.buttons & B.L2 ? 1 : src.l2 / 255; out.r2 = src.buttons & B.R2 ? 1 : src.r2 / 255;
// Touch and keyboard trigger buttons synthesize full pressure. HID has
// independent pressure bytes, even when its digital trigger flags are on.
const digital = analogTriggers ? 0 : src.buttons;
out.l2 = digital & B.L2 ? 1 : src.l2 / 255; out.r2 = digital & B.R2 ? 1 : src.r2 / 255;
for (const axis of axes) out.analog[axis].push(src[axis] === 128 ? 0 : src[axis] / 255 * 2 - 1);
return out;
}
@@ -3934,10 +4060,20 @@
if (viaHid) src.hold = {};
if (!src.hold) src.hold = ControllerMapping.holdBack(profile);
ControllerMapping.letGo(src.snapshot, profile, src.hold);
const waiting = Object.keys(src.hold).length > 0;
const waiting = Object.keys(src.hold).length > 0 || !!(viaHid &&
((src.hidHold || 0) & src.hidState.buttons || src.hidHoldL2 && src.hidState.l2 > 20 || src.hidHoldR2 && src.hidState.r2 > 20));
if (waiting !== src.waitNeutral) { src.waitNeutral = waiting; renderGamepads(); }
if (mapper.index === gp.index || ControllerMapping.unresolved(profile, signature)) src.mapped = ControllerMapping.empty();
else if (viaHid) src.mapped = ControllerMapping.fromSource(Object.assign(src, src.hidState));
else if (viaHid) {
// A held control must be released before it can play after reconnect,
// including when Bluetooth input comes only through WebHID.
src.hidHold = (src.hidHold || 0) & src.hidState.buttons;
if (src.hidState.l2 <= 20) src.hidHoldL2 = false;
if (src.hidState.r2 <= 20) src.hidHoldR2 = false;
const state = { ...src.hidState, buttons: src.hidState.buttons & ~src.hidHold,
l2: src.hidHoldL2 ? 0 : src.hidState.l2, r2: src.hidHoldR2 ? 0 : src.hidState.r2 };
src.mapped = ControllerMapping.fromSource(Object.assign(src, state), true);
}
else src.mapped = ControllerMapping.evaluate(src.snapshot, profile, src.transient, src.hold);
Object.assign(src, ControllerMapping.toSource(src.mapped));
if (experiments.triggers) pulledTrigger(src, viaHid);
@@ -3988,19 +4124,29 @@
// ---------------------------------------------------------------- rumble and lightbar
const rumble = Array.from({ length: PADS }, () => ({ large: 0, small: 0, lastKick: 0 }));
const rumble = Array.from({ length: PADS }, () => ({ large: 0, small: 0, lastKick: 0, timer: 0 }));
function onRumble(pad, large, small) {
if (!rumble[pad]) return;
rumble[pad].large = large;
rumble[pad].small = small;
rumble[pad].lastKick = 0;
clearTimeout(rumble[pad].timer);
// The service renews active rumble every 200 ms. Expire it locally if
// the final stop (or the connection) is lost, including WebHID motors.
if (large || small) rumble[pad].timer = setTimeout(() => {
rumble[pad].large = rumble[pad].small = 0;
stopRumble(pad);
}, 650);
if (!large && !small) stopRumble(pad);
}
function stopRumble(pad) {
for (const [index, src] of gamepadSources) {
if (hidLinks.has(index)) continue; /* a linked controller is told directly */
if (hidLinks.has(index)) {
if (src.pad === pad) hidUpdate(hidLinks.get(index));
continue;
}
if (src.pad === pad && src.gp && src.gp.vibrationActuator && src.gp.vibrationActuator.reset) {
try { src.gp.vibrationActuator.reset(); } catch (e) { /* ignore */ }
}
@@ -4010,8 +4156,7 @@
function stopAllRumble() {
for (let i = 0; i < PADS; i++) {
rumble[i].large = rumble[i].small = 0;
stopRumble(i);
onRumble(i, 0, 0);
}
}
@@ -4922,6 +5067,7 @@
const phoneMotion = newMotion();
const motionSentAt = Array(PADS).fill(0);
const motionLive = Array(PADS).fill(false);
const motionSeq = Array(PADS).fill(0);
// Mahony's filter: the gyro turns the orientation, gravity keeps up up.
function motionSample(m, gyro, accel, t) {
@@ -4980,18 +5126,25 @@
m.at = 0;
if (motionLive[pad]) {
motionLive[pad] = false;
if (conn.motion) send({ method: 'm', params: [pad, 0, 0, 0, 10000, 0, 0, 0, 0, 0, 0] });
if (conn.motion) sendMotion([pad, 0, 0, 0, 10000, 0, 0, 0, 0, 0, 0]);
}
}
function motionSend(pad, m, t) {
if (t - motionSentAt[pad] < 16 || !m.fresh) return;
if (!status[pad].mine || t - motionSentAt[pad] < 16 || !m.fresh) return;
m.fresh = false;
const q = [m.q[1], m.q[2], m.q[3], m.q[0]];
const params = [pad, ...q.map(v => Math.round(clamp(v, -1, 1) * 10000)),
...m.accel.map(v => Math.round(clamp(v, -16, 16) * 1000)),
...m.gyro.map(v => Math.round(clamp(v, -35, 35) * 1000))];
if (send({ method: 'm', params })) { motionSentAt[pad] = t; motionLive[pad] = true; }
if (sendMotion(params)) { motionSentAt[pad] = t; motionLive[pad] = true; }
}
function sendMotion(params) {
const pad = params[0], seq = motionSeq[pad] + 1;
if (!send({ method: 'm', params: [pad, seq, ...params.slice(1)] })) return false;
motionSeq[pad] = seq;
return true;
}
// Each frame: linked controllers' rumble and colour, and motion for the controllers that have it.
@@ -5070,12 +5223,17 @@
releaseScreen();
heldKeys.clear();
Object.assign(keySource, blankSource(keySource.pad));
motionLeave(phoneMotion);
for (const link of hidLinks.values()) motionLeave(link.motion);
for (const pad of claimedPads()) {
send({ method: 'u', params: [pad, 0, 128, 128, 128, 128, 0, 0, 0] });
if (status[pad].mine) sendInput([pad, 0, 128, 128, 128, 128, 0, 0, 0]);
lastSent[pad] = '';
}
stopAllRumble();
for (const src of gamepadSources.values()) clearDevice(src);
for (const src of gamepadSources.values()) {
clearDevice(src); src.hidState = null; src.hidAt = 0;
src.hidHold = VALID_BUTTONS; src.hidHoldL2 = src.hidHoldR2 = true;
}
keepAwake.off();
} else {
resendAll();
+5
View File
@@ -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.
+12 -1
View File
@@ -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
+176 -22
View File
@@ -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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 504 KiB

After

Width:  |  Height:  |  Size: 642 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 640 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 840 KiB

After

Width:  |  Height:  |  Size: 859 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 561 KiB

After

Width:  |  Height:  |  Size: 672 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 495 KiB

After

Width:  |  Height:  |  Size: 587 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 967 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 895 KiB

+11 -6
View File
@@ -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.
+69 -12
View File
@@ -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,8 +82,9 @@ appears under **Gamepads on this device**:
Keep the page's window in front: browsers only read gamepads for the active window.
A DualShock 4's or DualSense's touchpad clicks, but browsers don't pass on where the fingers are. Link it
under [Experimental features](#experimental-features) and its touches work too.
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
@@ -90,6 +95,10 @@ 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.
@@ -105,8 +114,8 @@ extras that stay off until you turn them on: see [Experimental features](#experi
| **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
@@ -143,15 +152,44 @@ whatever colour the game sets. Until one is set, a controller shows its own colo
**☰ → 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.
![Experimental settings in Chrome](images/experimental.png)
| Switch | What it does | Where it works |
|---|---|---|
| **Lights and rumble on Sony controllers** | A DualShock 4 or DualSense connected to a computer lights up in its PS4 controller's colour and rumbles directly, a DualSense included. Its touchpad passes on fingers, not only clicks: browsers' gamepads leave touches out. While linked, its buttons and sticks come straight from the controller, as on a PS4, so its **Mapping** doesn't apply. Press **Link** next to the controller and choose it in the browser's list. | Chrome or Edge on a computer |
| **Trigger rumble** | Xbox controllers and a linked DualSense vibrate the trigger you're pulling, following the game's rumble. With both pulled, the one you pulled last vibrates: aim with L2, fire with R2, and R2 feels the shot. PS4 games don't ask for this: Control4Free makes it up from the rumble. | Xbox controllers in Chrome or Edge on Windows; a linked DualSense |
| **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 |
Linking and motion use features browsers only give secure pages, and the console's page is plain `http`.
[Experimental features](troubleshooting.md#experimental-features) in the troubleshooting guide explains how to
let Chrome or Edge treat it as secure.
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
@@ -162,15 +200,34 @@ The app on the PS4 shows what each signed-in user's controller sends, read the w
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, set the light bar red, green or blue, reset it, reset the orientation, or go back. The light bar
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.
![RGB light-bar selector on the PS4](images/tester-color.png)
- 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
+124 -35
View File
@@ -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,22 +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, "motion": true}` |
| `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. |
| `m` | motion, 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` | `[]` | `{}` |
@@ -58,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 |
|---|---|---|---|
@@ -86,34 +137,47 @@ ignored: input never claims a controller.
| D-pad left | `0x0080` | Square | `0x8000` |
| PS | `0x10000` | Touchpad click | `0x100000` |
**`m`** sends the motion sensors of one controller this connection owns. Like `u`, it is ignored for any
**`m`** sends the motion sensors of one controller this connection owns. Like `i`, it is ignored for any
other controller.
```
[pad, qx, qy, qz, qw, ax, ay, az, gx, gy, gz]
[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 `u`. Before the first `m`, and whenever the controller goes neutral, it reports no motion: orientation
`0, 0, 0, 1` and everything else 0. `m` counts as input, like `u`. A service that understands `m` says
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. After 3
seconds without input the controller goes neutral and shows as `paused`; after 15 it's removed. When a
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 doesn't do that by itself after reconnecting: the player picks the
controller again.
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.
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.
`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.
@@ -130,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}
]
}
```
@@ -157,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`.
@@ -181,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.
+52 -13
View File
@@ -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,6 +174,11 @@ 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.
@@ -164,11 +194,9 @@ can treat your PS4's address as secure:
and set the option to **Enabled**.
3. Press **Relaunch**.
The setting belongs to that browser on that device. If the PS4's address changes, change it here too:
reserving the address for the PS4 in your router avoids that. Chrome warns about the setting on its pages;
it applies only to the address you typed. That address then gets the browser features an `https` site gets,
over a connection that isn't encrypted, so add only your PS4's address. Safari on an iPhone has no such
setting.
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.
@@ -179,13 +207,24 @@ Steam or DS4Windows, reconnect it, press a button on it, then press **Link** aga
**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**
+27
View File
@@ -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
+61
View File
@@ -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
+11 -6
View File
@@ -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;
+54
View File
@@ -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
+91
View File
@@ -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
+32
View File
@@ -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
+6 -1
View File
@@ -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;
}
+41
View File
@@ -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);
}
+4
View File
@@ -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
+264 -100
View File
@@ -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;
@@ -391,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);
@@ -409,18 +614,6 @@ int main(void)
OrbisUserServiceInitializeParams params = {};
params.priority = ORBIS_KERNEL_PRIO_FIFO_LOWEST;
sceUserServiceInitialize(&params);
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 };
static C4fTester tester;
c4fTesterInit(&tester, &testerOps);
C4fTesterHold launcherHold = {};
int waitRelease = 0;
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. */
@@ -441,95 +634,48 @@ 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 testDraws = 0, testDrawMax = 0, testDrawReportAt = 0;
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);
if (resumed) c4fTesterSuspend(&tester, now);
/* L3+R3 on the launcher's controller opens the controller test. It is
* tracked while testing too, so a hold still down after leaving the
* test does not open it again. */
int held = c4fTesterHoldUpdate(&launcherHold, input.standard.buttons, now);
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 = C4F_ACTION_NONE;
if (tester.active) {
/* Every button is under test, so the launcher's shortcuts are off. */
if (!c4fTesterFrame(&tester, now, input.remote.pressed)) waitRelease = 1;
} else {
uint32_t standardPressed = input.standard.pressed, remotePressed = input.remote.pressed;
if (waitRelease) {
/* The press that left the test must not act here as well. */
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)) {
c4fTesterOpen(&tester, now);
action = C4F_ACTION_NONE;
}
}
c4fTesterView(&tester, &c4fScreen.tester);
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; not during the controller test. */
uint32_t pressed = tester.active ? 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;
}
if (action == C4F_ACTION_CAPTURE || (pressed & ORBIS_PAD_BUTTON_OPTIONS)) c4fDiagCapture(4000);
#endif
c4fRenderAt = now;
done = c4fCloseRequested;
C4fLauncherScreen snapshot = c4fScreen;
C4fServiceStatus connectionStatus = c4fConnectionStatus;
int connectionService = c4fConnectionService;
uint64_t connectionCheckedAt = c4fConnectionCheckedAt;
#ifdef C4F_DIAG
uint64_t sampledAt = c4fInputAt;
#endif
pthread_mutex_unlock(&c4fMutex);
/* Redraw only when something changed, then copy it to each buffer once. */
if (!drawn || memcmp(&snapshot, &drawnState, sizeof(snapshot))) {
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();
@@ -547,18 +693,16 @@ int main(void)
c4fDrawLauncher(canvas, &snapshot);
#endif
}
drawnState = snapshot; drawn = 1; shown[0] = shown[1] = 0;
drawnState = visible; drawn = 1; shown[0] = shown[1] = 0;
}
#ifdef C4F_DIAG
/* How long the controller test takes to draw, for the frame budget. */
if (testDraws && now >= testDrawReportAt) {
printf("[c4f-launcher] controller test: %llu redraws, slowest %llu ms\n",
(unsigned long long)testDraws, (unsigned long long)testDrawMax);
testDraws = testDrawMax = 0;
testDrawReportAt = now + 5000;
}
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;
@@ -570,15 +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
c4fTesterClose(&tester);
c4fInputClose(&inputs);
sceNetCtlTerm();
c4fCloseVideo();
free(canvas);
+3
View File
@@ -678,6 +678,9 @@ int c4fScreenInit(void)
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;
}
+8
View File
@@ -18,6 +18,13 @@ typedef enum {
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;
@@ -25,6 +32,7 @@ typedef struct {
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];
+39 -2
View File
@@ -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;
}
+13 -1
View File
@@ -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. */
+169 -46
View File
@@ -5,10 +5,8 @@
#include "input.h"
#define C4F_TESTER_STICKS (C4F_INPUT_BUTTON_L3 | C4F_INPUT_BUTTON_R3)
/* Held keys that keep a new menu from accepting anything yet. */
#define C4F_TESTER_MENU_KEYS (C4F_TESTER_STICKS | C4F_INPUT_BUTTON_UP | C4F_INPUT_BUTTON_DOWN | \
C4F_INPUT_BUTTON_LEFT | C4F_INPUT_BUTTON_RIGHT | C4F_INPUT_BUTTON_CROSS | \
C4F_INPUT_BUTTON_CIRCLE)
#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)
{
@@ -53,6 +51,9 @@ static void c4fReaderRelease(C4fTester *t, int slot)
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));
}
@@ -155,12 +156,14 @@ static void c4fReaderPoll(C4fTester *t, int slot, uint64_t now)
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;
@@ -168,6 +171,12 @@ static void c4fReaderPoll(C4fTester *t, int slot, uint64_t now)
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;
@@ -206,7 +215,13 @@ static void c4fTesterResult(C4fTester *t, int result, const char *done)
static void c4fTesterAct(C4fTester *t, int item, uint64_t now)
{
static const uint8_t colors[3][3] = { { 255, 0, 0 }, { 0, 255, 0 }, { 0, 0, 255 } };
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;
@@ -234,16 +249,18 @@ static void c4fTesterAct(C4fTester *t, int item, uint64_t now)
c4fTesterResult(t, result, large ? "Large motor: rumbling for a second." : "Small motor: rumbling for a second.");
break;
}
case C4F_TESTER_LIGHT_RED:
case C4F_TESTER_LIGHT_GREEN:
case C4F_TESTER_LIGHT_BLUE: {
const uint8_t *rgb = colors[item - C4F_TESTER_LIGHT_RED];
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, "");
}
c4fTesterResult(t, result, "Light bar set. It goes back to normal when you leave.");
break;
}
case C4F_TESTER_LIGHT_RESET: {
@@ -279,9 +296,6 @@ void c4fTesterOpen(C4fTester *t, uint64_t now)
C4fTesterOps ops = t->ops;
c4fTesterInit(t, &ops);
t->active = 1;
/* The L3+R3 hold that opened the test must end before another counts. */
t->hold.holding = t->hold.fired = 1;
t->hold.since = now;
c4fTesterUsers(t, now);
}
@@ -304,8 +318,9 @@ void c4fTesterSuspend(C4fTester *t, uint64_t now)
}
}
t->menu = 0;
t->hold.holding = t->hold.fired = 1;
t->hold.since = now;
t->colorMenu = 0;
t->menuOpeners = 0;
t->armed = 0;
}
void c4fTesterClose(C4fTester *t)
@@ -317,26 +332,114 @@ void c4fTesterClose(C4fTester *t)
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);
uint32_t held = 0, pressed = 0;
int both = 0;
int menuOpener = -1;
for (int s = 0; s < C4F_TESTER_USERS; s++) {
c4fReaderPoll(t, s, now);
const C4fTesterReader *r = &t->readers[s];
if (r->state != C4F_TESTER_READY) continue;
held |= r->sample.buttons;
pressed |= r->pressed;
/* One controller holding both, not two holding one each. */
both |= (r->sample.buttons & C4F_TESTER_STICKS) == C4F_TESTER_STICKS;
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 (c4fTesterHoldUpdate(&t->hold, both ? C4F_TESTER_STICKS : 0, now) && !t->menu) {
t->menu = 1;
t->focus = C4F_TESTER_RESUME;
t->armed = 0;
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. */
@@ -345,26 +448,31 @@ int c4fTesterFrame(C4fTester *t, uint64_t now, uint32_t remotePressed)
return 0;
}
if (remotePressed & (C4F_INPUT_BUTTON_CROSS | C4F_INPUT_BUTTON_OPTIONS)) {
t->menu = 1;
t->focus = C4F_TESTER_RESUME;
t->armed = 0;
c4fTesterMenuOpen(t, -1);
}
return 1;
}
if (!t->armed) {
/* Nothing counts until the keys that opened the menu are let go. */
if (!(held & C4F_TESTER_MENU_KEYS)) t->armed = 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);
}
pressed |= remotePressed;
if (pressed & C4F_INPUT_BUTTON_CIRCLE) {
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 (pressed & C4F_INPUT_BUTTON_UP) {
t->focus = (t->focus + C4F_TESTER_ITEMS - 1) % C4F_TESTER_ITEMS;
} else if (pressed & C4F_INPUT_BUTTON_DOWN) {
t->focus = (t->focus + 1) % C4F_TESTER_ITEMS;
} else if (t->focus == C4F_TESTER_USER && (pressed & (C4F_INPUT_BUTTON_LEFT | C4F_INPUT_BUTTON_RIGHT))) {
c4fTesterNextUser(t, (pressed & C4F_INPUT_BUTTON_RIGHT) ? 1 : -1);
} else if (navigation) {
c4fTesterMenuMove(t, navigation);
} else if (pressed & C4F_INPUT_BUTTON_CROSS) {
if (t->focus == C4F_TESTER_EXIT) {
c4fTesterClose(t);
@@ -375,6 +483,18 @@ int c4fTesterFrame(C4fTester *t, uint64_t now, uint32_t remotePressed)
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;
@@ -385,15 +505,20 @@ static int16_t c4fScaled(float value, float scale)
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;
@@ -424,12 +549,10 @@ const char *c4fTesterItemLabel(int item)
{
switch (item) {
case C4F_TESTER_RESUME: return "Resume";
case C4F_TESTER_USER: return "User";
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_RED: return "Light bar: red";
case C4F_TESTER_LIGHT_GREEN: return "Light bar: green";
case C4F_TESTER_LIGHT_BLUE: return "Light bar: blue";
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";
+16 -7
View File
@@ -10,6 +10,9 @@
#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 {
@@ -48,10 +51,11 @@ enum { C4F_TESTER_OPENING, C4F_TESTER_READY, C4F_TESTER_NO_CONTROLLER, C4F_TESTE
/* The menu, in order. */
typedef enum {
C4F_TESTER_RESUME, C4F_TESTER_USER, C4F_TESTER_RUMBLE_LARGE, C4F_TESTER_RUMBLE_SMALL,
C4F_TESTER_LIGHT_RED, C4F_TESTER_LIGHT_GREEN, C4F_TESTER_LIGHT_BLUE, C4F_TESTER_LIGHT_RESET,
C4F_TESTER_ORIENTATION, C4F_TESTER_EXIT, C4F_TESTER_ITEMS
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 {
@@ -62,8 +66,9 @@ typedef struct {
typedef struct {
int32_t user; /* -1: empty slot */
int handle, owned, state, lightSet, seen;
uint32_t error, previous, pressed;
uint64_t retryAt;
uint32_t error, previous, pressed, menuBlocked, repeatDirection;
uint64_t retryAt, repeatAt;
C4fTesterHold hold;
uint8_t light[3];
char name[C4F_TESTER_NAME];
C4fTesterSample sample;
@@ -72,8 +77,9 @@ typedef struct {
typedef struct {
C4fTesterOps ops;
int active, menu, focus, armed, selected, rumble, rumbleReader;
int colorMenu, colorChannel, colorFine;
unsigned menuOpeners;
uint64_t usersAt, rumbleUntil;
C4fTesterHold hold;
C4fTesterReader readers[C4F_TESTER_USERS];
char note[96];
} C4fTester;
@@ -82,8 +88,9 @@ typedef struct {
* compares equal and causes no redraw. */
typedef struct {
int active, menu, focus, selected, rumble, lightSet;
int colorMenu, colorChannel, colorFine;
uint8_t light[3];
struct { char name[C4F_TESTER_NAME]; int state, used; uint32_t error; } user[C4F_TESTER_USERS];
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];
@@ -109,6 +116,8 @@ void c4fTesterSuspend(C4fTester *tester, uint64_t now);
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
+541 -93
View File
@@ -1,7 +1,7 @@
/* The controller test screen, in the launcher's look, with the controller drawn
* the way the controller page draws it (client/index.html: DEFAULT_LAYOUT and the
* .pad-area styles). A still layer is built once; each frame copies it and draws
* only what the controller is doing, so a moving stick redraws quickly. */
* .pad-area styles). Static backgrounds and expensive control artwork are
* cached. Live values still update every draw; tools have their own view. */
#include "screen.h"
#include <math.h>
#include <stdio.h>
@@ -61,6 +61,16 @@ typedef struct { float at; C4fColor color; } C4fStop;
static const C4fPlacement c4fIdentity = { 0, 0, 1, 0, 0, 0 };
static uint32_t *c4fLayer;
static uint32_t *c4fToolsBase, *c4fToolsFrame;
static int c4fToolsBaseReady;
typedef struct { uint32_t *px; int x, y, w, h; } C4fSprite;
static C4fSprite c4fKnobs[3], c4fPressed[16], c4fTouchGlow, c4fLightStrip;
static uint32_t c4fSpriteAccent;
static int c4fHasSpriteAccent;
static uint32_t *c4fSpriteScratch;
typedef struct { unsigned char *px; int x, y, w, h; } C4fMask;
static C4fMask c4fTriggerMasks[2];
/* The page's size unit, --u: min(1cqh, .46cqw) of the picture. */
static float c4fUnit(void) { return fminf(C4F_P_H / 100, .46f * C4F_P_W / 100); }
@@ -623,6 +633,12 @@ static void c4fBuildLayer(void)
c.px = px;
c4fPaintBackdrop(&c, C4F_S);
}
/* Preserve the clean waves before the live controller is painted. Opening
* tools later must not regenerate a whole-screen gradient and wave paths. */
if (!c4fToolsBase) {
c4fToolsBase = malloc(bytes);
if (c4fToolsBase) memcpy(c4fToolsBase, px, bytes);
}
/* .tile has backdrop-filter: blur(6px). */
c4fBlurUnder(&c, C4F_T_LEFT, C4F_T_TOP, C4F_T_PAD_W, C4F_T_HEIGHT, 6 * C4F_S, 6 * C4F_S);
c4fBlurUnder(&c, C4F_T_INFO_X, C4F_T_TOP, C4F_T_INFO_W, C4F_T_HEIGHT, 6 * C4F_S, 6 * C4F_S);
@@ -633,6 +649,7 @@ static void c4fBuildLayer(void)
x += c4fText(&c, C4F_FONT_LIGHT, 17 * C4F_S, x, 63, C4F_TEXT, "Control4Free") + 14 * C4F_S;
c4fDot(&c, x + 4 * C4F_S, 55, C4F_ACCENT, 0);
c4fText(&c, C4F_FONT_LIGHT, 13 * C4F_S, x + 16 * C4F_S, 62, C4F_MUTED, "Controller test");
c4fDrawSolidarityBadge(&c, C4F_SCREEN_WIDTH - 64, 40);
c4fText(&c, C4F_FONT_LIGHT, 48, C4F_T_LEFT, 140, C4F_TEXT, "Controller test");
c4fTextWrapped(&c, C4F_FONT_LIGHT, 24, C4F_T_LEFT, 184, C4F_T_RIGHT - C4F_T_LEFT, 34, 2, C4F_MUTED,
@@ -717,33 +734,31 @@ static void c4fTriggerFill(C4fCanvas *c, int id, int pressure, C4fColor accent)
if (pressure <= 0) return;
const C4fBox b = c4fControl(id);
const float top = b.y + b.h * (1 - pressure / 255.0f);
C4fPath p = { 0 };
c4fShapePath(&p, id, 0);
unsigned char *mask;
int x0, y0, w, h;
if (c4fPathMask(c, &p, c4fIdentity, &mask, &x0, &y0, &w, &h)) {
for (int y = 0; y < h; y++) {
float rowTop = y0 + y, cover = fmaxf(0, fminf(1, rowTop + 1 - top));
C4fMask *mask = &c4fTriggerMasks[id == C4F_C_R2];
if (!mask->px) {
C4fPath p = { 0 };
c4fShapePath(&p, id, 0);
c4fPathMask(c, &p, c4fIdentity, &mask->px, &mask->x, &mask->y, &mask->w, &mask->h);
c4fPathFree(&p);
}
if (mask->px) {
for (int y = 0; y < mask->h; y++) {
float rowTop = mask->y + y, cover = fmaxf(0, fminf(1, rowTop + 1 - top));
if (cover <= 0) continue;
for (int x = 0; x < w; x++)
if (mask[y * w + x]) c4fBlend(c, x0 + x, y0 + y, c4fAlpha(accent, .42f), cover * mask[y * w + x] / 255.0f);
for (int x = 0; x < mask->w; x++)
if (mask->px[y * mask->w + x]) c4fBlend(c, mask->x + x, mask->y + y, c4fAlpha(accent, .42f),
cover * mask->px[y * mask->w + x] / 255.0f);
}
free(mask);
}
c4fPathFree(&p);
}
static void c4fKnob(C4fCanvas *c, int id, int sx, int sy, int clicked, C4fColor accent)
static void c4fPaintKnob(C4fCanvas *c, float cx, float cy, float radius, int clicked, int highlighted, C4fColor accent)
{
const C4fBox b = c4fControl(id);
const int moved = sx != 128 || sy != 128;
const float travel = b.w * C4F_KNOB_TRAVEL;
const float cx = b.x + b.w / 2 + (sx - 128) / 127.5f * travel, cy = b.y + b.h / 2 + (sy - 128) / 127.5f * travel;
const float r = b.w / 4 * (clicked ? .9f : 1);
const float r = radius * (clicked ? .9f : 1);
C4fPath p = { 0 };
c4fPathCircle(&p, cx, cy, r, 1);
c4fShadowPath(c, &p, c4fIdentity, 0, 8, 9, C4F_RGBA(0, 0, 0, .7f));
if (moved || clicked) c4fShadowPath(c, &p, c4fIdentity, 0, 0, 9, c4fAlpha(accent, .65f));
if (highlighted) c4fShadowPath(c, &p, c4fIdentity, 0, 0, 9, c4fAlpha(accent, .65f));
const C4fStop body[3] = { { 0, c4fHex(clicked ? 0x56607b : 0x3f475b) }, { .55f, c4fHex(clicked ? 0x323a4d : 0x252b39) },
{ 1, c4fHex(clicked ? 0x1e2331 : 0x161a24) } };
c4fGradientPath(c, &p, body, 3, cy - r, 2 * r, 0, 0, 0);
@@ -756,7 +771,7 @@ static void c4fKnob(C4fCanvas *c, int id, int sx, int sy, int clicked, C4fColor
if (fmodf(degrees, 8) < 4) c4fBlend(c, x, y, C4F_RGBA(255, 255, 255, .045f), fminf(1, r - d));
}
c4fInsetPath(c, &p, 1, 1, 0, C4F_RGBA(255, 255, 255, .2f));
c4fRing(c, cx, cy, r + .5f, 1, (moved || clicked) ? c4fAlpha(accent, .8f) : C4F_RGBA(0, 0, 0, .55f));
c4fRing(c, cx, cy, r + .5f, 1, highlighted ? c4fAlpha(accent, .8f) : C4F_RGBA(0, 0, 0, .55f));
c4fPathFree(&p);
/* The thumb dish: dark at the top and lit at the bottom, so it reads as hollow. */
const float dish = r * .66f;
@@ -767,55 +782,203 @@ static void c4fKnob(C4fCanvas *c, int id, int sx, int sy, int clicked, C4fColor
c4fPathFree(&p);
}
static void c4fDrawInput(C4fCanvas *c, const C4fTesterView *v, C4fColor accent)
static void c4fPaintLightStrip(C4fCanvas *c, C4fColor accent)
{
const uint32_t b = v->buttons;
static const uint32_t shoulders[4] = { C4F_INPUT_BUTTON_L2, C4F_INPUT_BUTTON_L1, C4F_INPUT_BUTTON_R2, C4F_INPUT_BUTTON_R1 };
for (int i = 0; i < 4; i++) {
if (b & shoulders[i]) {
C4fPath p = { 0 };
c4fShapePath(&p, i, 0);
c4fPlasticLit(c, &p, c4fControl(i), accent);
c4fPathFree(&p);
}
if (i == C4F_C_L2 || i == C4F_C_R2) c4fTriggerFill(c, i, i == C4F_C_L2 ? v->l2 : v->r2, accent);
if ((b & shoulders[i]) || (i == C4F_C_L2 && v->l2) || (i == C4F_C_R2 && v->r2))
c4fEngraved(c, c4fControl(i), c4fShoulderNames[i], C4F_P_ENGRAVED);
}
if (b & C4F_INPUT_BUTTON_OPTIONS) {
const C4fBox pad = c4fControl(C4F_C_TOUCHPAD);
const float u = c4fUnit(), sx = pad.x + .07f * pad.w, sw = .86f * pad.w, sh = fmaxf(3, .8f * u);
c4fShadowRoundRect(c, sx, pad.y - 2, sw, sh + 2, 2, 0, -2, 3.5f * u, 0, c4fAlpha(accent, .55f));
c4fShadowRoundRect(c, sx, pad.y, sw, sh, 2, 0, 0, 2.5f * u, 0, c4fAlpha(accent, .9f));
c4fFillRoundRect(c, sx, pad.y, sw, sh, sh / 2, accent);
}
static void c4fPaintTouch(C4fCanvas *c, float x, float y, C4fColor accent)
{
const float r = 3.5f * c4fUnit();
c4fGlowCircle(c, x, y, r, 20, 0, c4fAlpha(accent, .8f));
c4fFillCircle(c, x, y, r, c4fAlpha(accent, .45f));
c4fRing(c, x, y, r, 1, c4fAlpha(accent, .85f));
}
/* One transparent sprite per pressed control. Overlapping glows still compose
* in their original order; the static controller is never baked into a sprite. */
static void c4fPaintPressed(C4fCanvas *c, int index, C4fColor accent)
{
if (index < 4 || index >= 14) {
const int id = index < 4 ? index : index == 14 ? C4F_C_L3 : C4F_C_R3;
C4fPath p = { 0 };
c4fShapePath(&p, id, 0);
c4fPlasticLit(c, &p, c4fControl(id), accent);
if (index >= 14) c4fEngraved(c, c4fControl(id), index == 14 ? "L3" : "R3", C4F_P_ENGRAVED);
c4fPathFree(&p);
} else if (index == 4) {
C4fPath p = { 0 };
c4fCapsulePath(&p, C4F_C_OPTIONS, 0);
c4fPlasticLit(c, &p, c4fCapsule(C4F_C_OPTIONS), accent);
c4fPathFree(&p);
}
static const uint32_t arms[4] = { C4F_INPUT_BUTTON_UP, C4F_INPUT_BUTTON_RIGHT, C4F_INPUT_BUTTON_DOWN, C4F_INPUT_BUTTON_LEFT };
for (int arm = 0; arm < 4; arm++) if (b & arms[arm]) c4fArm(c, arm, 1, accent);
static const uint32_t faces[4] = { C4F_INPUT_BUTTON_TRIANGLE, C4F_INPUT_BUTTON_CIRCLE, C4F_INPUT_BUTTON_CROSS, C4F_INPUT_BUTTON_SQUARE };
for (int i = 0; i < 4; i++) if (b & faces[i]) c4fFaceButton(c, i, 1, 1);
const C4fBox pad = c4fControl(C4F_C_TOUCHPAD);
if (b & C4F_INPUT_BUTTON_TOUCHPAD) {
/* .touchpad.clicked: a lit edge (its outer glow stays under the plate). */
} else if (index < 9) {
c4fArm(c, index - 5, 1, accent);
} else if (index < 13) {
c4fFaceButton(c, index - 9, 1, 1);
} else {
C4fPath p = { 0 };
c4fShapePath(&p, C4F_C_TOUCHPAD, 0);
c4fInsetPath(c, &p, 2, 0, 0, c4fAlpha(accent, .7f));
c4fPathFree(&p);
}
const float u = c4fUnit();
}
static const uint32_t c4fPressedBits[16] = {
C4F_INPUT_BUTTON_L2, C4F_INPUT_BUTTON_L1, C4F_INPUT_BUTTON_R2, C4F_INPUT_BUTTON_R1, C4F_INPUT_BUTTON_OPTIONS,
C4F_INPUT_BUTTON_UP, C4F_INPUT_BUTTON_RIGHT, C4F_INPUT_BUTTON_DOWN, C4F_INPUT_BUTTON_LEFT,
C4F_INPUT_BUTTON_TRIANGLE, C4F_INPUT_BUTTON_CIRCLE, C4F_INPUT_BUTTON_CROSS, C4F_INPUT_BUTTON_SQUARE,
C4F_INPUT_BUTTON_TOUCHPAD, C4F_INPUT_BUTTON_L3, C4F_INPUT_BUTTON_R3,
};
/* The drawing API blends onto an opaque destination. Black/white matte renders
* recover a premultiplied sprite once, including antialiasing and soft shadows.
* The caller's frame is scratch here, before the still layer is copied into it. */
static void c4fPaintSprite(C4fCanvas *c, int kind, int index, C4fColor accent)
{
if (kind == 0) c4fPaintKnob(c, 100, 100, c4fControl(C4F_C_LSTICK).w / 4, index == 2, index != 0, accent);
else if (kind == 1) c4fPaintPressed(c, index, accent);
else if (kind == 2) c4fPaintTouch(c, 100, 100, accent);
else c4fPaintLightStrip(c, accent);
}
static void c4fBuildSprite(C4fCanvas *work, C4fSprite *s, C4fBox box, int kind, int index, C4fColor accent)
{
if (s->px) return;
s->x = (int)floorf(fmaxf(0, box.x)); s->y = (int)floorf(fmaxf(0, box.y));
s->w = (int)ceilf(fminf(work->w, box.x + box.w)) - s->x;
s->h = (int)ceilf(fminf(work->h, box.y + box.h)) - s->y;
if (s->w <= 0 || s->h <= 0) return;
s->px = malloc((size_t)s->w * s->h * sizeof(uint32_t));
if (!s->px) return;
if (!c4fSpriteScratch) c4fSpriteScratch = malloc((size_t)work->w * work->h * sizeof(uint32_t));
if (!c4fSpriteScratch) { free(s->px); s->px = NULL; return; }
C4fCanvas black = { c4fSpriteScratch, work->w, work->h };
/* Painters may sample/paint outside the capture rectangle. Initializing the
* complete matte avoids undefined reads even if their footprint changes. */
for (int i = 0; i < work->w * work->h; i++) black.px[i] = 0xff000000;
c4fPaintSprite(&black, kind, index, accent);
for (int y = 0; y < s->h; y++)
memcpy(s->px + y * s->w, black.px + (s->y + y) * black.w + s->x, (size_t)s->w * sizeof(uint32_t));
for (int i = 0; i < work->w * work->h; i++) work->px[i] = 0xffffffff;
c4fPaintSprite(work, kind, index, accent);
for (int y = 0; y < s->h; y++)
for (int x = 0; x < s->w; x++) {
const uint32_t b = s->px[y * s->w + x], w = work->px[(s->y + y) * work->w + s->x + x];
int a = 255 - ((int)((w >> 8) & 255) - (int)((b >> 8) & 255));
if (a < 0) a = 0;
if (a > 255) a = 255;
const unsigned r = (b >> 16) & 255, g = (b >> 8) & 255, blue = b & 255;
s->px[y * s->w + x] = (uint32_t)a << 24 | (r > (unsigned)a ? (unsigned)a : r) << 16 |
(g > (unsigned)a ? (unsigned)a : g) << 8 | (blue > (unsigned)a ? (unsigned)a : blue);
}
}
static void c4fBlitSprite(C4fCanvas *c, const C4fSprite *s, int dx, int dy)
{
for (int y = 0; y < s->h; y++) {
const int py = s->y + y + dy;
if (py < 0 || py >= c->h) continue;
for (int x = 0; x < s->w; x++) {
const int px = s->x + x + dx;
if (px < 0 || px >= c->w) continue;
const uint32_t src = s->px[y * s->w + x];
const unsigned a = src >> 24;
if (!a) continue;
uint32_t *dst = c->px + py * c->w + px;
if (a == 255) { *dst = src; continue; }
const unsigned inv = 255 - a;
const unsigned r = ((src >> 16) & 255) + (((*dst >> 16) & 255) * inv + 127) / 255;
const unsigned g = ((src >> 8) & 255) + (((*dst >> 8) & 255) * inv + 127) / 255;
const unsigned b = (src & 255) + ((*dst & 255) * inv + 127) / 255;
*dst = 0xff000000 | r << 16 | g << 8 | b;
}
}
}
static C4fBox c4fPressedBounds(int index)
{
C4fBox b;
if (index < 4) b = c4fControl(index);
else if (index == 4) b = c4fCapsule(C4F_C_OPTIONS);
else if (index < 9) b = c4fControl(C4F_C_DPAD);
else if (index < 13) {
float x, y, r;
c4fFaceCentre(index - 9, &x, &y, &r);
b = (C4fBox){ x - r, y - r, 2 * r, 2 * r };
} else b = c4fControl(index == 13 ? C4F_C_TOUCHPAD : index == 14 ? C4F_C_L3 : C4F_C_R3);
return (C4fBox){ b.x - 40, b.y - 40, b.w + 80, b.h + 80 };
}
static void c4fPrepareSprites(C4fCanvas *work, const C4fTesterView *v, C4fColor accent, int ready)
{
const uint32_t rgb = (uint32_t)accent.r << 16 | (uint32_t)accent.g << 8 | accent.b;
if (!c4fHasSpriteAccent || c4fSpriteAccent != rgb) {
for (int i = 0; i < 3; i++) { free(c4fKnobs[i].px); c4fKnobs[i].px = NULL; }
for (int i = 0; i < 16; i++) { free(c4fPressed[i].px); c4fPressed[i].px = NULL; }
free(c4fTouchGlow.px); c4fTouchGlow.px = NULL;
free(c4fLightStrip.px); c4fLightStrip.px = NULL;
c4fSpriteAccent = rgb; c4fHasSpriteAccent = 1;
}
const C4fBox pad = c4fControl(C4F_C_TOUCHPAD);
c4fBuildSprite(work, &c4fLightStrip, (C4fBox){ pad.x - 30, pad.y - 55, pad.w + 60, 110 }, 3, 0, accent);
if (ready) {
/* All three knobs are warmed together: moving/clicking a stick should
* never introduce a first-use shadow/ridge calculation into a frame. */
for (int i = 0; i < 3; i++) c4fBuildSprite(work, &c4fKnobs[i], (C4fBox){ 30, 30, 140, 150 }, 0, i, accent);
for (int i = 0; i < 16; i++) if (v->buttons & c4fPressedBits[i])
c4fBuildSprite(work, &c4fPressed[i], c4fPressedBounds(i), 1, i, accent);
if (v->touches) c4fBuildSprite(work, &c4fTouchGlow, (C4fBox){ 45, 45, 110, 110 }, 2, 0, accent);
}
free(c4fSpriteScratch); c4fSpriteScratch = NULL;
}
static void c4fKnob(C4fCanvas *c, int id, int sx, int sy, int clicked, C4fColor accent)
{
const C4fBox b = c4fControl(id);
const float cx = b.x + b.w / 2 + (sx - 128) / 128.0f * b.w * C4F_KNOB_TRAVEL;
const float cy = b.y + b.h / 2 + (sy - 128) / 128.0f * b.h * C4F_KNOB_TRAVEL;
const int moved = abs(sx - 128) > 3 || abs(sy - 128) > 3, state = clicked ? 2 : moved ? 1 : 0;
if (c4fKnobs[state].px) c4fBlitSprite(c, &c4fKnobs[state], (int)lroundf(cx) - 100, (int)lroundf(cy) - 100);
else c4fPaintKnob(c, cx, cy, b.w / 4, clicked, moved || clicked, accent);
}
static void c4fDrawPressed(C4fCanvas *c, int index, C4fColor accent)
{
if (c4fPressed[index].px) c4fBlitSprite(c, &c4fPressed[index], 0, 0);
else c4fPaintPressed(c, index, accent);
}
static void c4fDrawInput(C4fCanvas *c, const C4fTesterView *v, C4fColor accent)
{
const uint32_t b = v->buttons;
static const uint32_t shoulders[4] = { C4F_INPUT_BUTTON_L2, C4F_INPUT_BUTTON_L1, C4F_INPUT_BUTTON_R2, C4F_INPUT_BUTTON_R1 };
for (int i = 0; i < 4; i++) {
if (b & shoulders[i]) c4fDrawPressed(c, i, accent);
if (i == C4F_C_L2 || i == C4F_C_R2) c4fTriggerFill(c, i, i == C4F_C_L2 ? v->l2 : v->r2, accent);
if ((b & shoulders[i]) || (i == C4F_C_L2 && v->l2) || (i == C4F_C_R2 && v->r2))
c4fEngraved(c, c4fControl(i), c4fShoulderNames[i], C4F_P_ENGRAVED);
}
if (b & C4F_INPUT_BUTTON_OPTIONS) c4fDrawPressed(c, 4, accent);
static const uint32_t arms[4] = { C4F_INPUT_BUTTON_UP, C4F_INPUT_BUTTON_RIGHT, C4F_INPUT_BUTTON_DOWN, C4F_INPUT_BUTTON_LEFT };
for (int arm = 0; arm < 4; arm++) if (b & arms[arm]) c4fDrawPressed(c, 5 + arm, accent);
static const uint32_t faces[4] = { C4F_INPUT_BUTTON_TRIANGLE, C4F_INPUT_BUTTON_CIRCLE, C4F_INPUT_BUTTON_CROSS, C4F_INPUT_BUTTON_SQUARE };
for (int i = 0; i < 4; i++) if (b & faces[i]) c4fDrawPressed(c, 9 + i, accent);
const C4fBox pad = c4fControl(C4F_C_TOUCHPAD);
if (b & C4F_INPUT_BUTTON_TOUCHPAD) c4fDrawPressed(c, 13, accent);
for (int i = 0; i < v->touches && i < 2; i++) {
float tx = pad.x + pad.w * (v->touchX[i] / 1919.0f), ty = pad.y + pad.h * (v->touchY[i] / 941.0f);
c4fGlowCircle(c, tx, ty, 3.5f * u, 20, 0, c4fAlpha(accent, .8f));
c4fFillCircle(c, tx, ty, 3.5f * u, c4fAlpha(accent, .45f));
c4fRing(c, tx, ty, 3.5f * u, 1, c4fAlpha(accent, .85f));
if (c4fTouchGlow.px) c4fBlitSprite(c, &c4fTouchGlow, (int)lroundf(tx) - 100, (int)lroundf(ty) - 100);
else c4fPaintTouch(c, tx, ty, accent);
}
c4fKnob(c, C4F_C_LSTICK, v->lx, v->ly, !!(b & C4F_INPUT_BUTTON_L3), accent);
c4fKnob(c, C4F_C_RSTICK, v->rx, v->ry, !!(b & C4F_INPUT_BUTTON_R3), accent);
for (int id = C4F_C_L3; id <= C4F_C_R3; id++) {
if (!(b & (id == C4F_C_L3 ? C4F_INPUT_BUTTON_L3 : C4F_INPUT_BUTTON_R3))) continue;
C4fPath p = { 0 };
c4fShapePath(&p, id, 0);
c4fPlasticLit(c, &p, c4fControl(id), accent);
c4fEngraved(c, c4fControl(id), id == C4F_C_L3 ? "L3" : "R3", C4F_P_ENGRAVED);
c4fPathFree(&p);
c4fDrawPressed(c, id == C4F_C_L3 ? 14 : 15, accent);
}
}
@@ -823,11 +986,8 @@ static void c4fDrawInput(C4fCanvas *c, const C4fTesterView *v, C4fColor accent)
static void c4fDrawTouchpad(C4fCanvas *c, C4fColor accent, const char *line1, const char *line2)
{
const C4fBox pad = c4fControl(C4F_C_TOUCHPAD);
const float u = c4fUnit(), sx = pad.x + .07f * pad.w, sw = .86f * pad.w, sh = fmaxf(3, .8f * u);
/* The light spills up over the plate's top edge. */
c4fShadowRoundRect(c, sx, pad.y - 2, sw, sh + 2, 2, 0, -2, 3.5f * u, 0, c4fAlpha(accent, .55f));
c4fShadowRoundRect(c, sx, pad.y, sw, sh, 2, 0, 0, 2.5f * u, 0, c4fAlpha(accent, .9f));
c4fFillRoundRect(c, sx, pad.y, sw, sh, sh / 2, accent);
if (c4fLightStrip.px) c4fBlitSprite(c, &c4fLightStrip, 0, 0);
else c4fPaintLightStrip(c, accent);
if (line1) c4fTextCentered(c, C4F_FONT_LIGHT, 22, pad.x + pad.w / 2, pad.y + pad.h * .9f - 30, C4F_P_ENGRAVED, line1);
if (line2) c4fTextCentered(c, C4F_FONT_LIGHT, 20, pad.x + pad.w / 2, pad.y + pad.h * .9f - 4,
C4F_RGBA(236, 240, 248, .36f), line2);
@@ -878,41 +1038,327 @@ static void c4fDrawValues(C4fCanvas *c, const C4fTesterView *v, int ready)
}
}
static void c4fDrawMenu(C4fCanvas *c, const C4fTesterView *v)
/* Browser-reported round trips, as on the controller page. This is separate
* from the native pad samples and does not measure input or TV display delay. */
static void c4fDrawConnection(C4fCanvas *c, const C4fTesterView *v, const C4fTesterConnection *connection,
float x, float baseline)
{
const float dw = 900, row = 60, dh = 168 + C4F_TESTER_ITEMS * row + 70;
const float dx = (c->w - dw) / 2, dy = (c->h - dh) / 2;
c4fFillRect(c, 0, 0, c->w, c->h, C4F_RGBA(2, 11, 31, .65f));
c4fShadowRoundRect(c, dx, dy, dw, dh, 6 * C4F_S, 0, 16 * C4F_S, 34 * C4F_S, -18 * C4F_S, C4F_RGBA(0, 0, 0, .9f));
c4fPanel(c, dx, dy, dw, dh, C4F_RGBA(2, 11, 31, .97f), C4F_LINE);
c4fBandRoundRect(c, dx, dy, dw, dh, 6 * C4F_S, 3 * C4F_S, C4F_ACCENT);
c4fText(c, C4F_FONT_LIGHT, 44, dx + 48, dy + 80, C4F_TEXT, "Controller test");
c4fText(c, C4F_FONT_LIGHT, 24, dx + 48, dy + 122, C4F_MUTED, "Choose with the D-pad or arrows, then press Cross or OK.");
for (int i = 0; i < C4F_TESTER_ITEMS; i++) {
const float rx = dx + 32, ry = dy + 150 + i * row, rw = dw - 64, rh = row - 8;
int focused = i == v->focus;
c4fFillRoundRect(c, rx, ry, rw, rh, 6 * C4F_S, focused ? C4F_RGBA(40, 103, 205, .85f) : C4F_RGBA(255, 255, 255, .04f));
c4fInsetRoundRect(c, rx, ry, rw, rh, 6 * C4F_S, focused ? 3 : 1 * C4F_S, focused ? C4F_ACCENT : C4F_LINE_SOFT);
c4fText(c, C4F_FONT_REGULAR, 28, rx + 24, ry + 36, focused ? C4F_TEXT : C4F_MUTED, c4fTesterItemLabel(i));
if (i == C4F_TESTER_USER) {
const char *name = v->selected >= 0 ? v->user[v->selected].name : "None";
float right = rx + rw - (focused ? 48 : 24);
c4fTextRight(c, C4F_FONT_REGULAR, 28, right, ry + 36, C4F_TEXT, name);
if (focused) {
float lx = right - c4fTextWidth(C4F_FONT_REGULAR, 28, name) - 22, cy = ry + rh / 2;
c4fSegment(c, lx + 4, cy - 9, lx - 5, cy, 3, C4F_TEXT);
c4fSegment(c, lx - 5, cy, lx + 4, cy + 9, 3, C4F_TEXT);
}
}
if (focused) {
float cx = rx + rw - 26, cy = ry + rh / 2;
c4fSegment(c, cx - 5, cy - 9, cx + 4, cy, 3, C4F_TEXT);
c4fSegment(c, cx + 4, cy, cx - 5, cy + 9, 3, C4F_TEXT);
c4fText(c, C4F_FONT_LIGHT, 20, x, baseline, C4F_MUTED, "Controller page RTT");
const float values = x + 236;
if (v->selected < 0 || v->selected >= C4F_TESTER_USERS || !connection->available) {
c4fText(c, C4F_FONT_REGULAR, 22, values, baseline, C4F_FAINT, "--");
} else if (!(connection->pads & 15U)) {
c4fText(c, C4F_FONT_LIGHT, 22, values, baseline, C4F_MUTED, "No controller page for this user");
} else {
int column = 0;
for (int slot = 0; slot < 4; slot++) {
if (!(connection->pads & (1U << slot))) continue;
char text[40];
if (connection->rttMs[slot] < 0) snprintf(text, sizeof(text), "C%d measuring...", slot + 1);
else snprintf(text, sizeof(text), "C%d %d ms", slot + 1, connection->rttMs[slot]);
c4fText(c, C4F_FONT_REGULAR, 22, values + column++ * 176, baseline,
connection->rttMs[slot] < 0 ? C4F_MUTED : C4F_TEXT, text);
}
}
static const C4fHint hints[2] = { { C4F_KEY_CROSS, "Select" }, { C4F_KEY_CIRCLE, "Back" } };
c4fText(c, C4F_FONT_LIGHT, 23, dx + 48, dy + dh - 30, C4F_MUTED, "TV remote: OK select / Back return");
c4fHints(c, dx + dw - 48, dy + dh - 38, hints, 2);
}
/* ---- dedicated tools view ---- */
#define C4F_TOOL_W 386
#define C4F_TOOL_H 220
#define C4F_TOOL_GAP_X 25
#define C4F_TOOL_GAP_Y 28
#define C4F_TOOL_Y 360
static C4fSprite c4fToolTiles[C4F_TESTER_ITEMS][4];
static C4fTesterView c4fToolsKey;
static C4fTesterConnection c4fToolsConnection;
static unsigned c4fToolsAvailable;
static int c4fHasToolsKey;
static C4fBox c4fToolBox(int item)
{
return (C4fBox){ C4F_T_LEFT + (item % C4F_TESTER_MENU_COLUMNS) * (C4F_TOOL_W + C4F_TOOL_GAP_X),
C4F_TOOL_Y + (item / C4F_TESTER_MENU_COLUMNS) * (C4F_TOOL_H + C4F_TOOL_GAP_Y),
C4F_TOOL_W, C4F_TOOL_H };
}
/* Small, consistent line icons, drawn once into the tile cache. */
static void c4fToolIcon(C4fCanvas *c, int item, float x, float y, C4fColor color)
{
static const char *const icons[C4F_TESTER_ITEMS] = {
"M21 14L49 32L21 50Z",
"M31 17C31 22 27 26 22 26C17 26 13 22 13 17C13 12 17 8 22 8C27 8 31 12 31 17Z "
"M7 48V43C7 36 14 31 22 31C30 31 37 36 37 43V48 M40 11C46 11 51 15 51 21C51 26 47 30 42 31 "
"M43 41H60M54 35L60 41L54 47",
"M53 25C50 14 39 8 28 11C17 14 11 25 14 36C17 47 29 53 40 49C45 48 49 44 51 40 "
"M53 12V25H40 M32 23V41M23 32H41",
"M24 43V39C24 35 17 32 17 24C17 16 24 10 32 10C40 10 47 16 47 24C47 32 40 35 40 39V43Z "
"M25 49H39M28 55H36 M32 1V4M10 8L14 12M1 25H7M54 12L58 8M57 25H63",
"M18 22H41V45H18Z M25 17V22M34 17V22M25 45V50M34 45V50 M12 23C7 29 7 38 12 44 "
"M47 23C52 29 52 38 47 44 M4 18C-4 27-4 40 4 49M55 18C63 27 63 40 55 49",
"M24 25H39V42H24Z M29 20V25M34 20V25M29 42V47M34 42V47 "
"M18 25C13 30 13 37 18 42 M45 25C50 30 50 37 45 42",
"M25 43V39C25 35 19 32 19 25C19 18 25 13 32 13C39 13 45 18 45 25C45 32 39 35 39 39V43Z "
"M26 49H38M29 55H35 M8 26C8 13 18 3 31 3C39 3 46 7 50 13M50 3V13H40",
"M28 10H11V54H28M35 14L53 32L35 50M23 32H53",
};
C4fPath p = { 0 };
c4fSvgPath(&p, icons[item], x, y, 1);
c4fStrokePath(c, &p, 3, color);
c4fPathFree(&p);
}
static const char *c4fToolHelp(int item, int available)
{
static const char *const help[C4F_TESTER_ITEMS] = {
"Return to live input", "Choose another player", "Recenter motion", "Choose any RGB color",
"One-second vibration", "One-second vibration", "Restore default color", "Back to the launcher",
};
if (!available) return item == C4F_TESTER_USER ? "No other signed-in user" : "Controller unavailable";
return help[item];
}
static void c4fPaintToolTile(C4fCanvas *c, int item, int focused, int available)
{
static const char *const labels[C4F_TESTER_ITEMS] = {
"Resume", "Switch user", "Reset orientation", "Light bar",
"Large motor", "Small motor", "Reset light", "Leave test",
};
const C4fBox b = c4fToolBox(item);
const C4fColor fill = focused ? (available ? C4F_RGBA(35, 91, 180, .94f) : C4F_RGBA(27, 57, 104, .96f)) :
available ? C4F_RGBA(7, 29, 72, .82f) : C4F_RGBA(5, 20, 49, .82f);
c4fFillRoundRect(c, b.x, b.y, b.w, b.h, 10, fill);
c4fInsetRoundRect(c, b.x, b.y, b.w, b.h, 10, focused ? 3 : 1.5f,
focused ? C4F_ACCENT : available ? C4F_LINE : C4F_LINE_SOFT);
C4fColor icon = available ? (focused ? C4F_TEXT : C4F_CROSS_KEY) : C4F_FAINT;
c4fToolIcon(c, item, b.x + 28, b.y + 29, icon);
c4fText(c, C4F_FONT_REGULAR, 30, b.x + 26, b.y + 137, available ? C4F_TEXT : C4F_FAINT, labels[item]);
c4fText(c, C4F_FONT_LIGHT, 22, b.x + 26, b.y + 174, available ? C4F_MUTED : C4F_FAINT, c4fToolHelp(item, available));
if (focused) {
c4fFillRoundRect(c, b.x + 26, b.y + b.h - 16, b.w - 52, 3, 1.5f, C4F_ACCENT);
c4fFillCircle(c, b.x + b.w - 28, b.y + 32, 4, C4F_TEXT);
}
if (!available) {
/* A lock gives unavailable actions a second cue besides muted color. */
const float x = b.x + b.w - 39, y = b.y + 54;
c4fFillRoundRect(c, x, y, 17, 14, 3, C4F_FAINT);
C4fPath p = { 0 };
c4fSvgPath(&p, "M3 8V4C3-3 14-3 14 4V8", x, y - 7, 1);
c4fStrokePath(c, &p, 2, C4F_FAINT);
c4fPathFree(&p);
}
}
static void c4fCopyOpaque(C4fCanvas *c, const C4fSprite *s)
{
for (int y = 0; y < s->h; y++)
memcpy(c->px + (s->y + y) * c->w + s->x, s->px + y * s->w, (size_t)s->w * sizeof(uint32_t));
}
static int c4fBuildTools(C4fCanvas *work)
{
if (c4fToolsBase && c4fToolsFrame) return 1;
const size_t bytes = (size_t)work->w * work->h * sizeof(uint32_t);
if (!c4fToolsBase) {
c4fToolsBase = c4fTakeCleanBackdrop();
if (!c4fToolsBase) {
c4fToolsBase = malloc(bytes);
if (!c4fToolsBase) return 0;
C4fCanvas background = { c4fToolsBase, work->w, work->h };
c4fPaintBackdrop(&background, C4F_S);
}
}
if (!c4fToolsBaseReady) {
C4fCanvas c = { c4fToolsBase, work->w, work->h };
c4fMark(&c, 88, 54, 48, C4F_WHITE);
float x = 128;
x += c4fText(&c, C4F_FONT_LIGHT, 17 * C4F_S, x, 63, C4F_TEXT, "Control4Free") + 14 * C4F_S;
c4fDot(&c, x + 4 * C4F_S, 55, C4F_ACCENT, 0);
c4fText(&c, C4F_FONT_LIGHT, 13 * C4F_S, x + 16 * C4F_S, 62, C4F_MUTED, "Controller test");
c4fDrawSolidarityBadge(&c, C4F_SCREEN_WIDTH - 64, 40);
c4fFillRect(&c, C4F_T_LEFT, 345, C4F_T_RIGHT - C4F_T_LEFT, 2, C4F_LINE);
c4fFillRoundRect(&c, C4F_T_LEFT, 858, C4F_T_RIGHT - C4F_T_LEFT, 92, 10, C4F_RGBA(3, 16, 44, .5f));
c4fText(&c, C4F_FONT_LIGHT, 20, C4F_T_LEFT, 1030, C4F_FAINT, "Control4Free " C4F_LAUNCHER_VERSION);
c4fToolsBaseReady = 1;
}
if (!c4fToolsFrame) c4fToolsFrame = malloc(bytes);
return c4fToolsFrame != NULL;
}
static void c4fDrawToolsDetails(C4fCanvas *c, const C4fTesterView *v, const C4fTesterConnection *connection)
{
c4fText(c, C4F_FONT_LIGHT, 48, C4F_T_LEFT, 154, C4F_TEXT, v->colorMenu ? "Light bar" : "Controller tools");
c4fText(c, C4F_FONT_LIGHT, 24, C4F_T_LEFT, 202, C4F_MUTED, v->colorMenu ?
"Mix red, green and blue. Changes preview on the selected controller." : "Test motion, vibration and the light bar.");
if (!v->colorMenu) c4fTextRight(c, C4F_FONT_LIGHT, 24, C4F_T_RIGHT, 202, C4F_MUTED, "D-pad or arrows to move");
const C4fHint hints[2] = { { C4F_KEY_CROSS, v->colorMenu ? (v->colorFine ? "Fast adjustment" : "Fine adjustment") : "Select" },
{ C4F_KEY_CIRCLE, v->colorMenu ? "Done" : "Back" } };
c4fHints(c, C4F_T_RIGHT, 1022, hints, 2);
c4fText(c, C4F_FONT_LIGHT, 23, C4F_T_LEFT, 991, C4F_MUTED, v->colorMenu ?
"Up/Down: channel / Left/Right: adjust / OK: step size / Back: done" : "TV remote: OK select / Back return");
const int selected = v->selected >= 0 && v->selected < C4F_TESTER_USERS;
const int state = selected ? v->user[v->selected].state : -1;
const char *name = selected ? v->user[v->selected].name : "No one signed in", *status;
char detail[96];
if (state == C4F_TESTER_READY) status = "Controller connected";
else if (state == C4F_TESTER_NO_CONTROLLER) status = "No controller connected";
else if (state == C4F_TESTER_SIGNED_OUT) status = "Signed out";
else if (state == C4F_TESTER_FAILED) {
snprintf(detail, sizeof(detail), "Cannot read (0x%08x)", (unsigned)v->user[v->selected].error);
status = detail;
} else status = selected ? "Opening controller..." : "Waiting for a signed-in user";
c4fText(c, C4F_FONT_LIGHT, 20, C4F_T_LEFT, 257, C4F_FAINT, "SELECTED USER");
c4fText(c, C4F_FONT_REGULAR, 30, C4F_T_LEFT, 300, C4F_TEXT, name);
c4fText(c, C4F_FONT_LIGHT, 20, 620, 257, C4F_FAINT, "CONTROLLER");
c4fDot(c, 629, 290, c4fStateColor(state), 0);
c4fText(c, C4F_FONT_REGULAR, 26, 650, 300, state == C4F_TESTER_FAILED ? C4F_BAD : C4F_MUTED, status);
c4fText(c, C4F_FONT_LIGHT, 20, 1240, 257, C4F_FAINT, "TEST OUTPUT");
if (v->lightSet) {
const C4fColor light = { v->light[0], v->light[1], v->light[2], 255 };
c4fFillRoundRect(c, 1240, 278, 28, 18, 4, light);
c4fText(c, C4F_FONT_LIGHT, 24, 1281, 294, C4F_MUTED, "Light bar set by this test");
} else c4fText(c, C4F_FONT_LIGHT, 24, 1240, 294, C4F_MUTED, "Light bar: PS4 default");
if (v->rumble) c4fText(c, C4F_FONT_REGULAR, 22, 1240, 322, C4F_WARN,
v->rumble == 1 ? "Large motor running" : "Small motor running");
c4fDrawConnection(c, v, connection, C4F_T_LEFT, 329);
const int focus = v->focus >= 0 && v->focus < C4F_TESTER_ITEMS ? v->focus : C4F_TESTER_RESUME;
const int available = c4fTesterItemAvailable(v, focus);
const char *note = v->note[0] ? v->note : v->colorMenu ? "The light bar returns to its PS4 color when you leave the tester." :
available ? "Choose a tool, or resume testing to see live input." :
focus == C4F_TESTER_USER ? "Sign in another PS4 user to switch players here." :
"Connect this user's controller to use this tool.";
c4fTextWrapped(c, C4F_FONT_LIGHT, 24, C4F_T_LEFT + 26, 898, C4F_T_RIGHT - C4F_T_LEFT - 52, 31, 2,
v->note[0] ? C4F_TEXT : available ? C4F_MUTED : C4F_WARN, note);
}
static C4fSprite c4fColorRows[3][2], c4fColorPreview;
static void c4fRememberPickerPanel(C4fCanvas *c, C4fSprite *sprite, int x, int y, int w, int h)
{
if (!c4fToolsBase) return;
sprite->px = malloc((size_t)w * h * sizeof(uint32_t));
if (!sprite->px) return;
sprite->x = x; sprite->y = y; sprite->w = w; sprite->h = h;
for (int row = 0; row < h; row++)
memcpy(sprite->px + row * w, c->px + (y + row) * c->w + x, (size_t)w * sizeof(uint32_t));
}
/* Fill the opaque interior in straight spans; only rounded corners and the
* thin edge need distance coverage. The color changes, the geometry does not. */
static void c4fPickerSwatch(C4fCanvas *c, C4fColor color)
{
enum { X = 1324, Y = 430, W = 412, H = 210, R = 9 };
c4fFillRect(c, X + R, Y, W - 2 * R, H, color);
c4fFillRect(c, X, Y + R, R, H - 2 * R, color);
c4fFillRect(c, X + W - R, Y + R, R, H - 2 * R, color);
c4fFillCircle(c, X + R, Y + R, R, color);
c4fFillCircle(c, X + W - R, Y + R, R, color);
c4fFillCircle(c, X + R, Y + H - R, R, color);
c4fFillCircle(c, X + W - R, Y + H - R, R, color);
for (int y = 0; y < H; y++) {
for (int x = 0; x < W; x++) {
if (y >= R + 1 && y < H - R - 1 && x >= 2 && x < W - 2) continue;
float d = c4fRoundBoxDistance(X + x + .5f, Y + y + .5f, X + W / 2.0f, Y + H / 2.0f, W / 2.0f, H / 2.0f, R);
float outer = fmaxf(0, fminf(1, .5f - d)), inner = fmaxf(0, fminf(1, -.5f - d));
if (outer > inner) c4fBlend(c, X + x, Y + y, C4F_LINE, outer - inner);
}
}
}
static void c4fDrawColorPicker(C4fCanvas *c, const C4fTesterView *v)
{
static const char *const channels[] = { "Red", "Green", "Blue" };
static const C4fColor ink[] = { C4F_RGBA(255, 103, 120, 1), C4F_RGBA(69, 224, 138, 1), C4F_RGBA(134, 182, 255, 1) };
const int trackX = 195, trackW = 885;
for (int channel = 0; channel < 3; channel++) {
const int y = 360 + channel * 156, focused = channel == v->colorChannel;
C4fSprite *row = &c4fColorRows[channel][focused];
if (row->px) c4fCopyOpaque(c, row);
else {
c4fFillRoundRect(c, 150, y, 1110, 136, 10, focused ? C4F_RGBA(18, 58, 123, .96f) : C4F_RGBA(7, 29, 72, .82f));
c4fInsetRoundRect(c, 150, y, 1110, 136, 10, focused ? 3 : 1, focused ? C4F_TEXT : C4F_LINE);
c4fText(c, C4F_FONT_REGULAR, 28, 184, y + 43, ink[channel], channels[channel]);
c4fRememberPickerPanel(c, row, 150, y, 1110, 136);
}
char value[16];
snprintf(value, sizeof(value), "%u", v->light[channel]);
c4fTextRight(c, C4F_FONT_REGULAR, 46, 1220, y + 88, C4F_TEXT, value);
for (int x = 0; x < trackW; x++) {
uint8_t rgb[3];
memcpy(rgb, v->light, sizeof(rgb));
rgb[channel] = (uint8_t)(x * 255 / (trackW - 1));
c4fFillRect(c, trackX + x, y + 75, 1, 22, (C4fColor){ rgb[0], rgb[1], rgb[2], 255 });
}
c4fInsetRoundRect(c, trackX, y + 75, trackW, 22, 0, 1, C4F_LINE);
const float knobX = trackX + v->light[channel] * (trackW - 1) / 255.0f;
c4fFillCircle(c, knobX, y + 86, focused ? 14 : 11, C4F_TEXT);
c4fFillCircle(c, knobX, y + 86, focused ? 9 : 7, ink[channel]);
}
if (c4fColorPreview.px) c4fCopyOpaque(c, &c4fColorPreview);
else {
c4fFillRoundRect(c, 1290, 360, 480, 448, 10, C4F_RGBA(3, 16, 44, .72f));
c4fText(c, C4F_FONT_LIGHT, 21, 1324, 404, C4F_MUTED, "COLOR PREVIEW");
c4fText(c, C4F_FONT_LIGHT, 22, 1324, 785, C4F_MUTED, "Cross / OK switches fine and fast");
c4fRememberPickerPanel(c, &c4fColorPreview, 1290, 360, 480, 448);
}
c4fPickerSwatch(c, (C4fColor){ v->light[0], v->light[1], v->light[2], 255 });
char value[32];
snprintf(value, sizeof(value), "#%02X%02X%02X", v->light[0], v->light[1], v->light[2]);
c4fText(c, C4F_FONT_REGULAR, 38, 1324, 701, C4F_TEXT, value);
snprintf(value, sizeof(value), "Step: %d", v->colorFine ? 1 : 16);
c4fText(c, C4F_FONT_REGULAR, 28, 1324, 753, C4F_TEXT, value);
}
static void c4fDrawMenu(C4fCanvas *c, const C4fTesterView *v, const C4fTesterConnection *connection)
{
const size_t bytes = (size_t)c->w * c->h * sizeof(uint32_t);
if (!c4fBuildTools(c)) {
/* Keep the tools usable if caching could not allocate a frame. */
if (c4fToolsBase) memcpy(c->px, c4fToolsBase, bytes);
else for (int i = 0; i < c->w * c->h; i++) c->px[i] = 0xff061b42;
if (v->colorMenu) c4fDrawColorPicker(c, v);
else for (int i = 0; i < C4F_TESTER_ITEMS; i++) c4fPaintToolTile(c, i, i == v->focus, c4fTesterItemAvailable(v, i));
c4fDrawToolsDetails(c, v, connection);
return;
}
C4fTesterView key = { 0 };
key.focus = v->focus; key.selected = v->selected; key.rumble = v->rumble; key.lightSet = v->lightSet;
key.colorMenu = v->colorMenu; key.colorChannel = v->colorChannel; key.colorFine = v->colorFine;
memcpy(key.light, v->light, sizeof(key.light));
memcpy(key.note, v->note, sizeof(key.note));
C4fTesterConnection connectionKey = { 0 };
connectionKey.available = connection->available; connectionKey.pads = connection->pads;
memcpy(connectionKey.rttMs, connection->rttMs, sizeof(connectionKey.rttMs));
unsigned available = 0;
for (int i = 0; i < C4F_TESTER_USERS; i++) {
key.user[i].used = v->user[i].used; key.user[i].state = v->user[i].state; key.user[i].error = v->user[i].error;
memcpy(key.user[i].name, v->user[i].name, sizeof(key.user[i].name));
}
for (int i = 0; i < C4F_TESTER_ITEMS; i++) if (c4fTesterItemAvailable(v, i)) available |= 1U << i;
if (!c4fHasToolsKey || c4fToolsAvailable != available || memcmp(&key, &c4fToolsKey, sizeof(key)) ||
memcmp(&connectionKey, &c4fToolsConnection, sizeof(connectionKey))) {
C4fCanvas frame = { c4fToolsFrame, c->w, c->h };
memcpy(frame.px, c4fToolsBase, bytes);
if (v->colorMenu) c4fDrawColorPicker(&frame, v);
else for (int i = 0; i < C4F_TESTER_ITEMS; i++) {
const int on = i == v->focus, enabled = !!(available & (1U << i)), variant = on + 2 * !enabled;
C4fSprite *tile = &c4fToolTiles[i][variant];
if (!tile->px) {
const C4fBox b = c4fToolBox(i);
tile->x = (int)b.x; tile->y = (int)b.y; tile->w = C4F_TOOL_W; tile->h = C4F_TOOL_H;
c4fPaintToolTile(&frame, i, on, enabled);
tile->px = malloc((size_t)tile->w * tile->h * sizeof(uint32_t));
if (tile->px) for (int y = 0; y < tile->h; y++)
memcpy(tile->px + y * tile->w, frame.px + (tile->y + y) * frame.w + tile->x,
(size_t)tile->w * sizeof(uint32_t));
} else c4fCopyOpaque(&frame, tile);
}
c4fDrawToolsDetails(&frame, v, connection);
c4fToolsKey = key; c4fToolsAvailable = available; c4fHasToolsKey = 1;
c4fToolsConnection = connectionKey;
}
memcpy(c->px, c4fToolsFrame, bytes);
}
void c4fDrawTester(uint32_t *pixels, const C4fLauncherScreen *s)
@@ -920,14 +1366,16 @@ void c4fDrawTester(uint32_t *pixels, const C4fLauncherScreen *s)
const C4fTesterView *v = &s->tester;
C4fCanvas c = { pixels, C4F_SCREEN_WIDTH, C4F_SCREEN_HEIGHT };
const size_t bytes = (size_t)c.w * (size_t)c.h * sizeof(uint32_t);
if (v->menu) { c4fDrawMenu(&c, v, &s->connection); return; }
const int state = v->selected >= 0 ? v->user[v->selected].state : -1, ready = state == C4F_TESTER_READY;
const C4fColor accent = v->lightSet ? (C4fColor){ v->light[0], v->light[1], v->light[2], 255 } : C4F_ACCENT;
if (!c4fLayer) c4fBuildLayer();
c4fPrepareSprites(&c, v, accent, ready);
if (c4fLayer) memcpy(pixels, c4fLayer, bytes);
else memset(pixels, 0, bytes);
c4fDrawTabs(&c, v);
const int state = v->selected >= 0 ? v->user[v->selected].state : -1, ready = state == C4F_TESTER_READY;
/* The page lights the controller in its colour; here, the colour this test set. */
const C4fColor accent = v->lightSet ? (C4fColor){ v->light[0], v->light[1], v->light[2], 255 } : C4F_ACCENT;
char line[96];
const char *name = v->selected >= 0 ? v->user[v->selected].name : "", *detail;
if (ready) {
@@ -946,11 +1394,11 @@ void c4fDrawTester(uint32_t *pixels, const C4fLauncherScreen *s)
}
c4fDrawTouchpad(&c, accent, name, detail);
c4fDrawValues(&c, v, ready);
c4fDrawConnection(&c, v, &s->connection, C4F_P_X + 4, 853);
if (v->rumble)
c4fText(&c, C4F_FONT_REGULAR, 26, C4F_P_X + 4, C4F_T_TOP + C4F_T_HEIGHT - 30, C4F_WARN,
v->rumble == 1 ? "Rumbling: large motor" : "Rumbling: small motor");
if (v->note[0]) c4fText(&c, C4F_FONT_LIGHT, 24, C4F_T_LEFT, 972, C4F_TEXT, v->note);
else c4fText(&c, C4F_FONT_LIGHT, 24, C4F_T_LEFT, 972, C4F_MUTED,
"Hold L3 + R3 for two seconds for the menu: change user, rumble, light bar, leave.");
if (v->menu) c4fDrawMenu(&c, v);
}
+118 -14
View File
@@ -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;
+631
View File
@@ -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));
}
+742
View File
@@ -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);
}
+157
View File
@@ -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;
}
+264 -72
View File
@@ -1,6 +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), m (motion),
* 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>
@@ -35,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;
@@ -49,7 +55,7 @@ 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. */
@@ -60,8 +66,23 @@ typedef struct {
* 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
@@ -79,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 */
@@ -103,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;
@@ -148,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;
@@ -160,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
@@ -194,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"
@@ -224,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
@@ -238,10 +266,20 @@ 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);
}
@@ -265,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);
@@ -418,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;
}
}
}
@@ -575,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);
}
@@ -597,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;
@@ -692,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));
@@ -721,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;
@@ -768,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)];
@@ -805,58 +848,158 @@ 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];
}
c4fApplyMotion(p, &data);
if (p->stale) { p->stale = 0; app->changed = 1; }
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 (c4fSince(now, p->lastReport) >= C4F_REPORT_FAST_MS) c4fReportPad(app, p, (int)r->args[0], now);
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
/* m: [pad, 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. The buttons, sticks and touches stay as
* the last u left them. */
static void c4fMotion(C4fWeb *app, C4fNetClient *c, C4fRequest *r)
/* 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)
{
static const int64_t limits[3] = { 10000, 16000, 35000 };
if (r->argc != 11 || r->args[0] < 0 || r->args[0] >= C4F_MAX_PADS) { c4fError(c, r, 400, "That motion was not in a form Control4Free understands"); return; }
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;
for (unsigned i = 1; i < 11; i++) {
int64_t limit = limits[i < 5 ? 0 : i < 8 ? 1 : 2];
if (r->args[i] < -limit || r->args[i] > limit) { c4fError(c, r, 400, "Motion was outside its range"); 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; }
}
for (int i = 0; i < 4; i++) p->quat[i] = (float)r->args[1+i] / 10000.0f;
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();
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[5+i] / 1000.0f;
p->gyro[i] = (float)r->args[8+i] / 1000.0f;
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;
if (p->stale) { p->stale = 0; app->changed = 1; }
uint64_t now = c4fTimeMs();
p->lastInput = now;
/* 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);
@@ -864,14 +1007,27 @@ static void c4fMotion(C4fWeb *app, C4fNetClient *c, C4fRequest *r)
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) {
@@ -881,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) {
@@ -893,18 +1057,27 @@ 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,\"motion\":true}}", (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; }
@@ -928,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);
@@ -967,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
@@ -982,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");
@@ -995,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
@@ -1021,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);
@@ -1041,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");
+103 -16
View File
@@ -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');
+100 -11
View File
@@ -1,15 +1,16 @@
// Settings → Experimental in a real browser, against the real service with the PS4
// stubbed (build/web-host-test). WebHID devices, gamepads and motion sensors are
// 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 + ' })();');
fs.copyFileSync('/src/build/web-host-test', '/tmp/c4f-experimental-service');
copyService('/tmp/c4f-experimental-service');
// An independent CRC-32 (zlib's), to check the Bluetooth reports' last four bytes.
function crc32(bytes) {
@@ -23,14 +24,9 @@ function crc32(bytes) {
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 socket's messages and the page's sensor listeners.
// record of the input messages and the page's sensor listeners.
function fakes({ insecure = false } = {}) {
window.sentMessages = [];
const socketSend = WebSocket.prototype.send;
WebSocket.prototype.send = function(data) {
try { sentMessages.push(JSON.parse(data)); } catch {}
return socketSend.call(this, data);
};
Object.defineProperty(window, 'sentMessages', { get: () => controllerMessages });
window.motionListeners = 0;
const addListener = window.addEventListener;
window.addEventListener = function(type, ...rest) {
@@ -121,9 +117,11 @@ 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) {
@@ -138,6 +136,39 @@ async function open(browser, options) {
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 {
@@ -190,6 +221,15 @@ const lastReport = (page, device) => page.evaluate(d => fakeHid[d].sent.at(-1),
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; });
@@ -201,8 +241,20 @@ const lastReport = (page, device) => page.evaluate(d => fakeHid[d].sent.at(-1),
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);
@@ -217,6 +269,7 @@ const lastReport = (page, device) => page.evaluate(d => fakeHid[d].sent.at(-1),
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
@@ -234,6 +287,7 @@ const lastReport = (page, device) => page.evaluate(d => fakeHid[d].sent.at(-1),
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);
@@ -256,7 +310,8 @@ const lastReport = (page, device) => page.evaluate(d => fakeHid[d].sent.at(-1),
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[32] = 0x80; bytes[36] = 0x80;
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);
});
@@ -329,10 +384,11 @@ const lastReport = (page, device) => page.evaluate(d => fakeHid[d].sent.at(-1),
}
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; bytes[10] = b2;
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);
@@ -418,6 +474,39 @@ const lastReport = (page, device) => page.evaluate(d => fakeHid[d].sent.at(-1),
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();
+4 -4
View File
@@ -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.'); };
}
+30 -24
View File
@@ -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 -4
View File
@@ -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
View File
@@ -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 = [];
+9
View File
@@ -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);
+310
View File
@@ -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");
}
+328 -53
View File
@@ -15,7 +15,7 @@
typedef struct {
int32_t user;
int handle, owned, openError, readError, connected;
int opens, closes, reads, motionOn, lightResets, orientationResets, vibrations;
int opens, closes, reads, motionOn, lightResets, orientationResets, vibrations, lightCalls, lightError;
int vibrating, large, small, lit;
uint8_t rgb[3];
C4fTesterSample sample;
@@ -96,6 +96,8 @@ static int fakeVibrate(void *context, int handle, uint8_t large, uint8_t small)
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;
@@ -151,6 +153,13 @@ static int frame(Fake *f, C4fTester *t, uint64_t now, uint32_t held, uint32_t re
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 };
@@ -182,6 +191,8 @@ static void readsUsers(void)
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);
@@ -189,6 +200,10 @@ static void readsUsers(void)
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);
@@ -197,10 +212,21 @@ static void readsUsers(void)
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");
}
@@ -225,59 +251,48 @@ static void menuActions(void)
static C4fTester t;
setup(&f, &t);
uint64_t now = openMenu(&f, &t);
/* Presses while the opening keys are still down count for nothing. */
frame(&f, &t, now + 10, STICKS | C4F_INPUT_BUTTON_DOWN, 0);
frame(&f, &t, now + 20, STICKS | C4F_INPUT_BUTTON_DOWN | C4F_INPUT_BUTTON_CROSS, 0);
assert(t.menu && !t.armed && t.focus == C4F_TESTER_RESUME);
frame(&f, &t, now + 30, 0, 0);
frame(&f, &t, now += 10, 0, 0);
assert(t.armed);
frame(&f, &t, now + 40, C4F_INPUT_BUTTON_DOWN, 0);
frame(&f, &t, now + 50, 0, 0);
frame(&f, &t, now + 60, C4F_INPUT_BUTTON_DOWN, 0);
frame(&f, &t, now + 70, 0, 0);
tap(&f, &t, &now, C4F_INPUT_BUTTON_DOWN);
assert(t.focus == C4F_TESTER_RUMBLE_LARGE);
frame(&f, &t, now + 80, C4F_INPUT_BUTTON_CROSS, 0);
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"));
frame(&f, &t, now + 1079, 0, 0);
now = t.rumbleUntil - 1;
frame(&f, &t, now, 0, 0);
assert(f.pads[0].vibrating);
frame(&f, &t, now + 1080, 0, 0);
frame(&f, &t, ++now, 0, 0);
assert(!f.pads[0].vibrating && !t.rumble);
/* Small motor, then the light bar and orientation. */
frame(&f, &t, now + 1100, C4F_INPUT_BUTTON_DOWN, 0);
frame(&f, &t, now + 1105, 0, 0);
/* A move and a select arriving together only move. */
frame(&f, &t, now + 1110, C4F_INPUT_BUTTON_DOWN | C4F_INPUT_BUTTON_CROSS, 0);
assert(t.focus == C4F_TESTER_LIGHT_RED && !f.pads[0].vibrating && !f.pads[0].lit);
frame(&f, &t, now + 1115, 0, 0);
frame(&f, &t, now + 1120, C4F_INPUT_BUTTON_UP, 0);
frame(&f, &t, now + 1125, 0, 0);
tap(&f, &t, &now, C4F_INPUT_BUTTON_RIGHT);
assert(t.focus == C4F_TESTER_RUMBLE_SMALL);
frame(&f, &t, now + 1130, C4F_INPUT_BUTTON_CROSS, 0);
assert(f.pads[0].vibrating && f.pads[0].small == 255 && f.pads[0].large == 0);
frame(&f, &t, now + 1140, C4F_INPUT_BUTTON_DOWN, 0);
frame(&f, &t, now + 1150, 0, 0);
frame(&f, &t, now + 1160, C4F_INPUT_BUTTON_CROSS, 0);
assert(f.pads[0].lit && f.pads[0].rgb[0] == 255 && f.pads[0].rgb[1] == 0);
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);
for (int i = 0; i < 4; i++) {
frame(&f, &t, now + 1170 + i * 20, C4F_INPUT_BUTTON_DOWN, 0);
frame(&f, &t, now + 1180 + i * 20, 0, 0);
}
tap(&f, &t, &now, C4F_INPUT_BUTTON_CIRCLE);
tap(&f, &t, &now, C4F_INPUT_BUTTON_LEFT);
assert(t.focus == C4F_TESTER_ORIENTATION);
frame(&f, &t, now + 1300, C4F_INPUT_BUTTON_CROSS, 0);
tap(&f, &t, &now, C4F_INPUT_BUTTON_CROSS);
assert(f.pads[0].orientationResets == 1);
/* Back to the launcher leaves and cleans up. */
frame(&f, &t, now + 1310, 0, 0);
frame(&f, &t, now + 1320, C4F_INPUT_BUTTON_DOWN, 0);
frame(&f, &t, now + 1330, 0, 0);
tap(&f, &t, &now, C4F_INPUT_BUTTON_RIGHT);
tap(&f, &t, &now, C4F_INPUT_BUTTON_DOWN);
assert(t.focus == C4F_TESTER_EXIT);
assert(!frame(&f, &t, now + 1340, C4F_INPUT_BUTTON_CROSS, 0));
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);
@@ -290,36 +305,287 @@ static void switchUsers(void)
static C4fTester t;
setup(&f, &t);
uint64_t now = openMenu(&f, &t);
frame(&f, &t, now + 10, 0, 0);
frame(&f, &t, now + 20, C4F_INPUT_BUTTON_DOWN, 0);
frame(&f, &t, now + 30, 0, 0);
frame(&f, &t, now += 10, 0, 0);
tap(&f, &t, &now, C4F_INPUT_BUTTON_RIGHT);
assert(t.focus == C4F_TESTER_USER);
frame(&f, &t, now + 40, C4F_INPUT_BUTTON_RIGHT, 0);
tap(&f, &t, &now, C4F_INPUT_BUTTON_CROSS);
assert(t.selected == 1);
frame(&f, &t, now + 50, 0, 0);
frame(&f, &t, now + 60, C4F_INPUT_BUTTON_RIGHT, 0);
tap(&f, &t, &now, C4F_INPUT_BUTTON_CROSS);
assert(t.selected == 0);
frame(&f, &t, now + 70, 0, 0);
/* The second user's own controller drives the menu as well. */
f.pads[1].sample.buttons = C4F_INPUT_BUTTON_LEFT;
frame(&f, &t, now + 80, 0, 0);
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 + 90, 0, 0);
frame(&f, &t, now += 10, 0, 0);
/* Rumble goes to the selected user's controller. */
frame(&f, &t, now + 100, C4F_INPUT_BUTTON_DOWN, 0);
frame(&f, &t, now + 110, 0, 0);
frame(&f, &t, now + 120, C4F_INPUT_BUTTON_CROSS, 0);
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. */
frame(&f, &t, now + 130, 0, 0);
frame(&f, &t, now + 140, C4F_INPUT_BUTTON_CIRCLE, 0);
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;
@@ -384,15 +650,20 @@ static void logout(void)
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);
@@ -413,7 +684,7 @@ static void remoteAndSuspend(void)
assert(t.menu);
frame(&f, &t, 20, 0, 0);
assert(t.armed);
frame(&f, &t, 30, 0, C4F_INPUT_BUTTON_DOWN);
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);
@@ -425,7 +696,7 @@ static void remoteAndSuspend(void)
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_BLUE;
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);
@@ -476,6 +747,10 @@ int main(void)
readsUsers();
menuActions();
switchUsers();
menuOpeningSafety();
gridNavigationAndRepeat();
colorPicker();
menuAvailability();
failures();
logout();
remoteAndSuspend();
+20
View File
@@ -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 } };
+30
View File
@@ -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)
+53
View File
@@ -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 };
+101
View File
@@ -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); });
+199
View File
@@ -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; });
+116
View File
@@ -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;
}
+433
View File
@@ -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;
}
+1 -1
View File
@@ -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():
+40 -1
View File
@@ -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):
@@ -123,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 = []
@@ -191,10 +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',
'tester', 'tester-motion', 'tester-menu', 'tester-remote', 'tester-disconnected', 'tester-error'):
'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
+30
View File
@@ -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()
+6 -3
View File
@@ -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)
+90
View File
@@ -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()
+92
View File
@@ -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()
+126 -7
View File
@@ -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')
@@ -441,7 +557,7 @@ def main():
try:
c = server.c
info = c.request('info')['result']
assert info['motion'] is True and info['protocol'] == 2, info
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)
@@ -470,12 +586,15 @@ def main():
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 counts as input and wakes it.
# 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()
+239
View File
@@ -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;
}
+52 -2
View File
@@ -57,6 +57,10 @@ static int c4fTesterPreview(const char *state, C4fLauncherScreen *s)
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;
@@ -72,18 +76,64 @@ static int c4fTesterPreview(const char *state, C4fLauncherScreen *s)
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")) {
} 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 {
} 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;
}
+1
View File
@@ -140,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()
+553
View File
@@ -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.
+4
View File
@@ -0,0 +1,4 @@
Makefile
*.sln
*.vcxproj
mbedtls/check_config
+631
View File
@@ -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 */
+642
View File
@@ -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 */
+390
View File
@@ -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 */
File diff suppressed because it is too large. Load diff
+194
View File
@@ -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 */
+209
View File
@@ -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 */
+351
View File
@@ -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 */
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+246
View File
@@ -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 */
+91
View File
@@ -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 */
+35
View File
@@ -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 */
+36
View File
@@ -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 */
+596
View File
@@ -0,0 +1,596 @@
/**
* \file ctr_drbg.h
*
* \brief This file contains definitions and functions for the
* CTR_DRBG pseudorandom generator.
*
* CTR_DRBG is a standardized way of building a PRNG from a block-cipher
* in counter mode operation, as defined in <em>NIST SP 800-90A:
* Recommendation for Random Number Generation Using Deterministic Random
* Bit Generators</em>.
*
* The Mbed TLS implementation of CTR_DRBG uses AES-256 (default) or AES-128
* (if \c MBEDTLS_CTR_DRBG_USE_128_BIT_KEY is enabled at compile time)
* as the underlying block cipher, with a derivation function.
*
* The security strength as defined in NIST SP 800-90A is
* 128 bits when AES-128 is used (\c MBEDTLS_CTR_DRBG_USE_128_BIT_KEY enabled)
* and 256 bits otherwise, provided that #MBEDTLS_CTR_DRBG_ENTROPY_LEN is
* kept at its default value (and not overridden in mbedtls_config.h) and that the
* DRBG instance is set up with default parameters.
* See the documentation of mbedtls_ctr_drbg_seed() for more
* information.
*/
/*
* Copyright The Mbed TLS Contributors
* SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
*/
#ifndef MBEDTLS_CTR_DRBG_H
#define MBEDTLS_CTR_DRBG_H
#include "mbedtls/private_access.h"
#include "mbedtls/build_info.h"
/* The CTR_DRBG implementation can either directly call the low-level AES
* module (gated by MBEDTLS_AES_C) or call the PSA API to perform AES
* operations. Calling the AES module directly is the default, both for
* maximum backward compatibility and because it's a bit more efficient
* (less glue code).
*
* When MBEDTLS_AES_C is disabled, the CTR_DRBG module calls PSA crypto and
* thus benefits from the PSA AES accelerator driver.
* It is technically possible to enable MBEDTLS_CTR_DRBG_USE_PSA_CRYPTO
* to use PSA even when MBEDTLS_AES_C is enabled, but there is very little
* reason to do so other than testing purposes and this is not officially
* supported.
*/
#if !defined(MBEDTLS_AES_C)
#define MBEDTLS_CTR_DRBG_USE_PSA_CRYPTO
#endif
#if defined(MBEDTLS_CTR_DRBG_USE_PSA_CRYPTO)
#include "psa/crypto.h"
#else
#include "mbedtls/aes.h"
#endif
#include "entropy.h"
#if defined(MBEDTLS_THREADING_C)
#include "mbedtls/threading.h"
#endif
/** The entropy source failed. */
#define MBEDTLS_ERR_CTR_DRBG_ENTROPY_SOURCE_FAILED -0x0034
/** The requested random buffer length is too big. */
#define MBEDTLS_ERR_CTR_DRBG_REQUEST_TOO_BIG -0x0036
/** The input (entropy + additional data) is too large. */
#define MBEDTLS_ERR_CTR_DRBG_INPUT_TOO_BIG -0x0038
/** Read or write error in file. */
#define MBEDTLS_ERR_CTR_DRBG_FILE_IO_ERROR -0x003A
#define MBEDTLS_CTR_DRBG_BLOCKSIZE 16 /**< The block size used by the cipher. */
#if defined(MBEDTLS_CTR_DRBG_USE_128_BIT_KEY)
#define MBEDTLS_CTR_DRBG_KEYSIZE 16
/**< The key size in bytes used by the cipher.
*
* Compile-time choice: 16 bytes (128 bits)
* because #MBEDTLS_CTR_DRBG_USE_128_BIT_KEY is enabled.
*/
#else
#define MBEDTLS_CTR_DRBG_KEYSIZE 32
/**< The key size in bytes used by the cipher.
*
* Compile-time choice: 32 bytes (256 bits)
* because \c MBEDTLS_CTR_DRBG_USE_128_BIT_KEY is disabled.
*/
#endif
#define MBEDTLS_CTR_DRBG_KEYBITS (MBEDTLS_CTR_DRBG_KEYSIZE * 8) /**< The key size for the DRBG operation, in bits. */
#define MBEDTLS_CTR_DRBG_SEEDLEN (MBEDTLS_CTR_DRBG_KEYSIZE + MBEDTLS_CTR_DRBG_BLOCKSIZE) /**< The seed length, calculated as (counter + AES key). */
/**
* \name SECTION: Module settings
*
* The configuration options you can set for this module are in this section.
* Either change them in mbedtls_config.h or define them using the compiler command
* line.
* \{
*/
/** \def MBEDTLS_CTR_DRBG_ENTROPY_LEN
*
* \brief The amount of entropy used per seed by default, in bytes.
*/
#if !defined(MBEDTLS_CTR_DRBG_ENTROPY_LEN)
#if defined(MBEDTLS_ENTROPY_SHA512_ACCUMULATOR)
/** This is 48 bytes because the entropy module uses SHA-512.
*/
#define MBEDTLS_CTR_DRBG_ENTROPY_LEN 48
#else /* MBEDTLS_ENTROPY_SHA512_ACCUMULATOR */
/** This is 32 bytes because the entropy module uses SHA-256.
*/
#if !defined(MBEDTLS_CTR_DRBG_USE_128_BIT_KEY)
/** \warning To achieve a 256-bit security strength, you must pass a nonce
* to mbedtls_ctr_drbg_seed().
*/
#endif /* !defined(MBEDTLS_CTR_DRBG_USE_128_BIT_KEY) */
#define MBEDTLS_CTR_DRBG_ENTROPY_LEN 32
#endif /* MBEDTLS_ENTROPY_SHA512_ACCUMULATOR */
#endif /* !defined(MBEDTLS_CTR_DRBG_ENTROPY_LEN) */
#if !defined(MBEDTLS_CTR_DRBG_RESEED_INTERVAL)
#define MBEDTLS_CTR_DRBG_RESEED_INTERVAL 10000
/**< The interval before reseed is performed by default. */
#endif
#if !defined(MBEDTLS_CTR_DRBG_MAX_INPUT)
#define MBEDTLS_CTR_DRBG_MAX_INPUT 256
/**< The maximum number of additional input Bytes. */
#endif
#if !defined(MBEDTLS_CTR_DRBG_MAX_REQUEST)
#define MBEDTLS_CTR_DRBG_MAX_REQUEST 1024
/**< The maximum number of requested Bytes per call. */
#endif
#if !defined(MBEDTLS_CTR_DRBG_MAX_SEED_INPUT)
#define MBEDTLS_CTR_DRBG_MAX_SEED_INPUT 384
/**< The maximum size of seed or reseed buffer in bytes. */
#endif
/** \} name SECTION: Module settings */
#define MBEDTLS_CTR_DRBG_PR_OFF 0
/**< Prediction resistance is disabled. */
#define MBEDTLS_CTR_DRBG_PR_ON 1
/**< Prediction resistance is enabled. */
#ifdef __cplusplus
extern "C" {
#endif
#if MBEDTLS_CTR_DRBG_ENTROPY_LEN >= MBEDTLS_CTR_DRBG_KEYSIZE * 3 / 2
/** The default length of the nonce read from the entropy source.
*
* This is \c 0 because a single read from the entropy source is sufficient
* to include a nonce.
* See the documentation of mbedtls_ctr_drbg_seed() for more information.
*/
#define MBEDTLS_CTR_DRBG_ENTROPY_NONCE_LEN 0
#else
/** The default length of the nonce read from the entropy source.
*
* This is half of the default entropy length because a single read from
* the entropy source does not provide enough material to form a nonce.
* See the documentation of mbedtls_ctr_drbg_seed() for more information.
*/
#define MBEDTLS_CTR_DRBG_ENTROPY_NONCE_LEN (MBEDTLS_CTR_DRBG_ENTROPY_LEN + 1) / 2
#endif
#if defined(MBEDTLS_CTR_DRBG_USE_PSA_CRYPTO)
typedef struct mbedtls_ctr_drbg_psa_context {
mbedtls_svc_key_id_t key_id;
psa_cipher_operation_t operation;
} mbedtls_ctr_drbg_psa_context;
#endif
/**
* \brief The CTR_DRBG context structure.
*/
typedef struct mbedtls_ctr_drbg_context {
unsigned char MBEDTLS_PRIVATE(counter)[16]; /*!< The counter (V). */
int MBEDTLS_PRIVATE(reseed_counter); /*!< The reseed counter.
* This is the number of requests that have
* been made since the last (re)seeding.
* Before the initial seeding, this field
* contains the amount of entropy in bytes
* to use as a nonce for the initial seeding,
* or -1 if no nonce length has been explicitly
* set (see mbedtls_ctr_drbg_set_nonce_len()).
*/
int MBEDTLS_PRIVATE(prediction_resistance); /*!< This determines whether prediction
resistance is enabled, that is
whether to systematically reseed before
each random generation. */
size_t MBEDTLS_PRIVATE(entropy_len); /*!< The amount of entropy grabbed on each
seed or reseed operation, in bytes. */
int MBEDTLS_PRIVATE(reseed_interval); /*!< The reseed interval.
* This is the maximum number of requests
* that can be made between reseedings. */
#if defined(MBEDTLS_CTR_DRBG_USE_PSA_CRYPTO)
mbedtls_ctr_drbg_psa_context MBEDTLS_PRIVATE(psa_ctx); /*!< The PSA context. */
#else
mbedtls_aes_context MBEDTLS_PRIVATE(aes_ctx); /*!< The AES context. */
#endif
/*
* Callbacks (Entropy)
*/
int(*MBEDTLS_PRIVATE(f_entropy))(void *, unsigned char *, size_t);
/*!< The entropy callback function. */
void *MBEDTLS_PRIVATE(p_entropy); /*!< The context for the entropy function. */
#if defined(MBEDTLS_THREADING_C)
/* Invariant: the mutex is initialized if and only if f_entropy != NULL.
* This means that the mutex is initialized during the initial seeding
* in mbedtls_ctr_drbg_seed() and freed in mbedtls_ctr_drbg_free().
*
* Note that this invariant may change without notice. Do not rely on it
* and do not access the mutex directly in application code.
*/
mbedtls_threading_mutex_t MBEDTLS_PRIVATE(mutex);
#endif
}
mbedtls_ctr_drbg_context;
/**
* \brief This function initializes the CTR_DRBG context,
* and prepares it for mbedtls_ctr_drbg_seed()
* or mbedtls_ctr_drbg_free().
*
* \note The reseed interval is
* #MBEDTLS_CTR_DRBG_RESEED_INTERVAL by default.
* You can override it by calling
* mbedtls_ctr_drbg_set_reseed_interval().
*
* \param ctx The CTR_DRBG context to initialize.
*/
void mbedtls_ctr_drbg_init(mbedtls_ctr_drbg_context *ctx);
/**
* \brief This function seeds and sets up the CTR_DRBG
* entropy source for future reseeds.
*
* A typical choice for the \p f_entropy and \p p_entropy parameters is
* to use the entropy module:
* - \p f_entropy is mbedtls_entropy_func();
* - \p p_entropy is an instance of ::mbedtls_entropy_context initialized
* with mbedtls_entropy_init() (which registers the platform's default
* entropy sources).
*
* The entropy length is #MBEDTLS_CTR_DRBG_ENTROPY_LEN by default.
* You can override it by calling mbedtls_ctr_drbg_set_entropy_len().
*
* The entropy nonce length is:
* - \c 0 if the entropy length is at least 3/2 times the entropy length,
* which guarantees that the security strength is the maximum permitted
* by the key size and entropy length according to NIST SP 800-90A §10.2.1;
* - Half the entropy length otherwise.
* You can override it by calling mbedtls_ctr_drbg_set_nonce_len().
* With the default entropy length, the entropy nonce length is
* #MBEDTLS_CTR_DRBG_ENTROPY_NONCE_LEN.
*
* You can provide a nonce and personalization string in addition to the
* entropy source, to make this instantiation as unique as possible.
* See SP 800-90A §8.6.7 for more details about nonces.
*
* The _seed_material_ value passed to the derivation function in
* the CTR_DRBG Instantiate Process described in NIST SP 800-90A §10.2.1.3.2
* is the concatenation of the following strings:
* - A string obtained by calling \p f_entropy function for the entropy
* length.
*/
#if MBEDTLS_CTR_DRBG_ENTROPY_NONCE_LEN == 0
/**
* - If mbedtls_ctr_drbg_set_nonce_len() has been called, a string
* obtained by calling \p f_entropy function for the specified length.
*/
#else
/**
* - A string obtained by calling \p f_entropy function for the entropy nonce
* length. If the entropy nonce length is \c 0, this function does not
* make a second call to \p f_entropy.
*/
#endif
#if defined(MBEDTLS_THREADING_C)
/**
* \note When Mbed TLS is built with threading support,
* after this function returns successfully,
* it is safe to call mbedtls_ctr_drbg_random()
* from multiple threads. Other operations, including
* reseeding, are not thread-safe.
*/
#endif /* MBEDTLS_THREADING_C */
/**
* - The \p custom string.
*
* \note To achieve the nominal security strength permitted
* by CTR_DRBG, the entropy length must be:
* - at least 16 bytes for a 128-bit strength
* (maximum achievable strength when using AES-128);
* - at least 32 bytes for a 256-bit strength
* (maximum achievable strength when using AES-256).
*
* In addition, if you do not pass a nonce in \p custom,
* the sum of the entropy length
* and the entropy nonce length must be:
* - at least 24 bytes for a 128-bit strength
* (maximum achievable strength when using AES-128);
* - at least 48 bytes for a 256-bit strength
* (maximum achievable strength when using AES-256).
*
* \param ctx The CTR_DRBG context to seed.
* It must have been initialized with
* mbedtls_ctr_drbg_init().
* After a successful call to mbedtls_ctr_drbg_seed(),
* you may not call mbedtls_ctr_drbg_seed() again on
* the same context unless you call
* mbedtls_ctr_drbg_free() and mbedtls_ctr_drbg_init()
* again first.
* After a failed call to mbedtls_ctr_drbg_seed(),
* you must call mbedtls_ctr_drbg_free().
* \param f_entropy The entropy callback, taking as arguments the
* \p p_entropy context, the buffer to fill, and the
* length of the buffer.
* \p f_entropy is always called with a buffer size
* less than or equal to the entropy length.
* \param p_entropy The entropy context to pass to \p f_entropy.
* \param custom The personalization string.
* This can be \c NULL, in which case the personalization
* string is empty regardless of the value of \p len.
* \param len The length of the personalization string.
* This must be at most
* #MBEDTLS_CTR_DRBG_MAX_SEED_INPUT
* - #MBEDTLS_CTR_DRBG_ENTROPY_LEN.
*
* \return \c 0 on success.
* \return #MBEDTLS_ERR_CTR_DRBG_ENTROPY_SOURCE_FAILED on failure.
*/
int mbedtls_ctr_drbg_seed(mbedtls_ctr_drbg_context *ctx,
int (*f_entropy)(void *, unsigned char *, size_t),
void *p_entropy,
const unsigned char *custom,
size_t len);
/**
* \brief This function resets CTR_DRBG context to the state immediately
* after initial call of mbedtls_ctr_drbg_init().
*
* \param ctx The CTR_DRBG context to clear.
*/
void mbedtls_ctr_drbg_free(mbedtls_ctr_drbg_context *ctx);
/**
* \brief This function turns prediction resistance on or off.
* The default value is off.
*
* \note If enabled, entropy is gathered at the beginning of
* every call to mbedtls_ctr_drbg_random_with_add()
* or mbedtls_ctr_drbg_random().
* Only use this if your entropy source has sufficient
* throughput.
*
* \param ctx The CTR_DRBG context.
* \param resistance #MBEDTLS_CTR_DRBG_PR_ON or #MBEDTLS_CTR_DRBG_PR_OFF.
*/
void mbedtls_ctr_drbg_set_prediction_resistance(mbedtls_ctr_drbg_context *ctx,
int resistance);
/**
* \brief This function sets the amount of entropy grabbed on each
* seed or reseed.
*
* The default value is #MBEDTLS_CTR_DRBG_ENTROPY_LEN.
*
* \note The security strength of CTR_DRBG is bounded by the
* entropy length. Thus:
* - When using AES-256
* (\c MBEDTLS_CTR_DRBG_USE_128_BIT_KEY is disabled,
* which is the default),
* \p len must be at least 32 (in bytes)
* to achieve a 256-bit strength.
* - When using AES-128
* (\c MBEDTLS_CTR_DRBG_USE_128_BIT_KEY is enabled)
* \p len must be at least 16 (in bytes)
* to achieve a 128-bit strength.
*
* \param ctx The CTR_DRBG context.
* \param len The amount of entropy to grab, in bytes.
* This must be at most #MBEDTLS_CTR_DRBG_MAX_SEED_INPUT
* and at most the maximum length accepted by the
* entropy function that is set in the context.
*/
void mbedtls_ctr_drbg_set_entropy_len(mbedtls_ctr_drbg_context *ctx,
size_t len);
/**
* \brief This function sets the amount of entropy grabbed
* as a nonce for the initial seeding.
*
* Call this function before calling mbedtls_ctr_drbg_seed() to read
* a nonce from the entropy source during the initial seeding.
*
* \param ctx The CTR_DRBG context.
* \param len The amount of entropy to grab for the nonce, in bytes.
* This must be at most #MBEDTLS_CTR_DRBG_MAX_SEED_INPUT
* and at most the maximum length accepted by the
* entropy function that is set in the context.
*
* \return \c 0 on success.
* \return #MBEDTLS_ERR_CTR_DRBG_INPUT_TOO_BIG if \p len is
* more than #MBEDTLS_CTR_DRBG_MAX_SEED_INPUT.
* \return #MBEDTLS_ERR_CTR_DRBG_ENTROPY_SOURCE_FAILED
* if the initial seeding has already taken place.
*/
int mbedtls_ctr_drbg_set_nonce_len(mbedtls_ctr_drbg_context *ctx,
size_t len);
/**
* \brief This function sets the reseed interval.
*
* The reseed interval is the number of calls to mbedtls_ctr_drbg_random()
* or mbedtls_ctr_drbg_random_with_add() after which the entropy function
* is called again.
*
* The default value is #MBEDTLS_CTR_DRBG_RESEED_INTERVAL.
*
* \param ctx The CTR_DRBG context.
* \param interval The reseed interval.
*/
void mbedtls_ctr_drbg_set_reseed_interval(mbedtls_ctr_drbg_context *ctx,
int interval);
/**
* \brief This function reseeds the CTR_DRBG context, that is
* extracts data from the entropy source.
*
* \note This function is not thread-safe. It is not safe
* to call this function if another thread might be
* concurrently obtaining random numbers from the same
* context or updating or reseeding the same context.
*
* \param ctx The CTR_DRBG context.
* \param additional Additional data to add to the state. Can be \c NULL.
* \param len The length of the additional data.
* This must be less than
* #MBEDTLS_CTR_DRBG_MAX_SEED_INPUT - \c entropy_len
* where \c entropy_len is the entropy length
* configured for the context.
*
* \return \c 0 on success.
* \return #MBEDTLS_ERR_CTR_DRBG_ENTROPY_SOURCE_FAILED on failure.
*/
int mbedtls_ctr_drbg_reseed(mbedtls_ctr_drbg_context *ctx,
const unsigned char *additional, size_t len);
/**
* \brief This function updates the state of the CTR_DRBG context.
*
* \note This function is not thread-safe. It is not safe
* to call this function if another thread might be
* concurrently obtaining random numbers from the same
* context or updating or reseeding the same context.
*
* \param ctx The CTR_DRBG context.
* \param additional The data to update the state with. This must not be
* \c NULL unless \p add_len is \c 0.
* \param add_len Length of \p additional in bytes. This must be at
* most #MBEDTLS_CTR_DRBG_MAX_SEED_INPUT.
*
* \return \c 0 on success.
* \return #MBEDTLS_ERR_CTR_DRBG_INPUT_TOO_BIG if
* \p add_len is more than
* #MBEDTLS_CTR_DRBG_MAX_SEED_INPUT.
* \return An error from the underlying AES cipher on failure.
*/
int mbedtls_ctr_drbg_update(mbedtls_ctr_drbg_context *ctx,
const unsigned char *additional,
size_t add_len);
/**
* \brief This function updates a CTR_DRBG instance with additional
* data and uses it to generate random data.
*
* This function automatically reseeds if the reseed counter is exceeded
* or prediction resistance is enabled.
*
* \note This function is not thread-safe. It is not safe
* to call this function if another thread might be
* concurrently obtaining random numbers from the same
* context or updating or reseeding the same context.
*
* \param p_rng The CTR_DRBG context. This must be a pointer to a
* #mbedtls_ctr_drbg_context structure.
* \param output The buffer to fill.
* \param output_len The length of the buffer in bytes.
* \param additional Additional data to update. Can be \c NULL, in which
* case the additional data is empty regardless of
* the value of \p add_len.
* \param add_len The length of the additional data
* if \p additional is not \c NULL.
* This must be less than #MBEDTLS_CTR_DRBG_MAX_INPUT
* and less than
* #MBEDTLS_CTR_DRBG_MAX_SEED_INPUT - \c entropy_len
* where \c entropy_len is the entropy length
* configured for the context.
*
* \return \c 0 on success.
* \return #MBEDTLS_ERR_CTR_DRBG_ENTROPY_SOURCE_FAILED or
* #MBEDTLS_ERR_CTR_DRBG_REQUEST_TOO_BIG on failure.
*/
int mbedtls_ctr_drbg_random_with_add(void *p_rng,
unsigned char *output, size_t output_len,
const unsigned char *additional, size_t add_len);
/**
* \brief This function uses CTR_DRBG to generate random data.
*
* This function automatically reseeds if the reseed counter is exceeded
* or prediction resistance is enabled.
*/
#if defined(MBEDTLS_THREADING_C)
/**
* \note When Mbed TLS is built with threading support,
* it is safe to call mbedtls_ctr_drbg_random()
* from multiple threads. Other operations, including
* reseeding, are not thread-safe.
*/
#endif /* MBEDTLS_THREADING_C */
/**
* \param p_rng The CTR_DRBG context. This must be a pointer to a
* #mbedtls_ctr_drbg_context structure.
* \param output The buffer to fill.
* \param output_len The length of the buffer in bytes.
*
* \return \c 0 on success.
* \return #MBEDTLS_ERR_CTR_DRBG_ENTROPY_SOURCE_FAILED or
* #MBEDTLS_ERR_CTR_DRBG_REQUEST_TOO_BIG on failure.
*/
int mbedtls_ctr_drbg_random(void *p_rng,
unsigned char *output, size_t output_len);
#if defined(MBEDTLS_FS_IO)
/**
* \brief This function writes a seed file.
*
* \param ctx The CTR_DRBG context.
* \param path The name of the file.
*
* \return \c 0 on success.
* \return #MBEDTLS_ERR_CTR_DRBG_FILE_IO_ERROR on file error.
* \return #MBEDTLS_ERR_CTR_DRBG_ENTROPY_SOURCE_FAILED on reseed
* failure.
*/
int mbedtls_ctr_drbg_write_seed_file(mbedtls_ctr_drbg_context *ctx, const char *path);
/**
* \brief This function reads and updates a seed file. The seed
* is added to this instance.
*
* \param ctx The CTR_DRBG context.
* \param path The name of the file.
*
* \return \c 0 on success.
* \return #MBEDTLS_ERR_CTR_DRBG_FILE_IO_ERROR on file error.
* \return #MBEDTLS_ERR_CTR_DRBG_ENTROPY_SOURCE_FAILED on
* reseed failure.
* \return #MBEDTLS_ERR_CTR_DRBG_INPUT_TOO_BIG if the existing
* seed file is too large.
*/
int mbedtls_ctr_drbg_update_seed_file(mbedtls_ctr_drbg_context *ctx, const char *path);
#endif /* MBEDTLS_FS_IO */
#if defined(MBEDTLS_SELF_TEST)
/**
* \brief The CTR_DRBG checkup routine.
*
* \return \c 0 on success.
* \return \c 1 on failure.
*/
int mbedtls_ctr_drbg_self_test(int verbose);
#endif /* MBEDTLS_SELF_TEST */
#ifdef __cplusplus
}
#endif
#endif /* ctr_drbg.h */
+156
View File
@@ -0,0 +1,156 @@
/**
* \file debug.h
*
* \brief Functions for controlling and providing debug output from the library.
*/
/*
* Copyright The Mbed TLS Contributors
* SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
*/
#ifndef MBEDTLS_DEBUG_H
#define MBEDTLS_DEBUG_H
#include "mbedtls/build_info.h"
#include "mbedtls/ssl.h"
#if defined(MBEDTLS_ECP_C)
#include "mbedtls/ecp.h"
#endif
#if defined(MBEDTLS_DEBUG_C)
#define MBEDTLS_DEBUG_STRIP_PARENS(...) __VA_ARGS__
#define MBEDTLS_SSL_DEBUG_MSG(level, args) \
mbedtls_debug_print_msg(ssl, level, __FILE__, __LINE__, \
MBEDTLS_DEBUG_STRIP_PARENS args)
#define MBEDTLS_SSL_DEBUG_RET(level, text, ret) \
mbedtls_debug_print_ret(ssl, level, __FILE__, __LINE__, text, ret)
#define MBEDTLS_SSL_DEBUG_BUF(level, text, buf, len) \
mbedtls_debug_print_buf(ssl, level, __FILE__, __LINE__, text, buf, len)
#if defined(MBEDTLS_BIGNUM_C)
#define MBEDTLS_SSL_DEBUG_MPI(level, text, X) \
mbedtls_debug_print_mpi(ssl, level, __FILE__, __LINE__, text, X)
#endif
#if defined(MBEDTLS_ECP_C)
#define MBEDTLS_SSL_DEBUG_ECP(level, text, X) \
mbedtls_debug_print_ecp(ssl, level, __FILE__, __LINE__, text, X)
#endif
#if defined(MBEDTLS_X509_CRT_PARSE_C)
#if !defined(MBEDTLS_X509_REMOVE_INFO)
#define MBEDTLS_SSL_DEBUG_CRT(level, text, crt) \
mbedtls_debug_print_crt(ssl, level, __FILE__, __LINE__, text, crt)
#else
#define MBEDTLS_SSL_DEBUG_CRT(level, text, crt) do { } while (0)
#endif /* MBEDTLS_X509_REMOVE_INFO */
#endif /* MBEDTLS_X509_CRT_PARSE_C */
#if defined(MBEDTLS_ECDH_C)
#define MBEDTLS_SSL_DEBUG_ECDH(level, ecdh, attr) \
mbedtls_debug_printf_ecdh(ssl, level, __FILE__, __LINE__, ecdh, attr)
#endif
#else /* MBEDTLS_DEBUG_C */
#define MBEDTLS_SSL_DEBUG_MSG(level, args) do { } while (0)
#define MBEDTLS_SSL_DEBUG_RET(level, text, ret) do { } while (0)
#define MBEDTLS_SSL_DEBUG_BUF(level, text, buf, len) do { } while (0)
#define MBEDTLS_SSL_DEBUG_MPI(level, text, X) do { } while (0)
#define MBEDTLS_SSL_DEBUG_ECP(level, text, X) do { } while (0)
#define MBEDTLS_SSL_DEBUG_CRT(level, text, crt) do { } while (0)
#define MBEDTLS_SSL_DEBUG_ECDH(level, ecdh, attr) do { } while (0)
#endif /* MBEDTLS_DEBUG_C */
/**
* \def MBEDTLS_PRINTF_ATTRIBUTE
*
* Mark a function as having printf attributes, and thus enable checking
* via -wFormat and other flags. This does nothing on builds with compilers
* that do not support the format attribute
*
* Module: library/debug.c
* Caller:
*
* This module provides debugging functions.
*/
#if defined(__has_attribute)
#if __has_attribute(format)
#if defined(__MINGW32__) && __USE_MINGW_ANSI_STDIO == 1
#define MBEDTLS_PRINTF_ATTRIBUTE(string_index, first_to_check) \
__attribute__((__format__(gnu_printf, string_index, first_to_check)))
#else /* defined(__MINGW32__) && __USE_MINGW_ANSI_STDIO == 1 */
#define MBEDTLS_PRINTF_ATTRIBUTE(string_index, first_to_check) \
__attribute__((format(printf, string_index, first_to_check)))
#endif
#else /* __has_attribute(format) */
#define MBEDTLS_PRINTF_ATTRIBUTE(string_index, first_to_check)
#endif /* __has_attribute(format) */
#else /* defined(__has_attribute) */
#define MBEDTLS_PRINTF_ATTRIBUTE(string_index, first_to_check)
#endif
/**
* \def MBEDTLS_PRINTF_SIZET
*
* MBEDTLS_PRINTF_xxx: Due to issues with older window compilers
* and MinGW we need to define the printf specifier for size_t
* and long long per platform.
*
* Module: library/debug.c
* Caller:
*
* This module provides debugging functions.
*/
#if defined(__MINGW32__) || (defined(_MSC_VER) && _MSC_VER < 1900)
#include <inttypes.h>
#define MBEDTLS_PRINTF_SIZET PRIuPTR
#define MBEDTLS_PRINTF_LONGLONG PRId64
#else \
/* defined(__MINGW32__) || (defined(_MSC_VER) && _MSC_VER < 1900) */
#define MBEDTLS_PRINTF_SIZET "zu"
#define MBEDTLS_PRINTF_LONGLONG "lld"
#endif \
/* defined(__MINGW32__) || (defined(_MSC_VER) && _MSC_VER < 1900) */
#if !defined(MBEDTLS_PRINTF_MS_TIME)
#include <inttypes.h>
#if !defined(PRId64)
#define MBEDTLS_PRINTF_MS_TIME MBEDTLS_PRINTF_LONGLONG
#else
#define MBEDTLS_PRINTF_MS_TIME PRId64
#endif
#endif /* MBEDTLS_PRINTF_MS_TIME */
#ifdef __cplusplus
extern "C" {
#endif
/**
* \brief Set the threshold error level to handle globally all debug output.
* Debug messages that have a level over the threshold value are
* discarded.
* (Default value: 0 = No debug )
*
* \param threshold threshold level of messages to filter on. Messages at a
* higher level will be discarded.
* - Debug levels
* - 0 No debug
* - 1 Error
* - 2 State change
* - 3 Informational
* - 4 Verbose
*/
void mbedtls_debug_set_threshold(int threshold);
#ifdef __cplusplus
}
#endif
#endif /* MBEDTLS_DEBUG_H */
+455
View File
@@ -0,0 +1,455 @@
/**
* \file ecdh.h
*
* \brief This file contains ECDH definitions and functions.
*
* The Elliptic Curve Diffie-Hellman (ECDH) protocol is an anonymous
* key agreement protocol allowing two parties to establish a shared
* secret over an insecure channel. Each party must have an
* elliptic-curve public private key pair.
*
* For more information, see <em>NIST SP 800-56A Rev. 2: Recommendation for
* Pair-Wise Key Establishment Schemes Using Discrete Logarithm
* Cryptography</em>.
*/
/*
* Copyright The Mbed TLS Contributors
* SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
*/
#ifndef MBEDTLS_ECDH_H
#define MBEDTLS_ECDH_H
#include "mbedtls/private_access.h"
#include "mbedtls/build_info.h"
#include "mbedtls/ecp.h"
/*
* Mbed TLS supports two formats for ECDH contexts (#mbedtls_ecdh_context
* defined in `ecdh.h`). For most applications, the choice of format makes
* no difference, since all library functions can work with either format,
* except that the new format is incompatible with MBEDTLS_ECP_RESTARTABLE.
* The new format used when this option is disabled is smaller
* (56 bytes on a 32-bit platform). In future versions of the library, it
* will support alternative implementations of ECDH operations.
* The new format is incompatible with applications that access
* context fields directly and with restartable ECP operations.
*/
#if defined(MBEDTLS_ECP_RESTARTABLE)
#define MBEDTLS_ECDH_LEGACY_CONTEXT
#else
#undef MBEDTLS_ECDH_LEGACY_CONTEXT
#endif
#if defined(MBEDTLS_ECDH_VARIANT_EVEREST_ENABLED)
#undef MBEDTLS_ECDH_LEGACY_CONTEXT
#include "everest/everest.h"
#endif
#ifdef __cplusplus
extern "C" {
#endif
/**
* Defines the source of the imported EC key.
*/
typedef enum {
MBEDTLS_ECDH_OURS, /**< Our key. */
MBEDTLS_ECDH_THEIRS, /**< The key of the peer. */
} mbedtls_ecdh_side;
#if !defined(MBEDTLS_ECDH_LEGACY_CONTEXT)
/**
* Defines the ECDH implementation used.
*
* Later versions of the library may add new variants, therefore users should
* not make any assumptions about them.
*/
typedef enum {
MBEDTLS_ECDH_VARIANT_NONE = 0, /*!< Implementation not defined. */
MBEDTLS_ECDH_VARIANT_MBEDTLS_2_0,/*!< The default Mbed TLS implementation */
#if defined(MBEDTLS_ECDH_VARIANT_EVEREST_ENABLED)
MBEDTLS_ECDH_VARIANT_EVEREST /*!< Everest implementation */
#endif
} mbedtls_ecdh_variant;
/**
* The context used by the default ECDH implementation.
*
* Later versions might change the structure of this context, therefore users
* should not make any assumptions about the structure of
* mbedtls_ecdh_context_mbed.
*/
typedef struct mbedtls_ecdh_context_mbed {
mbedtls_ecp_group MBEDTLS_PRIVATE(grp); /*!< The elliptic curve used. */
mbedtls_mpi MBEDTLS_PRIVATE(d); /*!< The private key. */
mbedtls_ecp_point MBEDTLS_PRIVATE(Q); /*!< The public key. */
mbedtls_ecp_point MBEDTLS_PRIVATE(Qp); /*!< The value of the public key of the peer. */
mbedtls_mpi MBEDTLS_PRIVATE(z); /*!< The shared secret. */
#if defined(MBEDTLS_ECP_RESTARTABLE)
mbedtls_ecp_restart_ctx MBEDTLS_PRIVATE(rs); /*!< The restart context for EC computations. */
#endif
} mbedtls_ecdh_context_mbed;
#endif
/**
*
* \warning Performing multiple operations concurrently on the same
* ECDSA context is not supported; objects of this type
* should not be shared between multiple threads.
* \brief The ECDH context structure.
*/
typedef struct mbedtls_ecdh_context {
#if defined(MBEDTLS_ECDH_LEGACY_CONTEXT)
mbedtls_ecp_group MBEDTLS_PRIVATE(grp); /*!< The elliptic curve used. */
mbedtls_mpi MBEDTLS_PRIVATE(d); /*!< The private key. */
mbedtls_ecp_point MBEDTLS_PRIVATE(Q); /*!< The public key. */
mbedtls_ecp_point MBEDTLS_PRIVATE(Qp); /*!< The value of the public key of the peer. */
mbedtls_mpi MBEDTLS_PRIVATE(z); /*!< The shared secret. */
int MBEDTLS_PRIVATE(point_format); /*!< The format of point export in TLS messages. */
mbedtls_ecp_point MBEDTLS_PRIVATE(Vi); /*!< The blinding value. */
mbedtls_ecp_point MBEDTLS_PRIVATE(Vf); /*!< The unblinding value. */
mbedtls_mpi MBEDTLS_PRIVATE(_d); /*!< The previous \p d. */
#if defined(MBEDTLS_ECP_RESTARTABLE)
int MBEDTLS_PRIVATE(restart_enabled); /*!< The flag for restartable mode. */
mbedtls_ecp_restart_ctx MBEDTLS_PRIVATE(rs); /*!< The restart context for EC computations. */
#endif /* MBEDTLS_ECP_RESTARTABLE */
#else
uint8_t MBEDTLS_PRIVATE(point_format); /*!< The format of point export in TLS messages
as defined in RFC 4492. */
mbedtls_ecp_group_id MBEDTLS_PRIVATE(grp_id);/*!< The elliptic curve used. */
mbedtls_ecdh_variant MBEDTLS_PRIVATE(var); /*!< The ECDH implementation/structure used. */
union {
mbedtls_ecdh_context_mbed MBEDTLS_PRIVATE(mbed_ecdh);
#if defined(MBEDTLS_ECDH_VARIANT_EVEREST_ENABLED)
mbedtls_ecdh_context_everest MBEDTLS_PRIVATE(everest_ecdh);
#endif
} MBEDTLS_PRIVATE(ctx); /*!< Implementation-specific context. The
context in use is specified by the \c var
field. */
#if defined(MBEDTLS_ECP_RESTARTABLE)
uint8_t MBEDTLS_PRIVATE(restart_enabled); /*!< The flag for restartable mode. Functions of
an alternative implementation not supporting
restartable mode must return
MBEDTLS_ERR_PLATFORM_FEATURE_UNSUPPORTED error
if this flag is set. */
#endif /* MBEDTLS_ECP_RESTARTABLE */
#endif /* MBEDTLS_ECDH_LEGACY_CONTEXT */
}
mbedtls_ecdh_context;
/**
* \brief Return the ECP group for provided context.
*
* \note To access group specific fields, users should use
* `mbedtls_ecp_curve_info_from_grp_id` or
* `mbedtls_ecp_group_load` on the extracted `group_id`.
*
* \param ctx The ECDH context to parse. This must not be \c NULL.
*
* \return The \c mbedtls_ecp_group_id of the context.
*/
mbedtls_ecp_group_id mbedtls_ecdh_get_grp_id(mbedtls_ecdh_context *ctx);
/**
* \brief Check whether a given group can be used for ECDH.
*
* \param gid The ECP group ID to check.
*
* \return \c 1 if the group can be used, \c 0 otherwise
*/
int mbedtls_ecdh_can_do(mbedtls_ecp_group_id gid);
/**
* \brief This function generates an ECDH keypair on an elliptic
* curve.
*
* This function performs the first of two core computations
* implemented during the ECDH key exchange. The second core
* computation is performed by mbedtls_ecdh_compute_shared().
*
* \see ecp.h
*
* \param grp The ECP group to use. This must be initialized and have
* domain parameters loaded, for example through
* mbedtls_ecp_load() or mbedtls_ecp_tls_read_group().
* \param d The destination MPI (private key).
* This must be initialized.
* \param Q The destination point (public key).
* This must be initialized.
* \param f_rng The RNG function to use. This must not be \c NULL.
* \param p_rng The RNG context to be passed to \p f_rng. This may be
* \c NULL in case \p f_rng doesn't need a context argument.
*
* \return \c 0 on success.
* \return Another \c MBEDTLS_ERR_ECP_XXX or
* \c MBEDTLS_MPI_XXX error code on failure.
*/
int mbedtls_ecdh_gen_public(mbedtls_ecp_group *grp, mbedtls_mpi *d, mbedtls_ecp_point *Q,
mbedtls_f_rng_t *f_rng,
void *p_rng);
/**
* \brief This function computes the shared secret.
*
* This function performs the second of two core computations
* implemented during the ECDH key exchange. The first core
* computation is performed by mbedtls_ecdh_gen_public().
*
* \see ecp.h
*
* \note If \p f_rng is not NULL, it is used to implement
* countermeasures against side-channel attacks.
* For more information, see mbedtls_ecp_mul().
*
* \param grp The ECP group to use. This must be initialized and have
* domain parameters loaded, for example through
* mbedtls_ecp_load() or mbedtls_ecp_tls_read_group().
* \param z The destination MPI (shared secret).
* This must be initialized.
* \param Q The public key from another party.
* This must be initialized.
* \param d Our secret exponent (private key).
* This must be initialized.
* \param f_rng The RNG function to use. This must not be \c NULL.
* \param p_rng The RNG context to be passed to \p f_rng. This may be
* \c NULL if \p f_rng is \c NULL or doesn't need a
* context argument.
*
* \return \c 0 on success.
* \return Another \c MBEDTLS_ERR_ECP_XXX or
* \c MBEDTLS_MPI_XXX error code on failure.
*/
int mbedtls_ecdh_compute_shared(mbedtls_ecp_group *grp, mbedtls_mpi *z,
const mbedtls_ecp_point *Q, const mbedtls_mpi *d,
mbedtls_f_rng_t *f_rng,
void *p_rng);
/**
* \brief This function initializes an ECDH context.
*
* \param ctx The ECDH context to initialize. This must not be \c NULL.
*/
void mbedtls_ecdh_init(mbedtls_ecdh_context *ctx);
/**
* \brief This function sets up the ECDH context with the information
* given.
*
* This function should be called after mbedtls_ecdh_init() but
* before mbedtls_ecdh_make_params(). There is no need to call
* this function before mbedtls_ecdh_read_params().
*
* This is the first function used by a TLS server for ECDHE
* ciphersuites.
*
* \param ctx The ECDH context to set up. This must be initialized.
* \param grp_id The group id of the group to set up the context for.
*
* \return \c 0 on success.
*/
int mbedtls_ecdh_setup(mbedtls_ecdh_context *ctx,
mbedtls_ecp_group_id grp_id);
/**
* \brief This function frees a context.
*
* \param ctx The context to free. This may be \c NULL, in which
* case this function does nothing. If it is not \c NULL,
* it must point to an initialized ECDH context.
*/
void mbedtls_ecdh_free(mbedtls_ecdh_context *ctx);
/**
* \brief This function generates an EC key pair and exports its
* in the format used in a TLS ServerKeyExchange handshake
* message.
*
* This is the second function used by a TLS server for ECDHE
* ciphersuites. (It is called after mbedtls_ecdh_setup().)
*
* \see ecp.h
*
* \param ctx The ECDH context to use. This must be initialized
* and bound to a group, for example via mbedtls_ecdh_setup().
* \param olen The address at which to store the number of Bytes written.
* \param buf The destination buffer. This must be a writable buffer of
* length \p blen Bytes.
* \param blen The length of the destination buffer \p buf in Bytes.
* \param f_rng The RNG function to use. This must not be \c NULL.
* \param p_rng The RNG context to be passed to \p f_rng. This may be
* \c NULL in case \p f_rng doesn't need a context argument.
*
* \return \c 0 on success.
* \return #MBEDTLS_ERR_ECP_IN_PROGRESS if maximum number of
* operations was reached: see \c mbedtls_ecp_set_max_ops().
* \return Another \c MBEDTLS_ERR_ECP_XXX error code on failure.
*/
int mbedtls_ecdh_make_params(mbedtls_ecdh_context *ctx, size_t *olen,
unsigned char *buf, size_t blen,
mbedtls_f_rng_t *f_rng,
void *p_rng);
/**
* \brief This function parses the ECDHE parameters in a
* TLS ServerKeyExchange handshake message.
*
* \note In a TLS handshake, this is the how the client
* sets up its ECDHE context from the server's public
* ECDHE key material.
*
* \see ecp.h
*
* \param ctx The ECDHE context to use. This must be initialized.
* \param buf On input, \c *buf must be the start of the input buffer.
* On output, \c *buf is updated to point to the end of the
* data that has been read. On success, this is the first byte
* past the end of the ServerKeyExchange parameters.
* On error, this is the point at which an error has been
* detected, which is usually not useful except to debug
* failures.
* \param end The end of the input buffer.
*
* \return \c 0 on success.
* \return An \c MBEDTLS_ERR_ECP_XXX error code on failure.
*
*/
int mbedtls_ecdh_read_params(mbedtls_ecdh_context *ctx,
const unsigned char **buf,
const unsigned char *end);
/**
* \brief This function sets up an ECDH context from an EC key.
*
* It is used by clients and servers in place of the
* ServerKeyExchange for static ECDH, and imports ECDH
* parameters from the EC key information of a certificate.
*
* \see ecp.h
*
* \param ctx The ECDH context to set up. This must be initialized.
* \param key The EC key to use. This must be initialized.
* \param side Defines the source of the key. Possible values are:
* - #MBEDTLS_ECDH_OURS: The key is ours.
* - #MBEDTLS_ECDH_THEIRS: The key is that of the peer.
*
* \return \c 0 on success.
* \return Another \c MBEDTLS_ERR_ECP_XXX error code on failure.
*
*/
int mbedtls_ecdh_get_params(mbedtls_ecdh_context *ctx,
const mbedtls_ecp_keypair *key,
mbedtls_ecdh_side side);
/**
* \brief This function generates a public key and exports it
* as a TLS ClientKeyExchange payload.
*
* This is the second function used by a TLS client for ECDH(E)
* ciphersuites.
*
* \see ecp.h
*
* \param ctx The ECDH context to use. This must be initialized
* and bound to a group, the latter usually by
* mbedtls_ecdh_read_params().
* \param olen The address at which to store the number of Bytes written.
* This must not be \c NULL.
* \param buf The destination buffer. This must be a writable buffer
* of length \p blen Bytes.
* \param blen The size of the destination buffer \p buf in Bytes.
* \param f_rng The RNG function to use. This must not be \c NULL.
* \param p_rng The RNG context to be passed to \p f_rng. This may be
* \c NULL in case \p f_rng doesn't need a context argument.
*
* \return \c 0 on success.
* \return #MBEDTLS_ERR_ECP_IN_PROGRESS if maximum number of
* operations was reached: see \c mbedtls_ecp_set_max_ops().
* \return Another \c MBEDTLS_ERR_ECP_XXX error code on failure.
*/
int mbedtls_ecdh_make_public(mbedtls_ecdh_context *ctx, size_t *olen,
unsigned char *buf, size_t blen,
mbedtls_f_rng_t *f_rng,
void *p_rng);
/**
* \brief This function parses and processes the ECDHE payload of a
* TLS ClientKeyExchange message.
*
* This is the third function used by a TLS server for ECDH(E)
* ciphersuites. (It is called after mbedtls_ecdh_setup() and
* mbedtls_ecdh_make_params().)
*
* \see ecp.h
*
* \param ctx The ECDH context to use. This must be initialized
* and bound to a group, for example via mbedtls_ecdh_setup().
* \param buf The pointer to the ClientKeyExchange payload. This must
* be a readable buffer of length \p blen Bytes.
* \param blen The length of the input buffer \p buf in Bytes.
*
* \return \c 0 on success.
* \return An \c MBEDTLS_ERR_ECP_XXX error code on failure.
*/
int mbedtls_ecdh_read_public(mbedtls_ecdh_context *ctx,
const unsigned char *buf, size_t blen);
/**
* \brief This function derives and exports the shared secret.
*
* This is the last function used by both TLS client
* and servers.
*
* \note If \p f_rng is not NULL, it is used to implement
* countermeasures against side-channel attacks.
* For more information, see mbedtls_ecp_mul().
*
* \see ecp.h
* \param ctx The ECDH context to use. This must be initialized
* and have its own private key generated and the peer's
* public key imported.
* \param olen The address at which to store the total number of
* Bytes written on success. This must not be \c NULL.
* \param buf The buffer to write the generated shared key to. This
* must be a writable buffer of size \p blen Bytes.
* A sufficient size is given by #MBEDTLS_ECP_MAX_BYTES.
* \param blen The length of the destination buffer \p buf in Bytes.
* \param f_rng The RNG function to use. This must not be \c NULL.
* \param p_rng The RNG context. This may be \c NULL if \p f_rng
* doesn't need a context argument.
*
* \return \c 0 on success.
* \return #MBEDTLS_ERR_ECP_IN_PROGRESS if maximum number of
* operations was reached: see \c mbedtls_ecp_set_max_ops().
* \return Another \c MBEDTLS_ERR_ECP_XXX error code on failure.
*/
int mbedtls_ecdh_calc_secret(mbedtls_ecdh_context *ctx, size_t *olen,
unsigned char *buf, size_t blen,
mbedtls_f_rng_t *f_rng,
void *p_rng);
#if defined(MBEDTLS_ECP_RESTARTABLE)
/**
* \brief This function enables restartable EC computations for this
* context. (Default: disabled.)
*
* \see \c mbedtls_ecp_set_max_ops()
*
* \note It is not possible to safely disable restartable
* computations once enabled, except by free-ing the context,
* which cancels possible in-progress operations.
*
* \param ctx The ECDH context to use. This must be initialized.
*/
void mbedtls_ecdh_enable_restart(mbedtls_ecdh_context *ctx);
#endif /* MBEDTLS_ECP_RESTARTABLE */
#ifdef __cplusplus
}
#endif
#endif /* ecdh.h */
+674
View File
@@ -0,0 +1,674 @@
/**
* \file ecdsa.h
*
* \brief This file contains ECDSA definitions and functions.
*
* The Elliptic Curve Digital Signature Algorithm (ECDSA) is defined in
* <em>Standards for Efficient Cryptography Group (SECG):
* SEC1 Elliptic Curve Cryptography</em>.
* The use of ECDSA for TLS is defined in <em>RFC-4492: Elliptic Curve
* Cryptography (ECC) Cipher Suites for Transport Layer Security (TLS)</em>.
*
*/
/*
* Copyright The Mbed TLS Contributors
* SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
*/
#ifndef MBEDTLS_ECDSA_H
#define MBEDTLS_ECDSA_H
#include "mbedtls/private_access.h"
#include "mbedtls/build_info.h"
#include "mbedtls/ecp.h"
#include "mbedtls/md.h"
/**
* \brief Maximum ECDSA signature size for a given curve bit size
*
* \param bits Curve size in bits
* \return Maximum signature size in bytes
*
* \note This macro returns a compile-time constant if its argument
* is one. It may evaluate its argument multiple times.
*/
/*
* Ecdsa-Sig-Value ::= SEQUENCE {
* r INTEGER,
* s INTEGER
* }
*
* For each of r and s, the value (V) may include an extra initial "0" bit.
*/
#define MBEDTLS_ECDSA_MAX_SIG_LEN(bits) \
(/*T,L of SEQUENCE*/ ((bits) >= 61 * 8 ? 3 : 2) + \
/*T,L of r,s*/ 2 * (((bits) >= 127 * 8 ? 3 : 2) + \
/*V of r,s*/ ((bits) + 8) / 8))
/** The maximal size of an ECDSA signature in Bytes. */
#define MBEDTLS_ECDSA_MAX_LEN MBEDTLS_ECDSA_MAX_SIG_LEN(MBEDTLS_ECP_MAX_BITS)
#ifdef __cplusplus
extern "C" {
#endif
/**
* \brief The ECDSA context structure.
*
* \warning Performing multiple operations concurrently on the same
* ECDSA context is not supported; objects of this type
* should not be shared between multiple threads.
*
* \note pk_wrap module assumes that "ecdsa_context" is identical
* to "ecp_keypair" (see for example structure
* "mbedtls_eckey_info" where ECDSA sign/verify functions
* are used also for EC key)
*/
typedef mbedtls_ecp_keypair mbedtls_ecdsa_context;
#if defined(MBEDTLS_ECP_RESTARTABLE)
/**
* \brief Internal restart context for ecdsa_verify()
*
* \note Opaque struct, defined in ecdsa.c
*/
typedef struct mbedtls_ecdsa_restart_ver mbedtls_ecdsa_restart_ver_ctx;
/**
* \brief Internal restart context for ecdsa_sign()
*
* \note Opaque struct, defined in ecdsa.c
*/
typedef struct mbedtls_ecdsa_restart_sig mbedtls_ecdsa_restart_sig_ctx;
#if defined(MBEDTLS_ECDSA_DETERMINISTIC)
/**
* \brief Internal restart context for ecdsa_sign_det()
*
* \note Opaque struct, defined in ecdsa.c
*/
typedef struct mbedtls_ecdsa_restart_det mbedtls_ecdsa_restart_det_ctx;
#endif
/**
* \brief General context for resuming ECDSA operations
*/
typedef struct {
mbedtls_ecp_restart_ctx MBEDTLS_PRIVATE(ecp); /*!< base context for ECP restart and
shared administrative info */
mbedtls_ecdsa_restart_ver_ctx *MBEDTLS_PRIVATE(ver); /*!< ecdsa_verify() sub-context */
mbedtls_ecdsa_restart_sig_ctx *MBEDTLS_PRIVATE(sig); /*!< ecdsa_sign() sub-context */
#if defined(MBEDTLS_ECDSA_DETERMINISTIC)
mbedtls_ecdsa_restart_det_ctx *MBEDTLS_PRIVATE(det); /*!< ecdsa_sign_det() sub-context */
#endif
} mbedtls_ecdsa_restart_ctx;
#else /* MBEDTLS_ECP_RESTARTABLE */
/* Now we can declare functions that take a pointer to that */
typedef void mbedtls_ecdsa_restart_ctx;
#endif /* MBEDTLS_ECP_RESTARTABLE */
/**
* \brief This function checks whether a given group can be used
* for ECDSA.
*
* \param gid The ECP group ID to check.
*
* \return \c 1 if the group can be used, \c 0 otherwise
*/
int mbedtls_ecdsa_can_do(mbedtls_ecp_group_id gid);
/**
* \brief This function computes the ECDSA signature of a
* previously-hashed message.
*
* \note The deterministic version implemented in
* mbedtls_ecdsa_sign_det_ext() is usually preferred.
*
* \note If the bitlength of the message hash is larger than the
* bitlength of the group order, then the hash is truncated
* as defined in <em>Standards for Efficient Cryptography Group
* (SECG): SEC1 Elliptic Curve Cryptography</em>, section
* 4.1.3, step 5.
*
* \see ecp.h
*
* \param grp The context for the elliptic curve to use.
* This must be initialized and have group parameters
* set, for example through mbedtls_ecp_group_load().
* \param r The MPI context in which to store the first part
* the signature. This must be initialized.
* \param s The MPI context in which to store the second part
* the signature. This must be initialized.
* \param d The private signing key. This must be initialized.
* \param buf The content to be signed. This is usually the hash of
* the original data to be signed. This must be a readable
* buffer of length \p blen Bytes. It may be \c NULL if
* \p blen is zero.
* \param blen The length of \p buf in Bytes.
* \param f_rng The RNG function, used both to generate the ECDSA nonce
* and for blinding. This must not be \c NULL.
* \param p_rng The RNG context to be passed to \p f_rng. This may be
* \c NULL if \p f_rng doesn't need a context parameter.
*
* \return \c 0 on success.
* \return An \c MBEDTLS_ERR_ECP_XXX
* or \c MBEDTLS_MPI_XXX error code on failure.
*/
int mbedtls_ecdsa_sign(mbedtls_ecp_group *grp, mbedtls_mpi *r, mbedtls_mpi *s,
const mbedtls_mpi *d, const unsigned char *buf, size_t blen,
mbedtls_f_rng_t *f_rng, void *p_rng);
#if defined(MBEDTLS_ECDSA_DETERMINISTIC)
/**
* \brief This function computes the ECDSA signature of a
* previously-hashed message, deterministic version.
*
* For more information, see <em>RFC-6979: Deterministic
* Usage of the Digital Signature Algorithm (DSA) and Elliptic
* Curve Digital Signature Algorithm (ECDSA)</em>.
*
* \note If the bitlength of the message hash is larger than the
* bitlength of the group order, then the hash is truncated as
* defined in <em>Standards for Efficient Cryptography Group
* (SECG): SEC1 Elliptic Curve Cryptography</em>, section
* 4.1.3, step 5.
*
* \see ecp.h
*
* \param grp The context for the elliptic curve to use.
* This must be initialized and have group parameters
* set, for example through mbedtls_ecp_group_load().
* \param r The MPI context in which to store the first part
* the signature. This must be initialized.
* \param s The MPI context in which to store the second part
* the signature. This must be initialized.
* \param d The private signing key. This must be initialized
* and setup, for example through mbedtls_ecp_gen_privkey().
* \param buf The hashed content to be signed. This must be a readable
* buffer of length \p blen Bytes. It may be \c NULL if
* \p blen is zero.
* \param blen The length of \p buf in Bytes.
* \param md_alg The hash algorithm used to hash the original data.
* \param f_rng_blind The RNG function used for blinding. This must not be
* \c NULL.
* \param p_rng_blind The RNG context to be passed to \p f_rng_blind. This
* may be \c NULL if \p f_rng_blind doesn't need a context
* parameter.
*
* \return \c 0 on success.
* \return An \c MBEDTLS_ERR_ECP_XXX or \c MBEDTLS_MPI_XXX
* error code on failure.
*/
int mbedtls_ecdsa_sign_det_ext(mbedtls_ecp_group *grp, mbedtls_mpi *r,
mbedtls_mpi *s, const mbedtls_mpi *d,
const unsigned char *buf, size_t blen,
mbedtls_md_type_t md_alg,
mbedtls_f_rng_t *f_rng_blind,
void *p_rng_blind);
#endif /* MBEDTLS_ECDSA_DETERMINISTIC */
#if !defined(MBEDTLS_ECDSA_SIGN_ALT)
/**
* \brief This function computes the ECDSA signature of a
* previously-hashed message, in a restartable way.
*
* \note The deterministic version implemented in
* mbedtls_ecdsa_sign_det_restartable() is usually
* preferred.
*
* \note This function is like \c mbedtls_ecdsa_sign() but
* it can return early and restart according to the
* limit set with \c mbedtls_ecp_set_max_ops() to
* reduce blocking.
*
* \note If the bitlength of the message hash is larger
* than the bitlength of the group order, then the
* hash is truncated as defined in <em>Standards for
* Efficient Cryptography Group (SECG): SEC1 Elliptic
* Curve Cryptography</em>, section 4.1.3, step 5.
*
* \see ecp.h
*
* \param grp The context for the elliptic curve to use.
* This must be initialized and have group parameters
* set, for example through mbedtls_ecp_group_load().
* \param r The MPI context in which to store the first part
* the signature. This must be initialized.
* \param s The MPI context in which to store the second part
* the signature. This must be initialized.
* \param d The private signing key. This must be initialized
* and setup, for example through
* mbedtls_ecp_gen_privkey().
* \param buf The hashed content to be signed. This must be a readable
* buffer of length \p blen Bytes. It may be \c NULL if
* \p blen is zero.
* \param blen The length of \p buf in Bytes.
* \param f_rng The RNG function used to generate the ECDSA nonce.
* This must not be \c NULL.
* \param p_rng The RNG context to be passed to \p f_rng. This may be
* \c NULL if \p f_rng doesn't need a context parameter.
* \param f_rng_blind The RNG function used for blinding. This must not be
* \c NULL.
* \param p_rng_blind The RNG context to be passed to \p f_rng. This may be
* \c NULL if \p f_rng doesn't need a context parameter.
* \param rs_ctx The restart context to use. This may be \c NULL
* to disable restarting. If it is not \c NULL, it
* must point to an initialized restart context.
*
* \return \c 0 on success.
* \return #MBEDTLS_ERR_ECP_IN_PROGRESS if maximum number of
* operations was reached: see \c
* mbedtls_ecp_set_max_ops().
* \return Another \c MBEDTLS_ERR_ECP_XXX, \c
* MBEDTLS_ERR_MPI_XXX or \c MBEDTLS_ERR_ASN1_XXX
* error code on failure.
*/
int mbedtls_ecdsa_sign_restartable(
mbedtls_ecp_group *grp,
mbedtls_mpi *r, mbedtls_mpi *s,
const mbedtls_mpi *d,
const unsigned char *buf, size_t blen,
mbedtls_f_rng_t *f_rng,
void *p_rng,
mbedtls_f_rng_t *f_rng_blind,
void *p_rng_blind,
mbedtls_ecdsa_restart_ctx *rs_ctx);
#endif /* !MBEDTLS_ECDSA_SIGN_ALT */
#if defined(MBEDTLS_ECDSA_DETERMINISTIC)
/**
* \brief This function computes the ECDSA signature of a
* previously-hashed message, in a restartable way.
*
* \note This function is like \c
* mbedtls_ecdsa_sign_det_ext() but it can return
* early and restart according to the limit set with
* \c mbedtls_ecp_set_max_ops() to reduce blocking.
*
* \note If the bitlength of the message hash is larger
* than the bitlength of the group order, then the
* hash is truncated as defined in <em>Standards for
* Efficient Cryptography Group (SECG): SEC1 Elliptic
* Curve Cryptography</em>, section 4.1.3, step 5.
*
* \see ecp.h
*
* \param grp The context for the elliptic curve to use.
* This must be initialized and have group parameters
* set, for example through mbedtls_ecp_group_load().
* \param r The MPI context in which to store the first part
* the signature. This must be initialized.
* \param s The MPI context in which to store the second part
* the signature. This must be initialized.
* \param d The private signing key. This must be initialized
* and setup, for example through
* mbedtls_ecp_gen_privkey().
* \param buf The hashed content to be signed. This must be a readable
* buffer of length \p blen Bytes. It may be \c NULL if
* \p blen is zero.
* \param blen The length of \p buf in Bytes.
* \param md_alg The hash algorithm used to hash the original data.
* \param f_rng_blind The RNG function used for blinding. This must not be
* \c NULL.
* \param p_rng_blind The RNG context to be passed to \p f_rng_blind. This may be
* \c NULL if \p f_rng_blind doesn't need a context parameter.
* \param rs_ctx The restart context to use. This may be \c NULL
* to disable restarting. If it is not \c NULL, it
* must point to an initialized restart context.
*
* \return \c 0 on success.
* \return #MBEDTLS_ERR_ECP_IN_PROGRESS if maximum number of
* operations was reached: see \c
* mbedtls_ecp_set_max_ops().
* \return Another \c MBEDTLS_ERR_ECP_XXX, \c
* MBEDTLS_ERR_MPI_XXX or \c MBEDTLS_ERR_ASN1_XXX
* error code on failure.
*/
int mbedtls_ecdsa_sign_det_restartable(
mbedtls_ecp_group *grp,
mbedtls_mpi *r, mbedtls_mpi *s,
const mbedtls_mpi *d, const unsigned char *buf, size_t blen,
mbedtls_md_type_t md_alg,
mbedtls_f_rng_t *f_rng_blind,
void *p_rng_blind,
mbedtls_ecdsa_restart_ctx *rs_ctx);
#endif /* MBEDTLS_ECDSA_DETERMINISTIC */
/**
* \brief This function verifies the ECDSA signature of a
* previously-hashed message.
*
* \note If the bitlength of the message hash is larger than the
* bitlength of the group order, then the hash is truncated as
* defined in <em>Standards for Efficient Cryptography Group
* (SECG): SEC1 Elliptic Curve Cryptography</em>, section
* 4.1.4, step 3.
*
* \see ecp.h
*
* \param grp The ECP group to use.
* This must be initialized and have group parameters
* set, for example through mbedtls_ecp_group_load().
* \param buf The hashed content that was signed. This must be a readable
* buffer of length \p blen Bytes. It may be \c NULL if
* \p blen is zero.
* \param blen The length of \p buf in Bytes.
* \param Q The public key to use for verification. This must be
* initialized and setup.
* \param r The first integer of the signature.
* This must be initialized.
* \param s The second integer of the signature.
* This must be initialized.
*
* \return \c 0 on success.
* \return An \c MBEDTLS_ERR_ECP_XXX or \c MBEDTLS_MPI_XXX
* error code on failure.
*/
int mbedtls_ecdsa_verify(mbedtls_ecp_group *grp,
const unsigned char *buf, size_t blen,
const mbedtls_ecp_point *Q, const mbedtls_mpi *r,
const mbedtls_mpi *s);
#if !defined(MBEDTLS_ECDSA_VERIFY_ALT)
/**
* \brief This function verifies the ECDSA signature of a
* previously-hashed message, in a restartable manner
*
* \note If the bitlength of the message hash is larger than the
* bitlength of the group order, then the hash is truncated as
* defined in <em>Standards for Efficient Cryptography Group
* (SECG): SEC1 Elliptic Curve Cryptography</em>, section
* 4.1.4, step 3.
*
* \see ecp.h
*
* \param grp The ECP group to use.
* This must be initialized and have group parameters
* set, for example through mbedtls_ecp_group_load().
* \param buf The hashed content that was signed. This must be a readable
* buffer of length \p blen Bytes. It may be \c NULL if
* \p blen is zero.
* \param blen The length of \p buf in Bytes.
* \param Q The public key to use for verification. This must be
* initialized and setup.
* \param r The first integer of the signature.
* This must be initialized.
* \param s The second integer of the signature.
* This must be initialized.
* \param rs_ctx The restart context to use. This may be \c NULL to disable
* restarting. If it is not \c NULL, it must point to an
* initialized restart context.
*
* \return \c 0 on success.
* \return #MBEDTLS_ERR_ECP_IN_PROGRESS if maximum number of
* operations was reached: see \c mbedtls_ecp_set_max_ops().
* \return An \c MBEDTLS_ERR_ECP_XXX or \c MBEDTLS_MPI_XXX
* error code on failure.
*/
int mbedtls_ecdsa_verify_restartable(mbedtls_ecp_group *grp,
const unsigned char *buf, size_t blen,
const mbedtls_ecp_point *Q,
const mbedtls_mpi *r,
const mbedtls_mpi *s,
mbedtls_ecdsa_restart_ctx *rs_ctx);
#endif /* !MBEDTLS_ECDSA_VERIFY_ALT */
/**
* \brief This function computes the ECDSA signature and writes it
* to a buffer, serialized as defined in <em>RFC-4492:
* Elliptic Curve Cryptography (ECC) Cipher Suites for
* Transport Layer Security (TLS)</em>.
*
* \warning It is not thread-safe to use the same context in
* multiple threads.
*
* \note The deterministic version is used if
* #MBEDTLS_ECDSA_DETERMINISTIC is defined. For more
* information, see <em>RFC-6979: Deterministic Usage
* of the Digital Signature Algorithm (DSA) and Elliptic
* Curve Digital Signature Algorithm (ECDSA)</em>.
*
* \note If the bitlength of the message hash is larger than the
* bitlength of the group order, then the hash is truncated as
* defined in <em>Standards for Efficient Cryptography Group
* (SECG): SEC1 Elliptic Curve Cryptography</em>, section
* 4.1.3, step 5.
*
* \see ecp.h
*
* \param ctx The ECDSA context to use. This must be initialized
* and have a group and private key bound to it, for example
* via mbedtls_ecdsa_genkey() or mbedtls_ecdsa_from_keypair().
* \param md_alg The message digest that was used to hash the message.
* \param hash The message hash to be signed. This must be a readable
* buffer of length \p hlen Bytes.
* \param hlen The length of the hash \p hash in Bytes.
* \param sig The buffer to which to write the signature. This must be a
* writable buffer of length at least twice as large as the
* size of the curve used, plus 9. For example, 73 Bytes if
* a 256-bit curve is used. A buffer length of
* #MBEDTLS_ECDSA_MAX_LEN is always safe.
* \param sig_size The size of the \p sig buffer in bytes.
* \param slen The address at which to store the actual length of
* the signature written. Must not be \c NULL.
* \param f_rng The RNG function. This is used for blinding.
* If #MBEDTLS_ECDSA_DETERMINISTIC is unset, this is also
* used to generate the ECDSA nonce.
* This must not be \c NULL.
* \param p_rng The RNG context to be passed to \p f_rng. This may be
* \c NULL if \p f_rng is \c NULL or doesn't use a context.
*
* \return \c 0 on success.
* \return An \c MBEDTLS_ERR_ECP_XXX, \c MBEDTLS_ERR_MPI_XXX or
* \c MBEDTLS_ERR_ASN1_XXX error code on failure.
*/
int mbedtls_ecdsa_write_signature(mbedtls_ecdsa_context *ctx,
mbedtls_md_type_t md_alg,
const unsigned char *hash, size_t hlen,
unsigned char *sig, size_t sig_size, size_t *slen,
mbedtls_f_rng_t *f_rng,
void *p_rng);
/**
* \brief This function computes the ECDSA signature and writes it
* to a buffer, in a restartable way.
*
* \see \c mbedtls_ecdsa_write_signature()
*
* \note This function is like \c mbedtls_ecdsa_write_signature()
* but it can return early and restart according to the limit
* set with \c mbedtls_ecp_set_max_ops() to reduce blocking.
*
* \param ctx The ECDSA context to use. This must be initialized
* and have a group and private key bound to it, for example
* via mbedtls_ecdsa_genkey() or mbedtls_ecdsa_from_keypair().
* \param md_alg The message digest that was used to hash the message.
* \param hash The message hash to be signed. This must be a readable
* buffer of length \p hlen Bytes.
* \param hlen The length of the hash \p hash in Bytes.
* \param sig The buffer to which to write the signature. This must be a
* writable buffer of length at least twice as large as the
* size of the curve used, plus 9. For example, 73 Bytes if
* a 256-bit curve is used. A buffer length of
* #MBEDTLS_ECDSA_MAX_LEN is always safe.
* \param sig_size The size of the \p sig buffer in bytes.
* \param slen The address at which to store the actual length of
* the signature written. Must not be \c NULL.
* \param f_rng The RNG function. This is used for blinding.
* If #MBEDTLS_ECDSA_DETERMINISTIC is unset, this is also
* used to generate the ECDSA nonce.
* This must not be \c NULL.
* \param p_rng The RNG context to be passed to \p f_rng. This may be
* \c NULL if \p f_rng is \c NULL or doesn't use a context.
* \param rs_ctx The restart context to use. This may be \c NULL to disable
* restarting. If it is not \c NULL, it must point to an
* initialized restart context.
*
* \return \c 0 on success.
* \return #MBEDTLS_ERR_ECP_IN_PROGRESS if maximum number of
* operations was reached: see \c mbedtls_ecp_set_max_ops().
* \return Another \c MBEDTLS_ERR_ECP_XXX, \c MBEDTLS_ERR_MPI_XXX or
* \c MBEDTLS_ERR_ASN1_XXX error code on failure.
*/
int mbedtls_ecdsa_write_signature_restartable(mbedtls_ecdsa_context *ctx,
mbedtls_md_type_t md_alg,
const unsigned char *hash, size_t hlen,
unsigned char *sig, size_t sig_size, size_t *slen,
mbedtls_f_rng_t *f_rng,
void *p_rng,
mbedtls_ecdsa_restart_ctx *rs_ctx);
/**
* \brief This function reads and verifies an ECDSA signature.
*
* \note If the bitlength of the message hash is larger than the
* bitlength of the group order, then the hash is truncated as
* defined in <em>Standards for Efficient Cryptography Group
* (SECG): SEC1 Elliptic Curve Cryptography</em>, section
* 4.1.4, step 3.
*
* \see ecp.h
*
* \param ctx The ECDSA context to use. This must be initialized
* and have a group and public key bound to it.
* \param hash The message hash that was signed. This must be a readable
* buffer of length \p hlen Bytes.
* \param hlen The size of the hash \p hash.
* \param sig The signature to read and verify. This must be a readable
* buffer of length \p slen Bytes.
* \param slen The size of \p sig in Bytes.
*
* \return \c 0 on success.
* \return #MBEDTLS_ERR_ECP_BAD_INPUT_DATA if signature is invalid.
* \return #MBEDTLS_ERR_ECP_SIG_LEN_MISMATCH if there is a valid
* signature in \p sig, but its length is less than \p siglen.
* \return An \c MBEDTLS_ERR_ECP_XXX or \c MBEDTLS_ERR_MPI_XXX
* error code on failure for any other reason.
*/
int mbedtls_ecdsa_read_signature(mbedtls_ecdsa_context *ctx,
const unsigned char *hash, size_t hlen,
const unsigned char *sig, size_t slen);
/**
* \brief This function reads and verifies an ECDSA signature,
* in a restartable way.
*
* \see \c mbedtls_ecdsa_read_signature()
*
* \note This function is like \c mbedtls_ecdsa_read_signature()
* but it can return early and restart according to the limit
* set with \c mbedtls_ecp_set_max_ops() to reduce blocking.
*
* \param ctx The ECDSA context to use. This must be initialized
* and have a group and public key bound to it.
* \param hash The message hash that was signed. This must be a readable
* buffer of length \p hlen Bytes.
* \param hlen The size of the hash \p hash.
* \param sig The signature to read and verify. This must be a readable
* buffer of length \p slen Bytes.
* \param slen The size of \p sig in Bytes.
* \param rs_ctx The restart context to use. This may be \c NULL to disable
* restarting. If it is not \c NULL, it must point to an
* initialized restart context.
*
* \return \c 0 on success.
* \return #MBEDTLS_ERR_ECP_BAD_INPUT_DATA if signature is invalid.
* \return #MBEDTLS_ERR_ECP_SIG_LEN_MISMATCH if there is a valid
* signature in \p sig, but its length is less than \p siglen.
* \return #MBEDTLS_ERR_ECP_IN_PROGRESS if maximum number of
* operations was reached: see \c mbedtls_ecp_set_max_ops().
* \return Another \c MBEDTLS_ERR_ECP_XXX or \c MBEDTLS_ERR_MPI_XXX
* error code on failure for any other reason.
*/
int mbedtls_ecdsa_read_signature_restartable(mbedtls_ecdsa_context *ctx,
const unsigned char *hash, size_t hlen,
const unsigned char *sig, size_t slen,
mbedtls_ecdsa_restart_ctx *rs_ctx);
/**
* \brief This function generates an ECDSA keypair on the given curve.
*
* \see ecp.h
*
* \param ctx The ECDSA context to store the keypair in.
* This must be initialized.
* \param gid The elliptic curve to use. One of the various
* \c MBEDTLS_ECP_DP_XXX macros depending on configuration.
* \param f_rng The RNG function to use. This must not be \c NULL.
* \param p_rng The RNG context to be passed to \p f_rng. This may be
* \c NULL if \p f_rng doesn't need a context argument.
*
* \return \c 0 on success.
* \return An \c MBEDTLS_ERR_ECP_XXX code on failure.
*/
int mbedtls_ecdsa_genkey(mbedtls_ecdsa_context *ctx, mbedtls_ecp_group_id gid,
mbedtls_f_rng_t *f_rng, void *p_rng);
/**
* \brief This function sets up an ECDSA context from an EC key pair.
*
* \see ecp.h
*
* \param ctx The ECDSA context to setup. This must be initialized.
* \param key The EC key to use. This must be initialized and hold
* a private-public key pair or a public key. In the former
* case, the ECDSA context may be used for signature creation
* and verification after this call. In the latter case, it
* may be used for signature verification.
*
* \return \c 0 on success.
* \return An \c MBEDTLS_ERR_ECP_XXX code on failure.
*/
int mbedtls_ecdsa_from_keypair(mbedtls_ecdsa_context *ctx,
const mbedtls_ecp_keypair *key);
/**
* \brief This function initializes an ECDSA context.
*
* \param ctx The ECDSA context to initialize.
* This must not be \c NULL.
*/
void mbedtls_ecdsa_init(mbedtls_ecdsa_context *ctx);
/**
* \brief This function frees an ECDSA context.
*
* \param ctx The ECDSA context to free. This may be \c NULL,
* in which case this function does nothing. If it
* is not \c NULL, it must be initialized.
*/
void mbedtls_ecdsa_free(mbedtls_ecdsa_context *ctx);
#if defined(MBEDTLS_ECP_RESTARTABLE)
/**
* \brief Initialize a restart context.
*
* \param ctx The restart context to initialize.
* This must not be \c NULL.
*/
void mbedtls_ecdsa_restart_init(mbedtls_ecdsa_restart_ctx *ctx);
/**
* \brief Free the components of a restart context.
*
* \param ctx The restart context to free. This may be \c NULL,
* in which case this function does nothing. If it
* is not \c NULL, it must be initialized.
*/
void mbedtls_ecdsa_restart_free(mbedtls_ecdsa_restart_ctx *ctx);
#endif /* MBEDTLS_ECP_RESTARTABLE */
#ifdef __cplusplus
}
#endif
#endif /* ecdsa.h */
+298
View File
@@ -0,0 +1,298 @@
/**
* \file ecjpake.h
*
* \brief Elliptic curve J-PAKE
*/
/*
* Copyright The Mbed TLS Contributors
* SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
*/
#ifndef MBEDTLS_ECJPAKE_H
#define MBEDTLS_ECJPAKE_H
#include "mbedtls/private_access.h"
/*
* J-PAKE is a password-authenticated key exchange that allows deriving a
* strong shared secret from a (potentially low entropy) pre-shared
* passphrase, with forward secrecy and mutual authentication.
* https://en.wikipedia.org/wiki/Password_Authenticated_Key_Exchange_by_Juggling
*
* This file implements the Elliptic Curve variant of J-PAKE,
* as defined in Chapter 7.4 of the Thread v1.0 Specification,
* available to members of the Thread Group http://threadgroup.org/
*
* As the J-PAKE algorithm is inherently symmetric, so is our API.
* Each party needs to send its first round message, in any order, to the
* other party, then each sends its second round message, in any order.
* The payloads are serialized in a way suitable for use in TLS, but could
* also be use outside TLS.
*/
#include "mbedtls/build_info.h"
#include "mbedtls/ecp.h"
#include "mbedtls/md.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* Roles in the EC J-PAKE exchange
*/
typedef enum {
MBEDTLS_ECJPAKE_CLIENT = 0, /**< Client */
MBEDTLS_ECJPAKE_SERVER, /**< Server */
MBEDTLS_ECJPAKE_NONE, /**< Undefined */
} mbedtls_ecjpake_role;
#if !defined(MBEDTLS_ECJPAKE_ALT)
/**
* EC J-PAKE context structure.
*
* J-PAKE is a symmetric protocol, except for the identifiers used in
* Zero-Knowledge Proofs, and the serialization of the second message
* (KeyExchange) as defined by the Thread spec.
*
* In order to benefit from this symmetry, we choose a different naming
* convention from the Thread v1.0 spec. Correspondence is indicated in the
* description as a pair C: client name, S: server name
*/
typedef struct mbedtls_ecjpake_context {
mbedtls_md_type_t MBEDTLS_PRIVATE(md_type); /**< Hash to use */
mbedtls_ecp_group MBEDTLS_PRIVATE(grp); /**< Elliptic curve */
mbedtls_ecjpake_role MBEDTLS_PRIVATE(role); /**< Are we client or server? */
int MBEDTLS_PRIVATE(point_format); /**< Format for point export */
mbedtls_ecp_point MBEDTLS_PRIVATE(Xm1); /**< My public key 1 C: X1, S: X3 */
mbedtls_ecp_point MBEDTLS_PRIVATE(Xm2); /**< My public key 2 C: X2, S: X4 */
mbedtls_ecp_point MBEDTLS_PRIVATE(Xp1); /**< Peer public key 1 C: X3, S: X1 */
mbedtls_ecp_point MBEDTLS_PRIVATE(Xp2); /**< Peer public key 2 C: X4, S: X2 */
mbedtls_ecp_point MBEDTLS_PRIVATE(Xp); /**< Peer public key C: Xs, S: Xc */
mbedtls_mpi MBEDTLS_PRIVATE(xm1); /**< My private key 1 C: x1, S: x3 */
mbedtls_mpi MBEDTLS_PRIVATE(xm2); /**< My private key 2 C: x2, S: x4 */
mbedtls_mpi MBEDTLS_PRIVATE(s); /**< Pre-shared secret (passphrase) */
} mbedtls_ecjpake_context;
#else /* MBEDTLS_ECJPAKE_ALT */
#include "ecjpake_alt.h"
#endif /* MBEDTLS_ECJPAKE_ALT */
/**
* \brief Initialize an ECJPAKE context.
*
* \param ctx The ECJPAKE context to initialize.
* This must not be \c NULL.
*/
void mbedtls_ecjpake_init(mbedtls_ecjpake_context *ctx);
/**
* \brief Set up an ECJPAKE context for use.
*
* \note Currently the only values for hash/curve allowed by the
* standard are #MBEDTLS_MD_SHA256/#MBEDTLS_ECP_DP_SECP256R1.
*
* \param ctx The ECJPAKE context to set up. This must be initialized.
* \param role The role of the caller. This must be either
* #MBEDTLS_ECJPAKE_CLIENT or #MBEDTLS_ECJPAKE_SERVER.
* \param hash The identifier of the hash function to use,
* for example #MBEDTLS_MD_SHA256.
* \param curve The identifier of the elliptic curve to use,
* for example #MBEDTLS_ECP_DP_SECP256R1.
* \param secret The pre-shared secret (passphrase). This must be
* a readable not empty buffer of length \p len Bytes. It need
* only be valid for the duration of this call.
* \param len The length of the pre-shared secret \p secret.
*
* \return \c 0 if successful.
* \return A negative error code on failure.
*/
int mbedtls_ecjpake_setup(mbedtls_ecjpake_context *ctx,
mbedtls_ecjpake_role role,
mbedtls_md_type_t hash,
mbedtls_ecp_group_id curve,
const unsigned char *secret,
size_t len);
/**
* \brief Set the point format for future reads and writes.
*
* \param ctx The ECJPAKE context to configure.
* \param point_format The point format to use:
* #MBEDTLS_ECP_PF_UNCOMPRESSED (default)
* or #MBEDTLS_ECP_PF_COMPRESSED.
*
* \return \c 0 if successful.
* \return #MBEDTLS_ERR_ECP_BAD_INPUT_DATA if \p point_format
* is invalid.
*/
int mbedtls_ecjpake_set_point_format(mbedtls_ecjpake_context *ctx,
int point_format);
/**
* \brief Check if an ECJPAKE context is ready for use.
*
* \param ctx The ECJPAKE context to check. This must be
* initialized.
*
* \return \c 0 if the context is ready for use.
* \return #MBEDTLS_ERR_ECP_BAD_INPUT_DATA otherwise.
*/
int mbedtls_ecjpake_check(const mbedtls_ecjpake_context *ctx);
/**
* \brief Generate and write the first round message
* (TLS: contents of the Client/ServerHello extension,
* excluding extension type and length bytes).
*
* \param ctx The ECJPAKE context to use. This must be
* initialized and set up.
* \param buf The buffer to write the contents to. This must be a
* writable buffer of length \p len Bytes.
* \param len The length of \p buf in Bytes.
* \param olen The address at which to store the total number
* of Bytes written to \p buf. This must not be \c NULL.
* \param f_rng The RNG function to use. This must not be \c NULL.
* \param p_rng The RNG parameter to be passed to \p f_rng. This
* may be \c NULL if \p f_rng doesn't use a context.
*
* \return \c 0 if successful.
* \return A negative error code on failure.
*/
int mbedtls_ecjpake_write_round_one(mbedtls_ecjpake_context *ctx,
unsigned char *buf, size_t len, size_t *olen,
mbedtls_f_rng_t *f_rng,
void *p_rng);
/**
* \brief Read and process the first round message
* (TLS: contents of the Client/ServerHello extension,
* excluding extension type and length bytes).
*
* \param ctx The ECJPAKE context to use. This must be initialized
* and set up.
* \param buf The buffer holding the first round message. This must
* be a readable buffer of length \p len Bytes.
* \param len The length in Bytes of \p buf.
*
* \return \c 0 if successful.
* \return A negative error code on failure.
*/
int mbedtls_ecjpake_read_round_one(mbedtls_ecjpake_context *ctx,
const unsigned char *buf,
size_t len);
/**
* \brief Generate and write the second round message
* (TLS: contents of the Client/ServerKeyExchange).
*
* \param ctx The ECJPAKE context to use. This must be initialized,
* set up, and already have performed round one.
* \param buf The buffer to write the round two contents to.
* This must be a writable buffer of length \p len Bytes.
* \param len The size of \p buf in Bytes.
* \param olen The address at which to store the total number of Bytes
* written to \p buf. This must not be \c NULL.
* \param f_rng The RNG function to use. This must not be \c NULL.
* \param p_rng The RNG parameter to be passed to \p f_rng. This
* may be \c NULL if \p f_rng doesn't use a context.
*
* \return \c 0 if successful.
* \return A negative error code on failure.
*/
int mbedtls_ecjpake_write_round_two(mbedtls_ecjpake_context *ctx,
unsigned char *buf, size_t len, size_t *olen,
mbedtls_f_rng_t *f_rng,
void *p_rng);
/**
* \brief Read and process the second round message
* (TLS: contents of the Client/ServerKeyExchange).
*
* \param ctx The ECJPAKE context to use. This must be initialized
* and set up and already have performed round one.
* \param buf The buffer holding the second round message. This must
* be a readable buffer of length \p len Bytes.
* \param len The length in Bytes of \p buf.
*
* \return \c 0 if successful.
* \return A negative error code on failure.
*/
int mbedtls_ecjpake_read_round_two(mbedtls_ecjpake_context *ctx,
const unsigned char *buf,
size_t len);
/**
* \brief Derive the shared secret
* (TLS: Pre-Master Secret).
*
* \param ctx The ECJPAKE context to use. This must be initialized,
* set up and have performed both round one and two.
* \param buf The buffer to write the derived secret to. This must
* be a writable buffer of length \p len Bytes.
* \param len The length of \p buf in Bytes.
* \param olen The address at which to store the total number of Bytes
* written to \p buf. This must not be \c NULL.
* \param f_rng The RNG function to use. This must not be \c NULL.
* \param p_rng The RNG parameter to be passed to \p f_rng. This
* may be \c NULL if \p f_rng doesn't use a context.
*
* \return \c 0 if successful.
* \return A negative error code on failure.
*/
int mbedtls_ecjpake_derive_secret(mbedtls_ecjpake_context *ctx,
unsigned char *buf, size_t len, size_t *olen,
mbedtls_f_rng_t *f_rng,
void *p_rng);
/**
* \brief Write the shared key material to be passed to a Key
* Derivation Function as described in RFC8236.
*
* \param ctx The ECJPAKE context to use. This must be initialized,
* set up and have performed both round one and two.
* \param buf The buffer to write the derived secret to. This must
* be a writable buffer of length \p len Bytes.
* \param len The length of \p buf in Bytes.
* \param olen The address at which to store the total number of bytes
* written to \p buf. This must not be \c NULL.
* \param f_rng The RNG function to use. This must not be \c NULL.
* \param p_rng The RNG parameter to be passed to \p f_rng. This
* may be \c NULL if \p f_rng doesn't use a context.
*
* \return \c 0 if successful.
* \return A negative error code on failure.
*/
int mbedtls_ecjpake_write_shared_key(mbedtls_ecjpake_context *ctx,
unsigned char *buf, size_t len, size_t *olen,
mbedtls_f_rng_t *f_rng,
void *p_rng);
/**
* \brief This clears an ECJPAKE context and frees any
* embedded data structure.
*
* \param ctx The ECJPAKE context to free. This may be \c NULL,
* in which case this function does nothing. If it is not
* \c NULL, it must point to an initialized ECJPAKE context.
*/
void mbedtls_ecjpake_free(mbedtls_ecjpake_context *ctx);
#if defined(MBEDTLS_SELF_TEST)
/**
* \brief Checkup routine
*
* \return 0 if successful, or 1 if a test failed
*/
int mbedtls_ecjpake_self_test(int verbose);
#endif /* MBEDTLS_SELF_TEST */
#ifdef __cplusplus
}
#endif
#endif /* ecjpake.h */
File diff suppressed because it is too large. Load diff
+274
View File
@@ -0,0 +1,274 @@
/**
* \file entropy.h
*
* \brief Entropy accumulator implementation
*/
/*
* Copyright The Mbed TLS Contributors
* SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
*/
#ifndef MBEDTLS_ENTROPY_H
#define MBEDTLS_ENTROPY_H
#include "mbedtls/private_access.h"
#include "mbedtls/build_info.h"
#include <stddef.h>
#include "md.h"
#if (defined(MBEDTLS_MD_CAN_SHA512) || defined(PSA_WANT_ALG_SHA_512)) && \
!defined(MBEDTLS_ENTROPY_FORCE_SHA256)
#define MBEDTLS_ENTROPY_SHA512_ACCUMULATOR
#define MBEDTLS_ENTROPY_MD MBEDTLS_MD_SHA512
#define MBEDTLS_ENTROPY_BLOCK_SIZE 64 /**< Block size of entropy accumulator (SHA-512) */
#else
#if (defined(MBEDTLS_MD_CAN_SHA256) || defined(PSA_WANT_ALG_SHA_256))
#define MBEDTLS_ENTROPY_SHA256_ACCUMULATOR
#define MBEDTLS_ENTROPY_MD MBEDTLS_MD_SHA256
#define MBEDTLS_ENTROPY_BLOCK_SIZE 32 /**< Block size of entropy accumulator (SHA-256) */
#endif
#endif
#if defined(MBEDTLS_THREADING_C)
#include "mbedtls/threading.h"
#endif
/** Critical entropy source failure. */
#define MBEDTLS_ERR_ENTROPY_SOURCE_FAILED -0x003C
/** No more sources can be added. */
#define MBEDTLS_ERR_ENTROPY_MAX_SOURCES -0x003E
/** No sources have been added to poll. */
#define MBEDTLS_ERR_ENTROPY_NO_SOURCES_DEFINED -0x0040
/** No strong sources have been added to poll. */
#define MBEDTLS_ERR_ENTROPY_NO_STRONG_SOURCE -0x003D
/** Read/write error in file. */
#define MBEDTLS_ERR_ENTROPY_FILE_IO_ERROR -0x003F
/**
* \name SECTION: Module settings
*
* The configuration options you can set for this module are in this section.
* Either change them in mbedtls_config.h or define them on the compiler command line.
* \{
*/
#if !defined(MBEDTLS_ENTROPY_MAX_SOURCES)
#define MBEDTLS_ENTROPY_MAX_SOURCES 20 /**< Maximum number of sources supported */
#endif
#if !defined(MBEDTLS_ENTROPY_MAX_GATHER)
#define MBEDTLS_ENTROPY_MAX_GATHER 128 /**< Maximum amount requested from entropy sources */
#endif
/** \} name SECTION: Module settings */
#define MBEDTLS_ENTROPY_MAX_SEED_SIZE 1024 /**< Maximum size of seed we read from seed file */
#define MBEDTLS_ENTROPY_SOURCE_MANUAL MBEDTLS_ENTROPY_MAX_SOURCES
#define MBEDTLS_ENTROPY_SOURCE_STRONG 1 /**< Entropy source is strong */
#define MBEDTLS_ENTROPY_SOURCE_WEAK 0 /**< Entropy source is weak */
#ifdef __cplusplus
extern "C" {
#endif
/**
* \brief Entropy poll callback pointer
*
* \param data Callback-specific data pointer
* \param output Data to fill
* \param len Maximum size to provide
* \param olen The actual amount of bytes put into the buffer (Can be 0)
*
* \return 0 if no critical failures occurred,
* MBEDTLS_ERR_ENTROPY_SOURCE_FAILED otherwise
*/
typedef int (*mbedtls_entropy_f_source_ptr)(void *data, unsigned char *output, size_t len,
size_t *olen);
/**
* \brief Entropy source state
*/
typedef struct mbedtls_entropy_source_state {
mbedtls_entropy_f_source_ptr MBEDTLS_PRIVATE(f_source); /**< The entropy source callback */
void *MBEDTLS_PRIVATE(p_source); /**< The callback data pointer */
size_t MBEDTLS_PRIVATE(size); /**< Amount received in bytes */
size_t MBEDTLS_PRIVATE(threshold); /**< Minimum bytes required before release */
int MBEDTLS_PRIVATE(strong); /**< Is the source strong? */
}
mbedtls_entropy_source_state;
/**
* \brief Entropy context structure
*/
typedef struct mbedtls_entropy_context {
mbedtls_md_context_t MBEDTLS_PRIVATE(accumulator);
int MBEDTLS_PRIVATE(accumulator_started); /* 0 after init.
* 1 after the first update.
* -1 after free. */
int MBEDTLS_PRIVATE(source_count); /* Number of entries used in source. */
mbedtls_entropy_source_state MBEDTLS_PRIVATE(source)[MBEDTLS_ENTROPY_MAX_SOURCES];
#if defined(MBEDTLS_THREADING_C)
mbedtls_threading_mutex_t MBEDTLS_PRIVATE(mutex); /*!< mutex */
#endif
#if defined(MBEDTLS_ENTROPY_NV_SEED)
int MBEDTLS_PRIVATE(initial_entropy_run);
#endif
}
mbedtls_entropy_context;
#if !defined(MBEDTLS_NO_PLATFORM_ENTROPY)
/**
* \brief Platform-specific entropy poll callback
*/
int mbedtls_platform_entropy_poll(void *data,
unsigned char *output, size_t len, size_t *olen);
#endif
/**
* \brief Initialize the context
*
* \param ctx Entropy context to initialize
*/
void mbedtls_entropy_init(mbedtls_entropy_context *ctx);
/**
* \brief Free the data in the context
*
* \param ctx Entropy context to free
*/
void mbedtls_entropy_free(mbedtls_entropy_context *ctx);
/**
* \brief Adds an entropy source to poll
* (Thread-safe if MBEDTLS_THREADING_C is enabled)
*
* \param ctx Entropy context
* \param f_source Entropy function
* \param p_source Function data
* \param threshold Minimum required from source before entropy is released
* ( with mbedtls_entropy_func() ) (in bytes)
* \param strong MBEDTLS_ENTROPY_SOURCE_STRONG or
* MBEDTLS_ENTROPY_SOURCE_WEAK.
* At least one strong source needs to be added.
* Weaker sources (such as the cycle counter) can be used as
* a complement.
*
* \return 0 if successful or MBEDTLS_ERR_ENTROPY_MAX_SOURCES
*/
int mbedtls_entropy_add_source(mbedtls_entropy_context *ctx,
mbedtls_entropy_f_source_ptr f_source, void *p_source,
size_t threshold, int strong);
/**
* \brief Trigger an extra gather poll for the accumulator
* (Thread-safe if MBEDTLS_THREADING_C is enabled)
*
* \param ctx Entropy context
*
* \return 0 if successful, or MBEDTLS_ERR_ENTROPY_SOURCE_FAILED
*/
int mbedtls_entropy_gather(mbedtls_entropy_context *ctx);
/**
* \brief Retrieve entropy from the accumulator
* (Maximum length: MBEDTLS_ENTROPY_BLOCK_SIZE)
* (Thread-safe if MBEDTLS_THREADING_C is enabled)
*
* \param data Entropy context
* \param output Buffer to fill
* \param len Number of bytes desired, must be at most MBEDTLS_ENTROPY_BLOCK_SIZE
*
* \return 0 if successful, or MBEDTLS_ERR_ENTROPY_SOURCE_FAILED
*/
int mbedtls_entropy_func(void *data, unsigned char *output, size_t len);
/**
* \brief Add data to the accumulator manually
* (Thread-safe if MBEDTLS_THREADING_C is enabled)
*
* \param ctx Entropy context
* \param data Data to add
* \param len Length of data
*
* \return 0 if successful
*/
int mbedtls_entropy_update_manual(mbedtls_entropy_context *ctx,
const unsigned char *data, size_t len);
#if defined(MBEDTLS_ENTROPY_NV_SEED)
/**
* \brief Trigger an update of the seed file in NV by using the
* current entropy pool.
*
* \param ctx Entropy context
*
* \return 0 if successful
*/
int mbedtls_entropy_update_nv_seed(mbedtls_entropy_context *ctx);
#endif /* MBEDTLS_ENTROPY_NV_SEED */
#if defined(MBEDTLS_FS_IO)
/**
* \brief Write a seed file
*
* \param ctx Entropy context
* \param path Name of the file
*
* \return 0 if successful,
* MBEDTLS_ERR_ENTROPY_FILE_IO_ERROR on file error, or
* MBEDTLS_ERR_ENTROPY_SOURCE_FAILED
*/
int mbedtls_entropy_write_seed_file(mbedtls_entropy_context *ctx, const char *path);
/**
* \brief Read and update a seed file. Seed is added to this
* instance. No more than MBEDTLS_ENTROPY_MAX_SEED_SIZE bytes are
* read from the seed file. The rest is ignored.
*
* \param ctx Entropy context
* \param path Name of the file
*
* \return 0 if successful,
* MBEDTLS_ERR_ENTROPY_FILE_IO_ERROR on file error,
* MBEDTLS_ERR_ENTROPY_SOURCE_FAILED
*/
int mbedtls_entropy_update_seed_file(mbedtls_entropy_context *ctx, const char *path);
#endif /* MBEDTLS_FS_IO */
#if defined(MBEDTLS_SELF_TEST)
/**
* \brief Checkup routine
*
* This module self-test also calls the entropy self-test,
* mbedtls_entropy_source_self_test();
*
* \return 0 if successful, or 1 if a test failed
*/
int mbedtls_entropy_self_test(int verbose);
#if defined(MBEDTLS_ENTROPY_HARDWARE_ALT)
/**
* \brief Checkup routine
*
* Verifies the integrity of the hardware entropy source
* provided by the function 'mbedtls_hardware_poll()'.
*
* Note this is the only hardware entropy source that is known
* at link time, and other entropy sources configured
* dynamically at runtime by the function
* mbedtls_entropy_add_source() will not be tested.
*
* \return 0 if successful, or 1 if a test failed
*/
int mbedtls_entropy_source_self_test(int verbose);
#endif /* MBEDTLS_ENTROPY_HARDWARE_ALT */
#endif /* MBEDTLS_SELF_TEST */
#ifdef __cplusplus
}
#endif
#endif /* entropy.h */
+201
View File
@@ -0,0 +1,201 @@
/**
* \file error.h
*
* \brief Error to string translation
*/
/*
* Copyright The Mbed TLS Contributors
* SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
*/
#ifndef MBEDTLS_ERROR_H
#define MBEDTLS_ERROR_H
#include "mbedtls/build_info.h"
#include <stddef.h>
/**
* Error code layout.
*
* Currently we try to keep all error codes within the negative space of 16
* bits signed integers to support all platforms (-0x0001 - -0x7FFF). In
* addition we'd like to give two layers of information on the error if
* possible.
*
* For that purpose the error codes are segmented in the following manner:
*
* 16 bit error code bit-segmentation
*
* 1 bit - Unused (sign bit)
* 3 bits - High level module ID
* 5 bits - Module-dependent error code
* 7 bits - Low level module errors
*
* For historical reasons, low-level error codes are divided in even and odd,
* even codes were assigned first, and -1 is reserved for other errors.
*
* Low-level module errors (0x0002-0x007E, 0x0001-0x007F)
*
* Module Nr Codes assigned
* ERROR 2 0x006E 0x0001
* MPI 7 0x0002-0x0010
* GCM 3 0x0012-0x0016 0x0013-0x0013
* THREADING 3 0x001A-0x001E
* AES 5 0x0020-0x0022 0x0021-0x0025
* CAMELLIA 3 0x0024-0x0026 0x0027-0x0027
* BASE64 2 0x002A-0x002C
* OID 1 0x002E-0x002E 0x000B-0x000B
* PADLOCK 1 0x0030-0x0030
* DES 2 0x0032-0x0032 0x0033-0x0033
* CTR_DBRG 4 0x0034-0x003A
* ENTROPY 3 0x003C-0x0040 0x003D-0x003F
* NET 13 0x0042-0x0052 0x0043-0x0049
* ARIA 4 0x0058-0x005E
* ASN1 7 0x0060-0x006C
* CMAC 1 0x007A-0x007A
* PBKDF2 1 0x007C-0x007C
* HMAC_DRBG 4 0x0003-0x0009
* CCM 3 0x000D-0x0011
* MD5 1 0x002F-0x002F
* RIPEMD160 1 0x0031-0x0031
* SHA1 1 0x0035-0x0035 0x0073-0x0073
* SHA256 1 0x0037-0x0037 0x0074-0x0074
* SHA512 1 0x0039-0x0039 0x0075-0x0075
* SHA-3 1 0x0076-0x0076
* CHACHA20 3 0x0051-0x0055
* POLY1305 3 0x0057-0x005B
* CHACHAPOLY 2 0x0054-0x0056
* PLATFORM 2 0x0070-0x0072
* LMS 5 0x0011-0x0019
*
* High-level module nr (3 bits - 0x0...-0x7...)
* Name ID Nr of Errors
* PEM 1 9
* PKCS#12 1 4 (Started from top)
* X509 2 20
* PKCS5 2 4 (Started from top)
* DHM 3 11
* PK 3 15 (Started from top)
* RSA 4 11
* ECP 4 10 (Started from top)
* MD 5 5
* HKDF 5 1 (Started from top)
* PKCS7 5 12 (Started from 0x5300)
* SSL 5 3 (Started from 0x5F00)
* CIPHER 6 8 (Started from 0x6080)
* SSL 6 22 (Started from top, plus 0x6000)
* SSL 7 20 (Started from 0x7000, gaps at
* 0x7380, 0x7900-0x7980, 0x7A80-0x7E80)
*
* Module dependent error code (5 bits 0x.00.-0x.F8.)
*/
#ifdef __cplusplus
extern "C" {
#endif
/** Generic error */
#define MBEDTLS_ERR_ERROR_GENERIC_ERROR -0x0001
/** This is a bug in the library */
#define MBEDTLS_ERR_ERROR_CORRUPTION_DETECTED -0x006E
/** Hardware accelerator failed */
#define MBEDTLS_ERR_PLATFORM_HW_ACCEL_FAILED -0x0070
/** The requested feature is not supported by the platform */
#define MBEDTLS_ERR_PLATFORM_FEATURE_UNSUPPORTED -0x0072
/**
* \brief Combines a high-level and low-level error code together.
*
* Wrapper macro for mbedtls_error_add(). See that function for
* more details.
*/
#define MBEDTLS_ERROR_ADD(high, low) \
mbedtls_error_add(high, low, __FILE__, __LINE__)
#if defined(MBEDTLS_TEST_HOOKS)
/**
* \brief Testing hook called before adding/combining two error codes together.
* Only used when invasive testing is enabled via MBEDTLS_TEST_HOOKS.
*/
extern void (*mbedtls_test_hook_error_add)(int, int, const char *, int);
#endif
/**
* \brief Combines a high-level and low-level error code together.
*
* This function can be called directly however it is usually
* called via the #MBEDTLS_ERROR_ADD macro.
*
* While a value of zero is not a negative error code, it is still an
* error code (that denotes success) and can be combined with both a
* negative error code or another value of zero.
*
* \note When invasive testing is enabled via #MBEDTLS_TEST_HOOKS, also try to
* call \link mbedtls_test_hook_error_add \endlink.
*
* \param high high-level error code. See error.h for more details.
* \param low low-level error code. See error.h for more details.
* \param file file where this error code addition occurred.
* \param line line where this error code addition occurred.
*/
static inline int mbedtls_error_add(int high, int low,
const char *file, int line)
{
#if defined(MBEDTLS_TEST_HOOKS)
if (*mbedtls_test_hook_error_add != NULL) {
(*mbedtls_test_hook_error_add)(high, low, file, line);
}
#endif
(void) file;
(void) line;
return high + low;
}
/**
* \brief Translate an Mbed TLS error code into a string representation.
* The result is truncated if necessary and always includes a
* terminating null byte.
*
* \param errnum error code
* \param buffer buffer to place representation in
* \param buflen length of the buffer
*/
void mbedtls_strerror(int errnum, char *buffer, size_t buflen);
/**
* \brief Translate the high-level part of an Mbed TLS error code into a string
* representation.
*
* This function returns a const pointer to an un-modifiable string. The caller
* must not try to modify the string. It is intended to be used mostly for
* logging purposes.
*
* \param error_code error code
*
* \return The string representation of the error code, or \c NULL if the error
* code is unknown.
*/
const char *mbedtls_high_level_strerr(int error_code);
/**
* \brief Translate the low-level part of an Mbed TLS error code into a string
* representation.
*
* This function returns a const pointer to an un-modifiable string. The caller
* must not try to modify the string. It is intended to be used mostly for
* logging purposes.
*
* \param error_code error code
*
* \return The string representation of the error code, or \c NULL if the error
* code is unknown.
*/
const char *mbedtls_low_level_strerr(int error_code);
#ifdef __cplusplus
}
#endif
#endif /* error.h */
+387
View File
@@ -0,0 +1,387 @@
/**
* \file gcm.h
*
* \brief This file contains GCM definitions and functions.
*
* The Galois/Counter Mode (GCM) for 128-bit block ciphers is defined
* in <em>D. McGrew, J. Viega, The Galois/Counter Mode of Operation
* (GCM), Natl. Inst. Stand. Technol.</em>
*
* For more information on GCM, see <em>NIST SP 800-38D: Recommendation for
* Block Cipher Modes of Operation: Galois/Counter Mode (GCM) and GMAC</em>.
*
*/
/*
* Copyright The Mbed TLS Contributors
* SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
*/
#ifndef MBEDTLS_GCM_H
#define MBEDTLS_GCM_H
#include "mbedtls/private_access.h"
#include "mbedtls/build_info.h"
#include "mbedtls/cipher.h"
#if defined(MBEDTLS_BLOCK_CIPHER_C)
#include "mbedtls/block_cipher.h"
#endif
#include <stdint.h>
#define MBEDTLS_GCM_ENCRYPT 1
#define MBEDTLS_GCM_DECRYPT 0
/** Authenticated decryption failed. */
#define MBEDTLS_ERR_GCM_AUTH_FAILED -0x0012
/** Bad input parameters to function. */
#define MBEDTLS_ERR_GCM_BAD_INPUT -0x0014
/** An output buffer is too small. */
#define MBEDTLS_ERR_GCM_BUFFER_TOO_SMALL -0x0016
#ifdef __cplusplus
extern "C" {
#endif
#if !defined(MBEDTLS_GCM_ALT)
#if defined(MBEDTLS_GCM_LARGE_TABLE)
#define MBEDTLS_GCM_HTABLE_SIZE 256
#else
#define MBEDTLS_GCM_HTABLE_SIZE 16
#endif
/**
* \brief The GCM context structure.
*/
typedef struct mbedtls_gcm_context {
#if defined(MBEDTLS_BLOCK_CIPHER_C)
mbedtls_block_cipher_context_t MBEDTLS_PRIVATE(block_cipher_ctx); /*!< The cipher context used. */
#else
mbedtls_cipher_context_t MBEDTLS_PRIVATE(cipher_ctx); /*!< The cipher context used. */
#endif
uint64_t MBEDTLS_PRIVATE(H)[MBEDTLS_GCM_HTABLE_SIZE][2]; /*!< Precalculated HTable. */
uint64_t MBEDTLS_PRIVATE(len); /*!< The total length of the encrypted data. */
uint64_t MBEDTLS_PRIVATE(add_len); /*!< The total length of the additional data. */
unsigned char MBEDTLS_PRIVATE(base_ectr)[16]; /*!< The first ECTR for tag. */
unsigned char MBEDTLS_PRIVATE(y)[16]; /*!< The Y working value. */
unsigned char MBEDTLS_PRIVATE(buf)[16]; /*!< The buf working value. */
unsigned char MBEDTLS_PRIVATE(mode); /*!< The operation to perform:
#MBEDTLS_GCM_ENCRYPT or
#MBEDTLS_GCM_DECRYPT. */
unsigned char MBEDTLS_PRIVATE(acceleration); /*!< The acceleration to use. */
}
mbedtls_gcm_context;
#else /* !MBEDTLS_GCM_ALT */
#include "gcm_alt.h"
#endif /* !MBEDTLS_GCM_ALT */
/**
* \brief This function initializes the specified GCM context,
* to make references valid, and prepares the context
* for mbedtls_gcm_setkey() or mbedtls_gcm_free().
*
* The function does not bind the GCM context to a particular
* cipher, nor set the key. For this purpose, use
* mbedtls_gcm_setkey().
*
* \param ctx The GCM context to initialize. This must not be \c NULL.
*/
void mbedtls_gcm_init(mbedtls_gcm_context *ctx);
/**
* \brief This function associates a GCM context with a
* cipher algorithm and a key.
*
* \param ctx The GCM context. This must be initialized.
* \param cipher The 128-bit block cipher to use.
* \param key The encryption key. This must be a readable buffer of at
* least \p keybits bits.
* \param keybits The key size 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 A cipher-specific error code on failure.
*/
int mbedtls_gcm_setkey(mbedtls_gcm_context *ctx,
mbedtls_cipher_id_t cipher,
const unsigned char *key,
unsigned int keybits);
/**
* \brief This function performs GCM encryption or decryption of a buffer.
*
* \note The output buffer \p output can be the same as the input
* buffer \p input. If \p output is greater than \p input, they
* cannot overlap.
*
* \warning When this function performs a decryption, it outputs the
* authentication tag and does not verify that the data is
* authentic. You should use this function to perform encryption
* only. For decryption, use mbedtls_gcm_auth_decrypt() instead.
*
* \param ctx The GCM context to use for encryption or decryption. This
* must be initialized.
* \param mode The operation to perform:
* - #MBEDTLS_GCM_ENCRYPT to perform authenticated encryption.
* The ciphertext is written to \p output and the
* authentication tag is written to \p tag.
* - #MBEDTLS_GCM_DECRYPT to perform decryption.
* The plaintext is written to \p output and the
* authentication tag is written to \p tag.
* Note that this mode is not recommended, because it does
* not verify the authenticity of the data. For this reason,
* you should use mbedtls_gcm_auth_decrypt() instead of
* calling this function in decryption mode.
* \param length The length of the input data, which is equal to the length
* of the output data.
* \param iv The initialization vector. This must be a readable buffer of
* at least \p iv_len Bytes.
* \param iv_len The length of the IV.
* \param add The buffer holding the additional data. This must be of at
* least that size in Bytes.
* \param add_len The length of the additional data.
* \param input The buffer holding the input data. If \p length is greater
* than zero, this must be a readable buffer of at least that
* size in Bytes.
* \param output The buffer for holding the output data. If \p length is greater
* than zero, this must be a writable buffer of at least that
* size in Bytes.
* \param tag_len The length of the tag to generate.
* \param tag The buffer for holding the tag. This must be a writable
* buffer of at least \p tag_len Bytes.
*
* \return \c 0 if the encryption or decryption was performed
* successfully. Note that in #MBEDTLS_GCM_DECRYPT mode,
* this does not indicate that the data is authentic.
* \return #MBEDTLS_ERR_GCM_BAD_INPUT if the lengths or pointers are
* not valid or a cipher-specific error code if the encryption
* or decryption failed.
*/
int mbedtls_gcm_crypt_and_tag(mbedtls_gcm_context *ctx,
int mode,
size_t length,
const unsigned char *iv,
size_t iv_len,
const unsigned char *add,
size_t add_len,
const unsigned char *input,
unsigned char *output,
size_t tag_len,
unsigned char *tag);
/**
* \brief This function performs a GCM authenticated decryption of a
* buffer.
*
* \note The output buffer \p output can be the same as the input
* buffer \p input. If \p output is greater than \p input, they
* cannot overlap. Implementations which require
* MBEDTLS_GCM_ALT to be enabled may not provide support for
* overlapping buffers.
*
* \param ctx The GCM context. This must be initialized.
* \param length The length of the ciphertext to decrypt, which is also
* the length of the decrypted plaintext.
* \param iv The initialization vector. This must be a readable buffer
* of at least \p iv_len Bytes.
* \param iv_len The length of the IV.
* \param add The buffer holding the additional data. This must be of at
* least that size in Bytes.
* \param add_len The length of the additional data.
* \param tag The buffer holding the tag to verify. This must be a
* readable buffer of at least \p tag_len Bytes.
* \param tag_len The length of the tag to verify.
* \param input The buffer holding the ciphertext. If \p length is greater
* than zero, this must be a readable buffer of at least that
* size.
* \param output The buffer for holding the decrypted plaintext. If \p length
* is greater than zero, this must be a writable buffer of at
* least that size.
*
* \return \c 0 if successful and authenticated.
* \return #MBEDTLS_ERR_GCM_AUTH_FAILED if the tag does not match.
* \return #MBEDTLS_ERR_GCM_BAD_INPUT if the lengths or pointers are
* not valid or a cipher-specific error code if the decryption
* failed.
*/
int mbedtls_gcm_auth_decrypt(mbedtls_gcm_context *ctx,
size_t length,
const unsigned char *iv,
size_t iv_len,
const unsigned char *add,
size_t add_len,
const unsigned char *tag,
size_t tag_len,
const unsigned char *input,
unsigned char *output);
/**
* \brief This function starts a GCM encryption or decryption
* operation.
*
* \param ctx The GCM context. This must be initialized.
* \param mode The operation to perform: #MBEDTLS_GCM_ENCRYPT or
* #MBEDTLS_GCM_DECRYPT.
* \param iv The initialization vector. This must be a readable buffer of
* at least \p iv_len Bytes.
* \param iv_len The length of the IV.
*
* \return \c 0 on success.
*/
int mbedtls_gcm_starts(mbedtls_gcm_context *ctx,
int mode,
const unsigned char *iv,
size_t iv_len);
/**
* \brief This function feeds an input buffer as associated data
* (authenticated but not encrypted data) in a GCM
* encryption or decryption operation.
*
* Call this function after mbedtls_gcm_starts() to pass
* the associated data. If the associated data is empty,
* you do not need to call this function. You may not
* call this function after calling mbedtls_cipher_update().
*
* \param ctx The GCM context. This must have been started with
* mbedtls_gcm_starts() and must not have yet received
* any input with mbedtls_gcm_update().
* \param add The buffer holding the additional data, or \c NULL
* if \p add_len is \c 0.
* \param add_len The length of the additional data. If \c 0,
* \p add may be \c NULL.
*
* \return \c 0 on success.
*/
int mbedtls_gcm_update_ad(mbedtls_gcm_context *ctx,
const unsigned char *add,
size_t add_len);
/**
* \brief This function feeds an input buffer into an ongoing GCM
* encryption or decryption operation.
*
* You may call this function zero, one or more times
* to pass successive parts of the input: the plaintext to
* encrypt, or the ciphertext (not including the tag) to
* decrypt. After the last part of the input, call
* mbedtls_gcm_finish().
*
* This function may produce output in one of the following
* ways:
* - Immediate output: the output length is always equal
* to the input length.
* - Buffered output: the output consists of a whole number
* of 16-byte blocks. If the total input length so far
* (not including associated data) is 16 \* *B* + *A*
* with *A* < 16 then the total output length is 16 \* *B*.
*
* In particular:
* - It is always correct to call this function with
* \p output_size >= \p input_length + 15.
* - If \p input_length is a multiple of 16 for all the calls
* to this function during an operation, then it is
* correct to use \p output_size = \p input_length.
*
* \note The output buffer \p output can be the same as the input
* buffer \p input. If \p output is greater than \p input, they
* cannot overlap. Implementations which require
* MBEDTLS_GCM_ALT to be enabled may not provide support for
* overlapping buffers.
*
* \param ctx The GCM context. This must be initialized.
* \param input The buffer holding the input data. If \p input_length
* is greater than zero, this must be a readable buffer
* of at least \p input_length bytes.
* \param input_length The length of the input data in bytes.
* \param output The buffer for the output data. If \p output_size
* is greater than zero, this must be a writable buffer of
* of at least \p output_size bytes.
* \param output_size The size of the output buffer in bytes.
* See the function description regarding the output size.
* \param output_length On success, \p *output_length contains the actual
* length of the output written in \p output.
* On failure, the content of \p *output_length is
* unspecified.
*
* \return \c 0 on success.
* \return #MBEDTLS_ERR_GCM_BAD_INPUT on failure:
* total input length too long,
* unsupported input/output buffer overlap detected,
* or \p output_size too small.
*/
int mbedtls_gcm_update(mbedtls_gcm_context *ctx,
const unsigned char *input, size_t input_length,
unsigned char *output, size_t output_size,
size_t *output_length);
/**
* \brief This function finishes the GCM operation and generates
* the authentication tag.
*
* It wraps up the GCM stream, and generates the
* tag. The tag can have a maximum length of 16 Bytes.
*
* \param ctx The GCM context. This must be initialized.
* \param tag The buffer for holding the tag. This must be a writable
* buffer of at least \p tag_len Bytes.
* \param tag_len The length of the tag to generate. This must be at least
* four.
* \param output The buffer for the final output.
* If \p output_size is nonzero, this must be a writable
* buffer of at least \p output_size bytes.
* \param output_size The size of the \p output buffer in bytes.
* This must be large enough for the output that
* mbedtls_gcm_update() has not produced. In particular:
* - If mbedtls_gcm_update() produces immediate output,
* or if the total input size is a multiple of \c 16,
* then mbedtls_gcm_finish() never produces any output,
* so \p output_size can be \c 0.
* - \p output_size never needs to be more than \c 15.
* \param output_length On success, \p *output_length contains the actual
* length of the output written in \p output.
* On failure, the content of \p *output_length is
* unspecified.
*
* \return \c 0 on success.
* \return #MBEDTLS_ERR_GCM_BAD_INPUT on failure:
* invalid value of \p tag_len,
* or \p output_size too small.
*/
int mbedtls_gcm_finish(mbedtls_gcm_context *ctx,
unsigned char *output, size_t output_size,
size_t *output_length,
unsigned char *tag, size_t tag_len);
/**
* \brief This function clears a GCM context and the underlying
* cipher sub-context.
*
* \param ctx The GCM context to clear. If this is \c NULL, the call has
* no effect. Otherwise, this must be initialized.
*/
void mbedtls_gcm_free(mbedtls_gcm_context *ctx);
#if defined(MBEDTLS_SELF_TEST)
/**
* \brief The GCM checkup routine.
*
* \return \c 0 on success.
* \return \c 1 on failure.
*/
int mbedtls_gcm_self_test(int verbose);
#endif /* MBEDTLS_SELF_TEST */
#ifdef __cplusplus
}
#endif
#endif /* gcm.h */
+434
View File
@@ -0,0 +1,434 @@
/**
* \file hmac_drbg.h
*
* \brief The HMAC_DRBG pseudorandom generator.
*
* This module implements the HMAC_DRBG pseudorandom generator described
* in <em>NIST SP 800-90A: Recommendation for Random Number Generation Using
* Deterministic Random Bit Generators</em>.
*/
/*
* Copyright The Mbed TLS Contributors
* SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
*/
#ifndef MBEDTLS_HMAC_DRBG_H
#define MBEDTLS_HMAC_DRBG_H
#include "mbedtls/private_access.h"
#include "mbedtls/build_info.h"
#include "mbedtls/md.h"
#if defined(MBEDTLS_THREADING_C)
#include "mbedtls/threading.h"
#endif
/*
* Error codes
*/
/** Too many random requested in single call. */
#define MBEDTLS_ERR_HMAC_DRBG_REQUEST_TOO_BIG -0x0003
/** Input too large (Entropy + additional). */
#define MBEDTLS_ERR_HMAC_DRBG_INPUT_TOO_BIG -0x0005
/** Read/write error in file. */
#define MBEDTLS_ERR_HMAC_DRBG_FILE_IO_ERROR -0x0007
/** The entropy source failed. */
#define MBEDTLS_ERR_HMAC_DRBG_ENTROPY_SOURCE_FAILED -0x0009
/**
* \name SECTION: Module settings
*
* The configuration options you can set for this module are in this section.
* Either change them in mbedtls_config.h or define them on the compiler command line.
* \{
*/
#if !defined(MBEDTLS_HMAC_DRBG_RESEED_INTERVAL)
#define MBEDTLS_HMAC_DRBG_RESEED_INTERVAL 10000 /**< Interval before reseed is performed by default */
#endif
#if !defined(MBEDTLS_HMAC_DRBG_MAX_INPUT)
#define MBEDTLS_HMAC_DRBG_MAX_INPUT 256 /**< Maximum number of additional input bytes */
#endif
#if !defined(MBEDTLS_HMAC_DRBG_MAX_REQUEST)
#define MBEDTLS_HMAC_DRBG_MAX_REQUEST 1024 /**< Maximum number of requested bytes per call */
#endif
#if !defined(MBEDTLS_HMAC_DRBG_MAX_SEED_INPUT)
#define MBEDTLS_HMAC_DRBG_MAX_SEED_INPUT 384 /**< Maximum size of (re)seed buffer */
#endif
/** \} name SECTION: Module settings */
#define MBEDTLS_HMAC_DRBG_PR_OFF 0 /**< No prediction resistance */
#define MBEDTLS_HMAC_DRBG_PR_ON 1 /**< Prediction resistance enabled */
#ifdef __cplusplus
extern "C" {
#endif
/**
* HMAC_DRBG context.
*/
typedef struct mbedtls_hmac_drbg_context {
/* Working state: the key K is not stored explicitly,
* but is implied by the HMAC context */
mbedtls_md_context_t MBEDTLS_PRIVATE(md_ctx); /*!< HMAC context (inc. K) */
unsigned char MBEDTLS_PRIVATE(V)[MBEDTLS_MD_MAX_SIZE]; /*!< V in the spec */
int MBEDTLS_PRIVATE(reseed_counter); /*!< reseed counter */
/* Administrative state */
size_t MBEDTLS_PRIVATE(entropy_len); /*!< entropy bytes grabbed on each (re)seed */
int MBEDTLS_PRIVATE(prediction_resistance); /*!< enable prediction resistance (Automatic
reseed before every random generation) */
int MBEDTLS_PRIVATE(reseed_interval); /*!< reseed interval */
/* Callbacks */
int(*MBEDTLS_PRIVATE(f_entropy))(void *, unsigned char *, size_t); /*!< entropy function */
void *MBEDTLS_PRIVATE(p_entropy); /*!< context for the entropy function */
#if defined(MBEDTLS_THREADING_C)
/* Invariant: the mutex is initialized if and only if
* md_ctx->md_info != NULL. This means that the mutex is initialized
* during the initial seeding in mbedtls_hmac_drbg_seed() or
* mbedtls_hmac_drbg_seed_buf() and freed in mbedtls_ctr_drbg_free().
*
* Note that this invariant may change without notice. Do not rely on it
* and do not access the mutex directly in application code.
*/
mbedtls_threading_mutex_t MBEDTLS_PRIVATE(mutex);
#endif
} mbedtls_hmac_drbg_context;
/**
* \brief HMAC_DRBG context initialization.
*
* This function makes the context ready for mbedtls_hmac_drbg_seed(),
* mbedtls_hmac_drbg_seed_buf() or mbedtls_hmac_drbg_free().
*
* \note The reseed interval is #MBEDTLS_HMAC_DRBG_RESEED_INTERVAL
* by default. Override this value by calling
* mbedtls_hmac_drbg_set_reseed_interval().
*
* \param ctx HMAC_DRBG context to be initialized.
*/
void mbedtls_hmac_drbg_init(mbedtls_hmac_drbg_context *ctx);
/**
* \brief HMAC_DRBG initial seeding.
*
* Set the initial seed and set up the entropy source for future reseeds.
*
* A typical choice for the \p f_entropy and \p p_entropy parameters is
* to use the entropy module:
* - \p f_entropy is mbedtls_entropy_func();
* - \p p_entropy is an instance of ::mbedtls_entropy_context initialized
* with mbedtls_entropy_init() (which registers the platform's default
* entropy sources).
*
* You can provide a personalization string in addition to the
* entropy source, to make this instantiation as unique as possible.
*
* \note By default, the security strength as defined by NIST is:
* - 128 bits if \p md_info is SHA-1;
* - 192 bits if \p md_info is SHA-224;
* - 256 bits if \p md_info is SHA-256, SHA-384 or SHA-512.
* Note that SHA-256 is just as efficient as SHA-224.
* The security strength can be reduced if a smaller
* entropy length is set with
* mbedtls_hmac_drbg_set_entropy_len().
*
* \note The default entropy length is the security strength
* (converted from bits to bytes). You can override
* it by calling mbedtls_hmac_drbg_set_entropy_len().
*
* \note During the initial seeding, this function calls
* the entropy source to obtain a nonce
* whose length is half the entropy length.
*/
#if defined(MBEDTLS_THREADING_C)
/**
* \note When Mbed TLS is built with threading support,
* after this function returns successfully,
* it is safe to call mbedtls_hmac_drbg_random()
* from multiple threads. Other operations, including
* reseeding, are not thread-safe.
*/
#endif /* MBEDTLS_THREADING_C */
/**
* \param ctx HMAC_DRBG context to be seeded.
* \param md_info MD algorithm to use for HMAC_DRBG.
* \param f_entropy The entropy callback, taking as arguments the
* \p p_entropy context, the buffer to fill, and the
* length of the buffer.
* \p f_entropy is always called with a length that is
* less than or equal to the entropy length.
* \param p_entropy The entropy context to pass to \p f_entropy.
* \param custom The personalization string.
* This can be \c NULL, in which case the personalization
* string is empty regardless of the value of \p len.
* \param len The length of the personalization string.
* This must be at most #MBEDTLS_HMAC_DRBG_MAX_INPUT
* and also at most
* #MBEDTLS_HMAC_DRBG_MAX_SEED_INPUT - \c entropy_len * 3 / 2
* where \c entropy_len is the entropy length
* described above.
*
* \return \c 0 if successful.
* \return #MBEDTLS_ERR_MD_BAD_INPUT_DATA if \p md_info is
* invalid.
* \return #MBEDTLS_ERR_MD_ALLOC_FAILED if there was not enough
* memory to allocate context data.
* \return #MBEDTLS_ERR_HMAC_DRBG_ENTROPY_SOURCE_FAILED
* if the call to \p f_entropy failed.
*/
int mbedtls_hmac_drbg_seed(mbedtls_hmac_drbg_context *ctx,
const mbedtls_md_info_t *md_info,
int (*f_entropy)(void *, unsigned char *, size_t),
void *p_entropy,
const unsigned char *custom,
size_t len);
/**
* \brief Initialisation of simplified HMAC_DRBG (never reseeds).
*
* This function is meant for use in algorithms that need a pseudorandom
* input such as deterministic ECDSA.
*/
#if defined(MBEDTLS_THREADING_C)
/**
* \note When Mbed TLS is built with threading support,
* after this function returns successfully,
* it is safe to call mbedtls_hmac_drbg_random()
* from multiple threads. Other operations, including
* reseeding, are not thread-safe.
*/
#endif /* MBEDTLS_THREADING_C */
/**
* \param ctx HMAC_DRBG context to be initialised.
* \param md_info MD algorithm to use for HMAC_DRBG.
* \param data Concatenation of the initial entropy string and
* the additional data.
* \param data_len Length of \p data in bytes.
*
* \return \c 0 if successful. or
* \return #MBEDTLS_ERR_MD_BAD_INPUT_DATA if \p md_info is
* invalid.
* \return #MBEDTLS_ERR_MD_ALLOC_FAILED if there was not enough
* memory to allocate context data.
*/
int mbedtls_hmac_drbg_seed_buf(mbedtls_hmac_drbg_context *ctx,
const mbedtls_md_info_t *md_info,
const unsigned char *data, size_t data_len);
/**
* \brief This function turns prediction resistance on or off.
* The default value is off.
*
* \note If enabled, entropy is gathered at the beginning of
* every call to mbedtls_hmac_drbg_random_with_add()
* or mbedtls_hmac_drbg_random().
* Only use this if your entropy source has sufficient
* throughput.
*
* \param ctx The HMAC_DRBG context.
* \param resistance #MBEDTLS_HMAC_DRBG_PR_ON or #MBEDTLS_HMAC_DRBG_PR_OFF.
*/
void mbedtls_hmac_drbg_set_prediction_resistance(mbedtls_hmac_drbg_context *ctx,
int resistance);
/**
* \brief This function sets the amount of entropy grabbed on each
* seed or reseed.
*
* See the documentation of mbedtls_hmac_drbg_seed() for the default value.
*
* \param ctx The HMAC_DRBG context.
* \param len The amount of entropy to grab, in bytes.
*/
void mbedtls_hmac_drbg_set_entropy_len(mbedtls_hmac_drbg_context *ctx,
size_t len);
/**
* \brief Set the reseed interval.
*
* The reseed interval is the number of calls to mbedtls_hmac_drbg_random()
* or mbedtls_hmac_drbg_random_with_add() after which the entropy function
* is called again.
*
* The default value is #MBEDTLS_HMAC_DRBG_RESEED_INTERVAL.
*
* \param ctx The HMAC_DRBG context.
* \param interval The reseed interval.
*/
void mbedtls_hmac_drbg_set_reseed_interval(mbedtls_hmac_drbg_context *ctx,
int interval);
/**
* \brief This function updates the state of the HMAC_DRBG context.
*
* \note This function is not thread-safe. It is not safe
* to call this function if another thread might be
* concurrently obtaining random numbers from the same
* context or updating or reseeding the same context.
*
* \param ctx The HMAC_DRBG context.
* \param additional The data to update the state with.
* If this is \c NULL, there is no additional data.
* \param add_len Length of \p additional in bytes.
* Unused if \p additional is \c NULL.
*
* \return \c 0 on success, or an error from the underlying
* hash calculation.
*/
int mbedtls_hmac_drbg_update(mbedtls_hmac_drbg_context *ctx,
const unsigned char *additional, size_t add_len);
/**
* \brief This function reseeds the HMAC_DRBG context, that is
* extracts data from the entropy source.
*
* \note This function is not thread-safe. It is not safe
* to call this function if another thread might be
* concurrently obtaining random numbers from the same
* context or updating or reseeding the same context.
*
* \param ctx The HMAC_DRBG context.
* \param additional Additional data to add to the state.
* If this is \c NULL, there is no additional data
* and \p len should be \c 0.
* \param len The length of the additional data.
* This must be at most #MBEDTLS_HMAC_DRBG_MAX_INPUT
* and also at most
* #MBEDTLS_HMAC_DRBG_MAX_SEED_INPUT - \c entropy_len
* where \c entropy_len is the entropy length
* (see mbedtls_hmac_drbg_set_entropy_len()).
*
* \return \c 0 if successful.
* \return #MBEDTLS_ERR_HMAC_DRBG_ENTROPY_SOURCE_FAILED
* if a call to the entropy function failed.
*/
int mbedtls_hmac_drbg_reseed(mbedtls_hmac_drbg_context *ctx,
const unsigned char *additional, size_t len);
/**
* \brief This function updates an HMAC_DRBG instance with additional
* data and uses it to generate random data.
*
* This function automatically reseeds if the reseed counter is exceeded
* or prediction resistance is enabled.
*
* \note This function is not thread-safe. It is not safe
* to call this function if another thread might be
* concurrently obtaining random numbers from the same
* context or updating or reseeding the same context.
*
* \param p_rng The HMAC_DRBG context. This must be a pointer to a
* #mbedtls_hmac_drbg_context structure.
* \param output The buffer to fill.
* \param output_len The length of the buffer in bytes.
* This must be at most #MBEDTLS_HMAC_DRBG_MAX_REQUEST.
* \param additional Additional data to update with.
* If this is \c NULL, there is no additional data
* and \p add_len should be \c 0.
* \param add_len The length of the additional data.
* This must be at most #MBEDTLS_HMAC_DRBG_MAX_INPUT.
*
* \return \c 0 if successful.
* \return #MBEDTLS_ERR_HMAC_DRBG_ENTROPY_SOURCE_FAILED
* if a call to the entropy source failed.
* \return #MBEDTLS_ERR_HMAC_DRBG_REQUEST_TOO_BIG if
* \p output_len > #MBEDTLS_HMAC_DRBG_MAX_REQUEST.
* \return #MBEDTLS_ERR_HMAC_DRBG_INPUT_TOO_BIG if
* \p add_len > #MBEDTLS_HMAC_DRBG_MAX_INPUT.
*/
int mbedtls_hmac_drbg_random_with_add(void *p_rng,
unsigned char *output, size_t output_len,
const unsigned char *additional,
size_t add_len);
/**
* \brief This function uses HMAC_DRBG to generate random data.
*
* This function automatically reseeds if the reseed counter is exceeded
* or prediction resistance is enabled.
*/
#if defined(MBEDTLS_THREADING_C)
/**
* \note When Mbed TLS is built with threading support,
* it is safe to call mbedtls_ctr_drbg_random()
* from multiple threads. Other operations, including
* reseeding, are not thread-safe.
*/
#endif /* MBEDTLS_THREADING_C */
/**
* \param p_rng The HMAC_DRBG context. This must be a pointer to a
* #mbedtls_hmac_drbg_context structure.
* \param output The buffer to fill.
* \param out_len The length of the buffer in bytes.
* This must be at most #MBEDTLS_HMAC_DRBG_MAX_REQUEST.
*
* \return \c 0 if successful.
* \return #MBEDTLS_ERR_HMAC_DRBG_ENTROPY_SOURCE_FAILED
* if a call to the entropy source failed.
* \return #MBEDTLS_ERR_HMAC_DRBG_REQUEST_TOO_BIG if
* \p out_len > #MBEDTLS_HMAC_DRBG_MAX_REQUEST.
*/
int mbedtls_hmac_drbg_random(void *p_rng, unsigned char *output, size_t out_len);
/**
* \brief This function resets HMAC_DRBG context to the state immediately
* after initial call of mbedtls_hmac_drbg_init().
*
* \param ctx The HMAC_DRBG context to free.
*/
void mbedtls_hmac_drbg_free(mbedtls_hmac_drbg_context *ctx);
#if defined(MBEDTLS_FS_IO)
/**
* \brief This function writes a seed file.
*
* \param ctx The HMAC_DRBG context.
* \param path The name of the file.
*
* \return \c 0 on success.
* \return #MBEDTLS_ERR_HMAC_DRBG_FILE_IO_ERROR on file error.
* \return #MBEDTLS_ERR_HMAC_DRBG_ENTROPY_SOURCE_FAILED on reseed
* failure.
*/
int mbedtls_hmac_drbg_write_seed_file(mbedtls_hmac_drbg_context *ctx, const char *path);
/**
* \brief This function reads and updates a seed file. The seed
* is added to this instance.
*
* \param ctx The HMAC_DRBG context.
* \param path The name of the file.
*
* \return \c 0 on success.
* \return #MBEDTLS_ERR_HMAC_DRBG_FILE_IO_ERROR on file error.
* \return #MBEDTLS_ERR_HMAC_DRBG_ENTROPY_SOURCE_FAILED on
* reseed failure.
* \return #MBEDTLS_ERR_HMAC_DRBG_INPUT_TOO_BIG if the existing
* seed file is too large.
*/
int mbedtls_hmac_drbg_update_seed_file(mbedtls_hmac_drbg_context *ctx, const char *path);
#endif /* MBEDTLS_FS_IO */
#if defined(MBEDTLS_SELF_TEST)
/**
* \brief The HMAC_DRBG Checkup routine.
*
* \return \c 0 if successful.
* \return \c 1 if the test failed.
*/
int mbedtls_hmac_drbg_self_test(int verbose);
#endif
#ifdef __cplusplus
}
#endif
#endif /* hmac_drbg.h */
Loaded 100 of 200 files, more files were not shown because too many files have changed in this diff. Show more