Skip to content

This PR adds local Wi‑Fi backup transfer between 3DS consoles.#528

Merged
BernardoGiordano merged 14 commits into
BernardoGiordano:masterfrom
edu1010:master
Jun 20, 2026
Merged

This PR adds local Wi‑Fi backup transfer between 3DS consoles.#528
BernardoGiordano merged 14 commits into
BernardoGiordano:masterfrom
edu1010:master

Conversation

@edu1010

@edu1010 edu1010 commented Apr 12, 2026

Copy link
Copy Markdown
Contributor

This PR adds local Wi-Fi backup transfer between 3DS consoles in Checkpoint.

What this adds

  • New Transfer flow in the 3DS UI (Send / Receive).

  • Receiver mode with IP / port / PIN token display.

  • Sender upload of the selected save/extdata backup over HTTP multipart.

  • Receiver endpoints for transfer negotiation and upload handling.

  • Support for both:

    • single-file backups (direct send)
    • multi-file backups (ZIP package + extraction)
  • Post-transfer completion state, automatic title list refresh.

edu1010 added 5 commits March 2, 2026 02:15
- fix const-correctness in Title API used by transfer (`id()` and `shortDescription()`)
- fix `Server::isRunning()` by using an explicit atomic runtime state
- replace deprecated `std::wstring_convert` usage with `StringUtils::UTF16toUTF8`
- mark pending ZIP transfer helpers/constants as `[[maybe_unused]]` to avoid noise
- make `ccache` optional in 3DS Makefile to remove repeated "which: no ccache" output
- set `GIT_REV` fallback to `unknown` when building outside a git repository
Make file sending compatible with savedata containing multiple internal files. Also translated some remaining parts that were still in Spanish into English. UI update after receiving data is still pending—currently, data only displays after closing and reopening. Additionally, some interface elements still need to be updated to improve clarity and consistency.
- add receiver completion and pending-refresh state, and trigger a full titles refresh from `MainScreen`
- improve transfer overlays (clearer send/receive prompt, shortcuts, and post-receive confirmation view)
- add fallback mapping by title name when title ID is missing/mismatched, with clearer user notices
- include folder entries in ZIP packaging and return a specific error for empty backups
- harden ZIP read/write and HTTP response handling to report corruption and receiver-side errors
- normalize line endings in GitHub issue templates
@BernardoGiordano

Copy link
Copy Markdown
Owner

Thanks for the contribution. I'll review as soon as I can

BernardoGiordano and others added 5 commits June 1, 2026 11:01
The PIN is the only authentication for the upload endpoint, but the
/transfer/info handler returned it in plaintext to any unauthenticated
caller on the LAN, letting anyone read it and then POST to
/transfer/upload. The sender never consumed this field anyway.

Remove the token from the info response; the PIN is now only shown on
the receiver's screen and must be entered manually on the sender.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The server buffered the entire HTTP request in a std::string driven by an
attacker-controlled Content-Length, and parseMultipart then copied the
whole body and the file part again (~3x the file size resident at once).
On the 3DS's small heap this could OOM/crash on large saves or via a
crafted Content-Length.

- Cap the buffered request at MAX_REQUEST_SIZE (32 MiB) and reply 413 when
  exceeded, also guarding against a client that never terminates headers.
- parseMultipart now returns the file part as an [offset,length) range into
  the original buffer instead of copying it, so the payload exists only
  once in memory and is written straight to disk.

Also drops a stray trailing-CRLF trim that could truncate binary payloads
ending in 0x0D0A by two bytes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The server runs on a single thread and the accepted socket is blocking, so
a client that connects and then stalls would block the receiver forever:
stopReceiver() only unregisters handlers and never closes the socket, and
no further connections could be accepted.

Set SO_RCVTIMEO (15s) on each client socket so a stalled recv unblocks,
the request is abandoned, and the server returns to accepting.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
g_transferMode (a std::string) and the g_transferBytes{Done,Total}
counters are written by the HTTP server thread while receiving and read
by the UI thread when drawing progress. Concurrent access to the
std::string is undefined behavior (and can crash), and 64-bit reads tear
on the 3DS's 32-bit core.

