// Copyright (C) 2017 Luke Shumaker // // This library is free software; you can redistribute it and/or // modify it under the terms of the GNU Lesser General Public // License as published by the Free Software Foundation; either // version 2.1 of the License, or (at your option) any later version. // // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA // 02110-1301 USA package nslcd_systemd_test import ( "context" "fmt" "io/ioutil" "net" "os" "runtime" "strings" "sync" "syscall" "testing" "time" "git.lukeshu.com/go/libnslcd/nslcd_proto" "git.lukeshu.com/go/libnslcd/nslcd_server" "git.lukeshu.com/go/libnslcd/nslcd_systemd" "golang.org/x/sys/unix" ) func init() { if fdIsDevNull(3) == nil { return } devnull, err := os.OpenFile("/dev/null", os.O_RDWR, 0666) if err != nil { panic(err) } if devnull.Fd() == 3 { return } fmt.Fprintln(os.Stderr, "Could not open /dev/null on FD 3; calling dup2 and re-exec()ing") // shell out to do the FD manipulation--If we made it here, // there's a good chance that FD3 was managed by the go // runtime, and would be changed before we execve(2). panic(syscall.Exec("/bin/sh", append([]string{"sh", "-c", "exec -- \"$@\" 3<>/dev/null"}, os.Args...), os.Environ())) } type testContext struct { *testing.T tmpdir string status chan<- string } func testDriver( t *testContext, backend nslcd_systemd.Backend, limits nslcd_server.Limits, notifyHandler func(dat []byte, oob []byte) error, client func(sockname string)) { t.status <- "setting up" errfatal := func(err error) { if err != nil { t.Fatal(err) } } evExitSupervisor := make(chan error) evExitServer := make(chan uint8) // supervisor ////////////////////////////////////////////////////////// notify_sock, err := sdNotifyListen(t.tmpdir + "/notify.sock") errfatal(err) go func() { evExitSupervisor <- sdNotifyHandle(notify_sock, notifyHandler) }() // server ////////////////////////////////////////////////////////////// errfatal(sdActivatedStream(t.tmpdir + "/nslcd.sock")) go func() { evExitServer <- nslcd_systemd.Main(backend, limits, context.Background()) }() // client/driver /////////////////////////////////////////////////////// t.status <- "running client" client(t.tmpdir + "/nslcd.sock") // exit //////////////////////////////////////////////////////////////// // A limitation of Unix sockets is that some may get dropped // if they arrive close together. So give it some (a half // second is probably generous by a couple orders of // magnitude) time between the client sending any signals and // us sending SIGTERM, so that we are sure it gets both. time.Sleep(time.Second / 2) t.status <- "waiting for server exit" errfatal(unix.Kill(unix.Getpid(), unix.SIGTERM)) status := <-evExitServer if status != 0 { t.Fatalf("Main() exited with %d", status) } t.status <- "waiting for supervisor exit" errfatal(notify_sock.SetReadDeadline(time.Now())) err = <-evExitSupervisor if nerr, ok := err.(net.Error); ok && nerr.Timeout() { err = nil } errfatal(err) errfatal(notify_sock.Close()) } func testClientHang(t *testContext, backend nslcd_systemd.Backend, toclose bool) { } type NonLockingBackend struct { nslcd_server.NilBackend } func (o *NonLockingBackend) Init() error { return nil } func (o *NonLockingBackend) Reload() error { return nil } func (o *NonLockingBackend) Close() {} func (o *NonLockingBackend) Passwd_All(ctx context.Context, req nslcd_proto.Request_Passwd_All) <-chan nslcd_proto.Passwd { ret := make(chan nslcd_proto.Passwd) go func() { defer close(ret) for i := 0; i < 500; i++ { ret <- nslcd_proto.Passwd{ Name: fmt.Sprintf("user%d", i), PwHash: "x", UID: int32(1000 + i), GID: 1000, GECOS: fmt.Sprintf("User %d", i), HomeDir: fmt.Sprintf("/home/user%d", i), Shell: "/bin/sh", } } }() return ret } type LockingBackend struct { NonLockingBackend lock sync.RWMutex } func (o *LockingBackend) Reload() error { o.lock.Lock() defer o.lock.Unlock() return o.NonLockingBackend.Reload() } func (o *LockingBackend) Close() { o.lock.Lock() defer o.lock.Unlock() o.NonLockingBackend.Close() } func (o *LockingBackend) Passwd_All(ctx context.Context, req nslcd_proto.Request_Passwd_All) <-chan nslcd_proto.Passwd { o.lock.RLock() ret := make(chan nslcd_proto.Passwd) go func() { defer o.lock.RUnlock() defer close(ret) for i := 0; i < 500; i++ { ret <- nslcd_proto.Passwd{ Name: fmt.Sprintf("user%d", i), PwHash: "x", UID: int32(1000 + i), GID: 1000, GECOS: fmt.Sprintf("User %d", i), HomeDir: fmt.Sprintf("/home/user%d", i), Shell: "/bin/sh", } } }() return ret } func TestClientHang(t *testing.T) { testcases := []struct { name string backend nslcd_systemd.Backend toclose bool }{ {"NoLocks-ClientOpen", &NonLockingBackend{}, false}, {"NoLocks-ClientClose", &NonLockingBackend{}, true}, {"Locking-ClientOpen", &LockingBackend{}, false}, {"Locking-ClientClose", &LockingBackend{}, true}, } for _, testcase := range testcases { func() { tmpdir, err := ioutil.TempDir("", "go-test-libnslcd-client-hang.") if err != nil { t.Fatal(err) } defer os.RemoveAll(tmpdir) defer sdActivatedReset() t.Run(testcase.name, func(t *testing.T) { testWithTimeout(t, 2*time.Second, func(t *testing.T, s chan<- string) { ctx := &testContext{T: t, tmpdir: tmpdir, status: s} backend := testcase.backend limits := nslcd_server.Limits{ Timeout: 1 * time.Second, } evReload := make(chan bool) notifyHandler := func() func(dat []byte, oob []byte) error { reloading := false return func(dat []byte, oob []byte) error { for _, line := range strings.Split(string(dat), "\n") { switch line { case "RELOADING=1": reloading = true case "READY=1": if reloading { evReload <- true } reloading = false } } return nil } }() client := func(sockname string) { errfatal := func(err error) { if err != nil { t.Fatal(err) } } ctx.status <- "talking with server" conn, err := net.Dial("unix", sockname) errfatal(err) errfatal(nslcd_proto.Write(conn, nslcd_proto.NSLCD_VERSION)) errfatal(nslcd_proto.Write(conn, nslcd_proto.NSLCD_ACTION_PASSWD_ALL)) // Wait for NSLCD_RESULT_*, to make sure the server has made // it in to backend code. var n int32 errfatal(nslcd_proto.Read(conn, &n)) if n != nslcd_proto.NSLCD_VERSION { ctx.Fatal("server version wrong") } errfatal(nslcd_proto.Read(conn, &n)) if n != nslcd_proto.NSLCD_ACTION_PASSWD_ALL { ctx.Fatal("server action wrong") } errfatal(nslcd_proto.Read(conn, &n)) if n != nslcd_proto.NSLCD_RESULT_BEGIN && n != nslcd_proto.NSLCD_RESULT_END { ctx.Fatal("server result malformed") } if testcase.toclose { errfatal(conn.Close()) } ctx.status <- "waiting for server reload" errfatal(unix.Kill(unix.Getpid(), unix.SIGHUP)) <-evReload } testDriver(ctx, backend, limits, notifyHandler, client) }) }) }() } } func TestLargeRequest(t *testing.T) { tmpdir, err := ioutil.TempDir("", "go-test-libnslcd-large-request.") if err != nil { t.Fatal(err) } defer os.RemoveAll(tmpdir) defer sdActivatedReset() t.Run("large-request", func(t *testing.T) { testWithTimeout(t, 2*time.Second, func(t *testing.T, s chan<- string) { KiB := 1024 GiB := 1024 * 1024 * 1024 ctx := &testContext{T: t, tmpdir: tmpdir, status: s} backend := &LockingBackend{} limits := nslcd_server.Limits{ Timeout: 1 * time.Second, RequestMaxSize: int64(1 * KiB), } notifyHandler := func(dat []byte, oob []byte) error { return nil } client := func(sockname string) { errfatal := func(err error) { if err != nil { t.Fatal(err) } } conn, err := net.Dial("unix", sockname) errfatal(err) errfatal(nslcd_proto.Write(conn, nslcd_proto.NSLCD_VERSION)) errfatal(nslcd_proto.Write(conn, nslcd_proto.NSLCD_ACTION_PASSWD_BYNAME)) errfatal(nslcd_proto.Write(conn, int32(1*GiB))) } testDriver(ctx, backend, limits, notifyHandler, client) var memstats runtime.MemStats runtime.ReadMemStats(&memstats) if memstats.HeapSys > uint64(1*GiB) { t.Fatalf("Used more than 1 GiB heap: %s B", humanizeU64(memstats.HeapSys)) } }) }) }