Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Outil local de gestion/analyse des combats Leek Wars. Voir README.md pour le dé

- Serveur : `npm run server` (ou `npm start` pour builder l'UI aussi) → `http://127.0.0.1:3737`. Vérifier avec `curl -s localhost:3737/api/status`.
- Doc des routes : `curl -s localhost:3737/api`.
- Accès : les requêtes `/api` avec un en-tête `Origin` hors liste blanche (Tauri, Vite :1420, le serveur lui-même, `LWL_ALLOWED_ORIGINS`) ou un `Host` non local sont refusées en 403 `forbidden_origin` (`rejectedAccess` dans `server/index.js`). curl et les scripts (sans Origin) passent.
- Comptes : plusieurs comptes Leek Wars (table `accounts`, une clé API chacun ; `GET/POST /api/accounts`). Un compte est actif (`PUT /api/accounts/active`) : vue éleveur par défaut (`?account=<farmerId>` pour un autre) et combats éleveur. Les actions sur un poireau utilisent automatiquement le compte propriétaire. Sans compte : 401 `code: "no_api_key"`. Ne jamais afficher une clé en entier.
- Poireaux suivis : `GET /api/leeks` (table `tracked_leeks`). `owned: false` = poireau d'un autre joueur suivi pour analyse (pas de lancement ni de capital). `POST /api/leeks/tracked {id}` suit n'importe quel poireau et importe son historique.
- Perspectives : `/api/fights`, `/api/stats` sans `leek` = mon éleveur ; avec `?leek=<id>` = du point de vue de ce poireau suivi. `/api/fights/:id?leek=<id>` accepte n'importe quel participant.
Expand Down
675 changes: 675 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,11 @@ ne reconnaîtraient plus les mises à jour).

Doc auto-générée : `GET http://127.0.0.1:3737/api`.

Le serveur n'écoute qu'en local et refuse (403 `forbidden_origin`) les requêtes envoyées par une page web autre que
l'interface (application, Vite en dev, interface servie par le serveur) : un site ouvert dans le navigateur ne peut pas
piloter l'API. Les clients sans navigateur (curl, scripts) ne sont pas concernés. Pour autoriser une autre origine :
`LWL_ALLOWED_ORIGINS=https://exemple.test,http://localhost:5173`.

| Méthode | Route | Description |
|---|---|---|
| GET | `/api/status` | éleveur, poireaux, combats restants, tâches en cours |
Expand Down Expand Up @@ -216,6 +221,12 @@ curl -s "localhost:3737/api/compare?a=since%3D1790700000%26until%3D1790800000&b=
- Les PT « inutilisés » sont estimés à partir du coût des objets ; les bonus de PT temporaires ne sont pas comptés.
- Certains combats sont refusés par Leek Wars (`fight_with_secret_trophy`) et ne peuvent pas être importés.

## Licence

LeekWars Local est un logiciel libre distribué sous licence [GNU GPL v3](LICENSE) : vous pouvez l'utiliser, l'étudier,
le modifier et le redistribuer, à condition que les versions modifiées que vous distribuez restent sous la même licence.
Les contributions (issues, pull requests) sont les bienvenues.

## Crédits

Images, polices et textes du jeu : client [Leek Wars](https://github.com/leek-wars/leek-wars) (licence GPL v3, voir
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "leekwarslocal",
"private": true,
"version": "0.6.0",
"version": "0.6.1",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
35 changes: 31 additions & 4 deletions server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1335,21 +1335,48 @@ const ready = migrateKeys()
.then(() => reanalyze())
.catch((e) => console.error("Initialisation :", e.message));

// Accès à l'API depuis un navigateur : seulement l'interface (application Tauri, Vite en dev, ce serveur lui-même).
// Sans ce filtre, n'importe quel site ouvert dans le navigateur pourrait piloter l'API locale (lancer des combats,
// dépenser du capital, des analyses IA payantes…). Les clients sans navigateur (curl, scripts) n'envoient pas
// d'en-tête Origin et restent acceptés. LWL_ALLOWED_ORIGINS (séparées par des virgules) en ajoute d'autres.
const LOOPBACK = new Set(["127.0.0.1", "localhost", "[::1]"]);
const ALLOWED_ORIGINS = new Set([
"tauri://localhost", "http://tauri.localhost", "https://tauri.localhost",
"http://localhost:1420", "http://127.0.0.1:1420",
...[...LOOPBACK].map((h) => `http://${h}:${PORT}`),
...(process.env.LWL_ALLOWED_ORIGINS ?? "").split(",").map((o) => o.trim()).filter(Boolean),
]);

/** Refus (message) d'une requête d'API venue d'ailleurs que de l'interface, sinon null. */
function rejectedAccess(req) {
// Rebinding DNS : un domaine qui pointerait vers 127.0.0.1 garderait son propre nom dans Host.
const host = String(req.headers.host ?? "").replace(/:\d+$/, "");
if (LOOPBACK.has(HOST) && !LOOPBACK.has(host)) return `Hôte non autorisé : ${host || "(absent)"}`;
const origin = req.headers.origin;
if (origin) return ALLOWED_ORIGINS.has(origin) ? null : `Origine non autorisée : ${origin}`;
// Requêtes sans Origin émises par une autre page (balise <img>, lien…) : signalées par Sec-Fetch-Site.
return req.headers["sec-fetch-site"] === "cross-site" ? "Requête intersite non autorisée" : null;
}

const server = http.createServer(async (req, res) => {
const url = new URL(req.url, `http://${req.headers.host}`);
const url = new URL(req.url, "http://127.0.0.1");
if (!url.pathname.startsWith("/api")) return serveStatic(req, res, url.pathname);
const origin = req.headers.origin;
const cors = origin && ALLOWED_ORIGINS.has(origin) ? { "Access-Control-Allow-Origin": origin, Vary: "Origin" } : { Vary: "Origin" };
const send = (status, data) => {
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Access-Control-Allow-Origin": "*" });
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8", ...cors });
res.end(JSON.stringify(data));
};
const denied = rejectedAccess(req);
if (denied) return send(403, { error: `${denied}. L'API locale n'accepte que l'interface LeekWars Local et les clients sans navigateur.`, code: "forbidden_origin" });
if (req.method === "OPTIONS") {
res.writeHead(204, {
"Access-Control-Allow-Origin": "*",
...cors,
"Access-Control-Allow-Methods": "GET,POST,PATCH,PUT,DELETE,OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
});
return res.end();
}
if (!url.pathname.startsWith("/api")) return serveStatic(req, res, url.pathname);
try {
await ready;
for (const r of routes) {
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "leekwarslocal"
version = "0.6.0"
version = "0.6.1"
description = "A Tauri App"
authors = ["you"]
edition = "2021"
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "LeekWars Local",
"version": "0.6.0",
"version": "0.6.1",
"identifier": "com.steodec.leekwarslocal",
"build": {
"beforeDevCommand": "npm run dev",
Expand Down
Loading