Route all access through mutex-protected helpers (transferSetMode/
transferGetMode/transferSetProgress/transferAddDone/transferGetProgress)
and protect the receiver notice/completed-name strings with their own
mutex behind setReceiverNotice/setReceiverCompletedName and locked
getters. UI readers now take a consistent snapshot under the lock.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Transfer start/stop now registers and unregisters HTTP handlers at runtime
(main thread) while the network thread concurrently looks them up, racing
on the std::map. Guard all access with a mutex; the request dispatcher
copies the handler out under the lock and releases it before invoking the
handler so a long upload doesn't block (un)registration.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@BernardoGiordano BernardoGiordano left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few low-severity quality/robustness notes from the review (the higher-severity security and concurrency items are being addressed in separate commits).

Comment thread 3ds/source/transfer.cpp Outdated
}

for (auto& entry : files) {
entry.crc = computeCrc(Archive::sdmc(), entry.absPath);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perf (low): every file is read fully twice during packaging — once here in computeCrc and again below to stream the data into the ZIP. On the 3DS's slow SD this roughly doubles the packaging time for large backups. Consider computing the CRC incrementally during the single write pass (update the CRC over each buffer as you write it) and backfilling the local-header CRC, or use a data descriptor.

Comment thread 3ds/source/transfer.cpp Outdated
input.close();
return false;
}
(void)crc;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Robustness (low): the per-entry CRC is read from the ZIP and then discarded ((void)crc;), so a corrupted/truncated transfer is silently extracted as-is. Since you already have a CRC routine, consider verifying the extracted data against crc and failing the upload on mismatch.

Comment thread 3ds/source/transfer.cpp Outdated
size_t headerEnd = requestData.find("\r\n\r\n");
std::string headers = headerEnd == std::string::npos ? requestData : requestData.substr(0, headerEnd);
std::string token = headerValue(headers, "X-CP-Token");
if (token != g_token) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: token != g_token is not a constant-time comparison. Low risk on a LAN with a short-lived PIN, but a constant-time compare is cheap insurance against timing-based PIN guessing.

Comment thread 3ds/source/transfer.cpp Outdated
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip.c_str());

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Robustness (low): inet_addr is deprecated and returns 0xFFFFFFFF (i.e. 255.255.255.255) on malformed input rather than signalling an error, so a typo'd IP silently produces a bogus destination. Prefer inet_pton(AF_INET, ip.c_str(), &addr.sin_addr) and surface an "Invalid IP" error when it returns != 1.

Comment thread 3ds/source/MainScreen.cpp Outdated
std::string backupName = nameFromCell(cellIndex);
std::u16string backupPath = Archive::mode() == MODE_SAVE ? title.fullSavePath(cellIndex) : title.fullExtdataPath(cellIndex);

std::string ipPort = rawKeyboard("192.168.1.10:8000", "IP:PUERTO del receptor", 32);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consistency (low): these keyboard hints are in Spanish ("IP:PUERTO del receptor" here and "PIN (4-6 digitos)" at line 809) while the rest of the UI is English. Worth aligning the language for the upstream project.

Comment thread 3ds/source/MainScreen.cpp Outdated
if (pin.empty()) {
return;
}
bool pinOk = pin.size() >= 4 && pin.size() <= 6 &&

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistency (low): the sender accepts a 4–6 digit PIN, but the receiver always generates exactly 4 digits (startReceiver: 1000 + rand() % 9000). A 5–6 digit PIN entered here can therefore never match. Either generate a variable-length PIN on the receiver or restrict the sender to 4 digits.

Comment thread 3ds/Makefile Outdated
CFLAGS += $(INCLUDE) -DARM11 -D__3DS__ -D_GNU_SOURCE=1

CXXFLAGS := $(CFLAGS) -fno-rtti -std=gnu++23
CXXFLAGS := $(CFLAGS) -fno-rtti -std=gnu++23

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: trailing whitespace introduced after -std=gnu++23.

