// 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 ( "fmt" "io/ioutil" "net" "os" "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" ) type testContext struct { *testing.T tmpdir string status chan<- string } func testBadClient(t *testContext, backend nslcd_systemd.Backend, toclose bool) { t.status <- "setting up" errfatal := func(err error) { if err != nil { t.Fatal(err) } } evExitSupervisor := make(chan error) evExitServer := make(chan uint8) evReload := make(chan bool) // supervisor ////////////////////////////////////////////////////////// notify_sock, err := sdNotifyListen(t.tmpdir + "/notify.sock") errfatal(err) go func() { reloading := false evExitSupervisor <- sdNotifyHandle(notify_sock, 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 }) }() // server ////////////////////////////////////////////////////////////// errfatal(sdActivatedStream(t.tmpdir + "/nslcd.sock")) go func() { evExitServer <- nslcd_systemd.Main(backend, nslcd_server.Limits{ Timeout: 1 * time.Second, }) }() // client/driver /////////////////////////////////////////////////////// t.status <- "talking with server" conn, err := net.Dial("unix", t.tmpdir+"/nslcd.sock") 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 { t.Fatal("server version wrong") } errfatal(nslcd_proto.Read(conn, &n)) if n != nslcd_proto.NSLCD_ACTION_PASSWD_ALL { t.Fatal("server action wrong") } errfatal(nslcd_proto.Read(conn, &n)) if n != nslcd_proto.NSLCD_RESULT_BEGIN && n != nslcd_proto.NSLCD_RESULT_END { t.Fatal("server result malformed") } if toclose { errfatal(conn.Close()) } t.status <- "waiting for server reload" errfatal(unix.Kill(unix.Getpid(), unix.SIGHUP)) <-evReload // 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 to handle SIGHUP before 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()) } 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(cred unix.Ucred, 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(cred unix.Ucred, 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 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())) } func TestBadClient(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-bad-client.") 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} testBadClient(ctx, testcase.backend, testcase.toclose) }) }) }() } }