mirror of
https://github.com/luxfi/license.git
synced 2026-10-30 16:21:14 +00:00
- Stdlib-only deterministic binary token format (no JWT / CBOR dep). - Go API: VerifyToken, LoadFromEnv, HasFeature, HasFeatureOrDie. - Stable C ABI + C++17 RAII wrapper for non-Go commercial binaries (luxcpp/cevm). - cmd/lux-license: keygen / issue / inspect / verify. - Embedded development key; warns once at runtime until production key swap. - 22 Go tests + 1 cgo round-trip test, all passing.
259 lines
5.9 KiB
Go
259 lines
5.9 KiB
Go
// Copyright (C) 2020-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
// Package main exposes a stable C ABI (see capi/include/lux_license.h) over
|
|
// the Go API in github.com/luxfi/license. Build as a shared library or static
|
|
// archive:
|
|
//
|
|
// go build -buildmode=c-shared -o liblux_license.so ./capi
|
|
// go build -buildmode=c-archive -o liblux_license.a ./capi
|
|
package main
|
|
|
|
/*
|
|
#include <stdlib.h>
|
|
*/
|
|
import "C"
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"sync"
|
|
"unsafe"
|
|
|
|
"github.com/luxfi/license"
|
|
)
|
|
|
|
// claimsHandle holds the parsed Claims and any C-string pointers we have
|
|
// returned to the caller. We allocate C-strings lazily via C.CString and free
|
|
// them all when the handle is freed; this guarantees the simple-by-default
|
|
// invariant that "any pointer returned by lux_license_* is valid until
|
|
// lux_license_free(handle)".
|
|
type claimsHandle struct {
|
|
c *license.Claims
|
|
cstrsMu sync.Mutex
|
|
cstrs []unsafe.Pointer
|
|
}
|
|
|
|
// dupAndTrack copies a Go string into a C-allocated NUL-terminated buffer and
|
|
// remembers the pointer for cleanup. Caller treats the result as read-only.
|
|
func (h *claimsHandle) dupAndTrack(s string) *C.char {
|
|
p := C.CString(s)
|
|
h.cstrsMu.Lock()
|
|
h.cstrs = append(h.cstrs, unsafe.Pointer(p))
|
|
h.cstrsMu.Unlock()
|
|
return p
|
|
}
|
|
|
|
func (h *claimsHandle) freeAll() {
|
|
h.cstrsMu.Lock()
|
|
for _, p := range h.cstrs {
|
|
C.free(p)
|
|
}
|
|
h.cstrs = nil
|
|
h.cstrsMu.Unlock()
|
|
}
|
|
|
|
// Handle table: opaque IDs are C-allocated 1-byte stubs (so the pointer is in
|
|
// C memory, never a Go heap address that the GC might move). We index from
|
|
// `uintptr(cptr) -> *claimsHandle` via sync.Map.
|
|
var handles sync.Map // uintptr -> *claimsHandle
|
|
|
|
func registerHandle(h *claimsHandle) unsafe.Pointer {
|
|
stub := C.malloc(1)
|
|
if stub == nil {
|
|
return nil
|
|
}
|
|
handles.Store(uintptr(stub), h)
|
|
return stub
|
|
}
|
|
|
|
func lookupHandle(p unsafe.Pointer) *claimsHandle {
|
|
if p == nil {
|
|
return nil
|
|
}
|
|
v, ok := handles.Load(uintptr(p))
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return v.(*claimsHandle)
|
|
}
|
|
|
|
func unregisterHandle(p unsafe.Pointer) *claimsHandle {
|
|
if p == nil {
|
|
return nil
|
|
}
|
|
v, ok := handles.LoadAndDelete(uintptr(p))
|
|
if !ok {
|
|
return nil
|
|
}
|
|
C.free(p)
|
|
return v.(*claimsHandle)
|
|
}
|
|
|
|
// Last error: cgo callbacks come in on locked OS threads, so per-thread state
|
|
// requires either TLS or a global. We use a global with a mutex; the C ABI
|
|
// promises only that lux_license_last_error reflects the most recent failure
|
|
// from this thread. In practice, license operations are infrequent and the
|
|
// global serialization is invisible.
|
|
var (
|
|
lastErrMu sync.Mutex
|
|
lastErrC *C.char
|
|
)
|
|
|
|
func setLastError(err error) {
|
|
lastErrMu.Lock()
|
|
defer lastErrMu.Unlock()
|
|
if lastErrC != nil {
|
|
C.free(unsafe.Pointer(lastErrC))
|
|
lastErrC = nil
|
|
}
|
|
if err == nil {
|
|
return
|
|
}
|
|
lastErrC = C.CString(err.Error())
|
|
}
|
|
|
|
func clearLastError() { setLastError(nil) }
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Exported API
|
|
// ---------------------------------------------------------------------------
|
|
|
|
//export lux_license_load_from_env
|
|
func lux_license_load_from_env() unsafe.Pointer {
|
|
clearLastError()
|
|
c, err := license.LoadFromEnv()
|
|
if err != nil {
|
|
setLastError(err)
|
|
return nil
|
|
}
|
|
return registerHandle(&claimsHandle{c: c})
|
|
}
|
|
|
|
//export lux_license_verify_base64
|
|
func lux_license_verify_base64(tokenB64 *C.char) unsafe.Pointer {
|
|
clearLastError()
|
|
if tokenB64 == nil {
|
|
setLastError(fmt.Errorf("lux_license_verify_base64: NULL token"))
|
|
return nil
|
|
}
|
|
tok, err := license.DecodeBase64(C.GoString(tokenB64))
|
|
if err != nil {
|
|
setLastError(fmt.Errorf("decode base64: %w", err))
|
|
return nil
|
|
}
|
|
c, err := license.VerifyToken(tok)
|
|
if err != nil {
|
|
setLastError(err)
|
|
return nil
|
|
}
|
|
return registerHandle(&claimsHandle{c: c})
|
|
}
|
|
|
|
//export lux_license_free
|
|
func lux_license_free(p unsafe.Pointer) {
|
|
h := unregisterHandle(p)
|
|
if h == nil {
|
|
return
|
|
}
|
|
h.freeAll()
|
|
}
|
|
|
|
//export lux_license_has_feature
|
|
func lux_license_has_feature(p unsafe.Pointer, scope *C.char) C.int {
|
|
h := lookupHandle(p)
|
|
if h == nil || scope == nil {
|
|
return 0
|
|
}
|
|
if h.c.HasScope(C.GoString(scope)) {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
//export lux_license_customer_id
|
|
func lux_license_customer_id(p unsafe.Pointer) *C.char {
|
|
h := lookupHandle(p)
|
|
if h == nil {
|
|
return nil
|
|
}
|
|
return h.dupAndTrack(h.c.CustomerID)
|
|
}
|
|
|
|
//export lux_license_license_id
|
|
func lux_license_license_id(p unsafe.Pointer) *C.char {
|
|
h := lookupHandle(p)
|
|
if h == nil {
|
|
return nil
|
|
}
|
|
return h.dupAndTrack(h.c.LicenseID)
|
|
}
|
|
|
|
//export lux_license_scope_count
|
|
func lux_license_scope_count(p unsafe.Pointer) C.int {
|
|
h := lookupHandle(p)
|
|
if h == nil {
|
|
return 0
|
|
}
|
|
return C.int(len(h.c.Scope))
|
|
}
|
|
|
|
//export lux_license_scope
|
|
func lux_license_scope(p unsafe.Pointer, i C.int) *C.char {
|
|
h := lookupHandle(p)
|
|
if h == nil {
|
|
return nil
|
|
}
|
|
idx := int(i)
|
|
if idx < 0 || idx >= len(h.c.Scope) {
|
|
return nil
|
|
}
|
|
return h.dupAndTrack(h.c.Scope[idx])
|
|
}
|
|
|
|
//export lux_license_issued_at_unix
|
|
func lux_license_issued_at_unix(p unsafe.Pointer) C.int64_t {
|
|
h := lookupHandle(p)
|
|
if h == nil || h.c.IssuedAt.IsZero() {
|
|
return 0
|
|
}
|
|
return C.int64_t(h.c.IssuedAt.Unix())
|
|
}
|
|
|
|
//export lux_license_expiry_unix
|
|
func lux_license_expiry_unix(p unsafe.Pointer) C.int64_t {
|
|
h := lookupHandle(p)
|
|
if h == nil || h.c.Expiry.IsZero() {
|
|
return 0
|
|
}
|
|
return C.int64_t(h.c.Expiry.Unix())
|
|
}
|
|
|
|
//export lux_license_require_feature_or_die
|
|
func lux_license_require_feature_or_die(scope *C.char) {
|
|
if scope == nil {
|
|
fmt.Fprintln(os.Stderr, "FATAL: lux_license_require_feature_or_die called with NULL scope")
|
|
os.Exit(1)
|
|
}
|
|
license.HasFeatureOrDie(C.GoString(scope))
|
|
}
|
|
|
|
//export lux_license_last_error
|
|
func lux_license_last_error() *C.char {
|
|
lastErrMu.Lock()
|
|
defer lastErrMu.Unlock()
|
|
if lastErrC == nil {
|
|
// Empty string sentinel: return a static empty C string. Allocate once.
|
|
if emptyErr == nil {
|
|
emptyErr = C.CString("")
|
|
}
|
|
return emptyErr
|
|
}
|
|
return lastErrC
|
|
}
|
|
|
|
var emptyErr *C.char
|
|
|
|
// main is required by cgo's c-shared / c-archive build modes but never called.
|
|
func main() {}
|