Read-only SFTP server
Serves sample files over SFTP and rejects write operations.
Port 30023Auth: none
Connect
Browse
sftp -P 30023 demo@modernssh-examples.manaf.chDownload
sftp -P 30023 demo@modernssh-examples.manaf.ch:/hello.txt ./hello.txtMinimal server
View complete containerimport { Server, SessionChannel, SFTPOpenFlags, SFTPStatusCode } from "@bunkerch/modernssh"
const hostKey = process.env.SSH_HOST_KEY_PRIVATE_KEY
if (!hostKey) throw new Error("SSH_HOST_KEY_PRIVATE_KEY is required")
const contents = Buffer.from("Hello from modernssh!\n")
const fileHandle = Buffer.from("hello.txt")
const directoryHandle = Buffer.from("root")
const fileAttributes = { size: BigInt(contents.length), permissions: 0o100444 }
const directoryAttributes = { size: 0n, permissions: 0o040555 }
const server = new Server({ hostKeys: [hostKey] })
server.hooker.hook("noneAuthentication", (_hook, _context, decision) => {
decision.allowLogin = true
})
server.hooker.hook("channelOpenRequest", (_hook, channel, decision) => {
decision.allowOpen = channel instanceof SessionChannel
})
server.on("connection", (connection) => {
connection.on("channel", (channel) => {
if (!(channel instanceof SessionChannel)) return
channel.hooker.hook("subsystemRequest", (_hook, context, decision) => {
decision.success = context.subsystem === "sftp"
})
channel.events.on("sftp", (sftp) => {
let directoryWasRead = false
sftp.hooker.hook("REALPATH", async (_hook, request) => {
await sftp.name(request.requestId, {
filename: Buffer.from("/"), longname: Buffer.from("/"), attributes: directoryAttributes,
})
})
sftp.hooker.hook("STAT", async (_hook, request) => {
const path = request.path.toString()
await sftp.attributes(request.requestId, path === "/hello.txt" ? fileAttributes : directoryAttributes)
})
sftp.hooker.hook("LSTAT", async (_hook, request) => {
const path = request.path.toString()
await sftp.attributes(request.requestId, path === "/hello.txt" ? fileAttributes : directoryAttributes)
})
sftp.hooker.hook("OPENDIR", async (_hook, request) => {
directoryWasRead = false
await sftp.handle(request.requestId, directoryHandle)
})
sftp.hooker.hook("READDIR", async (_hook, request) => {
if (directoryWasRead) {
await sftp.status(request.requestId, SFTPStatusCode.EOF)
return
}
directoryWasRead = true
await sftp.name(request.requestId, {
filename: Buffer.from("hello.txt"),
longname: Buffer.from("hello.txt"),
attributes: fileAttributes,
})
})
sftp.hooker.hook("OPEN", async (_hook, request) => {
if (request.filename.toString() !== "/hello.txt" || request.flags !== SFTPOpenFlags.Read) {
await sftp.status(request.requestId, SFTPStatusCode.PermissionDenied)
return
}
await sftp.handle(request.requestId, fileHandle)
})
sftp.hooker.hook("READ", async (_hook, request) => {
const start = Number(request.offset)
if (start >= contents.length) {
await sftp.status(request.requestId, SFTPStatusCode.EOF)
return
}
await sftp.data(request.requestId, contents.subarray(start, start + request.length))
})
sftp.hooker.hook("CLOSE", async (_hook, request) => {
await sftp.status(request.requestId, SFTPStatusCode.Ok)
})
sftp.hooker.hook("request", async (_hook, request) => {
await sftp.status(request.requestId, SFTPStatusCode.PermissionDenied, "Read-only server")
})
})
})
})
server.listen({ host: "0.0.0.0", port: Number(process.env.PORT ?? 2223) })