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

View File

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

View File

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

View File

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

View File

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

View File

@ -55,6 +55,7 @@ export class StreamUploader extends VoidUploader {
highWaterMark: DefaultChunkSize
});
const absoluteUrl = `${this.uri}/upload`;
const reqHeaders = {
"Content-Type": "application/octet-stream",
"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());
}
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",
mode: "cors",
body: this.#encrypt ? rsBase.pipeThrough(this.#encrypt!.getEncryptionTransform()) : rsBase,

View File

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

View File

@ -20,17 +20,37 @@ export class XHRUploader extends VoidUploader {
if (this.file.size > this.maxChunkSize) {
return await this.#doSplitXHRUpload(hash, this.maxChunkSize, headers);
} 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;
const segments = Math.ceil(this.file.size / splitSize);
for (let s = 0; s < segments; s++) {
const offset = s * splitSize;
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) {
break;
}
@ -48,10 +68,22 @@ export class XHRUploader extends VoidUploader {
* @param partOf Total number of segments
* @param headers
*/
async #xhrSegment(segment: ArrayBuffer | Blob, fullDigest: string,
id?: string, editSecret?: string, part?: number, partOf?: number, headers?: HeadersInit) {
async #xhrSegment(
segment: ArrayBuffer | Blob,
fullDigest: string,
id?: string,
editSecret?: string,
part?: number,
partOf?: number,
headers?: HeadersInit
) {
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) => {
try {
const req = new XMLHttpRequest();
@ -59,7 +91,10 @@ export class XHRUploader extends VoidUploader {
if (req.readyState === XMLHttpRequest.DONE && req.status === 200) {
const rsp = JSON.parse(req.responseText) as VoidUploadResult;
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");
if (contentType?.toLowerCase().trim().indexOf("text/html") === 0) {
this.onProxyChallenge?.(req.response);
@ -73,15 +108,21 @@ export class XHRUploader extends VoidUploader {
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("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-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-Segment", `${part}/${partOf}`)
if (this.auth) {
req.setRequestHeader("V-Segment", `${part}/${partOf}`);
if (authValue) {
req.withCredentials = true;
req.setRequestHeader("Authorization", `Bearer ${this.auth}`);
req.setRequestHeader("Authorization", authValue);
}
if (editSecret) {
req.setRequestHeader("V-EditSecret", editSecret);