//go:build pam

// Package pamauth authenticates a username/password pair against the host's PAM
// stack (/etc/pam.d/<service>). It is the backend for uopi's built-in HTTP Basic
// authentication: because the uopi host is typically already an SSSD/LDAP client,
// validating through PAM reuses the exact same login path as `login`/`ssh`
// (pam_sss → the site directory) without uopi needing any directory schema.
//
// This file is the real implementation, compiled only with the `pam` build tag
// (which also requires cgo + libpam). The default fully-static CGO_ENABLED=0
// build uses stub.go instead, where Authenticate reports PAM is unavailable.
package pamauth

/*
#cgo LDFLAGS: -lpam
#include <security/pam_appl.h>
#include <stdlib.h>
#include <string.h>

// uopiPamConv answers every password-style PAM prompt with the password passed
// through appdata_ptr. Informational/error messages get a NULL response. The PAM
// library takes ownership of the returned responses and frees them.
static int uopiPamConv(int num_msg, const struct pam_message **msg,
                       struct pam_response **resp, void *appdata_ptr) {
    if (num_msg <= 0 || num_msg > PAM_MAX_NUM_MSG) {
        return PAM_CONV_ERR;
    }
    struct pam_response *r = calloc((size_t)num_msg, sizeof(struct pam_response));
    if (r == NULL) {
        return PAM_BUF_ERR;
    }
    for (int i = 0; i < num_msg; i++) {
        int style = msg[i]->msg_style;
        if (style == PAM_PROMPT_ECHO_OFF || style == PAM_PROMPT_ECHO_ON) {
            r[i].resp = strdup((const char *)appdata_ptr);
            if (r[i].resp == NULL) {
                for (int j = 0; j < i; j++) {
                    free(r[j].resp);
                }
                free(r);
                return PAM_BUF_ERR;
            }
        }
        r[i].resp_retcode = 0;
    }
    *resp = r;
    return PAM_SUCCESS;
}

// uopiPamAuth runs authentication + account management for service/user using
// pass. Returns PAM_SUCCESS or the failing PAM error code.
static int uopiPamAuth(const char *service, const char *user, char *pass) {
    struct pam_conv conv;
    conv.conv = uopiPamConv;
    conv.appdata_ptr = (void *)pass;

    pam_handle_t *pamh = NULL;
    int ret = pam_start(service, user, &conv, &pamh);
    if (ret != PAM_SUCCESS) {
        return ret;
    }
    ret = pam_authenticate(pamh, PAM_DISALLOW_NULL_AUTHTOK);
    if (ret == PAM_SUCCESS) {
        ret = pam_acct_mgmt(pamh, PAM_DISALLOW_NULL_AUTHTOK);
    }
    pam_end(pamh, ret);
    return ret;
}

// uopiStrerror maps a PAM error code to a human-readable string. Linux-PAM
// ignores the handle, so NULL is fine after pam_end.
static const char *uopiStrerror(int code) {
    return pam_strerror(NULL, code);
}
*/
import "C"

import (
	"errors"
	"fmt"
	"unsafe"
)

// Available reports whether this build includes PAM support. True here.
const Available = true

// ErrUnavailable is returned by Authenticate in builds without PAM support. It
// is declared in both build variants so callers can compare against it.
var ErrUnavailable = errors.New("pamauth: PAM support not compiled in")

// Authenticate verifies username/password against the named PAM service
// (/etc/pam.d/<service>). It returns nil on success or an error describing the
// PAM failure. It is safe for concurrent use.
func Authenticate(service, username, password string) error {
	cService := C.CString(service)
	cUser := C.CString(username)
	cPass := C.CString(password)
	defer C.free(unsafe.Pointer(cService))
	defer C.free(unsafe.Pointer(cUser))
	defer C.free(unsafe.Pointer(cPass))

	if ret := C.uopiPamAuth(cService, cUser, cPass); ret != C.PAM_SUCCESS {
		return fmt.Errorf("pam: %s", C.GoString(C.uopiStrerror(ret)))
	}
	return nil
}
