Initial commit: WebDAV-Adapter für Internxt Drive
- Browser-Token-Auth (INXT_TOKEN, INXT_MNEMONIC) - Phase 1: PROPFIND (Verzeichnis auflisten) - Drive API + Pfad-Resolver - Dokumentation: Auth, Architektur, WSL Made-with: Cursor
This commit is contained in:
17
.env.example
Normal file
17
.env.example
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Internxt API (Production - aus internxt/cli .env.template)
|
||||||
|
DRIVE_API_URL=https://gateway.internxt.com/drive
|
||||||
|
|
||||||
|
# Crypto secret - CLI: 6KYQBP847D4ATSFA
|
||||||
|
# drive-web nutzt REACT_APP_CRYPTO_SECRET (evtl. anderer Wert - aus drive.internxt.com JS extrahieren)
|
||||||
|
CRYPTO_SECRET=6KYQBP847D4ATSFA
|
||||||
|
|
||||||
|
# DEBUG=1 # Salt-Decryption testen (ob CRYPTO_SECRET stimmt)
|
||||||
|
|
||||||
|
# Test credentials (für auth-poc.js)
|
||||||
|
INXT_EMAIL=
|
||||||
|
INXT_PASSWORD=
|
||||||
|
# INXT_2FA= # Bei 2FA: aktuellen Code eingeben
|
||||||
|
|
||||||
|
# Browser-Token (für token-test.js und WebDAV) – aus drive.internxt.com localStorage
|
||||||
|
# INXT_TOKEN= # xNewToken
|
||||||
|
# INXT_MNEMONIC= # xMnemonic (für Datei-Entschlüsselung)
|
||||||
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Environment (Secrets – Token, Mnemonic)
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Build / Cache
|
||||||
|
dist/
|
||||||
|
.cache/
|
||||||
62
README.md
Normal file
62
README.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# Internxt WebDAV Wrapper
|
||||||
|
|
||||||
|
WebDAV-Zugang zu Internxt Drive für Account-Tiers, die keinen CLI- oder Rclone-Zugang haben.
|
||||||
|
|
||||||
|
## Hintergrund
|
||||||
|
|
||||||
|
Internxt blockiert für bestimmte Account-Typen (z.B. Free, Partner-Accounts) den Zugang über:
|
||||||
|
- Internxt CLI (`cli access not allowed for this user tier`)
|
||||||
|
- Rclone Native-Backend (`rclone access not allowed for this user tier`, Status 402)
|
||||||
|
- Docker-Image `internxt/webdav` (nutzt dieselbe Auth)
|
||||||
|
|
||||||
|
**Lösung:** Das Web-UI (drive.internxt.com) funktioniert – es nutzt `login()` mit `clientName: "drive-web"`. Der Wrapper imitiert diese Auth.
|
||||||
|
|
||||||
|
## Auth-Proof-of-Concept
|
||||||
|
|
||||||
|
1. Abhängigkeiten installieren:
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
2. `.env` anlegen (von `.env.example` kopieren):
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Credentials eintragen:
|
||||||
|
```
|
||||||
|
INXT_EMAIL=deine@email.de
|
||||||
|
INXT_PASSWORD=dein_passwort
|
||||||
|
INXT_2FA=123456 # Falls 2FA aktiv
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Auth testen:
|
||||||
|
```bash
|
||||||
|
npm run auth-test
|
||||||
|
```
|
||||||
|
|
||||||
|
Bei Erfolg erscheint „Login erfolgreich!“ – dann kann der WebDAV-Server gebaut werden.
|
||||||
|
|
||||||
|
## Dokumentation
|
||||||
|
|
||||||
|
- [docs/auth-analysis.md](docs/auth-analysis.md) – Analyse Web vs CLI Auth, clientName-Unterschied
|
||||||
|
|
||||||
|
## Browser-Token-Auth (Alternative)
|
||||||
|
|
||||||
|
Falls der API-Login blockiert ist (z.B. Partner-Account):
|
||||||
|
|
||||||
|
1. Auf https://drive.internxt.com einloggen
|
||||||
|
2. DevTools (F12) → Console: `localStorage.getItem('xNewToken')` und `localStorage.getItem('xMnemonic')` ausführen
|
||||||
|
3. Werte in `.env` als `INXT_TOKEN` und `INXT_MNEMONIC` eintragen
|
||||||
|
4. Testen: `npm run token-test`
|
||||||
|
|
||||||
|
Details: [docs/browser-token-auth.md](docs/browser-token-auth.md)
|
||||||
|
|
||||||
|
## WSL (login mit Keys)
|
||||||
|
|
||||||
|
Unter Windows schlägt Kyber-WASM fehl. Unter WSL: [docs/wsl-setup.md](docs/wsl-setup.md)
|
||||||
|
|
||||||
|
## Nächste Schritte
|
||||||
|
|
||||||
|
- WebDAV-Server mit Token-Auth implementieren
|
||||||
|
- Storage-Client für Datei-Operationen anbinden
|
||||||
98
docs/auth-analysis.md
Normal file
98
docs/auth-analysis.md
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
# Internxt Auth-Analyse: Web vs CLI vs Rclone
|
||||||
|
|
||||||
|
## Kernbefund: Client-Identifikation bestimmt Zugang
|
||||||
|
|
||||||
|
Der Backend-Server blockiert bestimmte Account-Tiers basierend auf der **Client-Identifikation**:
|
||||||
|
|
||||||
|
| Client | clientName | Login-Methode | Endpoint | Status für eingeschränkte Tiers |
|
||||||
|
|--------|-----------|---------------|----------|--------------------------------|
|
||||||
|
| **drive-web** | `drive-web` | `login()` | `/auth/login` | ✅ Erlaubt |
|
||||||
|
| **drive-desktop** | `drive-desktop` | `login()` | `/auth/login` | ✅ Erlaubt |
|
||||||
|
| **internxt-cli** | `internxt-cli` | `loginAccess()` | `/auth/login/access` | ❌ Blockiert |
|
||||||
|
| **rclone** | (rclone-adapter) | loginAccess-ähnlich | `/auth/login/access` | ❌ Blockiert |
|
||||||
|
|
||||||
|
## Quellen
|
||||||
|
|
||||||
|
### drive-web ([auth.service.ts](drive-web/src/services/auth.service.ts))
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const getAuthClient = (authType: 'web' | 'desktop') => {
|
||||||
|
const AUTH_CLIENT = {
|
||||||
|
web: SdkFactory.getNewApiInstance().createAuthClient(), // clientName: "drive-web"
|
||||||
|
desktop: SdkFactory.getNewApiInstance().createDesktopAuthClient(), // clientName: "drive-desktop"
|
||||||
|
};
|
||||||
|
return AUTH_CLIENT[authType];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Login mit authClient.login() - NICHT loginAccess()
|
||||||
|
return authClient.login(loginDetails, cryptoProvider)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **createAuthClient()**: `clientName: packageJson.name` = `"drive-web"`
|
||||||
|
- **createDesktopAuthClient()**: `clientName: "drive-desktop"`
|
||||||
|
- **Methode**: `login()` (nicht `loginAccess`)
|
||||||
|
|
||||||
|
### CLI ([auth.service.ts](https://github.com/internxt/cli/blob/main/src/services/auth.service.ts))
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const authClient = SdkManager.instance.getAuth();
|
||||||
|
const data = await authClient.loginAccess(loginDetails, CryptoService.cryptoProvider);
|
||||||
|
```
|
||||||
|
|
||||||
|
- **getAppDetails()**: `clientName: packageJson.clientName` = `"internxt-cli"` (aus [package.json](https://github.com/internxt/cli/blob/main/package.json))
|
||||||
|
- **Methode**: `loginAccess()` (nicht `login`)
|
||||||
|
|
||||||
|
### SDK Factory ([drive-web](drive-web/src/app/core/factory/sdk/index.ts))
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
private static getAppDetails(): AppDetails {
|
||||||
|
return {
|
||||||
|
clientName: packageJson.name, // "drive-web"
|
||||||
|
clientVersion: packageJson.version,
|
||||||
|
customHeaders,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static getDesktopAppDetails(): AppDetails {
|
||||||
|
return {
|
||||||
|
clientName: 'drive-desktop',
|
||||||
|
clientVersion: packageJson.version,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lösung für WebDAV-Wrapper
|
||||||
|
|
||||||
|
**Strategie:** Den Auth-Client so konfigurieren, dass er sich als `drive-web` ausgibt und `login()` statt `loginAccess()` verwendet.
|
||||||
|
|
||||||
|
1. **@internxt/sdk** mit `Auth.client(apiUrl, appDetails, apiSecurity)` verwenden
|
||||||
|
2. **appDetails** setzen: `{ clientName: "drive-web", clientVersion: "1.0" }`
|
||||||
|
3. **login()** aufrufen (nicht `loginAccess()`)
|
||||||
|
4. CryptoProvider wie in drive-web implementieren (passToHash, decryptText, getKeys, parseAndDecryptUserKeys)
|
||||||
|
|
||||||
|
## Abhängigkeiten für WebDAV-Wrapper
|
||||||
|
|
||||||
|
- `@internxt/sdk` (Version 1.13.x oder kompatibel – drive-web nutzt 1.13.2)
|
||||||
|
- `@internxt/lib` (für aes, Crypto)
|
||||||
|
- Crypto-Logik aus drive-web: `app/crypto/services/keys.service`, `app/crypto/services/utils`
|
||||||
|
- Keys-Format: ECC + Kyber (post-quantum)
|
||||||
|
|
||||||
|
## Aktueller Status (Stand: Analyse)
|
||||||
|
|
||||||
|
- **CRYPTO_SECRET**: Korrekt (Salt-Decryption OK mit `6KYQBP847D4ATSFA`)
|
||||||
|
- **loginWithoutKeys**: Liefert weiterhin "Wrong login credentials" – möglicherweise lehnt das Backend diesen Flow für bestimmte Account-Typen (z.B. mailbox.org-Partner) ab
|
||||||
|
- **login() mit Keys**: Kyber-WASM schlägt unter Windows fehl (`@dashlane/pqc-kem-kyber512-node`)
|
||||||
|
|
||||||
|
## Nächste Schritte
|
||||||
|
|
||||||
|
1. **Ansatz B testen**: Browser-basierter Token-Extrakt – im Web einloggen, Session-Token aus localStorage/DevTools lesen, im Wrapper verwenden
|
||||||
|
2. **login() unter Linux**: Kyber-Paket könnte unter Linux funktionieren
|
||||||
|
3. **Internxt-Support**: Nachfragen, ob Partner-Accounts (mailbox.org) andere Auth-Flows nutzen
|
||||||
|
|
||||||
|
## CRYPTO_SECRET und API-URL
|
||||||
|
|
||||||
|
Aus [internxt/cli .env.template](https://github.com/internxt/cli/blob/main/.env.template):
|
||||||
|
- **DRIVE_API_URL**: `https://gateway.internxt.com/drive`
|
||||||
|
- **APP_CRYPTO_SECRET**: `6KYQBP847D4ATSFA`
|
||||||
|
|
||||||
|
Der PoC nutzt diese Werte als Fallback.
|
||||||
68
docs/browser-token-auth.md
Normal file
68
docs/browser-token-auth.md
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
# Browser-Token-Authentifizierung (Ansatz B)
|
||||||
|
|
||||||
|
Da der API-Login für Ihren Account-Typ blockiert ist, können Sie sich im Browser einloggen und die Session-Daten für den WebDAV-Wrapper verwenden.
|
||||||
|
|
||||||
|
## Ablauf
|
||||||
|
|
||||||
|
1. Auf https://drive.internxt.com einloggen
|
||||||
|
2. Token und Mnemonic aus dem Browser extrahieren
|
||||||
|
3. In `.env` eintragen
|
||||||
|
4. WebDAV-Server starten
|
||||||
|
|
||||||
|
## Token extrahieren
|
||||||
|
|
||||||
|
### Schritt 1: Alle gespeicherten Keys anzeigen
|
||||||
|
|
||||||
|
Auf **https://drive.internxt.com** eingeloggt sein. DevTools (F12) → **Console**:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Alle localStorage-Keys anzeigen
|
||||||
|
Object.keys(localStorage).filter(k => k.includes('x') || k.includes('token') || k.includes('Token')).forEach(k => console.log(k));
|
||||||
|
```
|
||||||
|
|
||||||
|
Damit sehen Sie, welche Keys es gibt (z.B. `xNewToken`, `xMnemonic`, `xUser`).
|
||||||
|
|
||||||
|
### Schritt 2: Token und Mnemonic auslesen
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Token und Mnemonic anzeigen
|
||||||
|
console.log('Token:', localStorage.getItem('xNewToken') || localStorage.getItem('xToken') || '(nicht gefunden)');
|
||||||
|
console.log('Mnemonic:', localStorage.getItem('xMnemonic') || '(nicht gefunden)');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Schritt 3: Falls nichts gefunden wird
|
||||||
|
|
||||||
|
- **Application-Tab prüfen:** DevTools → **Application** (oder **Anwendung**) → links **Local Storage** → **https://drive.internxt.com** auswählen. Dort alle Einträge durchsehen.
|
||||||
|
- **Richtige URL:** Sie müssen auf `https://drive.internxt.com` sein (nicht internxt.com) und **eingeloggt** sein – nach dem Login auf `/drive` oder `/app`.
|
||||||
|
- **Session vs. Local:** Manche Werte liegen in `sessionStorage`. Testen mit:
|
||||||
|
```javascript
|
||||||
|
console.log('sessionStorage:', Object.keys(sessionStorage));
|
||||||
|
```
|
||||||
|
- **Alle Keys anzeigen:** Zum Debuggen alle Keys mit Werten:
|
||||||
|
```javascript
|
||||||
|
for (let i = 0; i < localStorage.length; i++) {
|
||||||
|
const k = localStorage.key(i);
|
||||||
|
console.log(k + ':', localStorage.getItem(k)?.substring(0, 50) + '...');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## .env eintragen
|
||||||
|
|
||||||
|
```
|
||||||
|
INXT_TOKEN=eyJhbGciOiJIUzI1NiIs...
|
||||||
|
INXT_MNEMONIC=word1 word2 word3 ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## WebDAV-Server starten
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
Server läuft auf `http://127.0.0.1:3005`. Phase 1 (PROPFIND) ist aktiv – Verzeichnisinhalt kann aufgelistet werden. Windows Explorer: Netzlaufwerk verbinden → `http://127.0.0.1:3005`.
|
||||||
|
|
||||||
|
## Hinweise
|
||||||
|
|
||||||
|
- **Token-Ablauf**: Tokens laufen nach einiger Zeit ab (typisch Stunden). Bei 401-Fehlern erneut einloggen und Token aktualisieren.
|
||||||
|
- **Sicherheit**: Mnemonic und Token sind hochsensibel. Nicht in Git committen, `.env` in `.gitignore` belassen.
|
||||||
|
- **Nur für Sie**: Die Tokens sind an Ihre Session gebunden. Für andere Nutzer funktioniert dieser Ansatz nicht.
|
||||||
35
docs/crypto-secret-extract.md
Normal file
35
docs/crypto-secret-extract.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# CRYPTO_SECRET aus drive.internxt.com ermitteln
|
||||||
|
|
||||||
|
Falls der Login mit "Wrong login credentials" fehlschlägt, ist vermutlich der `CRYPTO_SECRET` falsch. drive-web nutzt `REACT_APP_CRYPTO_SECRET`, der CLI-Wert (`6KYQBP847D4ATSFA`) kann abweichen.
|
||||||
|
|
||||||
|
## Methode 1: DEBUG-Modus (Salt-Decryption prüfen)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DEBUG=1 npm run auth-test
|
||||||
|
```
|
||||||
|
|
||||||
|
- **"Salt-Decryption OK"** → CRYPTO_SECRET stimmt, Problem liegt woanders (Passwort, API)
|
||||||
|
- **"Salt-Decryption fehlgeschlagen"** → CRYPTO_SECRET ist falsch
|
||||||
|
|
||||||
|
## Methode 2: Secret im Browser suchen
|
||||||
|
|
||||||
|
1. https://drive.internxt.com öffnen
|
||||||
|
2. DevTools (F12) → **Sources**
|
||||||
|
3. **Strg+Shift+F** (Suche in allen Dateien)
|
||||||
|
4. Suchen nach:
|
||||||
|
- `6KYQBP847D4ATSFA` – falls gefunden, wird derselbe Wert wie beim CLI genutzt
|
||||||
|
- `REACT_APP_CRYPTO_SECRET` oder `CRYPTO_SECRET`
|
||||||
|
- Hex-Strings (z.B. 16 Zeichen wie `a1b2c3d4e5f6...`)
|
||||||
|
|
||||||
|
5. Gefundenen Wert in `.env` eintragen:
|
||||||
|
```
|
||||||
|
CRYPTO_SECRET=gefundener_wert
|
||||||
|
```
|
||||||
|
|
||||||
|
## Methode 3: drive-web lokal bauen (mit bekanntem Secret)
|
||||||
|
|
||||||
|
Falls Sie Zugriff auf drive-web haben und den korrekten Secret kennen:
|
||||||
|
|
||||||
|
1. In `drive-web` eine `.env` mit `REACT_APP_CRYPTO_SECRET=...` anlegen
|
||||||
|
2. `yarn build` ausführen
|
||||||
|
3. In den Build-Artefakten nach dem eingebetteten Wert suchen
|
||||||
74
docs/webdav-architektur.md
Normal file
74
docs/webdav-architektur.md
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
# WebDAV-Server Architektur
|
||||||
|
|
||||||
|
## Empfohlener Ansatz: Adapter (nicht Proxy)
|
||||||
|
|
||||||
|
Der WebDAV-Server ist ein **Adapter**: Er implementiert das WebDAV-Protokoll und übersetzt Anfragen in Internxt Drive API + Network-Aufrufe. Es wird **nicht** zu einem anderen WebDAV-Server weitergeleitet.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
subgraph Client [WebDAV Client]
|
||||||
|
Finder[Finder/Cyberduck]
|
||||||
|
Rclone[Rclone]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Adapter [WebDAV Adapter]
|
||||||
|
WebDAV[WebDAV Server]
|
||||||
|
Mapping[Path zu UUID]
|
||||||
|
Crypto[Encrypt/Decrypt]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Internxt [Internxt Backend]
|
||||||
|
DriveAPI[Drive API]
|
||||||
|
Network[Bridge/Network]
|
||||||
|
end
|
||||||
|
|
||||||
|
Finder -->|PROPFIND GET PUT| WebDAV
|
||||||
|
WebDAV --> Mapping
|
||||||
|
Mapping --> DriveAPI
|
||||||
|
WebDAV --> Crypto
|
||||||
|
Crypto --> Network
|
||||||
|
```
|
||||||
|
|
||||||
|
## Datenfluss
|
||||||
|
|
||||||
|
| WebDAV-Anfrage | Internxt-Operation |
|
||||||
|
|----------------|-------------------|
|
||||||
|
| PROPFIND (Verzeichnisinhalt) | Storage.getFolderContentByUuid |
|
||||||
|
| GET (Datei lesen) | File-Metadaten → Network.download → Entschlüsseln |
|
||||||
|
| PUT (Datei schreiben) | Verschlüsseln → Network.upload → createFileEntry |
|
||||||
|
| MKCOL (Ordner anlegen) | Storage.createFolderByUuid |
|
||||||
|
| DELETE | Trash oder permanent delete |
|
||||||
|
| MOVE | Storage.moveFileByUuid / moveFolderByUuid |
|
||||||
|
|
||||||
|
## Komplexität
|
||||||
|
|
||||||
|
- **Einfach:** PROPFIND, MKCOL – nur Drive API, keine Verschlüsselung
|
||||||
|
- **Mittel:** DELETE, MOVE – Drive API
|
||||||
|
- **Aufwändig:** GET, PUT – Bridge/Network + Mnemonic-Verschlüsselung
|
||||||
|
|
||||||
|
Die drive-web nutzt `Network.client` (Bridge) und `NetworkFacade` für Up-/Download. Die Bridge-Credentials kommen aus der User-Session.
|
||||||
|
|
||||||
|
## Implementierungsreihenfolge
|
||||||
|
|
||||||
|
1. **Phase 1:** PROPFIND (Verzeichnis auflisten) – ✅ implementiert
|
||||||
|
2. **Phase 2:** MKCOL, DELETE, MOVE
|
||||||
|
3. **Phase 3:** GET (Download) – Bridge + Entschlüsselung
|
||||||
|
4. **Phase 4:** PUT (Upload) – Verschlüsselung + Bridge
|
||||||
|
|
||||||
|
### Hinweis: Ordnernamen
|
||||||
|
|
||||||
|
Internxt nutzt Zero-Knowledge-Verschlüsselung. Die API liefert verschlüsselte Namen (`name`). Die Pfadauflösung funktioniert, weil WebDAV-URLs diese Namen enthalten. Eine spätere Phase kann Namensentschlüsselung mit Mnemonic ergänzen (drive-web Crypto-Logik).
|
||||||
|
|
||||||
|
## Token vs. Bridge-Credentials
|
||||||
|
|
||||||
|
- **Drive API:** Nutzt `xNewToken` (Authorization: Bearer)
|
||||||
|
- **Network/Bridge:** Braucht `bridgeUser` + `userId` (aus User-Credentials) und Mnemonic für Verschlüsselung
|
||||||
|
|
||||||
|
**Bridge-Credentials aus refreshUser:** Der Endpoint `/users/refresh` liefert `user` (UserResponseDto) mit:
|
||||||
|
- `bridgeUser` – E-Mail des Nutzers
|
||||||
|
- `userId` – wird mit SHA256 gehasht und als Bridge-Passwort genutzt
|
||||||
|
- `bucket` – Bucket-ID für Uploads
|
||||||
|
- `mnemonic` – für Dateiverschlüsselung
|
||||||
|
- `rootFolderId` – Root-Ordner-UUID
|
||||||
|
|
||||||
|
Damit liefert der Browser-Token (via refreshUser) alle nötigen Daten für Drive API und Bridge.
|
||||||
36
docs/wsl-setup.md
Normal file
36
docs/wsl-setup.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# Entwicklung unter WSL (login() mit Keys)
|
||||||
|
|
||||||
|
Unter Windows schlägt das Kyber-WASM-Modul fehl. Unter WSL (Ubuntu/Debian) funktioniert es in der Regel.
|
||||||
|
|
||||||
|
## Voraussetzungen
|
||||||
|
|
||||||
|
- WSL2 mit Ubuntu oder Debian
|
||||||
|
- Node.js 20+ (`node -v`)
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Im WSL-Terminal
|
||||||
|
cd /mnt/c/Users/mbusc/source/repos/internxt-webdav
|
||||||
|
|
||||||
|
# Abhängigkeiten installieren (inkl. Kyber für login mit Keys)
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# .env mit Credentials (INXT_EMAIL, INXT_PASSWORD)
|
||||||
|
# Optional: DEBUG=1 für Salt-Check
|
||||||
|
```
|
||||||
|
|
||||||
|
## Auth-PoC mit login() testen
|
||||||
|
|
||||||
|
Zuerst den Auth-PoC auf `login()` umstellen (mit Keys):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run auth-test
|
||||||
|
```
|
||||||
|
|
||||||
|
Falls der Fehler "Wrong login credentials" weiterhin auftritt, liegt das Problem nicht am Kyber-WASM, sondern am Backend/Account-Typ.
|
||||||
|
|
||||||
|
## Projektpfad
|
||||||
|
|
||||||
|
Windows-Pfad: `c:\Users\mbusc\source\repos\internxt-webdav`
|
||||||
|
WSL-Pfad: `/mnt/c/Users/mbusc/source/repos/internxt-webdav`
|
||||||
1234
package-lock.json
generated
Normal file
1234
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
package.json
Normal file
19
package.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "internxt-webdav",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "WebDAV wrapper for Internxt Drive (for accounts without CLI/Rclone access)",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"auth-test": "node src/auth-poc.js",
|
||||||
|
"token-test": "node src/token-test.js",
|
||||||
|
"start": "node src/server.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@internxt/lib": "^1.4.1",
|
||||||
|
"@internxt/sdk": "^1.13.2",
|
||||||
|
"crypto-js": "^4.1.1",
|
||||||
|
"dotenv": "^16.0.0",
|
||||||
|
"express": "^4.18.0",
|
||||||
|
"webdav-server": "^2.6.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
129
src/auth-poc.js
Normal file
129
src/auth-poc.js
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
/**
|
||||||
|
* Proof-of-Concept: Internxt Login mit clientName "drive-web"
|
||||||
|
*
|
||||||
|
* Testet ob Login mit Web-Client-Identität für eingeschränkte Account-Tiers funktioniert.
|
||||||
|
* CLI und Rclone nutzen loginAccess() mit "internxt-cli" - das wird blockiert.
|
||||||
|
* drive-web nutzt login() mit "drive-web" - das funktioniert.
|
||||||
|
*
|
||||||
|
* Ausführung: INXT_EMAIL=... INXT_PASSWORD=... node src/auth-poc.js
|
||||||
|
* Oder: .env mit Credentials füllen, dann node src/auth-poc.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'dotenv/config';
|
||||||
|
import { Auth } from '@internxt/sdk/dist/auth/index.js';
|
||||||
|
import CryptoJS from 'crypto-js';
|
||||||
|
|
||||||
|
const DRIVE_API_URL = process.env.DRIVE_API_URL || 'https://gateway.internxt.com/drive';
|
||||||
|
const CRYPTO_SECRET = process.env.CRYPTO_SECRET || process.env.APP_CRYPTO_SECRET || '6KYQBP847D4ATSFA';
|
||||||
|
|
||||||
|
const email = process.env.INXT_EMAIL;
|
||||||
|
const password = process.env.INXT_PASSWORD;
|
||||||
|
const twoFactorCode = process.env.INXT_2FA || '';
|
||||||
|
|
||||||
|
if (!email || !password) {
|
||||||
|
console.error('Fehler: INXT_EMAIL und INXT_PASSWORD müssen gesetzt sein.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// CryptoProvider wie in drive-web - passToHash und encryptText
|
||||||
|
function passToHash({ password, salt }) {
|
||||||
|
const saltParsed = salt ? CryptoJS.enc.Hex.parse(salt) : CryptoJS.lib.WordArray.random(128 / 8);
|
||||||
|
const hash = CryptoJS.PBKDF2(password, saltParsed, { keySize: 256 / 32, iterations: 10000 });
|
||||||
|
return {
|
||||||
|
salt: saltParsed.toString(),
|
||||||
|
hash: hash.toString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function encryptText(text) {
|
||||||
|
const bytes = CryptoJS.AES.encrypt(text, CRYPTO_SECRET).toString();
|
||||||
|
const text64 = CryptoJS.enc.Base64.parse(bytes);
|
||||||
|
return text64.toString(CryptoJS.enc.Hex);
|
||||||
|
}
|
||||||
|
|
||||||
|
function decryptText(encryptedText) {
|
||||||
|
const reb = CryptoJS.enc.Hex.parse(encryptedText);
|
||||||
|
const bytes = CryptoJS.AES.decrypt(reb.toString(CryptoJS.enc.Base64), CRYPTO_SECRET);
|
||||||
|
return bytes.toString(CryptoJS.enc.Utf8);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cryptoProvider = {
|
||||||
|
encryptPasswordHash(password, encryptedSalt) {
|
||||||
|
const salt = decryptText(encryptedSalt);
|
||||||
|
const hashObj = passToHash({ password, salt });
|
||||||
|
return encryptText(hashObj.hash);
|
||||||
|
},
|
||||||
|
async generateKeys() {
|
||||||
|
throw new Error('generateKeys not used - loginWithoutKeys');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// AppDetails mit clientName "drive-web" - der Schlüssel zum Bypass!
|
||||||
|
const appDetails = {
|
||||||
|
clientName: 'drive-web',
|
||||||
|
clientVersion: '1.0',
|
||||||
|
};
|
||||||
|
|
||||||
|
const apiSecurity = {
|
||||||
|
token: '',
|
||||||
|
unauthorizedCallback: () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log('Internxt Auth PoC - Login mit clientName "drive-web"');
|
||||||
|
console.log('API:', DRIVE_API_URL);
|
||||||
|
console.log('E-Mail:', email);
|
||||||
|
console.log('2FA:', twoFactorCode ? '***' + twoFactorCode.slice(-2) : '(nicht gesetzt)');
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
const authClient = Auth.client(DRIVE_API_URL, appDetails, apiSecurity);
|
||||||
|
|
||||||
|
// Optional: securityDetails prüfen (Salt-Decryption testet CRYPTO_SECRET)
|
||||||
|
if (process.env.DEBUG) {
|
||||||
|
try {
|
||||||
|
const details = await authClient.securityDetails(email.toLowerCase());
|
||||||
|
const salt = decryptText(details.encryptedSalt);
|
||||||
|
const isHex = /^[0-9a-f]+$/i.test(salt);
|
||||||
|
console.log('DEBUG: Salt-Decryption OK, Format:', isHex ? 'Hex' : 'anderes');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('DEBUG: Salt-Decryption fehlgeschlagen - CRYPTO_SECRET evtl. falsch:', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await authClient.loginWithoutKeys(
|
||||||
|
{
|
||||||
|
email: email.toLowerCase(),
|
||||||
|
password,
|
||||||
|
tfaCode: twoFactorCode || undefined,
|
||||||
|
},
|
||||||
|
cryptoProvider
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('Login erfolgreich!');
|
||||||
|
console.log('Token:', result.newToken?.substring(0, 20) + '...');
|
||||||
|
console.log('User:', result.user?.email);
|
||||||
|
console.log('');
|
||||||
|
console.log('Der WebDAV-Wrapper kann mit dieser Auth gebaut werden.');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Login fehlgeschlagen:', err.message);
|
||||||
|
if (err.response?.data) {
|
||||||
|
console.error('Response:', JSON.stringify(err.response.data, null, 2));
|
||||||
|
}
|
||||||
|
if (err.message?.includes('cli access not allowed') || err.message?.includes('rclone access not allowed')) {
|
||||||
|
console.error('');
|
||||||
|
console.error('Hinweis: Dieser Fehler sollte mit clientName "drive-web" NICHT auftreten.');
|
||||||
|
}
|
||||||
|
if (err.message?.includes('Wrong login credentials')) {
|
||||||
|
console.error('');
|
||||||
|
console.error('Mögliche Ursachen:');
|
||||||
|
console.error('1. CRYPTO_SECRET falsch - drive-web nutzt REACT_APP_CRYPTO_SECRET (evtl. anderer Wert)');
|
||||||
|
console.error(' -> DEBUG=1 setzen und erneut ausführen, um Salt-Decryption zu prüfen');
|
||||||
|
console.error('2. 2FA-Code abgelaufen (30s gültig) - neuen Code eingeben');
|
||||||
|
console.error('3. Passwort/E-Mail falsch');
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
41
src/internxt-client.js
Normal file
41
src/internxt-client.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* Internxt API Client – Drive API + User Refresh
|
||||||
|
* Nutzt Browser-Token (INXT_TOKEN) für drive-web Endpoints.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'dotenv/config';
|
||||||
|
import { Storage, Users } from '@internxt/sdk/dist/drive/index.js';
|
||||||
|
|
||||||
|
const DRIVE_API_URL = process.env.DRIVE_API_URL || 'https://gateway.internxt.com/drive';
|
||||||
|
|
||||||
|
const appDetails = { clientName: 'drive-web', clientVersion: '1.0' };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erstellt API-Clients mit aktuellem Token.
|
||||||
|
* @param {string} token - Bearer Token (xNewToken)
|
||||||
|
* @returns {{ users: Users, storage: Storage }}
|
||||||
|
*/
|
||||||
|
export function createClients(token) {
|
||||||
|
const apiSecurity = {
|
||||||
|
token,
|
||||||
|
unauthorizedCallback: () => {
|
||||||
|
throw new Error('Token abgelaufen oder ungültig');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
users: Users.client(DRIVE_API_URL, appDetails, apiSecurity),
|
||||||
|
storage: Storage.client(DRIVE_API_URL, appDetails, apiSecurity),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holt User-Daten inkl. Bridge-Credentials via refreshUser.
|
||||||
|
* @param {string} token
|
||||||
|
* @returns {Promise<{ user: object, newToken?: string }>}
|
||||||
|
*/
|
||||||
|
export async function refreshUser(token) {
|
||||||
|
const { users } = createClients(token);
|
||||||
|
const response = await users.refreshUser();
|
||||||
|
const user = response?.user ?? response;
|
||||||
|
return { user, newToken: response?.newToken };
|
||||||
|
}
|
||||||
72
src/path-resolver.js
Normal file
72
src/path-resolver.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
/**
|
||||||
|
* WebDAV-Pfad ↔ Internxt-UUID Auflösung
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalisiert WebDAV-Pfad zu Segmenten (ohne führendes /).
|
||||||
|
* "/" -> [], "/folder1" -> ["folder1"], "/folder1/sub" -> ["folder1","sub"]
|
||||||
|
*/
|
||||||
|
export function pathToSegments(path) {
|
||||||
|
const p = String(path || '').replace(/\/+/g, '/').replace(/^\//, '').replace(/\/$/, '');
|
||||||
|
return p ? p.split('/') : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baut Pfad aus Segmenten.
|
||||||
|
*/
|
||||||
|
export function segmentsToPath(segments) {
|
||||||
|
return '/' + (Array.isArray(segments) ? segments.join('/') : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolved einen WebDAV-Pfad zur Internxt-Ordner-UUID.
|
||||||
|
* @param {object} storage - Storage Client
|
||||||
|
* @param {string} rootFolderUuid - Root-Ordner-UUID
|
||||||
|
* @param {string} path - WebDAV-Pfad (z.B. "/" oder "/folder1/sub")
|
||||||
|
* @returns {Promise<{ uuid: string, type: 'folder', name: string } | null>}
|
||||||
|
*/
|
||||||
|
export async function resolveFolder(storage, rootFolderUuid, path) {
|
||||||
|
const segments = pathToSegments(path);
|
||||||
|
let currentUuid = rootFolderUuid;
|
||||||
|
|
||||||
|
for (const segment of segments) {
|
||||||
|
const [contentPromise] = storage.getFolderContentByUuid({ folderUuid: currentUuid });
|
||||||
|
const content = await contentPromise;
|
||||||
|
const child = content?.children?.find(
|
||||||
|
(c) => (c.plain_name || c.name || '').toLowerCase() === segment.toLowerCase()
|
||||||
|
);
|
||||||
|
if (!child) return null;
|
||||||
|
currentUuid = child.uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = segments.length ? segments[segments.length - 1] : '';
|
||||||
|
return { uuid: currentUuid, type: 'folder', name: name || 'Drive' };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listet Ordnerinhalt (Unterordner + Dateien) für einen Pfad.
|
||||||
|
* @param {object} storage
|
||||||
|
* @param {string} rootFolderUuid
|
||||||
|
* @param {string} path
|
||||||
|
* @returns {Promise<{ folders: Array<{uuid, name}>, files: Array<{uuid, name, size, updatedAt}> }>}
|
||||||
|
*/
|
||||||
|
export async function listFolder(storage, rootFolderUuid, path) {
|
||||||
|
const folder = await resolveFolder(storage, rootFolderUuid, path);
|
||||||
|
if (!folder) return null;
|
||||||
|
|
||||||
|
const [contentPromise] = storage.getFolderContentByUuid({ folderUuid: folder.uuid });
|
||||||
|
const content = await contentPromise;
|
||||||
|
|
||||||
|
const folders = (content?.children || []).map((c) => ({
|
||||||
|
uuid: c.uuid,
|
||||||
|
name: c.plain_name || c.name || 'Unbenannt',
|
||||||
|
}));
|
||||||
|
const files = (content?.files || []).map((f) => ({
|
||||||
|
uuid: f.uuid,
|
||||||
|
name: f.plain_name || f.name || f.plainName || 'Unbenannt',
|
||||||
|
size: f.size || 0,
|
||||||
|
updatedAt: f.updatedAt || f.modificationTime || f.creationTime,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return { folders, files };
|
||||||
|
}
|
||||||
201
src/server.js
Normal file
201
src/server.js
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
/**
|
||||||
|
* WebDAV-Server für Internxt Drive (Phase 1: PROPFIND)
|
||||||
|
*
|
||||||
|
* Nutzt Browser-Token (INXT_TOKEN, INXT_MNEMONIC) aus .env.
|
||||||
|
* Siehe docs/browser-token-auth.md
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'dotenv/config';
|
||||||
|
import express from 'express';
|
||||||
|
import { createClients, refreshUser } from './internxt-client.js';
|
||||||
|
import { pathToSegments, listFolder } from './path-resolver.js';
|
||||||
|
|
||||||
|
const PORT = parseInt(process.env.PORT || '3005', 10);
|
||||||
|
const token = process.env.INXT_TOKEN;
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
console.error('Fehler: INXT_TOKEN muss gesetzt sein. Siehe docs/browser-token-auth.md');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
// Request-Body für PROPFIND
|
||||||
|
app.use(express.text({ type: 'application/xml', limit: '1kb' }));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formatiert Datum für DAV:getlastmodified (RFC 2822)
|
||||||
|
*/
|
||||||
|
function toLastModified(iso) {
|
||||||
|
if (!iso) return 'Thu, 01 Jan 1970 00:00:00 GMT';
|
||||||
|
const d = new Date(iso);
|
||||||
|
return d.toUTCString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escaped XML-Text
|
||||||
|
*/
|
||||||
|
function escapeXml(s) {
|
||||||
|
if (s == null) return '';
|
||||||
|
return String(s)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erzeugt PROPFIND multistatus XML
|
||||||
|
*/
|
||||||
|
function buildPropfindResponse(baseUrl, items) {
|
||||||
|
const ns = 'DAV:';
|
||||||
|
const responses = items.map((item) => {
|
||||||
|
const href = baseUrl + (item.path === '/' ? '/' : item.path + (item.isCollection ? '/' : ''));
|
||||||
|
const lastMod = toLastModified(item.updatedAt);
|
||||||
|
const resourcetype = item.isCollection
|
||||||
|
? `<D:resourcetype><D:collection/></D:resourcetype>`
|
||||||
|
: '<D:resourcetype/>';
|
||||||
|
const contentLength = item.isCollection ? '' : `<D:getcontentlength>${item.size ?? 0}</D:getcontentlength>`;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<D:response>
|
||||||
|
<D:href>${escapeXml(href)}</D:href>
|
||||||
|
<D:propstat>
|
||||||
|
<D:prop>
|
||||||
|
<D:displayname>${escapeXml(item.name)}</D:displayname>
|
||||||
|
<D:getlastmodified>${escapeXml(lastMod)}</D:getlastmodified>
|
||||||
|
${resourcetype}
|
||||||
|
${contentLength}
|
||||||
|
</D:prop>
|
||||||
|
<D:status>HTTP/1.1 200 OK</D:status>
|
||||||
|
</D:propstat>
|
||||||
|
</D:response>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return `<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<D:multistatus xmlns:D="${ns}">${responses.join('')}
|
||||||
|
</D:multistatus>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PROPFIND Handler
|
||||||
|
*/
|
||||||
|
async function handlePropfind(req, res) {
|
||||||
|
const depth = req.headers['depth'] || '1';
|
||||||
|
let path = req.url || '/';
|
||||||
|
try {
|
||||||
|
path = decodeURIComponent(path);
|
||||||
|
} catch (_) {}
|
||||||
|
if (!path.startsWith('/')) path = '/' + path;
|
||||||
|
if (path !== '/' && path.endsWith('/')) path = path.slice(0, -1);
|
||||||
|
|
||||||
|
const baseUrl = `${req.protocol}://${req.get('host')}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { users, storage } = createClients(token);
|
||||||
|
const refresh = await refreshUser(token);
|
||||||
|
const user = refresh.user;
|
||||||
|
const rootUuid = user?.rootFolderUuid || user?.rootFolderId || user?.root_folder_id;
|
||||||
|
if (!rootUuid) {
|
||||||
|
res.status(500).send('Root-Ordner nicht gefunden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const listing = await listFolder(storage, rootUuid, path);
|
||||||
|
if (!listing) {
|
||||||
|
res.status(404).send('Nicht gefunden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = [];
|
||||||
|
const segments = pathToSegments(path);
|
||||||
|
const parentName = segments.length ? segments[segments.length - 1] : 'Drive';
|
||||||
|
|
||||||
|
// Aktuelle Ressource (Collection)
|
||||||
|
items.push({
|
||||||
|
path,
|
||||||
|
name: parentName,
|
||||||
|
isCollection: true,
|
||||||
|
updatedAt: null,
|
||||||
|
size: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Kinder bei depth 1
|
||||||
|
if (depth !== '0') {
|
||||||
|
for (const f of listing.folders) {
|
||||||
|
const childPath = path === '/' ? '/' + f.name : path + '/' + f.name;
|
||||||
|
items.push({
|
||||||
|
path: childPath,
|
||||||
|
name: f.name,
|
||||||
|
isCollection: true,
|
||||||
|
updatedAt: null,
|
||||||
|
size: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const f of listing.files) {
|
||||||
|
const childPath = path === '/' ? '/' + f.name : path + '/' + f.name;
|
||||||
|
items.push({
|
||||||
|
path: childPath,
|
||||||
|
name: f.name,
|
||||||
|
isCollection: false,
|
||||||
|
updatedAt: f.updatedAt,
|
||||||
|
size: f.size,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const xml = buildPropfindResponse(baseUrl, items);
|
||||||
|
res.set('Content-Type', 'application/xml; charset="utf-8"');
|
||||||
|
res.status(207).send(xml);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('PROPFIND Fehler:', err.message);
|
||||||
|
if (err.message?.includes('Token') || err.response?.status === 401) {
|
||||||
|
res.status(401).send('Nicht autorisiert – Token erneuern');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.status(500).send(err.message || 'Interner Fehler');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebDAV Endpoints
|
||||||
|
app.options('*', (req, res) => {
|
||||||
|
res.set('DAV', '1, 2');
|
||||||
|
res.set('Allow', 'OPTIONS, PROPFIND, GET, HEAD, PUT, DELETE, MKCOL, MOVE');
|
||||||
|
res.sendStatus(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Alle WebDAV-Methoden zentral
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
if (req.method === 'PROPFIND') {
|
||||||
|
handlePropfind(req, res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (req.method === 'GET' || req.method === 'HEAD') {
|
||||||
|
res.status(501).send('GET (Download) noch nicht implementiert – Phase 3');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (req.method === 'PUT') {
|
||||||
|
res.status(501).send('PUT (Upload) noch nicht implementiert – Phase 4');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (req.method === 'DELETE') {
|
||||||
|
res.status(501).send('DELETE noch nicht implementiert – Phase 2');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (req.method === 'MKCOL') {
|
||||||
|
res.status(501).send('MKCOL noch nicht implementiert – Phase 2');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (req.method === 'MOVE') {
|
||||||
|
res.status(501).send('MOVE noch nicht implementiert – Phase 2');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Internxt WebDAV Server – http://127.0.0.1:${PORT}`);
|
||||||
|
console.log('Phase 1: PROPFIND (Verzeichnis auflisten) aktiv.');
|
||||||
|
console.log('Verwendung: z.B. Windows Explorer → Netzlaufwerk verbinden');
|
||||||
|
});
|
||||||
59
src/token-test.js
Normal file
59
src/token-test.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
/**
|
||||||
|
* Token-Test: Prüft ob Browser-Token funktioniert
|
||||||
|
*
|
||||||
|
* Voraussetzung: Auf drive.internxt.com eingeloggt, Token + Mnemonic aus
|
||||||
|
* localStorage (xNewToken, xMnemonic) in .env als INXT_TOKEN und INXT_MNEMONIC
|
||||||
|
*
|
||||||
|
* Siehe docs/browser-token-auth.md
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'dotenv/config';
|
||||||
|
import { Storage, Users } from '@internxt/sdk/dist/drive/index.js';
|
||||||
|
|
||||||
|
const DRIVE_API_URL = process.env.DRIVE_API_URL || 'https://gateway.internxt.com/drive';
|
||||||
|
const token = process.env.INXT_TOKEN;
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
console.error('Fehler: INXT_TOKEN muss gesetzt sein (aus Browser localStorage)');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const appDetails = { clientName: 'drive-web', clientVersion: '1.0' };
|
||||||
|
const apiSecurity = {
|
||||||
|
token,
|
||||||
|
unauthorizedCallback: () => console.error('Token abgelaufen oder ungültig'),
|
||||||
|
};
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log('Token-Test – Drive API mit Browser-Token');
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
const usersClient = Users.client(DRIVE_API_URL, appDetails, apiSecurity);
|
||||||
|
const storageClient = Storage.client(DRIVE_API_URL, appDetails, apiSecurity);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// refreshUser nutzt /users/refresh (nicht CLI-Endpoint)
|
||||||
|
const response = await usersClient.refreshUser();
|
||||||
|
const user = response?.user ?? response;
|
||||||
|
console.log('Token OK – User:', user?.email);
|
||||||
|
console.log('Root Folder UUID:', user?.rootFolderId || user?.rootFolderUuid);
|
||||||
|
|
||||||
|
const rootUuid = user?.rootFolderUuid || user?.rootFolderId;
|
||||||
|
if (rootUuid) {
|
||||||
|
const [content] = storageClient.getFolderContentByUuid({ folderUuid: rootUuid });
|
||||||
|
const folderContent = await content;
|
||||||
|
console.log('Dateien/Ordner im Root:', folderContent.children?.length ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
console.log('Token funktioniert – WebDAV-Server kann gebaut werden.');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Fehler:', err.message);
|
||||||
|
if (err.response?.status === 401) {
|
||||||
|
console.error('Token abgelaufen – bitte erneut auf drive.internxt.com einloggen und Token aktualisieren.');
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
Reference in New Issue
Block a user