MCP EasyWeb: srovnani adresy a srozumitelna hlaska u HTTP 400
Overeno proti bezicimu serveru (samolepak klon z jejich settings.json): /login vraci 400 Bad Request na kazde udaje - spravne heslo, spatne heslo, prazdne heslo i neznamy ucet dostanou tutez odpoved, telo je vzdycky jen "Bad Request" v HTML. Bez hlavicky Authorization vraci 401, takze handler udaje cte a odmita je az uvnitr. Tvar pozadavku tedy sedi: totez posila jejich vlastni WPF klient a stejny 400 vraci i holy curl. Zmeny jsou proto v tom, aby z toho clovek poznal, co se deje: - 400 i 401 z /login hlasi jako odmitnute prihlaseni a rovnou rikaji, ze tenhle server odpovida stejnym kodem i na neznamy ucet. Driv to bylo holé "vratilo HTTP 400", coz posilalo hledat chybu v adrese - adresa se u EasyWebu srovnava jako v jejich klientovi: vlepena prihlasovaci cesta se odrizne a chybejici /mcp se doplni. Server je na cestu prisny, i /mcp/login/ s lomitkem vraci 404 - u obecne sluzby se adresa nemeni. Cizi server muze mit endpoint kdekoliv a "opravit" mu ji podle naseho odhadu znamena rozbit napojeni, ktere by jinak fungovalo Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
81e4348ad8
commit
d881dab30d
@@ -103,6 +103,28 @@ Otisk zarizeni se doplnuje z ID konektoru (`worknuke-con_abc123`), aby server
|
||||
poznal, ze jde porad o totez zarizeni, a aby si dve napojeni tehoz portalu
|
||||
nesahala do sezeni.
|
||||
|
||||
### Adresa se u EasyWebu srovnava
|
||||
|
||||
Vlepena prihlasovaci cesta se odrizne a chybejici `/mcp` se doplni, takze
|
||||
`.../centaur/mcp/login` i `.../CentaurWebApp` skonci tam, kde ma. Server je na
|
||||
cestu prisny - i `/mcp/login/` s lomitkem na konci vraci 404.
|
||||
|
||||
**U obecne sluzby se adresa nemeni.** Cizi server muze mit endpoint kdekoliv
|
||||
a "opravit" mu adresu podle naseho odhadu znamena rozbit napojeni, ktere by
|
||||
jinak fungovalo.
|
||||
|
||||
### Kdyz server odpovi 400
|
||||
|
||||
`400 Bad Request` z `/login` **neni chyba pozadavku u nas**. Server ho ma jako
|
||||
"malformed authorization request" a odpovida jim i na udaje, ktere neuzna.
|
||||
Overeno proti bezicimu serveru: spatne heslo, prazdne heslo i neznamy ucet
|
||||
vraceji tutez odpoved jako udaje spravne, telo je vzdycky jen `Bad Request`
|
||||
v HTML. Rozlisit se to zvenku neda, takze to hlaska rika narovinu misto toho,
|
||||
aby posilala cloveka hledat chybu v adrese.
|
||||
|
||||
Rozdil oproti chybejicimu prihlaseni je videt: `/login` **bez** hlavicky
|
||||
`Authorization` vraci 401, s ni uz 400.
|
||||
|
||||
## Prihlaseni a zivotnost tokenu
|
||||
|
||||
Cely zivotni cyklus resi `src/mcp/auth.ts`. **Token je kratkodoby, jeho
|
||||
|
||||
+18
-4
@@ -23,7 +23,7 @@
|
||||
import { config } from '../config.js';
|
||||
import type { ResolvedTarget } from '../scripts/connections.js';
|
||||
import { truncate } from '../scripts/util.js';
|
||||
import { dialectFor } from './dialect.js';
|
||||
import { dialectFor, normalizeServerUrl } from './dialect.js';
|
||||
|
||||
/**
|
||||
* O kolik driv nez vyprsi se token vymeni.
|
||||
@@ -104,7 +104,9 @@ function credentialsOf(target: ResolvedTarget): Credentials {
|
||||
const value = (key: string): string => (target.serviceConfig[key] ?? '').trim();
|
||||
return {
|
||||
serviceId: target.serviceId,
|
||||
serverUrl: value('serverUrl'),
|
||||
// Srovnana, aby prihlasovaci endpointy sedly i kdyz clovek vlepil adresu
|
||||
// vcetne `/login` nebo bez `/mcp`.
|
||||
serverUrl: normalizeServerUrl(target.serviceId, value('serverUrl')),
|
||||
token: value('token'),
|
||||
clientId: value('clientId'),
|
||||
clientSecret: value('clientSecret'),
|
||||
@@ -266,9 +268,21 @@ async function easyWebLogin(credentials: Credentials, signal: AbortSignal): Prom
|
||||
|
||||
if (!response.ok) {
|
||||
const detail = truncate(await response.text(), config.errorDetailBytes);
|
||||
/*
|
||||
* 400 tady neni rozbity pozadavek u nas.
|
||||
*
|
||||
* Server ma `400 Bad Request` jako "malformed authorization request"
|
||||
* a odpovida jim i na udaje, ktere neuzna - overeno proti bezicimu
|
||||
* serveru, kde spatne heslo, prazdne heslo i neznamy ucet vraceji stejnou
|
||||
* odpoved jako spravne udaje. Rozlisit se to zvenku neda, takze hlaska
|
||||
* musi rict obojí, jinak clovek hleda chybu v adrese.
|
||||
*/
|
||||
throw new AuthFailure(
|
||||
response.status === 401
|
||||
? `Server jméno a heslo nepřijal (HTTP 401 z ${url}).`
|
||||
response.status === 401 || response.status === 400
|
||||
? `Server přihlášení nepřijal (HTTP ${response.status} z ${url}). ` +
|
||||
'Ověřte jméno a heslo a to, že účet na tomhle serveru existuje. ' +
|
||||
'Tenhle server odpovídá stejným kódem i na neznámý účet, takže víc ' +
|
||||
'z odpovědi poznat nejde.'
|
||||
: `Přihlášení na ${url} vrátilo HTTP ${response.status}.`,
|
||||
detail === '' ? null : detail,
|
||||
);
|
||||
|
||||
+2
-2
@@ -26,7 +26,7 @@ import { targetSecrets, type ResolvedTarget } from '../scripts/connections.js';
|
||||
import { isPrivateHost } from '../scripts/http.js';
|
||||
import { createRedactor, truncate } from '../scripts/util.js';
|
||||
import { AuthFailure, authorize, forgetToken, type AuthMethod } from './auth.js';
|
||||
import { dialectFor, type McpDialect } from './dialect.js';
|
||||
import { dialectFor, normalizeServerUrl, type McpDialect } from './dialect.js';
|
||||
|
||||
/**
|
||||
* Verzi protokolu urcuje druh serveru (`dialect.ts`).
|
||||
@@ -120,7 +120,7 @@ class McpFailure extends Error {
|
||||
* nechat navazat spojeni na cokoliv, co je z containeru videt.
|
||||
*/
|
||||
function serverUrl(target: ResolvedTarget): URL {
|
||||
const raw = (target.serviceConfig.serverUrl ?? '').trim();
|
||||
const raw = normalizeServerUrl(target.serviceId, target.serviceConfig.serverUrl ?? '');
|
||||
if (raw === '') throw new McpFailure('Adresa MCP serveru není vyplněná.');
|
||||
|
||||
let url: URL;
|
||||
|
||||
@@ -77,3 +77,50 @@ export function isMcpService(serviceId: string): boolean {
|
||||
export function dialectFor(serviceId: string): McpDialect {
|
||||
return dialects[serviceId] ?? dialects[MCP_SERVICE_ID];
|
||||
}
|
||||
|
||||
/**
|
||||
* Cesty, ktere u EasyWebu patri prihlaseni, ne samotnemu MCP.
|
||||
*
|
||||
* Kdyz je nekdo vlepi do adresy serveru, je to pochopitelna zamena: v
|
||||
* dokumentaci serveru jsou hned vedle sebe. Odriznou se.
|
||||
*/
|
||||
const AUTH_SUFFIXES = ['/login', '/logout', '/renew-access-token', '/renew-refresh-token'];
|
||||
|
||||
/**
|
||||
* Srovna adresu serveru do tvaru, ktery server ceka.
|
||||
*
|
||||
* **Jen u EasyWebu.** Ten ma pevnou cestu `/mcp` a vedle ni prihlasovaci
|
||||
* endpointy, takze jde poznat, co je spravne, a spravit dve nejcastejsi
|
||||
* zameny: vlepenou prihlasovaci cestu a chybejici `/mcp` na konci. Server je
|
||||
* na to prisny - `/mcp/login/` s lomitkem uz vraci 404.
|
||||
*
|
||||
* U obecne sluzby se **nemeni nic**. Cizi server muze mit endpoint kdekoliv
|
||||
* a "opravit" mu adresu podle naseho odhadu znamena rozbit napojeni, ktere by
|
||||
* jinak fungovalo.
|
||||
*/
|
||||
export function normalizeServerUrl(serviceId: string, raw: string): string {
|
||||
const trimmed = raw.trim();
|
||||
if (dialectFor(serviceId).auth !== 'easyweb') return trimmed;
|
||||
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(trimmed);
|
||||
} catch {
|
||||
// Neplatnou adresu resi az volajici, tady se nehada.
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
let path = url.pathname.replace(/\/+$/, '');
|
||||
for (const suffix of AUTH_SUFFIXES) {
|
||||
if (path.toLowerCase().endsWith(suffix)) {
|
||||
path = path.slice(0, -suffix.length);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!path.toLowerCase().endsWith('/mcp')) path = `${path}/mcp`;
|
||||
|
||||
url.pathname = path;
|
||||
url.search = '';
|
||||
url.hash = '';
|
||||
return url.toString().replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user