// Program compare-cap is a sanity check that Go's cap package is
// inter-operable with the C libcap.
package main

import (
	"log"
	"os"
	"syscall"
	"unsafe"

	"libcap/cap"
)

// #include <stdlib.h>
// #include <sys/capability.h>
// #cgo CFLAGS: -I../libcap/include
// #cgo LDFLAGS: -L../libcap -lcap
import "C"

// tryFileCaps attempts to use the cap package to manipulate file
// capabilities. No reference to libcap in this function.
func tryFileCaps() {
	saved := cap.GetProc()

	// Capabilities we will place on a file.
	want := cap.NewSet()
	if err := want.SetFlag(cap.Permitted, true, cap.SETFCAP, cap.DAC_OVERRIDE); err != nil {
		log.Fatalf("failed to explore desired file capability: %v", err)
	}
	if err := want.SetFlag(cap.Effective, true, cap.SETFCAP, cap.DAC_OVERRIDE); err != nil {
		log.Fatalf("failed to raise the effective bits: %v", err)
	}

	if perm, err := saved.GetFlag(cap.Permitted, cap.SETFCAP); err != nil {
		log.Fatalf("failed to read capability: %v", err)
	} else if !perm {
		log.Printf("skipping file cap tests - insufficient privilege")
		return
	}

	if err := saved.ClearFlag(cap.Effective); err != nil {
		log.Fatalf("failed to drop effective: %v", err)
	}
	if err := saved.SetProc(); err != nil {
		log.Fatalf("failed to limit capabilities: %v", err)
	}

	// Failing attempt to remove capabilities.
	var empty *cap.Set
	if err := empty.SetFile(os.Args[0]); err != syscall.EPERM {
		log.Fatalf("failed to be blocked from removing filecaps: %v", err)
	}

	// The privilege we want (in the case we are root, we need the
	// DAC_OVERRIDE too).
	working, err := saved.Dup()
	if err != nil {
		log.Fatalf("failed to duplicate (%v): %v", saved, err)
	}
	if err := working.SetFlag(cap.Effective, true, cap.DAC_OVERRIDE, cap.SETFCAP); err != nil {
		log.Fatalf("failed to raise effective: %v", err)
	}

	// Critical (privilege using) section:
	if err := working.SetProc(); err != nil {
		log.Fatalf("failed to enable first effective privilege: %v", err)
	}
	// Delete capability
	if err := empty.SetFile(os.Args[0]); err != nil && err != syscall.ENODATA {
		log.Fatalf("blocked from removing filecaps: %v", err)
	}
	if got, err := cap.GetFile(os.Args[0]); err == nil {
		log.Fatalf("read deleted file caps: %v", got)
	}
	// Create file caps (this use employs the effective bit).
	if err := want.SetFile(os.Args[0]); err != nil {
		log.Fatalf("failed to set file capability: %v", err)
	}
	if err := saved.SetProc(); err != nil {
		log.Fatalf("failed to lower effective capability: %v", err)
	}
	// End of critical section.

	if got, err := cap.GetFile(os.Args[0]); err != nil {
		log.Fatalf("failed to read caps: %v", err)
	} else if is, was := got.String(), want.String(); is != was {
		log.Fatalf("read file caps do not match desired: got=%q want=%q", is, was)
	}

	// Now, do it all again but this time on an open file.
	f, err := os.Open(os.Args[0])
	if err != nil {
		log.Fatalf("failed to open %q: %v", os.Args[0], err)
	}
	defer f.Close()

	// Failing attempt to remove capabilities.
	if err := empty.SetFd(f); err != syscall.EPERM {
		log.Fatalf("failed to be blocked from fremoving filecaps: %v", err)
	}

	// For the next section, we won't set the effective bit on the file.
	want.ClearFlag(cap.Effective)

	// Critical (privilege using) section:
	if err := working.SetProc(); err != nil {
		log.Fatalf("failed to enable effective privilege: %v", err)
	}
	if err := empty.SetFd(f); err != nil && err != syscall.ENODATA {
		log.Fatalf("blocked from fremoving filecaps: %v", err)
	}
	if got, err := cap.GetFd(f); err == nil {
		log.Fatalf("read fdeleted file caps: %v", got)
	}
	// This one does not set the effective bit.
	if err := want.SetFd(f); err != nil {
		log.Fatalf("failed to fset file capability: %v", err)
	}
	if err := saved.SetProc(); err != nil {
		log.Fatalf("failed to lower effective capability: %v", err)
	}
	// End of critical section.

	if got, err := cap.GetFd(f); err != nil {
		log.Fatalf("failed to fread caps: %v", err)
	} else if is, was := got.String(), want.String(); is != was {
		log.Fatalf("fread file caps do not match desired: got=%q want=%q", is, was)
	}
}

