Skip to content

Commit b92891a

Browse files
committed
implement API key
1 parent d6b21df commit b92891a

3 files changed

Lines changed: 29 additions & 24 deletions

File tree

comiclib/main.py

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@
99
from pathlib import Path
1010
from zipfile import ZipFile
1111
import re
12-
import asyncio
12+
import base64
1313
import tempfile
1414
import multiprocessing
1515
from urllib.parse import quote, unquote, urlparse
1616

17-
from fastapi import FastAPI, Cookie, Request, Query, Depends, BackgroundTasks, Response, status, Form
17+
from fastapi import FastAPI, Cookie, Request, Query, Depends, BackgroundTasks, Response, status, Form, HTTPException
1818
from fastapi.responses import HTMLResponse, FileResponse, StreamingResponse, JSONResponse, RedirectResponse
19+
from fastapi.security import OAuth2PasswordBearer
1920
from fastapi.staticfiles import StaticFiles
2021

2122
from template import Template
@@ -43,14 +44,6 @@
4344
app.mount("/eHunter", StaticFiles(directory=app_path / "eHunter"))
4445
app.mount("/themes", StaticFiles(directory=app_path / "LANraragi/public/themes"))
4546

46-
@app.middleware("http")
47-
async def authentication(request: Request, call_next):
48-
if not settings.password is None and request.method not in ("GET", "HEAD") and request.url.path != "/login" and request.cookies.get("tokenv0") != settings.password:
49-
if request.url.path.endswith('/isnew') or '/progress/' in request.url.path: # temporary solution
50-
return JSONResponse({"success": 1})
51-
return Response("Not authenticated", status_code=status.HTTP_401_UNAUTHORIZED)
52-
return await call_next(request)
53-
5447
@app.middleware("http")
5548
async def add_COEPCOOP(request: Request, call_next):
5649
response = await call_next(request)
@@ -67,6 +60,18 @@ def get_db():
6760
finally:
6861
db.close()
6962

63+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")
64+
async def verify_token(request: Request):
65+
if request.cookies.get("tokenv0") != settings.password and base64.b64decode((await oauth2_scheme(request)).encode()).decode() != settings.password:
66+
raise HTTPException(
67+
status_code=status.HTTP_401_UNAUTHORIZED,
68+
detail="Invalid authentication credentials",
69+
headers={"WWW-Authenticate": "Bearer"},
70+
)
71+
if settings.password is None:
72+
authorization = None
73+
else:
74+
authorization = [Depends(verify_token)]
7075

7176
# https://sugoi.gitbook.io/lanraragi/v/dev/api-documentation/getting-started
7277

@@ -137,7 +142,7 @@ def get_random_archives(category: str = '', filter: str = '', count: int = 5, db
137142
return {"data": data}
138143

139144

140-
@app.delete("/api/search/cache")
145+
@app.delete("/api/search/cache", dependencies=authorization)
141146
def discard_search_cache():
142147
return {"operation": "clear_cache", "success": 1}
143148

@@ -186,7 +191,7 @@ def get_archive_metadata(id: str, db: Session = Depends(get_db)):
186191
return {"arcid": a.id, "isnew": "false", "pagecount": a.pagecount, "progress": 1, "tags": ", ".join(map(lambda t: t.tag, a.tags)), "title": a.title}
187192

188193

189-
@app.put("/api/archives/{id}/metadata")
194+
@app.put("/api/archives/{id}/metadata", dependencies=authorization)
190195
def update_archive_metadata(id: str, title: Annotated[str, Form()], tags: Annotated[str, Form()], db: Session = Depends(get_db)):
191196
db.execute(update(Archive).where(Archive.id == id).values(title=title))
192197
db.execute(delete(Tag).where(Tag.archive_id == id))
@@ -232,7 +237,7 @@ def get_archive_thumbnail(id: str, background_tasks: BackgroundTasks, response:
232237
return FileResponse(thumb_path)
233238

234239

235-
@app.put("/api/archives/{id}/thumbnail")
240+
@app.put("/api/archives/{id}/thumbnail", dependencies=authorization)
236241
def update_thumbnail(id: str, page: int = 1, db: Session = Depends(get_db)):
237242
a = db.get(Archive, id)
238243
if a is None:
@@ -321,7 +326,7 @@ def update_reading_progression(id: str, page: int):
321326
}
322327

323328

324-
@app.delete("/api/archives/{id}")
329+
@app.delete("/api/archives/{id}", dependencies=authorization)
325330
def delete_archive(id: str, db: Session = Depends(get_db)):
326331
a = db.get(Archive, id)
327332
if a is None:
@@ -354,7 +359,7 @@ def get_statistics(minweight: int = 1, db: Session = Depends(get_db)):
354359
]
355360

356361

357-
@app.post("/api/database/clean")
362+
@app.post("/api/database/clean", dependencies=authorization)
358363
def clean_database(db: Session = Depends(get_db)):
359364
deleted = 0
360365
for id, path in db.execute(select(Archive.id, Archive.path)):
@@ -371,7 +376,7 @@ def clean_database(db: Session = Depends(get_db)):
371376
}
372377

373378

374-
@app.post("/api/database/drop")
379+
@app.post("/api/database/drop", dependencies=authorization)
375380
def drop_database(db: Session = Depends(get_db)):
376381
Base.metadata.drop_all(bind=engine)
377382
Base.metadata.create_all(bind=engine)
@@ -381,7 +386,7 @@ def drop_database(db: Session = Depends(get_db)):
381386
}
382387

