sliftutils 1.4.2 → 1.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/filehoster.js +4 -0
- package/index.d.ts +105 -0
- package/package.json +72 -70
- package/render-utils/FullscreenModal.tsx +5 -1
- package/storage/BulkDatabase2/BulkDatabase2.d.ts +21 -0
- package/storage/BulkDatabase2/BulkDatabase2.ts +14 -0
- package/storage/BulkDatabase2/BulkDatabaseBase.d.ts +22 -0
- package/storage/BulkDatabase2/BulkDatabaseBase.ts +209 -69
- package/storage/BulkDatabase2/mergeLock.ts +5 -3
- package/storage/BulkDatabase2/streamLog.ts +4 -7
- package/storage/FileFolderAPI.tsx +81 -0
- package/storage/remoteFileServer.d.ts +16 -0
- package/storage/remoteFileServer.ts +439 -0
- package/storage/remoteFileStorage.d.ts +38 -0
- package/storage/remoteFileStorage.ts +236 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import https from "https";
|
|
2
|
+
import type { IncomingMessage, ServerResponse } from "http";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import crypto from "crypto";
|
|
6
|
+
import os from "os";
|
|
7
|
+
import { execFileSync } from "child_process";
|
|
8
|
+
import { getExternalIP } from "socket-function/src/networking";
|
|
9
|
+
import { forwardPort } from "socket-function/src/forwardPort";
|
|
10
|
+
|
|
11
|
+
// Remote file server for sliftutils getRemoteFileStorage / BulkDatabase2. Serves one folder on disk over
|
|
12
|
+
// self-signed HTTPS, authenticated with an auto-generated 6-word password (sent as
|
|
13
|
+
// `Authorization: Bearer <password>`). Run it with `yarn filehoster <folder>` (or the `filehoster` bin).
|
|
14
|
+
//
|
|
15
|
+
// SECURITY: the cert is self-signed, so a browser must accept it once (open the printed https URL and
|
|
16
|
+
// click through), and the Node client connects with cert verification disabled. The password is what
|
|
17
|
+
// authorizes access; keep it secret.
|
|
18
|
+
|
|
19
|
+
// Common, memorable words (frequency-ordered subset) for passphrase generation.
|
|
20
|
+
const WORDS: string[] = [
|
|
21
|
+
"that","with","which","this","from","have","they","were","their","there","when","them","said","been",
|
|
22
|
+
"would","will","what","more","then","into","some","could","time","very","than","your","other","upon",
|
|
23
|
+
"about","only","little","like","these","great","after","well","made","before","such","over","should",
|
|
24
|
+
"first","good","must","much","down","where","know","most","here","come","those","never","life","long",
|
|
25
|
+
"came","being","many","through","even","himself","back","every","shall","again","make","without","might",
|
|
26
|
+
"while","same","under","just","still","people","place","think","house","take","last","found","away",
|
|
27
|
+
"hand","went","thought","also","though","three","another","eyes","years","work","right","once","night",
|
|
28
|
+
"young","nothing","against","small","head","left","part","ever","world","each","father","give","between",
|
|
29
|
+
"face","king","things","love","because","took","always","tell","called","water","both","side","look",
|
|
30
|
+
"having","room","mind","half","heart","name","home","country","whole","however","find","among","going",
|
|
31
|
+
"thing","lord","looked","seen","mother","general","done","seemed","told","whom","days","soon","better",
|
|
32
|
+
"letter","woman","heard","asked","course","thus","moment","knew","light","enough","white","almost",
|
|
33
|
+
"until","quite","hands","words","death","large","taken","since","gave","given","best","state","brought",
|
|
34
|
+
"does","whose","door","others","power","perhaps","present","next","morning","poor","lady","four","high",
|
|
35
|
+
"year","turned","less","word","full","during","rather","want","order","near","feet","true","miss",
|
|
36
|
+
"matter","began","cannot","used","known","felt","above","round","thou","voice","till","case","nature",
|
|
37
|
+
"indeed","church","kind","certain","fire","often","stood","fact","friend","girl","five","land","says",
|
|
38
|
+
"john","myself","along","point","dear","wife","city","within","sent","times","keep","passed","form",
|
|
39
|
+
"second","body","money","believe","hundred","open","several","means","child","english","herself","sure",
|
|
40
|
+
"looking","women","already","black","alone","least","gone","held","itself","whether","hope","river",
|
|
41
|
+
"ground","either","number","chapter","england","leave","rest","town","hear","greek","friends","book",
|
|
42
|
+
"hour","short","cried","read","behind","became","making","family","earth","captain","around","dead",
|
|
43
|
+
"reason","call","become","lost","line","replied","help","coming","speak","manner","french","twenty",
|
|
44
|
+
"spirit","really","early","story","hard","close","human","public","truth","strong","master","care",
|
|
45
|
+
"towards","history","kept","later","states","dark","able","mean","return","brother","person","subject",
|
|
46
|
+
"soul","party","arms","thee","seems","common","fell","fine","feel","show","table","turn","wish","evening",
|
|
47
|
+
"free","cause","south","ready","north","across","rose","live","account","doubt","company","miles","road",
|
|
48
|
+
"bring","london","sense","horse","carried","hold","sight","fear","answer","idea","force","need","deep",
|
|
49
|
+
"further","nearly","past","army","blood","street","court","reached","view","school","sort","taking",
|
|
50
|
+
"else","chief","hours","beyond","cold","none","longer","strange","fellow","clear","service","natural",
|
|
51
|
+
"suppose","late","talk","front","stand","purpose","seem","neither","ought","west","real","except","sound",
|
|
52
|
+
"gold","forward","feeling","added","boys","self","peace","happy","living","husband","toward","spoke",
|
|
53
|
+
"fair","france","trees","effect","latter","length","change","died","green","united","fall","pretty",
|
|
54
|
+
"placed","meet","forth","office","comes","pass","written","ship","enemy","saying","tree","foot","blue",
|
|
55
|
+
"note","prince","hair","heaven","wild","play","entered","society","laid","wind","doing","paper","opened",
|
|
56
|
+
"bear","third","queen","mine","greater","various","faith","wanted","boat","stone","lived","george",
|
|
57
|
+
"window","doctor","action","books","tried","letters","makes","minutes","parts","wood","period","duty",
|
|
58
|
+
"york","instead","heavy","persons","battle","field","british","object","century","island","beauty",
|
|
59
|
+
"christ","sister","glad","below","east","opinion","save","places","months","food","sweet","trouble",
|
|
60
|
+
"system","born","desire","works","mary","chance","william","seven","single","hardly","sleep","mouth",
|
|
61
|
+
"horses","silence","ancient","broken","hall","send","rich","girls","besides","lips","henry","figure",
|
|
62
|
+
"slowly","hill","wall","future","lines","follow","german","march","filled","brown","deal","eight",
|
|
63
|
+
"covered","smile","former","week","drew","easy","paris","camp","stay","cross","seeing","result","started",
|
|
64
|
+
"caught","houses","appear","wrote","giving","simple","arrived","formed","bright","wide","uncle","piece",
|
|
65
|
+
"walked","knows","carry","middle","village","holy","merely","getting","raised","afraid","struck",
|
|
66
|
+
"charles","board","visit","royal","spring","dinner","evil","outside","wrong","summer","moved","danger",
|
|
67
|
+
"mark","fresh","plain","thirty","scene","quickly","wonder","jack","indian","walk","learned","class",
|
|
68
|
+
"secret","wait","command","easily","garden","loved","married","fight","leaving","music","usual","cases",
|
|
69
|
+
"winter","respect","value","quiet","please","stopped","write","laughed","america","chair","paid","duke",
|
|
70
|
+
"leaves","lower","colonel","perfect","james","worth","author","finally","youth","regard","glass",
|
|
71
|
+
"picture","built","modern","success","race","showed","tears","unless","mere","lives","grew","charge",
|
|
72
|
+
"beside","remain","waiting","study","hath","shot","unto","tone","iron","watch","grace","flowers","killed",
|
|
73
|
+
"allowed","news","step","proper","sitting","floor","justice","noble","goes","walls","laws","meant",
|
|
74
|
+
"bound","forms","fifty","drawn","private","fast","indians","judge","meeting","usually","sudden","gives",
|
|
75
|
+
"reach","bank","attempt","shore","stop","plan","silent","special","spot","officer","silver","passing",
|
|
76
|
+
"broke","lake","soft","journey","beneath","shown","turning","members","enter","lead","trade","names",
|
|
77
|
+
"escape","troops","bill","corner","rule","ladies","species","rock","similar","running","rate","cast",
|
|
78
|
+
"nation","post","likely","simply","orders","dress","fish","minute","birds","europe","passage","surface",
|
|
79
|
+
"snow","attack","higher","rise","grand","reply","honour","notice","speech","trying","game","writing",
|
|
80
|
+
"closed","rome","example","aunt","learn","morrow","offered","peter","equal","space","page","wished",
|
|
81
|
+
"changed","animal","social","twelve","appears","receive","valley","degree","train","steps","fixed",
|
|
82
|
+
"count","forest","matters","foreign","native","broad","moral","animals","safe","roman","break","pale",
|
|
83
|
+
"memory","warm","trust","yellow","sides","main","bird","reading","coast","pain","grave","forget","move",
|
|
84
|
+
"fortune","madame","proved","forty","lying","quick","wise","jesus","working","surely","looks","seat",
|
|
85
|
+
"divine","touch","loss","paul","heads","spent","ordered","report","caused","thomas","ones","drawing",
|
|
86
|
+
"talking","breath","meaning","month","union","pleased","powers","emperor","laugh","affairs","narrow",
|
|
87
|
+
"square","science","begin","promise","takes","conduct","serious","thank","curious","pieces","health",
|
|
88
|
+
"exactly","ways","upper","decided","nine","stream","wine","engaged","points","amount","named","song",
|
|
89
|
+
"worse","size","castle","crown","mass","liberty","stage","guard","servant","greatly","price","weeks",
|
|
90
|
+
"neck","ears","drink","crowd","thick","fallen","hills","spoken","golden","capital","sign","spite","terms",
|
|
91
|
+
"sake","council","threw","ships","portion","support","clothes","effort","fully","holding","majesty",
|
|
92
|
+
"glory","pure","facts","sword","sorry","fate","bread","prove","station","sharp","dream","vain","major",
|
|
93
|
+
"smiled","instant","spread","serve","sick","june","ideas","thrown","start","weather","gray","courage",
|
|
94
|
+
"anxious","woods","path","rising","grass","moon","gate","forced","fancy","bottom","expect","remains",
|
|
95
|
+
"shook","bishop","watched","settled","events","rain","quarter","heat","palace","glance","kingdom",
|
|
96
|
+
"papers","aside","worthy","taste","pride","july","opening","daily","leading","wounded","famous","offer",
|
|
97
|
+
"group","distant","weight","highest","vast","popular","passion","knowing","allow","sought","lies",
|
|
98
|
+
"marked","series","growing","measure","hearts","robert","obliged","western","hence","style","dropped",
|
|
99
|
+
"nations","grown","honor","edge","pray","tall","frank","poet","streets","season","pointed","mighty",
|
|
100
|
+
"temple","extent","thin","blow","freedom","entire","keeping","prevent","shut","storm","lose","college",
|
|
101
|
+
"cost","marry","spirits","worked","colour","ring","listen","fruit","waters","fashion","share","hung",
|
|
102
|
+
"proud","fingers","method","served","grow","fort","draw","dollars","quietly","yours","vessel","direct",
|
|
103
|
+
"refused","bridge","gods","inside","title","advance","priest","becomes","dick","double","seek","guess",
|
|
104
|
+
"spanish","larger","ball","finding","edward","removed","brave","tired","agreed","sacred","sons","guns",
|
|
105
|
+
"played","richard","devil","pounds","honest","false","cloth","soldier","tongue","smith","wealth","failed",
|
|
106
|
+
"height","faces","equally","legs","pocket","twice","volume","prayer","county","notes","louis","unknown",
|
|
107
|
+
"delight","rights","nice","taught","coat","dare","slight","rocks","enemies","control","forces","germany",
|
|
108
|
+
"kill","process","david","rapidly","comfort","islands","centre","salt","april","windows","noticed",
|
|
109
|
+
"truly","color","fields","date","august","desired","breast","skin","gentle","smoke","search","joined",
|
|
110
|
+
"nobody","results","needed","weak","type","stones","follows","theory","shape","waited","telling","relief",
|
|
111
|
+
"bearing","bent","mile","dressed","birth","spain","female","clearly","moving","drive","crossed","member",
|
|
112
|
+
"saved","teeth","flesh","cover","shows","farther","stands","figures","numbers","bodies","supply","motion",
|
|
113
|
+
"grey","bell","alive","seized","shadow","produce","touched","driven","talked","empire","sunday","pair",
|
|
114
|
+
"fourth","slave","regular","dozen","beat","cousin","sand","soil","rough","claim","angry","pity","walking",
|
|
115
|
+
"acts","pope","rooms","task","stock","tender","throw","reader","india","hotel","fifteen","arrival",
|
|
116
|
+
"plate","stars","willing","stories","saint","amongst","virtue","chamber","civil","yards","habit","writer",
|
|
117
|
+
"putting","origin","italy","hearing","genius","milk","busy","fool","burning","duties","brain","slow",
|
|
118
|
+
"belief","bore","mention","grant","library","inches","press","calm","total","ride","lands","labor",
|
|
119
|
+
"parties","clean","catch","treated","rode","whilst","level","efforts","minds","welcome","wisdom","supper",
|
|
120
|
+
"final","mercy","aware","lovely","empty","wants","midst","central","rank","proof","burst","term","farm",
|
|
121
|
+
"applied","loud","nearer","harry","closely","kings","explain","deck","noise","hurt","advice","absence",
|
|
122
|
+
"circle","objects","secure","plants","parents","falling","base","fellows","sorrow","flat","flower",
|
|
123
|
+
"calling","imagine","range","sold","favour","happen","showing","earl","hole","careful","dust","prison",
|
|
124
|
+
"useful","accept","firm","sing","port","seated","custom","wore","doors","lifted","edition","tale",
|
|
125
|
+
"affair","policy","roof","express","ahead","worship","partly","address","highly","victory","ocean",
|
|
126
|
+
"begun","buried","content","smiling","liked","active","quality","philip","current","divided","growth",
|
|
127
|
+
"huge","moments","article","speed","poetry","machine","join","nose","unable","ceased","gained","worn",
|
|
128
|
+
"eastern","safety","ages","vessels","hopes","corn","slaves","drop","plant","evident","local","police",
|
|
129
|
+
"october","obtain","italian","deeply","dying","wholly","dogs","playing","plenty","apart","suffer","maid",
|
|
130
|
+
"record","fairly","kindly","terror","drove","ends","sugar","capable","irish","message","chosen","flight",
|
|
131
|
+
"reasons","couple","meat","printed","blind","energy","bitter","baby","mistake","tower","ireland","trial",
|
|
132
|
+
"wear","brief","market","band","causes","clouds","goods","admit","cities","earlier","tells","stated",
|
|
133
|
+
"pressed","crime","fought","guide","hoped","list","banks","knight","fleet","pulled","tail","patient",
|
|
134
|
+
"mental","boats","kinds","stairs","star","cattle","rare","wings","disease","section","actual","bare",
|
|
135
|
+
"extreme","cruel","worst","escaped","dance","strike","views","eggs","needs","store","choice","fond",
|
|
136
|
+
"adopted","suit","singing","fail","souls","thanks","hidden","shame","faint","shop","older","joseph",
|
|
137
|
+
"knees","gently","blessed","yard","cent","grief","male","sooner","january","excited","region","praise",
|
|
138
|
+
"throne","classes","effects","demand","gain","fault","labour","variety","loose","cook","devoted","ended",
|
|
139
|
+
"changes","witness","bought","lover","visited","club","sheep","cloud","enjoy","asleep","cool","dull",
|
|
140
|
+
"painted","arose","armed","dignity","chiefly","voices","sail","event","signs","hero","schools","branch",
|
|
141
|
+
"hurried","arthur","kitchen","stick","coffee","thinks","eager","natives","flying","boston","eternal",
|
|
142
|
+
"source","fill","anger","vision","murder","viii","park","avoid","awful","raise","desert","plans","pardon",
|
|
143
|
+
"assured","wound","finger","bible","rear","details","cabin","whence","reign","weary","slavery","visible",
|
|
144
|
+
"shouted","seldom","skill","hast","throat","reality","leader","egypt","credit","smaller","severe","calls",
|
|
145
|
+
"younger","shell","sprang","anybody","handed","despair","asking","inch","mystery","manners","knife",
|
|
146
|
+
"design","sounds","wishes","stepped","towns","sixty","immense","china","favor","latin","hate","setting",
|
|
147
|
+
"excuse","chinese","behold","rushed","choose","ease","lights","paused","mission","voyage","gift","bold",
|
|
148
|
+
"meal","britain","smooth","mounted","utterly","lest","helped","picked","falls","pipe","bones","earnest",
|
|
149
|
+
"granted","swept","anxiety","teach","waves","softly","savage","hunting","defence","rapid","artist",
|
|
150
|
+
"recent","supreme","russian","created","habits","exist","guilty","pages","crew","hell","remark","request",
|
|
151
|
+
"harm","solemn","observe","trail","copy","violent","dreams","proceed","luck","steel","sell","temper",
|
|
152
|
+
"appeal","hide","fled","belong","triumph","abroad","waste","consent","writers","possess","jews","require",
|
|
153
|
+
"priests","railway","founded","pushed","host","solid","maybe","degrees","cheeks","mount","ruin","owing",
|
|
154
|
+
"fierce","reduced","mankind","text","swift","riding","silk","shade","career","wicked","afford","alas",
|
|
155
|
+
"rules","treaty","correct","spend","foolish","methods","remarks","horror","shining","enjoyed","tribes",
|
|
156
|
+
"lack","frame","gets","eagerly","russia","alike","somehow","nodded","problem","fever","sees","stern",
|
|
157
|
+
"dawn","forgive","flag","managed","fame","travel","estate","cotton","hurry","agree","feared","kiss",
|
|
158
|
+
"million","martin","mixed","risk","butter","jane","assumed","pause","benefit","unhappy","lighted",
|
|
159
|
+
"scheme","slept","plainly","forgot","delay","merry","rush","wooden","secured","poem","rolled","angel",
|
|
160
|
+
"guests","farmer","humble","glanced","shortly","rope","image","lately","africa","fired","retired","bull",
|
|
161
|
+
"steam","keen","loving","borne","readily","beloved","average","teacher","attend","flame","area","burned",
|
|
162
|
+
"thrust","negro","nurse","spare","fifth","thence","roads","refuse","tent","kissed","gates","gaze","fatal",
|
|
163
|
+
"israel","verse","scenes","confess","uttered","build","acted","hanging","reward","issue","utmost",
|
|
164
|
+
"fathers","noted","widow","related","urged","mode","failure","nervous","render","masters","tribe",
|
|
165
|
+
"colored","turns","alarm","rivers","using","eleven","rage","hers","lamp","retreat","amid","gospel",
|
|
166
|
+
"exposed","johnson","marks","tide","emotion","destroy","inner","sisters","lion","helen","tied","grounds",
|
|
167
|
+
"acid","wheel","issued","invited","gazed","senate","finds","seed","regret","clay","chain","crowded",
|
|
168
|
+
"bosom","limited","steady","chap","anne","francis","cavalry","freely","charm","faced","ideal","useless",
|
|
169
|
+
"warning","shelter","vote","xpage","cottage","beast","outer","folks","misery","anyone","expense","firmly",
|
|
170
|
+
"eating","lonely","owner","trace","coal","hastily","elder","utter","begins","chapel","nights","dared",
|
|
171
|
+
"signal","stared","track","lords","speaks","bottle","blame","claims","shoes","sigh","ghost","folk",
|
|
172
|
+
"dutch","flung","flew","cheek","staff","walter","clever","acting","hungry","poems","cutting","forever",
|
|
173
|
+
"exact","haste","dancing","trip","lincoln","pull","vice","slipped","thine","loves","permit","mill",
|
|
174
|
+
"engine","hollow","seeking","bowed","largely","charged","painful","madam","roll","leaf","prayers",
|
|
175
|
+
"element","agent","cries","songs","theatre","creek","match","ashamed","runs","aspect","canada","treat",
|
|
176
|
+
"legal","arts","corps","noon","nearest","scale","hunt","shadows","winds","landed","beings","tiny",
|
|
177
|
+
"gardens","bless","clerk","doth","derived","hang","heavens","cape","ours","brow","test","senses",
|
|
178
|
+
"opposed","error","driving","nest","headed","groups","princes","feast","admiral","poured","brings",
|
|
179
|
+
"pursued","germans","bears","lesson","journal","unusual","marched","wing","remove","studied","passes",
|
|
180
|
+
"actions","burden","colony","scott","bride","yield","crying","forming","rifle","powder","rested","shake",
|
|
181
|
+
"guest","begged","occur","altar","sorts","beaten","wave","papa","sang","depth","aloud","contact",
|
|
182
|
+
"intense","marble","poverty","detail","writes","root","metal","jean","existed","ability","purple",
|
|
183
|
+
"counsel","lane","wives","sending","heavily","leaders","queer","tales","sins","mexico","protect","clock",
|
|
184
|
+
"stuff","jones","pine","records","cave","baron","medical","pick","stayed","magic","seventy","pour",
|
|
185
|
+
"saddle","sank","dread","deny","wire","rude","holds","strain","autumn","shed","shoot","settle","basis",
|
|
186
|
+
"readers","route","beach","safely","oxford","notion","thunder","sale","saints","pound","shock","elected",
|
|
187
|
+
"signed","whereas","maiden","courts","rarely","wolf","mamma","billy","student","charity","impulse",
|
|
188
|
+
"tones","mortal","raising","wedding","belongs","chest","sharply","sounded","poets","lawyer","hugh",
|
|
189
|
+
"plays","awake","landing","debt","alice","venture","idle","uniform","contain","tobacco","boots","fears",
|
|
190
|
+
"leaned","studies","customs","prize","stir","obey","oath","badly","pursuit","resting",
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
export function generatePassword(wordCount: number): string {
|
|
194
|
+
const words: string[] = [];
|
|
195
|
+
for (let i = 0; i < wordCount; i++) words.push(WORDS[crypto.randomInt(WORDS.length)]);
|
|
196
|
+
return words.join(" ");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// The password is always a list of words, so we compare case-insensitively and ignore any non-letter
|
|
200
|
+
// characters. That makes it forgiving of voice dictation and mobile autocorrect (spacing, capitals,
|
|
201
|
+
// stray punctuation) without meaningfully weakening a 6-word passphrase.
|
|
202
|
+
function normalizePassword(p: string): string {
|
|
203
|
+
return String(p || "").toLowerCase().replace(/[^a-z]/g, "");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Generate (once) and cache a self-signed key + cert via openssl, outside the served folder so its
|
|
207
|
+
// private key is never reachable through the API.
|
|
208
|
+
function getCert(): { key: Buffer; cert: Buffer } {
|
|
209
|
+
const dir = path.join(os.homedir(), ".sliftutils-remote");
|
|
210
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
211
|
+
const keyPath = path.join(dir, "key.pem");
|
|
212
|
+
const certPath = path.join(dir, "cert.pem");
|
|
213
|
+
if (!fs.existsSync(keyPath) || !fs.existsSync(certPath)) {
|
|
214
|
+
try {
|
|
215
|
+
execFileSync("openssl", [
|
|
216
|
+
"req", "-x509", "-newkey", "rsa:2048", "-nodes",
|
|
217
|
+
"-keyout", keyPath, "-out", certPath, "-days", "3650",
|
|
218
|
+
"-subj", "/CN=sliftutils-remote",
|
|
219
|
+
"-addext", "subjectAltName=DNS:localhost,IP:127.0.0.1",
|
|
220
|
+
], { stdio: "ignore" });
|
|
221
|
+
} catch (e) {
|
|
222
|
+
throw new Error("Failed to generate a self-signed certificate with openssl (is openssl installed?): " + (e as Error).message);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return { key: fs.readFileSync(keyPath), cert: fs.readFileSync(certPath) };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function timingSafeEqualStr(a: string, b: string): boolean {
|
|
229
|
+
const ab = Buffer.from(a || "", "utf8");
|
|
230
|
+
const bb = Buffer.from(b || "", "utf8");
|
|
231
|
+
if (ab.length !== bb.length) return false;
|
|
232
|
+
return crypto.timingSafeEqual(ab, bb);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Resolve a client-supplied relative path against the root, refusing anything that escapes it.
|
|
236
|
+
function safeResolve(root: string, rel: string | null): string | undefined {
|
|
237
|
+
if (rel == null) rel = "";
|
|
238
|
+
if (rel.indexOf("\0") >= 0) return undefined;
|
|
239
|
+
const full = path.resolve(root, "." + path.sep + rel.replace(/\\/g, "/"));
|
|
240
|
+
if (full !== root && !full.startsWith(root + path.sep)) return undefined;
|
|
241
|
+
return full;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function readBody(req: IncomingMessage): Promise<Buffer> {
|
|
245
|
+
return new Promise((resolve, reject) => {
|
|
246
|
+
const chunks: Buffer[] = [];
|
|
247
|
+
req.on("data", c => chunks.push(c as Buffer));
|
|
248
|
+
req.on("end", () => resolve(Buffer.concat(chunks)));
|
|
249
|
+
req.on("error", reject);
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function formatBytes(n: number): string {
|
|
254
|
+
if (n < 1024) return n + "B";
|
|
255
|
+
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + "KB";
|
|
256
|
+
if (n < 1024 * 1024 * 1024) return (n / 1024 / 1024).toFixed(1) + "MB";
|
|
257
|
+
return (n / 1024 / 1024 / 1024).toFixed(2) + "GB";
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export type RemoteFileServerOptions = {
|
|
261
|
+
root: string;
|
|
262
|
+
port?: number;
|
|
263
|
+
host?: string;
|
|
264
|
+
password?: string;
|
|
265
|
+
// When true, log batched per-(client, file) access totals every 5s. The CLI turns this on; the
|
|
266
|
+
// library (tests) leaves it off.
|
|
267
|
+
logAccess?: boolean;
|
|
268
|
+
};
|
|
269
|
+
export type RemoteFileServerHandle = { port: number; password: string; url: string; close: () => Promise<void> };
|
|
270
|
+
|
|
271
|
+
export function startRemoteFileServer(options: RemoteFileServerOptions): Promise<RemoteFileServerHandle> {
|
|
272
|
+
const root = path.resolve(options.root);
|
|
273
|
+
if (!fs.existsSync(root)) fs.mkdirSync(root, { recursive: true });
|
|
274
|
+
const port = options.port ?? 8787;
|
|
275
|
+
const host = options.host || "0.0.0.0";
|
|
276
|
+
const password = options.password || generatePassword(6);
|
|
277
|
+
const normPassword = normalizePassword(password);
|
|
278
|
+
const { key, cert } = getCert();
|
|
279
|
+
|
|
280
|
+
// Batched access log: aggregate per (ip, op, file) so hammering one file logs once per 5s with the
|
|
281
|
+
// total request count and bytes, instead of a line per request.
|
|
282
|
+
type Acc = { ip: string; op: string; path: string; count: number; bytes: number };
|
|
283
|
+
const accessLog = new Map<string, Acc>();
|
|
284
|
+
let flushTimer: ReturnType<typeof setInterval> | undefined;
|
|
285
|
+
const recordAccess = (ip: string, op: string, p: string, bytes: number) => {
|
|
286
|
+
if (!options.logAccess) return;
|
|
287
|
+
const k = ip + "|" + op + "|" + p;
|
|
288
|
+
let e = accessLog.get(k);
|
|
289
|
+
if (!e) { e = { ip, op, path: p, count: 0, bytes: 0 }; accessLog.set(k, e); }
|
|
290
|
+
e.count++;
|
|
291
|
+
e.bytes += bytes;
|
|
292
|
+
};
|
|
293
|
+
if (options.logAccess) {
|
|
294
|
+
flushTimer = setInterval(() => {
|
|
295
|
+
if (!accessLog.size) return;
|
|
296
|
+
const rows = [...accessLog.values()].sort((a, b) => b.bytes - a.bytes);
|
|
297
|
+
accessLog.clear();
|
|
298
|
+
for (const e of rows) console.log(` [${e.op}] ${e.ip} ${e.path} ${e.count}x ${formatBytes(e.bytes)}`);
|
|
299
|
+
}, 5000);
|
|
300
|
+
flushTimer.unref?.();
|
|
301
|
+
}
|
|
302
|
+
const clientIp = (req: IncomingMessage) => (req.socket.remoteAddress || "?").replace(/^::ffff:/, "");
|
|
303
|
+
|
|
304
|
+
const server = https.createServer({ key, cert }, async (req: IncomingMessage, res: ServerResponse) => {
|
|
305
|
+
const origin = (req.headers["origin"] as string) || "*";
|
|
306
|
+
const cors: Record<string, string> = {
|
|
307
|
+
"Access-Control-Allow-Origin": origin,
|
|
308
|
+
"Access-Control-Allow-Methods": "GET, PUT, POST, DELETE, OPTIONS",
|
|
309
|
+
"Access-Control-Allow-Headers": "authorization, content-type",
|
|
310
|
+
"Access-Control-Max-Age": "86400",
|
|
311
|
+
"Vary": "Origin",
|
|
312
|
+
};
|
|
313
|
+
const send = (status: number, headers: Record<string, string>, body?: Buffer | string) => {
|
|
314
|
+
res.writeHead(status, Object.assign({}, cors, headers));
|
|
315
|
+
res.end(body);
|
|
316
|
+
};
|
|
317
|
+
const sendJson = (status: number, obj: unknown) => send(status, { "Content-Type": "application/json" }, JSON.stringify(obj));
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
if (req.method === "OPTIONS") return send(204, {});
|
|
321
|
+
|
|
322
|
+
const auth = (req.headers["authorization"] as string) || "";
|
|
323
|
+
const token = auth.startsWith("Bearer ") ? auth.slice(7) : "";
|
|
324
|
+
if (!timingSafeEqualStr(normalizePassword(token), normPassword)) return sendJson(401, { error: "unauthorized" });
|
|
325
|
+
|
|
326
|
+
const url = new URL(req.url || "/", "https://localhost");
|
|
327
|
+
const op = url.pathname;
|
|
328
|
+
const relPath = url.searchParams.get("path") || "";
|
|
329
|
+
const full = safeResolve(root, relPath);
|
|
330
|
+
if (full === undefined) return sendJson(403, { error: "path escapes root" });
|
|
331
|
+
|
|
332
|
+
if (req.method === "GET" && op === "/list") {
|
|
333
|
+
const wantFolders = url.searchParams.get("folders") === "1";
|
|
334
|
+
let entries: fs.Dirent[];
|
|
335
|
+
try { entries = fs.readdirSync(full, { withFileTypes: true }); }
|
|
336
|
+
catch (e) { return (e as NodeJS.ErrnoException).code === "ENOENT" ? sendJson(200, []) : sendJson(500, { error: (e as Error).message }); }
|
|
337
|
+
return sendJson(200, entries.filter(d => wantFolders ? d.isDirectory() : d.isFile()).map(d => d.name));
|
|
338
|
+
}
|
|
339
|
+
if (req.method === "GET" && op === "/info") {
|
|
340
|
+
let st: fs.Stats;
|
|
341
|
+
try { st = fs.statSync(full); } catch { return sendJson(404, { error: "not found" }); }
|
|
342
|
+
return sendJson(200, { size: st.size, lastModified: st.mtimeMs });
|
|
343
|
+
}
|
|
344
|
+
if (req.method === "GET" && op === "/hasDir") {
|
|
345
|
+
let st: fs.Stats; try { st = fs.statSync(full); } catch { return sendJson(200, { exists: false }); }
|
|
346
|
+
return sendJson(200, { exists: st.isDirectory() });
|
|
347
|
+
}
|
|
348
|
+
if (req.method === "GET" && op === "/read") {
|
|
349
|
+
let st: fs.Stats;
|
|
350
|
+
try { st = fs.statSync(full); } catch { return sendJson(404, { error: "not found" }); }
|
|
351
|
+
let start = parseInt(url.searchParams.get("start") || "0", 10);
|
|
352
|
+
let end = url.searchParams.get("end") != null ? parseInt(url.searchParams.get("end") as string, 10) : st.size;
|
|
353
|
+
start = Math.min(Math.max(start, 0), st.size);
|
|
354
|
+
end = Math.min(Math.max(end, start), st.size);
|
|
355
|
+
recordAccess(clientIp(req), "read", relPath, end - start);
|
|
356
|
+
res.writeHead(200, Object.assign({}, cors, { "Content-Type": "application/octet-stream", "Content-Length": String(end - start) }));
|
|
357
|
+
if (end === start) return res.end();
|
|
358
|
+
fs.createReadStream(full, { start, end: end - 1 }).on("error", () => res.end()).pipe(res);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if ((req.method === "PUT" || req.method === "POST") && (op === "/append" || op === "/set")) {
|
|
362
|
+
const body = await readBody(req);
|
|
363
|
+
fs.mkdirSync(path.dirname(full), { recursive: true });
|
|
364
|
+
if (op === "/append") fs.appendFileSync(full, body);
|
|
365
|
+
else fs.writeFileSync(full, body);
|
|
366
|
+
recordAccess(clientIp(req), "write", relPath, body.length);
|
|
367
|
+
return sendJson(200, { ok: true });
|
|
368
|
+
}
|
|
369
|
+
if (req.method === "DELETE" && op === "/remove") {
|
|
370
|
+
try { fs.unlinkSync(full); } catch (e) { if ((e as NodeJS.ErrnoException).code !== "ENOENT") return sendJson(500, { error: (e as Error).message }); }
|
|
371
|
+
return sendJson(200, { ok: true });
|
|
372
|
+
}
|
|
373
|
+
if (req.method === "DELETE" && op === "/removeDir") {
|
|
374
|
+
try { fs.rmSync(full, { recursive: true, force: true }); } catch (e) { return sendJson(500, { error: (e as Error).message }); }
|
|
375
|
+
return sendJson(200, { ok: true });
|
|
376
|
+
}
|
|
377
|
+
if (req.method === "POST" && op === "/reset") {
|
|
378
|
+
try { for (const name of fs.readdirSync(full)) fs.rmSync(path.join(full, name), { recursive: true, force: true }); }
|
|
379
|
+
catch (e) { if ((e as NodeJS.ErrnoException).code !== "ENOENT") return sendJson(500, { error: (e as Error).message }); }
|
|
380
|
+
return sendJson(200, { ok: true });
|
|
381
|
+
}
|
|
382
|
+
return sendJson(404, { error: "unknown endpoint" });
|
|
383
|
+
} catch (e) {
|
|
384
|
+
try { sendJson(500, { error: String((e as Error)?.message || e) }); } catch { /* response already sent */ }
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
return new Promise<RemoteFileServerHandle>((resolve, reject) => {
|
|
389
|
+
server.on("error", reject);
|
|
390
|
+
server.listen(port, host, () => {
|
|
391
|
+
const addr = server.address();
|
|
392
|
+
const actualPort = typeof addr === "object" && addr ? addr.port : port;
|
|
393
|
+
resolve({
|
|
394
|
+
port: actualPort,
|
|
395
|
+
password,
|
|
396
|
+
url: `https://localhost:${actualPort}`,
|
|
397
|
+
close: () => new Promise<void>(r => { if (flushTimer) clearInterval(flushTimer); server.close(() => r()); }),
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// CLI entry (invoked by bin/filehoster.js or `yarn filehoster <folder>`). Starts the server, logs the
|
|
404
|
+
// password + local/public URLs + batched access, and keeps the UPnP port mapping alive.
|
|
405
|
+
export async function runFileHoster(): Promise<void> {
|
|
406
|
+
const args = process.argv.slice(2);
|
|
407
|
+
const root = args.find(a => !a.startsWith("--") && !a.endsWith(".ts") && !a.endsWith(".js"));
|
|
408
|
+
if (!root) {
|
|
409
|
+
console.error("Usage: yarn filehoster <folder> [--port N] (set a fixed password with PASSWORD=...)");
|
|
410
|
+
process.exit(1);
|
|
411
|
+
}
|
|
412
|
+
const portIdx = args.indexOf("--port");
|
|
413
|
+
const port = portIdx >= 0 ? parseInt(args[portIdx + 1], 10) : 8787;
|
|
414
|
+
|
|
415
|
+
const info = await startRemoteFileServer({ root, port, password: process.env.PASSWORD, logAccess: true });
|
|
416
|
+
let externalIP: string | undefined;
|
|
417
|
+
try { externalIP = (await getExternalIP()).trim(); } catch { /* offline / unreachable */ }
|
|
418
|
+
|
|
419
|
+
console.log("");
|
|
420
|
+
console.log(" Serving: " + path.resolve(root));
|
|
421
|
+
console.log(" Password: " + info.password);
|
|
422
|
+
console.log(" Local: " + info.url);
|
|
423
|
+
if (externalIP) console.log(" Public: https://" + externalIP + ":" + info.port + " (once port-forwarding succeeds)");
|
|
424
|
+
console.log("");
|
|
425
|
+
console.log(" In the app, choose \"Connect to a server\" and enter the URL + password.");
|
|
426
|
+
console.log(" (Self-signed cert: open the URL once in your browser and accept it first.)");
|
|
427
|
+
console.log("");
|
|
428
|
+
|
|
429
|
+
// Keep the UPnP port mapping alive — leases expire ~hourly, so refresh well within that. No-op on
|
|
430
|
+
// Linux (forwardPort returns early there).
|
|
431
|
+
const refresh = async () => {
|
|
432
|
+
try { await forwardPort({ externalPort: info.port, internalPort: info.port }); }
|
|
433
|
+
catch (e) { console.warn(" port forwarding failed:", (e as Error).message); }
|
|
434
|
+
};
|
|
435
|
+
await refresh();
|
|
436
|
+
setInterval(refresh, 30 * 60 * 1000);
|
|
437
|
+
|
|
438
|
+
console.log(" [access] request logging on (batched every 5s)\n");
|
|
439
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
/// <reference types="node" />
|
|
3
|
+
/// <reference types="node" />
|
|
4
|
+
import https from "https";
|
|
5
|
+
import type { FileStorage } from "./FileFolderAPI";
|
|
6
|
+
export type RemoteFileStorageOptions = {
|
|
7
|
+
chunkBytes?: number;
|
|
8
|
+
cacheBytes?: number;
|
|
9
|
+
latencyMs?: number;
|
|
10
|
+
};
|
|
11
|
+
type Connection = {
|
|
12
|
+
url: string;
|
|
13
|
+
password: string;
|
|
14
|
+
latencyMs: number;
|
|
15
|
+
agent: https.Agent | undefined;
|
|
16
|
+
cache: RangeCache;
|
|
17
|
+
stats: {
|
|
18
|
+
requestCount: number;
|
|
19
|
+
bytesFetched: number;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
declare class RangeCache {
|
|
23
|
+
private chunkBytes;
|
|
24
|
+
private budget;
|
|
25
|
+
private chunks;
|
|
26
|
+
private bytes;
|
|
27
|
+
constructor(chunkBytes: number, budget: number);
|
|
28
|
+
private key;
|
|
29
|
+
private peek;
|
|
30
|
+
private store;
|
|
31
|
+
invalidate(path: string): void;
|
|
32
|
+
getRange(conn: Connection, path: string, start: number, end: number): Promise<Buffer | undefined>;
|
|
33
|
+
}
|
|
34
|
+
export type RemoteStorageFactory = ((path: string) => Promise<FileStorage>) & {
|
|
35
|
+
stats: Connection["stats"];
|
|
36
|
+
};
|
|
37
|
+
export declare function getRemoteFileStorage(url: string, password: string, options?: RemoteFileStorageOptions): RemoteStorageFactory;
|
|
38
|
+
export {};
|