Escolha a sua linguagem
Cada biblioteca fala o mesmo rascunho do protocolo. O que muda: para que serve, o que exige e até onde vai.
- Versão do SDK 0.5.0
- Rascunho da especificação 0.11
- Versão da API C 7
Ainda nada foi publicado: as linhas de instalação aparecem com a primeira release. Releases · GitHub
Qual é a minha?
- Homebrew numa consola: C++
- Uma app móvel (Swift, Kotlin): C
- Juntar o Lantern a uma app Flask ou FastAPI: Python
- Node, um navegador ou Hono: TypeScript
C++23
Completa para o rascunho Cliente e servidor
Cliente 16/16 Servidor 0/16
- Exige
- C++23 (CMake 3.28)
- Pacote
lantern- Funciona com
- libcurl, mbedTLS, POSIX sockets
C
Completa para o rascunho Cliente
Cliente 16/16 Servidor 0/16
- Exige
- C11, over the C++ library
- Pacote
lantern (C ABI)- Funciona com
- Swift, Kotlin (JNI)
Python
Completa para o rascunho Cliente e servidor
Cliente 16/16 Servidor 16/16
- Exige
- Python 3.11+
- Pacote
lantern-protocol- Funciona com
- Flask, FastAPI, httpx
TypeScript
Completa para o rascunho Cliente e servidor
Cliente 16/16 Servidor 16/16
- Exige
- Node 22.18+ or a browser
- Pacote
lantern-protocol- Funciona com
- node:http, Hono (web Requests)
Go
Em construção Cliente e servidor
Cliente 5/16 Servidor 0/16
- Exige
- Go 1.24+
- Pacote
github.com/JigSawFr/ lantern-sdk/ go - Funciona com
- net/http
Rust
Em construção Cliente e servidor
Cliente 0/16 Servidor 0/16
- Exige
- Rust 1.90+
- Pacote
lantern-protocol
Paridade
Cada família da especificação, lida por um cliente e servida por um servidor, em cada biblioteca. A regra: o que uma linguagem faz do protocolo, as outras fazem, no mesmo rascunho.
| Família | C++ | C | Python | TypeScript | Go | Rust | ||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Cliente | Servidor | Cliente | Servidor | Cliente | Servidor | Cliente | Servidor | Cliente | Servidor | Cliente | Servidor | |
library | Sim | Em parte | Sim | Ainda não | Sim | Sim | Sim | Sim | Sim | Ainda não | Ainda não | Ainda não |
titles | Sim | Em parte | Sim | Ainda não | Sim | Sim | Sim | Sim | Sim | Ainda não | Ainda não | Ainda não |
changes | Sim | Em parte | Sim | Ainda não | Sim | Sim | Sim | Sim | Sim | Ainda não | Ainda não | Ainda não |
sections | Sim | Em parte | Sim | Ainda não | Sim | Sim | Sim | Sim | Ainda não | Ainda não | Ainda não | Ainda não |
homebrew | Sim | Em parte | Sim | Ainda não | Sim | Sim | Sim | Sim | Ainda não | Ainda não | Ainda não | Ainda não |
saves | Sim | Em parte | Sim | Ainda não | Sim | Sim | Sim | Sim | Ainda não | Ainda não | Ainda não | Ainda não |
cheats | Sim | Em parte | Sim | Ainda não | Sim | Sim | Sim | Sim | Ainda não | Ainda não | Ainda não | Ainda não |
requests | Sim | Em parte | Sim | Ainda não | Sim | Sim | Sim | Sim | Ainda não | Ainda não | Ainda não | Ainda não |
pairing | Sim | Em parte | Sim | Ainda não | Sim | Sim | Sim | Sim | Sim | Ainda não | Ainda não | Ainda não |
signatures | Sim | Em parte | Sim | Ainda não | Sim | Sim | Sim | Sim | Sim | Ainda não | Ainda não | Ainda não |
admin | Sim | Em parte | Sim | Ainda não | Sim | Sim | Sim | Sim | Ainda não | Ainda não | Ainda não | Ainda não |
events | Sim | Em parte | Sim | Ainda não | Sim | Sim | Sim | Sim | Ainda não | Ainda não | Ainda não | Ainda não |
peers | Sim | Em parte | Sim | Ainda não | Sim | Sim | Sim | Sim | Ainda não | Ainda não | Ainda não | Ainda não |
sync | Sim | Ainda não | Sim | Ainda não | Sim | Sim | Sim | Sim | Ainda não | Ainda não | Ainda não | Ainda não |
encrypted_saves | Sim | Em parte | Sim | Ainda não | Sim | Sim | Sim | Sim | Ainda não | Ainda não | Ainda não | Ainda não |
deltas | Sim | Ainda não | Sim | Ainda não | Sim | Sim | Sim | Sim | Ainda não | Ainda não | Ainda não | Ainda não |
| Coberto | 16/16 | 0/16 | 16/16 | 0/16 | 16/16 | 16/16 | 16/16 | 16/16 | 5/16 | 0/16 | 0/16 | 0/16 |
C++ Cliente 16/16 · Servidor 0/16
| Família | Cliente | Servidor |
|---|---|---|
library | Sim | Em parte |
titles | Sim | Em parte |
changes | Sim | Em parte |
sections | Sim | Em parte |
homebrew | Sim | Em parte |
saves | Sim | Em parte |
cheats | Sim | Em parte |
requests | Sim | Em parte |
pairing | Sim | Em parte |
signatures | Sim | Em parte |
admin | Sim | Em parte |
events | Sim | Em parte |
peers | Sim | Em parte |
sync | Sim | Ainda não |
encrypted_saves | Sim | Em parte |
deltas | Sim | Ainda não |
C Cliente 16/16 · Servidor 0/16
| Família | Cliente | Servidor |
|---|---|---|
library | Sim | Ainda não |
titles | Sim | Ainda não |
changes | Sim | Ainda não |
sections | Sim | Ainda não |
homebrew | Sim | Ainda não |
saves | Sim | Ainda não |
cheats | Sim | Ainda não |
requests | Sim | Ainda não |
pairing | Sim | Ainda não |
signatures | Sim | Ainda não |
admin | Sim | Ainda não |
events | Sim | Ainda não |
peers | Sim | Ainda não |
sync | Sim | Ainda não |
encrypted_saves | Sim | Ainda não |
deltas | Sim | Ainda não |
Python Cliente 16/16 · Servidor 16/16
| Família | Cliente | Servidor |
|---|---|---|
library | Sim | Sim |
titles | Sim | Sim |
changes | Sim | Sim |
sections | Sim | Sim |
homebrew | Sim | Sim |
saves | Sim | Sim |
cheats | Sim | Sim |
requests | Sim | Sim |
pairing | Sim | Sim |
signatures | Sim | Sim |
admin | Sim | Sim |
events | Sim | Sim |
peers | Sim | Sim |
sync | Sim | Sim |
encrypted_saves | Sim | Sim |
deltas | Sim | Sim |
TypeScript Cliente 16/16 · Servidor 16/16
| Família | Cliente | Servidor |
|---|---|---|
library | Sim | Sim |
titles | Sim | Sim |
changes | Sim | Sim |
sections | Sim | Sim |
homebrew | Sim | Sim |
saves | Sim | Sim |
cheats | Sim | Sim |
requests | Sim | Sim |
pairing | Sim | Sim |
signatures | Sim | Sim |
admin | Sim | Sim |
events | Sim | Sim |
peers | Sim | Sim |
sync | Sim | Sim |
encrypted_saves | Sim | Sim |
deltas | Sim | Sim |
Go Cliente 5/16 · Servidor 0/16
| Família | Cliente | Servidor |
|---|---|---|
library | Sim | Ainda não |
titles | Sim | Ainda não |
changes | Sim | Ainda não |
sections | Ainda não | Ainda não |
homebrew | Ainda não | Ainda não |
saves | Ainda não | Ainda não |
cheats | Ainda não | Ainda não |
requests | Ainda não | Ainda não |
pairing | Sim | Ainda não |
signatures | Sim | Ainda não |
admin | Ainda não | Ainda não |
events | Ainda não | Ainda não |
peers | Ainda não | Ainda não |
sync | Ainda não | Ainda não |
encrypted_saves | Ainda não | Ainda não |
deltas | Ainda não | Ainda não |
Rust Cliente 0/16 · Servidor 0/16
| Família | Cliente | Servidor |
|---|---|---|
library | Ainda não | Ainda não |
titles | Ainda não | Ainda não |
changes | Ainda não | Ainda não |
sections | Ainda não | Ainda não |
homebrew | Ainda não | Ainda não |
saves | Ainda não | Ainda não |
cheats | Ainda não | Ainda não |
requests | Ainda não | Ainda não |
pairing | Ainda não | Ainda não |
signatures | Ainda não | Ainda não |
admin | Ainda não | Ainda não |
events | Ainda não | Ainda não |
peers | Ainda não | Ainda não |
sync | Ainda não | Ainda não |
encrypted_saves | Ainda não | Ainda não |
deltas | Ainda não | Ainda não |
✓ sim · em parte · – ainda não. Um servidor só tem ✓ numa família se a suite de conformidade a passar na CI.
Níveis de conformidade alcançados
Os níveis (SPEC.md § 17) que cada servidor verificado alcança na CI, com um token que tem todos os âmbitos.
A mesma chamada em cada linguagem
Um gesto do protocolo e o nome da sua chamada em cada biblioteca, com ligação para a definição.
| Gesto | C++ | C | Python | TypeScript |
|---|---|---|---|---|
| Handshake § 4.2 | handshake | lantern_client_handshake | handshake | handshake |
| Listar as apps § 6.1 | apps | lantern_client_apps | iter_apps | iterApps |
| O que falta § 6.3 | missing | lantern_client_missing | missing | missing |
| Transferência verificada § 6.6 | download | lantern_client_download | download_app | downloadAppTo |
| Pedir o emparelhamento § 5.2 | start_pairing | lantern_client_start_pairing | pair | pair |
| Esperar pela aprovação § 5.4 | poll_pairing | lantern_client_poll_pairing | wait_for_pairing | waitForPairing |
| Enviar um jogo guardado § 9.3 | upload_save | lantern_client_upload_save | upload_save | uploadSave |
| Seguir os eventos § 18 | events | lantern_client_events | events | events |
| Servir uma biblioteca § 17 | Server | Ainda não | LanternApp | LanternApp |
| Escrever uma biblioteca estática § 21 | Ainda não | Ainda não | export | writeStatic |
| Ler uma biblioteca estática § 21 | StaticLibrary | Ainda não | StaticLibrary | StaticLibrary |
As chamadas de Go, Rust aparecerão aqui à medida que as bibliotecas avançarem.
Exemplos
Ficheiros da pasta de exemplos de cada biblioteca, executados tal como estão na CI: funcionam.
Ler uma biblioteca
The README's example: the handshake, a listing, a verified download · cpp/examples/read_library.cpp
// The README's example: the handshake, a listing, a verified download.
//
// read_library http://127.0.0.1:8080 [platform]
//
// Over the loopback adapter (plain HTTP on 127.0.0.1 only); an app gives
// its own Transport, with TLS.
#include <cstdio>
#include <string>
#include <lantern/lantern.hpp>
#include "posix_http.hpp"
namespace
{
// Bytes kept in memory; an app writes them to its storage.
class Counting final : public lantern::DownloadSink
{
public:
std::uint64_t bytes = 0;
lantern::Result<void> write(std::span<const std::byte> chunk) override
{
bytes += chunk.size();
return {};
}
lantern::Result<void> restart() override
{
bytes = 0;
return {};
}
};
int report(const lantern::Error& error)
{
const std::string_view category = lantern::category_name(error.category());
const std::string_view code = lantern::code_name(error.effective_code());
std::fprintf(stderr, "error (%.*s, %u %.*s): %.*s\n", static_cast<int>(category.size()), category.data(),
static_cast<unsigned>(error.status()), static_cast<int>(code.size()), code.data(),
static_cast<int>(error.detail().size()), error.detail().data());
return 1;
}
} // namespace
int main(int argc, char** argv)
{
if (argc < 2)
{
std::fprintf(stderr, "usage: read_library ORIGIN [platform]\n");
return 2;
}
lantern::posix::LoopbackTransport transport;
lantern::ClientConfig config;
config.origin = argv[1];
config.user_agent = "ReadLibrary/0.3";
config.platform = argc > 2 ? argv[2] : "switch";
auto client = lantern::Client::create(transport, config);
if (!client)
return report(client.error());
// 1. The handshake: the server's name, its base path, what it offers
const auto server = client->handshake();
if (!server)
return report(server.error());
if (!*server)
{
std::fprintf(stderr, "the server speaks a newer schema than this client reads\n");
return 1;
}
std::printf("%s (%s)\n", (*server)->name.c_str(), (*server)->uid.c_str());
// 2. The listing, page by page
lantern::AppsQuery query;
query.page_size = 50;
std::string cursor;
std::optional<lantern::App> first;
do
{
query.cursor = cursor;
const auto page = client->apps(query);
if (!page)
return report(page.error());
if (!*page)
break; // a newer schema: the listing is unavailable to this client
for (const lantern::App& app : (*page)->items)
{
std::printf(" %-24s %-6s v%-10llu %s\n", app.name.c_str(), app.type.c_str(),
static_cast<unsigned long long>(app.version_code), app.app_id.c_str());
if (!first)
first.emplace(app, client->allocator());
}
cursor.assign((*page)->next);
} while (!cursor.empty());
// 3. A download, checked against the listing's size and SHA-256: success
// only when the whole file's digest matches
if (first)
{
Counting sink;
lantern::DownloadState state;
const auto done = client->download({ first->file, first->size, first->sha256 }, state, sink);
if (!done)
return report(done.error());
std::printf("downloaded %s: %llu bytes, SHA-256 verified\n", first->name.c_str(),
static_cast<unsigned long long>(sink.bytes));
}
return 0;
}
Reads a Lantern server's library, page by page, and downloads its first app, checked, over libcurl: c/README.md's example · c/examples/read_library.c
/* Reads a Lantern server's library, page by page, and downloads its first
* app, checked, over libcurl: c/README.md's example.
* read_library https://shelf.example.org [TOKEN SEED_HEX] */
#include <lantern/lantern.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static lantern_status write_file(void* user, const uint8_t* bytes, size_t size)
{
return fwrite(bytes, 1, size, (FILE*)user) == size ? LANTERN_OK : LANTERN_ERROR_STORAGE;
}
/* The server sends the file from its start again */
static lantern_status restart_file(void* user)
{
return fseek((FILE*)user, 0, SEEK_SET) == 0 ? LANTERN_OK : LANTERN_ERROR_STORAGE;
}
static int report(const char* what, const lantern_error* error)
{
fprintf(stderr, "%s: %s (%s, HTTP %u, %s)\n", what, error->detail, lantern_status_name(error->status),
(unsigned)error->http_status, error->code_text);
return EXIT_FAILURE;
}
static bool seed_of(const char* hex, uint8_t seed[32])
{
return lantern_digest_from_hex(hex, seed); /* 64 lower-case hex digits, as a digest */
}
int main(int argc, char** argv)
{
if (argc != 2 && argc != 4)
{
fprintf(stderr, "usage: %s ORIGIN [TOKEN SEED_HEX]\n", argv[0]);
return EXIT_FAILURE;
}
if (lantern_version() < LANTERN_C_API_VERSION)
{
fprintf(stderr, "liblantern_c is older than its header\n");
return EXIT_FAILURE;
}
lantern_error error = LANTERN_INIT(lantern_error);
lantern_trust* trust = NULL;
lantern_transport* transport = NULL;
lantern_random* random = NULL;
lantern_client* client = NULL;
lantern_handshake* server = NULL;
lantern_apps_page* first = NULL;
int status = EXIT_FAILURE;
/* libcurl's TLS, the server's key checked by the trust (SPEC.md 5.3) */
lantern_curl_options options = LANTERN_INIT(lantern_curl_options);
if (lantern_trust_create(NULL, &trust, &error) != LANTERN_OK
|| lantern_curl_global_init(&error) != LANTERN_OK)
goto done;
options.trust = trust;
if (lantern_transport_create_curl(&options, NULL, &transport, &error) != LANTERN_OK
|| lantern_random_create_system(NULL, &random, &error) != LANTERN_OK)
goto done;
lantern_client_config config = LANTERN_INIT(lantern_client_config);
config.origin = argv[1];
config.user_agent = "ReadLibrary/1.0";
config.platform = "switch";
config.transport = transport;
config.random = random;
config.trust = trust;
if (lantern_client_create(&config, &client, &error) != LANTERN_OK)
goto done;
if (argc == 4)
{
/* A token bound to a device key (SPEC.md 5.4): every request is signed */
uint8_t seed[32];
lantern_device_key* key = NULL;
if (!seed_of(argv[3], seed) || lantern_device_key_from_seed(seed, NULL, &key, &error) != LANTERN_OK)
goto done;
const lantern_status set = lantern_client_set_credentials(client, argv[2], key, &error);
lantern_device_key_free(key); /* the client keeps a copy */
if (set != LANTERN_OK)
goto done;
}
const lantern_status shake = lantern_client_handshake(client, &server, &error);
if (shake == LANTERN_ABSENT)
{
fprintf(stderr, "a server of a newer protocol: update the app\n");
goto done;
}
if (shake != LANTERN_OK)
goto done;
printf("%s\n", lantern_handshake_get_name(server).data);
/* Every page, by its cursor; the first page is kept for its first app */
char cursor[512] = "";
do
{
lantern_apps_query query = LANTERN_INIT(lantern_apps_query);
query.cursor = cursor;
lantern_apps_page* page = NULL;
if (lantern_client_apps(client, &query, &page, &error) != LANTERN_OK)
goto done;
for (size_t i = 0; i < lantern_apps_page_get_items_count(page); ++i)
{
const lantern_app* app = lantern_apps_page_get_items_at(page, i);
printf(" %s (%s)\n", lantern_app_get_name(app).data, lantern_app_get_app_id(app).data);
}
snprintf(cursor, sizeof(cursor), "%s", lantern_apps_page_get_next(page).data);
if (first == NULL)
first = page;
else
lantern_apps_page_free(page);
} while (cursor[0] != '\0');
const lantern_app* app = lantern_apps_page_get_items_at(first, 0);
if (app == NULL)
{
status = EXIT_SUCCESS;
goto done;
}
/* The file, its size and SHA-256 as the listing gave them */
lantern_download_target target = LANTERN_INIT(lantern_download_target);
target.url = lantern_app_get_file(app).data;
lantern_app_get_size(app, &target.size);
lantern_app_get_sha256(app, target.sha256);
FILE* file = fopen("first-app.part", "wb");
if (file == NULL)
goto done;
lantern_download_callbacks callbacks = LANTERN_INIT(lantern_download_callbacks);
callbacks.user = file;
callbacks.write = write_file;
callbacks.restart = restart_file;
lantern_download_sink* sink = NULL;
lantern_download_state* state = NULL;
lantern_status got = lantern_download_sink_create(&callbacks, NULL, &sink, &error);
if (got == LANTERN_OK)
got = lantern_download_state_create(NULL, &state, &error);
if (got == LANTERN_OK)
got = lantern_client_download(client, &target, state, sink, &error);
lantern_download_sink_free(sink);
lantern_download_state_free(state);
fclose(file);
/* Only now, with the SHA-256 checked, is the file the app's */
if (got == LANTERN_OK && rename("first-app.part", "first-app.bin") == 0)
status = EXIT_SUCCESS;
done:
if (status != EXIT_SUCCESS && error.status != LANTERN_OK)
report("lantern", &error);
lantern_apps_page_free(first);
lantern_handshake_free(server);
lantern_client_free(client);
lantern_transport_free(transport);
lantern_random_free(random);
lantern_trust_free(trust);
return status;
}
Reads a Lantern library: who serves it, its apps, what a console is missing, and one verified download · python/examples/read_library.py
"""Reads a Lantern library: who serves it, its apps, what a console is missing, and one verified download.
python examples/read_library.py https://shelf.example.org/ [platform]
The download is checked against the SHA-256 the server lists and renamed only when it matches; a `.part`
file is resumed with a Range request after a cut (SPEC.md § 6.6).
"""
from __future__ import annotations
import sys
import tempfile
from pathlib import Path
import lantern
def main(url: str, platform: str = "switch") -> None:
with lantern.LanternClient(url, platform=platform, user_agent="ReadLibrary/1.0") as client:
hello = client.handshake()
print(f"{hello.name}: {', '.join(sorted(hello.features))}")
apps = list(client.iter_apps(page_size=5)) # every page, cursor by cursor
for app in apps:
print(f" {app.name} {app.version_name or app.version_code} ({app.size} bytes)")
# What a console that holds the first app, at version 0, is missing.
if apps:
page = client.missing([(apps[0].app_id, 0)], want=("update", "dlc"))
print(f"missing: {len(page.items)} on the first page")
# The smallest non-empty file, downloaded to a temporary folder and checked on the way.
if apps:
smallest = min((a for a in apps if a.size), key=lambda a: a.size)
with tempfile.TemporaryDirectory() as folder:
saved = client.download_app(smallest, Path(folder) / f"{smallest.app_id}.pkg")
print(
f"downloaded {smallest.name}: {saved.stat().st_size} bytes, sha256 {smallest.sha256[:12]}… matched"
)
if __name__ == "__main__":
if len(sys.argv) < 2:
sys.exit(__doc__)
main(*sys.argv[1:3])
Reads a Lantern library: who serves it, its apps, what a console is missing, and one verified download · typescript/examples/read-library.ts
/**
* Reads a Lantern library: who serves it, its apps, what a console is missing, and one verified download.
*
* node --conditions=source examples/read-library.ts https://shelf.example.org/ [platform]
*
* (In your own project, `npm install lantern-protocol` and run it with plain `node`.) The download is checked
* against the SHA-256 the server lists and renamed only when it matches; a `.part` file is resumed with a Range
* request after a cut (SPEC.md § 6.6).
*/
import { mkdtempSync, rmSync, statSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { NodeLanternClient } from "lantern-protocol/node";
const [url, platform = "switch"] = process.argv.slice(2);
if (url === undefined) {
console.error("usage: read-library.ts <url> [platform]");
process.exit(2);
}
const client = new NodeLanternClient(url, { platform, userAgent: "ReadLibrary/1.0" });
const hello = await client.handshake();
console.log(`${hello.name}: ${Object.keys(hello.features).sort().join(", ")}`);
const apps = [];
for await (const app of client.iterApps({ pageSize: 5 })) {
// every page, cursor by cursor
apps.push(app);
console.log(` ${app.name} ${app.version_name ?? app.version_code} (${app.size} bytes)`);
}
// What a console that holds the first app, at version 0, is missing.
const first = apps[0];
if (first !== undefined) {
const page = await client.missing([[first.app_id, 0]]);
console.log(`missing: ${page.items.length} on the first page`);
}
// The smallest non-empty file, downloaded to a temporary folder and checked on the way.
const smallest = apps.filter((a) => a.size > 0).sort((a, b) => a.size - b.size)[0];
if (smallest !== undefined) {
const folder = mkdtempSync(join(tmpdir(), "lantern-"));
try {
const saved = await client.downloadAppTo(smallest, join(folder, `${smallest.app_id}.pkg`));
console.log(
`downloaded ${smallest.name}: ${statSync(saved).size} bytes, sha256 ${smallest.sha256.slice(0, 12)}… matched`,
);
} finally {
rmSync(folder, { recursive: true, force: true });
}
}
Emparelhar um dispositivo
The README's pairing example: a device key, the code and the QR code's address shown, the polls signed, the token kept · cpp/examples/pair_device.cpp
// The README's pairing example: a device key, the code and the QR code's
// address shown, the polls signed, the token kept.
//
// pair_device http://127.0.0.1:8080 "Lea's Switch"
//
// Over the loopback adapter (plain HTTP on 127.0.0.1 only); an app gives
// its own Transport, with TLS and a ServerTrust (adapters/curl,
// adapters/mbedtls), and keeps the credentials where its system keeps
// secrets.
#include <array>
#include <chrono>
#include <cstdio>
#include <optional>
#include <string>
#include <string_view>
#include <thread>
#include <lantern/lantern.hpp>
#include "posix_http.hpp"
#include "posix_system.hpp"
namespace
{
// Kept in memory here; an app writes them where its system protects secrets.
class Kept final : public lantern::CredentialStore
{
public:
lantern::Result<std::optional<lantern::Credentials>> load(std::string_view, lantern::Allocator) override
{
return std::optional<lantern::Credentials> {};
}
lantern::Result<void> store(std::string_view uid, const lantern::Credentials& credentials) override
{
std::printf("kept for %.*s: a token%s\n", static_cast<int>(uid.size()), uid.data(),
credentials.key ? " and its device key" : "");
return {};
}
lantern::Result<void> erase(std::string_view) override { return {}; }
};
int report(const lantern::Error& error)
{
std::fprintf(stderr, "error (%u): %.*s\n", static_cast<unsigned>(error.status()),
static_cast<int>(error.detail().size()), error.detail().data());
return 1;
}
} // namespace
int main(int argc, char** argv)
{
if (argc < 3)
{
std::fprintf(stderr, "usage: pair_device ORIGIN DEVICE-NAME\n");
return 2;
}
lantern::posix::LoopbackTransport transport;
lantern::posix::SystemRandom random;
lantern::ClientConfig config;
config.origin = argv[1];
config.user_agent = "PairDevice/0.5";
config.platform = "switch";
config.random = &random;
auto client = lantern::Client::create(transport, config);
if (!client)
return report(client.error());
if (const auto server = client->handshake(); !server || !*server)
return server ? 1 : report(server.error());
const std::array<std::string_view, 2> scopes { "library", "saves" };
lantern::PairingOptions options;
options.device = argv[2];
options.scopes = scopes;
auto pairing = client->start_pairing(options); // with a new device key
if (!pairing)
return report(pairing.error());
// The code, and the address a QR code holds (the app draws it): the
// user approves on the server's page, or an admin's device
std::printf("code %s\nopen %s\n", pairing->code.code.c_str(), pairing->code.verify_complete.c_str());
Kept store;
for (;;)
{
std::this_thread::sleep_for(std::chrono::seconds(pairing->wait));
const auto polled = client->poll_pairing(*pairing, &store); // signed by the device key
if (!polled)
return report(polled.error());
if (*polled == lantern::PollState::paired)
break;
}
const auto server = client->handshake(); // signed from now on
if (!server || !*server)
return server ? 1 : report(server.error());
std::printf("paired as %s\n", (*server)->account.name.c_str());
return 0;
}
Servir uma biblioteca
The server core over a small fictional library, for a look and for the conformance run · cpp/examples/demo_server.cpp
// The server core over a small fictional library, for a look and for the
// conformance run:
//
// lantern_demo_server [port] [--private] [--no-encrypted-saves]
// python3 spec/conformance/run.py http://127.0.0.1:PORT/ --origin https://app.example.org
//
// It serves the library (Lantern Core: the handshake, the apps, what a
// device is missing, the files, with its changes and sections, its titles
// with their images, its homebrew and its cheat sets) to anyone, takes cheat
// sets from a token with the scope cheats.write, keeps save backups (also
// encrypted ones) in memory for the accounts with the scope saves, keeps the
// requests for games of the accounts with the scope requests, tells what
// changes as events (a stream and the long poll of `changes`, § 18), and pairs
// devices: the accounts of demo_accounts.hpp, a token bound to a key, signed
// requests, invitations. An account with the scope admin runs the
// administration API: accounts, devices, pairings, invitations, requests.
// `--private` asks for a token on the library; `--no-encrypted-saves` keeps
// no backup encrypted on the device (a 400, § 9.4). Plain HTTP on 127.0.0.1; it
// prints its URL and serves until it is stopped. No port, or 0: a free one.
#include <atomic>
#include <chrono>
#include <csignal>
#include <cstdio>
#include <cstdlib>
#include <string>
#include <string_view>
#include <thread>
#include <lantern/lantern.hpp>
#include "demo_accounts.hpp"
#include "demo_library.hpp"
#include "posix_system.hpp"
#include "server_posix.hpp"
namespace
{
std::atomic<bool> stop { false };
extern "C" void on_signal(int /*signal*/)
{
stop.store(true);
}
int report(const lantern::Error& error)
{
std::fprintf(
stderr, "demo server: %.*s\n", static_cast<int>(error.detail().size()), error.detail().data());
return 1;
}
} // namespace
int main(int argc, char** argv)
{
unsigned port = 0;
bool is_private = false;
bool encrypted = true;
for (int i = 1; i < argc; ++i)
{
if (std::string_view(argv[i]) == "--private")
{
is_private = true;
continue;
}
if (std::string_view(argv[i]) == "--no-encrypted-saves")
{
encrypted = false;
continue;
}
char* end = nullptr;
const long n = std::strtol(argv[i], &end, 10);
if (end == argv[i] || *end != '\0' || n < 0 || n > 65535)
{
std::fprintf(stderr, "usage: lantern_demo_server [port] [--private] [--no-encrypted-saves]\n");
return 2;
}
port = static_cast<unsigned>(n);
}
lantern::MemoryLibrary library(lantern::demo::base);
if (auto filled = lantern::demo::fill(library); !filled)
return report(filled.error());
lantern::SystemClock clock;
lantern::MemoryAccountStore accounts;
if (auto filled = lantern::demo::fill_accounts(accounts, clock.unix_seconds()); !filled)
return report(filled.error());
lantern::MemorySaveStore saves;
lantern::MemoryRequestStore requests;
const std::array<std::string_view, 1> origins { lantern::demo::origin };
lantern::ServerOptions options;
options.uid = lantern::demo::uid;
options.name = "Demo shelf";
options.base = lantern::demo::base;
options.scheme = "http";
options.cors_origins = origins;
options.software_name = "lantern-sdk C++ demo";
options.software_version = "0.1";
options.accounts = &accounts;
options.require_auth = is_private;
options.saves = &saves;
options.requests = &requests;
options.encrypted_saves = encrypted;
// Events: a line on a stream every 5 seconds at least, which keeps the conformance run short
options.events = true;
options.heartbeat = 5;
options.wait_limit = 30;
lantern::posix::SystemRandom random;
auto server = lantern::Server::create(library, options, random, clock);
if (!server)
return report(server.error());
lantern::posix::ListenOptions listen;
listen.port = static_cast<std::uint16_t>(port);
// A cheat set is 1 MiB of text at most (§ 13), escaped in its JSON body
listen.max_body = std::size_t { 3 } << 20U;
// A save of up to save_limits.size is streamed to the store, never buffered
listen.max_stream = options.save_limits.size;
auto listener = lantern::posix::HttpListener::listen(*server, listen);
if (!listener)
return report(listener.error());
std::signal(SIGINT, on_signal);
std::signal(SIGTERM, on_signal);
std::printf("http://127.0.0.1:%u/\n", static_cast<unsigned>(listener->port()));
std::fflush(stdout);
listener->serve(stop);
// The streams still open are told, then the listener ends them
server->shutdown();
std::this_thread::sleep_for(std::chrono::milliseconds(200));
return 0;
}
Adds Lantern to a FastAPI app: the same one-game library as serve_flask.py · python/examples/serve_fastapi.py
"""Adds Lantern to a FastAPI app: the same one-game library as serve_flask.py.
uvicorn --factory examples.serve_fastapi:make_app --port 8080
python examples/serve_fastapi.py 8080
"""
from __future__ import annotations
import sys
from fastapi import FastAPI
from lantern.server import LanternApp, MemoryLibrary
from lantern.server.fastapi import include
def make_app() -> FastAPI:
library = MemoryLibrary(base="/api/lantern")
library.add_app_file(
data=b"a made-up game",
platform="switch",
title_id="0100ABCD00010000",
app_id="0100ABCD00010000",
type="base",
version_code=0,
name="Frost Garden",
format="nsp",
version_name="1.0.0",
)
app = FastAPI()
include(app, LanternApp(library, uid="3d5c7e1a-0b2f-4c6d-9e8a-1f2b3c4d5e6f", name="Living room shelf"))
return app
if __name__ == "__main__":
import uvicorn
uvicorn.run(make_app(), host="127.0.0.1", port=int(sys.argv[1]) if len(sys.argv) > 1 else 8080)
Adds Lantern to a Flask app: a library of one made-up game, served anonymously · python/examples/serve_flask.py
"""Adds Lantern to a Flask app: a library of one made-up game, served anonymously.
python examples/serve_flask.py 8080 # then: curl http://127.0.0.1:8080/.well-known/lantern
`MemoryLibrary` holds apps in memory; a real server implements `lantern.server.Library` over its own
files and database. For tokens, pairing and signed requests, pass `accounts=MemoryAccounts(...)`
(or an `authenticate` hook) to `LanternApp`.
"""
from __future__ import annotations
import sys
from flask import Flask
from lantern.server import LanternApp, MemoryLibrary
from lantern.server.flask import init_app
def make_app() -> Flask:
library = MemoryLibrary(base="/api/lantern")
library.add_app_file(
data=b"a made-up game",
platform="switch",
title_id="0100ABCD00010000",
app_id="0100ABCD00010000",
type="base",
version_code=0,
name="Frost Garden",
format="nsp",
version_name="1.0.0",
)
app = Flask(__name__)
init_app(app, LanternApp(library, uid="3d5c7e1a-0b2f-4c6d-9e8a-1f2b3c4d5e6f", name="Living room shelf"))
return app
if __name__ == "__main__":
make_app().run(host="127.0.0.1", port=int(sys.argv[1]) if len(sys.argv) > 1 else 8080)
Adds Lantern to a Node server: a library of one made-up game, served anonymously by node:http · typescript/examples/serve-node.ts
/**
* Adds Lantern to a Node server: a library of one made-up game, served anonymously by node:http.
*
* node --conditions=source examples/serve-node.ts 8080 # then: curl http://127.0.0.1:8080/.well-known/lantern
*
* `MemoryLibrary` holds apps in memory; a real server implements `Library` over its own files and database.
* For tokens, pairing and signed requests, pass `accounts: new MemoryAccounts(...)` (or `authenticate`) to
* `LanternApp`. Behind Hono or any framework of web Requests, use `fetchHandler(app)` instead of `listen()`:
* `hono.all("/*", (c) => handler(c.req.raw))`.
*/
import { LanternApp, listen, MemoryLibrary } from "lantern-protocol/server";
export function makeApp(): LanternApp {
const library = new MemoryLibrary({ base: "/api/lantern" });
library.addAppFile({
data: new TextEncoder().encode("a made-up game"),
platform: "switch",
title_id: "0100ABCD00010000",
app_id: "0100ABCD00010000",
type: "base",
version_code: 0,
version_name: "1.0.0",
name: "Frost Garden",
format: "nsp",
});
return new LanternApp(library, { uid: "3d5c7e1a-0b2f-4c6d-9e8a-1f2b3c4d5e6f", name: "Living room shelf" });
}
const served = await listen(makeApp(), { port: Number(process.argv[2] ?? 8080), host: "127.0.0.1" });
console.log(`serving ${served.url}`);
process.on("SIGTERM", () => void served.close().then(() => process.exit(0)));
process.on("SIGINT", () => void served.close().then(() => process.exit(0)));
Publicar uma biblioteca estática
A static library: a library written as JSON files (GitHub Pages, a NAS, a USB stick), then read back · python/examples/static_library.py
"""A static library: a library written as JSON files (GitHub Pages, a NAS, a USB stick), then read back.
python examples/static_library.py out/ # serve out/ with any web server, or open it as a folder
SPEC.md § 21. The files are the same documents a live server sends, with their SHA-256 and a handshake at
`lantern.json`, so a client reads them with no server code behind them.
"""
from __future__ import annotations
import sys
import tempfile
from pathlib import Path
from lantern.server import MemoryLibrary
from lantern.static import StaticLibrary, export
def main(out: Path) -> None:
library = MemoryLibrary(base="/api/lantern")
library.add_app_file(
data=b"a made-up game",
platform="switch",
title_id="0100ABCD00010000",
app_id="0100ABCD00010000",
type="base",
version_code=0,
name="Frost Garden",
format="nsp",
version_name="1.0.0",
)
summary = export(library, out, uid="3d5c7e1a-0b2f-4c6d-9e8a-1f2b3c4d5e6f", name="Shelf on a stick")
print(f"wrote {summary}")
with StaticLibrary(out) as shelf:
print(shelf.handshake().name, [app.name for app in shelf.iter_apps("switch")])
if __name__ == "__main__":
if len(sys.argv) > 1:
main(Path(sys.argv[1]))
else:
with tempfile.TemporaryDirectory() as folder:
main(Path(folder) / "shelf")
A static library: a library written as JSON files (GitHub Pages, a NAS, a USB stick), then read back (StaticLibrary.open("https://…") reads one over HTTP) · typescript/examples/static-library.ts
/**
* A static library: a library written as JSON files (GitHub Pages, a NAS, a USB stick), then read back (`StaticLibrary.open("https://…")` reads one over HTTP).
*
* node --conditions=source examples/static-library.ts out/
*
* SPEC.md § 21. The files are the same documents a live server sends, with their SHA-256 and a handshake in
* `lantern.json`, so a client reads them with no server code behind them.
*/
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { StaticLibrary } from "lantern-protocol";
import { FileStaticSource, libraryInput, writeStatic } from "lantern-protocol/node";
import { MemoryLibrary } from "lantern-protocol/server";
const library = new MemoryLibrary({ base: "/api/lantern" });
library.addAppFile({
data: new TextEncoder().encode("a made-up game"),
platform: "switch",
title_id: "0100ABCD00010000",
app_id: "0100ABCD00010000",
type: "base",
version_code: 0,
version_name: "1.0.0",
name: "Frost Garden",
format: "nsp",
});
const given = process.argv[2];
const out = given ?? join(mkdtempSync(join(tmpdir(), "lantern-static-")), "shelf");
try {
const summary = await writeStatic(
await libraryInput(library, { uid: "3d5c7e1a-0b2f-4c6d-9e8a-1f2b3c4d5e6f", name: "Shelf on a stick" }),
out,
);
console.log(`wrote ${JSON.stringify(summary)}`);
const shelf = await StaticLibrary.open(new FileStaticSource(out));
const names: string[] = [];
for await (const app of shelf.apps("switch")) names.push(app.name ?? app.app_id);
console.log(shelf.hello.name, JSON.stringify(names));
} finally {
if (given === undefined) rmSync(join(out, ".."), { recursive: true, force: true });
}
O que as torna fiáveis
Suite de conformidade
Cada servidor passa a suite da especificação na CI, nível a nível.
Um servidor de teste hostil
Cada cliente recusa resumos errados, uma chave alterada, manifestos hostis e um esquema mais recente.
Vetores partilhados
Os mesmos vetores de teste correm em cada linguagem.
Versões assinadas
As versões serão construídas a partir da etiqueta pela CI, de forma reprodutível, com um SBOM e proveniência SLSA.
Versões
Uma só versão para todo o SDK, a mesma em cada linguagem.
| Versão do SDK | Rascunho da especificação | Versão da API C | Última release |
|---|---|---|---|
| 0.5.0 | 0.11 | 7 (ABI 1) | nenhuma ainda |
Dados do lantern-sdk no commit 9fc3b4f.
Comece de uma biblioteca, não do zero
Experimente uma com a biblioteca de demonstração, ou leia o código.