Выберите язык
Каждая библиотека говорит на одном и том же черновике протокола. Отличается то, для чего она нужна, что требует и как далеко заходит.
- Версия SDK 0.5.0
- Черновик спецификации 0.11
- Версия C API 7
Пока ничего не выпущено: строки установки появятся с первым релизом. Релизы · GitHub
Какая подойдёт мне?
- Homebrew на консоли: C++
- Мобильное приложение (Swift, Kotlin): C
- Добавить Lantern в приложение на Flask или FastAPI: Python
- Node, браузер или Hono: TypeScript
C++23
Полная для черновика Клиент и сервер
Клиент 16/16 Сервер 0/16
- Требует
- C++23 (CMake 3.28)
- Пакет
lantern- Работает с
- libcurl, mbedTLS, POSIX sockets
C
Полная для черновика Клиент
Клиент 16/16 Сервер 0/16
- Требует
- C11, over the C++ library
- Пакет
lantern (C ABI)- Работает с
- Swift, Kotlin (JNI)
Python
Полная для черновика Клиент и сервер
Клиент 16/16 Сервер 16/16
- Требует
- Python 3.11+
- Пакет
lantern-protocol- Работает с
- Flask, FastAPI, httpx
TypeScript
Полная для черновика Клиент и сервер
Клиент 16/16 Сервер 16/16
- Требует
- Node 22.18+ or a browser
- Пакет
lantern-protocol- Работает с
- node:http, Hono (web Requests)
Go
В разработке Клиент и сервер
Клиент 5/16 Сервер 0/16
- Требует
- Go 1.24+
- Пакет
github.com/JigSawFr/ lantern-sdk/ go - Работает с
- net/http
Rust
В разработке Клиент и сервер
Клиент 0/16 Сервер 0/16
- Требует
- Rust 1.90+
- Пакет
lantern-protocol
Паритет
Каждое семейство спецификации, как его читает клиент и как обслуживает сервер, в каждой библиотеке. Правило: что один язык умеет из протокола, то умеют и остальные, в том же черновике.
| Семейство | C++ | C | Python | TypeScript | Go | Rust | ||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Клиент | Сервер | Клиент | Сервер | Клиент | Сервер | Клиент | Сервер | Клиент | Сервер | Клиент | Сервер | |
library | Да | Частично | Да | Пока нет | Да | Да | Да | Да | Да | Пока нет | Пока нет | Пока нет |
titles | Да | Частично | Да | Пока нет | Да | Да | Да | Да | Да | Пока нет | Пока нет | Пока нет |
changes | Да | Частично | Да | Пока нет | Да | Да | Да | Да | Да | Пока нет | Пока нет | Пока нет |
sections | Да | Частично | Да | Пока нет | Да | Да | Да | Да | Пока нет | Пока нет | Пока нет | Пока нет |
homebrew | Да | Частично | Да | Пока нет | Да | Да | Да | Да | Пока нет | Пока нет | Пока нет | Пока нет |
saves | Да | Частично | Да | Пока нет | Да | Да | Да | Да | Пока нет | Пока нет | Пока нет | Пока нет |
cheats | Да | Частично | Да | Пока нет | Да | Да | Да | Да | Пока нет | Пока нет | Пока нет | Пока нет |
requests | Да | Частично | Да | Пока нет | Да | Да | Да | Да | Пока нет | Пока нет | Пока нет | Пока нет |
pairing | Да | Частично | Да | Пока нет | Да | Да | Да | Да | Да | Пока нет | Пока нет | Пока нет |
signatures | Да | Частично | Да | Пока нет | Да | Да | Да | Да | Да | Пока нет | Пока нет | Пока нет |
admin | Да | Частично | Да | Пока нет | Да | Да | Да | Да | Пока нет | Пока нет | Пока нет | Пока нет |
events | Да | Частично | Да | Пока нет | Да | Да | Да | Да | Пока нет | Пока нет | Пока нет | Пока нет |
peers | Да | Частично | Да | Пока нет | Да | Да | Да | Да | Пока нет | Пока нет | Пока нет | Пока нет |
sync | Да | Пока нет | Да | Пока нет | Да | Да | Да | Да | Пока нет | Пока нет | Пока нет | Пока нет |
encrypted_saves | Да | Частично | Да | Пока нет | Да | Да | Да | Да | Пока нет | Пока нет | Пока нет | Пока нет |
deltas | Да | Пока нет | Да | Пока нет | Да | Да | Да | Да | Пока нет | Пока нет | Пока нет | Пока нет |
| Покрыто | 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++ Клиент 16/16 · Сервер 0/16
| Семейство | Клиент | Сервер |
|---|---|---|
library | Да | Частично |
titles | Да | Частично |
changes | Да | Частично |
sections | Да | Частично |
homebrew | Да | Частично |
saves | Да | Частично |
cheats | Да | Частично |
requests | Да | Частично |
pairing | Да | Частично |
signatures | Да | Частично |
admin | Да | Частично |
events | Да | Частично |
peers | Да | Частично |
sync | Да | Пока нет |
encrypted_saves | Да | Частично |
deltas | Да | Пока нет |
C Клиент 16/16 · Сервер 0/16
| Семейство | Клиент | Сервер |
|---|---|---|
library | Да | Пока нет |
titles | Да | Пока нет |
changes | Да | Пока нет |
sections | Да | Пока нет |
homebrew | Да | Пока нет |
saves | Да | Пока нет |
cheats | Да | Пока нет |
requests | Да | Пока нет |
pairing | Да | Пока нет |
signatures | Да | Пока нет |
admin | Да | Пока нет |
events | Да | Пока нет |
peers | Да | Пока нет |
sync | Да | Пока нет |
encrypted_saves | Да | Пока нет |
deltas | Да | Пока нет |
Python Клиент 16/16 · Сервер 16/16
| Семейство | Клиент | Сервер |
|---|---|---|
library | Да | Да |
titles | Да | Да |
changes | Да | Да |
sections | Да | Да |
homebrew | Да | Да |
saves | Да | Да |
cheats | Да | Да |
requests | Да | Да |
pairing | Да | Да |
signatures | Да | Да |
admin | Да | Да |
events | Да | Да |
peers | Да | Да |
sync | Да | Да |
encrypted_saves | Да | Да |
deltas | Да | Да |
TypeScript Клиент 16/16 · Сервер 16/16
| Семейство | Клиент | Сервер |
|---|---|---|
library | Да | Да |
titles | Да | Да |
changes | Да | Да |
sections | Да | Да |
homebrew | Да | Да |
saves | Да | Да |
cheats | Да | Да |
requests | Да | Да |
pairing | Да | Да |
signatures | Да | Да |
admin | Да | Да |
events | Да | Да |
peers | Да | Да |
sync | Да | Да |
encrypted_saves | Да | Да |
deltas | Да | Да |
Go Клиент 5/16 · Сервер 0/16
| Семейство | Клиент | Сервер |
|---|---|---|
library | Да | Пока нет |
titles | Да | Пока нет |
changes | Да | Пока нет |
sections | Пока нет | Пока нет |
homebrew | Пока нет | Пока нет |
saves | Пока нет | Пока нет |
cheats | Пока нет | Пока нет |
requests | Пока нет | Пока нет |
pairing | Да | Пока нет |
signatures | Да | Пока нет |
admin | Пока нет | Пока нет |
events | Пока нет | Пока нет |
peers | Пока нет | Пока нет |
sync | Пока нет | Пока нет |
encrypted_saves | Пока нет | Пока нет |
deltas | Пока нет | Пока нет |
Rust Клиент 0/16 · Сервер 0/16
| Семейство | Клиент | Сервер |
|---|---|---|
library | Пока нет | Пока нет |
titles | Пока нет | Пока нет |
changes | Пока нет | Пока нет |
sections | Пока нет | Пока нет |
homebrew | Пока нет | Пока нет |
saves | Пока нет | Пока нет |
cheats | Пока нет | Пока нет |
requests | Пока нет | Пока нет |
pairing | Пока нет | Пока нет |
signatures | Пока нет | Пока нет |
admin | Пока нет | Пока нет |
events | Пока нет | Пока нет |
peers | Пока нет | Пока нет |
sync | Пока нет | Пока нет |
encrypted_saves | Пока нет | Пока нет |
deltas | Пока нет | Пока нет |
✓ да · частично · – пока нет. Сервер получает ✓ для семейства, только если набор тестов на соответствие проходит его в CI.
Достигнутые уровни соответствия
Уровни (SPEC.md § 17), которых каждый проверяемый сервер достигает в CI, с токеном, у которого есть все области доступа.
Один вызов на каждом языке
Одно действие протокола и имя его вызова в каждой библиотеке, со ссылкой на определение.
| Действие | C++ | C | Python | TypeScript |
|---|---|---|---|---|
| Рукопожатие § 4.2 | handshake | lantern_client_handshake | handshake | handshake |
| Список приложений § 6.1 | apps | lantern_client_apps | iter_apps | iterApps |
| Чего не хватает § 6.3 | missing | lantern_client_missing | missing | missing |
| Проверенная загрузка § 6.6 | download | lantern_client_download | download_app | downloadAppTo |
| Запросить сопряжение § 5.2 | start_pairing | lantern_client_start_pairing | pair | pair |
| Ждать одобрения § 5.4 | poll_pairing | lantern_client_poll_pairing | wait_for_pairing | waitForPairing |
| Отправить сохранение § 9.3 | upload_save | lantern_client_upload_save | upload_save | uploadSave |
| Следить за событиями § 18 | events | lantern_client_events | events | events |
| Обслуживать библиотеку § 17 | Server | Пока нет | LanternApp | LanternApp |
| Записать статическую библиотеку § 21 | Пока нет | Пока нет | export | writeStatic |
| Прочитать статическую библиотеку § 21 | StaticLibrary | Пока нет | StaticLibrary | StaticLibrary |
Вызовы Go, Rust появятся здесь по мере готовности их библиотек.
Примеры
Файлы из папки примеров каждой библиотеки, выполняемые как есть в CI: они работают.
Прочитать библиотеку
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 });
}
}
Выполнить сопряжение устройства
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;
}
Раздать библиотеку
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)));
Опубликовать статическую библиотеку
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 });
}
Почему им можно доверять
Набор проверок соответствия
Каждый сервер проходит набор проверок спецификации в CI, уровень за уровнем.
Враждебный тестовый сервер
Каждый клиент отвергает неверные хэши, смену ключа, враждебные манифесты и более новую схему.
Общие векторы
Одни и те же тестовые векторы запускаются во всех языках.
Подписанные релизы
Релизы будут собираться CI из тега воспроизводимо, с SBOM и происхождением SLSA.
Версии
Одна версия на весь SDK, одинаковая для каждого языка.
| Версия SDK | Черновик спецификации | Версия C API | Последний релиз |
|---|---|---|---|
| 0.5.0 | 0.11 | 7 (ABI 1) | пока нет |
Данные из lantern-sdk, коммит 9fc3b4f.
Начните с библиотеки, а не с нуля
Попробуйте одну на демонстрационной библиотеке или прочитайте код.