This commit is contained in:
Kieran 2023-10-13 21:46:00 +01:00
parent 6f28c3f293
commit ef4ca27f4b
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
8 changed files with 373 additions and 281 deletions

View File

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.StaticFiles; using Microsoft.AspNetCore.StaticFiles;
@ -58,6 +59,8 @@ namespace VoidCat.Controllers
[HttpPost] [HttpPost]
[DisableRequestSizeLimit] [DisableRequestSizeLimit]
[DisableFormValueModelBinding] [DisableFormValueModelBinding]
[Authorize(AuthenticationSchemes = "Bearer,Nostr")]
[AllowAnonymous]
public async Task<IActionResult> UploadFile([FromQuery] bool cli = false) public async Task<IActionResult> UploadFile([FromQuery] bool cli = false)
{ {
try try

View File

@ -95,7 +95,7 @@ public class UserManager
} }
var uid = await _store.LookupUser(user.Email); var uid = await _store.LookupUser(user.Email);
if (uid.HasValue) if (uid.HasValue && uid.Value != Guid.Empty)
{ {
var existingUser = await _store.Get(uid.Value); var existingUser = await _store.Get(uid.Value);
if (existingUser?.AuthType == UserAuthType.OAuth2) if (existingUser?.AuthType == UserAuthType.OAuth2)
@ -119,7 +119,7 @@ public class UserManager
public async ValueTask<User> LoginOrRegister(string pubkey) public async ValueTask<User> LoginOrRegister(string pubkey)
{ {
var uid = await _store.LookupUser(pubkey); var uid = await _store.LookupUser(pubkey);
if (uid.HasValue) if (uid.HasValue && uid.Value != Guid.Empty)
{ {
var existingUser = await _store.Get(uid.Value); var existingUser = await _store.Get(uid.Value);
if (existingUser?.AuthType == UserAuthType.Nostr) if (existingUser?.AuthType == UserAuthType.Nostr)

View File

@ -140,7 +140,7 @@ public static class VoidStartup
services.AddAuthentication(o => services.AddAuthentication(o =>
{ {
o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
o.AddScheme<NostrAuthHandler>(NostrAuth.Scheme, "Nostr"); o.AddScheme<NostrAuthHandler>(NostrAuth.Scheme, null);
}) })
.AddJwtBearer(options => .AddJwtBearer(options =>
{ {

View File

@ -1,6 +1,6 @@
{ {
"name": "@void-cat/api", "name": "@void-cat/api",
"version": "1.0.8", "version": "1.0.10",
"description": "void.cat API package", "description": "void.cat API package",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",

View File

@ -1,49 +1,58 @@
import { import {
AdminProfile, AdminUserListResult, AdminProfile,
ApiError, ApiKey, AdminUserListResult,
ApiError,
ApiKey,
LoginSession, LoginSession,
PagedRequest, PagedRequest,
PagedResponse, PagedResponse,
PaymentOrder, PaymentOrder,
Profile, SetPaymentConfigRequest, Profile,
SetPaymentConfigRequest,
SiteInfoResponse, SiteInfoResponse,
VoidFileResponse VoidFileResponse,
} from "./index"; } from "./index";
import { ProgressHandler, ProxyChallengeHandler, StateChangeHandler, VoidUploader } from "./upload"; import {
ProgressHandler,
ProxyChallengeHandler,
StateChangeHandler,
VoidUploader,
} from "./upload";
import { StreamUploader } from "./stream-uploader"; import { StreamUploader } from "./stream-uploader";
import { XHRUploader } from "./xhr-uploader"; import { XHRUploader } from "./xhr-uploader";
export class VoidApi { export type AuthHandler = (url: string, method: string) => Promise<string>;
readonly #uri: string
readonly #auth?: string
readonly #scheme?: string
constructor(uri: string, auth?: string, scheme?: string) { export class VoidApi {
readonly #uri: string;
readonly #auth?: AuthHandler;
constructor(uri: string, auth?: AuthHandler) {
this.#uri = uri; this.#uri = uri;
this.#auth = auth; this.#auth = auth;
this.#scheme = scheme;
} }
async #req<T>(method: string, url: string, body?: object): Promise<T> { async #req<T>(method: string, url: string, body?: object): Promise<T> {
let headers: HeadersInit = { const absoluteUrl = `${this.#uri}${url}`;
"Accept": "application/json" const headers: HeadersInit = {
Accept: "application/json",
}; };
if (this.#auth) { if (this.#auth) {
headers["Authorization"] = `${this.#scheme ?? "Bearer"} ${this.#auth}`; headers["Authorization"] = await this.#auth(absoluteUrl, method);
} }
if (body) { if (body) {
headers["Content-Type"] = "application/json"; headers["Content-Type"] = "application/json";
} }
const res = await fetch(`${this.#uri}${url}`, { const res = await fetch(absoluteUrl, {
method, method,
headers, headers,
mode: "cors", mode: "cors",
body: body ? JSON.stringify(body) : undefined body: body ? JSON.stringify(body) : undefined,
}); });
const text = await res.text(); const text = await res.text();
if (res.ok) { if (res.ok) {
return text ? JSON.parse(text) as T : {} as T; return text ? (JSON.parse(text) as T) : ({} as T);
} else { } else {
throw new ApiError(res.status, text); throw new ApiError(res.status, text);
} }
@ -60,9 +69,25 @@ export class VoidApi {
chunkSize?: number chunkSize?: number
): VoidUploader { ): VoidUploader {
if (StreamUploader.canUse()) { if (StreamUploader.canUse()) {
return new StreamUploader(this.#uri, file, stateChange, progress, proxyChallenge, this.#auth, chunkSize); return new StreamUploader(
this.#uri,
file,
stateChange,
progress,
proxyChallenge,
this.#auth,
chunkSize
);
} else { } else {
return new XHRUploader(this.#uri, file, stateChange, progress, proxyChallenge, this.#auth, chunkSize); return new XHRUploader(
this.#uri,
file,
stateChange,
progress,
proxyChallenge,
this.#auth,
chunkSize
);
} }
} }
@ -70,7 +95,7 @@ export class VoidApi {
* General site information * General site information
*/ */
info() { info() {
return this.#req<SiteInfoResponse>("GET", "/info") return this.#req<SiteInfoResponse>("GET", "/info");
} }
fileInfo(id: string) { fileInfo(id: string) {
@ -90,11 +115,19 @@ export class VoidApi {
} }
login(username: string, password: string, captcha?: string) { login(username: string, password: string, captcha?: string) {
return this.#req<LoginSession>("POST", `/auth/login`, { username, password, captcha }); return this.#req<LoginSession>("POST", `/auth/login`, {
username,
password,
captcha,
});
} }
register(username: string, password: string, captcha?: string) { register(username: string, password: string, captcha?: string) {
return this.#req<LoginSession>("POST", `/auth/register`, { username, password, captcha }); return this.#req<LoginSession>("POST", `/auth/register`, {
username,
password,
captcha,
});
} }
getUser(id: string) { getUser(id: string) {
@ -106,7 +139,11 @@ export class VoidApi {
} }
listUserFiles(uid: string, pageReq: PagedRequest) { listUserFiles(uid: string, pageReq: PagedRequest) {
return this.#req<PagedResponse<VoidFileResponse>>("POST", `/user/${uid}/files`, pageReq); return this.#req<PagedResponse<VoidFileResponse>>(
"POST",
`/user/${uid}/files`,
pageReq
);
} }
submitVerifyCode(uid: string, code: string) { submitVerifyCode(uid: string, code: string) {
@ -130,7 +167,11 @@ export class VoidApi {
} }
adminListFiles(pageReq: PagedRequest) { adminListFiles(pageReq: PagedRequest) {
return this.#req<PagedResponse<VoidFileResponse>>("POST", "/admin/file", pageReq); return this.#req<PagedResponse<VoidFileResponse>>(
"POST",
"/admin/file",
pageReq
);
} }
adminDeleteFile(id: string) { adminDeleteFile(id: string) {
@ -138,7 +179,11 @@ export class VoidApi {
} }
adminUserList(pageReq: PagedRequest) { adminUserList(pageReq: PagedRequest) {
return this.#req<PagedResponse<AdminUserListResult>>("POST", `/admin/users`, pageReq); return this.#req<PagedResponse<AdminUserListResult>>(
"POST",
`/admin/users`,
pageReq
);
} }
adminUpdateUser(u: AdminProfile) { adminUpdateUser(u: AdminProfile) {

View File

@ -55,6 +55,7 @@ export class StreamUploader extends VoidUploader {
highWaterMark: DefaultChunkSize highWaterMark: DefaultChunkSize
}); });
const absoluteUrl = `${this.uri}/upload`;
const reqHeaders = { const reqHeaders = {
"Content-Type": "application/octet-stream", "Content-Type": "application/octet-stream",
"V-Content-Type": !this.file.type ? "application/octet-stream" : this.file.type, "V-Content-Type": !this.file.type ? "application/octet-stream" : this.file.type,
@ -65,9 +66,9 @@ export class StreamUploader extends VoidUploader {
reqHeaders["V-EncryptionParams"] = JSON.stringify(this.#encrypt!.getParams()); reqHeaders["V-EncryptionParams"] = JSON.stringify(this.#encrypt!.getParams());
} }
if (this.auth) { if (this.auth) {
reqHeaders["Authorization"] = `Bearer ${this.auth}`; reqHeaders["Authorization"] = await this.auth(absoluteUrl, "POST");
} }
const req = await fetch(`${this.uri}/upload`, { const req = await fetch(absoluteUrl, {
method: "POST", method: "POST",
mode: "cors", mode: "cors",
body: this.#encrypt ? rsBase.pipeThrough(this.#encrypt!.getEncryptionTransform()) : rsBase, body: this.#encrypt ? rsBase.pipeThrough(this.#encrypt!.getEncryptionTransform()) : rsBase,

View File

@ -2,6 +2,8 @@ import { VoidUploadResult } from "./index";
import sjcl from "sjcl"; import sjcl from "sjcl";
import { sjclcodec } from "./codecBytes"; import { sjclcodec } from "./codecBytes";
import { buf2hex } from "./utils"; import { buf2hex } from "./utils";
import { AuthHandler } from "./api";
/** /**
* Generic upload state * Generic upload state
*/ */
@ -12,7 +14,7 @@ export enum UploadState {
Uploading, Uploading,
Done, Done,
Failed, Failed,
Challenge Challenge,
} }
export type StateChangeHandler = (s: UploadState) => void; export type StateChangeHandler = (s: UploadState) => void;
@ -25,7 +27,7 @@ export type ProxyChallengeHandler = (html: string) => void;
export abstract class VoidUploader { export abstract class VoidUploader {
protected uri: string; protected uri: string;
protected file: File | Blob; protected file: File | Blob;
protected auth?: string; protected auth?: AuthHandler;
protected maxChunkSize: number; protected maxChunkSize: number;
protected onStateChange?: StateChangeHandler; protected onStateChange?: StateChangeHandler;
protected onProgress?: ProgressHandler; protected onProgress?: ProgressHandler;
@ -37,7 +39,7 @@ export abstract class VoidUploader {
stateChange?: StateChangeHandler, stateChange?: StateChangeHandler,
progress?: ProgressHandler, progress?: ProgressHandler,
proxyChallenge?: ProxyChallengeHandler, proxyChallenge?: ProxyChallengeHandler,
auth?: string, auth?: AuthHandler,
chunkSize?: number chunkSize?: number
) { ) {
this.uri = uri; this.uri = uri;
@ -65,7 +67,7 @@ export abstract class VoidUploader {
const slice = file.slice(offset, offset + ChunkSize); const slice = file.slice(offset, offset + ChunkSize);
const chunk = await slice.arrayBuffer(); const chunk = await slice.arrayBuffer();
sha.update(sjclcodec.toBits(new Uint8Array(chunk))); sha.update(sjclcodec.toBits(new Uint8Array(chunk)));
this.onProgress?.(progress += chunk.byteLength); this.onProgress?.((progress += chunk.byteLength));
} }
return buf2hex(sjclcodec.fromBits(sha.finalize())); return buf2hex(sjclcodec.fromBits(sha.finalize()));
} }

View File

@ -20,17 +20,37 @@ export class XHRUploader extends VoidUploader {
if (this.file.size > this.maxChunkSize) { if (this.file.size > this.maxChunkSize) {
return await this.#doSplitXHRUpload(hash, this.maxChunkSize, headers); return await this.#doSplitXHRUpload(hash, this.maxChunkSize, headers);
} else { } else {
return await this.#xhrSegment(this.file, hash, undefined, undefined, 1, 1, headers); return await this.#xhrSegment(
this.file,
hash,
undefined,
undefined,
1,
1,
headers
);
} }
} }
async #doSplitXHRUpload(hash: string, splitSize: number, headers?: HeadersInit) { async #doSplitXHRUpload(
hash: string,
splitSize: number,
headers?: HeadersInit
) {
let xhr: VoidUploadResult | null = null; let xhr: VoidUploadResult | null = null;
const segments = Math.ceil(this.file.size / splitSize); const segments = Math.ceil(this.file.size / splitSize);
for (let s = 0; s < segments; s++) { for (let s = 0; s < segments; s++) {
const offset = s * splitSize; const offset = s * splitSize;
const slice = this.file.slice(offset, offset + splitSize, this.file.type); const slice = this.file.slice(offset, offset + splitSize, this.file.type);
xhr = await this.#xhrSegment(slice, hash, xhr?.file?.id, xhr?.file?.metadata?.editSecret, s + 1, segments, headers); xhr = await this.#xhrSegment(
slice,
hash,
xhr?.file?.id,
xhr?.file?.metadata?.editSecret,
s + 1,
segments,
headers
);
if (!xhr.ok) { if (!xhr.ok) {
break; break;
} }
@ -48,10 +68,22 @@ export class XHRUploader extends VoidUploader {
* @param partOf Total number of segments * @param partOf Total number of segments
* @param headers * @param headers
*/ */
async #xhrSegment(segment: ArrayBuffer | Blob, fullDigest: string, async #xhrSegment(
id?: string, editSecret?: string, part?: number, partOf?: number, headers?: HeadersInit) { segment: ArrayBuffer | Blob,
fullDigest: string,
id?: string,
editSecret?: string,
part?: number,
partOf?: number,
headers?: HeadersInit
) {
this.onStateChange?.(UploadState.Uploading); this.onStateChange?.(UploadState.Uploading);
const absoluteUrl = id ? `${this.uri}/upload/${id}` : `${this.uri}/upload`;
const authValue = this.auth
? await this.auth(absoluteUrl, "POST")
: undefined;
return await new Promise<VoidUploadResult>((resolve, reject) => { return await new Promise<VoidUploadResult>((resolve, reject) => {
try { try {
const req = new XMLHttpRequest(); const req = new XMLHttpRequest();
@ -59,7 +91,10 @@ export class XHRUploader extends VoidUploader {
if (req.readyState === XMLHttpRequest.DONE && req.status === 200) { if (req.readyState === XMLHttpRequest.DONE && req.status === 200) {
const rsp = JSON.parse(req.responseText) as VoidUploadResult; const rsp = JSON.parse(req.responseText) as VoidUploadResult;
resolve(rsp); resolve(rsp);
} else if (req.readyState === XMLHttpRequest.DONE && req.status === 403) { } else if (
req.readyState === XMLHttpRequest.DONE &&
req.status === 403
) {
const contentType = req.getResponseHeader("content-type"); const contentType = req.getResponseHeader("content-type");
if (contentType?.toLowerCase().trim().indexOf("text/html") === 0) { if (contentType?.toLowerCase().trim().indexOf("text/html") === 0) {
this.onProxyChallenge?.(req.response); this.onProxyChallenge?.(req.response);
@ -73,15 +108,21 @@ export class XHRUploader extends VoidUploader {
this.onProgress?.(e.loaded); this.onProgress?.(e.loaded);
} }
}; };
req.open("POST", id ? `${this.uri}/upload/${id}` : `${this.uri}/upload`); req.open("POST", absoluteUrl);
req.setRequestHeader("Content-Type", "application/octet-stream"); req.setRequestHeader("Content-Type", "application/octet-stream");
req.setRequestHeader("V-Content-Type", !this.file.type ? "application/octet-stream" : this.file.type); req.setRequestHeader(
req.setRequestHeader("V-Filename", "name" in this.file ? this.file.name : ""); "V-Content-Type",
!this.file.type ? "application/octet-stream" : this.file.type
);
req.setRequestHeader(
"V-Filename",
"name" in this.file ? this.file.name : ""
);
req.setRequestHeader("V-Full-Digest", fullDigest); req.setRequestHeader("V-Full-Digest", fullDigest);
req.setRequestHeader("V-Segment", `${part}/${partOf}`) req.setRequestHeader("V-Segment", `${part}/${partOf}`);
if (this.auth) { if (authValue) {
req.withCredentials = true; req.withCredentials = true;
req.setRequestHeader("Authorization", `Bearer ${this.auth}`); req.setRequestHeader("Authorization", authValue);
} }
if (editSecret) { if (editSecret) {
req.setRequestHeader("V-EditSecret", editSecret); req.setRequestHeader("V-EditSecret", editSecret);