├── .github ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ └── go.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── conn.go ├── errors.go ├── example ├── helloworld │ └── main.go ├── osnfs │ ├── changeos.go │ ├── changeos_unix.go │ └── main.go └── osview │ └── main.go ├── file.go ├── file ├── file.go ├── file_other.go ├── file_unix.go ├── file_wasm.go └── file_windows.go ├── filesystem.go ├── go.mod ├── go.sum ├── handler.go ├── helpers ├── cachinghandler.go ├── memfs │ ├── memfs.go │ └── storage.go └── nullauthhandler.go ├── log.go ├── mount.go ├── mountinterface.go ├── nfs.go ├── nfs_onaccess.go ├── nfs_oncommit.go ├── nfs_oncreate.go ├── nfs_onfsinfo.go ├── nfs_onfsstat.go ├── nfs_ongetattr.go ├── nfs_onlink.go ├── nfs_onlookup.go ├── nfs_onmkdir.go ├── nfs_onmknod.go ├── nfs_onpathconf.go ├── nfs_onread.go ├── nfs_onreaddir.go ├── nfs_onreaddirplus.go ├── nfs_onreadlink.go ├── nfs_onremove.go ├── nfs_onrename.go ├── nfs_onrmdir.go ├── nfs_onsetattr.go ├── nfs_onsymlink.go ├── nfs_onwrite.go ├── nfs_test.go ├── nfsinterface.go ├── server.go └── time.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "Code scanning - action" 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: '0 18 * * 3' 8 | 9 | jobs: 10 | CodeQL-Build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v2 17 | with: 18 | # We must fetch at least the immediate parents so that if this is 19 | # a pull request then we can checkout the head. 20 | fetch-depth: 2 21 | 22 | # If this run was triggered by a pull request event, then checkout 23 | # the head of the pull request instead of the merge commit. 24 | - run: git checkout HEAD^2 25 | if: ${{ github.event_name == 'pull_request' }} 26 | 27 | # Initializes the CodeQL tools for scanning. 28 | - name: Initialize CodeQL 29 | uses: github/codeql-action/init@v1 30 | # Override language selection by uncommenting this and choosing your languages 31 | # with: 32 | # languages: go, javascript, csharp, python, cpp, java 33 | 34 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 35 | # If this step fails, then you should remove it and run the build manually (see below) 36 | - name: Autobuild 37 | uses: github/codeql-action/autobuild@v1 38 | 39 | # ℹ️ Command-line programs to run using the OS shell. 40 | # 📚 https://git.io/JvXDl 41 | 42 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 43 | # and modify them (or add more) to build your code if your project 44 | # uses a compiled language 45 | 46 | #- run: | 47 | # make bootstrap 48 | # make release 49 | 50 | - name: Perform CodeQL Analysis 51 | uses: github/codeql-action/analyze@v1 52 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | name: Build 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Set up Go 1.x 18 | uses: actions/setup-go@v3 19 | with: 20 | go-version: ^1.19 21 | id: go 22 | 23 | - name: Check out code into the Go module directory 24 | uses: actions/checkout@v3 25 | 26 | - name: Get dependencies 27 | run: go get -v -t -d ./... 28 | 29 | - name: Build 30 | run: go build -v ./... 31 | 32 | - name: golangci-lint 33 | uses: golangci/golangci-lint-action@v3 34 | 35 | - name: Test 36 | run: go test -v . 37 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | We appreciate your interest in improving go-nfs! 4 | 5 | ## Looking for ways to contribute? 6 | 7 | There are several ways you can contribute: 8 | - Start contributing immediately via the [opened](https://github.com/willscott/go-nfs/issues) issues on GitHub. 9 | Defined issues provide an excellent starting point. 10 | - Reporting issues, bugs, mistakes, or inconsistencies. 11 | As many open source projects, we are short-staffed, we thus kindly ask you to be open to contribute a fix for discovered issues. 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Golang Network File Server 2 | === 3 | 4 | NFSv3 protocol implementation in pure Golang. 5 | 6 | Current Status: 7 | * Minimally tested 8 | * Mounts, read-only and read-write support 9 | 10 | Usage 11 | === 12 | 13 | The most interesting demo is currently in `example/osview`. 14 | 15 | Start the server 16 | `go run ./example/osview .`. 17 | 18 | The local folder at `.` will be the initial view in the mount. mutations to metadata or contents 19 | will be stored purely in memory and not written back to the OS. When run, this 20 | demo will print the port it is listening on. 21 | 22 | The mount can be accessed using a command similar to 23 | `mount -o port=,mountport= -t nfs localhost:/mount ` (For Mac users) 24 | 25 | or 26 | 27 | `mount -o port=,mountport=,nfsvers=3,noacl,tcp -t nfs localhost:/mount ` (For Linux users) 28 | 29 | API 30 | === 31 | 32 | The NFS server runs on a `net.Listener` to export a file system to NFS clients. 33 | Usage is structured similarly to many other golang network servers. 34 | 35 | ```golang 36 | package main 37 | 38 | import ( 39 | "fmt" 40 | "log" 41 | "net" 42 | 43 | "github.com/go-git/go-billy/v5/memfs" 44 | nfs "github.com/willscott/go-nfs" 45 | nfshelper "github.com/willscott/go-nfs/helpers" 46 | ) 47 | 48 | func main() { 49 | listener, err := net.Listen("tcp", ":0") 50 | panicOnErr(err, "starting TCP listener") 51 | fmt.Printf("Server running at %s\n", listener.Addr()) 52 | mem := memfs.New() 53 | f, err := mem.Create("hello.txt") 54 | panicOnErr(err, "creating file") 55 | _, err = f.Write([]byte("hello world")) 56 | panicOnErr(err, "writing data") 57 | f.Close() 58 | handler := nfshelper.NewNullAuthHandler(mem) 59 | cacheHelper := nfshelper.NewCachingHandler(handler, 1) 60 | panicOnErr(nfs.Serve(listener, cacheHelper), "serving nfs") 61 | } 62 | 63 | func panicOnErr(err error, desc ...interface{}) { 64 | if err == nil { 65 | return 66 | } 67 | log.Println(desc...) 68 | log.Panicln(err) 69 | } 70 | ``` 71 | 72 | Notes 73 | --- 74 | 75 | * Ports are typically determined through portmap. The need for running portmap 76 | (which is the only part that needs a privileged listening port) can be avoided 77 | through specific mount options. e.g. 78 | `mount -o port=n,mountport=n -t nfs host:/mount /localmount` 79 | 80 | * This server currently uses [billy](https://github.com/go-git/go-billy/) to 81 | provide a file system abstraction layer. There are some edges of the NFS protocol 82 | which do not translate to this abstraction. 83 | * NFS expects access to an `inode` or equivalent unique identifier to reference 84 | files in a file system. These are considered opaque identifiers here, which 85 | means they will not work as expected in cases of hard linking. 86 | * The billy abstraction layer does not extend to exposing `uid` and `gid` 87 | ownership of files. If ownership is important to your file system, you 88 | will need to ensure that the `os.FileInfo` meets additional constraints. 89 | In particular, the `Sys()` escape hatch is queried by this library, and 90 | if your file system populates a [`syscall.Stat_t`](https://golang.org/pkg/syscall/#Stat_t) 91 | concrete struct, the ownership specified in that object will be used. 92 | You can also return a [`file.FileInfo`](https://github.com/willscott/go-nfs/blob/master/file/file.go#L5) 93 | which doesn't vary between platforms so may be easier to deal with. 94 | 95 | * Relevant RFCS: 96 | [5531 - RPC protocol](https://tools.ietf.org/html/rfc5531), 97 | [1813 - NFSv3](https://tools.ietf.org/html/rfc1813), 98 | [1094 - NFS](https://tools.ietf.org/html/rfc1094) 99 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | The latest release reflects the current best recommendation / supported version at this time. 6 | 7 | ## Reporting a Vulnerability 8 | 9 | Please email Will (the git commit author) if you need to report issues privately. 10 | I will endeavor to respond within a day, but if I am offline, responses may be delayed longer than that. 11 | If you need a stronger SLA to have confidence in using this code, feel free to reach out. 12 | -------------------------------------------------------------------------------- /conn.go: -------------------------------------------------------------------------------- 1 | package nfs 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "context" 7 | "encoding/binary" 8 | "errors" 9 | "fmt" 10 | "io" 11 | "net" 12 | 13 | xdr2 "github.com/rasky/go-xdr/xdr2" 14 | "github.com/willscott/go-nfs-client/nfs/rpc" 15 | "github.com/willscott/go-nfs-client/nfs/xdr" 16 | ) 17 | 18 | var ( 19 | // ErrInputInvalid is returned when input cannot be parsed 20 | ErrInputInvalid = errors.New("invalid input") 21 | // ErrAlreadySent is returned when writing a header/status multiple times 22 | ErrAlreadySent = errors.New("response already started") 23 | ) 24 | 25 | // ResponseCode is a combination of accept_stat and reject_stat. 26 | type ResponseCode uint32 27 | 28 | // ResponseCode Codes 29 | const ( 30 | ResponseCodeSuccess ResponseCode = iota 31 | ResponseCodeProgUnavailable 32 | ResponseCodeProcUnavailable 33 | ResponseCodeGarbageArgs 34 | ResponseCodeSystemErr 35 | ResponseCodeRPCMismatch 36 | ResponseCodeAuthError 37 | ) 38 | 39 | type conn struct { 40 | *Server 41 | writeSerializer chan []byte 42 | net.Conn 43 | } 44 | 45 | func (c *conn) serve(ctx context.Context) { 46 | connCtx, cancel := context.WithCancel(ctx) 47 | defer cancel() 48 | c.writeSerializer = make(chan []byte, 1) 49 | go c.serializeWrites(connCtx) 50 | 51 | bio := bufio.NewReader(c.Conn) 52 | for { 53 | w, err := c.readRequestHeader(connCtx, bio) 54 | if err != nil { 55 | if err == io.EOF { 56 | // Clean close. 57 | c.Close() 58 | return 59 | } 60 | return 61 | } 62 | Log.Tracef("request: %v", w.req) 63 | err = c.handle(connCtx, w) 64 | respErr := w.finish(connCtx) 65 | if err != nil { 66 | Log.Errorf("error handling req: %v", err) 67 | // failure to handle at a level needing to close the connection. 68 | c.Close() 69 | return 70 | } 71 | if respErr != nil { 72 | Log.Errorf("error sending response: %v", respErr) 73 | c.Close() 74 | return 75 | } 76 | } 77 | } 78 | 79 | func (c *conn) serializeWrites(ctx context.Context) { 80 | // todo: maybe don't need the extra buffer 81 | writer := bufio.NewWriter(c.Conn) 82 | var fragmentBuf [4]byte 83 | var fragmentInt uint32 84 | for { 85 | select { 86 | case <-ctx.Done(): 87 | return 88 | case msg, ok := <-c.writeSerializer: 89 | if !ok { 90 | return 91 | } 92 | // prepend the fragmentation header 93 | fragmentInt = uint32(len(msg)) 94 | fragmentInt |= (1 << 31) 95 | binary.BigEndian.PutUint32(fragmentBuf[:], fragmentInt) 96 | n, err := writer.Write(fragmentBuf[:]) 97 | if n < 4 || err != nil { 98 | return 99 | } 100 | n, err = writer.Write(msg) 101 | if err != nil { 102 | return 103 | } 104 | if n < len(msg) { 105 | panic("todo: ensure writes complete fully.") 106 | } 107 | if err = writer.Flush(); err != nil { 108 | return 109 | } 110 | } 111 | } 112 | } 113 | 114 | // Handle a request. errors from this method indicate a failure to read or 115 | // write on the network stream, and trigger a disconnection of the connection. 116 | func (c *conn) handle(ctx context.Context, w *response) error { 117 | handler := c.Server.handlerFor(w.req.Header.Prog, w.req.Header.Proc) 118 | if handler == nil { 119 | Log.Errorf("No handler for %d.%d", w.req.Header.Prog, w.req.Header.Proc) 120 | if err := w.drain(ctx); err != nil { 121 | return err 122 | } 123 | return c.err(ctx, w, &ResponseCodeProcUnavailableError{}) 124 | } 125 | appError := handler(ctx, w, c.Server.Handler) 126 | if drainErr := w.drain(ctx); drainErr != nil { 127 | return drainErr 128 | } 129 | if appError != nil && !w.responded { 130 | if err := c.err(ctx, w, appError); err != nil { 131 | return err 132 | } 133 | } 134 | if !w.responded { 135 | Log.Errorf("Handler did not indicate response status via writing or erroring") 136 | if err := c.err(ctx, w, &ResponseCodeSystemError{}); err != nil { 137 | return err 138 | } 139 | } 140 | return nil 141 | } 142 | 143 | func (c *conn) err(ctx context.Context, w *response, err error) error { 144 | select { 145 | case <-ctx.Done(): 146 | return nil 147 | default: 148 | } 149 | 150 | if w.err == nil { 151 | w.err = err 152 | } 153 | 154 | if w.responded { 155 | return nil 156 | } 157 | 158 | rpcErr := w.errorFmt(err) 159 | if writeErr := w.writeHeader(rpcErr.Code()); writeErr != nil { 160 | return writeErr 161 | } 162 | 163 | body, _ := rpcErr.MarshalBinary() 164 | return w.Write(body) 165 | } 166 | 167 | type request struct { 168 | xid uint32 169 | rpc.Header 170 | Body io.Reader 171 | } 172 | 173 | func (r *request) String() string { 174 | if r.Header.Prog == nfsServiceID { 175 | return fmt.Sprintf("RPC #%d (nfs.%s)", r.xid, NFSProcedure(r.Header.Proc)) 176 | } else if r.Header.Prog == mountServiceID { 177 | return fmt.Sprintf("RPC #%d (mount.%s)", r.xid, MountProcedure(r.Header.Proc)) 178 | } 179 | return fmt.Sprintf("RPC #%d (%d.%d)", r.xid, r.Header.Prog, r.Header.Proc) 180 | } 181 | 182 | type response struct { 183 | *conn 184 | writer *bytes.Buffer 185 | responded bool 186 | err error 187 | errorFmt func(error) RPCError 188 | req *request 189 | } 190 | 191 | func (w *response) writeXdrHeader() error { 192 | err := xdr.Write(w.writer, &w.req.xid) 193 | if err != nil { 194 | return err 195 | } 196 | respType := uint32(1) 197 | err = xdr.Write(w.writer, &respType) 198 | if err != nil { 199 | return err 200 | } 201 | return nil 202 | } 203 | 204 | func (w *response) writeHeader(code ResponseCode) error { 205 | if w.responded { 206 | return ErrAlreadySent 207 | } 208 | w.responded = true 209 | if err := w.writeXdrHeader(); err != nil { 210 | return err 211 | } 212 | 213 | status := rpc.MsgAccepted 214 | if code == ResponseCodeAuthError || code == ResponseCodeRPCMismatch { 215 | status = rpc.MsgDenied 216 | } 217 | 218 | err := xdr.Write(w.writer, &status) 219 | if err != nil { 220 | return err 221 | } 222 | 223 | if status == rpc.MsgAccepted { 224 | // Write opaque_auth header. 225 | err = xdr.Write(w.writer, &rpc.AuthNull) 226 | if err != nil { 227 | return err 228 | } 229 | } 230 | 231 | return xdr.Write(w.writer, &code) 232 | } 233 | 234 | // Write a response to an xdr message 235 | func (w *response) Write(dat []byte) error { 236 | if !w.responded { 237 | if err := w.writeHeader(ResponseCodeSuccess); err != nil { 238 | return err 239 | } 240 | } 241 | 242 | acc := 0 243 | for acc < len(dat) { 244 | n, err := w.writer.Write(dat[acc:]) 245 | if err != nil { 246 | return err 247 | } 248 | acc += n 249 | } 250 | return nil 251 | } 252 | 253 | // drain reads the rest of the request frame if not consumed by the handler. 254 | func (w *response) drain(ctx context.Context) error { 255 | if reader, ok := w.req.Body.(*io.LimitedReader); ok { 256 | if reader.N == 0 { 257 | return nil 258 | } 259 | // todo: wrap body in a context reader. 260 | _, err := io.CopyN(io.Discard, w.req.Body, reader.N) 261 | if err == nil || err == io.EOF { 262 | return nil 263 | } 264 | return err 265 | } 266 | return io.ErrUnexpectedEOF 267 | } 268 | 269 | func (w *response) finish(ctx context.Context) error { 270 | select { 271 | case w.conn.writeSerializer <- w.writer.Bytes(): 272 | return nil 273 | case <-ctx.Done(): 274 | return ctx.Err() 275 | } 276 | } 277 | 278 | func (c *conn) readRequestHeader(ctx context.Context, reader *bufio.Reader) (w *response, err error) { 279 | fragment, err := xdr.ReadUint32(reader) 280 | if err != nil { 281 | if xdrErr, ok := err.(*xdr2.UnmarshalError); ok { 282 | if xdrErr.Err == io.EOF { 283 | return nil, io.EOF 284 | } 285 | } 286 | return nil, err 287 | } 288 | if fragment&(1<<31) == 0 { 289 | Log.Warnf("Warning: haven't implemented fragment reconstruction.\n") 290 | return nil, ErrInputInvalid 291 | } 292 | reqLen := fragment - uint32(1<<31) 293 | if reqLen < 40 { 294 | return nil, ErrInputInvalid 295 | } 296 | 297 | r := io.LimitedReader{R: reader, N: int64(reqLen)} 298 | 299 | xid, err := xdr.ReadUint32(&r) 300 | if err != nil { 301 | return nil, err 302 | } 303 | reqType, err := xdr.ReadUint32(&r) 304 | if err != nil { 305 | return nil, err 306 | } 307 | if reqType != 0 { // 0 = request, 1 = response 308 | return nil, ErrInputInvalid 309 | } 310 | 311 | req := request{ 312 | xid, 313 | rpc.Header{}, 314 | &r, 315 | } 316 | if err = xdr.Read(&r, &req.Header); err != nil { 317 | return nil, err 318 | } 319 | 320 | w = &response{ 321 | conn: c, 322 | req: &req, 323 | errorFmt: basicErrorFormatter, 324 | // TODO: use a pool for these. 325 | writer: bytes.NewBuffer([]byte{}), 326 | } 327 | return w, nil 328 | } 329 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package nfs 2 | 3 | import ( 4 | "encoding" 5 | "encoding/binary" 6 | "errors" 7 | "fmt" 8 | ) 9 | 10 | // RPCError provides the error interface for errors thrown by 11 | // procedures to be transmitted over the XDR RPC channel 12 | type RPCError interface { 13 | // An RPCError is an `error` with this method 14 | Error() string 15 | // Code is the RPC Response code to send 16 | Code() ResponseCode 17 | // BinaryMarshaler is the on-wire representation of this error 18 | encoding.BinaryMarshaler 19 | } 20 | 21 | // AuthStat is an enumeration of why authentication ahs failed 22 | type AuthStat uint32 23 | 24 | // AuthStat Codes 25 | const ( 26 | AuthStatOK AuthStat = iota 27 | AuthStatBadCred 28 | AuthStatRejectedCred 29 | AuthStatBadVerifier 30 | AuthStatRejectedVerfier 31 | AuthStatTooWeak 32 | AuthStatInvalidResponse 33 | AuthStatFailed 34 | AuthStatKerbGeneric 35 | AuthStatTimeExpire 36 | AuthStatTktFile 37 | AuthStatDecode 38 | AuthStatNetAddr 39 | AuthStatRPCGSSCredProblem 40 | AuthStatRPCGSSCTXProblem 41 | ) 42 | 43 | // AuthError is an RPCError 44 | type AuthError struct { 45 | AuthStat 46 | } 47 | 48 | // Code for AuthErrors is ResponseCodeAuthError 49 | func (a *AuthError) Code() ResponseCode { 50 | return ResponseCodeAuthError 51 | } 52 | 53 | // Error is a textual representaiton of the auth error. From the RFC 54 | func (a *AuthError) Error() string { 55 | switch a.AuthStat { 56 | case AuthStatOK: 57 | return "Auth Status: OK" 58 | case AuthStatBadCred: 59 | return "Auth Status: bad credential" 60 | case AuthStatRejectedCred: 61 | return "Auth Status: client must begin new session" 62 | case AuthStatBadVerifier: 63 | return "Auth Status: bad verifier" 64 | case AuthStatRejectedVerfier: 65 | return "Auth Status: verifier expired or replayed" 66 | case AuthStatTooWeak: 67 | return "Auth Status: rejected for security reasons" 68 | case AuthStatInvalidResponse: 69 | return "Auth Status: bogus response verifier" 70 | case AuthStatFailed: 71 | return "Auth Status: reason unknown" 72 | case AuthStatKerbGeneric: 73 | return "Auth Status: kerberos generic error" 74 | case AuthStatTimeExpire: 75 | return "Auth Status: time of credential expired" 76 | case AuthStatTktFile: 77 | return "Auth Status: problem with ticket file" 78 | case AuthStatDecode: 79 | return "Auth Status: can't decode authenticator" 80 | case AuthStatNetAddr: 81 | return "Auth Status: wrong net address in ticket" 82 | case AuthStatRPCGSSCredProblem: 83 | return "Auth Status: no credentials for user" 84 | case AuthStatRPCGSSCTXProblem: 85 | return "Auth Status: problem with context" 86 | } 87 | return "Auth Status: Unknown" 88 | } 89 | 90 | // MarshalBinary sends the specific auth status 91 | func (a *AuthError) MarshalBinary() (data []byte, err error) { 92 | var resp [4]byte 93 | binary.LittleEndian.PutUint32(resp[:], uint32(a.AuthStat)) 94 | return resp[:], nil 95 | } 96 | 97 | // RPCMismatchError is an RPCError 98 | type RPCMismatchError struct { 99 | Low uint32 100 | High uint32 101 | } 102 | 103 | // Code for RPCMismatchError is ResponseCodeRPCMismatch 104 | func (r *RPCMismatchError) Code() ResponseCode { 105 | return ResponseCodeRPCMismatch 106 | } 107 | 108 | func (r *RPCMismatchError) Error() string { 109 | return fmt.Sprintf("RPC Mismatch: Expected version between %d and %d.", r.Low, r.High) 110 | } 111 | 112 | // MarshalBinary sends the specific rpc mismatch range 113 | func (r *RPCMismatchError) MarshalBinary() (data []byte, err error) { 114 | var resp [8]byte 115 | binary.LittleEndian.PutUint32(resp[0:4], uint32(r.Low)) 116 | binary.LittleEndian.PutUint32(resp[4:8], uint32(r.High)) 117 | return resp[:], nil 118 | } 119 | 120 | // ResponseCodeProcUnavailableError is an RPCError 121 | type ResponseCodeProcUnavailableError struct { 122 | } 123 | 124 | // Code for ResponseCodeProcUnavailableError 125 | func (r *ResponseCodeProcUnavailableError) Code() ResponseCode { 126 | return ResponseCodeProcUnavailable 127 | } 128 | 129 | func (r *ResponseCodeProcUnavailableError) Error() string { 130 | return "The requested procedure is unexported" 131 | } 132 | 133 | // MarshalBinary - this error has no associated body 134 | func (r *ResponseCodeProcUnavailableError) MarshalBinary() (data []byte, err error) { 135 | return []byte{}, nil 136 | } 137 | 138 | // ResponseCodeSystemError is an RPCError 139 | type ResponseCodeSystemError struct { 140 | } 141 | 142 | // Code for ResponseCodeSystemError 143 | func (r *ResponseCodeSystemError) Code() ResponseCode { 144 | return ResponseCodeSystemErr 145 | } 146 | 147 | func (r *ResponseCodeSystemError) Error() string { 148 | return "memory allocation failure" 149 | } 150 | 151 | // MarshalBinary - this error has no associated body 152 | func (r *ResponseCodeSystemError) MarshalBinary() (data []byte, err error) { 153 | return []byte{}, nil 154 | } 155 | 156 | // basicErrorFormatter is the default error handler for response errors. 157 | // if the error is already formatted, it is directly written. Otherwise, 158 | // ResponseCodeSystemError is sent to the client. 159 | func basicErrorFormatter(err error) RPCError { 160 | var rpcErr RPCError 161 | if errors.As(err, &rpcErr) { 162 | return rpcErr 163 | } 164 | return &ResponseCodeSystemError{} 165 | } 166 | 167 | // NFSStatusError represents an error at the NFS level. 168 | type NFSStatusError struct { 169 | NFSStatus 170 | WrappedErr error 171 | } 172 | 173 | // Error is The wrapped error 174 | func (s *NFSStatusError) Error() string { 175 | message := s.NFSStatus.String() 176 | if s.WrappedErr != nil { 177 | message = fmt.Sprintf("%s: %v", message, s.WrappedErr) 178 | } 179 | return message 180 | } 181 | 182 | // Code for NFS issues are successful RPC responses 183 | func (s *NFSStatusError) Code() ResponseCode { 184 | return ResponseCodeSuccess 185 | } 186 | 187 | // MarshalBinary - The binary form of the code. 188 | func (s *NFSStatusError) MarshalBinary() (data []byte, err error) { 189 | var resp [4]byte 190 | binary.BigEndian.PutUint32(resp[0:4], uint32(s.NFSStatus)) 191 | return resp[:], nil 192 | } 193 | 194 | // Unwrap unpacks wrapped errors 195 | func (s *NFSStatusError) Unwrap() error { 196 | return s.WrappedErr 197 | } 198 | 199 | // StatusErrorWithBody is an NFS error with a payload. 200 | type StatusErrorWithBody struct { 201 | NFSStatusError 202 | Body []byte 203 | } 204 | 205 | // MarshalBinary provides the wire format of the error response 206 | func (s *StatusErrorWithBody) MarshalBinary() (data []byte, err error) { 207 | head, err := s.NFSStatusError.MarshalBinary() 208 | return append(head, s.Body...), err 209 | } 210 | 211 | // errFormatterWithBody appends a provided body to errors 212 | func errFormatterWithBody(body []byte) func(err error) RPCError { 213 | return func(err error) RPCError { 214 | if nerr, ok := err.(*NFSStatusError); ok { 215 | return &StatusErrorWithBody{*nerr, body[:]} 216 | } 217 | var rErr RPCError 218 | if errors.As(err, &rErr) { 219 | return rErr 220 | } 221 | return &ResponseCodeSystemError{} 222 | } 223 | } 224 | 225 | var ( 226 | opAttrErrorBody = [4]byte{} 227 | opAttrErrorFormatter = errFormatterWithBody(opAttrErrorBody[:]) 228 | wccDataErrorBody = [8]byte{} 229 | wccDataErrorFormatter = errFormatterWithBody(wccDataErrorBody[:]) 230 | ) 231 | -------------------------------------------------------------------------------- /example/helloworld/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | 7 | "github.com/go-git/go-billy/v5" 8 | "github.com/go-git/go-billy/v5/memfs" 9 | 10 | nfs "github.com/willscott/go-nfs" 11 | nfshelper "github.com/willscott/go-nfs/helpers" 12 | ) 13 | 14 | // ROFS is an intercepter for the filesystem indicating it should 15 | // be read only. The undelrying billy.Memfs indicates it supports 16 | // writing, but does not in implement billy.Change to support 17 | // modification of permissions / modTimes, and as such cannot be 18 | // used as RW system. 19 | type ROFS struct { 20 | billy.Filesystem 21 | } 22 | 23 | // Capabilities exports the filesystem as readonly 24 | func (ROFS) Capabilities() billy.Capability { 25 | return billy.ReadCapability | billy.SeekCapability 26 | } 27 | 28 | func main() { 29 | listener, err := net.Listen("tcp", ":0") 30 | if err != nil { 31 | fmt.Printf("Failed to listen: %v\n", err) 32 | return 33 | } 34 | fmt.Printf("Server running at %s\n", listener.Addr()) 35 | 36 | mem := memfs.New() 37 | f, err := mem.Create("hello.txt") 38 | if err != nil { 39 | fmt.Printf("Failed to create file: %v\n", err) 40 | return 41 | } 42 | _, _ = f.Write([]byte("hello world")) 43 | _ = f.Close() 44 | 45 | handler := nfshelper.NewNullAuthHandler(ROFS{mem}) 46 | cacheHelper := nfshelper.NewCachingHandler(handler, 1024) 47 | fmt.Printf("%v", nfs.Serve(listener, cacheHelper)) 48 | } 49 | -------------------------------------------------------------------------------- /example/osnfs/changeos.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "time" 6 | 7 | "github.com/go-git/go-billy/v5" 8 | ) 9 | 10 | // NewChangeOSFS wraps billy osfs to add the change interface 11 | func NewChangeOSFS(fs billy.Filesystem) billy.Filesystem { 12 | return COS{fs} 13 | } 14 | 15 | // COS or OSFS + Change wraps a billy.FS to not fail the `Change` interface. 16 | type COS struct { 17 | billy.Filesystem 18 | } 19 | 20 | // Chmod changes mode 21 | func (fs COS) Chmod(name string, mode os.FileMode) error { 22 | return os.Chmod(fs.Join(fs.Root(), name), mode) 23 | } 24 | 25 | // Lchown changes ownership 26 | func (fs COS) Lchown(name string, uid, gid int) error { 27 | return os.Lchown(fs.Join(fs.Root(), name), uid, gid) 28 | } 29 | 30 | // Chown changes ownership 31 | func (fs COS) Chown(name string, uid, gid int) error { 32 | return os.Chown(fs.Join(fs.Root(), name), uid, gid) 33 | } 34 | 35 | // Chtimes changes access time 36 | func (fs COS) Chtimes(name string, atime time.Time, mtime time.Time) error { 37 | return os.Chtimes(fs.Join(fs.Root(), name), atime, mtime) 38 | } 39 | -------------------------------------------------------------------------------- /example/osnfs/changeos_unix.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || dragonfly || freebsd || linux || nacl || netbsd || openbsd || solaris 2 | 3 | package main 4 | 5 | import ( 6 | "golang.org/x/sys/unix" 7 | ) 8 | 9 | func (fs COS) Mknod(path string, mode uint32, major uint32, minor uint32) error { 10 | dev := unix.Mkdev(major, minor) 11 | return unix.Mknod(fs.Join(fs.Root(), path), mode, int(dev)) 12 | } 13 | 14 | func (fs COS) Mkfifo(path string, mode uint32) error { 15 | return unix.Mkfifo(fs.Join(fs.Root(), path), mode) 16 | } 17 | 18 | func (fs COS) Link(path string, link string) error { 19 | return unix.Link(fs.Join(fs.Root(), path), link) 20 | } 21 | 22 | func (fs COS) Socket(path string) error { 23 | fd, err := unix.Socket(unix.AF_UNIX, unix.SOCK_STREAM, 0) 24 | if err != nil { 25 | return err 26 | } 27 | return unix.Bind(fd, &unix.SockaddrUnix{Name: fs.Join(fs.Root(), path)}) 28 | } 29 | -------------------------------------------------------------------------------- /example/osnfs/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "os" 7 | 8 | osfs "github.com/go-git/go-billy/v5/osfs" 9 | nfs "github.com/willscott/go-nfs" 10 | nfshelper "github.com/willscott/go-nfs/helpers" 11 | ) 12 | 13 | func main() { 14 | port := "" 15 | if len(os.Args) < 2 { 16 | fmt.Printf("Usage: osnfs [port]\n") 17 | return 18 | } else if len(os.Args) == 3 { 19 | port = os.Args[2] 20 | } 21 | 22 | listener, err := net.Listen("tcp", ":"+port) 23 | if err != nil { 24 | fmt.Printf("Failed to listen: %v\n", err) 25 | return 26 | } 27 | fmt.Printf("osnfs server running at %s\n", listener.Addr()) 28 | 29 | bfs := osfs.New(os.Args[1]) 30 | bfsPlusChange := NewChangeOSFS(bfs) 31 | 32 | handler := nfshelper.NewNullAuthHandler(bfsPlusChange) 33 | cacheHelper := nfshelper.NewCachingHandler(handler, 1024) 34 | fmt.Printf("%v", nfs.Serve(listener, cacheHelper)) 35 | } 36 | -------------------------------------------------------------------------------- /example/osview/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "os" 7 | 8 | "github.com/willscott/memphis" 9 | 10 | nfs "github.com/willscott/go-nfs" 11 | nfshelper "github.com/willscott/go-nfs/helpers" 12 | ) 13 | 14 | func main() { 15 | port := "" 16 | if len(os.Args) < 2 { 17 | fmt.Printf("Usage: osview [port]\n") 18 | return 19 | } else if len(os.Args) == 3 { 20 | port = os.Args[2] 21 | } 22 | 23 | listener, err := net.Listen("tcp", ":"+port) 24 | if err != nil { 25 | fmt.Printf("Failed to listen: %v\n", err) 26 | return 27 | } 28 | fmt.Printf("Server running at %s\n", listener.Addr()) 29 | 30 | fs := memphis.FromOS(os.Args[1]) 31 | bfs := fs.AsBillyFS(0, 0) 32 | 33 | handler := nfshelper.NewNullAuthHandler(bfs) 34 | cacheHelper := nfshelper.NewCachingHandler(handler, 1024) 35 | fmt.Printf("%v", nfs.Serve(listener, cacheHelper)) 36 | } 37 | -------------------------------------------------------------------------------- /file.go: -------------------------------------------------------------------------------- 1 | package nfs 2 | 3 | import ( 4 | "errors" 5 | "hash/fnv" 6 | "io" 7 | "math" 8 | "os" 9 | "time" 10 | 11 | "github.com/go-git/go-billy/v5" 12 | "github.com/willscott/go-nfs-client/nfs/xdr" 13 | "github.com/willscott/go-nfs/file" 14 | ) 15 | 16 | // FileAttribute holds metadata about a filesystem object 17 | type FileAttribute struct { 18 | Type FileType 19 | FileMode uint32 20 | Nlink uint32 21 | UID uint32 22 | GID uint32 23 | Filesize uint64 24 | Used uint64 25 | SpecData [2]uint32 26 | FSID uint64 27 | Fileid uint64 28 | Atime, Mtime, Ctime FileTime 29 | } 30 | 31 | // FileType represents a NFS File Type 32 | type FileType uint32 33 | 34 | // Enumeration of NFS FileTypes 35 | const ( 36 | FileTypeRegular FileType = iota + 1 37 | FileTypeDirectory 38 | FileTypeBlock 39 | FileTypeCharacter 40 | FileTypeLink 41 | FileTypeSocket 42 | FileTypeFIFO 43 | ) 44 | 45 | func (f FileType) String() string { 46 | switch f { 47 | case FileTypeRegular: 48 | return "Regular" 49 | case FileTypeDirectory: 50 | return "Directory" 51 | case FileTypeBlock: 52 | return "Block Device" 53 | case FileTypeCharacter: 54 | return "Character Device" 55 | case FileTypeLink: 56 | return "Symbolic Link" 57 | case FileTypeSocket: 58 | return "Socket" 59 | case FileTypeFIFO: 60 | return "FIFO" 61 | default: 62 | return "Unknown" 63 | } 64 | } 65 | 66 | // Mode provides the OS interpreted mode of the file attributes 67 | func (f *FileAttribute) Mode() os.FileMode { 68 | return os.FileMode(f.FileMode) 69 | } 70 | 71 | // FileCacheAttribute is the subset of FileAttribute used by 72 | // wcc_attr 73 | type FileCacheAttribute struct { 74 | Filesize uint64 75 | Mtime, Ctime FileTime 76 | } 77 | 78 | // AsCache provides the wcc view of the file attributes 79 | func (f FileAttribute) AsCache() *FileCacheAttribute { 80 | wcc := FileCacheAttribute{ 81 | Filesize: f.Filesize, 82 | Mtime: f.Mtime, 83 | Ctime: f.Ctime, 84 | } 85 | return &wcc 86 | } 87 | 88 | // ToFileAttribute creates an NFS fattr3 struct from an OS.FileInfo 89 | func ToFileAttribute(info os.FileInfo, filePath string) *FileAttribute { 90 | f := FileAttribute{} 91 | 92 | m := info.Mode() 93 | f.FileMode = uint32(m) 94 | if info.IsDir() { 95 | f.Type = FileTypeDirectory 96 | } else if m&os.ModeSymlink != 0 { 97 | f.Type = FileTypeLink 98 | } else if m&os.ModeCharDevice != 0 { 99 | f.Type = FileTypeCharacter 100 | } else if m&os.ModeDevice != 0 { 101 | f.Type = FileTypeBlock 102 | } else if m&os.ModeSocket != 0 { 103 | f.Type = FileTypeSocket 104 | } else if m&os.ModeNamedPipe != 0 { 105 | f.Type = FileTypeFIFO 106 | } else { 107 | f.Type = FileTypeRegular 108 | } 109 | // The number of hard links to the file. 110 | f.Nlink = 1 111 | 112 | if a := file.GetInfo(info); a != nil { 113 | f.Nlink = a.Nlink 114 | f.UID = a.UID 115 | f.GID = a.GID 116 | f.SpecData = [2]uint32{a.Major, a.Minor} 117 | f.Fileid = a.Fileid 118 | } else { 119 | hasher := fnv.New64() 120 | _, _ = hasher.Write([]byte(filePath)) 121 | f.Fileid = hasher.Sum64() 122 | } 123 | 124 | f.Filesize = uint64(info.Size()) 125 | f.Used = uint64(info.Size()) 126 | f.Atime = ToNFSTime(info.ModTime()) 127 | f.Mtime = f.Atime 128 | f.Ctime = f.Atime 129 | return &f 130 | } 131 | 132 | // tryStat attempts to create a FileAttribute from a path. 133 | func tryStat(fs billy.Filesystem, path []string) *FileAttribute { 134 | fullPath := fs.Join(path...) 135 | attrs, err := fs.Lstat(fullPath) 136 | if err != nil || attrs == nil { 137 | Log.Errorf("err loading attrs for %s: %v", fs.Join(path...), err) 138 | return nil 139 | } 140 | return ToFileAttribute(attrs, fullPath) 141 | } 142 | 143 | // WriteWcc writes the `wcc_data` representation of an object. 144 | func WriteWcc(writer io.Writer, pre *FileCacheAttribute, post *FileAttribute) error { 145 | if pre == nil { 146 | if err := xdr.Write(writer, uint32(0)); err != nil { 147 | return err 148 | } 149 | } else { 150 | if err := xdr.Write(writer, uint32(1)); err != nil { 151 | return err 152 | } 153 | if err := xdr.Write(writer, *pre); err != nil { 154 | return err 155 | } 156 | } 157 | if post == nil { 158 | if err := xdr.Write(writer, uint32(0)); err != nil { 159 | return err 160 | } 161 | } else { 162 | if err := xdr.Write(writer, uint32(1)); err != nil { 163 | return err 164 | } 165 | if err := xdr.Write(writer, *post); err != nil { 166 | return err 167 | } 168 | } 169 | return nil 170 | } 171 | 172 | // WritePostOpAttrs writes the `post_op_attr` representation of a files attributes 173 | func WritePostOpAttrs(writer io.Writer, post *FileAttribute) error { 174 | if post == nil { 175 | if err := xdr.Write(writer, uint32(0)); err != nil { 176 | return err 177 | } 178 | } else { 179 | if err := xdr.Write(writer, uint32(1)); err != nil { 180 | return err 181 | } 182 | if err := xdr.Write(writer, *post); err != nil { 183 | return err 184 | } 185 | } 186 | return nil 187 | } 188 | 189 | // SetFileAttributes represents a command to update some metadata 190 | // about a file. 191 | type SetFileAttributes struct { 192 | SetMode *uint32 193 | SetUID *uint32 194 | SetGID *uint32 195 | SetSize *uint64 196 | SetAtime *time.Time 197 | SetMtime *time.Time 198 | } 199 | 200 | // Apply uses a `Change` implementation to set defined attributes on a 201 | // provided file. 202 | func (s *SetFileAttributes) Apply(changer billy.Change, fs billy.Filesystem, file string) error { 203 | curOS, err := fs.Lstat(file) 204 | if errors.Is(err, os.ErrNotExist) { 205 | return &NFSStatusError{NFSStatusNoEnt, os.ErrNotExist} 206 | } else if errors.Is(err, os.ErrPermission) { 207 | return &NFSStatusError{NFSStatusAccess, os.ErrPermission} 208 | } else if err != nil { 209 | return nil 210 | } 211 | curr := ToFileAttribute(curOS, file) 212 | 213 | if s.SetMode != nil { 214 | mode := os.FileMode(*s.SetMode) & os.ModePerm 215 | if mode != curr.Mode().Perm() { 216 | if changer == nil { 217 | return &NFSStatusError{NFSStatusNotSupp, os.ErrPermission} 218 | } 219 | if err := changer.Chmod(file, mode); err != nil { 220 | if errors.Is(err, os.ErrPermission) { 221 | return &NFSStatusError{NFSStatusAccess, os.ErrPermission} 222 | } 223 | return err 224 | } 225 | } 226 | } 227 | if s.SetUID != nil || s.SetGID != nil { 228 | euid := curr.UID 229 | if s.SetUID != nil { 230 | euid = *s.SetUID 231 | } 232 | egid := curr.GID 233 | if s.SetGID != nil { 234 | egid = *s.SetGID 235 | } 236 | if euid != curr.UID || egid != curr.GID { 237 | if changer == nil { 238 | return &NFSStatusError{NFSStatusNotSupp, os.ErrPermission} 239 | } 240 | if err := changer.Lchown(file, int(euid), int(egid)); err != nil { 241 | if errors.Is(err, os.ErrPermission) { 242 | return &NFSStatusError{NFSStatusAccess, os.ErrPermission} 243 | } 244 | return err 245 | } 246 | } 247 | } 248 | if s.SetSize != nil { 249 | if curr.Mode()&os.ModeSymlink != 0 { 250 | return &NFSStatusError{NFSStatusNotSupp, os.ErrInvalid} 251 | } 252 | fp, err := fs.OpenFile(file, os.O_WRONLY|os.O_EXCL, 0) 253 | if errors.Is(err, os.ErrPermission) { 254 | return &NFSStatusError{NFSStatusAccess, err} 255 | } else if err != nil { 256 | return err 257 | } 258 | if *s.SetSize > math.MaxInt64 { 259 | return &NFSStatusError{NFSStatusInval, os.ErrInvalid} 260 | } 261 | if err := fp.Truncate(int64(*s.SetSize)); err != nil { 262 | return err 263 | } 264 | if err := fp.Close(); err != nil { 265 | return err 266 | } 267 | } 268 | 269 | if s.SetAtime != nil || s.SetMtime != nil { 270 | atime := curr.Atime.Native() 271 | if s.SetAtime != nil { 272 | atime = s.SetAtime 273 | } 274 | mtime := curr.Mtime.Native() 275 | if s.SetMtime != nil { 276 | mtime = s.SetMtime 277 | } 278 | if atime != curr.Atime.Native() || mtime != curr.Mtime.Native() { 279 | if changer == nil { 280 | return &NFSStatusError{NFSStatusNotSupp, os.ErrPermission} 281 | } 282 | if err := changer.Chtimes(file, *atime, *mtime); err != nil { 283 | if errors.Is(err, os.ErrPermission) { 284 | return &NFSStatusError{NFSStatusAccess, err} 285 | } 286 | return err 287 | } 288 | } 289 | } 290 | return nil 291 | } 292 | 293 | // Mode returns a mode if specified or the provided default mode. 294 | func (s *SetFileAttributes) Mode(def os.FileMode) os.FileMode { 295 | if s.SetMode != nil { 296 | return os.FileMode(*s.SetMode) & os.ModePerm 297 | } 298 | return def 299 | } 300 | 301 | // ReadSetFileAttributes reads an sattr3 xdr stream into a go struct. 302 | func ReadSetFileAttributes(r io.Reader) (*SetFileAttributes, error) { 303 | attrs := SetFileAttributes{} 304 | hasMode, err := xdr.ReadUint32(r) 305 | if err != nil { 306 | return nil, err 307 | } 308 | if hasMode != 0 { 309 | mode, err := xdr.ReadUint32(r) 310 | if err != nil { 311 | return nil, err 312 | } 313 | attrs.SetMode = &mode 314 | } 315 | hasUID, err := xdr.ReadUint32(r) 316 | if err != nil { 317 | return nil, err 318 | } 319 | if hasUID != 0 { 320 | uid, err := xdr.ReadUint32(r) 321 | if err != nil { 322 | return nil, err 323 | } 324 | attrs.SetUID = &uid 325 | } 326 | hasGID, err := xdr.ReadUint32(r) 327 | if err != nil { 328 | return nil, err 329 | } 330 | if hasGID != 0 { 331 | gid, err := xdr.ReadUint32(r) 332 | if err != nil { 333 | return nil, err 334 | } 335 | attrs.SetGID = &gid 336 | } 337 | hasSize, err := xdr.ReadUint32(r) 338 | if err != nil { 339 | return nil, err 340 | } 341 | if hasSize != 0 { 342 | var size uint64 343 | attrs.SetSize = &size 344 | if err := xdr.Read(r, &size); err != nil { 345 | return nil, err 346 | } 347 | } 348 | aTime, err := xdr.ReadUint32(r) 349 | if err != nil { 350 | return nil, err 351 | } 352 | if aTime == 1 { 353 | now := time.Now() 354 | attrs.SetAtime = &now 355 | } else if aTime == 2 { 356 | t := FileTime{} 357 | if err := xdr.Read(r, &t); err != nil { 358 | return nil, err 359 | } 360 | attrs.SetAtime = t.Native() 361 | } 362 | mTime, err := xdr.ReadUint32(r) 363 | if err != nil { 364 | return nil, err 365 | } 366 | if mTime == 1 { 367 | now := time.Now() 368 | attrs.SetMtime = &now 369 | } else if mTime == 2 { 370 | t := FileTime{} 371 | if err := xdr.Read(r, &t); err != nil { 372 | return nil, err 373 | } 374 | attrs.SetMtime = t.Native() 375 | } 376 | return &attrs, nil 377 | } 378 | -------------------------------------------------------------------------------- /file/file.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import "os" 4 | 5 | type FileInfo struct { 6 | Nlink uint32 7 | UID uint32 8 | GID uint32 9 | Major uint32 10 | Minor uint32 11 | Fileid uint64 12 | } 13 | 14 | // GetInfo extracts some non-standardized items from the result of a Stat call. 15 | func GetInfo(fi os.FileInfo) *FileInfo { 16 | sys := fi.Sys() 17 | switch v := sys.(type) { 18 | case FileInfo: 19 | return &v 20 | case *FileInfo: 21 | return v 22 | default: 23 | return getOSFileInfo(fi) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /file/file_other.go: -------------------------------------------------------------------------------- 1 | //go:build !windows && !darwin && !dragonfly && !freebsd && !linux && !nacl && !netbsd && !openbsd && !solaris && !wasm 2 | 3 | package file 4 | 5 | import ( 6 | "os" 7 | ) 8 | 9 | func getOSFileInfo(_ os.FileInfo) *FileInfo { 10 | return nil 11 | } 12 | -------------------------------------------------------------------------------- /file/file_unix.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || dragonfly || freebsd || linux || nacl || netbsd || openbsd || solaris 2 | 3 | package file 4 | 5 | import ( 6 | "os" 7 | "syscall" 8 | 9 | "golang.org/x/sys/unix" 10 | ) 11 | 12 | func getOSFileInfo(info os.FileInfo) *FileInfo { 13 | fi := &FileInfo{} 14 | if s, ok := info.Sys().(*syscall.Stat_t); ok { 15 | fi.Nlink = uint32(s.Nlink) 16 | fi.UID = s.Uid 17 | fi.GID = s.Gid 18 | fi.Major = unix.Major(uint64(s.Rdev)) 19 | fi.Minor = unix.Minor(uint64(s.Rdev)) 20 | fi.Fileid = s.Ino 21 | return fi 22 | } 23 | return nil 24 | } 25 | -------------------------------------------------------------------------------- /file/file_wasm.go: -------------------------------------------------------------------------------- 1 | //go:build wasm 2 | 3 | package file 4 | 5 | import ( 6 | "os" 7 | "syscall" 8 | ) 9 | 10 | func getOSFileInfo(info os.FileInfo) *FileInfo { 11 | fi := &FileInfo{} 12 | if s, ok := info.Sys().(*syscall.Stat_t); ok { 13 | fi.Nlink = uint32(s.Nlink) 14 | fi.UID = s.Uid 15 | fi.GID = s.Gid 16 | fi.Fileid = s.Ino 17 | return fi 18 | } 19 | return nil 20 | } 21 | -------------------------------------------------------------------------------- /file/file_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package file 4 | 5 | import "os" 6 | 7 | func getOSFileInfo(info os.FileInfo) *FileInfo { 8 | // https://godoc.org/golang.org/x/sys/windows#GetFileInformationByHandle 9 | // can be potentially used to populate Nlink 10 | 11 | return nil 12 | } 13 | -------------------------------------------------------------------------------- /filesystem.go: -------------------------------------------------------------------------------- 1 | package nfs 2 | 3 | import "time" 4 | 5 | // FSStat returns metadata about a file system 6 | type FSStat struct { 7 | TotalSize uint64 8 | FreeSize uint64 9 | AvailableSize uint64 10 | TotalFiles uint64 11 | FreeFiles uint64 12 | AvailableFiles uint64 13 | // CacheHint is called "invarsec" in the nfs standard 14 | CacheHint time.Duration 15 | } 16 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/willscott/go-nfs 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/go-git/go-billy/v5 v5.6.0 7 | github.com/google/uuid v1.6.0 8 | github.com/hashicorp/golang-lru/v2 v2.0.7 9 | github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93 10 | github.com/willscott/go-nfs-client v0.0.0-20240104095149-b44639837b00 11 | github.com/willscott/memphis v0.0.0-20241203204924-a148a489d367 12 | golang.org/x/sys v0.24.0 13 | ) 14 | 15 | require ( 16 | github.com/cyphar/filepath-securejoin v0.2.5 // indirect 17 | github.com/polydawn/go-timeless-api v0.0.0-20220821201550-b93919e12c56 // indirect 18 | github.com/polydawn/refmt v0.0.0-20201211092308-30ac6d18308e // indirect 19 | github.com/polydawn/rio v0.0.0-20220823181337-7c31ad9831a4 // indirect 20 | github.com/warpfork/go-errcat v0.0.0-20180917083543-335044ffc86e // indirect 21 | ) 22 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cyphar/filepath-securejoin v0.2.5 h1:6iR5tXJ/e6tJZzzdMc1km3Sa7RRIVBKAK32O2s7AYfo= 2 | github.com/cyphar/filepath-securejoin v0.2.5/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/go-git/go-billy/v5 v5.6.0 h1:w2hPNtoehvJIxR00Vb4xX94qHQi/ApZfX+nBE2Cjio8= 5 | github.com/go-git/go-billy/v5 v5.6.0/go.mod h1:sFDq7xD3fn3E0GOwUSZqHo9lrkmx8xJhA0ZrfvjBRGM= 6 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 7 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 8 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 9 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 10 | github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= 11 | github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= 12 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 13 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 14 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 15 | github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= 16 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 17 | github.com/polydawn/go-timeless-api v0.0.0-20220821201550-b93919e12c56 h1:LQ103HjiN76aqIxnQNgdZ+7NveuKd45+Q+TYGJVVsyw= 18 | github.com/polydawn/go-timeless-api v0.0.0-20220821201550-b93919e12c56/go.mod h1:OAK6p/pJUakz6jQ+HlSw16gVMnuohxqJFGoypUYyr4w= 19 | github.com/polydawn/refmt v0.0.0-20201211092308-30ac6d18308e h1:ZOcivgkkFRnjfoTcGsDq3UQYiBmekwLA+qg0OjyB/ls= 20 | github.com/polydawn/refmt v0.0.0-20201211092308-30ac6d18308e/go.mod h1:uIp+gprXxxrWSjjklXD+mN4wed/tMfjMMmN/9+JsA9o= 21 | github.com/polydawn/rio v0.0.0-20220823181337-7c31ad9831a4 h1:SNhgcsCNGEqz7Tp46YHEvcjF1s5x+ZGWcVzFoghkuMA= 22 | github.com/polydawn/rio v0.0.0-20220823181337-7c31ad9831a4/go.mod h1:fZ8OGW5CVjZHyQeNs8QH3X3tUxrPcx1jxHSl2z6Xv00= 23 | github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93 h1:UVArwN/wkKjMVhh2EQGC0tEc1+FqiLlvYXY5mQ2f8Wg= 24 | github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93/go.mod h1:Nfe4efndBz4TibWycNE+lqyJZiMX4ycx+QKV8Ta0f/o= 25 | github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= 26 | github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= 27 | github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= 28 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 29 | github.com/warpfork/go-errcat v0.0.0-20180917083543-335044ffc86e h1:FIB2fi7XJGHIdf5rWNsfFQqatIKxutT45G+wNuMQNgs= 30 | github.com/warpfork/go-errcat v0.0.0-20180917083543-335044ffc86e/go.mod h1:/qe02xr3jvTUz8u/PV0FHGpP8t96OQNP7U9BJMwMLEw= 31 | github.com/warpfork/go-wish v0.0.0-20200122115046-b9ea61034e4a h1:G++j5e0OC488te356JvdhaM8YS6nMsjLAYF7JxCv07w= 32 | github.com/willscott/go-nfs-client v0.0.0-20240104095149-b44639837b00 h1:U0DnHRZFzoIV1oFEZczg5XyPut9yxk9jjtax/9Bxr/o= 33 | github.com/willscott/go-nfs-client v0.0.0-20240104095149-b44639837b00/go.mod h1:Tq++Lr/FgiS3X48q5FETemXiSLGuYMQT2sPjYNPJSwA= 34 | github.com/willscott/memphis v0.0.0-20241203204924-a148a489d367 h1:A9hsyc7kKeultwdUhS99FVq2S4xT6QVZqOEptPGjHpM= 35 | github.com/willscott/memphis v0.0.0-20241203204924-a148a489d367/go.mod h1:mAQkn9EwN7WZdbH1DnV+9Nmr3oMjPbG4a0zDM2yI2iA= 36 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= 37 | golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= 38 | golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= 39 | golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 40 | golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= 41 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 42 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 43 | -------------------------------------------------------------------------------- /handler.go: -------------------------------------------------------------------------------- 1 | package nfs 2 | 3 | import ( 4 | "context" 5 | "io/fs" 6 | "net" 7 | 8 | billy "github.com/go-git/go-billy/v5" 9 | ) 10 | 11 | // Handler represents the interface of the file system / vfs being exposed over NFS 12 | type Handler interface { 13 | // Required methods 14 | 15 | Mount(context.Context, net.Conn, MountRequest) (MountStatus, billy.Filesystem, []AuthFlavor) 16 | 17 | // Change can return 'nil' if filesystem is read-only 18 | // If the returned value can be cast to `UnixChange`, mknod and link RPCs will be available. 19 | Change(billy.Filesystem) billy.Change 20 | 21 | // Optional methods - generic helpers or trivial implementations can be sufficient depending on use case. 22 | 23 | // Fill in information about a file system's free space. 24 | FSStat(context.Context, billy.Filesystem, *FSStat) error 25 | 26 | // represent file objects as opaque references 27 | // Can be safely implemented via helpers/cachinghandler. 28 | ToHandle(fs billy.Filesystem, path []string) []byte 29 | FromHandle(fh []byte) (billy.Filesystem, []string, error) 30 | InvalidateHandle(billy.Filesystem, []byte) error 31 | 32 | // How many handles can be safely maintained by the handler. 33 | HandleLimit() int 34 | } 35 | 36 | // UnixChange extends the billy `Change` interface with support for special files. 37 | type UnixChange interface { 38 | billy.Change 39 | Mknod(path string, mode uint32, major uint32, minor uint32) error 40 | Mkfifo(path string, mode uint32) error 41 | Socket(path string) error 42 | Link(path string, link string) error 43 | } 44 | 45 | // CachingHandler represents the optional caching work that a user may wish to over-ride with 46 | // their own implementations, but which can be otherwise provided through defaults. 47 | type CachingHandler interface { 48 | VerifierFor(path string, contents []fs.FileInfo) uint64 49 | 50 | // fs.FileInfo needs to be sorted by Name(), nil in case of a cache-miss 51 | DataForVerifier(path string, verifier uint64) []fs.FileInfo 52 | } 53 | -------------------------------------------------------------------------------- /helpers/cachinghandler.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/binary" 6 | "io/fs" 7 | "reflect" 8 | 9 | "github.com/willscott/go-nfs" 10 | 11 | "github.com/go-git/go-billy/v5" 12 | "github.com/google/uuid" 13 | lru "github.com/hashicorp/golang-lru/v2" 14 | ) 15 | 16 | // NewCachingHandler wraps a handler to provide a basic to/from-file handle cache. 17 | func NewCachingHandler(h nfs.Handler, limit int) nfs.Handler { 18 | return NewCachingHandlerWithVerifierLimit(h, limit, limit) 19 | } 20 | 21 | // NewCachingHandlerWithVerifierLimit provides a basic to/from-file handle cache that can be tuned with a smaller cache of active directory listings. 22 | func NewCachingHandlerWithVerifierLimit(h nfs.Handler, limit int, verifierLimit int) nfs.Handler { 23 | if limit < 2 || verifierLimit < 2 { 24 | nfs.Log.Warnf("Caching handler created with insufficient cache to support directory listing", "size", limit, "verifiers", verifierLimit) 25 | } 26 | cache, _ := lru.New[uuid.UUID, entry](limit) 27 | reverseCache := make(map[string][]uuid.UUID) 28 | verifiers, _ := lru.New[uint64, verifier](verifierLimit) 29 | return &CachingHandler{ 30 | Handler: h, 31 | activeHandles: cache, 32 | reverseHandles: reverseCache, 33 | activeVerifiers: verifiers, 34 | cacheLimit: limit, 35 | } 36 | } 37 | 38 | // CachingHandler implements to/from handle via an LRU cache. 39 | type CachingHandler struct { 40 | nfs.Handler 41 | activeHandles *lru.Cache[uuid.UUID, entry] 42 | reverseHandles map[string][]uuid.UUID 43 | activeVerifiers *lru.Cache[uint64, verifier] 44 | cacheLimit int 45 | } 46 | 47 | type entry struct { 48 | f billy.Filesystem 49 | p []string 50 | } 51 | 52 | // ToHandle takes a file and represents it with an opaque handle to reference it. 53 | // In stateless nfs (when it's serving a unix fs) this can be the device + inode 54 | // but we can generalize with a stateful local cache of handed out IDs. 55 | func (c *CachingHandler) ToHandle(f billy.Filesystem, path []string) []byte { 56 | joinedPath := f.Join(path...) 57 | 58 | if handle := c.searchReverseCache(f, joinedPath); handle != nil { 59 | return handle 60 | } 61 | 62 | id := uuid.New() 63 | 64 | newPath := make([]string, len(path)) 65 | 66 | copy(newPath, path) 67 | evictedKey, evictedPath, ok := c.activeHandles.GetOldest() 68 | if evicted := c.activeHandles.Add(id, entry{f, newPath}); evicted && ok { 69 | rk := evictedPath.f.Join(evictedPath.p...) 70 | c.evictReverseCache(rk, evictedKey) 71 | } 72 | 73 | if _, ok := c.reverseHandles[joinedPath]; !ok { 74 | c.reverseHandles[joinedPath] = []uuid.UUID{} 75 | } 76 | c.reverseHandles[joinedPath] = append(c.reverseHandles[joinedPath], id) 77 | b, _ := id.MarshalBinary() 78 | 79 | return b 80 | } 81 | 82 | // FromHandle converts from an opaque handle to the file it represents 83 | func (c *CachingHandler) FromHandle(fh []byte) (billy.Filesystem, []string, error) { 84 | id, err := uuid.FromBytes(fh) 85 | if err != nil { 86 | return nil, []string{}, err 87 | } 88 | 89 | if f, ok := c.activeHandles.Get(id); ok { 90 | for _, k := range c.activeHandles.Keys() { 91 | candidate, _ := c.activeHandles.Peek(k) 92 | if hasPrefix(f.p, candidate.p) { 93 | _, _ = c.activeHandles.Get(k) 94 | } 95 | } 96 | if ok { 97 | newP := make([]string, len(f.p)) 98 | copy(newP, f.p) 99 | return f.f, newP, nil 100 | } 101 | } 102 | return nil, []string{}, &nfs.NFSStatusError{NFSStatus: nfs.NFSStatusStale} 103 | } 104 | 105 | func (c *CachingHandler) searchReverseCache(f billy.Filesystem, path string) []byte { 106 | uuids, exists := c.reverseHandles[path] 107 | 108 | if !exists { 109 | return nil 110 | } 111 | 112 | for _, id := range uuids { 113 | if candidate, ok := c.activeHandles.Get(id); ok { 114 | if reflect.DeepEqual(candidate.f, f) { 115 | return id[:] 116 | } 117 | } 118 | } 119 | 120 | return nil 121 | } 122 | 123 | func (c *CachingHandler) evictReverseCache(path string, handle uuid.UUID) { 124 | uuids, exists := c.reverseHandles[path] 125 | 126 | if !exists { 127 | return 128 | } 129 | for i, u := range uuids { 130 | if u == handle { 131 | uuids = append(uuids[:i], uuids[i+1:]...) 132 | c.reverseHandles[path] = uuids 133 | return 134 | } 135 | } 136 | } 137 | 138 | func (c *CachingHandler) InvalidateHandle(fs billy.Filesystem, handle []byte) error { 139 | //Remove from cache 140 | id, _ := uuid.FromBytes(handle) 141 | entry, ok := c.activeHandles.Get(id) 142 | if ok { 143 | rk := entry.f.Join(entry.p...) 144 | c.evictReverseCache(rk, id) 145 | } 146 | c.activeHandles.Remove(id) 147 | return nil 148 | } 149 | 150 | // HandleLimit exports how many file handles can be safely stored by this cache. 151 | func (c *CachingHandler) HandleLimit() int { 152 | return c.cacheLimit 153 | } 154 | 155 | func hasPrefix(path, prefix []string) bool { 156 | if len(prefix) > len(path) { 157 | return false 158 | } 159 | for i, e := range prefix { 160 | if path[i] != e { 161 | return false 162 | } 163 | } 164 | return true 165 | } 166 | 167 | type verifier struct { 168 | path string 169 | contents []fs.FileInfo 170 | } 171 | 172 | func hashPathAndContents(path string, contents []fs.FileInfo) uint64 { 173 | //calculate a cookie-verifier. 174 | vHash := sha256.New() 175 | 176 | // Add the path to avoid collisions of directories with the same content 177 | vHash.Write(binary.BigEndian.AppendUint64([]byte{}, uint64(len(path)))) 178 | vHash.Write([]byte(path)) 179 | 180 | for _, c := range contents { 181 | vHash.Write([]byte(c.Name())) // Never fails according to the docs 182 | } 183 | 184 | verify := vHash.Sum(nil)[0:8] 185 | return binary.BigEndian.Uint64(verify) 186 | } 187 | 188 | func (c *CachingHandler) VerifierFor(path string, contents []fs.FileInfo) uint64 { 189 | id := hashPathAndContents(path, contents) 190 | c.activeVerifiers.Add(id, verifier{path, contents}) 191 | return id 192 | } 193 | 194 | func (c *CachingHandler) DataForVerifier(path string, id uint64) []fs.FileInfo { 195 | if cache, ok := c.activeVerifiers.Get(id); ok { 196 | return cache.contents 197 | } 198 | return nil 199 | } 200 | -------------------------------------------------------------------------------- /helpers/memfs/memfs.go: -------------------------------------------------------------------------------- 1 | // Package memfs is a variant of "github.com/go-git/go-billy/v5/memfs" with 2 | // stable mtimes for items. 3 | package memfs 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "io" 9 | "os" 10 | "path/filepath" 11 | "sort" 12 | "strings" 13 | "syscall" 14 | "time" 15 | 16 | "github.com/go-git/go-billy/v5" 17 | "github.com/go-git/go-billy/v5/helper/chroot" 18 | "github.com/go-git/go-billy/v5/util" 19 | ) 20 | 21 | const separator = filepath.Separator 22 | 23 | // Memory a very convenient filesystem based on memory files 24 | type Memory struct { 25 | s *storage 26 | } 27 | 28 | // New returns a new Memory filesystem. 29 | func New() billy.Filesystem { 30 | fs := &Memory{s: newStorage()} 31 | return chroot.New(fs, string(separator)) 32 | } 33 | 34 | func (fs *Memory) Create(filename string) (billy.File, error) { 35 | return fs.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) 36 | } 37 | 38 | func (fs *Memory) Open(filename string) (billy.File, error) { 39 | return fs.OpenFile(filename, os.O_RDONLY, 0) 40 | } 41 | 42 | func (fs *Memory) OpenFile(filename string, flag int, perm os.FileMode) (billy.File, error) { 43 | f, has := fs.s.Get(filename) 44 | if !has { 45 | if !isCreate(flag) { 46 | return nil, os.ErrNotExist 47 | } 48 | 49 | var err error 50 | f, err = fs.s.New(filename, perm, flag) 51 | if err != nil { 52 | return nil, err 53 | } 54 | } else { 55 | if isExclusive(flag) { 56 | return nil, os.ErrExist 57 | } 58 | 59 | if target, isLink := fs.resolveLink(filename, f); isLink { 60 | return fs.OpenFile(target, flag, perm) 61 | } 62 | } 63 | 64 | if f.mode.IsDir() { 65 | return nil, fmt.Errorf("cannot open directory: %s", filename) 66 | } 67 | 68 | return f.Duplicate(filename, perm, flag), nil 69 | } 70 | 71 | func (fs *Memory) resolveLink(fullpath string, f *file) (target string, isLink bool) { 72 | if !isSymlink(f.mode) { 73 | return fullpath, false 74 | } 75 | 76 | target = string(f.content.bytes) 77 | if !isAbs(target) { 78 | target = fs.Join(filepath.Dir(fullpath), target) 79 | } 80 | 81 | return target, true 82 | } 83 | 84 | // On Windows OS, IsAbs validates if a path is valid based on if stars with a 85 | // unit (eg.: `C:\`) to assert that is absolute, but in this mem implementation 86 | // any path starting by `separator` is also considered absolute. 87 | func isAbs(path string) bool { 88 | return filepath.IsAbs(path) || strings.HasPrefix(path, string(separator)) 89 | } 90 | 91 | func (fs *Memory) Stat(filename string) (os.FileInfo, error) { 92 | f, has := fs.s.Get(filename) 93 | if !has { 94 | return nil, os.ErrNotExist 95 | } 96 | 97 | fi, _ := f.Stat() 98 | 99 | var err error 100 | if target, isLink := fs.resolveLink(filename, f); isLink { 101 | fi, err = fs.Stat(target) 102 | if err != nil { 103 | return nil, err 104 | } 105 | } 106 | 107 | // the name of the file should always the name of the stated file, so we 108 | // overwrite the Stat returned from the storage with it, since the 109 | // filename may belong to a link. 110 | fi.(*fileInfo).name = filepath.Base(filename) 111 | return fi, nil 112 | } 113 | 114 | func (fs *Memory) Lstat(filename string) (os.FileInfo, error) { 115 | f, has := fs.s.Get(filename) 116 | if !has { 117 | return nil, os.ErrNotExist 118 | } 119 | 120 | return f.Stat() 121 | } 122 | 123 | type ByName []os.FileInfo 124 | 125 | func (a ByName) Len() int { return len(a) } 126 | func (a ByName) Less(i, j int) bool { return a[i].Name() < a[j].Name() } 127 | func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 128 | 129 | func (fs *Memory) ReadDir(path string) ([]os.FileInfo, error) { 130 | if f, has := fs.s.Get(path); has { 131 | if target, isLink := fs.resolveLink(path, f); isLink { 132 | return fs.ReadDir(target) 133 | } 134 | } else { 135 | return nil, &os.PathError{Op: "open", Path: path, Err: syscall.ENOENT} 136 | } 137 | 138 | var entries []os.FileInfo 139 | for _, f := range fs.s.Children(path) { 140 | fi, _ := f.Stat() 141 | entries = append(entries, fi) 142 | } 143 | 144 | sort.Sort(ByName(entries)) 145 | 146 | return entries, nil 147 | } 148 | 149 | func (fs *Memory) MkdirAll(path string, perm os.FileMode) error { 150 | _, err := fs.s.New(path, perm|os.ModeDir, 0) 151 | return err 152 | } 153 | 154 | func (fs *Memory) TempFile(dir, prefix string) (billy.File, error) { 155 | return util.TempFile(fs, dir, prefix) 156 | } 157 | 158 | func (fs *Memory) Rename(from, to string) error { 159 | return fs.s.Rename(from, to) 160 | } 161 | 162 | func (fs *Memory) Remove(filename string) error { 163 | return fs.s.Remove(filename) 164 | } 165 | 166 | func (fs *Memory) Join(elem ...string) string { 167 | return filepath.Join(elem...) 168 | } 169 | 170 | func (fs *Memory) Symlink(target, link string) error { 171 | _, err := fs.Stat(link) 172 | if err == nil { 173 | return os.ErrExist 174 | } 175 | 176 | if !os.IsNotExist(err) { 177 | return err 178 | } 179 | 180 | return util.WriteFile(fs, link, []byte(target), 0777|os.ModeSymlink) 181 | } 182 | 183 | func (fs *Memory) Readlink(link string) (string, error) { 184 | f, has := fs.s.Get(link) 185 | if !has { 186 | return "", os.ErrNotExist 187 | } 188 | 189 | if !isSymlink(f.mode) { 190 | return "", &os.PathError{ 191 | Op: "readlink", 192 | Path: link, 193 | Err: fmt.Errorf("not a symlink"), 194 | } 195 | } 196 | 197 | return string(f.content.bytes), nil 198 | } 199 | 200 | // Capabilities implements the Capable interface. 201 | func (fs *Memory) Capabilities() billy.Capability { 202 | return billy.WriteCapability | 203 | billy.ReadCapability | 204 | billy.ReadAndWriteCapability | 205 | billy.SeekCapability | 206 | billy.TruncateCapability 207 | } 208 | 209 | type file struct { 210 | name string 211 | content *content 212 | position int64 213 | flag int 214 | mode os.FileMode 215 | mtime time.Time 216 | 217 | isClosed bool 218 | } 219 | 220 | func (f *file) Name() string { 221 | return f.name 222 | } 223 | 224 | func (f *file) Read(b []byte) (int, error) { 225 | n, err := f.ReadAt(b, f.position) 226 | f.position += int64(n) 227 | 228 | if err == io.EOF && n != 0 { 229 | err = nil 230 | } 231 | 232 | return n, err 233 | } 234 | 235 | func (f *file) ReadAt(b []byte, off int64) (int, error) { 236 | if f.isClosed { 237 | return 0, os.ErrClosed 238 | } 239 | 240 | if !isReadAndWrite(f.flag) && !isReadOnly(f.flag) { 241 | return 0, errors.New("read not supported") 242 | } 243 | 244 | n, err := f.content.ReadAt(b, off) 245 | 246 | return n, err 247 | } 248 | 249 | func (f *file) Seek(offset int64, whence int) (int64, error) { 250 | if f.isClosed { 251 | return 0, os.ErrClosed 252 | } 253 | 254 | switch whence { 255 | case io.SeekCurrent: 256 | f.position += offset 257 | case io.SeekStart: 258 | f.position = offset 259 | case io.SeekEnd: 260 | f.position = int64(f.content.Len()) + offset 261 | } 262 | 263 | return f.position, nil 264 | } 265 | 266 | func (f *file) Write(p []byte) (int, error) { 267 | return f.WriteAt(p, f.position) 268 | } 269 | 270 | func (f *file) WriteAt(p []byte, off int64) (int, error) { 271 | if f.isClosed { 272 | return 0, os.ErrClosed 273 | } 274 | 275 | if !isReadAndWrite(f.flag) && !isWriteOnly(f.flag) { 276 | return 0, errors.New("write not supported") 277 | } 278 | 279 | n, err := f.content.WriteAt(p, off) 280 | f.position = off + int64(n) 281 | f.mtime = time.Now() 282 | 283 | return n, err 284 | } 285 | 286 | func (f *file) Close() error { 287 | if f.isClosed { 288 | return os.ErrClosed 289 | } 290 | 291 | f.isClosed = true 292 | return nil 293 | } 294 | 295 | func (f *file) Truncate(size int64) error { 296 | if size < int64(len(f.content.bytes)) { 297 | f.content.bytes = f.content.bytes[:size] 298 | } else if more := int(size) - len(f.content.bytes); more > 0 { 299 | f.content.bytes = append(f.content.bytes, make([]byte, more)...) 300 | } 301 | f.mtime = time.Now() 302 | 303 | return nil 304 | } 305 | 306 | func (f *file) Duplicate(filename string, mode os.FileMode, flag int) billy.File { 307 | new := &file{ 308 | name: filename, 309 | content: f.content, 310 | mode: mode, 311 | flag: flag, 312 | mtime: time.Now(), 313 | } 314 | 315 | if isTruncate(flag) { 316 | new.content.Truncate() 317 | } 318 | 319 | if isAppend(flag) { 320 | new.position = int64(new.content.Len()) 321 | } 322 | 323 | return new 324 | } 325 | 326 | func (f *file) Stat() (os.FileInfo, error) { 327 | return &fileInfo{ 328 | name: f.Name(), 329 | mode: f.mode, 330 | size: f.content.Len(), 331 | mtime: f.mtime, 332 | }, nil 333 | } 334 | 335 | // Lock is a no-op in memfs. 336 | func (f *file) Lock() error { 337 | return nil 338 | } 339 | 340 | // Unlock is a no-op in memfs. 341 | func (f *file) Unlock() error { 342 | return nil 343 | } 344 | 345 | type fileInfo struct { 346 | name string 347 | size int 348 | mode os.FileMode 349 | mtime time.Time 350 | } 351 | 352 | func (fi *fileInfo) Name() string { 353 | return fi.name 354 | } 355 | 356 | func (fi *fileInfo) Size() int64 { 357 | return int64(fi.size) 358 | } 359 | 360 | func (fi *fileInfo) Mode() os.FileMode { 361 | return fi.mode 362 | } 363 | 364 | func (fi *fileInfo) ModTime() time.Time { 365 | return fi.mtime 366 | } 367 | 368 | func (fi *fileInfo) IsDir() bool { 369 | return fi.mode.IsDir() 370 | } 371 | 372 | func (*fileInfo) Sys() interface{} { 373 | return nil 374 | } 375 | 376 | func (c *content) Truncate() { 377 | c.bytes = make([]byte, 0) 378 | } 379 | 380 | func (c *content) Len() int { 381 | return len(c.bytes) 382 | } 383 | 384 | func isCreate(flag int) bool { 385 | return flag&os.O_CREATE != 0 386 | } 387 | 388 | func isExclusive(flag int) bool { 389 | return flag&os.O_EXCL != 0 390 | } 391 | 392 | func isAppend(flag int) bool { 393 | return flag&os.O_APPEND != 0 394 | } 395 | 396 | func isTruncate(flag int) bool { 397 | return flag&os.O_TRUNC != 0 398 | } 399 | 400 | func isReadAndWrite(flag int) bool { 401 | return flag&os.O_RDWR != 0 402 | } 403 | 404 | func isReadOnly(flag int) bool { 405 | return flag == os.O_RDONLY 406 | } 407 | 408 | func isWriteOnly(flag int) bool { 409 | return flag&os.O_WRONLY != 0 410 | } 411 | 412 | func isSymlink(m os.FileMode) bool { 413 | return m&os.ModeSymlink != 0 414 | } 415 | -------------------------------------------------------------------------------- /helpers/memfs/storage.go: -------------------------------------------------------------------------------- 1 | package memfs 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | "sync" 11 | "time" 12 | ) 13 | 14 | type storage struct { 15 | files map[string]*file 16 | children map[string]map[string]*file 17 | } 18 | 19 | func newStorage() *storage { 20 | return &storage{ 21 | files: make(map[string]*file, 0), 22 | children: make(map[string]map[string]*file, 0), 23 | } 24 | } 25 | 26 | func (s *storage) Has(path string) bool { 27 | path = clean(path) 28 | 29 | _, ok := s.files[path] 30 | return ok 31 | } 32 | 33 | func (s *storage) New(path string, mode os.FileMode, flag int) (*file, error) { 34 | path = clean(path) 35 | if s.Has(path) { 36 | if !s.MustGet(path).mode.IsDir() { 37 | return nil, fmt.Errorf("file already exists %q", path) 38 | } 39 | 40 | return nil, nil 41 | } 42 | 43 | name := filepath.Base(path) 44 | 45 | f := &file{ 46 | name: name, 47 | content: &content{name: name}, 48 | mode: mode, 49 | flag: flag, 50 | mtime: time.Now(), 51 | } 52 | 53 | s.files[path] = f 54 | if err := s.createParent(path, mode, f); err != nil { 55 | return nil, err 56 | } 57 | return f, nil 58 | } 59 | 60 | func (s *storage) createParent(path string, mode os.FileMode, f *file) error { 61 | base := filepath.Dir(path) 62 | base = clean(base) 63 | if f.Name() == string(separator) { 64 | return nil 65 | } 66 | 67 | if _, err := s.New(base, mode.Perm()|os.ModeDir, 0); err != nil { 68 | return err 69 | } 70 | 71 | if _, ok := s.children[base]; !ok { 72 | s.children[base] = make(map[string]*file, 0) 73 | } 74 | 75 | s.children[base][f.Name()] = f 76 | return nil 77 | } 78 | 79 | func (s *storage) Children(path string) []*file { 80 | path = clean(path) 81 | 82 | l := make([]*file, 0) 83 | for _, f := range s.children[path] { 84 | l = append(l, f) 85 | } 86 | 87 | return l 88 | } 89 | 90 | func (s *storage) MustGet(path string) *file { 91 | f, ok := s.Get(path) 92 | if !ok { 93 | panic(fmt.Errorf("couldn't find %q", path)) 94 | } 95 | 96 | return f 97 | } 98 | 99 | func (s *storage) Get(path string) (*file, bool) { 100 | path = clean(path) 101 | if !s.Has(path) { 102 | return nil, false 103 | } 104 | 105 | file, ok := s.files[path] 106 | return file, ok 107 | } 108 | 109 | func (s *storage) Rename(from, to string) error { 110 | from = clean(from) 111 | to = clean(to) 112 | 113 | if !s.Has(from) { 114 | return os.ErrNotExist 115 | } 116 | 117 | move := [][2]string{{from, to}} 118 | 119 | for pathFrom := range s.files { 120 | if pathFrom == from || !strings.HasPrefix(pathFrom, from) { 121 | continue 122 | } 123 | 124 | rel, _ := filepath.Rel(from, pathFrom) 125 | pathTo := filepath.Join(to, rel) 126 | 127 | move = append(move, [2]string{pathFrom, pathTo}) 128 | } 129 | 130 | for _, ops := range move { 131 | from := ops[0] 132 | to := ops[1] 133 | 134 | if err := s.move(from, to); err != nil { 135 | return err 136 | } 137 | } 138 | 139 | return nil 140 | } 141 | 142 | func (s *storage) move(from, to string) error { 143 | s.files[to] = s.files[from] 144 | s.files[to].name = filepath.Base(to) 145 | s.children[to] = s.children[from] 146 | 147 | defer func() { 148 | delete(s.children, from) 149 | delete(s.files, from) 150 | delete(s.children[filepath.Dir(from)], filepath.Base(from)) 151 | }() 152 | 153 | return s.createParent(to, 0644, s.files[to]) 154 | } 155 | 156 | func (s *storage) Remove(path string) error { 157 | path = clean(path) 158 | 159 | f, has := s.Get(path) 160 | if !has { 161 | return os.ErrNotExist 162 | } 163 | 164 | if f.mode.IsDir() && len(s.children[path]) != 0 { 165 | return fmt.Errorf("dir: %s contains files", path) 166 | } 167 | 168 | base, file := filepath.Split(path) 169 | base = filepath.Clean(base) 170 | 171 | delete(s.children[base], file) 172 | delete(s.files, path) 173 | return nil 174 | } 175 | 176 | func clean(path string) string { 177 | return filepath.Clean(filepath.FromSlash(path)) 178 | } 179 | 180 | type content struct { 181 | name string 182 | bytes []byte 183 | 184 | m sync.RWMutex 185 | } 186 | 187 | func (c *content) WriteAt(p []byte, off int64) (int, error) { 188 | if off < 0 { 189 | return 0, &os.PathError{ 190 | Op: "writeat", 191 | Path: c.name, 192 | Err: errors.New("negative offset"), 193 | } 194 | } 195 | 196 | c.m.Lock() 197 | prev := len(c.bytes) 198 | 199 | diff := int(off) - prev 200 | if diff > 0 { 201 | c.bytes = append(c.bytes, make([]byte, diff)...) 202 | } 203 | 204 | c.bytes = append(c.bytes[:off], p...) 205 | if len(c.bytes) < prev { 206 | c.bytes = c.bytes[:prev] 207 | } 208 | c.m.Unlock() 209 | 210 | return len(p), nil 211 | } 212 | 213 | func (c *content) ReadAt(b []byte, off int64) (n int, err error) { 214 | if off < 0 { 215 | return 0, &os.PathError{ 216 | Op: "readat", 217 | Path: c.name, 218 | Err: errors.New("negative offset"), 219 | } 220 | } 221 | 222 | c.m.RLock() 223 | size := int64(len(c.bytes)) 224 | if off >= size { 225 | c.m.RUnlock() 226 | return 0, io.EOF 227 | } 228 | 229 | l := int64(len(b)) 230 | if off+l > size { 231 | l = size - off 232 | } 233 | 234 | btr := c.bytes[off : off+l] 235 | n = copy(b, btr) 236 | 237 | if len(btr) < len(b) { 238 | err = io.EOF 239 | } 240 | c.m.RUnlock() 241 | 242 | return 243 | } 244 | -------------------------------------------------------------------------------- /helpers/nullauthhandler.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "context" 5 | "net" 6 | 7 | "github.com/go-git/go-billy/v5" 8 | "github.com/willscott/go-nfs" 9 | ) 10 | 11 | // NewNullAuthHandler creates a handler for the provided filesystem 12 | func NewNullAuthHandler(fs billy.Filesystem) nfs.Handler { 13 | return &NullAuthHandler{fs} 14 | } 15 | 16 | // NullAuthHandler returns a NFS backing that exposes a given file system in response to all mount requests. 17 | type NullAuthHandler struct { 18 | fs billy.Filesystem 19 | } 20 | 21 | // Mount backs Mount RPC Requests, allowing for access control policies. 22 | func (h *NullAuthHandler) Mount(ctx context.Context, conn net.Conn, req nfs.MountRequest) (status nfs.MountStatus, hndl billy.Filesystem, auths []nfs.AuthFlavor) { 23 | status = nfs.MountStatusOk 24 | hndl = h.fs 25 | auths = []nfs.AuthFlavor{nfs.AuthFlavorNull} 26 | return 27 | } 28 | 29 | // Change provides an interface for updating file attributes. 30 | func (h *NullAuthHandler) Change(fs billy.Filesystem) billy.Change { 31 | if c, ok := h.fs.(billy.Change); ok { 32 | return c 33 | } 34 | return nil 35 | } 36 | 37 | // FSStat provides information about a filesystem. 38 | func (h *NullAuthHandler) FSStat(ctx context.Context, f billy.Filesystem, s *nfs.FSStat) error { 39 | return nil 40 | } 41 | 42 | // ToHandle handled by CachingHandler 43 | func (h *NullAuthHandler) ToHandle(f billy.Filesystem, s []string) []byte { 44 | return []byte{} 45 | } 46 | 47 | // FromHandle handled by CachingHandler 48 | func (h *NullAuthHandler) FromHandle([]byte) (billy.Filesystem, []string, error) { 49 | return nil, []string{}, nil 50 | } 51 | 52 | func (c *NullAuthHandler) InvalidateHandle(billy.Filesystem, []byte) error { 53 | return nil 54 | } 55 | 56 | // HandleLImit handled by cachingHandler 57 | func (h *NullAuthHandler) HandleLimit() int { 58 | return -1 59 | } 60 | -------------------------------------------------------------------------------- /log.go: -------------------------------------------------------------------------------- 1 | package nfs 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | ) 8 | 9 | var ( 10 | Log Logger = &DefaultLogger{} 11 | ) 12 | 13 | type LogLevel int 14 | 15 | const ( 16 | PanicLevel LogLevel = iota 17 | FatalLevel 18 | ErrorLevel 19 | WarnLevel 20 | InfoLevel 21 | DebugLevel 22 | TraceLevel 23 | 24 | panicLevelStr string = "[PANIC] " 25 | fatalLevelStr string = "[FATAL] " 26 | errorLevelStr string = "[ERROR] " 27 | warnLevelStr string = "[WARN] " 28 | infoLevelStr string = "[INFO] " 29 | debugLevelStr string = "[DEBUG] " 30 | traceLevelStr string = "[TRACE] " 31 | ) 32 | 33 | type Logger interface { 34 | SetLevel(level LogLevel) 35 | GetLevel() LogLevel 36 | ParseLevel(level string) (LogLevel, error) 37 | 38 | Panic(args ...interface{}) 39 | Fatal(args ...interface{}) 40 | Error(args ...interface{}) 41 | Warn(args ...interface{}) 42 | Info(args ...interface{}) 43 | Debug(args ...interface{}) 44 | Trace(args ...interface{}) 45 | Print(args ...interface{}) 46 | 47 | Panicf(format string, args ...interface{}) 48 | Fatalf(format string, args ...interface{}) 49 | Errorf(format string, args ...interface{}) 50 | Warnf(format string, args ...interface{}) 51 | Infof(format string, args ...interface{}) 52 | Debugf(format string, args ...interface{}) 53 | Tracef(format string, args ...interface{}) 54 | Printf(format string, args ...interface{}) 55 | } 56 | 57 | type DefaultLogger struct { 58 | Level LogLevel 59 | } 60 | 61 | func SetLogger(logger Logger) { 62 | Log = logger 63 | } 64 | 65 | func init() { 66 | if os.Getenv("LOG_LEVEL") != "" { 67 | if level, err := Log.ParseLevel(os.Getenv("LOG_LEVEL")); err == nil { 68 | Log.SetLevel(level) 69 | } 70 | } else { 71 | // set default log level to info 72 | Log.SetLevel(InfoLevel) 73 | } 74 | } 75 | 76 | func (l *DefaultLogger) GetLevel() LogLevel { 77 | return l.Level 78 | } 79 | 80 | func (l *DefaultLogger) SetLevel(level LogLevel) { 81 | l.Level = level 82 | } 83 | 84 | func (l *DefaultLogger) ParseLevel(level string) (LogLevel, error) { 85 | switch level { 86 | case "panic": 87 | return PanicLevel, nil 88 | case "fatal": 89 | return FatalLevel, nil 90 | case "error": 91 | return ErrorLevel, nil 92 | case "warn": 93 | return WarnLevel, nil 94 | case "info": 95 | return InfoLevel, nil 96 | case "debug": 97 | return DebugLevel, nil 98 | case "trace": 99 | return TraceLevel, nil 100 | } 101 | var ll LogLevel 102 | return ll, fmt.Errorf("invalid log level %q", level) 103 | } 104 | 105 | func (l *DefaultLogger) Panic(args ...interface{}) { 106 | if l.Level < PanicLevel { 107 | return 108 | } 109 | args = append([]interface{}{panicLevelStr}, args...) 110 | log.Print(args...) 111 | } 112 | 113 | func (l *DefaultLogger) Panicf(format string, args ...interface{}) { 114 | if l.Level < PanicLevel { 115 | return 116 | } 117 | log.Printf(panicLevelStr+format, args...) 118 | } 119 | 120 | func (l *DefaultLogger) Fatal(args ...interface{}) { 121 | if l.Level < FatalLevel { 122 | return 123 | } 124 | args = append([]interface{}{fatalLevelStr}, args...) 125 | log.Print(args...) 126 | } 127 | 128 | func (l *DefaultLogger) Fatalf(format string, args ...interface{}) { 129 | if l.Level < FatalLevel { 130 | return 131 | } 132 | log.Printf(fatalLevelStr+format, args...) 133 | } 134 | 135 | func (l *DefaultLogger) Error(args ...interface{}) { 136 | if l.Level < ErrorLevel { 137 | return 138 | } 139 | args = append([]interface{}{errorLevelStr}, args...) 140 | log.Print(args...) 141 | } 142 | 143 | func (l *DefaultLogger) Errorf(format string, args ...interface{}) { 144 | if l.Level < ErrorLevel { 145 | return 146 | } 147 | log.Printf(errorLevelStr+format, args...) 148 | } 149 | 150 | func (l *DefaultLogger) Warn(args ...interface{}) { 151 | if l.Level < WarnLevel { 152 | return 153 | } 154 | args = append([]interface{}{warnLevelStr}, args...) 155 | log.Print(args...) 156 | } 157 | 158 | func (l *DefaultLogger) Warnf(format string, args ...interface{}) { 159 | if l.Level < WarnLevel { 160 | return 161 | } 162 | log.Printf(warnLevelStr+format, args...) 163 | } 164 | 165 | func (l *DefaultLogger) Info(args ...interface{}) { 166 | if l.Level < InfoLevel { 167 | return 168 | } 169 | args = append([]interface{}{infoLevelStr}, args...) 170 | log.Print(args...) 171 | } 172 | 173 | func (l *DefaultLogger) Infof(format string, args ...interface{}) { 174 | if l.Level < InfoLevel { 175 | return 176 | } 177 | log.Printf(infoLevelStr+format, args...) 178 | } 179 | 180 | func (l *DefaultLogger) Debug(args ...interface{}) { 181 | if l.Level < DebugLevel { 182 | return 183 | } 184 | args = append([]interface{}{debugLevelStr}, args...) 185 | log.Print(args...) 186 | } 187 | 188 | func (l *DefaultLogger) Debugf(format string, args ...interface{}) { 189 | if l.Level < DebugLevel { 190 | return 191 | } 192 | log.Printf(debugLevelStr+format, args...) 193 | } 194 | 195 | func (l *DefaultLogger) Trace(args ...interface{}) { 196 | if l.Level < TraceLevel { 197 | return 198 | } 199 | args = append([]interface{}{traceLevelStr}, args...) 200 | log.Print(args...) 201 | } 202 | 203 | func (l *DefaultLogger) Tracef(format string, args ...interface{}) { 204 | if l.Level < TraceLevel { 205 | return 206 | } 207 | log.Printf(traceLevelStr+format, args...) 208 | } 209 | 210 | func (l *DefaultLogger) Print(args ...interface{}) { 211 | log.Print(args...) 212 | } 213 | 214 | func (l *DefaultLogger) Printf(format string, args ...interface{}) { 215 | log.Printf(format, args...) 216 | } 217 | -------------------------------------------------------------------------------- /mount.go: -------------------------------------------------------------------------------- 1 | package nfs 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | 7 | "github.com/willscott/go-nfs-client/nfs/xdr" 8 | ) 9 | 10 | const ( 11 | mountServiceID = 100005 12 | ) 13 | 14 | func init() { 15 | _ = RegisterMessageHandler(mountServiceID, uint32(MountProcNull), onMountNull) 16 | _ = RegisterMessageHandler(mountServiceID, uint32(MountProcMount), onMount) 17 | _ = RegisterMessageHandler(mountServiceID, uint32(MountProcUmnt), onUMount) 18 | } 19 | 20 | func onMountNull(ctx context.Context, w *response, userHandle Handler) error { 21 | return w.writeHeader(ResponseCodeSuccess) 22 | } 23 | 24 | func onMount(ctx context.Context, w *response, userHandle Handler) error { 25 | // TODO: auth check. 26 | dirpath, err := xdr.ReadOpaque(w.req.Body) 27 | if err != nil { 28 | return err 29 | } 30 | mountReq := MountRequest{Header: w.req.Header, Dirpath: dirpath} 31 | status, handle, flavors := userHandle.Mount(ctx, w.conn, mountReq) 32 | 33 | if err := w.writeHeader(ResponseCodeSuccess); err != nil { 34 | return err 35 | } 36 | 37 | writer := bytes.NewBuffer([]byte{}) 38 | if err := xdr.Write(writer, uint32(status)); err != nil { 39 | return err 40 | } 41 | 42 | rootHndl := userHandle.ToHandle(handle, []string{}) 43 | 44 | if status == MountStatusOk { 45 | _ = xdr.Write(writer, rootHndl) 46 | _ = xdr.Write(writer, flavors) 47 | } 48 | return w.Write(writer.Bytes()) 49 | } 50 | 51 | func onUMount(ctx context.Context, w *response, userHandle Handler) error { 52 | _, err := xdr.ReadOpaque(w.req.Body) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | return w.writeHeader(ResponseCodeSuccess) 58 | } 59 | -------------------------------------------------------------------------------- /mountinterface.go: -------------------------------------------------------------------------------- 1 | package nfs 2 | 3 | import ( 4 | "github.com/willscott/go-nfs-client/nfs/rpc" 5 | ) 6 | 7 | // FHSize is the maximum size of a FileHandle 8 | const FHSize = 64 9 | 10 | // MNTNameLen is the maximum size of a mount name 11 | const MNTNameLen = 255 12 | 13 | // MntPathLen is the maximum size of a mount path 14 | const MntPathLen = 1024 15 | 16 | // FileHandle maps to a fhandle3 17 | type FileHandle []byte 18 | 19 | // MountStatus defines the response to the Mount Procedure 20 | type MountStatus uint32 21 | 22 | // MountStatus Codes 23 | const ( 24 | MountStatusOk MountStatus = 0 25 | MountStatusErrPerm MountStatus = 1 26 | MountStatusErrNoEnt MountStatus = 2 27 | MountStatusErrIO MountStatus = 5 28 | MountStatusErrAcces MountStatus = 13 29 | MountStatusErrNotDir MountStatus = 20 30 | MountStatusErrInval MountStatus = 22 31 | MountStatusErrNameTooLong MountStatus = 63 32 | MountStatusErrNotSupp MountStatus = 10004 33 | MountStatusErrServerFault MountStatus = 10006 34 | ) 35 | 36 | // MountProcedure is the valid RPC calls for the mount service. 37 | type MountProcedure uint32 38 | 39 | // MountProcedure Codes 40 | const ( 41 | MountProcNull MountProcedure = iota 42 | MountProcMount 43 | MountProcDump 44 | MountProcUmnt 45 | MountProcUmntAll 46 | MountProcExport 47 | ) 48 | 49 | func (m MountProcedure) String() string { 50 | switch m { 51 | case MountProcNull: 52 | return "Null" 53 | case MountProcMount: 54 | return "Mount" 55 | case MountProcDump: 56 | return "Dump" 57 | case MountProcUmnt: 58 | return "Umnt" 59 | case MountProcUmntAll: 60 | return "UmntAll" 61 | case MountProcExport: 62 | return "Export" 63 | default: 64 | return "Unknown" 65 | } 66 | } 67 | 68 | // AuthFlavor is a form of authentication, per rfc1057 section 7.2 69 | type AuthFlavor uint32 70 | 71 | // AuthFlavor Codes 72 | const ( 73 | AuthFlavorNull AuthFlavor = 0 74 | AuthFlavorUnix AuthFlavor = 1 75 | AuthFlavorShort AuthFlavor = 2 76 | AuthFlavorDES AuthFlavor = 3 77 | ) 78 | 79 | // MountRequest contains the format of a client request to open a mount. 80 | type MountRequest struct { 81 | rpc.Header 82 | Dirpath []byte 83 | } 84 | 85 | // MountResponse is the server's response with status `MountStatusOk` 86 | type MountResponse struct { 87 | rpc.Header 88 | FileHandle 89 | AuthFlavors []int 90 | } 91 | -------------------------------------------------------------------------------- /nfs.go: -------------------------------------------------------------------------------- 1 | package nfs 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | const ( 8 | nfsServiceID = 100003 9 | ) 10 | 11 | func init() { 12 | _ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureNull), onNull) // 0 13 | _ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureGetAttr), onGetAttr) // 1 14 | _ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureSetAttr), onSetAttr) // 2 15 | _ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureLookup), onLookup) // 3 16 | _ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureAccess), onAccess) // 4 17 | _ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureReadlink), onReadLink) // 5 18 | _ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureRead), onRead) // 6 19 | _ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureWrite), onWrite) // 7 20 | _ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureCreate), onCreate) // 8 21 | _ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureMkDir), onMkdir) // 9 22 | _ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureSymlink), onSymlink) // 10 23 | _ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureMkNod), onMknod) // 11 24 | _ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureRemove), onRemove) // 12 25 | _ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureRmDir), onRmDir) // 13 26 | _ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureRename), onRename) // 14 27 | _ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureLink), onLink) // 15 28 | _ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureReadDir), onReadDir) // 16 29 | _ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureReadDirPlus), onReadDirPlus) // 17 30 | _ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureFSStat), onFSStat) // 18 31 | _ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureFSInfo), onFSInfo) // 19 32 | _ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedurePathConf), onPathConf) // 20 33 | _ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureCommit), onCommit) // 21 34 | } 35 | 36 | func onNull(ctx context.Context, w *response, userHandle Handler) error { 37 | return w.Write([]byte{}) 38 | } 39 | -------------------------------------------------------------------------------- /nfs_onaccess.go: -------------------------------------------------------------------------------- 1 | package nfs 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | 7 | "github.com/go-git/go-billy/v5" 8 | "github.com/willscott/go-nfs-client/nfs/xdr" 9 | ) 10 | 11 | func onAccess(ctx context.Context, w *response, userHandle Handler) error { 12 | w.errorFmt = opAttrErrorFormatter 13 | roothandle, err := xdr.ReadOpaque(w.req.Body) 14 | if err != nil { 15 | return &NFSStatusError{NFSStatusInval, err} 16 | } 17 | fs, path, err := userHandle.FromHandle(roothandle) 18 | if err != nil { 19 | return &NFSStatusError{NFSStatusStale, err} 20 | } 21 | mask, err := xdr.ReadUint32(w.req.Body) 22 | if err != nil { 23 | return &NFSStatusError{NFSStatusInval, err} 24 | } 25 | 26 | writer := bytes.NewBuffer([]byte{}) 27 | if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil { 28 | return &NFSStatusError{NFSStatusServerFault, err} 29 | } 30 | if err := WritePostOpAttrs(writer, tryStat(fs, path)); err != nil { 31 | return &NFSStatusError{NFSStatusServerFault, err} 32 | } 33 | 34 | if !billy.CapabilityCheck(fs, billy.WriteCapability) { 35 | mask = mask & (1 | 2 | 0x20) 36 | } 37 | 38 | if err := xdr.Write(writer, mask); err != nil { 39 | return &NFSStatusError{NFSStatusServerFault, err} 40 | } 41 | if err := w.Write(writer.Bytes()); err != nil { 42 | return &NFSStatusError{NFSStatusServerFault, err} 43 | } 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /nfs_oncommit.go: -------------------------------------------------------------------------------- 1 | package nfs 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "os" 7 | 8 | "github.com/go-git/go-billy/v5" 9 | "github.com/willscott/go-nfs-client/nfs/xdr" 10 | ) 11 | 12 | // onCommit - note this is a no-op, as we always push writes to the backing store. 13 | func onCommit(ctx context.Context, w *response, userHandle Handler) error { 14 | w.errorFmt = wccDataErrorFormatter 15 | handle, err := xdr.ReadOpaque(w.req.Body) 16 | if err != nil { 17 | return &NFSStatusError{NFSStatusInval, err} 18 | } 19 | // The conn will drain the unread offset and count arguments. 20 | 21 | fs, path, err := userHandle.FromHandle(handle) 22 | if err != nil { 23 | return &NFSStatusError{NFSStatusStale, err} 24 | } 25 | if !billy.CapabilityCheck(fs, billy.WriteCapability) { 26 | return &NFSStatusError{NFSStatusServerFault, os.ErrPermission} 27 | } 28 | 29 | writer := bytes.NewBuffer([]byte{}) 30 | if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil { 31 | return err 32 | } 33 | 34 | // no pre-op cache data. 35 | if err := xdr.Write(writer, uint32(0)); err != nil { 36 | return &NFSStatusError{NFSStatusServerFault, err} 37 | } 38 | if err := WritePostOpAttrs(writer, tryStat(fs, path)); err != nil { 39 | return &NFSStatusError{NFSStatusServerFault, err} 40 | } 41 | // write the 8 bytes of write verification. 42 | if err := xdr.Write(writer, w.Server.ID); err != nil { 43 | return &NFSStatusError{NFSStatusServerFault, err} 44 | } 45 | 46 | if err := w.Write(writer.Bytes()); err != nil { 47 | return &NFSStatusError{NFSStatusServerFault, err} 48 | } 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /nfs_oncreate.go: -------------------------------------------------------------------------------- 1 | package nfs 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "os" 7 | 8 | "github.com/go-git/go-billy/v5" 9 | "github.com/willscott/go-nfs-client/nfs/xdr" 10 | ) 11 | 12 | const ( 13 | createModeUnchecked = 0 14 | createModeGuarded = 1 15 | createModeExclusive = 2 16 | ) 17 | 18 | func onCreate(ctx context.Context, w *response, userHandle Handler) error { 19 | w.errorFmt = wccDataErrorFormatter 20 | obj := DirOpArg{} 21 | err := xdr.Read(w.req.Body, &obj) 22 | if err != nil { 23 | return &NFSStatusError{NFSStatusInval, err} 24 | } 25 | how, err := xdr.ReadUint32(w.req.Body) 26 | if err != nil { 27 | return &NFSStatusError{NFSStatusInval, err} 28 | } 29 | var attrs *SetFileAttributes 30 | if how == createModeUnchecked || how == createModeGuarded { 31 | sattr, err := ReadSetFileAttributes(w.req.Body) 32 | if err != nil { 33 | return &NFSStatusError{NFSStatusInval, err} 34 | } 35 | attrs = sattr 36 | } else if how == createModeExclusive { 37 | // read createverf3 38 | var verf [8]byte 39 | if err := xdr.Read(w.req.Body, &verf); err != nil { 40 | return &NFSStatusError{NFSStatusInval, err} 41 | } 42 | Log.Errorf("failing create to indicate lack of support for 'exclusive' mode.") 43 | // TODO: support 'exclusive' mode. 44 | return &NFSStatusError{NFSStatusNotSupp, os.ErrPermission} 45 | } else { 46 | // invalid 47 | return &NFSStatusError{NFSStatusNotSupp, os.ErrInvalid} 48 | } 49 | 50 | fs, path, err := userHandle.FromHandle(obj.Handle) 51 | if err != nil { 52 | return &NFSStatusError{NFSStatusStale, err} 53 | } 54 | if !billy.CapabilityCheck(fs, billy.WriteCapability) { 55 | return &NFSStatusError{NFSStatusROFS, os.ErrPermission} 56 | } 57 | 58 | if len(string(obj.Filename)) > PathNameMax { 59 | return &NFSStatusError{NFSStatusNameTooLong, nil} 60 | } 61 | 62 | newFile := append(path, string(obj.Filename)) 63 | newFilePath := fs.Join(newFile...) 64 | if s, err := fs.Stat(newFilePath); err == nil { 65 | if s.IsDir() { 66 | return &NFSStatusError{NFSStatusExist, nil} 67 | } 68 | if how == createModeGuarded { 69 | return &NFSStatusError{NFSStatusExist, os.ErrPermission} 70 | } 71 | } else { 72 | if s, err := fs.Stat(fs.Join(path...)); err != nil { 73 | return &NFSStatusError{NFSStatusAccess, err} 74 | } else if !s.IsDir() { 75 | return &NFSStatusError{NFSStatusNotDir, nil} 76 | } 77 | } 78 | 79 | file, err := fs.Create(newFilePath) 80 | if err != nil { 81 | Log.Errorf("Error Creating: %v", err) 82 | return &NFSStatusError{NFSStatusAccess, err} 83 | } 84 | if err := file.Close(); err != nil { 85 | Log.Errorf("Error Creating: %v", err) 86 | return &NFSStatusError{NFSStatusAccess, err} 87 | } 88 | 89 | fp := userHandle.ToHandle(fs, newFile) 90 | changer := userHandle.Change(fs) 91 | if err := attrs.Apply(changer, fs, newFilePath); err != nil { 92 | Log.Errorf("Error applying attributes: %v\n", err) 93 | return &NFSStatusError{NFSStatusIO, err} 94 | } 95 | 96 | writer := bytes.NewBuffer([]byte{}) 97 | if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil { 98 | return &NFSStatusError{NFSStatusServerFault, err} 99 | } 100 | 101 | // "handle follows" 102 | if err := xdr.Write(writer, uint32(1)); err != nil { 103 | return &NFSStatusError{NFSStatusServerFault, err} 104 | } 105 | if err := xdr.Write(writer, fp); err != nil { 106 | return &NFSStatusError{NFSStatusServerFault, err} 107 | } 108 | if err := WritePostOpAttrs(writer, tryStat(fs, []string{file.Name()})); err != nil { 109 | return &NFSStatusError{NFSStatusServerFault, err} 110 | } 111 | 112 | // dir_wcc (we don't include pre_op_attr) 113 | if err := xdr.Write(writer, uint32(0)); err != nil { 114 | return &NFSStatusError{NFSStatusServerFault, err} 115 | } 116 | if err := WritePostOpAttrs(writer, tryStat(fs, path)); err != nil { 117 | return &NFSStatusError{NFSStatusServerFault, err} 118 | } 119 | 120 | if err := w.Write(writer.Bytes()); err != nil { 121 | return &NFSStatusError{NFSStatusServerFault, err} 122 | } 123 | return nil 124 | } 125 | -------------------------------------------------------------------------------- /nfs_onfsinfo.go: -------------------------------------------------------------------------------- 1 | package nfs 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | 7 | "github.com/go-git/go-billy/v5" 8 | "github.com/willscott/go-nfs-client/nfs/xdr" 9 | ) 10 | 11 | const ( 12 | // FSInfoPropertyLink does the FS support hard links? 13 | FSInfoPropertyLink = 0x0001 14 | // FSInfoPropertySymlink does the FS support soft links? 15 | FSInfoPropertySymlink = 0x0002 16 | // FSInfoPropertyHomogeneous does the FS need PATHCONF calls for each file 17 | FSInfoPropertyHomogeneous = 0x0008 18 | // FSInfoPropertyCanSetTime can the FS support setting access/mod times? 19 | FSInfoPropertyCanSetTime = 0x0010 20 | ) 21 | 22 | func onFSInfo(ctx context.Context, w *response, userHandle Handler) error { 23 | roothandle, err := xdr.ReadOpaque(w.req.Body) 24 | if err != nil { 25 | return &NFSStatusError{NFSStatusInval, err} 26 | } 27 | fs, path, err := userHandle.FromHandle(roothandle) 28 | if err != nil { 29 | return &NFSStatusError{NFSStatusStale, err} 30 | } 31 | 32 | writer := bytes.NewBuffer([]byte{}) 33 | if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil { 34 | return &NFSStatusError{NFSStatusServerFault, err} 35 | } 36 | if err := WritePostOpAttrs(writer, tryStat(fs, path)); err != nil { 37 | return &NFSStatusError{NFSStatusServerFault, err} 38 | } 39 | 40 | type fsinfores struct { 41 | Rtmax uint32 42 | Rtpref uint32 43 | Rtmult uint32 44 | Wtmax uint32 45 | Wtpref uint32 46 | Wtmult uint32 47 | Dtpref uint32 48 | Maxfilesize uint64 49 | TimeDelta uint64 50 | Properties uint32 51 | } 52 | 53 | res := fsinfores{ 54 | Rtmax: 1 << 30, 55 | Rtpref: 1 << 30, 56 | Rtmult: 4096, 57 | Wtmax: 1 << 30, 58 | Wtpref: 1 << 30, 59 | Wtmult: 4096, 60 | Dtpref: 8192, 61 | Maxfilesize: 1 << 62, // wild guess. this seems big. 62 | TimeDelta: 1, // nanosecond precision. 63 | Properties: 0, 64 | } 65 | 66 | // TODO: these aren't great indications of support, really. 67 | if _, ok := fs.(billy.Symlink); ok { 68 | res.Properties |= FSInfoPropertyLink 69 | res.Properties |= FSInfoPropertySymlink 70 | } 71 | // TODO: if the nfs share spans multiple virtual mounts, may need 72 | // to support granular PATHINFO responses. 73 | res.Properties |= FSInfoPropertyHomogeneous 74 | // TODO: not a perfect indicator 75 | if billy.CapabilityCheck(fs, billy.WriteCapability) { 76 | res.Properties |= FSInfoPropertyCanSetTime 77 | } 78 | // TODO: this whole struct should be specifiable by the userhandler. 79 | 80 | if err := xdr.Write(writer, res); err != nil { 81 | return &NFSStatusError{NFSStatusServerFault, err} 82 | } 83 | if err := w.Write(writer.Bytes()); err != nil { 84 | return &NFSStatusError{NFSStatusServerFault, err} 85 | } 86 | return nil 87 | } 88 | -------------------------------------------------------------------------------- /nfs_onfsstat.go: -------------------------------------------------------------------------------- 1 | package nfs 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | 7 | "github.com/go-git/go-billy/v5" 8 | "github.com/willscott/go-nfs-client/nfs/xdr" 9 | ) 10 | 11 | func onFSStat(ctx context.Context, w *response, userHandle Handler) error { 12 | roothandle, err := xdr.ReadOpaque(w.req.Body) 13 | if err != nil { 14 | return &NFSStatusError{NFSStatusInval, err} 15 | } 16 | fs, path, err := userHandle.FromHandle(roothandle) 17 | if err != nil { 18 | return &NFSStatusError{NFSStatusStale, err} 19 | } 20 | 21 | defaults := FSStat{ 22 | TotalSize: 1 << 62, 23 | FreeSize: 1 << 62, 24 | AvailableSize: 1 << 62, 25 | TotalFiles: 1 << 62, 26 | FreeFiles: 1 << 62, 27 | AvailableFiles: 1 << 62, 28 | CacheHint: 0, 29 | } 30 | if !billy.CapabilityCheck(fs, billy.WriteCapability) { 31 | defaults.AvailableFiles = 0 32 | defaults.AvailableSize = 0 33 | } 34 | 35 | err = userHandle.FSStat(ctx, fs, &defaults) 36 | if err != nil { 37 | if _, ok := err.(*NFSStatusError); ok { 38 | return err 39 | } 40 | return &NFSStatusError{NFSStatusServerFault, err} 41 | } 42 | 43 | writer := bytes.NewBuffer([]byte{}) 44 | if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil { 45 | return &NFSStatusError{NFSStatusServerFault, err} 46 | } 47 | if err := WritePostOpAttrs(writer, tryStat(fs, path)); err != nil { 48 | return &NFSStatusError{NFSStatusServerFault, err} 49 | } 50 | 51 | if err := xdr.Write(writer, defaults); err != nil { 52 | return &NFSStatusError{NFSStatusServerFault, err} 53 | } 54 | if err := w.Write(writer.Bytes()); err != nil { 55 | return &NFSStatusError{NFSStatusServerFault, err} 56 | } 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /nfs_ongetattr.go: -------------------------------------------------------------------------------- 1 | package nfs 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "os" 7 | 8 | "github.com/willscott/go-nfs-client/nfs/xdr" 9 | ) 10 | 11 | func onGetAttr(ctx context.Context, w *response, userHandle Handler) error { 12 | handle, err := xdr.ReadOpaque(w.req.Body) 13 | if err != nil { 14 | return &NFSStatusError{NFSStatusInval, err} 15 | } 16 | 17 | fs, path, err := userHandle.FromHandle(handle) 18 | if err != nil { 19 | return &NFSStatusError{NFSStatusStale, err} 20 | } 21 | 22 | fullPath := fs.Join(path...) 23 | info, err := fs.Lstat(fullPath) 24 | if err != nil { 25 | if os.IsNotExist(err) { 26 | return &NFSStatusError{NFSStatusNoEnt, err} 27 | } 28 | return &NFSStatusError{NFSStatusIO, err} 29 | } 30 | attr := ToFileAttribute(info, fullPath) 31 | 32 | writer := bytes.NewBuffer([]byte{}) 33 | if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil { 34 | return &NFSStatusError{NFSStatusServerFault, err} 35 | } 36 | if err := xdr.Write(writer, attr); err != nil { 37 | return &NFSStatusError{NFSStatusServerFault, err} 38 | } 39 | 40 | if err := w.Write(writer.Bytes()); err != nil { 41 | return &NFSStatusError{NFSStatusServerFault, err} 42 | } 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /nfs_onlink.go: -------------------------------------------------------------------------------- 1 | package nfs 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "os" 7 | 8 | "github.com/go-git/go-billy/v5" 9 | "github.com/willscott/go-nfs-client/nfs/xdr" 10 | ) 11 | 12 | // Backing billy.FS doesn't support hard links 13 | func onLink(ctx context.Context, w *response, userHandle Handler) error { 14 | w.errorFmt = wccDataErrorFormatter 15 | obj := DirOpArg{} 16 | err := xdr.Read(w.req.Body, &obj) 17 | if err != nil { 18 | return &NFSStatusError{NFSStatusInval, err} 19 | } 20 | attrs, err := ReadSetFileAttributes(w.req.Body) 21 | if err != nil { 22 | return &NFSStatusError{NFSStatusInval, err} 23 | } 24 | 25 | target, err := xdr.ReadOpaque(w.req.Body) 26 | if err != nil { 27 | return &NFSStatusError{NFSStatusInval, err} 28 | } 29 | 30 | fs, path, err := userHandle.FromHandle(obj.Handle) 31 | if err != nil { 32 | return &NFSStatusError{NFSStatusStale, err} 33 | } 34 | if !billy.CapabilityCheck(fs, billy.WriteCapability) { 35 | return &NFSStatusError{NFSStatusROFS, os.ErrPermission} 36 | } 37 | 38 | if len(string(obj.Filename)) > PathNameMax { 39 | return &NFSStatusError{NFSStatusNameTooLong, os.ErrInvalid} 40 | } 41 | 42 | newFilePath := fs.Join(append(path, string(obj.Filename))...) 43 | if _, err := fs.Stat(newFilePath); err == nil { 44 | return &NFSStatusError{NFSStatusExist, os.ErrExist} 45 | } 46 | if s, err := fs.Stat(fs.Join(path...)); err != nil { 47 | return &NFSStatusError{NFSStatusAccess, err} 48 | } else if !s.IsDir() { 49 | return &NFSStatusError{NFSStatusNotDir, nil} 50 | } 51 | 52 | fp := userHandle.ToHandle(fs, append(path, string(obj.Filename))) 53 | changer := userHandle.Change(fs) 54 | if changer == nil { 55 | return &NFSStatusError{NFSStatusAccess, err} 56 | } 57 | cos, ok := changer.(UnixChange) 58 | if !ok { 59 | return &NFSStatusError{NFSStatusAccess, err} 60 | } 61 | 62 | err = cos.Link(string(target), newFilePath) 63 | if err != nil { 64 | return &NFSStatusError{NFSStatusAccess, err} 65 | } 66 | if err := attrs.Apply(changer, fs, newFilePath); err != nil { 67 | return &NFSStatusError{NFSStatusIO, err} 68 | } 69 | 70 | writer := bytes.NewBuffer([]byte{}) 71 | if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil { 72 | return &NFSStatusError{NFSStatusServerFault, err} 73 | } 74 | 75 | // "handle follows" 76 | if err := xdr.Write(writer, uint32(1)); err != nil { 77 | return &NFSStatusError{NFSStatusServerFault, err} 78 | } 79 | if err := xdr.Write(writer, fp); err != nil { 80 | return &NFSStatusError{NFSStatusServerFault, err} 81 | } 82 | if err := WritePostOpAttrs(writer, tryStat(fs, append(path, string(obj.Filename)))); err != nil { 83 | return &NFSStatusError{NFSStatusServerFault, err} 84 | } 85 | 86 | if err := WriteWcc(writer, nil, tryStat(fs, path)); err != nil { 87 | return &NFSStatusError{NFSStatusServerFault, err} 88 | } 89 | 90 | if err := w.Write(writer.Bytes()); err != nil { 91 | return &NFSStatusError{NFSStatusServerFault, err} 92 | } 93 | return nil 94 | } 95 | -------------------------------------------------------------------------------- /nfs_onlookup.go: -------------------------------------------------------------------------------- 1 | package nfs 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "os" 7 | 8 | "github.com/go-git/go-billy/v5" 9 | "github.com/willscott/go-nfs-client/nfs/xdr" 10 | ) 11 | 12 | func lookupSuccessResponse(handle []byte, entPath, dirPath []string, fs billy.Filesystem) ([]byte, error) { 13 | writer := bytes.NewBuffer([]byte{}) 14 | if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil { 15 | return nil, err 16 | } 17 | if err := xdr.Write(writer, handle); err != nil { 18 | return nil, err 19 | } 20 | if err := WritePostOpAttrs(writer, tryStat(fs, entPath)); err != nil { 21 | return nil, err 22 | } 23 | if err := WritePostOpAttrs(writer, tryStat(fs, dirPath)); err != nil { 24 | return nil, err 25 | } 26 | return writer.Bytes(), nil 27 | } 28 | 29 | func onLookup(ctx context.Context, w *response, userHandle Handler) error { 30 | w.errorFmt = opAttrErrorFormatter 31 | obj := DirOpArg{} 32 | err := xdr.Read(w.req.Body, &obj) 33 | if err != nil { 34 | return &NFSStatusError{NFSStatusInval, err} 35 | } 36 | 37 | fs, p, err := userHandle.FromHandle(obj.Handle) 38 | if err != nil { 39 | return &NFSStatusError{NFSStatusStale, err} 40 | } 41 | dirInfo, err := fs.Lstat(fs.Join(p...)) 42 | if err != nil || !dirInfo.IsDir() { 43 | return &NFSStatusError{NFSStatusNotDir, err} 44 | } 45 | 46 | // Special cases for "." and ".." 47 | if bytes.Equal(obj.Filename, []byte(".")) { 48 | resp, err := lookupSuccessResponse(obj.Handle, p, p, fs) 49 | if err != nil { 50 | return &NFSStatusError{NFSStatusServerFault, err} 51 | } 52 | if err := w.Write(resp); err != nil { 53 | return &NFSStatusError{NFSStatusServerFault, err} 54 | } 55 | return nil 56 | } 57 | if bytes.Equal(obj.Filename, []byte("..")) { 58 | if len(p) == 0 { 59 | return &NFSStatusError{NFSStatusAccess, os.ErrPermission} 60 | } 61 | pPath := p[0 : len(p)-1] 62 | pHandle := userHandle.ToHandle(fs, pPath) 63 | resp, err := lookupSuccessResponse(pHandle, pPath, p, fs) 64 | if err != nil { 65 | return &NFSStatusError{NFSStatusServerFault, err} 66 | } 67 | if err := w.Write(resp); err != nil { 68 | return &NFSStatusError{NFSStatusServerFault, err} 69 | } 70 | return nil 71 | } 72 | 73 | reqPath := append(p, string(obj.Filename)) 74 | if _, err = fs.Lstat(fs.Join(reqPath...)); err != nil { 75 | return &NFSStatusError{NFSStatusNoEnt, os.ErrNotExist} 76 | } 77 | 78 | newHandle := userHandle.ToHandle(fs, reqPath) 79 | resp, err := lookupSuccessResponse(newHandle, reqPath, p, fs) 80 | if err != nil { 81 | return &NFSStatusError{NFSStatusServerFault, err} 82 | } 83 | if err := w.Write(resp); err != nil { 84 | return &NFSStatusError{NFSStatusServerFault, err} 85 | } 86 | return nil 87 | } 88 | -------------------------------------------------------------------------------- /nfs_onmkdir.go: -------------------------------------------------------------------------------- 1 | package nfs 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "os" 7 | 8 | "github.com/go-git/go-billy/v5" 9 | "github.com/willscott/go-nfs-client/nfs/xdr" 10 | ) 11 | 12 | const ( 13 | mkdirDefaultMode = 755 14 | ) 15 | 16 | func onMkdir(ctx context.Context, w *response, userHandle Handler) error { 17 | w.errorFmt = wccDataErrorFormatter 18 | obj := DirOpArg{} 19 | err := xdr.Read(w.req.Body, &obj) 20 | if err != nil { 21 | return &NFSStatusError{NFSStatusInval, err} 22 | } 23 | 24 | attrs, err := ReadSetFileAttributes(w.req.Body) 25 | if err != nil { 26 | return &NFSStatusError{NFSStatusInval, err} 27 | } 28 | 29 | fs, path, err := userHandle.FromHandle(obj.Handle) 30 | if err != nil { 31 | return &NFSStatusError{NFSStatusStale, err} 32 | } 33 | if !billy.CapabilityCheck(fs, billy.WriteCapability) { 34 | return &NFSStatusError{NFSStatusROFS, os.ErrPermission} 35 | } 36 | 37 | if len(string(obj.Filename)) > PathNameMax { 38 | return &NFSStatusError{NFSStatusNameTooLong, os.ErrInvalid} 39 | } 40 | if string(obj.Filename) == "." || string(obj.Filename) == ".." { 41 | return &NFSStatusError{NFSStatusExist, os.ErrExist} 42 | } 43 | 44 | newFolder := append(path, string(obj.Filename)) 45 | newFolderPath := fs.Join(newFolder...) 46 | if s, err := fs.Stat(newFolderPath); err == nil { 47 | if s.IsDir() { 48 | return &NFSStatusError{NFSStatusExist, nil} 49 | } 50 | } else { 51 | if s, err := fs.Stat(fs.Join(path...)); err != nil { 52 | return &NFSStatusError{NFSStatusAccess, err} 53 | } else if !s.IsDir() { 54 | return &NFSStatusError{NFSStatusNotDir, nil} 55 | } 56 | } 57 | 58 | if err := fs.MkdirAll(newFolderPath, attrs.Mode(mkdirDefaultMode)); err != nil { 59 | return &NFSStatusError{NFSStatusAccess, err} 60 | } 61 | 62 | fp := userHandle.ToHandle(fs, newFolder) 63 | changer := userHandle.Change(fs) 64 | if changer != nil { 65 | if err := attrs.Apply(changer, fs, newFolderPath); err != nil { 66 | return &NFSStatusError{NFSStatusIO, err} 67 | } 68 | } 69 | 70 | writer := bytes.NewBuffer([]byte{}) 71 | if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil { 72 | return &NFSStatusError{NFSStatusServerFault, err} 73 | } 74 | 75 | // "handle follows" 76 | if err := xdr.Write(writer, uint32(1)); err != nil { 77 | return &NFSStatusError{NFSStatusServerFault, err} 78 | } 79 | if err := xdr.Write(writer, fp); err != nil { 80 | return &NFSStatusError{NFSStatusServerFault, err} 81 | } 82 | if err := WritePostOpAttrs(writer, tryStat(fs, newFolder)); err != nil { 83 | return &NFSStatusError{NFSStatusServerFault, err} 84 | } 85 | 86 | if err := WriteWcc(writer, nil, tryStat(fs, path)); err != nil { 87 | return &NFSStatusError{NFSStatusServerFault, err} 88 | } 89 | 90 | if err := w.Write(writer.Bytes()); err != nil { 91 | return &NFSStatusError{NFSStatusServerFault, err} 92 | } 93 | return nil 94 | } 95 | -------------------------------------------------------------------------------- /nfs_onmknod.go: -------------------------------------------------------------------------------- 1 | package nfs 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "os" 7 | 8 | "github.com/go-git/go-billy/v5" 9 | "github.com/willscott/go-nfs-client/nfs/xdr" 10 | ) 11 | 12 | type nfs_ftype int32 13 | 14 | const ( 15 | FTYPE_NF3REG nfs_ftype = 1 16 | FTYPE_NF3DIR nfs_ftype = 2 17 | FTYPE_NF3BLK nfs_ftype = 3 18 | FTYPE_NF3CHR nfs_ftype = 4 19 | FTYPE_NF3LNK nfs_ftype = 5 20 | FTYPE_NF3SOCK nfs_ftype = 6 21 | FTYPE_NF3FIFO nfs_ftype = 7 22 | ) 23 | 24 | // Backing billy.FS doesn't support creation of 25 | // char, block, socket, or fifo pipe nodes 26 | func onMknod(ctx context.Context, w *response, userHandle Handler) error { 27 | w.errorFmt = wccDataErrorFormatter 28 | obj := DirOpArg{} 29 | err := xdr.Read(w.req.Body, &obj) 30 | if err != nil { 31 | return &NFSStatusError{NFSStatusInval, err} 32 | } 33 | 34 | ftype, err := xdr.ReadUint32(w.req.Body) 35 | if err != nil { 36 | return &NFSStatusError{NFSStatusInval, err} 37 | } 38 | 39 | // see if the filesystem supports mknod 40 | fs, path, err := userHandle.FromHandle(obj.Handle) 41 | if err != nil { 42 | return &NFSStatusError{NFSStatusStale, err} 43 | } 44 | if !billy.CapabilityCheck(fs, billy.WriteCapability) { 45 | return &NFSStatusError{NFSStatusROFS, os.ErrPermission} 46 | } 47 | c := userHandle.Change(fs) 48 | if c == nil { 49 | return &NFSStatusError{NFSStatusAccess, os.ErrPermission} 50 | } 51 | cu, ok := c.(UnixChange) 52 | if !ok { 53 | return &NFSStatusError{NFSStatusAccess, os.ErrPermission} 54 | } 55 | 56 | if len(string(obj.Filename)) > PathNameMax { 57 | return &NFSStatusError{NFSStatusNameTooLong, os.ErrInvalid} 58 | } 59 | 60 | newFilePath := fs.Join(append(path, string(obj.Filename))...) 61 | if _, err := fs.Stat(newFilePath); err == nil { 62 | return &NFSStatusError{NFSStatusExist, os.ErrExist} 63 | } 64 | parent, err := fs.Stat(fs.Join(path...)) 65 | if err != nil { 66 | return &NFSStatusError{NFSStatusAccess, err} 67 | } else if !parent.IsDir() { 68 | return &NFSStatusError{NFSStatusNotDir, nil} 69 | } 70 | fp := userHandle.ToHandle(fs, append(path, string(obj.Filename))) 71 | 72 | switch nfs_ftype(ftype) { 73 | case FTYPE_NF3CHR: 74 | case FTYPE_NF3BLK: 75 | // read devicedata3 = {sattr3, specdata3} 76 | attrs, err := ReadSetFileAttributes(w.req.Body) 77 | if err != nil { 78 | return &NFSStatusError{NFSStatusInval, err} 79 | } 80 | specData1, err := xdr.ReadUint32(w.req.Body) 81 | if err != nil { 82 | return &NFSStatusError{NFSStatusInval, err} 83 | } 84 | specData2, err := xdr.ReadUint32(w.req.Body) 85 | if err != nil { 86 | return &NFSStatusError{NFSStatusInval, err} 87 | } 88 | 89 | err = cu.Mknod(newFilePath, uint32(attrs.Mode(parent.Mode())), specData1, specData2) 90 | if err != nil { 91 | return &NFSStatusError{NFSStatusAccess, err} 92 | } 93 | if err = attrs.Apply(cu, fs, newFilePath); err != nil { 94 | return &NFSStatusError{NFSStatusServerFault, err} 95 | } 96 | 97 | case FTYPE_NF3SOCK: 98 | // read sattr3 99 | attrs, err := ReadSetFileAttributes(w.req.Body) 100 | if err != nil { 101 | return &NFSStatusError{NFSStatusInval, err} 102 | } 103 | if err := cu.Socket(newFilePath); err != nil { 104 | return &NFSStatusError{NFSStatusAccess, err} 105 | } 106 | if err = attrs.Apply(cu, fs, newFilePath); err != nil { 107 | return &NFSStatusError{NFSStatusServerFault, err} 108 | } 109 | 110 | case FTYPE_NF3FIFO: 111 | // read sattr3 112 | attrs, err := ReadSetFileAttributes(w.req.Body) 113 | if err != nil { 114 | return &NFSStatusError{NFSStatusInval, err} 115 | } 116 | err = cu.Mkfifo(newFilePath, uint32(attrs.Mode(parent.Mode()))) 117 | if err != nil { 118 | return &NFSStatusError{NFSStatusAccess, err} 119 | } 120 | if err = attrs.Apply(cu, fs, newFilePath); err != nil { 121 | return &NFSStatusError{NFSStatusServerFault, err} 122 | } 123 | 124 | default: 125 | return &NFSStatusError{NFSStatusBadType, os.ErrInvalid} 126 | // end of input. 127 | } 128 | 129 | writer := bytes.NewBuffer([]byte{}) 130 | if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil { 131 | return &NFSStatusError{NFSStatusServerFault, err} 132 | } 133 | 134 | // "handle follows" 135 | if err := xdr.Write(writer, uint32(1)); err != nil { 136 | return &NFSStatusError{NFSStatusServerFault, err} 137 | } 138 | // fh3 139 | if err := xdr.Write(writer, fp); err != nil { 140 | return &NFSStatusError{NFSStatusServerFault, err} 141 | } 142 | // attr 143 | if err := WritePostOpAttrs(writer, tryStat(fs, append(path, string(obj.Filename)))); err != nil { 144 | return &NFSStatusError{NFSStatusServerFault, err} 145 | } 146 | // wcc 147 | if err := WriteWcc(writer, nil, tryStat(fs, path)); err != nil { 148 | return &NFSStatusError{NFSStatusServerFault, err} 149 | } 150 | 151 | if err := w.Write(writer.Bytes()); err != nil { 152 | return &NFSStatusError{NFSStatusServerFault, err} 153 | } 154 | 155 | return nil 156 | } 157 | -------------------------------------------------------------------------------- /nfs_onpathconf.go: -------------------------------------------------------------------------------- 1 | package nfs 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | 7 | "github.com/willscott/go-nfs-client/nfs/xdr" 8 | ) 9 | 10 | // PathNameMax is the maximum length for a file name 11 | const PathNameMax = 255 12 | 13 | func onPathConf(ctx context.Context, w *response, userHandle Handler) error { 14 | roothandle, err := xdr.ReadOpaque(w.req.Body) 15 | if err != nil { 16 | return &NFSStatusError{NFSStatusInval, err} 17 | } 18 | fs, path, err := userHandle.FromHandle(roothandle) 19 | if err != nil { 20 | return &NFSStatusError{NFSStatusStale, err} 21 | } 22 | 23 | writer := bytes.NewBuffer([]byte{}) 24 | if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil { 25 | return &NFSStatusError{NFSStatusServerFault, err} 26 | } 27 | if err := WritePostOpAttrs(writer, tryStat(fs, path)); err != nil { 28 | return &NFSStatusError{NFSStatusServerFault, err} 29 | } 30 | 31 | type PathConf struct { 32 | LinkMax uint32 33 | NameMax uint32 34 | NoTrunc uint32 35 | ChownRestricted uint32 36 | CaseInsensitive uint32 37 | CasePreserving uint32 38 | } 39 | 40 | defaults := PathConf{ 41 | LinkMax: 1, 42 | NameMax: PathNameMax, 43 | NoTrunc: 1, 44 | ChownRestricted: 0, 45 | CaseInsensitive: 0, 46 | CasePreserving: 1, 47 | } 48 | if err := xdr.Write(writer, defaults); err != nil { 49 | return &NFSStatusError{NFSStatusServerFault, err} 50 | } 51 | if err := w.Write(writer.Bytes()); err != nil { 52 | return &NFSStatusError{NFSStatusServerFault, err} 53 | } 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /nfs_onread.go: -------------------------------------------------------------------------------- 1 | package nfs 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "io" 8 | "os" 9 | 10 | "github.com/willscott/go-nfs-client/nfs/xdr" 11 | ) 12 | 13 | type nfsReadArgs struct { 14 | Handle []byte 15 | Offset uint64 16 | Count uint32 17 | } 18 | 19 | type nfsReadResponse struct { 20 | Count uint32 21 | EOF uint32 22 | Data []byte 23 | } 24 | 25 | // MaxRead is the advertised largest buffer the server is willing to read 26 | const MaxRead = 1 << 24 27 | 28 | // CheckRead is a size where - if a request to read is larger than this, 29 | // the server will stat the file to learn it's actual size before allocating 30 | // a buffer to read into. 31 | const CheckRead = 1 << 15 32 | 33 | func onRead(ctx context.Context, w *response, userHandle Handler) error { 34 | w.errorFmt = opAttrErrorFormatter 35 | var obj nfsReadArgs 36 | err := xdr.Read(w.req.Body, &obj) 37 | if err != nil { 38 | return &NFSStatusError{NFSStatusInval, err} 39 | } 40 | fs, path, err := userHandle.FromHandle(obj.Handle) 41 | if err != nil { 42 | return &NFSStatusError{NFSStatusStale, err} 43 | } 44 | 45 | fh, err := fs.Open(fs.Join(path...)) 46 | if err != nil { 47 | if os.IsNotExist(err) { 48 | return &NFSStatusError{NFSStatusNoEnt, err} 49 | } 50 | return &NFSStatusError{NFSStatusAccess, err} 51 | } 52 | defer fh.Close() 53 | 54 | resp := nfsReadResponse{} 55 | 56 | if obj.Count > CheckRead { 57 | info, err := fs.Stat(fs.Join(path...)) 58 | if err != nil { 59 | return &NFSStatusError{NFSStatusAccess, err} 60 | } 61 | if info.Size()-int64(obj.Offset) < int64(obj.Count) { 62 | obj.Count = uint32(uint64(info.Size()) - obj.Offset) 63 | } 64 | } 65 | if obj.Count > MaxRead { 66 | obj.Count = MaxRead 67 | } 68 | resp.Data = make([]byte, obj.Count) 69 | // todo: multiple reads if size isn't full 70 | cnt, err := fh.ReadAt(resp.Data, int64(obj.Offset)) 71 | if err != nil && !errors.Is(err, io.EOF) { 72 | return &NFSStatusError{NFSStatusIO, err} 73 | } 74 | resp.Count = uint32(cnt) 75 | resp.Data = resp.Data[:resp.Count] 76 | if errors.Is(err, io.EOF) { 77 | resp.EOF = 1 78 | } 79 | 80 | writer := bytes.NewBuffer([]byte{}) 81 | if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil { 82 | return &NFSStatusError{NFSStatusServerFault, err} 83 | } 84 | if err := WritePostOpAttrs(writer, tryStat(fs, path)); err != nil { 85 | return &NFSStatusError{NFSStatusServerFault, err} 86 | } 87 | 88 | if err := xdr.Write(writer, resp); err != nil { 89 | return &NFSStatusError{NFSStatusServerFault, err} 90 | } 91 | if err := w.Write(writer.Bytes()); err != nil { 92 | return &NFSStatusError{NFSStatusServerFault, err} 93 | } 94 | return nil 95 | } 96 | -------------------------------------------------------------------------------- /nfs_onreaddir.go: -------------------------------------------------------------------------------- 1 | package nfs 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/sha256" 7 | "encoding/binary" 8 | "io" 9 | "io/fs" 10 | "os" 11 | "path" 12 | "sort" 13 | 14 | "github.com/willscott/go-nfs-client/nfs/xdr" 15 | ) 16 | 17 | type readDirArgs struct { 18 | Handle []byte 19 | Cookie uint64 20 | CookieVerif uint64 21 | Count uint32 22 | } 23 | 24 | type readDirEntity struct { 25 | FileID uint64 26 | Name []byte 27 | Cookie uint64 28 | Next bool 29 | } 30 | 31 | func onReadDir(ctx context.Context, w *response, userHandle Handler) error { 32 | w.errorFmt = opAttrErrorFormatter 33 | obj := readDirArgs{} 34 | err := xdr.Read(w.req.Body, &obj) 35 | if err != nil { 36 | return &NFSStatusError{NFSStatusInval, err} 37 | } 38 | 39 | if obj.Count < 1024 { 40 | return &NFSStatusError{NFSStatusTooSmall, io.ErrShortBuffer} 41 | } 42 | 43 | fs, p, err := userHandle.FromHandle(obj.Handle) 44 | if err != nil { 45 | return &NFSStatusError{NFSStatusStale, err} 46 | } 47 | 48 | contents, verifier, err := getDirListingWithVerifier(userHandle, obj.Handle, obj.CookieVerif) 49 | if err != nil { 50 | return err 51 | } 52 | if obj.Cookie > 0 && obj.CookieVerif > 0 && verifier != obj.CookieVerif { 53 | return &NFSStatusError{NFSStatusBadCookie, nil} 54 | } 55 | 56 | entities := make([]readDirEntity, 0) 57 | maxBytes := uint32(100) // conservative overhead measure 58 | 59 | started := obj.Cookie == 0 60 | if started { 61 | // add '.' and '..' to entities 62 | dotdotFileID := uint64(0) 63 | if len(p) > 0 { 64 | dda := tryStat(fs, p[0:len(p)-1]) 65 | if dda != nil { 66 | dotdotFileID = dda.Fileid 67 | } 68 | } 69 | dotFileID := uint64(0) 70 | da := tryStat(fs, p) 71 | if da != nil { 72 | dotFileID = da.Fileid 73 | } 74 | entities = append(entities, 75 | readDirEntity{Name: []byte("."), Cookie: 0, Next: true, FileID: dotFileID}, 76 | readDirEntity{Name: []byte(".."), Cookie: 1, Next: true, FileID: dotdotFileID}, 77 | ) 78 | } 79 | 80 | eof := true 81 | maxEntities := userHandle.HandleLimit() / 2 82 | for i, c := range contents { 83 | // cookie equates to index within contents + 2 (for '.' and '..') 84 | cookie := uint64(i + 2) 85 | if started { 86 | maxBytes += 512 // TODO: better estimation. 87 | if maxBytes > obj.Count || len(entities) > maxEntities { 88 | eof = false 89 | break 90 | } 91 | 92 | attrs := ToFileAttribute(c, path.Join(append(p, c.Name())...)) 93 | entities = append(entities, readDirEntity{ 94 | FileID: attrs.Fileid, 95 | Name: []byte(c.Name()), 96 | Cookie: cookie, 97 | Next: true, 98 | }) 99 | } else if cookie == obj.Cookie { 100 | started = true 101 | } 102 | } 103 | 104 | writer := bytes.NewBuffer([]byte{}) 105 | if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil { 106 | return &NFSStatusError{NFSStatusServerFault, err} 107 | } 108 | if err := WritePostOpAttrs(writer, tryStat(fs, p)); err != nil { 109 | return &NFSStatusError{NFSStatusServerFault, err} 110 | } 111 | 112 | if err := xdr.Write(writer, verifier); err != nil { 113 | return &NFSStatusError{NFSStatusServerFault, err} 114 | } 115 | 116 | if err := xdr.Write(writer, len(entities) > 0); err != nil { // next 117 | return &NFSStatusError{NFSStatusServerFault, err} 118 | } 119 | if len(entities) > 0 { 120 | entities[len(entities)-1].Next = false 121 | // no next for last entity 122 | 123 | for _, e := range entities { 124 | if err := xdr.Write(writer, e); err != nil { 125 | return &NFSStatusError{NFSStatusServerFault, err} 126 | } 127 | } 128 | } 129 | if err := xdr.Write(writer, eof); err != nil { 130 | return &NFSStatusError{NFSStatusServerFault, err} 131 | } 132 | // TODO: track writer size at this point to validate maxcount estimation and stop early if needed. 133 | 134 | if err := w.Write(writer.Bytes()); err != nil { 135 | return &NFSStatusError{NFSStatusServerFault, err} 136 | } 137 | return nil 138 | } 139 | 140 | func getDirListingWithVerifier(userHandle Handler, fsHandle []byte, verifier uint64) ([]fs.FileInfo, uint64, error) { 141 | // figure out what directory it is. 142 | fs, p, err := userHandle.FromHandle(fsHandle) 143 | if err != nil { 144 | return nil, 0, &NFSStatusError{NFSStatusStale, err} 145 | } 146 | 147 | path := fs.Join(p...) 148 | // see if the verifier has this dir cached: 149 | if vh, ok := userHandle.(CachingHandler); verifier != 0 && ok { 150 | entries := vh.DataForVerifier(path, verifier) 151 | if entries != nil { 152 | return entries, verifier, nil 153 | } 154 | } 155 | // load the entries. 156 | contents, err := fs.ReadDir(path) 157 | if err != nil { 158 | if os.IsPermission(err) { 159 | return nil, 0, &NFSStatusError{NFSStatusAccess, err} 160 | } 161 | return nil, 0, &NFSStatusError{NFSStatusNotDir, err} 162 | } 163 | 164 | sort.Slice(contents, func(i, j int) bool { 165 | return contents[i].Name() < contents[j].Name() 166 | }) 167 | 168 | if vh, ok := userHandle.(CachingHandler); ok { 169 | // let the user handler make a verifier if it can. 170 | v := vh.VerifierFor(path, contents) 171 | return contents, v, nil 172 | } 173 | 174 | id := hashPathAndContents(path, contents) 175 | return contents, id, nil 176 | } 177 | 178 | func hashPathAndContents(path string, contents []fs.FileInfo) uint64 { 179 | //calculate a cookie-verifier. 180 | vHash := sha256.New() 181 | 182 | // Add the path to avoid collisions of directories with the same content 183 | vHash.Write([]byte(path)) 184 | 185 | for _, c := range contents { 186 | vHash.Write([]byte(c.Name())) // Never fails according to the docs 187 | } 188 | 189 | verify := vHash.Sum(nil)[0:8] 190 | return binary.BigEndian.Uint64(verify) 191 | } 192 | -------------------------------------------------------------------------------- /nfs_onreaddirplus.go: -------------------------------------------------------------------------------- 1 | package nfs 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "path" 7 | 8 | "github.com/willscott/go-nfs-client/nfs/xdr" 9 | ) 10 | 11 | type readDirPlusArgs struct { 12 | Handle []byte 13 | Cookie uint64 14 | CookieVerif uint64 15 | DirCount uint32 16 | MaxCount uint32 17 | } 18 | 19 | type readDirPlusEntity struct { 20 | FileID uint64 21 | Name []byte 22 | Cookie uint64 23 | Attributes *FileAttribute `xdr:"optional"` 24 | Handle *[]byte `xdr:"optional"` 25 | Next bool 26 | } 27 | 28 | func joinPath(parent []string, elements ...string) []string { 29 | joinedPath := make([]string, 0, len(parent)+len(elements)) 30 | joinedPath = append(joinedPath, parent...) 31 | joinedPath = append(joinedPath, elements...) 32 | return joinedPath 33 | } 34 | 35 | func onReadDirPlus(ctx context.Context, w *response, userHandle Handler) error { 36 | w.errorFmt = opAttrErrorFormatter 37 | obj := readDirPlusArgs{} 38 | if err := xdr.Read(w.req.Body, &obj); err != nil { 39 | return &NFSStatusError{NFSStatusInval, err} 40 | } 41 | 42 | // in case of test, nfs-client send: 43 | // DirCount = 512 44 | // MaxCount = 4096 45 | if obj.DirCount < 512 || obj.MaxCount < 4096 { 46 | return &NFSStatusError{NFSStatusTooSmall, nil} 47 | } 48 | 49 | fs, p, err := userHandle.FromHandle(obj.Handle) 50 | if err != nil { 51 | return &NFSStatusError{NFSStatusStale, err} 52 | } 53 | 54 | contents, verifier, err := getDirListingWithVerifier(userHandle, obj.Handle, obj.CookieVerif) 55 | if err != nil { 56 | return err 57 | } 58 | if obj.Cookie > 0 && obj.CookieVerif > 0 && verifier != obj.CookieVerif { 59 | return &NFSStatusError{NFSStatusBadCookie, nil} 60 | } 61 | 62 | entities := make([]readDirPlusEntity, 0) 63 | dirBytes := uint32(0) 64 | maxBytes := uint32(100) // conservative overhead measure 65 | 66 | started := obj.Cookie == 0 67 | if started { 68 | // add '.' and '..' to entities 69 | dotdotFileID := uint64(0) 70 | if len(p) > 0 { 71 | dda := tryStat(fs, p[0:len(p)-1]) 72 | if dda != nil { 73 | dotdotFileID = dda.Fileid 74 | } 75 | } 76 | dotFileID := uint64(0) 77 | da := tryStat(fs, p) 78 | if da != nil { 79 | dotFileID = da.Fileid 80 | } 81 | entities = append(entities, 82 | readDirPlusEntity{Name: []byte("."), Cookie: 0, Next: true, FileID: dotFileID, Attributes: da}, 83 | readDirPlusEntity{Name: []byte(".."), Cookie: 1, Next: true, FileID: dotdotFileID}, 84 | ) 85 | } 86 | 87 | eof := true 88 | maxEntities := userHandle.HandleLimit() / 2 89 | fb := 0 90 | fss := 0 91 | for i, c := range contents { 92 | // cookie equates to index within contents + 2 (for '.' and '..') 93 | cookie := uint64(i + 2) 94 | fb++ 95 | if started { 96 | fss++ 97 | dirBytes += uint32(len(c.Name()) + 20) 98 | maxBytes += 512 // TODO: better estimation. 99 | if dirBytes > obj.DirCount || maxBytes > obj.MaxCount || len(entities) > maxEntities { 100 | eof = false 101 | break 102 | } 103 | 104 | filePath := joinPath(p, c.Name()) 105 | handle := userHandle.ToHandle(fs, filePath) 106 | attrs := ToFileAttribute(c, path.Join(filePath...)) 107 | entities = append(entities, readDirPlusEntity{ 108 | FileID: attrs.Fileid, 109 | Name: []byte(c.Name()), 110 | Cookie: cookie, 111 | Attributes: attrs, 112 | Handle: &handle, 113 | Next: true, 114 | }) 115 | } else if cookie == obj.Cookie { 116 | started = true 117 | } 118 | } 119 | 120 | writer := bytes.NewBuffer([]byte{}) 121 | if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil { 122 | return &NFSStatusError{NFSStatusServerFault, err} 123 | } 124 | if err := WritePostOpAttrs(writer, tryStat(fs, p)); err != nil { 125 | return &NFSStatusError{NFSStatusServerFault, err} 126 | } 127 | if err := xdr.Write(writer, verifier); err != nil { 128 | return &NFSStatusError{NFSStatusServerFault, err} 129 | } 130 | 131 | if err := xdr.Write(writer, len(entities) > 0); err != nil { // next 132 | return &NFSStatusError{NFSStatusServerFault, err} 133 | } 134 | if len(entities) > 0 { 135 | entities[len(entities)-1].Next = false 136 | // no next for last entity 137 | 138 | for _, e := range entities { 139 | if err := xdr.Write(writer, e); err != nil { 140 | return &NFSStatusError{NFSStatusServerFault, err} 141 | } 142 | } 143 | } 144 | if err := xdr.Write(writer, eof); err != nil { 145 | return &NFSStatusError{NFSStatusServerFault, err} 146 | } 147 | // TODO: track writer size at this point to validate maxcount estimation and stop early if needed. 148 | 149 | if err := w.Write(writer.Bytes()); err != nil { 150 | return &NFSStatusError{NFSStatusServerFault, err} 151 | } 152 | return nil 153 | } 154 | -------------------------------------------------------------------------------- /nfs_onreadlink.go: -------------------------------------------------------------------------------- 1 | package nfs 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "os" 7 | 8 | "github.com/willscott/go-nfs-client/nfs/xdr" 9 | ) 10 | 11 | func onReadLink(ctx context.Context, w *response, userHandle Handler) error { 12 | w.errorFmt = opAttrErrorFormatter 13 | handle, err := xdr.ReadOpaque(w.req.Body) 14 | if err != nil { 15 | return &NFSStatusError{NFSStatusInval, err} 16 | } 17 | fs, path, err := userHandle.FromHandle(handle) 18 | if err != nil { 19 | return &NFSStatusError{NFSStatusStale, err} 20 | } 21 | 22 | out, err := fs.Readlink(fs.Join(path...)) 23 | if err != nil { 24 | if info, err := fs.Stat(fs.Join(path...)); err == nil { 25 | if info.Mode()&os.ModeSymlink == 0 { 26 | return &NFSStatusError{NFSStatusInval, err} 27 | } 28 | } 29 | if os.IsNotExist(err) { 30 | return &NFSStatusError{NFSStatusNoEnt, err} 31 | } 32 | 33 | return &NFSStatusError{NFSStatusAccess, err} 34 | } 35 | 36 | writer := bytes.NewBuffer([]byte{}) 37 | if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil { 38 | return &NFSStatusError{NFSStatusServerFault, err} 39 | } 40 | if err := WritePostOpAttrs(writer, tryStat(fs, path)); err != nil { 41 | return &NFSStatusError{NFSStatusServerFault, err} 42 | } 43 | 44 | if err := xdr.Write(writer, out); err != nil { 45 | return &NFSStatusError{NFSStatusServerFault, err} 46 | } 47 | if err := w.Write(writer.Bytes()); err != nil { 48 | return &NFSStatusError{NFSStatusServerFault, err} 49 | } 50 | return nil 51 | } 52 | -------------------------------------------------------------------------------- /nfs_onremove.go: -------------------------------------------------------------------------------- 1 | package nfs 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "os" 7 | 8 | "github.com/go-git/go-billy/v5" 9 | "github.com/willscott/go-nfs-client/nfs/xdr" 10 | ) 11 | 12 | func onRemove(ctx context.Context, w *response, userHandle Handler) error { 13 | w.errorFmt = wccDataErrorFormatter 14 | obj := DirOpArg{} 15 | if err := xdr.Read(w.req.Body, &obj); err != nil { 16 | return &NFSStatusError{NFSStatusInval, err} 17 | } 18 | fs, path, err := userHandle.FromHandle(obj.Handle) 19 | if err != nil { 20 | return &NFSStatusError{NFSStatusStale, err} 21 | } 22 | 23 | if !billy.CapabilityCheck(fs, billy.WriteCapability) { 24 | return &NFSStatusError{NFSStatusROFS, os.ErrPermission} 25 | } 26 | 27 | if len(string(obj.Filename)) > PathNameMax { 28 | return &NFSStatusError{NFSStatusNameTooLong, nil} 29 | } 30 | 31 | fullPath := fs.Join(path...) 32 | dirInfo, err := fs.Stat(fullPath) 33 | if err != nil { 34 | if os.IsNotExist(err) { 35 | return &NFSStatusError{NFSStatusNoEnt, err} 36 | } 37 | if os.IsPermission(err) { 38 | return &NFSStatusError{NFSStatusAccess, err} 39 | } 40 | return &NFSStatusError{NFSStatusIO, err} 41 | } 42 | if !dirInfo.IsDir() { 43 | return &NFSStatusError{NFSStatusNotDir, nil} 44 | } 45 | preCacheData := ToFileAttribute(dirInfo, fullPath).AsCache() 46 | 47 | toDelete := fs.Join(append(path, string(obj.Filename))...) 48 | toDeleteHandle := userHandle.ToHandle(fs, append(path, string(obj.Filename))) 49 | 50 | err = fs.Remove(toDelete) 51 | if err != nil { 52 | if os.IsNotExist(err) { 53 | return &NFSStatusError{NFSStatusNoEnt, err} 54 | } 55 | if os.IsPermission(err) { 56 | return &NFSStatusError{NFSStatusAccess, err} 57 | } 58 | return &NFSStatusError{NFSStatusIO, err} 59 | } 60 | 61 | if err := userHandle.InvalidateHandle(fs, toDeleteHandle); err != nil { 62 | return &NFSStatusError{NFSStatusServerFault, err} 63 | } 64 | 65 | writer := bytes.NewBuffer([]byte{}) 66 | if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil { 67 | return &NFSStatusError{NFSStatusServerFault, err} 68 | } 69 | 70 | if err := WriteWcc(writer, preCacheData, tryStat(fs, path)); err != nil { 71 | return &NFSStatusError{NFSStatusServerFault, err} 72 | } 73 | 74 | if err := w.Write(writer.Bytes()); err != nil { 75 | return &NFSStatusError{NFSStatusServerFault, err} 76 | } 77 | return nil 78 | } 79 | -------------------------------------------------------------------------------- /nfs_onrename.go: -------------------------------------------------------------------------------- 1 | package nfs 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "os" 7 | "reflect" 8 | 9 | "github.com/go-git/go-billy/v5" 10 | "github.com/willscott/go-nfs-client/nfs/xdr" 11 | ) 12 | 13 | var doubleWccErrorBody = [16]byte{} 14 | 15 | func onRename(ctx context.Context, w *response, userHandle Handler) error { 16 | w.errorFmt = errFormatterWithBody(doubleWccErrorBody[:]) 17 | from := DirOpArg{} 18 | err := xdr.Read(w.req.Body, &from) 19 | if err != nil { 20 | return &NFSStatusError{NFSStatusInval, err} 21 | } 22 | fs, fromPath, err := userHandle.FromHandle(from.Handle) 23 | if err != nil { 24 | return &NFSStatusError{NFSStatusStale, err} 25 | } 26 | 27 | to := DirOpArg{} 28 | if err = xdr.Read(w.req.Body, &to); err != nil { 29 | return &NFSStatusError{NFSStatusInval, err} 30 | } 31 | fs2, toPath, err := userHandle.FromHandle(to.Handle) 32 | if err != nil { 33 | return &NFSStatusError{NFSStatusStale, err} 34 | } 35 | // check the two fs are the same 36 | if !reflect.DeepEqual(fs, fs2) { 37 | return &NFSStatusError{NFSStatusNotSupp, os.ErrPermission} 38 | } 39 | 40 | if !billy.CapabilityCheck(fs, billy.WriteCapability) { 41 | return &NFSStatusError{NFSStatusROFS, os.ErrPermission} 42 | } 43 | 44 | if len(string(from.Filename)) > PathNameMax || len(string(to.Filename)) > PathNameMax { 45 | return &NFSStatusError{NFSStatusNameTooLong, os.ErrInvalid} 46 | } 47 | 48 | fromDirPath := fs.Join(fromPath...) 49 | fromDirInfo, err := fs.Stat(fromDirPath) 50 | if err != nil { 51 | if os.IsNotExist(err) { 52 | return &NFSStatusError{NFSStatusNoEnt, err} 53 | } 54 | return &NFSStatusError{NFSStatusIO, err} 55 | } 56 | if !fromDirInfo.IsDir() { 57 | return &NFSStatusError{NFSStatusNotDir, nil} 58 | } 59 | preCacheData := ToFileAttribute(fromDirInfo, fromDirPath).AsCache() 60 | 61 | toDirPath := fs.Join(toPath...) 62 | toDirInfo, err := fs.Stat(toDirPath) 63 | if err != nil { 64 | if os.IsNotExist(err) { 65 | return &NFSStatusError{NFSStatusNoEnt, err} 66 | } 67 | return &NFSStatusError{NFSStatusIO, err} 68 | } 69 | if !toDirInfo.IsDir() { 70 | return &NFSStatusError{NFSStatusNotDir, nil} 71 | } 72 | preDestData := ToFileAttribute(toDirInfo, toDirPath).AsCache() 73 | 74 | oldHandle := userHandle.ToHandle(fs, append(fromPath, string(from.Filename))) 75 | 76 | fromLoc := fs.Join(append(fromPath, string(from.Filename))...) 77 | toLoc := fs.Join(append(toPath, string(to.Filename))...) 78 | 79 | err = fs.Rename(fromLoc, toLoc) 80 | if err != nil { 81 | if os.IsNotExist(err) { 82 | return &NFSStatusError{NFSStatusNoEnt, err} 83 | } 84 | if os.IsPermission(err) { 85 | return &NFSStatusError{NFSStatusAccess, err} 86 | } 87 | return &NFSStatusError{NFSStatusIO, err} 88 | } 89 | 90 | if err := userHandle.InvalidateHandle(fs, oldHandle); err != nil { 91 | return &NFSStatusError{NFSStatusServerFault, err} 92 | } 93 | 94 | writer := bytes.NewBuffer([]byte{}) 95 | if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil { 96 | return &NFSStatusError{NFSStatusServerFault, err} 97 | } 98 | 99 | if err := WriteWcc(writer, preCacheData, tryStat(fs, fromPath)); err != nil { 100 | return &NFSStatusError{NFSStatusServerFault, err} 101 | } 102 | if err := WriteWcc(writer, preDestData, tryStat(fs, toPath)); err != nil { 103 | return &NFSStatusError{NFSStatusServerFault, err} 104 | } 105 | 106 | if err := w.Write(writer.Bytes()); err != nil { 107 | return &NFSStatusError{NFSStatusServerFault, err} 108 | } 109 | return nil 110 | } 111 | -------------------------------------------------------------------------------- /nfs_onrmdir.go: -------------------------------------------------------------------------------- 1 | package nfs 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | func onRmDir(ctx context.Context, w *response, userHandle Handler) error { 8 | return onRemove(ctx, w, userHandle) 9 | } 10 | -------------------------------------------------------------------------------- /nfs_onsetattr.go: -------------------------------------------------------------------------------- 1 | package nfs 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "os" 7 | 8 | "github.com/go-git/go-billy/v5" 9 | "github.com/willscott/go-nfs-client/nfs/xdr" 10 | ) 11 | 12 | func onSetAttr(ctx context.Context, w *response, userHandle Handler) error { 13 | w.errorFmt = wccDataErrorFormatter 14 | handle, err := xdr.ReadOpaque(w.req.Body) 15 | if err != nil { 16 | return &NFSStatusError{NFSStatusInval, err} 17 | } 18 | 19 | fs, path, err := userHandle.FromHandle(handle) 20 | if err != nil { 21 | return &NFSStatusError{NFSStatusStale, err} 22 | } 23 | attrs, err := ReadSetFileAttributes(w.req.Body) 24 | if err != nil { 25 | return &NFSStatusError{NFSStatusInval, err} 26 | } 27 | 28 | fullPath := fs.Join(path...) 29 | info, err := fs.Lstat(fullPath) 30 | if err != nil { 31 | if os.IsNotExist(err) { 32 | return &NFSStatusError{NFSStatusNoEnt, err} 33 | } 34 | return &NFSStatusError{NFSStatusAccess, err} 35 | } 36 | 37 | // see if there's a "guard" 38 | if guard, err := xdr.ReadUint32(w.req.Body); err != nil { 39 | return &NFSStatusError{NFSStatusInval, err} 40 | } else if guard != 0 { 41 | // read the ctime. 42 | t := FileTime{} 43 | if err := xdr.Read(w.req.Body, &t); err != nil { 44 | return &NFSStatusError{NFSStatusInval, err} 45 | } 46 | attr := ToFileAttribute(info, fullPath) 47 | if t != attr.Ctime { 48 | return &NFSStatusError{NFSStatusNotSync, nil} 49 | } 50 | } 51 | 52 | if !billy.CapabilityCheck(fs, billy.WriteCapability) { 53 | return &NFSStatusError{NFSStatusROFS, os.ErrPermission} 54 | } 55 | 56 | changer := userHandle.Change(fs) 57 | if err := attrs.Apply(changer, fs, fs.Join(path...)); err != nil { 58 | // Already an nfsstatuserror 59 | return err 60 | } 61 | 62 | preAttr := ToFileAttribute(info, fullPath).AsCache() 63 | 64 | writer := bytes.NewBuffer([]byte{}) 65 | if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil { 66 | return &NFSStatusError{NFSStatusServerFault, err} 67 | } 68 | if err := WriteWcc(writer, preAttr, tryStat(fs, path)); err != nil { 69 | return &NFSStatusError{NFSStatusServerFault, err} 70 | } 71 | 72 | if err := w.Write(writer.Bytes()); err != nil { 73 | return &NFSStatusError{NFSStatusServerFault, err} 74 | } 75 | return nil 76 | } 77 | -------------------------------------------------------------------------------- /nfs_onsymlink.go: -------------------------------------------------------------------------------- 1 | package nfs 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "os" 7 | 8 | "github.com/go-git/go-billy/v5" 9 | "github.com/willscott/go-nfs-client/nfs/xdr" 10 | ) 11 | 12 | func onSymlink(ctx context.Context, w *response, userHandle Handler) error { 13 | w.errorFmt = wccDataErrorFormatter 14 | obj := DirOpArg{} 15 | err := xdr.Read(w.req.Body, &obj) 16 | if err != nil { 17 | return &NFSStatusError{NFSStatusInval, err} 18 | } 19 | attrs, err := ReadSetFileAttributes(w.req.Body) 20 | if err != nil { 21 | return &NFSStatusError{NFSStatusInval, err} 22 | } 23 | 24 | target, err := xdr.ReadOpaque(w.req.Body) 25 | if err != nil { 26 | return &NFSStatusError{NFSStatusInval, err} 27 | } 28 | 29 | fs, path, err := userHandle.FromHandle(obj.Handle) 30 | if err != nil { 31 | return &NFSStatusError{NFSStatusStale, err} 32 | } 33 | if !billy.CapabilityCheck(fs, billy.WriteCapability) { 34 | return &NFSStatusError{NFSStatusROFS, os.ErrPermission} 35 | } 36 | 37 | if len(string(obj.Filename)) > PathNameMax { 38 | return &NFSStatusError{NFSStatusNameTooLong, os.ErrInvalid} 39 | } 40 | 41 | newFilePath := fs.Join(append(path, string(obj.Filename))...) 42 | if _, err := fs.Stat(newFilePath); err == nil { 43 | return &NFSStatusError{NFSStatusExist, os.ErrExist} 44 | } 45 | if s, err := fs.Stat(fs.Join(path...)); err != nil { 46 | return &NFSStatusError{NFSStatusAccess, err} 47 | } else if !s.IsDir() { 48 | return &NFSStatusError{NFSStatusNotDir, nil} 49 | } 50 | 51 | err = fs.Symlink(string(target), newFilePath) 52 | if err != nil { 53 | return &NFSStatusError{NFSStatusAccess, err} 54 | } 55 | 56 | fp := userHandle.ToHandle(fs, append(path, string(obj.Filename))) 57 | changer := userHandle.Change(fs) 58 | if changer != nil { 59 | if err := attrs.Apply(changer, fs, newFilePath); err != nil { 60 | return &NFSStatusError{NFSStatusIO, err} 61 | } 62 | } 63 | 64 | writer := bytes.NewBuffer([]byte{}) 65 | if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil { 66 | return &NFSStatusError{NFSStatusServerFault, err} 67 | } 68 | 69 | // "handle follows" 70 | if err := xdr.Write(writer, uint32(1)); err != nil { 71 | return &NFSStatusError{NFSStatusServerFault, err} 72 | } 73 | if err := xdr.Write(writer, fp); err != nil { 74 | return &NFSStatusError{NFSStatusServerFault, err} 75 | } 76 | if err := WritePostOpAttrs(writer, tryStat(fs, append(path, string(obj.Filename)))); err != nil { 77 | return &NFSStatusError{NFSStatusServerFault, err} 78 | } 79 | 80 | if err := WriteWcc(writer, nil, tryStat(fs, path)); err != nil { 81 | return &NFSStatusError{NFSStatusServerFault, err} 82 | } 83 | 84 | if err := w.Write(writer.Bytes()); err != nil { 85 | return &NFSStatusError{NFSStatusServerFault, err} 86 | } 87 | return nil 88 | } 89 | -------------------------------------------------------------------------------- /nfs_onwrite.go: -------------------------------------------------------------------------------- 1 | package nfs 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "io" 7 | "math" 8 | "os" 9 | 10 | "github.com/go-git/go-billy/v5" 11 | "github.com/willscott/go-nfs-client/nfs/xdr" 12 | ) 13 | 14 | // writeStability is the level of durability requested with the write 15 | type writeStability uint32 16 | 17 | const ( 18 | unstable writeStability = 0 19 | dataSync writeStability = 1 20 | fileSync writeStability = 2 21 | ) 22 | 23 | type writeArgs struct { 24 | Handle []byte 25 | Offset uint64 26 | Count uint32 27 | How uint32 28 | Data []byte 29 | } 30 | 31 | func onWrite(ctx context.Context, w *response, userHandle Handler) error { 32 | w.errorFmt = wccDataErrorFormatter 33 | var req writeArgs 34 | if err := xdr.Read(w.req.Body, &req); err != nil { 35 | return &NFSStatusError{NFSStatusInval, err} 36 | } 37 | 38 | fs, path, err := userHandle.FromHandle(req.Handle) 39 | if err != nil { 40 | return &NFSStatusError{NFSStatusStale, err} 41 | } 42 | if !billy.CapabilityCheck(fs, billy.WriteCapability) { 43 | return &NFSStatusError{NFSStatusROFS, os.ErrPermission} 44 | } 45 | if len(req.Data) > math.MaxInt32 || req.Count > math.MaxInt32 { 46 | return &NFSStatusError{NFSStatusFBig, os.ErrInvalid} 47 | } 48 | if req.How != uint32(unstable) && req.How != uint32(dataSync) && req.How != uint32(fileSync) { 49 | return &NFSStatusError{NFSStatusInval, os.ErrInvalid} 50 | } 51 | 52 | // stat first for pre-op wcc. 53 | fullPath := fs.Join(path...) 54 | info, err := fs.Stat(fullPath) 55 | if err != nil { 56 | if os.IsNotExist(err) { 57 | return &NFSStatusError{NFSStatusNoEnt, err} 58 | } 59 | return &NFSStatusError{NFSStatusAccess, err} 60 | } 61 | if !info.Mode().IsRegular() { 62 | return &NFSStatusError{NFSStatusInval, os.ErrInvalid} 63 | } 64 | preOpCache := ToFileAttribute(info, fullPath).AsCache() 65 | 66 | // now the actual op. 67 | file, err := fs.OpenFile(fs.Join(path...), os.O_RDWR, info.Mode().Perm()) 68 | if err != nil { 69 | return &NFSStatusError{NFSStatusAccess, err} 70 | } 71 | if req.Offset > 0 { 72 | if _, err := file.Seek(int64(req.Offset), io.SeekStart); err != nil { 73 | return &NFSStatusError{NFSStatusIO, err} 74 | } 75 | } 76 | end := req.Count 77 | if len(req.Data) < int(end) { 78 | end = uint32(len(req.Data)) 79 | } 80 | writtenCount, err := file.Write(req.Data[:end]) 81 | if err != nil { 82 | Log.Errorf("Error writing: %v", err) 83 | return &NFSStatusError{NFSStatusIO, err} 84 | } 85 | if err := file.Close(); err != nil { 86 | Log.Errorf("error closing: %v", err) 87 | return &NFSStatusError{NFSStatusIO, err} 88 | } 89 | 90 | writer := bytes.NewBuffer([]byte{}) 91 | if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil { 92 | return &NFSStatusError{NFSStatusServerFault, err} 93 | } 94 | 95 | if err := WriteWcc(writer, preOpCache, tryStat(fs, path)); err != nil { 96 | return &NFSStatusError{NFSStatusServerFault, err} 97 | } 98 | if err := xdr.Write(writer, uint32(writtenCount)); err != nil { 99 | return &NFSStatusError{NFSStatusServerFault, err} 100 | } 101 | if err := xdr.Write(writer, fileSync); err != nil { 102 | return &NFSStatusError{NFSStatusServerFault, err} 103 | } 104 | if err := xdr.Write(writer, w.Server.ID); err != nil { 105 | return &NFSStatusError{NFSStatusServerFault, err} 106 | } 107 | 108 | if err := w.Write(writer.Bytes()); err != nil { 109 | return &NFSStatusError{NFSStatusServerFault, err} 110 | } 111 | return nil 112 | } 113 | -------------------------------------------------------------------------------- /nfs_test.go: -------------------------------------------------------------------------------- 1 | package nfs_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "math/rand" 7 | "net" 8 | "os" 9 | "reflect" 10 | "sort" 11 | "sync" 12 | "testing" 13 | 14 | "github.com/go-git/go-billy/v5" 15 | nfs "github.com/willscott/go-nfs" 16 | "github.com/willscott/go-nfs/helpers" 17 | "github.com/willscott/go-nfs/helpers/memfs" 18 | 19 | nfsc "github.com/willscott/go-nfs-client/nfs" 20 | rpc "github.com/willscott/go-nfs-client/nfs/rpc" 21 | "github.com/willscott/go-nfs-client/nfs/util" 22 | "github.com/willscott/go-nfs-client/nfs/xdr" 23 | ) 24 | 25 | type OpenArgs struct { 26 | File string 27 | Flag int 28 | Perm os.FileMode 29 | } 30 | 31 | func (o *OpenArgs) String() string { 32 | return fmt.Sprintf("\"%s\"; %05xd %s", o.File, o.Flag, o.Perm) 33 | } 34 | 35 | // NewTrackingFS wraps fs to detect file handle leaks. 36 | func NewTrackingFS(fs billy.Filesystem) *trackingFS { 37 | return &trackingFS{Filesystem: fs, open: make(map[int64]OpenArgs)} 38 | } 39 | 40 | // trackingFS wraps a Filesystem to detect file handle leaks. 41 | type trackingFS struct { 42 | billy.Filesystem 43 | mu sync.Mutex 44 | open map[int64]OpenArgs 45 | } 46 | 47 | func (t *trackingFS) ListOpened() []OpenArgs { 48 | t.mu.Lock() 49 | defer t.mu.Unlock() 50 | ret := make([]OpenArgs, 0, len(t.open)) 51 | for _, o := range t.open { 52 | ret = append(ret, o) 53 | } 54 | return ret 55 | } 56 | 57 | func (t *trackingFS) Create(filename string) (billy.File, error) { 58 | return t.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) 59 | } 60 | 61 | func (t *trackingFS) Open(filename string) (billy.File, error) { 62 | return t.OpenFile(filename, os.O_RDONLY, 0) 63 | } 64 | 65 | func (t *trackingFS) OpenFile(filename string, flag int, perm os.FileMode) (billy.File, error) { 66 | open, err := t.Filesystem.OpenFile(filename, flag, perm) 67 | if err != nil { 68 | return nil, err 69 | } 70 | t.mu.Lock() 71 | defer t.mu.Unlock() 72 | id := rand.Int63() 73 | t.open[id] = OpenArgs{filename, flag, perm} 74 | closer := func() { 75 | delete(t.open, id) 76 | } 77 | open = &trackingFile{ 78 | File: open, 79 | onClose: closer, 80 | } 81 | return open, err 82 | } 83 | 84 | type trackingFile struct { 85 | billy.File 86 | onClose func() 87 | } 88 | 89 | func (f *trackingFile) Close() error { 90 | f.onClose() 91 | return f.File.Close() 92 | } 93 | 94 | func TestNFS(t *testing.T) { 95 | if testing.Verbose() { 96 | util.DefaultLogger.SetDebug(true) 97 | } 98 | 99 | // make an empty in-memory server. 100 | listener, err := net.Listen("tcp", "localhost:0") 101 | if err != nil { 102 | t.Fatal(err) 103 | } 104 | 105 | mem := NewTrackingFS(memfs.New()) 106 | 107 | defer func() { 108 | if opened := mem.ListOpened(); len(opened) > 0 { 109 | t.Errorf("Unclosed files: %v", opened) 110 | } 111 | }() 112 | 113 | // File needs to exist in the root for memfs to acknowledge the root exists. 114 | r, _ := mem.Create("/test") 115 | r.Close() 116 | 117 | handler := helpers.NewNullAuthHandler(mem) 118 | cacheHelper := helpers.NewCachingHandler(handler, 1024) 119 | go func() { 120 | _ = nfs.Serve(listener, cacheHelper) 121 | }() 122 | 123 | c, err := rpc.DialTCP(listener.Addr().Network(), listener.Addr().(*net.TCPAddr).String(), false) 124 | if err != nil { 125 | t.Fatal(err) 126 | } 127 | defer c.Close() 128 | 129 | var mounter nfsc.Mount 130 | mounter.Client = c 131 | target, err := mounter.Mount("/", rpc.AuthNull) 132 | if err != nil { 133 | t.Fatal(err) 134 | } 135 | defer func() { 136 | _ = mounter.Unmount() 137 | }() 138 | 139 | _, err = target.FSInfo() 140 | if err != nil { 141 | t.Fatal(err) 142 | } 143 | 144 | // Validate sample file creation 145 | _, err = target.Create("/helloworld.txt", 0666) 146 | if err != nil { 147 | t.Fatal(err) 148 | } 149 | if info, err := mem.Stat("/helloworld.txt"); err != nil { 150 | t.Fatal(err) 151 | } else { 152 | if info.Size() != 0 || info.Mode().Perm() != 0666 { 153 | t.Fatal("incorrect creation.") 154 | } 155 | } 156 | 157 | // Validate writing to a file. 158 | f, err := target.OpenFile("/helloworld.txt", 0666) 159 | if err != nil { 160 | t.Fatal(err) 161 | } 162 | defer f.Close() 163 | b := []byte("hello world") 164 | _, err = f.Write(b) 165 | if err != nil { 166 | t.Fatal(err) 167 | } 168 | 169 | mf, err := target.Open("/helloworld.txt") 170 | if err != nil { 171 | t.Fatal(err) 172 | } 173 | defer mf.Close() 174 | buf := make([]byte, len(b)) 175 | if _, err = mf.Read(buf[:]); err != nil { 176 | t.Fatal(err) 177 | } 178 | if !bytes.Equal(buf, b) { 179 | t.Fatal("written does not match expected") 180 | } 181 | 182 | // for test nfs.ReadDirPlus in case of many files 183 | dirF1, err := mem.ReadDir("/") 184 | if err != nil { 185 | t.Fatal(err) 186 | } 187 | shouldBeNames := []string{} 188 | for _, f := range dirF1 { 189 | shouldBeNames = append(shouldBeNames, f.Name()) 190 | } 191 | for i := 0; i < 2000; i++ { 192 | fName := fmt.Sprintf("f-%04d.txt", i) 193 | shouldBeNames = append(shouldBeNames, fName) 194 | f, err := mem.Create(fName) 195 | if err != nil { 196 | t.Fatal(err) 197 | } 198 | f.Close() 199 | } 200 | 201 | manyEntitiesPlus, err := target.ReadDirPlus("/") 202 | if err != nil { 203 | t.Fatal(err) 204 | } 205 | actualBeNamesPlus := []string{} 206 | for _, e := range manyEntitiesPlus { 207 | actualBeNamesPlus = append(actualBeNamesPlus, e.Name()) 208 | } 209 | 210 | as := sort.StringSlice(shouldBeNames) 211 | bs := sort.StringSlice(actualBeNamesPlus) 212 | as.Sort() 213 | bs.Sort() 214 | if !reflect.DeepEqual(as, bs) { 215 | t.Fatal("nfs.ReadDirPlus error") 216 | } 217 | 218 | // for test nfs.ReadDir in case of many files 219 | manyEntities, err := readDir(target, "/") 220 | if err != nil { 221 | t.Fatal(err) 222 | } 223 | actualBeNames := []string{} 224 | for _, e := range manyEntities { 225 | actualBeNames = append(actualBeNames, e.FileName) 226 | } 227 | 228 | as2 := sort.StringSlice(shouldBeNames) 229 | bs2 := sort.StringSlice(actualBeNames) 230 | as2.Sort() 231 | bs2.Sort() 232 | if !reflect.DeepEqual(as2, bs2) { 233 | fmt.Printf("should be %v\n", as2) 234 | fmt.Printf("actual be %v\n", bs2) 235 | t.Fatal("nfs.ReadDir error") 236 | } 237 | 238 | // confirm rename works as expected 239 | oldFA, _, err := target.Lookup("/f-0010.txt", false) 240 | if err != nil { 241 | t.Fatal(err) 242 | } 243 | 244 | if err := target.Rename("/f-0010.txt", "/g-0010.txt"); err != nil { 245 | t.Fatal(err) 246 | } 247 | new, _, err := target.Lookup("/g-0010.txt", false) 248 | if err != nil { 249 | t.Fatal(err) 250 | } 251 | if new.Sys() != oldFA.Sys() { 252 | t.Fatal("rename failed to update") 253 | } 254 | _, _, err = target.Lookup("/f-0010.txt", false) 255 | if err == nil { 256 | t.Fatal("old handle should be invalid") 257 | } 258 | 259 | // for test nfs.ReadDirPlus in case of empty directory 260 | _, err = target.Mkdir("/empty", 0755) 261 | if err != nil { 262 | t.Fatal(err) 263 | } 264 | 265 | emptyEntitiesPlus, err := target.ReadDirPlus("/empty") 266 | if err != nil { 267 | t.Fatal(err) 268 | } 269 | if len(emptyEntitiesPlus) != 0 { 270 | t.Fatal("nfs.ReadDirPlus error reading empty dir") 271 | } 272 | 273 | // for test nfs.ReadDir in case of empty directory 274 | emptyEntities, err := readDir(target, "/empty") 275 | if err != nil { 276 | t.Fatal(err) 277 | } 278 | if len(emptyEntities) != 0 { 279 | t.Fatal("nfs.ReadDir error reading empty dir") 280 | } 281 | } 282 | 283 | type readDirEntry struct { 284 | FileId uint64 285 | FileName string 286 | Cookie uint64 287 | } 288 | 289 | // readDir implementation "appropriated" from go-nfs-client implementation of READDIRPLUS 290 | func readDir(target *nfsc.Target, dir string) ([]*readDirEntry, error) { 291 | _, fh, err := target.Lookup(dir) 292 | if err != nil { 293 | return nil, err 294 | } 295 | 296 | type readDirArgs struct { 297 | rpc.Header 298 | Handle []byte 299 | Cookie uint64 300 | CookieVerif uint64 301 | Count uint32 302 | } 303 | 304 | type readDirList struct { 305 | IsSet bool `xdr:"union"` 306 | Entry readDirEntry `xdr:"unioncase=1"` 307 | } 308 | 309 | type readDirListOK struct { 310 | DirAttrs nfsc.PostOpAttr 311 | CookieVerf uint64 312 | } 313 | 314 | cookie := uint64(0) 315 | cookieVerf := uint64(0) 316 | eof := false 317 | 318 | var entries []*readDirEntry 319 | for !eof { 320 | res, err := target.Call(&readDirArgs{ 321 | Header: rpc.Header{ 322 | Rpcvers: 2, 323 | Vers: nfsc.Nfs3Vers, 324 | Prog: nfsc.Nfs3Prog, 325 | Proc: uint32(nfs.NFSProcedureReadDir), 326 | Cred: rpc.AuthNull, 327 | Verf: rpc.AuthNull, 328 | }, 329 | Handle: fh, 330 | Cookie: cookie, 331 | CookieVerif: cookieVerf, 332 | Count: 4096, 333 | }) 334 | if err != nil { 335 | return nil, err 336 | } 337 | 338 | status, err := xdr.ReadUint32(res) 339 | if err != nil { 340 | return nil, err 341 | } 342 | 343 | if err = nfsc.NFS3Error(status); err != nil { 344 | return nil, err 345 | } 346 | 347 | dirListOK := new(readDirListOK) 348 | if err = xdr.Read(res, dirListOK); err != nil { 349 | return nil, err 350 | } 351 | 352 | for { 353 | var item readDirList 354 | if err = xdr.Read(res, &item); err != nil { 355 | return nil, err 356 | } 357 | 358 | if !item.IsSet { 359 | break 360 | } 361 | 362 | cookie = item.Entry.Cookie 363 | if item.Entry.FileName == "." || item.Entry.FileName == ".." { 364 | continue 365 | } 366 | entries = append(entries, &item.Entry) 367 | } 368 | 369 | if err = xdr.Read(res, &eof); err != nil { 370 | return nil, err 371 | } 372 | 373 | cookieVerf = dirListOK.CookieVerf 374 | } 375 | 376 | return entries, nil 377 | } 378 | -------------------------------------------------------------------------------- /nfsinterface.go: -------------------------------------------------------------------------------- 1 | package nfs 2 | 3 | // NFSProcedure is the valid RPC calls for the nfs service. 4 | type NFSProcedure uint32 5 | 6 | // NfsProcedure Codes 7 | const ( 8 | NFSProcedureNull NFSProcedure = iota 9 | NFSProcedureGetAttr 10 | NFSProcedureSetAttr 11 | NFSProcedureLookup 12 | NFSProcedureAccess 13 | NFSProcedureReadlink 14 | NFSProcedureRead 15 | NFSProcedureWrite 16 | NFSProcedureCreate 17 | NFSProcedureMkDir 18 | NFSProcedureSymlink 19 | NFSProcedureMkNod 20 | NFSProcedureRemove 21 | NFSProcedureRmDir 22 | NFSProcedureRename 23 | NFSProcedureLink 24 | NFSProcedureReadDir 25 | NFSProcedureReadDirPlus 26 | NFSProcedureFSStat 27 | NFSProcedureFSInfo 28 | NFSProcedurePathConf 29 | NFSProcedureCommit 30 | ) 31 | 32 | func (n NFSProcedure) String() string { 33 | switch n { 34 | case NFSProcedureNull: 35 | return "Null" 36 | case NFSProcedureGetAttr: 37 | return "GetAttr" 38 | case NFSProcedureSetAttr: 39 | return "SetAttr" 40 | case NFSProcedureLookup: 41 | return "Lookup" 42 | case NFSProcedureAccess: 43 | return "Access" 44 | case NFSProcedureReadlink: 45 | return "ReadLink" 46 | case NFSProcedureRead: 47 | return "Read" 48 | case NFSProcedureWrite: 49 | return "Write" 50 | case NFSProcedureCreate: 51 | return "Create" 52 | case NFSProcedureMkDir: 53 | return "Mkdir" 54 | case NFSProcedureSymlink: 55 | return "Symlink" 56 | case NFSProcedureMkNod: 57 | return "Mknod" 58 | case NFSProcedureRemove: 59 | return "Remove" 60 | case NFSProcedureRmDir: 61 | return "Rmdir" 62 | case NFSProcedureRename: 63 | return "Rename" 64 | case NFSProcedureLink: 65 | return "Link" 66 | case NFSProcedureReadDir: 67 | return "ReadDir" 68 | case NFSProcedureReadDirPlus: 69 | return "ReadDirPlus" 70 | case NFSProcedureFSStat: 71 | return "FSStat" 72 | case NFSProcedureFSInfo: 73 | return "FSInfo" 74 | case NFSProcedurePathConf: 75 | return "PathConf" 76 | case NFSProcedureCommit: 77 | return "Commit" 78 | default: 79 | return "Unknown" 80 | } 81 | } 82 | 83 | // NFSStatus (nfsstat3) is a result code for nfs rpc calls 84 | type NFSStatus uint32 85 | 86 | // NFSStatus codes 87 | const ( 88 | NFSStatusOk NFSStatus = 0 89 | NFSStatusPerm NFSStatus = 1 90 | NFSStatusNoEnt NFSStatus = 2 91 | NFSStatusIO NFSStatus = 5 92 | NFSStatusNXIO NFSStatus = 6 93 | NFSStatusAccess NFSStatus = 13 94 | NFSStatusExist NFSStatus = 17 95 | NFSStatusXDev NFSStatus = 18 96 | NFSStatusNoDev NFSStatus = 19 97 | NFSStatusNotDir NFSStatus = 20 98 | NFSStatusIsDir NFSStatus = 21 99 | NFSStatusInval NFSStatus = 22 100 | NFSStatusFBig NFSStatus = 27 101 | NFSStatusNoSPC NFSStatus = 28 102 | NFSStatusROFS NFSStatus = 30 103 | NFSStatusMlink NFSStatus = 31 104 | NFSStatusNameTooLong NFSStatus = 63 105 | NFSStatusNotEmpty NFSStatus = 66 106 | NFSStatusDQuot NFSStatus = 69 107 | NFSStatusStale NFSStatus = 70 108 | NFSStatusRemote NFSStatus = 71 109 | NFSStatusBadHandle NFSStatus = 10001 110 | NFSStatusNotSync NFSStatus = 10002 111 | NFSStatusBadCookie NFSStatus = 10003 112 | NFSStatusNotSupp NFSStatus = 10004 113 | NFSStatusTooSmall NFSStatus = 10005 114 | NFSStatusServerFault NFSStatus = 10006 115 | NFSStatusBadType NFSStatus = 10007 116 | NFSStatusJukebox NFSStatus = 10008 117 | ) 118 | 119 | func (s NFSStatus) String() string { 120 | switch s { 121 | case NFSStatusOk: 122 | return "Call Completed Successfull" 123 | case NFSStatusPerm: 124 | return "Not Owner" 125 | case NFSStatusNoEnt: 126 | return "No such file or directory" 127 | case NFSStatusIO: 128 | return "I/O error" 129 | case NFSStatusNXIO: 130 | return "I/O error: No such device" 131 | case NFSStatusAccess: 132 | return "Permission denied" 133 | case NFSStatusExist: 134 | return "File exists" 135 | case NFSStatusXDev: 136 | return "Attempt to do a cross device hard link" 137 | case NFSStatusNoDev: 138 | return "No such device" 139 | case NFSStatusNotDir: 140 | return "Not a directory" 141 | case NFSStatusIsDir: 142 | return "Is a directory" 143 | case NFSStatusInval: 144 | return "Invalid argument" 145 | case NFSStatusFBig: 146 | return "File too large" 147 | case NFSStatusNoSPC: 148 | return "No space left on device" 149 | case NFSStatusROFS: 150 | return "Read only file system" 151 | case NFSStatusMlink: 152 | return "Too many hard links" 153 | case NFSStatusNameTooLong: 154 | return "Name too long" 155 | case NFSStatusNotEmpty: 156 | return "Not empty" 157 | case NFSStatusDQuot: 158 | return "Resource quota exceeded" 159 | case NFSStatusStale: 160 | return "Invalid file handle" 161 | case NFSStatusRemote: 162 | return "Too many levels of remote in path" 163 | case NFSStatusBadHandle: 164 | return "Illegal NFS file handle" 165 | case NFSStatusNotSync: 166 | return "Synchronization mismatch" 167 | case NFSStatusBadCookie: 168 | return "Cookie is Stale" 169 | case NFSStatusNotSupp: 170 | return "Operation not supported" 171 | case NFSStatusTooSmall: 172 | return "Buffer or request too small" 173 | case NFSStatusServerFault: 174 | return "Unmapped error (EIO)" 175 | case NFSStatusBadType: 176 | return "Type not supported" 177 | case NFSStatusJukebox: 178 | return "Initiated, but too slow. Try again with new txn" 179 | default: 180 | return "unknown" 181 | } 182 | } 183 | 184 | // DirOpArg is a common serialization used for referencing an object in a directory 185 | type DirOpArg struct { 186 | Handle []byte 187 | Filename []byte 188 | } 189 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package nfs 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/rand" 7 | "errors" 8 | "net" 9 | "time" 10 | ) 11 | 12 | // Server is a handle to the listening NFS server. 13 | type Server struct { 14 | Handler 15 | ID [8]byte 16 | context.Context 17 | } 18 | 19 | // RegisterMessageHandler registers a handler for a specific 20 | // XDR procedure. 21 | func RegisterMessageHandler(protocol uint32, proc uint32, handler HandleFunc) error { 22 | if registeredHandlers == nil { 23 | registeredHandlers = make(map[registeredHandlerID]HandleFunc) 24 | } 25 | for k := range registeredHandlers { 26 | if k.protocol == protocol && k.proc == proc { 27 | return errors.New("already registered") 28 | } 29 | } 30 | id := registeredHandlerID{protocol, proc} 31 | registeredHandlers[id] = handler 32 | return nil 33 | } 34 | 35 | // HandleFunc represents a handler for a specific protocol message. 36 | type HandleFunc func(ctx context.Context, w *response, userHandler Handler) error 37 | 38 | // TODO: store directly as a uint64 for more efficient lookups 39 | type registeredHandlerID struct { 40 | protocol uint32 41 | proc uint32 42 | } 43 | 44 | var registeredHandlers map[registeredHandlerID]HandleFunc 45 | 46 | // Serve listens on the provided listener port for incoming client requests. 47 | func (s *Server) Serve(l net.Listener) error { 48 | defer l.Close() 49 | baseCtx := context.Background() 50 | if s.Context != nil { 51 | baseCtx = s.Context 52 | } 53 | if bytes.Equal(s.ID[:], []byte{0, 0, 0, 0, 0, 0, 0, 0}) { 54 | if _, err := rand.Reader.Read(s.ID[:]); err != nil { 55 | return err 56 | } 57 | } 58 | 59 | var tempDelay time.Duration 60 | 61 | for { 62 | conn, err := l.Accept() 63 | if err != nil { 64 | if ne, ok := err.(net.Error); ok && ne.Timeout() { 65 | if tempDelay == 0 { 66 | tempDelay = 5 * time.Millisecond 67 | } else { 68 | tempDelay *= 2 69 | } 70 | if max := 1 * time.Second; tempDelay > max { 71 | tempDelay = max 72 | } 73 | time.Sleep(tempDelay) 74 | continue 75 | } 76 | return err 77 | } 78 | tempDelay = 0 79 | c := s.newConn(conn) 80 | go c.serve(baseCtx) 81 | } 82 | } 83 | 84 | func (s *Server) newConn(nc net.Conn) *conn { 85 | c := &conn{ 86 | Server: s, 87 | Conn: nc, 88 | } 89 | return c 90 | } 91 | 92 | // TODO: keep an immutable map for each server instance to have less 93 | // chance of races. 94 | func (s *Server) handlerFor(prog uint32, proc uint32) HandleFunc { 95 | for k, v := range registeredHandlers { 96 | if k.protocol == prog && k.proc == proc { 97 | return v 98 | } 99 | } 100 | return nil 101 | } 102 | 103 | // Serve is a singleton listener paralleling http.Serve 104 | func Serve(l net.Listener, handler Handler) error { 105 | srv := &Server{Handler: handler} 106 | return srv.Serve(l) 107 | } 108 | -------------------------------------------------------------------------------- /time.go: -------------------------------------------------------------------------------- 1 | package nfs 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // FileTime is the NFS wire time format 8 | // This is equivalent to go-nfs-client/nfs.NFS3Time 9 | type FileTime struct { 10 | Seconds uint32 11 | Nseconds uint32 12 | } 13 | 14 | // ToNFSTime generates the nfs 64bit time format from a golang time. 15 | func ToNFSTime(t time.Time) FileTime { 16 | return FileTime{ 17 | Seconds: uint32(t.Unix()), 18 | Nseconds: uint32(t.UnixNano() % int64(time.Second)), 19 | } 20 | } 21 | 22 | // Native generates a golang time from an nfs time spec 23 | func (t FileTime) Native() *time.Time { 24 | ts := time.Unix(int64(t.Seconds), int64(t.Nseconds)) 25 | return &ts 26 | } 27 | 28 | // EqualTimespec returns if this time is equal to a local time spec 29 | func (t FileTime) EqualTimespec(sec int64, nsec int64) bool { 30 | // TODO: bounds check on sec/nsec overflow 31 | return t.Nseconds == uint32(nsec) && t.Seconds == uint32(sec) 32 | } 33 | --------------------------------------------------------------------------------