// tryProcCaps performs a set of convenience functions and compares
// the results with those seen by libcap. At the end of this function,
// the running process has no privileges at all. So exiting the
// program is the only option.
func tryProcCaps() {
	c := cap.GetProc()
	if v, err := c.GetFlag(cap.Permitted, cap.SETPCAP); err != nil {
		log.Fatalf("failed to read permitted setpcap: %v", err)
	} else if !v {
		log.Printf("skipping proc cap tests - insufficient privilege")
		return
	}
	if err := cap.SetUID(99); err != nil {
		log.Fatalf("failed to set uid=99: %v", err)
	}
	if u := syscall.Getuid(); u != 99 {
		log.Fatal("uid=99 did not take: got=%d", u)
	}
	if err := cap.SetGroups(98, 100, 101); err != nil {
		log.Fatalf("failed to set groups=98 [100, 101]: %v", err)
	}
	if g := syscall.Getgid(); g != 98 {
		log.Fatalf("gid=98 did not take: got=%d", g)
	}
	if gs, err := syscall.Getgroups(); err != nil {
		log.Fatalf("error getting groups: %v", err)
	} else if len(gs) != 2 || gs[0] != 100 || gs[1] != 101 {
		log.Fatalf("wrong of groups: got=%v want=[100 l01]", gs)
	}

	if mode := cap.GetMode(); mode != cap.ModeUncertain {
		log.Fatalf("initial mode should be 0 (UNCERTAIN), got: %d (%v)", mode, mode)
	}

	// To distinguish PURE1E and PURE1E_INIT we need an inheritable capability set.
	working := cap.GetProc()
	if err := working.SetFlag(cap.Inheritable, true, cap.SETPCAP); err != nil {
		log.Fatalf("unable to raise inheritable bit: %v", err)
	}
	if err := working.SetProc(); err != nil {
		log.Fatalf("failed to add inheritable bit: %v", err)
	}

	for i, mode := range []cap.Mode{cap.ModePure1E, cap.ModePure1EInit, cap.ModeNoPriv} {
		if err := mode.Set(); err != nil {
			log.Fatalf("[%d] in mode=%v and failed to set mode to %d (%v): %v", i, cap.GetMode(), mode, mode, err)
		}
		if got := cap.GetMode(); got != mode {
			log.Fatalf("[%d] unable to recognise mode %d (%v), got: %d (%v)", i, mode, mode, got, got)
		}
		cM := C.cap_get_mode()
		if mode != cap.Mode(cM) {
			log.Fatalf("[%d] C and Go disagree on mode: %d vs %d", cM, mode)
		}
	}

	// The current process is now without any access to privelege.
}

