fix(nextcloud-files): WR-01 Adresspruefung folgt keiner Weiterleitung und meldet nur grob
- testAddress fragt status.php ueber ncRequest ab (keine Weiterleitung, Aufrufsperre, keine Zugangsdaten) statt ueber fetchNextcloudStatus mit bis zu drei Weiterleitungen - Ergebnis nur noch ok / maintenance / paused / locked / redirect / not-nextcloud / unreachable, ohne HTTP-Statuscode, Fehlerkuerzel oder Weiterleitungsziel - NEXTCLOUD_STATUS_FETCHER entfaellt; Specs mit Attrappen-Transport, Anleitung angepasst Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,49 +4,37 @@ vi.mock('../prisma/prisma-tenant.extension', () => ({
|
||||
forTenant: vi.fn((db: any, tenantId: string) => db.__bound(tenantId)),
|
||||
}));
|
||||
|
||||
import type { NextcloudCheckResult } from '../nextcloud-status/nextcloud-status-fetch';
|
||||
import { Readable } from 'node:stream';
|
||||
import { NextcloudCallGate } from './nextcloud-call-gate';
|
||||
import { NextcloudFilesSettingsService } from './nextcloud-files-settings.service';
|
||||
import type { NcTransportRequest, NcTransportResponse } from './nextcloud-http';
|
||||
|
||||
interface Row {
|
||||
baseUrl: string;
|
||||
}
|
||||
|
||||
function reachable(over: Partial<NextcloudCheckResult> = {}): NextcloudCheckResult {
|
||||
return {
|
||||
reachable: true,
|
||||
maintenance: false,
|
||||
needsDbUpgrade: false,
|
||||
versionString: '34.0.4',
|
||||
edition: '',
|
||||
productName: 'Nextcloud',
|
||||
errorKind: null,
|
||||
errorDetail: null,
|
||||
...over,
|
||||
};
|
||||
const STATUS_OK = {
|
||||
installed: true,
|
||||
maintenance: false,
|
||||
needsDbUpgrade: false,
|
||||
version: '34.0.4.1',
|
||||
versionstring: '34.0.4',
|
||||
edition: '',
|
||||
productname: 'Nextcloud',
|
||||
};
|
||||
|
||||
function reply(
|
||||
statusCode: number,
|
||||
body: unknown = '',
|
||||
headers: Record<string, string> = {},
|
||||
): NcTransportResponse {
|
||||
const text = typeof body === 'string' ? body : JSON.stringify(body);
|
||||
return { statusCode, headers, body: Readable.from(text === '' ? [] : [Buffer.from(text)]) };
|
||||
}
|
||||
|
||||
function unreachable(
|
||||
errorKind: NextcloudCheckResult['errorKind'],
|
||||
errorDetail: string | null = null,
|
||||
): NextcloudCheckResult {
|
||||
return {
|
||||
reachable: false,
|
||||
maintenance: null,
|
||||
needsDbUpgrade: null,
|
||||
versionString: null,
|
||||
edition: null,
|
||||
productName: null,
|
||||
errorKind,
|
||||
errorDetail,
|
||||
};
|
||||
}
|
||||
type Handler = (req: NcTransportRequest) => NcTransportResponse;
|
||||
|
||||
function makeService(opts: {
|
||||
row?: Row | null;
|
||||
activeAccounts?: number;
|
||||
check?: NextcloudCheckResult;
|
||||
}) {
|
||||
function makeService(opts: { row?: Row | null; activeAccounts?: number; handler?: Handler }) {
|
||||
const state = { row: opts.row ?? null };
|
||||
const upsert = vi.fn(async ({ create, update }: any) => {
|
||||
state.row = state.row ? { ...state.row, ...update } : { ...create };
|
||||
@@ -64,10 +52,14 @@ function makeService(opts: {
|
||||
};
|
||||
},
|
||||
};
|
||||
const fetcher = vi.fn(async (..._a: unknown[]) => opts.check ?? reachable());
|
||||
const calls: NcTransportRequest[] = [];
|
||||
const transport = vi.fn(async (req: NcTransportRequest) => {
|
||||
calls.push(req);
|
||||
return (opts.handler ?? (() => reply(200, STATUS_OK)))(req);
|
||||
});
|
||||
const gate = new NextcloudCallGate();
|
||||
const service = new NextcloudFilesSettingsService(db as any, gate, fetcher as any);
|
||||
return { service, state, upsert, updateMany, count, fetcher, gate, tenants };
|
||||
const service = new NextcloudFilesSettingsService(db as any, gate, transport);
|
||||
return { service, state, upsert, updateMany, count, calls, transport, gate, tenants };
|
||||
}
|
||||
|
||||
const codeOf = async (p: Promise<unknown>) => {
|
||||
@@ -163,7 +155,7 @@ describe('NextcloudFilesSettingsService — Speichern', () => {
|
||||
});
|
||||
|
||||
it('mit confirmReconnect: Adresse gespeichert, alle Konten der Organisation abgelaufen, Zuhoerer gerufen', async () => {
|
||||
const { service, upsert, updateMany, fetcher } = makeService({
|
||||
const { service, upsert, updateMany, calls } = makeService({
|
||||
row: { baseUrl: 'https://alt.example' },
|
||||
activeAccounts: 2,
|
||||
});
|
||||
@@ -182,8 +174,8 @@ describe('NextcloudFilesSettingsService — Speichern', () => {
|
||||
});
|
||||
expect(listener).toHaveBeenCalledWith('t1');
|
||||
expect(other).toHaveBeenCalledWith('t1');
|
||||
expect(fetcher).toHaveBeenCalledTimes(1);
|
||||
expect(fetcher.mock.calls[0][0]).toBe('https://neu.example');
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(calls[0].url).toBe('https://neu.example/status.php');
|
||||
expect(view.check?.ok).toBe(true);
|
||||
expect(view.connectedAccounts).toBe(0);
|
||||
});
|
||||
@@ -195,9 +187,9 @@ describe('NextcloudFilesSettingsService — Speichern', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('NextcloudFilesSettingsService — Verbindungspruefung', () => {
|
||||
it('erreichbar: ok mit Version', async () => {
|
||||
const { service, fetcher } = makeService({ check: reachable() });
|
||||
describe('NextcloudFilesSettingsService — Verbindungspruefung (WR-01: grob, ohne Weiterleitung)', () => {
|
||||
it('erreichbar: ok mit Version, ein einziger GET auf status.php ohne Zugangsdaten', async () => {
|
||||
const { service, calls } = makeService({});
|
||||
const res = await service.testAddress('https://cloud.example');
|
||||
expect(res).toMatchObject({
|
||||
ok: true,
|
||||
@@ -206,26 +198,57 @@ describe('NextcloudFilesSettingsService — Verbindungspruefung', () => {
|
||||
productName: 'Nextcloud',
|
||||
});
|
||||
expect(res.message).toContain('34.0.4');
|
||||
expect(fetcher.mock.calls[0][0]).toBe('https://cloud.example');
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(calls[0].method).toBe('GET');
|
||||
expect(calls[0].url).toBe('https://cloud.example/status.php');
|
||||
expect(calls[0].headers.authorization).toBeUndefined();
|
||||
expect(calls[0].headers.cookie).toBeUndefined();
|
||||
});
|
||||
|
||||
it('HTTP 400 ist der Hinweis auf die vertrauenswuerdigen Domains', async () => {
|
||||
const { service } = makeService({ check: unreachable('http-status', 'HTTP 400') });
|
||||
it('eine Weiterleitung wird NICHT befolgt; das Ergebnis nennt weder Ziel noch Status', async () => {
|
||||
const { service, calls } = makeService({
|
||||
handler: () => reply(302, '', { location: 'http://10.0.0.7:8080/secret' }),
|
||||
});
|
||||
const res = await service.testAddress('https://cloud.example');
|
||||
expect(res.ok).toBe(false);
|
||||
expect(res.kind).toBe('trusted-domains');
|
||||
expect(res.message).toContain('vertrauenswürdigen Domains');
|
||||
expect(res).toMatchObject({ ok: false, kind: 'redirect' });
|
||||
expect(calls).toHaveLength(1);
|
||||
const text = JSON.stringify(res);
|
||||
expect(text).not.toContain('10.0.0.7');
|
||||
expect(text).not.toContain('302');
|
||||
});
|
||||
|
||||
it('Zeitueberschreitung hat einen deutschen Text', async () => {
|
||||
const { service } = makeService({ check: unreachable('timeout') });
|
||||
const res = await service.testAddress('https://cloud.example');
|
||||
expect(res).toMatchObject({ ok: false, kind: 'timeout' });
|
||||
expect(res.message).toContain('nicht rechtzeitig');
|
||||
it('jeder andere HTTP-Fehler heisst nur "keine Nextcloud", ohne Statuscode', async () => {
|
||||
for (const status of [400, 401, 403, 404, 500, 502]) {
|
||||
const { service } = makeService({ handler: () => reply(status, 'interne Seite') });
|
||||
const res = await service.testAddress('https://cloud.example');
|
||||
expect(res.kind, String(status)).toBe('not-nextcloud');
|
||||
expect(JSON.stringify(res)).not.toContain(String(status));
|
||||
expect(JSON.stringify(res)).not.toContain('interne Seite');
|
||||
}
|
||||
});
|
||||
|
||||
it('keine Nextcloud-Antwort (200 ohne installed:true) ist not-nextcloud', async () => {
|
||||
const { service } = makeService({ handler: () => reply(200, '<html>Router</html>') });
|
||||
expect((await service.testAddress('https://cloud.example')).kind).toBe('not-nextcloud');
|
||||
});
|
||||
|
||||
it('Netzfehler, Zeitueberschreitung und Zertifikat sind gleichermassen "nicht erreichbar"', async () => {
|
||||
for (const code of ['ECONNREFUSED', 'UND_ERR_HEADERS_TIMEOUT', 'CERT_HAS_EXPIRED']) {
|
||||
const { service } = makeService({
|
||||
handler: () => {
|
||||
throw Object.assign(new Error('x'), { code });
|
||||
},
|
||||
});
|
||||
const res = await service.testAddress('https://cloud.example');
|
||||
expect(res, code).toMatchObject({ ok: false, kind: 'unreachable' });
|
||||
expect(JSON.stringify(res)).not.toContain(code);
|
||||
}
|
||||
});
|
||||
|
||||
it('Wartungsmodus ist kein Erfolg', async () => {
|
||||
const { service } = makeService({ check: reachable({ maintenance: true }) });
|
||||
const { service } = makeService({
|
||||
handler: () => reply(200, { ...STATUS_OK, maintenance: true }),
|
||||
});
|
||||
expect(await service.testAddress('https://cloud.example')).toMatchObject({
|
||||
ok: false,
|
||||
kind: 'maintenance',
|
||||
@@ -233,15 +256,15 @@ describe('NextcloudFilesSettingsService — Verbindungspruefung', () => {
|
||||
});
|
||||
|
||||
it('eine gesperrte Adresse (Aufrufsperre) fragt Nextcloud gar nicht erst', async () => {
|
||||
const { service, fetcher, gate } = makeService({});
|
||||
const { service, calls, gate } = makeService({});
|
||||
gate.pause('https://cloud.example');
|
||||
const res = await service.testAddress('https://cloud.example');
|
||||
expect(res).toMatchObject({ ok: false, kind: 'paused' });
|
||||
expect(fetcher).not.toHaveBeenCalled();
|
||||
expect(calls).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('ein 429 aus der Pruefung sperrt den Ursprung fuer alle weiteren Aufrufe', async () => {
|
||||
const { service, gate } = makeService({ check: unreachable('http-status', 'HTTP 429') });
|
||||
const { service, gate } = makeService({ handler: () => reply(429) });
|
||||
const res = await service.testAddress('https://cloud.example');
|
||||
expect(res.kind).toBe('locked');
|
||||
expect(gate.isPaused('https://cloud.example').paused).toBe(true);
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { fetch as undiciFetch } from 'undici';
|
||||
import {
|
||||
normalizeCloudUrl,
|
||||
parseNextcloudStatus,
|
||||
} from '../nextcloud-status/nextcloud-status-fetch';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { forTenant } from '../prisma/prisma-tenant.extension';
|
||||
import {
|
||||
type FetchStatusOptions,
|
||||
fetchNextcloudStatus,
|
||||
type NextcloudCheckResult,
|
||||
normalizeCloudUrl,
|
||||
} from '../nextcloud-status/nextcloud-status-fetch';
|
||||
import { NextcloudCallGate } from './nextcloud-call-gate';
|
||||
import {
|
||||
type NextcloudFilesCheckView,
|
||||
@@ -15,14 +12,17 @@ import {
|
||||
type NextcloudFilesStatusView,
|
||||
ncErrorDefault,
|
||||
} from './nextcloud-files.types';
|
||||
import {
|
||||
discardBody,
|
||||
NEXTCLOUD_TRANSPORT,
|
||||
type NextcloudTransport,
|
||||
ncRequest,
|
||||
readCappedText,
|
||||
} from './nextcloud-http';
|
||||
|
||||
/** Nest-Token fuer den Abruf von `status.php` (Tests setzen eine Attrappe ein). */
|
||||
export const NEXTCLOUD_STATUS_FETCHER = 'NEXTCLOUD_STATUS_FETCHER';
|
||||
export type NextcloudStatusFetcher = (
|
||||
baseUrl: string,
|
||||
opts?: FetchStatusOptions,
|
||||
) => Promise<NextcloudCheckResult>;
|
||||
export const defaultStatusFetcher: NextcloudStatusFetcher = fetchNextcloudStatus;
|
||||
/** Zeitgrenze und Groessendeckel der Adresspruefung (`status.php`). */
|
||||
const CHECK_TIMEOUT_MS = 10_000;
|
||||
const CHECK_MAX_BYTES = 64 * 1024;
|
||||
|
||||
export type AddressChangeListener = (tenantId: string) => void;
|
||||
|
||||
@@ -30,8 +30,12 @@ interface ConfigRow {
|
||||
baseUrl: string;
|
||||
}
|
||||
|
||||
const TRUSTED_DOMAINS_TEXT =
|
||||
'Nextcloud lehnt diese Adresse ab. Bitte nehmen Sie den Rechnernamen in die vertrauenswürdigen Domains (trusted_domains) der Nextcloud auf.';
|
||||
const NOT_NEXTCLOUD_TEXT =
|
||||
'Unter dieser Adresse antwortet keine Nextcloud. Steht dort doch eine Nextcloud, nehmen Sie bitte den Rechnernamen in ihre vertrauenswürdigen Domains (trusted_domains) auf.';
|
||||
const UNREACHABLE_TEXT =
|
||||
'Unter dieser Adresse ist keine Nextcloud erreichbar. Bitte prüfen Sie die Adresse. Bei https muss der Tessera-Server dem Zertifikat vertrauen (eigene Zertifizierungsstelle siehe Betriebshandbuch).';
|
||||
const REDIRECT_TEXT =
|
||||
'Die Adresse leitet auf eine andere Adresse weiter. Tessera folgt keiner Weiterleitung; tragen Sie bitte die endgültige Adresse ein (zum Beispiel mit https).';
|
||||
|
||||
function hostOf(baseUrl: string): string | null {
|
||||
try {
|
||||
@@ -41,14 +45,6 @@ function hostOf(baseUrl: string): string | null {
|
||||
}
|
||||
}
|
||||
|
||||
function originOf(baseUrl: string): string | null {
|
||||
try {
|
||||
return new URL(baseUrl).origin;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Einstellungen des Moduls "Nextcloud-Dateien" (quick-261008-mzu): die EINE
|
||||
* Nextcloud-Adresse der Organisation. Gesamter Zugriff auf `nextcloudFilesConfig`
|
||||
@@ -67,7 +63,7 @@ export class NextcloudFilesSettingsService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly gate: NextcloudCallGate,
|
||||
@Inject(NEXTCLOUD_STATUS_FETCHER) private readonly statusFetcher: NextcloudStatusFetcher,
|
||||
@Inject(NEXTCLOUD_TRANSPORT) private readonly transport: NextcloudTransport,
|
||||
) {}
|
||||
|
||||
/** Meldet einen Zuhoerer an, der nach einem Adresswechsel mit der Organisation aufgerufen wird. */
|
||||
@@ -149,48 +145,20 @@ export class NextcloudFilesSettingsService {
|
||||
// --- Verbindungspruefung -----------------------------------------------------
|
||||
|
||||
/**
|
||||
* Fragt `status.php` der Adresse ab (ohne Zugangsdaten). Antwortet immer mit
|
||||
* `{ ok, kind, message, version, productName }`; Netz- und HTTP-Probleme sind
|
||||
* keine Serverfehler. Steht der Ursprung wegen eines 429 auf der Aufrufsperre,
|
||||
* geht gar keine Anfrage raus.
|
||||
* Fragt `status.php` der Adresse ab (ohne Zugangsdaten) — ueber `ncRequest`, also
|
||||
* mit denselben Regeln wie jeder andere Nextcloud-Aufruf: KEINE Weiterleitung wird
|
||||
* befolgt, und steht der Ursprung wegen eines 429 auf der Aufrufsperre, geht gar
|
||||
* keine Anfrage raus.
|
||||
*
|
||||
* Das Ergebnis ist bewusst GROB (WR-01): erreichbare Nextcloud (`ok`), Wartung,
|
||||
* gesperrt, Weiterleitung auf eine andere Adresse (`redirect`), keine Nextcloud
|
||||
* (`not-nextcloud`) oder nicht erreichbar (`unreachable`). Kein HTTP-Statuscode,
|
||||
* kein Fehlerkuerzel, kein Ziel einer Weiterleitung verlaesst den Server — die
|
||||
* Pruefung taugt so nicht zum Abtasten interner Adressen und Dienste.
|
||||
*/
|
||||
async testAddress(rawBaseUrl: string): Promise<NextcloudFilesCheckView> {
|
||||
const baseUrl = normalizeCloudUrl(rawBaseUrl);
|
||||
if (baseUrl === null) throw ncErrorDefault('invalidUrl');
|
||||
const origin = originOf(baseUrl);
|
||||
|
||||
if (origin) {
|
||||
const pause = this.gate.isPaused(origin);
|
||||
if (pause.paused) {
|
||||
const minutes = Math.max(1, Math.ceil(pause.retryAfterSeconds / 60));
|
||||
return {
|
||||
ok: false,
|
||||
kind: 'paused',
|
||||
message: `Nextcloud sperrt Anmeldungen vom Tessera-Server zurzeit. Bitte warten Sie etwa ${minutes} Minuten.`,
|
||||
version: null,
|
||||
productName: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Merkt, ob die Adresse nur ueber eine Weiterleitung antwortet: im Betrieb folgt Tessera
|
||||
// keiner Weiterleitung, die Adresse muss dann direkt eingetragen werden.
|
||||
let redirected = false;
|
||||
const recordingFetch = (async (...args: Parameters<typeof undiciFetch>) => {
|
||||
const response = await undiciFetch(...args);
|
||||
if (response.status >= 300 && response.status < 400) redirected = true;
|
||||
return response;
|
||||
}) as typeof undiciFetch;
|
||||
|
||||
const result = await this.statusFetcher(baseUrl, { fetchImpl: recordingFetch });
|
||||
return this.describeCheck(result, redirected, origin);
|
||||
}
|
||||
|
||||
private describeCheck(
|
||||
result: NextcloudCheckResult,
|
||||
redirected: boolean,
|
||||
origin: string | null,
|
||||
): NextcloudFilesCheckView {
|
||||
const fail = (kind: string, message: string): NextcloudFilesCheckView => ({
|
||||
ok: false,
|
||||
kind,
|
||||
@@ -199,64 +167,53 @@ export class NextcloudFilesSettingsService {
|
||||
productName: null,
|
||||
});
|
||||
|
||||
if (result.reachable) {
|
||||
if (result.maintenance) {
|
||||
return fail('maintenance', 'Nextcloud befindet sich im Wartungsmodus.');
|
||||
const res = await ncRequest(this.transport, this.gate, {
|
||||
baseUrl,
|
||||
prefix: '/status.php',
|
||||
method: 'GET',
|
||||
headers: { accept: 'application/json' },
|
||||
headersTimeoutMs: CHECK_TIMEOUT_MS,
|
||||
bodyTimeoutMs: CHECK_TIMEOUT_MS,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
if (res.kind === 'paused' || (res.kind === 'http' && res.status === 429)) {
|
||||
const minutes = Math.max(1, Math.ceil((res.retryAfterSeconds ?? 900) / 60));
|
||||
return fail(
|
||||
res.kind === 'paused' ? 'paused' : 'locked',
|
||||
`Nextcloud sperrt Anmeldungen vom Tessera-Server zurzeit. Bitte warten Sie etwa ${minutes} Minuten.`,
|
||||
);
|
||||
}
|
||||
const name = result.productName ?? 'Nextcloud';
|
||||
const version = result.versionString;
|
||||
const base = version
|
||||
? `Verbindung erfolgreich: ${name} ${version}.`
|
||||
: 'Verbindung erfolgreich.';
|
||||
const hint = redirected
|
||||
? ' Die Adresse leitet allerdings um. Tessera folgt keiner Weiterleitung, tragen Sie bitte die endgültige Adresse ein (zum Beispiel mit https).'
|
||||
: '';
|
||||
return {
|
||||
ok: true,
|
||||
kind: redirected ? 'redirected' : 'ok',
|
||||
message: base + hint,
|
||||
version,
|
||||
productName: result.productName,
|
||||
};
|
||||
if (res.kind === 'redirect') return fail('redirect', REDIRECT_TEXT);
|
||||
return fail('unreachable', UNREACHABLE_TEXT);
|
||||
}
|
||||
|
||||
switch (result.errorKind) {
|
||||
case 'timeout':
|
||||
return fail(
|
||||
'timeout',
|
||||
'Nextcloud hat nicht rechtzeitig geantwortet. Bitte prüfen Sie die Adresse und versuchen Sie es erneut.',
|
||||
);
|
||||
case 'network':
|
||||
return fail('network', 'Unter dieser Adresse ist keine Nextcloud erreichbar.');
|
||||
case 'tls':
|
||||
return fail(
|
||||
'tls',
|
||||
'Das Zertifikat der Nextcloud konnte nicht geprüft werden. Bitte prüfen Sie die Adresse oder hinterlegen Sie die Zertifizierungsstelle auf dem Tessera-Server.',
|
||||
);
|
||||
case 'http-status': {
|
||||
if (result.errorDetail === 'HTTP 400') return fail('trusted-domains', TRUSTED_DOMAINS_TEXT);
|
||||
if (result.errorDetail === 'HTTP 429') {
|
||||
if (origin) this.gate.pause(origin);
|
||||
return fail(
|
||||
'locked',
|
||||
'Nextcloud sperrt Anmeldungen vom Tessera-Server vorübergehend. Bitte versuchen Sie es in einigen Minuten erneut.',
|
||||
);
|
||||
}
|
||||
return fail(
|
||||
'http-status',
|
||||
`Nextcloud hat unter dieser Adresse mit einem Fehler geantwortet (${result.errorDetail ?? 'unbekannt'}).`,
|
||||
);
|
||||
}
|
||||
case 'redirect':
|
||||
return fail(
|
||||
'redirect',
|
||||
'Die Adresse leitet mehrfach um oder führt nicht zu einer Nextcloud. Bitte prüfen Sie sie.',
|
||||
);
|
||||
case 'too-large':
|
||||
case 'not-nextcloud':
|
||||
return fail('not-nextcloud', 'Unter dieser Adresse antwortet keine Nextcloud.');
|
||||
default:
|
||||
return fail('network', 'Unter dieser Adresse ist keine Nextcloud erreichbar.');
|
||||
if (res.status === 503) {
|
||||
discardBody(res.body);
|
||||
return fail('maintenance', 'Nextcloud befindet sich im Wartungsmodus.');
|
||||
}
|
||||
if (res.status < 200 || res.status >= 300) {
|
||||
discardBody(res.body);
|
||||
return fail('not-nextcloud', NOT_NEXTCLOUD_TEXT);
|
||||
}
|
||||
const text = await readCappedText(res.body, CHECK_MAX_BYTES);
|
||||
if (!text.ok) {
|
||||
return text.kind === 'too-large'
|
||||
? fail('not-nextcloud', NOT_NEXTCLOUD_TEXT)
|
||||
: fail('unreachable', UNREACHABLE_TEXT);
|
||||
}
|
||||
const parsed = parseNextcloudStatus(text.text);
|
||||
if (!parsed) return fail('not-nextcloud', NOT_NEXTCLOUD_TEXT);
|
||||
if (parsed.maintenance) return fail('maintenance', 'Nextcloud befindet sich im Wartungsmodus.');
|
||||
|
||||
const name = parsed.productName ?? 'Nextcloud';
|
||||
const version = parsed.versionString;
|
||||
return {
|
||||
ok: true,
|
||||
kind: 'ok',
|
||||
message: version ? `Verbindung erfolgreich: ${name} ${version}.` : 'Verbindung erfolgreich.',
|
||||
version,
|
||||
productName: parsed.productName,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,7 @@ import { NextcloudFilesController } from './nextcloud-files.controller';
|
||||
import { seedNextcloudFilesModule } from './nextcloud-files.seed';
|
||||
import { NextcloudFilesService } from './nextcloud-files.service';
|
||||
import { NextcloudFilesAccountService } from './nextcloud-files-account.service';
|
||||
import {
|
||||
defaultStatusFetcher,
|
||||
NEXTCLOUD_STATUS_FETCHER,
|
||||
NextcloudFilesSettingsService,
|
||||
} from './nextcloud-files-settings.service';
|
||||
import { NextcloudFilesSettingsService } from './nextcloud-files-settings.service';
|
||||
import { NextcloudFilesTransferService } from './nextcloud-files-transfer.service';
|
||||
import { NEXTCLOUD_TRANSPORT, undiciTransport } from './nextcloud-http';
|
||||
import { LoginFlowStore, NextcloudLoginGuard } from './nextcloud-login-guard';
|
||||
@@ -36,7 +32,6 @@ import { NextcloudServerInfoService } from './nextcloud-server-info';
|
||||
LoginFlowStore,
|
||||
NextcloudCallGate,
|
||||
{ provide: NEXTCLOUD_TRANSPORT, useValue: undiciTransport },
|
||||
{ provide: NEXTCLOUD_STATUS_FETCHER, useValue: defaultStatusFetcher },
|
||||
],
|
||||
exports: [
|
||||
NextcloudCallGate,
|
||||
|
||||
@@ -360,8 +360,8 @@ Das Modul „Nextcloud-Status“ (Gruppe Infrastruktur) aktivieren Sie wie jedes
|
||||
|
||||
Das Modul „Dateien“ (Gruppe Infrastruktur) aktivieren Sie wie jedes Modul im Marktplatz; wer es nutzen soll, bekommt die Freigabe „Benutzen“. **Die Adresse der Nextcloud legt ausschließlich ein Administrator fest** (Reiter „Einstellungen“ im Modul). Der Grund: Alle Benutzer schicken ihr Nextcloud-Passwort an diese Adresse, und das ist oft zugleich ihr Windows-Passwort. Benutzer mit der Freigabestufe „Verwalten“ sehen den Reiter ebenfalls, können die eingetragene Adresse aber nur ansehen und mit „Verbindung prüfen“ testen, nicht ändern. Wer nur „Benutzen“ hat, sieht den Reiter nicht.
|
||||
|
||||
- **Adresse:** Tragen Sie die Adresse so ein, wie Ihre Benutzer die Nextcloud im Browser aufrufen, möglichst mit `https://` (zum Beispiel `https://cloud.ihre-firma.de`). Es gibt genau eine Nextcloud für die ganze Installation. Interne Adressen sind erlaubt. „Verbindung prüfen“ ruft nur die öffentliche Statusseite ab und nennt Version oder den Grund, warum es nicht klappt. Tessera folgt nie einer Weiterleitung: Leitet die Adresse um (zum Beispiel von `http` auf `https`), tragen Sie die endgültige Adresse ein.
|
||||
- **Vertrauenswürdige Domains:** Meldet die Prüfung „Nextcloud lehnt diese Adresse ab“, muss der Rechnername der Adresse in der Nextcloud unter `trusted_domains` stehen (`config.php`, oder `occ config:system:set trusted_domains 1 --value=<Rechnername>`).
|
||||
- **Adresse:** Tragen Sie die Adresse so ein, wie Ihre Benutzer die Nextcloud im Browser aufrufen, möglichst mit `https://` (zum Beispiel `https://cloud.ihre-firma.de`). Es gibt genau eine Nextcloud für die ganze Installation. Interne Adressen sind erlaubt. „Verbindung prüfen“ ruft nur die öffentliche Statusseite ab und meldet bewusst nur grob: Nextcloud erreichbar (mit Version), Wartungsmodus, „leitet weiter“, „keine Nextcloud“ oder „nicht erreichbar“. Fehlernummern oder Ziele einer Weiterleitung nennt Tessera nicht, damit sich die Prüfung nicht zum Abtasten interner Adressen eignet. Tessera folgt nie einer Weiterleitung: Leitet die Adresse um (zum Beispiel von `http` auf `https`), tragen Sie die endgültige Adresse ein.
|
||||
- **Vertrauenswürdige Domains:** Meldet die Prüfung „keine Nextcloud“, obwohl unter der Adresse eine Nextcloud läuft, muss der Rechnername der Adresse in der Nextcloud unter `trusted_domains` stehen (`config.php`, oder `occ config:system:set trusted_domains 1 --value=<Rechnername>`).
|
||||
- **Zugang der Benutzer:** Jeder Benutzer meldet sich mit seinem eigenen Nextcloud-Konto an. Tessera speichert nur ein von Nextcloud ausgestelltes App-Passwort (verschlüsselt), nie das Passwort. Konten mit Zwei-Faktor-Anmeldung melden sich über den Browser an; das Passwortfenster genügt für sie nicht.
|
||||
- **Ausnahme für den Tessera-Server in der Nextcloud (wichtig):** Nextcloud sperrt nach mehreren fehlgeschlagenen Anmeldungen die Absender-Adresse (Brute-Force-Schutz). Alle Tessera-Benutzer kommen von **derselben Adresse**, nämlich der des Tessera-Servers; eine Sperre träfe also alle gleichzeitig. Tessera begrenzt Fehlversuche deshalb selbst (höchstens drei je Benutzer in 15 Minuten und acht für den ganzen Server in 30 Minuten) und **hält alle Anfragen an die Nextcloud für bis zu 15 Minuten an**, sobald Nextcloud „zu viele Anfragen“ meldet; Benutzer sehen dann eine Wartezeit. Tragen Sie die Adresse des Tessera-Servers in die Ausnahmeliste der Nextcloud ein, dann kommt es dazu gar nicht erst. Auf dem Nextcloud-Server:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user