@BernardoGiordano

Copy link
Copy Markdown
Owner

I pushed fixes for the security/concurrency findings (PIN exposure via /transfer/info, unbounded upload buffering, missing recv timeout, the shared-state data races, and the unsynchronized handlers map) as a PR into this branch: edu1010#1. Merging that will fold them into this PR. The remaining low-severity items are the inline comments above.

BernardoGiordano and others added 4 commits June 1, 2026 12:09
The top-screen transfer loader drew the mode label, percentage and byte
counts as one centered line, so longer labels like "Preparing backup
package" ran off-screen and didn't match the new modal loader. Split it
into two centered lines (label/percent, then size) like the bottom modal.

Also format the transferred/total size in MB across the top loader, the
bottom progress modal, and the receiver overlay instead of raw bytes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Security & concurrency hardening for the Wi-Fi transfer feature
Resolve conflicts keeping the Wi-Fi transfer feature alongside upstream's
recent work (system saves, asian fonts, utf8/utf16 refactor, multi-file
savedata progress UI):

- 3ds/include/main.hpp: keep the thread-safe transfer status helpers and
  add upstream's g_multiSelectCount/g_multiSelectTotal globals.
- 3ds/source/MainScreen.cpp: keep the network-transfer progress modal
  (g_transferIsNetwork branch) and adopt upstream's improved local-copy
  modal with the multi-select overall progress bar.
- 3ds/source/server.cpp: keep the atomic serverIsRunning concurrency fix.
- common/common.cpp: take upstream's removal of UTF16toUTF8 (relocated to
  3ds/source/util.cpp, rewritten without the deprecated codecvt).

Builds clean for the 3ds target (Checkpoint.3dsx/.cia produced).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ordano#528

- transfer.cpp (writeZip): compute each file's CRC during the single
  streaming pass and backfill it into the local header, instead of a
  separate computeCrc pre-pass. Halves file reads when packaging large
  backups on the slow SD.
- transfer.cpp (extractZip): verify the extracted data against the CRC
  stored in each ZIP entry and fail on mismatch, so a corrupted or
  truncated transfer is rejected instead of written out as-is.
- transfer.cpp (handleUpload): compare the PIN token in constant time to
  avoid leaking it through comparison timing.
- transfer.cpp (sendBackup): parse the destination IP with inet_pton and
  surface an 'Invalid IP address' error instead of inet_addr, which maps
  malformed input to 255.255.255.255.
- MainScreen.cpp: translate the IP/PIN keyboard hints to English and
  require an exactly-4-digit PIN, matching the receiver which always
  generates 4 digits (a 5-6 digit PIN could never match).
- Makefile: drop trailing whitespace after -std=gnu++23.

Builds clean for the 3ds target.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@edu1010

edu1010 commented Jun 17, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review! I used Claude Code to help resolve the conflicts and work through your feedback, so apologies in advance if anything looks off — happy to adjust.

I've merged the latest upstream/master to resolve the conflicts, and addressed all the inline comments:

  • transfer.cpp (packaging CRC): the CRC is now computed during the single streaming pass and backfilled into the local header, so each file is read only once instead of twice.

  • transfer.cpp (extract CRC): the extracted data is now verified against the per-entry CRC and the upload fails on mismatch, instead of (void)crc;.

  • transfer.cpp (token compare): the PIN token is now compared in constant time.

  • transfer.cpp (IP parsing): switched from inet_addr to inet_pton, surfacing an "Invalid IP address" error on malformed input.

  • MainScreen.cpp (keyboard hints): translated the IP/PIN hints to English.

  • MainScreen.cpp (PIN length): the sender now requires exactly 4 digits to match the receiver (which always generates a 4-digit PIN), so a 5–6 digit PIN can no longer be entered and silently fail.

  • Makefile: removed the trailing whitespace after -std=gnu++23.
    Builds clean for the 3ds target. Let me know if you'd like any changes!

@BernardoGiordano
BernardoGiordano merged commit 43ab2b0 into BernardoGiordano:master Jun 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants