// SPDX-License-Identifier: GPL-3.0-only // Copyright (c) 2022, Sylvain Huet, Ambermind // Minimacy (r) System use core.net.ssh;; use core.net.ssh.sftp.common;; const SSH_DEBUG=false;; // permission bits const _S_IFIFO =0x1000;; // FIFO const _S_IFCHR =0x2000;; // Character const _S_IFBLK =0x3000;; // Block const _S_IFDIR =0x4000;; // Directory const _S_IFREG =0x8000;; // Regular : normal file const _S_IFMT =0xF000;; // File type mask const _S_IEXEC =0x0040;; const _S_IWRITE =0x0080;; const _S_IREAD =0x0100;; export sum SftpCallback= fileListS _, handleS _, errorS _ _, fileContentS _, attrS _, okS;; export sum SftpSort= SORT_BY_NAME, SORT_BY_NAME_DESC, SORT_BY_SIZE, SORT_BY_SIZE_DESC, SORT_BY_MODIFICATION_TIME, SORT_BY_MODIFICATION_TIME_DESC, SORT_BY_ACCESS_TIME, SORT_BY_ACCESS_TIME_DESC;; struct Sftp=SSH + [ channelS, fNotifyEventS, countSF, cbSF, homeSF, offsetSF, pendingDataSF, sendingDataSF, sendingOffsetSF ];; struct SftpFile=[ shortF, longF, sizeF, uidF, gidF, permissionsF, atimeF, mtimeF, extendedF ];; fun sftpOnEvent(h, fNotify)= set h.fNotifyEventS=(lambda(code, data) = call fNotify(h, code, data);0);; fun sftpNotifyEvent(h, code, data)= if SSH_DEBUG then echoLn strFormat("> sftpNotifyEvent code *", code); call h.fNotifyEventS(code, data);; fun _nextCount(h)= set h.countSF=h.countSF+1;; fun _getAndClearCb(h)= let h.cbSF -> cb in ( set h.cbSF=nil; cb );; fun ___sftpParseExtended(words, result, len)= if words==nil then [len, listReverse(result)] else let words->(type::data::next) in ___sftpParseExtended(next, [type, data]::result, len+8+strLength(type)+strLength(data));; fun _sftpParseExtended(data, i, count)= let sshParseVals(data, i, count*2) -> words in ___sftpParseExtended(words, nil, i);; fun _sftpParseAttrs(f, data, i)= let strRead32Msb(data, i) -> mask in let i+4->i in ( if 0<> (mask&SSH_FILEXFER_ATTR_SIZE) then ( set f.sizeF= (strRead32Msb(data, i)<<32)+strRead32Msb(data, i+4); set i=i+8 ); if 0<> (mask&SSH_FILEXFER_ATTR_UIDGID) then ( set f.uidF= strRead32Msb(data, i); set f.gidF= strRead32Msb(data, i+4); set i=i+8 ); if 0<> (mask&SSH_FILEXFER_ATTR_PERMISSIONS) then ( set f.permissionsF= strRead32Msb(data, i); set i=i+4 ); if 0<> (mask&SSH_FILEXFER_ATTR_ACMODTIME) then ( set f.atimeF= strRead32Msb(data, i); set f.mtimeF= strRead32Msb(data, i+4); set i=i+8 ); if 0<> (mask&SSH_FILEXFER_ATTR_EXTENDED) then let strRead32Msb(data, i) -> count in let _sftpParseExtended(data, i+4, count*2) -> [iNext, extended] in ( set f.extendedF=extended; set i=iNext ); i );; fun _sftpParseName(data, i, count)= if count>0 then let sshParseVals(data, i, 2) -> (short::long::_) in let [shortF=short, longF=long] -> file in let i+4+strLength(short)+4+strLength(long) -> i in let _sftpParseAttrs(file, data, i) -> i in file::_sftpParseName(data, i, count-1);; //-------------------------------- fun sftpSend(h, code, data, cb)= set h.cbSF=(lambda(arg)=call cb(arg);0); sshSendChannelData(h.channelS, sshMsgStr(strBuild([strInt8(code), sshMsgInt(_nextCount(h)), data]))); nil;; fun sftpParseVersion(h, data)= if SSH_DEBUG then echoLn "sftp> Version"; let strRead32Msb(data, 1) -> version in let _sftpParseExtended(data, 5, 100) -> [i, options] in ( if SSH_DEBUG then echoLn ["version ",version]; if SSH_DEBUG then echoLn ["options ", strJoin(",", options)]; sftpSend(h, SSH_FXP_REALPATH, sshMsgStr("."), lambda(arg)= match arg with fileListS fileList-> ( set h.homeSF=(head(fileList)).shortF; sshNotifyEvent(h, SSH_READY, nil) ), errorS code msg -> sshNotifyEvent(h, SSH_ERROR, msg) ) );; fun sftpParseName(h, data)= if SSH_DEBUG then echoLn "sftp> Name"; let strRead32Msb(data, 1) -> id in let strRead32Msb(data, 5) -> count in let _sftpParseName(data, 9, count) -> listNames in call _getAndClearCb(h)(fileListS listNames);; fun sftpParseAttrs(h, data)= if SSH_DEBUG then echoLn "sftp> Attrs"; let strRead32Msb(data, 1) -> id in let [SftpFile] -> attrs in ( _sftpParseAttrs(attrs, data, 5); call _getAndClearCb(h)(attrS attrs) );; fun sftpParseHandle(h, data)= if SSH_DEBUG then echoLn "sftp> Handle"; let strRead32Msb(data, 1) -> id in let sshParseVals(data, 5, 1) -> (handle::_) in call _getAndClearCb(h)(handleS handle);; fun sftpParseStatus(h, data)= if SSH_DEBUG then echoLn "sftp> Status"; let strRead32Msb(data, 1) -> id in let strRead32Msb(data, 5) -> code in let sshParseVals(data, 9, 2) -> (message::lang::_) in call _getAndClearCb(h)(if code==SSH_FX_OK then okS else errorS code message);; fun sftpParseData(h, data)= if SSH_DEBUG then echoLn "sftp> Data"; let strRead32Msb(data, 1) -> id in let sshParseVals(data, 5, 1) -> (content::_) in call _getAndClearCb(h)(fileContentS content);; fun _sftpParseChannelData(h, data)= if SSH_DEBUG then echoLn ["_sftpParseChannelData Ready code=", strGet(data, 0)]; let strGet(data, 0) -> code in match code with SSH_FXP_DATA -> sftpParseData(h, data), SSH_FXP_VERSION -> sftpParseVersion(h, data), SSH_FXP_NAME -> sftpParseName(h, data), SSH_FXP_HANDLE -> sftpParseHandle(h, data), SSH_FXP_STATUS -> sftpParseStatus(h, data), SSH_FXP_ATTRS -> sftpParseAttrs(h, data), _ -> (echoLn ["unknown code ", code];nil);; fun _sftpProcessFrames(h)= let strRead32Msb(h.pendingDataSF, 0) -> frameSize in if (frameSize+4)<= strLength(h.pendingDataSF) then let strSlice(h.pendingDataSF, 4, frameSize) -> data in ( set h.pendingDataSF=strSlice(h.pendingDataSF, 4+frameSize, nil); _sftpParseChannelData(h, data); _sftpProcessFrames(h) );; fun sftpOpenSubsystem(h) = sshSendChannelRequest(h.channelS, "subsystem", true, sshMsgStr("sftp")); sshOnChannelEvent(h.channelS, lambda(code, data)= if code==SSH_OK then sshSendChannelData(h.channelS, sshMsgStr(strBuild([strInt8(SSH_FXP_INIT), sshMsgInt(SFTP_VERSION)]))) else if code==SSH_DATA then ( set h.pendingDataSF=strConcat(h.pendingDataSF, data); _sftpProcessFrames(h) ) else sftpNotifyEvent(h, code, data) );; fun sftpFtpStartSession(h)= set h.channelS=sshSendChannelOpen(h, "session", 0); sshOnChannelEvent(h.channelS, lambda(code, data)= if code==SSH_OK then void sftpOpenSubsystem(h) else sftpNotifyEvent(h, code, data) );; fun _sftpMakeCb(cb)= (lambda(arg)=call cb(arg);nil);; fun _sftpSimpleRequest(h, code, data, cb)= let _sftpMakeCb(cb) ->cb in sftpSend(h, code, data, cb);; // ------------------------ ASYNC API export fun sftpConnectAsync(host, port, login, auth, fCheckPublickKey, fNotify)= let [Sftp] -> h in ( sftpOnEvent(h, fNotify); sshConnect(h, host, port, login, auth, fCheckPublickKey, (lambda(code, data)= if code==SSH_OK then void sftpFtpStartSession(h) else sftpNotifyEvent(h, code, data) )); h );; fun _sftpDir(h, handle, cb, result)= sftpSend(h, SSH_FXP_READDIR, sshMsgStr(handle), (lambda(arg)= match arg with fileListS fileList-> return _sftpDir(h, handle, cb, listConcat(fileList, result)), errorS code msg -> if code==SSH_FX_EOF then return sftpSend(h, SSH_FXP_CLOSE, sshMsgStr(handle), (lambda(arg)= call cb(if arg==okS then fileListS result else arg) )); call cb(arg); ));; export fun sftpDirAsync(h, path, cb)= let _sftpMakeCb(cb) ->cb in sftpSend(h, SSH_FXP_OPENDIR, sshMsgStr(path), (lambda(arg)= match arg with handleS handle-> return _sftpDir(h, handle, cb, nil); call cb(arg) ));; export fun sftpMkdirAsync(h, path, attr, cb)= let if attr==nil then {sshMsgInt(SSH_FILEXFER_ATTR_PERMISSIONS), sshMsgInt(0x1ff)} else attr->attr in _sftpSimpleRequest(h, SSH_FXP_MKDIR, [ sshMsgStr(path), attr ], cb);; export fun sftpRmdirAsync(h, path, cb)= _sftpSimpleRequest(h, SSH_FXP_RMDIR, [ sshMsgStr(path) ], cb);; fun _sftpGet(h, handle, cb, result)= sftpSend(h, SSH_FXP_READ, {sshMsgStr(handle), sshMsgInt64(0, h.offsetSF), sshMsgInt(0x8000)}, (lambda(arg)= match arg with fileContentS content-> ( set h.offsetSF=h.offsetSF+strLength(content); _sftpGet(h, handle, cb, content::result); return nil ), errorS code msg -> if code==SSH_FX_EOF then return sftpSend(h, SSH_FXP_CLOSE, sshMsgStr(handle), (lambda(arg)= call cb(if arg==okS then (fileContentS strListConcat(listReverse(result))) else arg) )); call cb(arg) ));; export fun sftpGetAsync(h, path, cb)= set h.offsetSF=0; let _sftpMakeCb(cb) ->cb in sftpSend(h, SSH_FXP_OPEN, {sshMsgStr(path), sshMsgInt(SSH_FXF_READ), sshMsgInt(0)},lambda(arg)= match arg with handleS handle-> _sftpGet(h, handle, cb, nil), _ -> call cb(arg) );; export fun sftpStatAsync(h, path, cb)= let _sftpMakeCb(cb) ->cb in sftpSend(h, SSH_FXP_STAT, sshMsgStr(path), cb);; export fun sftpLStatAsync(h, path, cb)= let _sftpMakeCb(cb) ->cb in sftpSend(h, SSH_FXP_LSTAT, sshMsgStr(path), cb);; fun _sftpPut(h, handle, cb)= if h.sendingOffsetSF>= strLength(h.sendingDataSF) then sftpSend(h, SSH_FXP_CLOSE, sshMsgStr(handle), cb) else let strSlice(h.sendingDataSF, h.sendingOffsetSF, sshChannelPeerPacketMaxSize(h.channelS)) -> content in sftpSend(h, SSH_FXP_WRITE, {sshMsgStr(handle), sshMsgInt64(0, h.sendingOffsetSF), sshMsgStr(content)}, (lambda(arg)= set h.sendingOffsetSF=h.sendingOffsetSF+strLength(content); if arg==okS then _sftpPut(h, handle, cb) else call cb(arg) ));; export fun sftpPutAsync(h, path, content, attr, cb)= let if attr==nil then {sshMsgInt(SSH_FILEXFER_ATTR_PERMISSIONS), sshMsgInt(0644)} else attr->attr in let _sftpMakeCb(cb) ->cb in sftpSend(h, SSH_FXP_OPEN, [ sshMsgStr(path), sshMsgInt(SSH_FXF_WRITE|SSH_FXF_CREAT|SSH_FXF_TRUNC) , attr ], lambda(arg)= match arg with handleS handle-> ( set h.sendingDataSF=content; set h.sendingOffsetSF=0; _sftpPut(h, handle, cb) ), _ -> call cb(arg) );; export fun sftpSetStatAsync(h, path, attr, cb)= _sftpSimpleRequest(h, SSH_FXP_SETSTAT, [ sshMsgStr(path), attr ], cb);; export fun sftpRenameAsync(h, oldPath, newPath, cb)= _sftpSimpleRequest(h, SSH_FXP_RENAME, [ sshMsgStr(oldPath), sshMsgStr(newPath) ], cb);; export fun sftpRemoveAsync(h, path, cb)= _sftpSimpleRequest(h, SSH_FXP_REMOVE, [ sshMsgStr(path) ], cb);; //--------------------- SYNC API export fun sftpIsDir(permission)= (permission & _S_IFMT)==_S_IFDIR;; export fun sftpEchoDir(fileList)= for f in fileList do echoLn f.longF; fileList;; export fun sftpAttrFromPermission(permission)= {sshMsgInt(SSH_FILEXFER_ATTR_PERMISSIONS), sshMsgInt(permission)};; export fun sftpHome(h)= h.homeSF;; export fun sftpSort(sort, fileList) = quicksort(fileList, match sort with SORT_BY_SIZE -> (lambda(a, b)= a.sizeF (lambda(a, b)= a.sizeF>b.sizeF), SORT_BY_MODIFICATION_TIME -> (lambda(a, b)= a.mtimeF (lambda(a, b)= a.mtimeF>b.mtimeF), SORT_BY_ACCESS_TIME -> (lambda(a, b)= a.atimeF (lambda(a, b)= a.atimeF>b.atimeF), SORT_BY_NAME_DESC -> (lambda(a, b)= 0 lambda(a, b)= 0>strCmp(a.shortF, b.shortF));; export fun sftpConnect(host, port, login, auth, fCheckPublickKey)= await(lambda(join)=sftpConnectAsync(host, port, login, auth, fCheckPublickKey, lambda(h, code, data)= joinSend(join, if code==SSH_READY then h)));; export fun sftpDir(h, path)= await(lambda(join)=sftpDirAsync(h, path, lambda(arg)= joinSend(join, match arg with fileListS fileList -> fileList)));; export fun sftpMkdir(h, path, attr)= await(lambda(join)=sftpMkdirAsync(h, path, attr, lambda(arg)= joinSend(join, match arg with okS -> true)));; export fun sftpRmdir(h, path)= await(lambda(join)=sftpRmdirAsync(h, path, lambda(arg)= joinSend(join, match arg with okS -> true)));; export fun sftpGet(h, path)= await(lambda(join)=sftpGetAsync(h, path, lambda(arg)= joinSend(join, match arg with fileContentS data -> data)));; export fun sftpStat(h, path)= await(lambda(join)=sftpStatAsync(h, path, lambda(arg)= joinSend(join, match arg with attrS attr -> attr)));; export fun sftpLStat(h, path)= await(lambda(join)=sftpLStatAsync(h, path, lambda(arg)= joinSend(join, match arg with attrS attr -> attr)));; export fun sftpPut(h, path, content, attr)= await(lambda(join)=sftpPutAsync(h, path, content, attr, lambda(arg)= joinSend(join, match arg with okS -> true)));; export fun sftpSetStat(h, path, attr)= await(lambda(join)=sftpSetStatAsync(h, path, attr, lambda(arg)= joinSend(join, match arg with okS -> true)));; export fun sftpRename(h, oldPath, newPath)= await(lambda(join)=sftpRenameAsync(h, oldPath, newPath, lambda(arg)= joinSend(join, match arg with okS -> true)));; export fun sftpRemove(h, path)= await(lambda(join)=sftpRemoveAsync(h, path, lambda(arg)= joinSend(join, match arg with okS -> true)));; fun _sftpMkPath(h, www, uri, i)= let strPos(uri, "/", i) -> i in if i<>nil then ( sftpMkdir(h, strConcat(www, strLeft(uri, i)), nil); _sftpMkPath(h, www, uri, i+1) );; // make sure that the remote path of www/uri exists, assuming that www already exists fun sftpMkPath(h, www, uri)= _sftpMkPath(h, www, strFromUrl(uri), 1);; // 1 -> skip the initial '/' of the uri export fun sftpWWWSend(host, port, login, auth, fCheckPublickKey, www, uri, content)= let sftpConnect(host, port, login, auth, fCheckPublickKey) -> h in if h<>nil then ( sftpMkPath(h, www, uri); sftpPut(h, strConcat(www, uri), content, nil); sshClose(h); );;