Verbindungen werden pro Nutzer in den Einstellungen angelegt (Nextcloud,
ownCloud, Synology oder generisches WebDAV) — der Server ist bewusst nicht
auf eine feste Instanz verdrahtet. Im Bücher-Tab lässt sich der entfernte
Ordner durchblättern und eine .epub/.pdf direkt in die Bibliothek ziehen.
Der Download läuft über den Server, weil der WebDAV-Host cross-origin ist
und keine CORS-Header schickt. Verschlüsselt wird trotzdem erst im Browser:
uploadEbook ist in _importEbookBuffer aufgeteilt, das sich lokaler Upload
und Cloud-Import teilen. Gespeichert wird wie bisher nur Geheimtext.
Weil jeder registrierte Nutzer die Ziel-URL bestimmt und diora im Docker-Netz
neben anderen Diensten läuft, ist der Import eine SSRF-Fläche. Dagegen:
- assert_safe_url weist Hosts ab, die auf nicht-öffentliche Adressen
auflösen (inkl. NAT64 und IPv4-kompatibler v6-Adressen, die is_global
durchlässt)
- _assert_peer_is_safe prüft die tatsächliche Peer-Adresse nach dem
Verbinden — requests löst den Namen ein zweites Mal auf, sonst wäre der
Guard per DNS-Rebinding umgehbar
- Redirects werden abgelehnt statt verfolgt
- identische Fehlermeldung für "nicht auflösbar" und "privat", ohne die
IP zu nennen, damit der Endpunkt kein Scanner für interne Dienste wird
Antwort-Bodies laufen durch _read_capped, und DTDs werden vor dem Parsen
abgewiesen: ElementTree expandiert interne Entities, und seit Python 3.12
gibt es XMLParser.parser nicht mehr, um einen Handler zu setzen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
20 lines
1.1 KiB
Python
20 lines
1.1 KiB
Python
from django.urls import path
|
|
from . import views, webdav
|
|
|
|
urlpatterns = [
|
|
path('', views.book_list, name='book_list'),
|
|
path('upload/', views.upload_book, name='upload_book'),
|
|
path('cloud/sources/', webdav.cloud_sources, name='cloud_sources'),
|
|
path('cloud/<int:pk>/browse/', webdav.cloud_browse, name='cloud_browse'),
|
|
path('cloud/<int:pk>/fetch/', webdav.cloud_fetch, name='cloud_fetch'),
|
|
path('metadata-lookup/', views.lookup_book_metadata, name='lookup_book_metadata'),
|
|
path('<int:pk>/data/', views.get_book_data, name='get_book_data'),
|
|
path('<int:pk>/delete/', views.delete_book, name='delete_book'),
|
|
path('<int:pk>/read/', views.set_book_read, name='set_book_read'),
|
|
path('<int:pk>/meta/', views.update_book_meta, name='update_book_meta'),
|
|
path('<int:pk>/replace-data/', views.replace_book_data, name='replace_book_data'),
|
|
path('<int:pk>/rekey/', views.rekey_book, name='rekey_book'),
|
|
path('<int:pk>/progress/', views.save_progress, name='save_book_progress'),
|
|
path('<int:pk>/highlights/', views.book_highlights, name='book_highlights'),
|
|
path('<int:pk>/bookmarks/', views.book_bookmarks, name='book_bookmarks'),
|
|
]
|