From 16221facb9066ccf03015ccfe9e7ca784b0d2099 Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Sat, 9 May 2020 17:36:41 +0200 Subject: ctlsock: create exported ctlsock client library The former interal ctlsock server package is renamed to ctlsocksrv. --- ctlsock/ctlsock.go | 26 +++ internal/ctlsock/ctlsock_serve.go | 183 --------------------- internal/ctlsock/sanitize.go | 32 ---- internal/ctlsock/sanitize_test.go | 30 ---- internal/ctlsocksrv/ctlsock_serve.go | 163 ++++++++++++++++++ internal/ctlsocksrv/sanitize.go | 32 ++++ internal/ctlsocksrv/sanitize_test.go | 30 ++++ internal/fusefrontend/ctlsock_interface.go | 4 +- internal/fusefrontend_reverse/ctlsock_interface.go | 4 +- mount.go | 8 +- tests/defaults/ctlsock_test.go | 2 +- tests/reverse/correctness_test.go | 2 +- tests/reverse/ctlsock_test.go | 2 +- tests/reverse/exclude_test.go | 2 +- tests/test_helpers/helpers.go | 4 +- 15 files changed, 265 insertions(+), 259 deletions(-) create mode 100644 ctlsock/ctlsock.go delete mode 100644 internal/ctlsock/ctlsock_serve.go delete mode 100644 internal/ctlsock/sanitize.go delete mode 100644 internal/ctlsock/sanitize_test.go create mode 100644 internal/ctlsocksrv/ctlsock_serve.go create mode 100644 internal/ctlsocksrv/sanitize.go create mode 100644 internal/ctlsocksrv/sanitize_test.go diff --git a/ctlsock/ctlsock.go b/ctlsock/ctlsock.go new file mode 100644 index 0000000..893b3b0 --- /dev/null +++ b/ctlsock/ctlsock.go @@ -0,0 +1,26 @@ +// Package ctlsock is a Go library that can be used to query the +// gocryptfs control socket interface. This interface can be +// activated by passing `-ctlsock /tmp/my.sock` to gocryptfs on the +// command line. +package ctlsock + +// RequestStruct is sent by a client +type RequestStruct struct { + EncryptPath string + DecryptPath string +} + +// ResponseStruct is sent by the server in response to a request +type ResponseStruct struct { + // Result is the resulting decrypted or encrypted path. Empty on error. + Result string + // ErrNo is the error number as defined in errno.h. + // 0 means success and -1 means that the error number is not known + // (look at ErrText in this case). + ErrNo int32 + // ErrText is a detailed error message. + ErrText string + // WarnText contains warnings that may have been encountered while + // processing the message. + WarnText string +} diff --git a/internal/ctlsock/ctlsock_serve.go b/internal/ctlsock/ctlsock_serve.go deleted file mode 100644 index 8b19e8a..0000000 --- a/internal/ctlsock/ctlsock_serve.go +++ /dev/null @@ -1,183 +0,0 @@ -// Package ctlsock implements the control socket interface that can be -// activated by passing "-ctlsock" on the command line. -package ctlsock - -import ( - "encoding/json" - "errors" - "fmt" - "io" - "net" - "os" - "syscall" - - "github.com/rfjakob/gocryptfs/internal/tlog" -) - -// Interface should be implemented by fusefrontend[_reverse] -type Interface interface { - EncryptPath(string) (string, error) - DecryptPath(string) (string, error) -} - -// RequestStruct is sent by a client -type RequestStruct struct { - EncryptPath string - DecryptPath string -} - -// ResponseStruct is sent by us as response to a request -type ResponseStruct struct { - // Result is the resulting decrypted or encrypted path. Empty on error. - Result string - // ErrNo is the error number as defined in errno.h. - // 0 means success and -1 means that the error number is not known - // (look at ErrText in this case). - ErrNo int32 - // ErrText is a detailed error message. - ErrText string - // WarnText contains warnings that may have been encountered while - // processing the message. - WarnText string -} - -type ctlSockHandler struct { - fs Interface - socket *net.UnixListener -} - -// Serve serves incoming connections on "sock". This call blocks so you -// probably want to run it in a new goroutine. -func Serve(sock net.Listener, fs Interface) { - handler := ctlSockHandler{ - fs: fs, - socket: sock.(*net.UnixListener), - } - handler.acceptLoop() -} - -func (ch *ctlSockHandler) acceptLoop() { - for { - conn, err := ch.socket.Accept() - if err != nil { - // This can trigger on program exit with "use of closed network connection". - // Special-casing this is hard due to https://github.com/golang/go/issues/4373 - // so just don't use tlog.Warn to not cause panics in the tests. - tlog.Info.Printf("ctlsock: Accept error: %v", err) - break - } - go ch.handleConnection(conn.(*net.UnixConn)) - } -} - -// ReadBufSize is the size of the request read buffer. -// The longest possible path is 4096 bytes on Linux and 1024 on Mac OS X so -// 5000 bytes should be enough to hold the whole JSON request. This -// assumes that the path does not contain too many characters that had to be -// be escaped in JSON (for example, a null byte blows up to "\u0000"). -// We abort the connection if the request is bigger than this. -const ReadBufSize = 5000 - -// handleConnection reads and parses JSON requests from "conn" -func (ch *ctlSockHandler) handleConnection(conn *net.UnixConn) { - buf := make([]byte, ReadBufSize) - for { - n, err := conn.Read(buf) - if err == io.EOF { - conn.Close() - return - } else if err != nil { - tlog.Warn.Printf("ctlsock: Read error: %#v", err) - conn.Close() - return - } - if n == ReadBufSize { - tlog.Warn.Printf("ctlsock: request too big (max = %d bytes)", ReadBufSize-1) - conn.Close() - return - } - data := buf[:n] - var in RequestStruct - err = json.Unmarshal(data, &in) - if err != nil { - tlog.Warn.Printf("ctlsock: JSON Unmarshal error: %#v", err) - err = errors.New("JSON Unmarshal error: " + err.Error()) - sendResponse(conn, err, "", "") - continue - } - ch.handleRequest(&in, conn) - } -} - -// handleRequest handles an already-unmarshaled JSON request -func (ch *ctlSockHandler) handleRequest(in *RequestStruct, conn *net.UnixConn) { - var err error - var inPath, outPath, clean, warnText string - // You cannot perform both decryption and encryption in one request - if in.DecryptPath != "" && in.EncryptPath != "" { - err = errors.New("Ambiguous") - sendResponse(conn, err, "", "") - return - } - // Neither encryption nor encryption has been requested, makes no sense - if in.DecryptPath == "" && in.EncryptPath == "" { - err = errors.New("Empty input") - sendResponse(conn, err, "", "") - return - } - // Canonicalize input path - if in.EncryptPath != "" { - inPath = in.EncryptPath - } else { - inPath = in.DecryptPath - } - clean = SanitizePath(inPath) - // Warn if a non-canonical path was passed - if inPath != clean { - warnText = fmt.Sprintf("Non-canonical input path '%s' has been interpreted as '%s'.", inPath, clean) - } - // Error out if the canonical path is now empty - if clean == "" { - err = errors.New("Empty input after canonicalization") - sendResponse(conn, err, "", warnText) - return - } - // Actual encrypt or decrypt operation - if in.EncryptPath != "" { - outPath, err = ch.fs.EncryptPath(clean) - } else { - outPath, err = ch.fs.DecryptPath(clean) - } - sendResponse(conn, err, outPath, warnText) -} - -// sendResponse sends a JSON response message -func sendResponse(conn *net.UnixConn, err error, result string, warnText string) { - msg := ResponseStruct{ - Result: result, - WarnText: warnText, - } - if err != nil { - msg.ErrText = err.Error() - msg.ErrNo = -1 - // Try to extract the actual error number - if pe, ok := err.(*os.PathError); ok { - if se, ok := pe.Err.(syscall.Errno); ok { - msg.ErrNo = int32(se) - } - } else if err == syscall.ENOENT { - msg.ErrNo = int32(syscall.ENOENT) - } - } - jsonMsg, err := json.Marshal(msg) - if err != nil { - tlog.Warn.Printf("ctlsock: Marshal failed: %v", err) - return - } - // For convenience for the user, add a newline at the end. - jsonMsg = append(jsonMsg, '\n') - _, err = conn.Write(jsonMsg) - if err != nil { - tlog.Warn.Printf("ctlsock: Write failed: %v", err) - } -} diff --git a/internal/ctlsock/sanitize.go b/internal/ctlsock/sanitize.go deleted file mode 100644 index 7cf77a5..0000000 --- a/internal/ctlsock/sanitize.go +++ /dev/null @@ -1,32 +0,0 @@ -package ctlsock - -import ( - "path/filepath" - "strings" -) - -// SanitizePath adapts filepath.Clean for FUSE paths. -// 1) Leading slash(es) are dropped -// 2) It returns "" instead of "." -// 3) If the cleaned path points above CWD (start with ".."), an empty string -// is returned -// See the TestSanitizePath testcases for examples. -func SanitizePath(path string) string { - // (1) - for len(path) > 0 && path[0] == '/' { - path = path[1:] - } - if len(path) == 0 { - return "" - } - clean := filepath.Clean(path) - // (2) - if clean == "." { - return "" - } - // (3) - if clean == ".." || strings.HasPrefix(clean, "../") { - return "" - } - return clean -} diff --git a/internal/ctlsock/sanitize_test.go b/internal/ctlsock/sanitize_test.go deleted file mode 100644 index d79fa7c..0000000 --- a/internal/ctlsock/sanitize_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package ctlsock - -import ( - "testing" -) - -func TestSanitizePath(t *testing.T) { - testCases := [][]string{ - {"", ""}, - {".", ""}, - {"/", ""}, - {"foo", "foo"}, - {"/foo", "foo"}, - {"foo/", "foo"}, - {"/foo/", "foo"}, - {"/foo/./foo", "foo/foo"}, - {"./", ""}, - {"..", ""}, - {"foo/../..", ""}, - {"foo/../../aaaaaa", ""}, - {"/foo/../../aaaaaa", ""}, - {"/////", ""}, - } - for _, tc := range testCases { - res := SanitizePath(tc[0]) - if res != tc[1] { - t.Errorf("%q: got %q, want %q", tc[0], res, tc[1]) - } - } -} diff --git a/internal/ctlsocksrv/ctlsock_serve.go b/internal/ctlsocksrv/ctlsock_serve.go new file mode 100644 index 0000000..b63759e --- /dev/null +++ b/internal/ctlsocksrv/ctlsock_serve.go @@ -0,0 +1,163 @@ +// Package ctlsocksrv implements the control socket interface that can be +// activated by passing "-ctlsock" on the command line. +package ctlsocksrv + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net" + "os" + "syscall" + + "github.com/rfjakob/gocryptfs/ctlsock" + "github.com/rfjakob/gocryptfs/internal/tlog" +) + +// Interface should be implemented by fusefrontend[_reverse] +type Interface interface { + EncryptPath(string) (string, error) + DecryptPath(string) (string, error) +} + +type ctlSockHandler struct { + fs Interface + socket *net.UnixListener +} + +// Serve serves incoming connections on "sock". This call blocks so you +// probably want to run it in a new goroutine. +func Serve(sock net.Listener, fs Interface) { + handler := ctlSockHandler{ + fs: fs, + socket: sock.(*net.UnixListener), + } + handler.acceptLoop() +} + +func (ch *ctlSockHandler) acceptLoop() { + for { + conn, err := ch.socket.Accept() + if err != nil { + // This can trigger on program exit with "use of closed network connection". + // Special-casing this is hard due to https://github.com/golang/go/issues/4373 + // so just don't use tlog.Warn to not cause panics in the tests. + tlog.Info.Printf("ctlsock: Accept error: %v", err) + break + } + go ch.handleConnection(conn.(*net.UnixConn)) + } +} + +// ReadBufSize is the size of the request read buffer. +// The longest possible path is 4096 bytes on Linux and 1024 on Mac OS X so +// 5000 bytes should be enough to hold the whole JSON request. This +// assumes that the path does not contain too many characters that had to be +// be escaped in JSON (for example, a null byte blows up to "\u0000"). +// We abort the connection if the request is bigger than this. +const ReadBufSize = 5000 + +// handleConnection reads and parses JSON requests from "conn" +func (ch *ctlSockHandler) handleConnection(conn *net.UnixConn) { + buf := make([]byte, ReadBufSize) + for { + n, err := conn.Read(buf) + if err == io.EOF { + conn.Close() + return + } else if err != nil { + tlog.Warn.Printf("ctlsock: Read error: %#v", err) + conn.Close() + return + } + if n == ReadBufSize { + tlog.Warn.Printf("ctlsock: request too big (max = %d bytes)", ReadBufSize-1) + conn.Close() + return + } + data := buf[:n] + var in ctlsock.RequestStruct + err = json.Unmarshal(data, &in) + if err != nil { + tlog.Warn.Printf("ctlsock: JSON Unmarshal error: %#v", err) + err = errors.New("JSON Unmarshal error: " + err.Error()) + sendResponse(conn, err, "", "") + continue + } + ch.handleRequest(&in, conn) + } +} + +// handleRequest handles an already-unmarshaled JSON request +func (ch *ctlSockHandler) handleRequest(in *ctlsock.RequestStruct, conn *net.UnixConn) { + var err error + var inPath, outPath, clean, warnText string + // You cannot perform both decryption and encryption in one request + if in.DecryptPath != "" && in.EncryptPath != "" { + err = errors.New("Ambiguous") + sendResponse(conn, err, "", "") + return + } + // Neither encryption nor encryption has been requested, makes no sense + if in.DecryptPath == "" && in.EncryptPath == "" { + err = errors.New("Empty input") + sendResponse(conn, err, "", "") + return + } + // Canonicalize input path + if in.EncryptPath != "" { + inPath = in.EncryptPath + } else { + inPath = in.DecryptPath + } + clean = SanitizePath(inPath) + // Warn if a non-canonical path was passed + if inPath != clean { + warnText = fmt.Sprintf("Non-canonical input path '%s' has been interpreted as '%s'.", inPath, clean) + } + // Error out if the canonical path is now empty + if clean == "" { + err = errors.New("Empty input after canonicalization") + sendResponse(conn, err, "", warnText) + return + } + // Actual encrypt or decrypt operation + if in.EncryptPath != "" { + outPath, err = ch.fs.EncryptPath(clean) + } else { + outPath, err = ch.fs.DecryptPath(clean) + } + sendResponse(conn, err, outPath, warnText) +} + +// sendResponse sends a JSON response message +func sendResponse(conn *net.UnixConn, err error, result string, warnText string) { + msg := ctlsock.ResponseStruct{ + Result: result, + WarnText: warnText, + } + if err != nil { + msg.ErrText = err.Error() + msg.ErrNo = -1 + // Try to extract the actual error number + if pe, ok := err.(*os.PathError); ok { + if se, ok := pe.Err.(syscall.Errno); ok { + msg.ErrNo = int32(se) + } + } else if err == syscall.ENOENT { + msg.ErrNo = int32(syscall.ENOENT) + } + } + jsonMsg, err := json.Marshal(msg) + if err != nil { + tlog.Warn.Printf("ctlsock: Marshal failed: %v", err) + return + } + // For convenience for the user, add a newline at the end. + jsonMsg = append(jsonMsg, '\n') + _, err = conn.Write(jsonMsg) + if err != nil { + tlog.Warn.Printf("ctlsock: Write failed: %v", err) + } +} diff --git a/internal/ctlsocksrv/sanitize.go b/internal/ctlsocksrv/sanitize.go new file mode 100644 index 0000000..4333872 --- /dev/null +++ b/internal/ctlsocksrv/sanitize.go @@ -0,0 +1,32 @@ +package ctlsocksrv + +import ( + "path/filepath" + "strings" +) + +// SanitizePath adapts filepath.Clean for FUSE paths. +// 1) Leading slash(es) are dropped +// 2) It returns "" instead of "." +// 3) If the cleaned path points above CWD (start with ".."), an empty string +// is returned +// See the TestSanitizePath testcases for examples. +func SanitizePath(path string) string { + // (1) + for len(path) > 0 && path[0] == '/' { + path = path[1:] + } + if len(path) == 0 { + return "" + } + clean := filepath.Clean(path) + // (2) + if clean == "." { + return "" + } + // (3) + if clean == ".." || strings.HasPrefix(clean, "../") { + return "" + } + return clean +} diff --git a/internal/ctlsocksrv/sanitize_test.go b/internal/ctlsocksrv/sanitize_test.go new file mode 100644 index 0000000..2462d5d --- /dev/null +++ b/internal/ctlsocksrv/sanitize_test.go @@ -0,0 +1,30 @@ +package ctlsocksrv + +import ( + "testing" +) + +func TestSanitizePath(t *testing.T) { + testCases := [][]string{ + {"", ""}, + {".", ""}, + {"/", ""}, + {"foo", "foo"}, + {"/foo", "foo"}, + {"foo/", "foo"}, + {"/foo/", "foo"}, + {"/foo/./foo", "foo/foo"}, + {"./", ""}, + {"..", ""}, + {"foo/../..", ""}, + {"foo/../../aaaaaa", ""}, + {"/foo/../../aaaaaa", ""}, + {"/////", ""}, + } + for _, tc := range testCases { + res := SanitizePath(tc[0]) + if res != tc[1] { + t.Errorf("%q: got %q, want %q", tc[0], res, tc[1]) + } + } +} diff --git a/internal/fusefrontend/ctlsock_interface.go b/internal/fusefrontend/ctlsock_interface.go index e96d08d..2131463 100644 --- a/internal/fusefrontend/ctlsock_interface.go +++ b/internal/fusefrontend/ctlsock_interface.go @@ -7,13 +7,13 @@ import ( "strings" "syscall" - "github.com/rfjakob/gocryptfs/internal/ctlsock" + "github.com/rfjakob/gocryptfs/internal/ctlsocksrv" "github.com/rfjakob/gocryptfs/internal/nametransform" "github.com/rfjakob/gocryptfs/internal/syscallcompat" "github.com/rfjakob/gocryptfs/internal/tlog" ) -var _ ctlsock.Interface = &FS{} // Verify that interface is implemented. +var _ ctlsocksrv.Interface = &FS{} // Verify that interface is implemented. // EncryptPath implements ctlsock.Backend // diff --git a/internal/fusefrontend_reverse/ctlsock_interface.go b/internal/fusefrontend_reverse/ctlsock_interface.go index a1e782d..f7b8afd 100644 --- a/internal/fusefrontend_reverse/ctlsock_interface.go +++ b/internal/fusefrontend_reverse/ctlsock_interface.go @@ -6,11 +6,11 @@ import ( "golang.org/x/sys/unix" - "github.com/rfjakob/gocryptfs/internal/ctlsock" + "github.com/rfjakob/gocryptfs/internal/ctlsocksrv" "github.com/rfjakob/gocryptfs/internal/pathiv" ) -var _ ctlsock.Interface = &ReverseFS{} // Verify that interface is implemented. +var _ ctlsocksrv.Interface = &ReverseFS{} // Verify that interface is implemented. // EncryptPath implements ctlsock.Backend. // This is used for the control socket and for the "-exclude" logic. diff --git a/mount.go b/mount.go index c0ef190..45773ce 100644 --- a/mount.go +++ b/mount.go @@ -29,7 +29,7 @@ import ( "github.com/rfjakob/gocryptfs/internal/configfile" "github.com/rfjakob/gocryptfs/internal/contentenc" "github.com/rfjakob/gocryptfs/internal/cryptocore" - "github.com/rfjakob/gocryptfs/internal/ctlsock" + "github.com/rfjakob/gocryptfs/internal/ctlsocksrv" "github.com/rfjakob/gocryptfs/internal/exitcodes" "github.com/rfjakob/gocryptfs/internal/fusefrontend" "github.com/rfjakob/gocryptfs/internal/fusefrontend_reverse" @@ -222,11 +222,11 @@ func setOpenFileLimit() { } } -// ctlsockFs satisfies both the pathfs.FileSystem and the ctlsock.Interface +// ctlsockFs satisfies both the pathfs.FileSystem and the ctlsocksrv.Interface // interfaces type ctlsockFs interface { pathfs.FileSystem - ctlsock.Interface + ctlsocksrv.Interface } // initFuseFrontend - initialize gocryptfs/fusefrontend @@ -331,7 +331,7 @@ func initFuseFrontend(args *argContainer) (pfs pathfs.FileSystem, wipeKeys func( // We have opened the socket early so that we cannot fail here after // asking the user for the password if args._ctlsockFd != nil { - go ctlsock.Serve(args._ctlsockFd, fs) + go ctlsocksrv.Serve(args._ctlsockFd, fs) } return fs, func() { cCore.Wipe() } } diff --git a/tests/defaults/ctlsock_test.go b/tests/defaults/ctlsock_test.go index b987bf6..64b72a2 100644 --- a/tests/defaults/ctlsock_test.go +++ b/tests/defaults/ctlsock_test.go @@ -5,7 +5,7 @@ import ( "syscall" "testing" - "github.com/rfjakob/gocryptfs/internal/ctlsock" + "github.com/rfjakob/gocryptfs/ctlsock" "github.com/rfjakob/gocryptfs/tests/test_helpers" ) diff --git a/tests/reverse/correctness_test.go b/tests/reverse/correctness_test.go index 206bebd..9ebff37 100644 --- a/tests/reverse/correctness_test.go +++ b/tests/reverse/correctness_test.go @@ -10,7 +10,7 @@ import ( "golang.org/x/sys/unix" - "github.com/rfjakob/gocryptfs/internal/ctlsock" + "github.com/rfjakob/gocryptfs/ctlsock" "github.com/rfjakob/gocryptfs/internal/syscallcompat" "github.com/rfjakob/gocryptfs/tests/test_helpers" ) diff --git a/tests/reverse/ctlsock_test.go b/tests/reverse/ctlsock_test.go index 6ccc724..ecb0b96 100644 --- a/tests/reverse/ctlsock_test.go +++ b/tests/reverse/ctlsock_test.go @@ -5,7 +5,7 @@ import ( "syscall" "testing" - "github.com/rfjakob/gocryptfs/internal/ctlsock" + "github.com/rfjakob/gocryptfs/ctlsock" "github.com/rfjakob/gocryptfs/tests/test_helpers" ) diff --git a/tests/reverse/exclude_test.go b/tests/reverse/exclude_test.go index 1662f93..110a407 100644 --- a/tests/reverse/exclude_test.go +++ b/tests/reverse/exclude_test.go @@ -5,7 +5,7 @@ import ( "path/filepath" "testing" - "github.com/rfjakob/gocryptfs/internal/ctlsock" + "github.com/rfjakob/gocryptfs/ctlsock" "github.com/rfjakob/gocryptfs/internal/nametransform" "github.com/rfjakob/gocryptfs/tests/test_helpers" ) diff --git a/tests/test_helpers/helpers.go b/tests/test_helpers/helpers.go index 93d34d6..1fe0c19 100644 --- a/tests/test_helpers/helpers.go +++ b/tests/test_helpers/helpers.go @@ -16,7 +16,7 @@ import ( "testing" "time" - "github.com/rfjakob/gocryptfs/internal/ctlsock" + "github.com/rfjakob/gocryptfs/ctlsock" "github.com/rfjakob/gocryptfs/internal/nametransform" "github.com/rfjakob/gocryptfs/internal/syscallcompat" ) @@ -362,7 +362,7 @@ func QueryCtlSock(t *testing.T, socketPath string, req ctlsock.RequestStruct) (r if err != nil { t.Fatal(err) } - buf := make([]byte, ctlsock.ReadBufSize) + buf := make([]byte, 5000) n, err := conn.Read(buf) if err != nil { t.Fatal(err) -- cgit v1.2.3