diff --git a/Package.swift b/Package.swift index b32ae1f5..31febdc7 100644 --- a/Package.swift +++ b/Package.swift @@ -64,6 +64,14 @@ var targets: [PackageDescription.Target] = [ .product(name: "NIOEmbedded", package: "swift-nio"), .product(name: "NIOHTTP1", package: "swift-nio"), ]), + .executableTarget( + name: "NIOExtrasNFS3Demo", + dependencies: [ + "NIONFS3", + "NIOExtras", + .product(name: "NIO", package: "swift-nio"), + .product(name: "Logging", package: "swift-log"), + ]), .target( name: "NIOSOCKS", dependencies: [ @@ -168,6 +176,7 @@ let package = Package( .package(url: "https://github.com/apple/swift-nio-http2.git", from: "1.27.0"), .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), .package(url: "https://github.com/apple/swift-http-types", from: "1.0.0"), + .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"), ], targets: targets ) diff --git a/Sources/NIOExtrasNFS3Demo/CloseOnErrorHandler.swift b/Sources/NIOExtrasNFS3Demo/CloseOnErrorHandler.swift new file mode 100644 index 00000000..6fa147a2 --- /dev/null +++ b/Sources/NIOExtrasNFS3Demo/CloseOnErrorHandler.swift @@ -0,0 +1,31 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-2023 Apple Inc. and the SwiftNIO project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftNIO project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Logging +import NIOCore + +final class CloseOnErrorHandler: ChannelInboundHandler { + typealias InboundIn = Never + + private let logger: Logger + + init(logger: Logger) { + self.logger = logger + } + + func errorCaught(context: ChannelHandlerContext, error: Error) { + self.logger.warning("encountered error, closing NFS connection", metadata: ["error": "\(error)"]) + context.close(promise: nil) + } +} diff --git a/Sources/NIOExtrasNFS3Demo/DummyFS.swift b/Sources/NIOExtrasNFS3Demo/DummyFS.swift new file mode 100644 index 00000000..a0ef1ac6 --- /dev/null +++ b/Sources/NIOExtrasNFS3Demo/DummyFS.swift @@ -0,0 +1,302 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-2023 Apple Inc. and the SwiftNIO project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftNIO project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import NIOCore +import NIONFS3 +import Logging + +final class DummyFS: NFS3FileSystemNoAuth { + struct ChildEntry { + var name: String + var index: Int + } + + struct InodeEntry { + var type: NFS3FileType + var children: [ChildEntry] + } + + private var files: [InodeEntry] = [] + private var root: Int = 7 + private let fileContent: ByteBuffer = { + var buffer = ByteBuffer(repeating: UInt8(ascii: "E"), count: 1 * 1024 * 1024) + buffer.setInteger(UInt8(ascii: "H"), at: 0) + buffer.setInteger(UInt8(ascii: "L"), at: buffer.writerIndex - 3) + buffer.setInteger(UInt8(ascii: "L"), at: buffer.writerIndex - 2) + buffer.setInteger(UInt8(ascii: "O"), at: buffer.writerIndex - 1) + return buffer + }() + + init() { + // 0 doesn't exist? + self.files.append(.init(type: .regular, children: [])) + + let idDirFileA = self.files.count + self.files.append(.init(type: .regular, children: [])) + + let idDirFileB = self.files.count + self.files.append(.init(type: .regular, children: [])) + + let idDirFileC = self.files.count + self.files.append(.init(type: .regular, children: [])) + + let idDirFileD = self.files.count + self.files.append(.init(type: .regular, children: [])) + + let idDirFileE = self.files.count + self.files.append(.init(type: .regular, children: [])) + + let idDirFileF = self.files.count + self.files.append(.init(type: .regular, children: [])) + + let idDir = self.files.count + self.files.append( + .init( + type: .directory, + children: [ + .init(name: ".", index: idDir), + .init(name: "file", index: idDirFileA), + .init(name: "file1", index: idDirFileB), + .init(name: "file2", index: idDirFileC), + .init(name: "file3", index: idDirFileD), + .init(name: "file4", index: idDirFileE), + .init(name: "file5", index: idDirFileF), + ])) + + let idRoot = self.files.count + self.files.append( + .init( + type: .directory, + children: [ + .init(name: ".", index: idRoot), + .init(name: "dir", index: idDir), + ])) + + self.files[idDir].children.append(.init(name: "..", index: idRoot)) + self.files[idRoot].children.append(.init(name: "..", index: idRoot)) + + self.root = idRoot + } + + func mount(_ call: MountCallMount, logger: Logger, promise: EventLoopPromise) { + promise.succeed(.init(result: .okay(.init(fileHandle: NFS3FileHandle(UInt64(self.root)))))) + } + + func unmount(_ call: MountCallUnmount, logger: Logger, promise: EventLoopPromise) { + promise.succeed(.init()) + } + + func getattr(_ call: NFS3CallGetAttr, logger: Logger, promise: EventLoopPromise) { + if let result = self.getFile(call.fileHandle) { + promise.succeed(.init(result: .okay(.init(attributes: result)))) + } else { + promise.succeed(.init(result: .fail(.errorBADHANDLE, NFS3Nothing()))) + } + } + + func lookup(fileName: String, inDirectory dirHandle: NFS3FileHandle) -> (NFS3FileHandle, NFS3FileAttr)? { + guard let dirEntry = self.getEntry(fileHandle: dirHandle) else { + return nil + } + + guard let index = self.files[dirEntry.0].children.first(where: { $0.name == fileName })?.index else { + return nil + } + let fileHandle = NFS3FileHandle(UInt64(index)) + + return (fileHandle, self.getFile(fileHandle)!) + } + + func getEntry(index: Int) -> InodeEntry? { + guard index >= 0 && index < self.files.count else { + return nil + } + return self.files[index] + } + + func getEntry(fileHandle: NFS3FileHandle) -> (Int, InodeEntry)? { + return UInt64(fileHandle).flatMap { + Int(exactly: $0) + }.flatMap { index in + self.getEntry(index: index).map { + (index, $0) + } + } + } + + func getFile(_ fileHandle: NFS3FileHandle) -> NFS3FileAttr? { + guard let entry = self.getEntry(fileHandle: fileHandle) else { + return nil + } + + return .init( + type: entry.1.type, + mode: 0o777, + nlink: 1, + uid: 1, + gid: 1, + size: NFS3Size(rawValue: 1 * 1024 * 1024), + used: 1, + rdev: 1, + fsid: 1, + fileid: NFS3FileID(rawValue: UInt64(entry.0)), + atime: .init(seconds: 0, nanoseconds: 0), + mtime: .init(seconds: 0, nanoseconds: 0), + ctime: .init(seconds: 0, nanoseconds: 0)) + } + + func fsinfo(_ call: NFS3CallFSInfo, logger: Logger, promise: EventLoopPromise) { + promise.succeed( + NFS3ReplyFSInfo( + result: .okay( + .init( + attributes: nil, + rtmax: 1_000_000, + rtpref: 128_000, + rtmult: 4096, + wtmax: 1_000_000, + wtpref: 128_000, + wtmult: 4096, + dtpref: 128_000, + maxFileSize: NFS3Size(rawValue: UInt64(Int.max)), + timeDelta: NFS3Time(seconds: 0, nanoseconds: 0), + properties: .default)))) + } + + func pathconf(_ call: NFS3CallPathConf, logger: Logger, promise: EventLoopPromise) { + promise.succeed( + .init( + result: .okay( + .init( + attributes: nil, + linkMax: 1_000_000, + nameMax: 4096, + noTrunc: false, + chownRestricted: false, + caseInsensitive: false, + casePreserving: true)))) + } + + func fsstat(_ call: NFS3CallFSStat, logger: Logger, promise: EventLoopPromise) { + promise.succeed( + .init( + result: .okay( + .init( + attributes: nil, + tbytes: 0x100_0000_0000, + fbytes: 0, + abytes: 0, + tfiles: 0x1000_0000, + ffiles: 0, + afiles: 0, + invarsec: 0)))) + } + + func access(_ call: NFS3CallAccess, logger: Logger, promise: EventLoopPromise) { + promise.succeed(.init(result: .okay(.init(dirAttributes: nil, access: .allReadOnly)))) + } + + func lookup(_ call: NFS3CallLookup, logger: Logger, promise: EventLoopPromise) { + if let entry = self.lookup(fileName: call.name, inDirectory: call.dir) { + promise.succeed( + .init( + result: .okay( + .init( + fileHandle: entry.0, + attributes: entry.1, + dirAttributes: nil)))) + } else { + promise.succeed(.init(result: .fail(.errorNOENT, .init(dirAttributes: nil)))) + + } + } + + func readdirplus(_ call: NFS3CallReadDirPlus, logger: Logger, promise: EventLoopPromise) { + if let entry = self.getEntry(fileHandle: call.fileHandle) { + var entries: [NFS3ReplyReadDirPlus.Entry] = [] + for fileIndex in entry.1.children.enumerated().dropFirst(Int(min(UInt64(Int.max), + call.cookie.rawValue))) { + entries.append( + .init( + fileID: NFS3FileID(rawValue: UInt64(fileIndex.element.index)), + fileName: fileIndex.element.name, + cookie: NFS3Cookie(rawValue: UInt64(fileIndex.offset)), + nameAttributes: nil, + nameHandle: nil)) + } + promise.succeed( + .init( + result: .okay( + .init( + dirAttributes: nil, + cookieVerifier: call.cookieVerifier, + entries: entries, + eof: true)))) + } else { + promise.succeed(.init(result: .fail(.errorNOENT, .init(dirAttributes: nil)))) + + } + } + + func read(_ call: NFS3CallRead, logger: Logger, promise: EventLoopPromise) { + if let file = self.getFile(call.fileHandle) { + if file.type == .regular { + var slice = self.fileContent + guard call.offset.rawValue <= UInt64(Int.max) else { + promise.succeed(.init(result: .fail(.errorFBIG, .init(attributes: nil)))) + return + } + let offsetLegal = slice.readSlice(length: Int(call.offset.rawValue)) != nil + if offsetLegal { + let actualSlice = slice.readSlice(length: min(slice.readableBytes, Int(call.count.rawValue)))! + let isEOF = slice.readableBytes == 0 + + promise.succeed( + .init( + result: .okay( + .init( + attributes: nil, + count: NFS3Count(rawValue: UInt32(actualSlice.readableBytes)), + eof: isEOF, + data: actualSlice)))) + } else { + promise.succeed( + .init( + result: .okay( + .init( + attributes: nil, + count: 0, + eof: true, + data: ByteBuffer())))) + } + } else { + promise.succeed(.init(result: .fail(.errorISDIR, .init(attributes: nil)))) + } + } else { + promise.succeed(.init(result: .fail(.errorNOENT, .init(attributes: nil)))) + } + } + + func readlink(_ call: NFS3CallReadlink, logger: Logger, promise: EventLoopPromise) { + promise.succeed(.init(result: .fail(.errorNOENT, .init(symlinkAttributes: nil)))) + } + + func setattr(_ call: NFS3CallSetattr, logger: Logger, promise: EventLoopPromise) { + promise.succeed(.init(result: .fail(.errorROFS, .init(wcc: .init(before: nil, after: nil))))) + } + + func shutdown(promise: EventLoopPromise) { + promise.succeed(()) + } +} diff --git a/Sources/NIOExtrasNFS3Demo/FileSystemServer.swift b/Sources/NIOExtrasNFS3Demo/FileSystemServer.swift new file mode 100644 index 00000000..16557e68 --- /dev/null +++ b/Sources/NIOExtrasNFS3Demo/FileSystemServer.swift @@ -0,0 +1,213 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-2023 Apple Inc. and the SwiftNIO project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftNIO project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Dispatch +import Logging +import NIONFS3 +import NIOCore +import NIOConcurrencyHelpers +import NIOPosix + +public final class FileSystemServer: /* self-locked */ @unchecked Sendable { + private let lock = NIOLock() + private let logger: Logger + + // all protected with `self.lock` + private var running = false + private var mounters: [MountNFS] = [] + private var channel: Channel? + private var fileSystem: NFS3FileSystemNoAuth? + private var group: EventLoopGroup? + private var eventLoop: EventLoop? { + return self.group?.next() + } + + public enum Error: Swift.Error { + case alreadyRunning + case alreadyShutDown + case notRunning + } + + public struct FileSystemInfo { + public var serverAddress: SocketAddress + public var fileSystem: NFS3FileSystemNoAuth + } + + public init(logger: Logger) { + self.logger = logger + } + + deinit { + assert(!self.running, "FileSystemServer deinitialised whilst still running, please call syncShutdown().") + } + + private func tearEverythingDownLocked() throws { + var maybeError: Swift.Error? = nil + assert(!self.running) + + for mounter in self.mounters { + do { + try mounter.unmount(logger: self.logger) + } catch { + maybeError = error + logger.warning( + "unmount failed", + metadata: [ + "error": "\(error)", + "mount-point": "\(mounter.mountPoint)", + ]) + } + } + + do { + try self.channel?.close().wait() + } catch { + maybeError = error + logger.warning("channel close failed", metadata: ["error": "\(error)"]) + } + + do { + let shutdownPromise = self.group!.any().makePromise(of: Void.self) + self.fileSystem!.shutdown(promise: shutdownPromise) + try shutdownPromise.futureResult.wait() + } catch { + maybeError = error + logger.warning("FileSystem shutdown failed", metadata: ["error": "\(error)"]) + } + + try! self.group?.syncShutdownGracefully() + + if let error = maybeError { + throw error + } + + self.group = nil + self.channel = nil + self.fileSystem = nil + self.mounters = [] + } + + public func waitUntilServerFinishes() { + let g = DispatchGroup() + g.enter() + self.lock.withLock { + if let channel = self.channel { + channel.closeFuture.whenComplete { _ in + g.leave() + } + } else { + g.leave() + } + } + g.wait() + } + + public func syncShutdown() throws { + try self.lock.withLock { + guard self.running else { + throw Error.alreadyShutDown + } + self.running = false + + try self.tearEverythingDownLocked() + } + } + + public func mount( + at mountPoint: String, + pathIntoMount: String?, + nfsMountTimeoutSeconds: Int? = nil, + nfsMountDeadTimeoutSeconds: Int? = nil, + logger: Logger + ) throws { + let mounter = try self.lock.withLock { () throws -> MountNFS in + guard let localAddress = self.channel?.localAddress else { + throw Error.notRunning + } + + let mounter = MountNFS( + port: localAddress.port!, + host: localAddress.ipAddress!, + pathIntoMount: pathIntoMount ?? "/", + mountPoint: mountPoint, + nfsMountTimeoutSeconds: nfsMountTimeoutSeconds, + nfsMountDeadTimeoutSeconds: nfsMountDeadTimeoutSeconds) + self.mounters.append(mounter) + return mounter + } + try mounter.mount(logger: logger) + } + + @discardableResult + public func start( + serveHost: String = "127.0.0.1", + servePort: Int? = nil + ) throws -> FileSystemInfo { + return try self.lock.withLock { () throws -> FileSystemInfo in + guard !self.running else { + throw Error.alreadyRunning + } + + var connectionID = 0 + + self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1) + + let fileSystem = DummyFS() + self.fileSystem = fileSystem + + do { + let channel = try ServerBootstrap(group: self.eventLoop!) + .serverChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) + .childChannelInitializer { channel in + self.eventLoop!.assertInEventLoop() + + connectionID += 1 // not locked but okay because we only have one EventLoop + var logger = self.logger + let connectionString = """ + \(connectionID)@\ + \(channel.remoteAddress?.ipAddress ?? "n/a"):\ + \(channel.remoteAddress?.port ?? -1) + """ + logger[metadataKey: "tcp-connection"] = "\(connectionString)" + return channel.pipeline.addHandlers([ + NFS3FileSystemServerHandler( + fileSystem, + logger: logger), /* NOTE: FS is shared here across all channels. */ + CloseOnErrorHandler(logger: logger), + ]) + } + .bind(host: serveHost, port: servePort ?? 0) + .wait() + let channelLocalAddress = channel.localAddress! + self.logger.info( + "FileSystemServer up and running", + metadata: [ + "address": "\(channelLocalAddress)", + "pid": "\(getpid())", + ]) + + self.channel = channel + self.running = true + + return FileSystemInfo(serverAddress: channelLocalAddress, fileSystem: fileSystem) + } catch { + assert(!self.running) + + try? self.tearEverythingDownLocked() + + throw error + } + } + } +} diff --git a/Sources/NIOExtrasNFS3Demo/Main.swift b/Sources/NIOExtrasNFS3Demo/Main.swift new file mode 100644 index 00000000..a7e1b5f5 --- /dev/null +++ b/Sources/NIOExtrasNFS3Demo/Main.swift @@ -0,0 +1,77 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-2023 Apple Inc. and the SwiftNIO project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftNIO project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Dispatch +import Foundation +import Logging +import NIONFS3 +import NIOConcurrencyHelpers +import NIOCore +import NIOPosix + +struct IAMSorry: Error { + var because: String +} + +@main +struct NIOExtrasNFS3Demo { + static func main() throws { + let mount: String? = CommandLine.arguments.dropFirst().first ?? "/tmp/mount" + let nfsMountTimeoutSeconds = 100 + let nfsMountDeadTimeoutSeconds = 300 + + let group = MultiThreadedEventLoopGroup(numberOfThreads: 2) + defer { + try! group.syncShutdownGracefully() + } + + let logger = Logger(label: "com.apple.swift-nio-extras-nfs3-demo") + + let server = FileSystemServer(logger: logger) + try server.start() + if let mount = mount { + try server.mount( + at: mount, + pathIntoMount: "/", + nfsMountTimeoutSeconds: nfsMountTimeoutSeconds, + nfsMountDeadTimeoutSeconds: nfsMountDeadTimeoutSeconds, + logger: logger + ) + } + + let waitGroup = DispatchGroup() + let queue = DispatchQueue(label: "signal queue") + let signalSource = DispatchSource.makeSignalSource(signal: SIGINT, queue: queue) + signal(SIGINT, SIG_IGN) + signalSource.setEventHandler { + waitGroup.leave() + _ = signalSource // retaining this here + } + signalSource.resume() + + waitGroup.enter() + waitGroup.notify(queue: DispatchQueue.main) { + do { + try server.syncShutdown() + } catch { + logger.warning("FileSystemServer shutdown failed", metadata: ["error": "\(error)"]) + } + + logger.info("exiting") + exit(0) + } + + RunLoop.main.run() + } +} diff --git a/Sources/NIOExtrasNFS3Demo/MountHelper.swift b/Sources/NIOExtrasNFS3Demo/MountHelper.swift new file mode 100644 index 00000000..00672683 --- /dev/null +++ b/Sources/NIOExtrasNFS3Demo/MountHelper.swift @@ -0,0 +1,170 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-2023 Apple Inc. and the SwiftNIO project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftNIO project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation +import Logging + +public struct MountNFS { + public let port: Int + public let host: String + public let pathIntoMount: String + public let mountPoint: String + public let extraOptions: String = "" + public let nfsAttributeCacheTimeoutSeconds: Int = 3600 * 24 * 365 // 1 year + public let nfsMountTimeoutSeconds: Int + public let nfsMountDeadTimeoutSeconds: Int + + public init( + port: Int, + host: String, + pathIntoMount: String, + mountPoint: String, + nfsMountTimeoutSeconds: Int? = nil, + nfsMountDeadTimeoutSeconds: Int? = nil + ) { + self.port = port + self.host = host + self.pathIntoMount = pathIntoMount + self.mountPoint = mountPoint + self.nfsMountTimeoutSeconds = nfsMountTimeoutSeconds ?? 5 * 60 + self.nfsMountDeadTimeoutSeconds = nfsMountDeadTimeoutSeconds ?? 24 * 3600 + } + + func preflightCheck(logger: Logger) throws { + var logger = logger + logger[metadataKey: "mount-point"] = "\(self.mountPoint)" + + var isDir: ObjCBool = false + if !FileManager.default.fileExists(atPath: self.mountPoint) { + logger.notice("mount point does not exist, creating") + try FileManager.default.createDirectory( + at: URL(fileURLWithPath: self.mountPoint), + withIntermediateDirectories: true) + } + + guard FileManager.default.fileExists(atPath: self.mountPoint, isDirectory: &isDir) else { + struct MountPointDoesNotExist: Error {} + logger.error("even after trying to create it, mount point does not exist") + throw MountPointDoesNotExist() + } + + guard isDir.boolValue else { + struct MountPointNotADirectory: Error {} + logger.error("mount point not a directory") + throw MountPointNotADirectory() + } + + guard try FileManager.default.contentsOfDirectory(atPath: self.mountPoint).count == 0 else { + struct MountPointNotEmpty: Error {} + logger.error("mount point not empty") + throw MountPointNotEmpty() + } + } + + public func mount(logger: Logger) throws { + struct MountFailed: Error {} + + try self.preflightCheck(logger: logger) + + let p = Process() + #if canImport(Darwin) + p.executableURL = URL(fileURLWithPath: "/sbin/mount") + p.arguments = [ + "-o", + """ + ro,\ + dumbtimer,\ + timeo=\(self.nfsMountTimeoutSeconds),\ + deadtimeout=\(self.nfsMountDeadTimeoutSeconds),\ + port=\(self.port),mountport=\(self.port),\ + acregmin=\(self.nfsAttributeCacheTimeoutSeconds),\ + acregmax=\(self.nfsAttributeCacheTimeoutSeconds),\ + acdirmin=\(self.nfsAttributeCacheTimeoutSeconds),\ + acdirmax=\(self.nfsAttributeCacheTimeoutSeconds),\ + locallocks\ + \(extraOptions.isEmpty ? "" : ",\(self.extraOptions)") + """, + "-t", "nfs", + "\(self.host):/\(self.pathIntoMount)", self.mountPoint, + ] + #elseif os(Linux) + p.executableURL = URL(fileURLWithPath: "/bin/mount") + p.arguments = [ + "-o", + // NOTE: timeo is deciseconds on Linux, so 5*60 decaseconds is 30 seconds /shrug + """ + tcp,\ + timeo=\(self.nfsMountTimeoutSeconds),\ + port=\(self.port),mountport=\(self.port),\ + acregmin=\(self.nfsAttributeCacheTimeoutSeconds),\ + acregmax=\(self.nfsAttributeCacheTimeoutSeconds),\ + acdirmin=\(self.nfsAttributeCacheTimeoutSeconds),\ + acdirmax=\(self.nfsAttributeCacheTimeoutSeconds),\ + local_lock=all,\ + nolock,\ + noacl,rdirplus,\ + ro,\ + nfsvers=3\ + \(extraOptions.isEmpty ? "" : ",\(self.extraOptions)") + """, + "-t", "nfs", + "\(self.host):\(self.pathIntoMount)", self.mountPoint, + ] + #endif + try p.run() + logger.info( + "attempting mount", + metadata: ["mount-command": "\(p.executableURL!.path) \(p.arguments!.joined(separator: " ")) -- "]) + p.waitUntilExit() + switch (p.terminationReason, p.terminationStatus) { + case (.exit, 0): + logger.info("mount successful", metadata: ["mount-point": "\(self.mountPoint)"]) + default: + logger.error( + "mount failed", + metadata: [ + "termination-reason": "\(p.terminationReason)", + "termination-status": "\(p.terminationStatus)", + ]) + throw MountFailed() + } + } + + public func unmount(logger: Logger) throws { + struct UnMountFailed: Error {} + + let p = Process() + #if canImport(Darwin) + p.executableURL = URL(fileURLWithPath: "/sbin/umount") + #elseif os(Linux) + p.executableURL = URL(fileURLWithPath: "/bin/umount") + #endif + p.arguments = [self.mountPoint] + try p.run() + logger.info("attempting unmount", metadata: ["arguments": "\(p.arguments!)"]) + p.waitUntilExit() + switch (p.terminationReason, p.terminationStatus) { + case (.exit, 0): + logger.info("unmount successful") + default: + logger.error( + "unmount failed", + metadata: [ + "termination-reason": "\(p.terminationReason)", + "termination-status": "\(p.terminationStatus)", + ]) + throw UnMountFailed() + } + } +} diff --git a/Sources/NIOExtrasNFS3Demo/NFSFileSystem.swift b/Sources/NIOExtrasNFS3Demo/NFSFileSystem.swift new file mode 100644 index 00000000..05acd1a6 --- /dev/null +++ b/Sources/NIOExtrasNFS3Demo/NFSFileSystem.swift @@ -0,0 +1,81 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-2023 Apple Inc. and the SwiftNIO project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftNIO project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import NIOCore +import Logging +import NIONFS3 + +public protocol NFS3FileSystemNoAuth { + func mount(_ call: MountCallMount, logger: Logger, promise: EventLoopPromise) + func unmount(_ call: MountCallUnmount, logger: Logger, promise: EventLoopPromise) + func getattr(_ call: NFS3CallGetAttr, logger: Logger, promise: EventLoopPromise) + func fsinfo(_ call: NFS3CallFSInfo, logger: Logger, promise: EventLoopPromise) + func pathconf(_ call: NFS3CallPathConf, logger: Logger, promise: EventLoopPromise) + func fsstat(_ call: NFS3CallFSStat, logger: Logger, promise: EventLoopPromise) + func access(_ call: NFS3CallAccess, logger: Logger, promise: EventLoopPromise) + func lookup(_ call: NFS3CallLookup, logger: Logger, promise: EventLoopPromise) + func readdirplus(_ call: NFS3CallReadDirPlus, logger: Logger, promise: EventLoopPromise) + func read(_ call: NFS3CallRead, logger: Logger, promise: EventLoopPromise) + func readlink(_ call: NFS3CallReadlink, logger: Logger, promise: EventLoopPromise) + func setattr(_ call: NFS3CallSetattr, logger: Logger, promise: EventLoopPromise) + func readdir(_ call: NFS3CallReadDir, logger: Logger, promise: EventLoopPromise) + + func shutdown(promise: EventLoopPromise) +} + +extension NFS3FileSystemNoAuth { + public func readdir( + _ call: NFS3CallReadDir, logger: Logger, promise originalPromise: EventLoopPromise + ) { + let promise = originalPromise.futureResult.eventLoop.makePromise(of: NFS3ReplyReadDirPlus.self) + self.readdirplus( + NFS3CallReadDirPlus( + fileHandle: call.fileHandle, + cookie: call.cookie, + cookieVerifier: call.cookieVerifier, + dirCount: NFS3Count(integerLiteral: .max), + maxCount: call.maxResultByteCount), + logger: logger, + promise: promise) + + promise.futureResult.whenComplete { readDirPlusResult in + switch readDirPlusResult { + case .success(let readDirPlusSuccessResult): + switch readDirPlusSuccessResult.result { + case .okay(let readDirPlusOkay): + originalPromise.succeed( + NFS3ReplyReadDir( + result: .okay( + .init( + cookieVerifier: readDirPlusOkay.cookieVerifier, + entries: readDirPlusOkay.entries.map { readDirPlusEntry in + NFS3ReplyReadDir.Entry( + fileID: readDirPlusEntry.fileID, + fileName: readDirPlusEntry.fileName, + cookie: readDirPlusEntry.cookie) + }, eof: readDirPlusOkay.eof)))) + case .fail(let nfsStatus, let readDirPlusFailure): + originalPromise.succeed( + NFS3ReplyReadDir( + result: .fail( + nfsStatus, + .init(dirAttributes: readDirPlusFailure.dirAttributes)))) + + } + case .failure(let error): + originalPromise.fail(error) + } + } + } +} diff --git a/Sources/NIOExtrasNFS3Demo/NFSFileSystemInvoker.swift b/Sources/NIOExtrasNFS3Demo/NFSFileSystemInvoker.swift new file mode 100644 index 00000000..525b73ad --- /dev/null +++ b/Sources/NIOExtrasNFS3Demo/NFSFileSystemInvoker.swift @@ -0,0 +1,221 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-2023 Apple Inc. and the SwiftNIO project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftNIO project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Logging +import NIOCore +import NIONFS3 + +internal protocol NFS3FileSystemResponder { + func sendSuccessfulReply(_ reply: NFS3Reply, call: RPCNFS3Call) + func sendError(_ error: Error, call: RPCNFS3Call) +} + +internal struct NFS3FileSystemInvoker { + private let sink: Sink + private let fs: FS + private let eventLoop: EventLoop + + internal init(sink: Sink, fileSystem: FS, eventLoop: EventLoop) { + self.sink = sink + self.fs = fileSystem + self.eventLoop = eventLoop + } + + func shutdown() -> EventLoopFuture { + let promise = self.eventLoop.makePromise(of: Void.self) + self.fs.shutdown(promise: promise) + return promise.futureResult + } + + func handleNFSCall(_ callMessage: RPCNFS3Call, logger: Logger) { + switch callMessage.nfsCall { + case .mountNull: + self.sink.sendSuccessfulReply(.mountNull, call: callMessage) + case .mount(let call): + let promise = self.eventLoop.makePromise(of: MountReplyMount.self) + + self.fs.mount(call, logger: logger, promise: promise) + + promise.futureResult.whenComplete { result in + switch result { + case .success(let reply): + self.sink.sendSuccessfulReply(.mount(reply), call: callMessage) + case .failure(let error): + self.sink.sendError(error, call: callMessage) + } + } + case .unmount(let call): + let promise = self.eventLoop.makePromise(of: MountReplyUnmount.self) + + self.fs.unmount(call, logger: logger, promise: promise) + + promise.futureResult.whenComplete { result in + switch result { + case .success(let reply): + self.sink.sendSuccessfulReply(.unmount(reply), call: callMessage) + case .failure(let error): + self.sink.sendError(error, call: callMessage) + } + } + case .null: + self.sink.sendSuccessfulReply(.null, call: callMessage) + case .getattr(let call): + let promise = self.eventLoop.makePromise(of: NFS3ReplyGetAttr.self) + + self.fs.getattr(call, logger: logger, promise: promise) + + promise.futureResult.whenComplete { result in + switch result { + case .success(let reply): + self.sink.sendSuccessfulReply(.getattr(reply), call: callMessage) + case .failure(let error): + self.sink.sendError(error, call: callMessage) + } + } + case .fsinfo(let call): + let promise = self.eventLoop.makePromise(of: NFS3ReplyFSInfo.self) + + self.fs.fsinfo(call, logger: logger, promise: promise) + + promise.futureResult.whenComplete { result in + switch result { + case .success(let reply): + self.sink.sendSuccessfulReply(.fsinfo(reply), call: callMessage) + case .failure(let error): + self.sink.sendError(error, call: callMessage) + } + } + case .pathconf(let call): + let promise = self.eventLoop.makePromise(of: NFS3ReplyPathConf.self) + + self.fs.pathconf(call, logger: logger, promise: promise) + + promise.futureResult.whenComplete { result in + switch result { + case .success(let reply): + self.sink.sendSuccessfulReply(.pathconf(reply), call: callMessage) + case .failure(let error): + self.sink.sendError(error, call: callMessage) + } + } + case .fsstat(let call): + let promise = self.eventLoop.makePromise(of: NFS3ReplyFSStat.self) + + self.fs.fsstat(call, logger: logger, promise: promise) + + promise.futureResult.whenComplete { result in + switch result { + case .success(let reply): + self.sink.sendSuccessfulReply(.fsstat(reply), call: callMessage) + case .failure(let error): + self.sink.sendError(error, call: callMessage) + } + } + case .access(let call): + let promise = self.eventLoop.makePromise(of: NFS3ReplyAccess.self) + + self.fs.access(call, logger: logger, promise: promise) + + promise.futureResult.whenComplete { result in + switch result { + case .success(let reply): + self.sink.sendSuccessfulReply(.access(reply), call: callMessage) + case .failure(let error): + self.sink.sendError(error, call: callMessage) + } + } + case .lookup(let call): + let promise = self.eventLoop.makePromise(of: NFS3ReplyLookup.self) + + self.fs.lookup(call, logger: logger, promise: promise) + + promise.futureResult.whenComplete { result in + switch result { + case .success(let reply): + self.sink.sendSuccessfulReply(.lookup(reply), call: callMessage) + case .failure(let error): + self.sink.sendError(error, call: callMessage) + } + } + case .readdirplus(let call): + let promise = self.eventLoop.makePromise(of: NFS3ReplyReadDirPlus.self) + + self.fs.readdirplus(call, logger: logger, promise: promise) + + promise.futureResult.whenComplete { result in + switch result { + case .success(let reply): + self.sink.sendSuccessfulReply(.readdirplus(reply), call: callMessage) + case .failure(let error): + self.sink.sendError(error, call: callMessage) + } + } + case .read(let call): + let promise = self.eventLoop.makePromise(of: NFS3ReplyRead.self) + + self.fs.read(call, logger: logger, promise: promise) + + promise.futureResult.whenComplete { result in + switch result { + case .success(let reply): + self.sink.sendSuccessfulReply(.read(reply), call: callMessage) + case .failure(let error): + self.sink.sendError(error, call: callMessage) + } + } + case .readdir(let call): + let promise = self.eventLoop.makePromise(of: NFS3ReplyReadDir.self) + + self.fs.readdir(call, logger: logger, promise: promise) + + promise.futureResult.whenComplete { result in + switch result { + case .success(let reply): + self.sink.sendSuccessfulReply(.readdir(reply), call: callMessage) + case .failure(let error): + self.sink.sendError(error, call: callMessage) + } + } + case .readlink(let call): + let promise = self.eventLoop.makePromise(of: NFS3ReplyReadlink.self) + + self.fs.readlink(call, logger: logger, promise: promise) + + promise.futureResult.whenComplete { result in + switch result { + case .success(let reply): + self.sink.sendSuccessfulReply(.readlink(reply), call: callMessage) + case .failure(let error): + self.sink.sendError(error, call: callMessage) + } + } + case .setattr(let call): + let promise = self.eventLoop.makePromise(of: NFS3ReplySetattr.self) + + self.fs.setattr(call, logger: logger, promise: promise) + + promise.futureResult.whenComplete { result in + switch result { + case .success(let reply): + self.sink.sendSuccessfulReply(.setattr(reply), call: callMessage) + case .failure(let error): + self.sink.sendError(error, call: callMessage) + } + } + case ._PLEASE_DO_NOT_EXHAUSTIVELY_MATCH_THIS_ENUM_NEW_CASES_MIGHT_BE_ADDED_IN_THE_FUTURE: + // inside the module, matching exhaustively is okay + preconditionFailure("unknown NFS3 call, this should never happen. Please report a bug.") + } + } +} diff --git a/Sources/NIOExtrasNFS3Demo/NFSFileSystemServerHandler.swift b/Sources/NIOExtrasNFS3Demo/NFSFileSystemServerHandler.swift new file mode 100644 index 00000000..ca83f1de --- /dev/null +++ b/Sources/NIOExtrasNFS3Demo/NFSFileSystemServerHandler.swift @@ -0,0 +1,147 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-2023 Apple Inc. and the SwiftNIO project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftNIO project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import NIOCore +import NIONFS3 +import Logging + +public final class NFS3FileSystemServerHandler { + public typealias InboundIn = ByteBuffer + public typealias OutboundOut = ByteBuffer + + private var error: Error? = nil + private var b2md = NIOSingleStepByteToMessageProcessor( + NFS3CallDecoder(), + maximumBufferSize: 4 * 1024 * 1024) + private let filesystem: FS + private let rpcReplySuccess: RPCReplyStatus = .messageAccepted( + .init( + verifier: .init( + flavor: .noAuth, + opaque: nil), + status: .success)) + private var invoker: NFS3FileSystemInvoker>? + private var context: ChannelHandlerContext? = nil + private var writeBuffer = ByteBuffer() + private let fillByteBuffer = ByteBuffer(repeating: 0x41, count: 4) + private var logger: Logger + + public init(_ fs: FS, logger: Logger) { + self.filesystem = fs + self.logger = logger + } +} + +extension NFS3FileSystemServerHandler: ChannelInboundHandler { + public func handlerAdded(context: ChannelHandlerContext) { + self.context = context + self.invoker = NFS3FileSystemInvoker(sink: self, fileSystem: self.filesystem, eventLoop: context.eventLoop) + } + + public func handlerRemoved(context: ChannelHandlerContext) { + self.invoker = nil + self.context = nil + } + + public func channelRead(context: ChannelHandlerContext, data: NIOAny) { + let data = self.unwrapInboundIn(data) + guard self.error == nil else { + context.fireErrorCaught( + ByteToMessageDecoderError.dataReceivedInErrorState( + self.error!, + data)) + return + } + + do { + try self.b2md.process(buffer: data) { nfsCall in + self.invoker?.handleNFSCall(nfsCall, logger: self.logger) + } + } catch { + self.error = error + self.invoker = nil + context.fireErrorCaught(error) + } + } + + public func errorCaught(context: ChannelHandlerContext, error: Error) { + switch error as? NFS3Error { + case .unknownProgramOrProcedure(.call(let call)): + print("UNKNOWN CALL: \(call)") + let reply = RPCNFS3Reply( + rpcReply: .init( + xid: call.xid, + status: .messageAccepted( + .init( + verifier: .init(flavor: .noAuth, opaque: nil), + status: .procedureUnavailable))), + nfsReply: .null) + self.writeBuffer.clear() + self.writeBuffer.writeRPCNFS3Reply(reply) + return + default: + () + } + context.fireErrorCaught(error) + } +} + +extension NFS3FileSystemServerHandler: NFS3FileSystemResponder { + func sendSuccessfulReply(_ reply: NFS3Reply, call: RPCNFS3Call) { + if let context = self.context { + let reply = RPCNFS3Reply( + rpcReply: .init( + xid: call.rpcCall.xid, + status: self.rpcReplySuccess), + nfsReply: reply) + + self.writeBuffer.clear() + switch self.writeBuffer.writeRPCNFS3ReplyPartially(reply).1 { + case .doNothing: + context.writeAndFlush(self.wrapOutboundOut(self.writeBuffer), promise: nil) + case .writeBlob(let buffer, numberOfFillBytes: let fillBytes): + context.write(self.wrapOutboundOut(self.writeBuffer), promise: nil) + context.write(self.wrapOutboundOut(buffer), promise: nil) + if fillBytes > 0 { + var fillers = self.fillByteBuffer + context.write(self.wrapOutboundOut(fillers.readSlice(length: fillBytes)!), promise: nil) + } + context.flush() + } + } + } + + func sendError(_ error: Error, call: RPCNFS3Call) { + if let context = self.context { + let reply = RPCNFS3Reply( + rpcReply: .init( + xid: call.rpcCall.xid, + status: self.rpcReplySuccess), + nfsReply: .mount( + .init( + result: .fail( + .errorSERVERFAULT, + NFS3Nothing())))) + + self.writeBuffer.clear() + self.writeBuffer.writeRPCNFS3Reply(reply) + + context.fireErrorCaught(error) + context.writeAndFlush(self.wrapOutboundOut(self.writeBuffer), promise: nil) + } + } +} + +@available(*, unavailable) // Not Sendable +extension NFS3FileSystemServerHandler: Sendable {} diff --git a/Sources/NIOExtrasNFS3Demo/WriteAllIncomingBytes.swift b/Sources/NIOExtrasNFS3Demo/WriteAllIncomingBytes.swift new file mode 100644 index 00000000..d2821ed3 --- /dev/null +++ b/Sources/NIOExtrasNFS3Demo/WriteAllIncomingBytes.swift @@ -0,0 +1,56 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2021-2023 Apple Inc. and the SwiftNIO project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftNIO project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import NIOCore +import NIOExtras + +final class WriteAllBytesHandler: ChannelDuplexHandler { + typealias InboundIn = ByteBuffer + typealias InboundOut = ByteBuffer + typealias OutboundIn = ByteBuffer + typealias OutboundOut = ByteBuffer + + private let inFileSink: NIOWritePCAPHandler.SynchronizedFileSink + private let outFileSink: NIOWritePCAPHandler.SynchronizedFileSink + + init(path: String) { + self.inFileSink = try! NIOWritePCAPHandler.SynchronizedFileSink.fileSinkWritingToFile(path: path + "-in") { + error in + print("ERROR (\(#line)): \(error)") + } + self.outFileSink = try! NIOWritePCAPHandler.SynchronizedFileSink.fileSinkWritingToFile(path: path + "-out") { + error in + print("ERROR (\(#line)): \(error)") + } + } + + func channelRead(context: ChannelHandlerContext, data: NIOAny) { + context.fireChannelRead(data) + + let buffer = self.unwrapInboundIn(data) + self.inFileSink.write(buffer: buffer) + } + + func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { + context.write(data, promise: promise) + + let buffer = self.unwrapOutboundIn(data) + self.outFileSink.write(buffer: buffer) + } + + func handlerRemoved(context: ChannelHandlerContext) { + try! self.inFileSink.syncClose() + try! self.outFileSink.syncClose() + } +}