func main() {
	// Use the C libcap to obtain a non-trivial capability in text form (from init).
	cC := C.cap_get_pid(1)
	if cC == nil {
		log.Fatal("basic c caps from init function failure")
	}
	defer C.cap_free(unsafe.Pointer(cC))
	var tCLen C.ssize_t
	tC := C.cap_to_text(cC, &tCLen)
	if tC == nil {
		log.Fatal("basic c init caps -> text failure")
	}
	defer C.cap_free(unsafe.Pointer(tC))

	importT := C.GoString(tC)
	if got, want := len(importT), int(tCLen); got != want {
		log.Fatalf("C string import failed: got=%d [%q] want=%d", got, importT, want)
	}

	// Validate that it can be decoded in Go.
	cGo, err := cap.FromText(importT)
	if err != nil {
		log.Fatalf("go parsing of c text import failed: %v", err)
	}

	// Validate that it matches the one directly loaded in Go.
	c, err := cap.GetPID(1)
	if err != nil {
		log.Fatalf("...failed to read init's capabilities:", err)
	}
	tGo := c.String()
	if got, want := tGo, cGo.String(); got != want {
		log.Fatalf("go text rep does not match c: got=%q, want=%q", got, want)
	}

	// Export it in text form again from Go.
	tForC := C.CString(tGo)
	defer C.free(unsafe.Pointer(tForC))

	// Validate it can be encoded in C.
	cC2 := C.cap_from_text(tForC)
	if cC2 == nil {
		log.Fatal("go text rep not parsable by c")
	}
	defer C.cap_free(unsafe.Pointer(cC2))

	// Validate that it can be exported in binary form in C
	const enoughForAnyone = 1000
	eC := make([]byte, enoughForAnyone)
	eCLen := C.cap_copy_ext(unsafe.Pointer(&eC[0]), cC2, C.ssize_t(len(eC)))
	if eCLen < 5 {
		log.Fatalf("c export yielded bad length: %d", eCLen)
	}

	// Validate that it can be imported from binary in Go
	iGo, err := cap.Import(eC[:eCLen])
	if err != nil {
		log.Fatalf("go import of c binary failed: %v", err)
	}
	if got, want := iGo.String(), importT; got != want {
		log.Fatalf("go import of c binary miscompare: got=%q want=%q", got, want)
	}

	// Validate that it can be exported in binary in Go
	iE, err := iGo.Export()
	if err != nil {
		log.Fatalf("go failed to export binary: %v", err)
	}

	// Validate that it can be imported in binary in C
	iC := C.cap_copy_int(unsafe.Pointer(&iE[0]))
	if iC == nil {
		log.Fatal("c failed to import go binary")
	}
	defer C.cap_free(unsafe.Pointer(iC))
	fC := C.cap_to_text(cC, &tCLen)
	if fC == nil {
		log.Fatal("basic c init caps -> text failure")
	}
	defer C.cap_free(unsafe.Pointer(fC))
	if got, want := C.GoString(fC), importT; got != want {
		log.Fatalf("c import from go yielded bad caps: got=%q want=%q", got, want)
	}

	// Validate that everyone agrees what all is:
	want := "=ep"
	all, err := cap.FromText("all=ep")
	if err != nil {
		log.Fatalf("unable to parse all=ep: %v", err)
	}
	if got := all.String(); got != want {
		log.Fatalf("all decode failed in Go: got=%q, want=%q", got, want)
	}

	iab, err := cap.IABFromText("cap_chown,!cap_setuid,^cap_setgid")
	if err != nil {
		log.Fatalf("failed to initialize iab from text: %v", err)
	}
	cIAB := C.cap_iab_init()
	defer C.cap_free(unsafe.Pointer(cIAB))
	for c := cap.MaxBits(); c > 0; {
		c--
		if en, err := iab.GetVector(cap.Inh, c); err != nil {
			log.Fatalf("failed to read iab.i[%v]", c)
		} else if en {
			if C.cap_iab_set_vector(cIAB, C.CAP_IAB_INH, C.cap_value_t(int(c)), C.CAP_SET) != 0 {
				log.Fatalf("failed to set C's AIB.I %v: %v", c)
			}
		}
		if en, err := iab.GetVector(cap.Amb, c); err != nil {
			log.Fatalf("failed to read iab.a[%v]", c)
		} else if en {
			if C.cap_iab_set_vector(cIAB, C.CAP_IAB_AMB, C.cap_value_t(int(c)), C.CAP_SET) != 0 {
				log.Fatalf("failed to set C's AIB.A %v: %v", c)
			}
		}
		if en, err := iab.GetVector(cap.Bound, c); err != nil {
			log.Fatalf("failed to read iab.b[%v]", c)
		} else if en {
			if C.cap_iab_set_vector(cIAB, C.CAP_IAB_BOUND, C.cap_value_t(int(c)), C.CAP_SET) != 0 {
				log.Fatalf("failed to set C's AIB.B %v: %v", c)
			}
		}
	}
	iabC := C.cap_iab_to_text(cIAB)
	if iabC == nil {
		log.Fatalf("failed to get text from C for %q", iab)
	}
	defer C.cap_free(unsafe.Pointer(iabC))
	if got, want := C.GoString(iabC), iab.String(); got != want {
		log.Fatalf("IAB for Go and C differ: got=%q, want=%q", got, want)
	}

	// Next, we attempt to manipulate some file capabilities on
	// the running program.  These are optional, based on whether
	// the current program is capable enough and do not involve
	// any cgo calls to libcap.
	tryFileCaps()

	// Nothing left to do but exit after this one.
	tryProcCaps()
	log.Printf("compare-cap success!")
}