383388

384-
@app.delete("/api/database/isnew")
389+
@app.delete("/api/database/isnew", dependencies=authorization)
385390
def clean_all_new_flag(db: Session = Depends(get_db)):
386391
raise NotImplementedError
387392

@@ -397,7 +402,7 @@ def get_all_categories(db: Session = Depends(get_db)):
397402
]
398403

399404

400-
@app.put("/api/categories")
405+
@app.put("/api/categories", dependencies=authorization)
401406
def create_category(name: Annotated[str, Form()] = None, name2: Annotated[str, Query(alias="name")] = None, pinned: bool = False, search: Union[str, None] = None, db: Session = Depends(get_db)):
402407
if name is None: name = name2
403408
if not search is None and len(search) == 0: search = None
@@ -425,7 +430,7 @@ def get_single_category(id: str, response: Response, db: Session = Depends(get_d
425430
return {"archives": [a.id for a in c.archive], "id": c.id, "last_used": 0, "name": c.name, "pinned": c.pinned, "search": c.search}
426431

427432

428-
@app.put("/api/categories/{id}")
433+
@app.put("/api/categories/{id}", dependencies=authorization)
429434
def update_category(id: str, name: Union[str, None], search: Union[str, None] = None, pinned: bool = False, db: Session = Depends(get_db)):
430435
stmt = update(Category).where(Category.id == id)
431436
if name:
@@ -442,7 +447,7 @@ def update_category(id: str, name: Union[str, None], search: Union[str, None] =
442447
}
443448

444449

445-
@app.delete("/api/categories/{id}")
450+
@app.delete("/api/categories/{id}", dependencies=authorization)
446451
def delete_category(id: str, db: Session = Depends(get_db)):
447452
db.execute(delete(Category).where(Category.id == id))
448453
db.commit()
@@ -452,7 +457,7 @@ def delete_category(id: str, db: Session = Depends(get_db)):
452457
}
453458

454459

455-
@app.put("/api/categories/{id}/{archive}")
460+
@app.put("/api/categories/{id}/{archive}", dependencies=authorization)
456461
def add_archive_to_category(id: str, archive: str, db: Session = Depends(get_db)):
457462
a = db.get(Archive, archive)
458463
c = db.get(Category, id)
@@ -467,7 +472,7 @@ def add_archive_to_category(id: str, archive: str, db: Session = Depends(get_db)
467472
}
468473

469474

470-
@app.delete("/api/categories/{id}/{archive}")
475+
@app.delete("/api/categories/{id}/{archive}", dependencies=authorization)
471476
def remove_archive_from_category(id: str, archive: str, db: Session = Depends(get_db)):
472477
a = db.get(Archive, archive)
473478
c = db.get(Category, id)

docs/en/docs/settings.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ The following is a list of available settings:
3131
| `content` | The path where the comic file is stored | `.` |
3232
| `thumb` | The path where the generated thumbnails are stored | `./thumb`|
3333
| `metadata` | The URL for metadata database, refer to [SQLAlchemy documentation](https://docs.sqlalchemy.org/en/20/core/engines.html#database-urls) | `sqlite:///./comiclib_metadata.db` |
34-
| `password` | Admin password. If it is `None`, any visitor will have editing permissions. This feature is designed to protect against gentlemen but not villains. If you need security protection, please use e.g. the HTTP basic authentication of the reverse proxy, Cloudflare Access or TLS client certificate, etc. | `None`|
34+
| `password` | Admin password (also used as API Key currently). If it is `None`, any visitor will have editing permissions. This feature is designed to protect against gentlemen but not villains. If you need security protection, please use e.g. the HTTP basic authentication of the reverse proxy, Cloudflare Access or TLS client certificate, etc. | `None`|
3535
| `skip_exists`| Skip comics that have been scanned into the metadata database during scanning? (`True`/`False`) | `True` |
3636
| `watch` | Monitor comic folders and automatically scan (`True`/`False`) | `True` |
3737
| `UA_convert_jxl` | For requests with matched user-agent, convert JPEG XL files to other popular formats on the server side. The value is a regular expression. | `Android` |

docs/zh/docs/settings.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
| `content` | 漫画文件存放的路径 | `.` |
3232
| `thumb` | 生成的缩略图存放的路径 | `./thumb`|
3333
| `metadata` | 元数据库 URL,参考[SQLAlchemy 文档](https://docs.sqlalchemy.org/en/20/core/engines.html#database-urls) | `sqlite:///./comiclib_metadata.db` |
34-
| `password` | 管理密码,若为`None`则任何访客皆可编辑。此功能防君子不防小人,若需安全保护请借助反向代理的 HTTP 基本验证、Cloudflare Access 或 TLS 客户端证书等。| `None`|
34+
| `password` | 管理密码(目前也用作 API 密钥),若为`None`则任何访客皆可编辑。此功能防君子不防小人,若需安全保护请借助反向代理的 HTTP 基本验证、Cloudflare Access 或 TLS 客户端证书等。| `None`|
3535
| `skip_exists`| 扫描时是否跳过曾扫入元数据库的漫画?(`True`/`False`| `True` |
3636
| `watch` | 监视漫画文件夹,自动扫描 (`True`/`False`| `True` |
3737
| `UA_convert_jxl` | 对于哪些 user-agent 的请求在服务端将 JPEG XL 文件转为其他流行格式,该值是一个用于匹配的正则表达式 | `Android` |

0 commit comments

Comments
 (0)