-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathcli.mjs
More file actions
7 lines (7 loc) · 9.4 KB
/
Copy pathcli.mjs
File metadata and controls
7 lines (7 loc) · 9.4 KB
1
2
3
4
5
6
7
#!/usr/bin/env node
import H from"ws";var io=4e3;function g(o,n,e,c,t){let l=`${o.replace(/^http/,"ws")}/ws`,s=new H(l),m=f=>s.readyState===H.OPEN&&s.send(JSON.stringify(f)),u=(f,p)=>m({type:"signal",to:f,data:p});return s.on("open",()=>m({type:"join",code:n,name:c,apiKey:e})),s.on("close",()=>t.onClose?.()),s.on("error",f=>t.onWsError?.(f)??console.error("connection error:",f.message)),s.on("message",f=>{let p=JSON.parse(f.toString());if(p.type==="error")return t.onFail?.(p.reason)??(console.error("signal error:",p.reason),process.exit(1));if(p.type==="welcome")return t.onWelcome?.(p);if(p.type==="roster")return t.onRoster?.(p);if(p.type==="room")return t.onRoom?.(p.from,p.name,p.data);if(p.type==="signal")return t.onSignal?.(p.from,p.data)}),{ws:s,post:m,signal:u}}function x(o,n,e,c){return new Promise(t=>{let l=new Map,{ws:s}=g(o,n,e,c,{onRoom(m,u,f){let p=JSON.parse(f);if(p.t==="list")for(let w of p.files)l.set(w.id,{...w,ownerId:m,ownerName:u||m})}});setTimeout(()=>{s.close(),t([...l.values()])},io)})}var v=process.env.SFT_HOST||"https://samfiletransfer.com",J={"password-required":"this pool has a password","bad-password":"wrong password",invalid:"invalid or stale poolKey \u2014 copy a fresh one from the pool page (the key may have been rotated)",expired:"this pool has expired","bad-request":"invalid request"};async function k(o,n){let e=await fetch(`${v}/api/pool/resolve`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(n?{token:o,password:n}:{token:o})}).catch(()=>null),c=await e?.json().catch(()=>null);return{ok:!!e?.ok,body:c}}function M(o){return new Promise(n=>{process.stdout.write(o);let e=process.stdin,c=e.isRaw,t="";e.resume(),e.isTTY&&e.setRawMode(!0);let l=()=>{e.removeListener("data",s),e.isTTY&&e.setRawMode(!!c),e.pause()},s=m=>{let u=m.toString("utf8");if(u===`
`||u==="\r"||u==="")return l(),process.stdout.write(`
`),n(t);u===""&&(l(),process.exit(130)),u==="\x7F"||u==="\b"?t=t.slice(0,-1):t+=u};e.on("data",s)})}import{readFileSync as ao,writeFileSync as co,unlinkSync as lo,chmodSync as fo}from"node:fs";import{join as po}from"node:path";import{homedir as mo}from"node:os";var R=po(mo(),".sft-pool-session.json");function uo(){try{return JSON.parse(ao(R,"utf8"))}catch{return null}}function S(){let o=uo();return o||(console.error("not connected yet: samfiletransfer connect <poolKey>"),process.exit(1)),o}function L(o){co(R,JSON.stringify(o),{mode:384}),fo(R,384)}function j(){try{return lo(R),!0}catch{return!1}}function wo(){console.error("usage: samfiletransfer connect <poolKey> [name]")}async function F(o,n){if(!o)return wo(),process.exit(1);let{ok:e,body:c}=await k(o);if(!e&&c?.error==="password-required"){let s=await M("this pool is password-protected, enter password: ");({ok:e,body:c}=await k(o,s))}if(!e)return console.error(J[c?.error]??"could not connect"),process.exit(1);let{code:t,apiKey:l}=c;await new Promise((s,m)=>{let{ws:u}=g(v,t,l,n||"cli",{onWelcome(f){console.log(`connected: ${v} ${t} (${f.peers.length} people in pool)`),u.close(),s()},onFail(f){m(new Error(f))},onWsError(f){m(f)}});setTimeout(()=>m(new Error("timed out")),1e4)}),L({host:v,code:t,apiKey:l,name:n||"cli"}),console.log("pool info saved \u2014 list / upload / download / downloadall now work.")}async function W(){console.log(j()?"pool connection info removed.":"not connected.")}import{statSync as yo}from"node:fs";function b(o){let n=["B","KB","MB","GB","TB"],e=0;for(;o>=1024&&e<n.length-1;)o/=1024,e++;return`${o.toFixed(e?1:0)} ${n[e]}`}var z=o=>new Promise(n=>setTimeout(n,o));function V(o){try{return yo(o)}catch{return null}}async function q(){let{host:o,code:n,apiKey:e,name:c}=S(),t=await x(o,n,e,c);if(!t.length)return console.log("no files shared in this pool yet (or nobody announced any).");for(let l of t)console.log(`${l.id} ${b(l.size).padEnd(8)} ${l.name} (${l.ownerName})`)}import{readFileSync as go}from"node:fs";import{basename as So}from"node:path";import{randomUUID as ho}from"node:crypto";import{RTCPeerConnection as xo}from"werift";import{webcrypto as $}from"node:crypto";var _=new TextEncoder,E=12,U=16*1024,T=4;async function D(o){let n=await $.subtle.importKey("raw",_.encode(o),"HKDF",!1,["deriveKey"]);return $.subtle.deriveKey({name:"HKDF",hash:"SHA-256",salt:_.encode("samfiletransfer/pool/v1"),info:_.encode("file-chunks")},n,{name:"AES-GCM",length:256},!1,["encrypt","decrypt"])}async function G(o,n){let e=$.getRandomValues(new Uint8Array(E)),c=await $.subtle.encrypt({name:"AES-GCM",iv:e},o,n),t=new Uint8Array(E+c.byteLength);return t.set(e),t.set(new Uint8Array(c),E),t.buffer}async function I(o,n){let e=n.subarray(0,E),c=n.subarray(E);return Buffer.from(await $.subtle.decrypt({name:"AES-GCM",iv:e},o,c))}function K(o){let n=2166136261;for(let e=0;e<o.length;e++)n^=o.charCodeAt(e),n=Math.imul(n,16777619);return n>>>0}function Y(o,n){let e=new Uint8Array(T+n.byteLength);return new DataView(e.buffer).setUint32(0,o),e.set(new Uint8Array(n),T),e}var vo=[{urls:"stun:stun.l.google.com:19302"}],bo=1<<20;async function Q(o){if(!o)return console.error("usage: samfiletransfer upload <file>"),process.exit(1);let{host:n,code:e,apiKey:c,name:t}=S(),l=go(o),s={id:ho(),name:So(o),size:l.length,type:"application/octet-stream"},m=await D(e),u=()=>JSON.stringify({t:"list",files:[s],have:[]}),f=new Map;function p(r,i,a){a.binaryType="arraybuffer",i.ch=a,a.onopen=()=>i.waiters.splice(0).forEach(d=>d(a)),a.onclose=()=>f.delete(r)}function w(r,i){let a=f.get(r);if(a)return a;let d=new xo({iceServers:vo});return a={pc:d,ch:null,waiters:[]},f.set(r,a),d.onicecandidate=y=>y.candidate&&i(r,{ice:y.candidate}),p(r,a,d.createDataChannel("pool",{ordered:!0})),(async()=>(await d.setLocalDescription(await d.createOffer()),i(r,{sdp:d.localDescription})))(),a}function C(r){return r.ch?.readyState==="open"?Promise.resolve(r.ch):new Promise(i=>{r.waiters.push(i),setTimeout(()=>i(r.ch?.readyState==="open"?r.ch:null),2e4)})}async function N(r){for(;(r.bufferedAmount??0)>bo;)await z(100)}async function A(r,i,a){console.log(`request: ${r} <- ${s.name} (from byte ${i})`);let d=await C(w(r,a));if(!d)return console.error("channel did not open:",r);d.send(JSON.stringify({t:"meta",file:s}));let y=K(s.id);for(let O=i;O<s.size;O+=U){let ro=l.subarray(O,O+U),so=await G(m,ro);await N(d),d.send(Buffer.from(Y(y,so)))}d.send(JSON.stringify({t:"eof",id:s.id})),console.log(`done: ${r}`)}let{post:h,signal:P}=g(n,e,c,t,{onWelcome(r){console.log(`joined: ${r.id}, ${r.peers.length} people in pool, file: ${s.name} (${s.size} bytes)`);for(let i of r.peers)h({type:"room",to:i.id,data:u()})},onRoster(r){for(let i of r.joined??[])h({type:"room",to:i.id,data:u()})},onRoom(r,i,a){let d=JSON.parse(a);d.t==="get"&&d.id===s.id&&A(r,d.from??0,P)},async onSignal(r,i){let a=f.get(r);a&&(i.sdp?await a.pc.setRemoteDescription(i.sdp):i.ice&&await a.pc.addIceCandidate(i.ice).catch(()=>{}))},onClose(){console.log("connection closed")}});console.log("Press Ctrl+C to stop.")}import{createWriteStream as Eo,mkdirSync as X}from"node:fs";import{dirname as $o,join as Z}from"node:path";import{RTCPeerConnection as Co}from"werift";var Oo=[{urls:"stun:stun.l.google.com:19302"}];async function oo(o,n,e,c,t,l){X($o(l),{recursive:!0});let s=Eo(l),m=await D(n),u=K(t.id);return new Promise((f,p)=>{let w=null,C=0;function N(r){r.binaryType="arraybuffer",r.onmessage=async i=>{if(typeof i.data=="string"){let y=JSON.parse(i.data);y.t==="eof"&&y.id===t.id&&(s.end(),A.close(),f(l));return}let a=Buffer.from(i.data);if(a.readUInt32BE(0)!==u)return;let d=await I(m,a.subarray(T));C+=d.byteLength,s.write(d),process.stdout.write(`\r${t.name}: ${b(C)} / ${b(t.size)}`)}}let{ws:A,signal:h,post:P}=g(o,n,e,c,{onWelcome(){P({type:"room",to:t.ownerId,data:JSON.stringify({t:"get",id:t.id,from:0})})},async onSignal(r,i){if(w||(w=new Co({iceServers:Oo}),w.onicecandidate=a=>a.candidate&&h(t.ownerId,{ice:a.candidate}),w.ondatachannel=a=>N(a.channel)),i.sdp){if(await w.setRemoteDescription(i.sdp),i.sdp.type==="offer"){let a=await w.createAnswer();await w.setLocalDescription(a),h(r,{sdp:w.localDescription})}}else i.ice&&await w.addIceCandidate(i.ice).catch(()=>{})},onRoom(r,i,a){let d=JSON.parse(a);d.t==="deny"&&d.id===t.id&&p(new Error(`denied: ${d.reason}`))}})})}async function eo(o,n="."){if(!o)return B(n);let{host:e,code:c,apiKey:t,name:l}=S(),m=(await x(e,c,t,l)).filter(w=>w.id===o||w.name===o);if(!m.length)return console.error(`not found: ${o}`),process.exit(1);if(m.length>1){console.error("multiple matches, specify the file id instead:");for(let w of m)console.error(` ${w.id} ${w.name} (${w.ownerName})`);return process.exit(1)}let u=m[0],f=V(n)?.isDirectory()?Z(n,u.name):n,p=await oo(e,c,t,l,u,f);console.log(`
saved: ${p}`)}async function B(o="."){let{host:n,code:e,apiKey:c,name:t}=S(),l=await x(n,e,c,t);if(!l.length)return console.log("nothing to download.");X(o,{recursive:!0});for(let s of l){console.log(`
downloading: ${s.name}`);let m=await oo(n,e,c,t,s,Z(o,s.name)).catch(u=>(console.error("error:",u.message),null));m&&console.log(`
saved: ${m}`)}}function Ro(){console.error("usage:"),console.error(" samfiletransfer connect <poolKey> [name]"),console.error(" samfiletransfer list"),console.error(" samfiletransfer upload <file>"),console.error(" samfiletransfer download [name-or-id] [dest]"),console.error(" samfiletransfer downloadall [dest]"),console.error(" samfiletransfer exit"),console.error(`poolKey: shown in the pool page's "Upload from terminal" box.`),process.exit(1)}var no={connect:F,list:q,upload:Q,download:eo,downloadall:B,exit:W},[,,to,...To]=process.argv;no[to]||Ro();await no[to](...To);