]> git.ipfire.org Git - thirdparty/util-linux.git/commitdiff
lastlog2 - Y2038 safe version of lastlog
authorStefan Schubert <schubi@suse.de>
Thu, 7 Sep 2023 15:30:23 +0000 (17:30 +0200)
committerStefan Schubert <schubi@suse.de>
Thu, 18 Jan 2024 08:08:04 +0000 (09:08 +0100)
pam_lastlog2 - PAM module which logs user login with lastlog2

61 files changed:
.github/workflows/cibuild-setup-ubuntu.sh
.gitignore
.packit.yaml
AUTHORS
Makefile.am
bash-completion/Makemodule.am
bash-completion/lastlog2 [new file with mode: 0644]
configure.ac
liblastlog2/COPYING [new file with mode: 0644]
liblastlog2/Makemodule.am [new file with mode: 0644]
liblastlog2/README.md [new file with mode: 0644]
liblastlog2/lastlog2.pc.in [new file with mode: 0644]
liblastlog2/man/Makemodule.am [new file with mode: 0644]
liblastlog2/man/lastlog2.3.adoc [new file with mode: 0644]
liblastlog2/man/ll2_import_lastlog.3.adoc [new file with mode: 0644]
liblastlog2/man/ll2_new_context.3.adoc [new file with mode: 0644]
liblastlog2/man/ll2_read_all.3.adoc [new file with mode: 0644]
liblastlog2/man/ll2_read_entry.3.adoc [new file with mode: 0644]
liblastlog2/man/ll2_remove_entry.3.adoc [new file with mode: 0644]
liblastlog2/man/ll2_rename_user.3.adoc [new file with mode: 0644]
liblastlog2/man/ll2_unref_context.3.adoc [new file with mode: 0644]
liblastlog2/man/ll2_update_login_time.3.adoc [new file with mode: 0644]
liblastlog2/man/ll2_write_entry.3.adoc [new file with mode: 0644]
liblastlog2/meson.build [new file with mode: 0644]
liblastlog2/src/Makemodule.am [new file with mode: 0644]
liblastlog2/src/lastlog2.c [new file with mode: 0644]
liblastlog2/src/lastlog2.h [new file with mode: 0644]
liblastlog2/src/lastlog2P.h [new file with mode: 0644]
liblastlog2/src/liblastlog2.sym [new file with mode: 0644]
liblastlog2/src/tests/tst_dlopen.c [new file with mode: 0644]
liblastlog2/src/tests/tst_pam_lastlog2_output.c [new file with mode: 0644]
liblastlog2/src/tests/tst_remove_entry.c [new file with mode: 0644]
liblastlog2/src/tests/tst_rename_user.c [new file with mode: 0644]
liblastlog2/src/tests/tst_write_read_user.c [new file with mode: 0644]
liblastlog2/src/tests/tst_y2038_ll2_read_all.c [new file with mode: 0644]
liblastlog2/src/tests/tst_y2038_sqlite3_time.c [new file with mode: 0644]
meson.build
meson_options.txt
misc-utils/.gitignore
misc-utils/Makemodule.am
misc-utils/lastlog2-import.service.in [new file with mode: 0644]
misc-utils/lastlog2.8.adoc [new file with mode: 0644]
misc-utils/lastlog2.c [new file with mode: 0644]
misc-utils/lastlog2.conf.in [new file with mode: 0644]
misc-utils/meson.build
pam_lastlog2/COPYING [new file with mode: 0644]
pam_lastlog2/Makemodule.am [new file with mode: 0644]
pam_lastlog2/man/Makemodule.am [new file with mode: 0644]
pam_lastlog2/man/pam_lastlog2.8.adoc [new file with mode: 0644]
pam_lastlog2/meson.build [new file with mode: 0644]
pam_lastlog2/src/Makemodule.am [new file with mode: 0644]
pam_lastlog2/src/pam_lastlog2.c [new file with mode: 0644]
pam_lastlog2/src/pam_lastlog2.sym [new file with mode: 0644]
tests/commands.sh
tests/ts/liblastlog2/dlopen [new file with mode: 0755]
tests/ts/liblastlog2/pam_lastlog2_output [new file with mode: 0755]
tests/ts/liblastlog2/remove_entry [new file with mode: 0755]
tests/ts/liblastlog2/rename_user [new file with mode: 0755]
tests/ts/liblastlog2/sqlite3_time [new file with mode: 0755]
tests/ts/liblastlog2/write_read_user [new file with mode: 0755]
tests/ts/liblastlog2/y2038_ll2_read_all [new file with mode: 0755]

index 34774d3a42b1391d11e48f25b1eff9a405410b49..7e75025d10778256d52cee551cf5963599570afe 100755 (executable)
@@ -32,6 +32,7 @@ PACKAGES=(
        gawk
        bison
        flex
+       libsqlite3-dev
 )
 
 PACKAGES_OPTIONAL=(
index aa33f35aaa6edbaa292f460295ca99cc609492b7..3af79d132539aded41b863b4aead491b6e492525 100644 (file)
@@ -203,3 +203,5 @@ ylwrap
 /wipefs
 /write
 /zramctl
+/lsclocks
+/lastlog2
index 9409d6209098a00f0bc06de2789399e292375385..0229a9cc443776c0b70717b26e1528b3c2e0c26e 100644 (file)
@@ -24,7 +24,7 @@ actions:
     # Drop all patches, since they're already included in the tarball
     - "sed -ri '/^Patch[0-9]+:/d' .packit_rpm/util-linux.spec"
     # Install additional dependencies we need for the build/tests
-    - "sed -i '/^### Dependencies/aBuildRequires: autoconf automake bc bison flex iproute libtool procps-ng socat' .packit_rpm/util-linux.spec"
+    - "sed -i '/^### Dependencies/aBuildRequires: autoconf automake bc bison flex iproute libtool procps-ng socat sqlite-devel' .packit_rpm/util-linux.spec"
     # We need to call autogen, since we use a custom tarball
     - "sed -i '/^unset LINGUAS/a./autogen.sh' .packit_rpm/util-linux.spec"
     # Enable tests after build
diff --git a/AUTHORS b/AUTHORS
index 64cddb1735a73453fd884f29a835fd41aa2a2ad0..8e3da1f24abdd2d0807985b9c399000f35536166 100644 (file)
--- a/AUTHORS
+++ b/AUTHORS
@@ -42,6 +42,7 @@ AUTHORS (merged projects & commands):
                        Theodore Ts'o <tytso@mit.edu>
       libmount:        Karel Zak <kzak@redhat.com>
       libuuid:         Theodore Ts'o <tytso@mit.edu>
+      liblastlog2:     Thorsten Kukuk <kukuk@suse.com>
       lscpu:           Cai Qian <qcai@redhat.com>
       lsclocks:        Thomas Weißschuh <thomas@t-8ch.de>
       lsblk:           Milan Broz <gmazyland@gmail.com>
index 2df89ff6dfeee3bd9a7f48989a983fa018a6e241..796e82fabd73f8d54de4b404eeaea87a6fe1255c 100644 (file)
@@ -41,7 +41,7 @@ ul_libblkid_incdir = $(top_builddir)/libblkid/src
 ul_libmount_incdir = $(top_builddir)/libmount/src
 ul_libsmartcols_incdir = $(top_builddir)/libsmartcols/src
 ul_libfdisk_incdir  = $(top_builddir)/libfdisk/src
-
+ul_liblastlog2_incdir  = $(top_srcdir)/liblastlog2/src
 ul_libuuid_incdir  = $(top_srcdir)/libuuid/src
 
 bashcompletiondir = @bashcompletiondir@
@@ -59,6 +59,7 @@ bin_PROGRAMS =
 sbin_PROGRAMS =
 dist_usrbin_exec_SCRIPTS =
 systemdsystemunit_DATA =
+tmpfiles_DATA =
 dist_bashcompletion_DATA =
 check_PROGRAMS =
 dist_check_SCRIPTS =
@@ -103,6 +104,7 @@ include tools/Makemodule.am
 include include/Makemodule.am
 include lib/Makemodule.am
 include libuuid/Makemodule.am
+include liblastlog2/Makemodule.am
 include libblkid/Makemodule.am
 include libmount/Makemodule.am
 include libsmartcols/Makemodule.am
@@ -116,6 +118,7 @@ include sys-utils/Makemodule.am
 include misc-utils/Makemodule.am
 include disk-utils/Makemodule.am
 
+include pam_lastlog2/Makemodule.am
 include bash-completion/Makemodule.am
 include man-common/Makemodule.am
 
@@ -138,6 +141,7 @@ EXTRA_DIST += \
        po/meson.build \
        lib/meson.build \
        libuuid/meson.build \
+       liblastlog2/meson.build \
        sys-utils/meson.build \
        libfdisk/meson.build \
        term-utils/meson.build \
@@ -176,6 +180,7 @@ edit_cmd = sed \
         -e 's|@VERSION[@]|$(VERSION)|g' \
         -e 's|@ADJTIME_PATH[@]|$(ADJTIME_PATH)|g' \
         -e 's|@LIBUUID_VERSION[@]|$(LIBUUID_VERSION)|g' \
+        -e 's|@LIBLASTLOG2_VERSION[@]|$(LIBLASTLOG2_VERSION)|g' \
         -e 's|@LIBMOUNT_VERSION[@]|$(LIBMOUNT_VERSION)|g' \
         -e 's|@LIBMOUNT_MAJOR_VERSION[@]|$(LIBMOUNT_MAJOR_VERSION)|g' \
         -e 's|@LIBMOUNT_MINOR_VERSION[@]|$(LIBMOUNT_MINOR_VERSION)|g' \
@@ -346,8 +351,8 @@ DISTCHECK_CONFIGURE_FLAGS = \
        --enable-gtk-doc \
        --with-python \
        --with-bashcompletiondir=$$dc_install_base/$(bashcompletiondir) \
-       --with-systemdsystemunitdir=$$dc_install_base/$(systemdsystemunitdir)
-
+       --with-systemdsystemunitdir=$$dc_install_base/$(systemdsystemunitdir) \
+       --with-tmpfilesdir=$$dc_install_base/$(tmpfilesdir)
 
 BUILT_SOURCES += .version
 .version:
index b4a6db1962b5d221a345b6e4ea5ce341b042b21c..034f9434aa2962d0aa355be9face932b23405cb8 100644 (file)
@@ -201,6 +201,9 @@ endif
 if BUILD_UUIDGEN
 dist_bashcompletion_DATA += bash-completion/uuidgen
 endif
+if BUILD_LIBLASTLOG2
+dist_bashcompletion_DATA += bash-completion/lastlog2
+endif
 if BUILD_UUIDPARSE
 dist_bashcompletion_DATA += bash-completion/uuidparse
 endif
diff --git a/bash-completion/lastlog2 b/bash-completion/lastlog2
new file mode 100644 (file)
index 0000000..09aa4ed
--- /dev/null
@@ -0,0 +1,57 @@
+_lastlog2_module()
+{
+       local cur prev OPTS
+       COMPREPLY=()
+       cur="${COMP_WORDS[COMP_CWORD]}"
+       prev="${COMP_WORDS[COMP_CWORD-1]}"
+       case $prev in
+               '-b'|'--before')
+                       COMPREPLY=( $(compgen -W "days" -- $cur) )
+                       return 0
+                       ;;
+               '-t'|'--time')
+                       COMPREPLY=( $(compgen -W "days" -- $cur) )
+                       return 0
+                       ;;
+               '-i'|'--import')
+                       COMPREPLY=( $(compgen -W "file" -- "$cur") )
+                       return 0
+                       ;;
+               '-r'|'--rename')
+                       COMPREPLY=( $(compgen -W "user_name" -- "$cur") )
+                       return 0
+                       ;;
+               '-u'|'--user')
+                       COMPREPLY=( $(compgen -W "login" -- "$cur") )
+                       return 0
+                       ;;                      
+               '-d'|'--database')
+                       COMPREPLY=( $(compgen -W "file" -- "$cur") )
+                       return 0
+                       ;;                      
+               '-h'|'--help'|'-V'|'--version')
+                       return 0
+                       ;;
+       esac
+       case $cur in
+               -*)
+                       OPTS="
+                               --before
+                               --clear
+                               --database
+                               --help
+                               --import
+                               --rename
+                               --service
+                               --set
+                               --time
+                               --user
+                               --version
+                       "
+                       COMPREPLY=( $(compgen -W "${OPTS[*]}" -- $cur) )
+                       return 0
+                       ;;
+       esac
+       return 0
+}
+complete -F _lastlog2_module lastlog2
index a03f3c4428f5b362993c97c20134422f5dfc9cf7..600ff2a811f16f94b553d3f2ba0ba5976450375f 100644 (file)
@@ -42,6 +42,13 @@ LIBUUID_LT_MINOR=3
 LIBUUID_LT_MICRO=0
 LIBUUID_VERSION_INFO=`expr $LIBUUID_LT_MAJOR + $LIBUUID_LT_MINOR`:$LIBUUID_LT_MICRO:$LIBUUID_LT_MINOR
 
+dnl liblastlog2 version
+LIBLASTLOG2_VERSION="$PACKAGE_VERSION_MAJOR.$PACKAGE_VERSION_MINOR.$PACKAGE_VERSION_RELEASE"
+LIBLASTLOG2_LT_MAJOR=1
+LIBLASTLOG2_LT_MINOR=3
+LIBLASTLOG2_LT_MICRO=0
+LIBLASTLOG2_VERSION_INFO=`expr $LIBLASTLOG2_LT_MAJOR + $LIBLASTLOG2_LT_MINOR`:$LIBLASTLOG2_LT_MICRO:$LIBLASTLOG2_LT_MINOR
+
 dnl libmount version
 LIBMOUNT_VERSION="$PACKAGE_VERSION_MAJOR.$PACKAGE_VERSION_MINOR.$PACKAGE_VERSION_RELEASE"
 LIBMOUNT_LT_MAJOR=1
@@ -383,6 +390,7 @@ AC_CHECK_HEADERS([ \
        unistd.h \
        utmp.h \
        utmpx.h \
+       sqlite3.h \
 ])
 
 # There is a collision in old kernel-headers. The both files mount.h and fs.h
@@ -505,6 +513,7 @@ have_shadow_h=$ac_cv_header_shadow_h
 have_sys_signalfd_h=$ac_cv_header_sys_signalfd_h
 have_utmpx_h=$ac_cv_header_utmpx_h
 have_mntent_h=$ac_cv_header_mntent_h
+have_sqlite3_h=$ac_cv_header_sqlite3_h
 
 AS_CASE([$linux_os:$have_linux_version_h],
   [yes:no],
@@ -1211,6 +1220,32 @@ AS_IF([test "x$build_libuuid" = xyes], [
   AC_DEFINE(HAVE_LIBUUID, 1, [Define to 1 if you have the -luuid.])
 ])
 
+dnl
+dnl liblastlog2
+dnl
+AC_ARG_ENABLE([liblastlog2],
+  AS_HELP_STRING([--disable-liblastlog2], [do not build liblastlog2 and lastlog2 utilities]),
+  [enable_liblastlog2=no], [enable_liblastlog2=check]
+)
+AC_SUBST([LIBLASTLOG2_VERSION])
+AC_SUBST([LIBLASTLOG2_VERSION_INFO])
+AC_DEFINE_UNQUOTED([LIBLASTLOG2_VERSION], ["$LIBLASTLOG2_VERSION"], [liblastlog2 version string])
+
+have_lastlog2=no
+AS_IF([test "x$enable_liblastlog2" != xno], [
+  AS_CASE([$enable_liblastlog2:$have_sqlite3_h],
+    [yes:no],
+       [AC_MSG_ERROR([lastlog2 selected but sqlite3.h not found])],
+    [check:no],
+       [AC_MSG_WARN([sqlite3.h not found, do not build with lastlog2 support])],
+    [*:yes],
+       [have_lastlog2=yes
+        AC_DEFINE(HAVE_LIBLASTLOG2, 1, [Define to 1 if you have the -llastlog2.])]
+  )
+])
+AM_CONDITIONAL([BUILD_LIBLASTLOG2], [test "x$have_lastlog2" = xyes])
+AM_CONDITIONAL([BUILD_LIBLASTLOG2_TESTS], [test "x$have_lastlog2" = xyes && test "x$enable_static" = xyes])
+
 dnl
 dnl libblkid
 dnl
@@ -2594,6 +2629,18 @@ AS_IF([test "x$with_systemdsystemunitdir" != "xno"], [
   AC_SUBST([systemdsystemunitdir], [$with_systemdsystemunitdir])
 ])
 
+AC_ARG_WITH([tmpfilesdir],
+  AS_HELP_STRING([--with-tmpfilesdir=DIR], [directory for tmpfiles. See tmpfiles.d(5) for details]),
+  [], [
+  AS_IF([test "x$have_systemd" = xyes], [
+    PKG_CHECK_VAR([with_tmpfilesdir], [systemd], [tmpfilesdir],
+      [],
+      [with_tmpfilesdir=no])
+  ])
+])
+AS_IF([test "x$with_tmpfilesdir" != "xno"], [
+  AC_SUBST([tmpfilesdir], [$with_tmpfilesdir])
+])
 
 AC_ARG_WITH([smack],
   AS_HELP_STRING([--with-smack], [build with SMACK support]),
@@ -2937,8 +2984,10 @@ AC_MSG_RESULT([
         Bash completions:          ${with_bashcompletiondir}
         Systemd support:           ${have_systemd}
         Systemd unitdir:           ${with_systemdsystemunitdir}
+        tmpfilesdir:               ${with_tmpfilesdir}
         libeconf support:          ${have_econf}
         Btrfs support:             ${have_btrfs}
+        lastlog2 support:          ${have_lastlog2}
         Wide-char support:         ${build_widechar}
         libcryptsetup support:     ${have_cryptsetup}
 
diff --git a/liblastlog2/COPYING b/liblastlog2/COPYING
new file mode 100644 (file)
index 0000000..df71a98
--- /dev/null
@@ -0,0 +1,24 @@
+BSD 2-Clause License
+
+Copyright (c) 2023, Thorsten Kukuk
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/liblastlog2/Makemodule.am b/liblastlog2/Makemodule.am
new file mode 100644 (file)
index 0000000..2f69f88
--- /dev/null
@@ -0,0 +1,10 @@
+if BUILD_LIBLASTLOG2
+
+include liblastlog2/man/Makemodule.am
+include liblastlog2/src/Makemodule.am
+
+pkgconfig_DATA += liblastlog2/lastlog2.pc
+PATHFILES      += liblastlog2/lastlog2.pc
+EXTRA_DIST     += liblastlog2/COPYING
+
+endif # BUILD_LIBLASTLOG2
diff --git a/liblastlog2/README.md b/liblastlog2/README.md
new file mode 100644 (file)
index 0000000..8bcfb39
--- /dev/null
@@ -0,0 +1,40 @@
+# lastlog2
+
+**Y2038 safe version of lastlog**
+
+## Background
+
+`lastlog` reports the last login of a given user or of all users who did ever login on a system.
+
+The standard `/var/log/lastlog` implementation using `lastlog.h` from glibc uses a **32bit** **time_t** in `struct lastlog` on bi-arch systems like x86-64 (so which can execute 64bit and 32bit binaries). So even if you have a pure 64bit system, on many architectures using glibc you have a Y2038 problem.
+
+For background on the Y2038 problem (32bit time_t counter will overflow) I suggest to start with the wikipedia [Year 2038 problem](https://en.wikipedia.org/wiki/Year_2038_problem) article.
+
+There is also a more [technical document](https://github.com/thkukuk/utmpx/blob/main/Y2038.md), describing the problem in more detail, which also contains a list of affected packages. And a more highlevel blog "[Y2038, glibc and /var/log/lastlog on 64bit architectures](https://www.thkukuk.de/blog/Y2038_glibc_lastlog_64bit/)"
+
+Additional, `/var/log/lastlog` can become really huge if there are big UIDs in use on the system. Since it is a sparse file, this is normally not a problem, but depending on the filesystem or the tools used for backup, this can become a real problem.
+
+Since there are only few applications which really support `lastlog`, the data is also not always correct.
+
+## lastlog2
+
+`lastlog2` tries to solve this problems:
+
+* It's using sqlite3 as database backend.
+* Data is only collected via a PAM module, so that every tools can make use of it, without modifying existing packages.
+* The output is as compatible as possible with the old lastlog implementation.
+* The old `/var/log/lastlog` file can be imported into the new database.
+* The size of the database depends on the amount of users, not how big the biggest UID is.
+
+**IMPORTANT** To be Y2038 safe on 32bit architectures, the binaries needs to be build with a **64bit time_t**. This should be the standard on 64bit architectures.
+
+Besides the lastlog2 library in this directory there are additional parts like service definition, PAM module and applications:
+
+* `liblastlog2.so.0` contains all high level functions to manage the data (util-linux/liblastlog2).
+* `pam_lastlog2.so` shows the last login of a user and stores the new login into the database (util-linux/pam_lastlog2).
+* `lastlog2` will display the last logins for all users, who did ever login (util-linux/misc-utils).
+* `lastlog2-import.service` a service which imports lastlog data into lastlog2 database (util-linux/misc-utils).
+* `lastlog2.conf` configuration for tmpfiles.d. See tmpfiles.d(5) for details (util-linux/misc-utils).
+
+By default the database will be written as `/var/lib/lastlog/lastlog2.db`.
+
diff --git a/liblastlog2/lastlog2.pc.in b/liblastlog2/lastlog2.pc.in
new file mode 100644 (file)
index 0000000..a2b61dd
--- /dev/null
@@ -0,0 +1,11 @@
+prefix=@prefix@
+exec_prefix=@exec_prefix@
+libdir=@usrlib_execdir@
+includedir=@includedir@
+
+Name: lastlog2
+Description: Y2038 safe version of lastlog
+Version: @LIBLASTLOG2_VERSION@
+Requires:
+Cflags: -I${includedir}/lastlog2
+Libs: -L${libdir} -llastlog2
diff --git a/liblastlog2/man/Makemodule.am b/liblastlog2/man/Makemodule.am
new file mode 100644 (file)
index 0000000..803bfb7
--- /dev/null
@@ -0,0 +1,20 @@
+
+MANPAGES += \
+       liblastlog2/man/lastlog2.3 \
+       liblastlog2/man/ll2_import_lastlog.3 \
+       liblastlog2/man/ll2_read_all.3 \
+       liblastlog2/man/ll2_read_entry.3 \
+       liblastlog2/man/ll2_remove_entry.3 \
+       liblastlog2/man/ll2_rename_user.3 \
+       liblastlog2/man/ll2_update_login_time.3 \
+       liblastlog2/man/ll2_write_entry.3
+
+dist_noinst_DATA += \
+       liblastlog2/man/lastlog2.3.adoc \
+       liblastlog2/man/ll2_import_lastlog.3.adoc \
+       liblastlog2/man/ll2_read_all.3.adoc \
+       liblastlog2/man/ll2_read_entry.3.adoc \
+       liblastlog2/man/ll2_remove_entry.3.adoc \
+       liblastlog2/man/ll2_rename_user.3.adoc \
+       liblastlog2/man/ll2_update_login_time.3.adoc \
+       liblastlog2/man/ll2_write_entry.3.adoc
diff --git a/liblastlog2/man/lastlog2.3.adoc b/liblastlog2/man/lastlog2.3.adoc
new file mode 100644 (file)
index 0000000..05cab12
--- /dev/null
@@ -0,0 +1,51 @@
+//po4a: entry man manual
+= lastlog2(3)
+:doctype: manpage
+:man manual: Programmer's Manual
+:man source: util-linux {release-version}
+:page-layout: base
+:lib: liblastlog2
+:firstversion: 2.40
+
+== NAME
+
+lastlog2 - Y2038 safe version of lastlog library.
+
+== SYNOPSIS
+
+*#include <lastlog2.h>*
+
+== DESCRIPTION
+
+*lastlog2* reports the last login of a given user or of all users who did ever login on a system.
+
+It's using sqlite3 as database backend. Data is only collected via a PAM module, so that every
+tools can make use of it, without modifying existing packages.
+The output is as compatible as possible with the old lastlog implementation.
+By default the database will be written as `/var/lib/lastlog/lastlog2.db`.
+The size of the database depends on the amount of users, not how big the biggest UID is.
+
+== AUTHORS
+
+Thorsten Kukuk (kukuk@suse.de)
+
+== SEE ALSO
+
+*lastlog2*(3),
+*ll2_new_context(3),
+*ll2_unref_context(3),
+*ll2_write_entry*(3),
+*ll2_read_all*(3),
+*ll2_read_entry*(3),
+*ll2_update_login_time*(3),
+*ll2_remove_entry*(3),
+*ll2_rename_user*(3),
+*ll2_import_lastlog*(3)
+
+include::man-common/bugreports.adoc[]
+
+include::man-common/footer-lib.adoc[]
+
+ifdef::translation[]
+include::man-common/translation.adoc[]
+endif::[]
diff --git a/liblastlog2/man/ll2_import_lastlog.3.adoc b/liblastlog2/man/ll2_import_lastlog.3.adoc
new file mode 100644 (file)
index 0000000..9ddf54b
--- /dev/null
@@ -0,0 +1,65 @@
+//po4a: entry man manual
+= ll2_import_lastlog(3)
+:doctype: manpage
+:man manual: Programmer's Manual
+:man source: util-linux {release-version}
+:page-layout: base
+:lib: liblastlog2
+:firstversion: 2.40
+
+== NAME
+
+ll2_import_lastlog - Import old lastlog file.
+
+== SYNOPSIS
+
+*#include <lastlog2.h>*
+
+*int ll2_import_lastlog (struct ll2_context *__context__,
+                        const char *__lastlog_file__,
+                        char **__error__);*
+
+== DESCRIPTION
+
+Importing all entries from _lastlog_file_ file (lastlog(8)) into
+lastlog2 database defined with _context_.
+If _context_ is NULL, the default database, defined in _LL2_DEFAULT_DATABASE_,
+will be taken.
+
+--------------------------------------
+char    *error = NULL;
+const   char *lastlog_path = "/var/log/lastlog";
+
+int ret = ll2_import_lastlog (NULL, lastlog_path, &error);
+--------------------------------------
+
+== RETURN VALUE
+
+Returns 0 on success, -ENOMEM or -1 on other failure.
+_error_ contains an error string if the return value is -1.
+_error_ is not guaranteed to contain an error string, could also be NULL.
+_error_ should be freed by the caller.
+
+== AUTHORS
+
+Thorsten Kukuk (kukuk@suse.de)
+
+== SEE ALSO
+
+*lastlog2*(3),
+*ll2_new_context(3),
+*ll2_unref_context(3),
+*ll2_read_all*(3),
+*ll2_write_entry*(3),
+*ll2_read_entry*(3),
+*ll2_update_login_time*(3),
+*ll2_rename_user*(3),
+*ll2_remove_entry*(3)
+
+include::man-common/bugreports.adoc[]
+
+include::man-common/footer-lib.adoc[]
+
+ifdef::translation[]
+include::man-common/translation.adoc[]
+endif::[]
diff --git a/liblastlog2/man/ll2_new_context.3.adoc b/liblastlog2/man/ll2_new_context.3.adoc
new file mode 100644 (file)
index 0000000..9a46646
--- /dev/null
@@ -0,0 +1,61 @@
+//po4a: entry man manual
+= ll2_new_context(3)
+:doctype: manpage
+:man manual: Programmer's Manual
+:man source: util-linux {release-version}
+:page-layout: base
+:lib: liblastlog2
+:firstversion: 2.40
+
+== NAME
+
+ll2_new_context - Context which defines the lastlog2 environment.
+
+== SYNOPSIS
+
+*#include <lastlog2.h>*
+
+*ll2_context * ll2_new_context(const char *__db_path__);
+
+== DESCRIPTION
+
+Defining lastlog2 context e.g. database file, which will be used for
+any other lastlog2 calls. If _db_path_ is NULL, the default path defined
+in _LL2_DEFAULT_DATABASE_ will be taken.
+
+--------------------------------------
+
+const   char *db_path = "/var/lib/lastlog/lastlog2.db";
+ll2_context *context = ll2_new_context(db_path);
+
+--------------------------------------
+
+== RETURN VALUE
+
+Returns context which will used for all other lastlog2 library calls.
+This context should be released with _ll2_unref_context_ when it is not
+needed anymore.
+Returns NULL on an error.
+
+== AUTHORS
+
+Thorsten Kukuk (kukuk@suse.de)
+
+== SEE ALSO
+
+*lastlog2*(3),
+*ll2_unref_context(3),
+*ll2_read_all*(3),
+*ll2_write_entry*(3),
+*ll2_read_entry*(3),
+*ll2_remove_entry*(3),
+*ll2_update_login_time*(3),
+*ll2_import_lastlog*(3)
+
+include::man-common/bugreports.adoc[]
+
+include::man-common/footer-lib.adoc[]
+
+ifdef::translation[]
+include::man-common/translation.adoc[]
+endif::[]
diff --git a/liblastlog2/man/ll2_read_all.3.adoc b/liblastlog2/man/ll2_read_all.3.adoc
new file mode 100644 (file)
index 0000000..fbe589d
--- /dev/null
@@ -0,0 +1,74 @@
+//po4a: entry man manual
+= ll2_read_all(3)
+:doctype: manpage
+:man manual: Programmer's Manual
+:man source: util-linux {release-version}
+:page-layout: base
+:lib: liblastlog2
+:firstversion: 2.40
+
+== NAME
+
+ll2_read_all -  Reads all entries from database and calls the callback function for each entry.
+
+== SYNOPSIS
+
+*#include <lastlog2.h>*
+*int ll2_read_all (struct ll2_context *__context__,
+                  int (*__callback__)(const char *__user__, int64_t __ll_time__,
+                                      const char *__tty__, const char *__rhost__,
+                                      const char *__pam_service__, const char *__cb_error__),
+                  char **__error__);*
+
+== DESCRIPTION
+
+Reads all entries from database, defined in _context_, and calls callback fuction _callback_ for each entry.
+If _context_ is NULL, the default database, defined in _LL2_DEFAULT_DATABASE_, will be taken.
+
+--------------------------------------
+char  *error = NULL;
+const char *user = "root";
+
+static int
+callback (const char *res_user, int64_t ll_time, const char *res_tty,
+         const char *res_rhost, const char *res_service, const char *cb_error)
+{
+   /* returning != 0 if no further entry has to be handled by the callback */
+   return 0;
+}
+
+int ret = ll2_read_all (NULL, callback, &error);
+--------------------------------------
+
+== RETURN VALUE
+
+Returns 0 on success, -ENOMEM or -1 on other failure.
+_error_ contains an error string if the return value is -1.
+_error_ is not guaranteed to contain an error string, could also be NULL.
+_error_ should be freed by the caller.
+If lastlog2 database does not exist at all, the errno ENOENT has been set
+and can be checked.
+
+== AUTHORS
+
+Thorsten Kukuk (kukuk@suse.de)
+
+== SEE ALSO
+
+*lastlog2*(3),
+*ll2_new_context(3),
+*ll2_unref_context(3),
+*ll2_write_entry*(3),
+*ll2_read_entry*(3),
+*ll2_update_login_time*(3),
+*ll2_remove_entry*(3),
+*ll2_rename_user*(3),
+*ll2_import_lastlog*(3)
+
+include::man-common/bugreports.adoc[]
+
+include::man-common/footer-lib.adoc[]
+
+ifdef::translation[]
+include::man-common/translation.adoc[]
+endif::[]
diff --git a/liblastlog2/man/ll2_read_entry.3.adoc b/liblastlog2/man/ll2_read_entry.3.adoc
new file mode 100644 (file)
index 0000000..3bef1a8
--- /dev/null
@@ -0,0 +1,72 @@
+//po4a: entry man manual
+= ll2_read_entry(3)
+:doctype: manpage
+:man manual: Programmer's Manual
+:man source: util-linux {release-version}
+:page-layout: base
+:lib: liblastlog2
+:firstversion: 2.40
+
+== NAME
+
+ll2_read_entry -  Reads one entry from database and returns that.
+
+== SYNOPSIS
+
+*#include <lastlog2.h>*
+
+*int ll2_read_entry (struct ll2_context *__context__, const char *__user__,
+                    int64_t *__ll_time__, char **__tty__, char **__rhost__,
+                    char **__pam_service__, char **__error__);*
+
+== DESCRIPTION
+
+Reads the first entry from database, defined in _context_, for user _user_.
+If _context_ is NULL, the default database, defined in _LL2_DEFAULT_DATABASE_,
+will be taken.
+
+--------------------------------------
+char    *error = NULL;
+const   char *user = "root";
+int64_t res_time;
+char    *res_tty = NULL;
+char    *res_rhost = NULL;
+char    *res_service = NULL;
+
+int ret = ll2_read_entry (NULL, user, &res_time, &res_tty, &res_rhost, &res_service, &error);
+--------------------------------------
+
+== RETURN VALUE
+
+Returns 0 on success, -ENOMEM or -1 on other failure.
+_error_ contains an error string if the return value is -1.
+_error_ is not guaranteed to contain an error string, could also be NULL.
+_error_ should be freed by the caller.
+If lastlog2 database does not exist at all, the errno ENOENT has been set
+and can be checked.
+
+The evaluated values are returned by _ll_time_, _tty_, _rhost_ and _pam_service_.
+
+== AUTHORS
+
+Thorsten Kukuk (kukuk@suse.de)
+
+== SEE ALSO
+
+*lastlog2*(3),
+*ll2_new_context(3),
+*ll2_unref_context(3),
+*ll2_read_all*(3),
+*ll2_write_entry*(3),
+*ll2_update_login_time*(3),
+*ll2_remove_entry*(3),
+*ll2_rename_user*(3),
+*ll2_import_lastlog*(3)
+
+include::man-common/bugreports.adoc[]
+
+include::man-common/footer-lib.adoc[]
+
+ifdef::translation[]
+include::man-common/translation.adoc[]
+endif::[]
diff --git a/liblastlog2/man/ll2_remove_entry.3.adoc b/liblastlog2/man/ll2_remove_entry.3.adoc
new file mode 100644 (file)
index 0000000..f38ab66
--- /dev/null
@@ -0,0 +1,63 @@
+//po4a: entry man manual
+= ll2_remove_entry(3)
+:doctype: manpage
+:man manual: Programmer's Manual
+:man source: util-linux {release-version}
+:page-layout: base
+:lib: liblastlog2
+:firstversion: 2.40
+
+== NAME
+
+ll2_remove_entry - Remove all entries of an user.
+
+== SYNOPSIS
+
+*#include <lastlog2.h>*
+
+*int ll2_remove_entry (struct ll2_context *__context__, const char *__user__,
+                      char **__error__);*
+
+== DESCRIPTION
+
+Removing all database entries, defined in _context_, with the user name _user_.
+If _context_ is NULL, the default database, defined in _LL2_DEFAULT_DATABASE_,
+will be taken.
+
+--------------------------------------
+char    *error = NULL;
+const   char *user = "root";
+
+int ret = ll2_remove_entry (NULL, user, &error);
+--------------------------------------
+
+== RETURN VALUE
+
+Returns 0 on success, -ENOMEM or -1 on other failure.
+_error_ contains an error string if the return value is -1.
+_error_ is not guaranteed to contain an error string, could also be NULL.
+_error_ should be freed by the caller.
+
+== AUTHORS
+
+Thorsten Kukuk (kukuk@suse.de)
+
+== SEE ALSO
+
+*lastlog2*(3),
+*ll2_new_context(3),
+*ll2_unref_context(3),
+*ll2_read_all*(3),
+*ll2_write_entry*(3),
+*ll2_read_entry*(3),
+*ll2_update_login_time*(3),
+*ll2_rename_user*(3),
+*ll2_import_lastlog*(3)
+
+include::man-common/bugreports.adoc[]
+
+include::man-common/footer-lib.adoc[]
+
+ifdef::translation[]
+include::man-common/translation.adoc[]
+endif::[]
diff --git a/liblastlog2/man/ll2_rename_user.3.adoc b/liblastlog2/man/ll2_rename_user.3.adoc
new file mode 100644 (file)
index 0000000..2202fe2
--- /dev/null
@@ -0,0 +1,66 @@
+//po4a: entry man manual
+= ll2_rename_user(3)
+:doctype: manpage
+:man manual: Programmer's Manual
+:man source: util-linux {release-version}
+:page-layout: base
+:lib: liblastlog2
+:firstversion: 2.40
+
+== NAME
+
+ll2_rename_user - Renames an user entry.
+
+== SYNOPSIS
+
+*#include <lastlog2.h>*
+
+*int ll2_rename_user (struct ll2_context *__context__, const char *__user__,
+                     const char *__newname__, char **__error__);*
+
+== DESCRIPTION
+
+Changing user name from _user_ to _newname_ of one entry in
+database, which is defined by _context_. All other entries with the user _user_
+will be deleted.
+If _context_ is NULL, the default database, defined in _LL2_DEFAULT_DATABASE_,
+will be taken.
+
+--------------------------------------
+char    *error = NULL;
+const   char *user = "root";
+const   char *new_user = "notroot";
+
+int ret = ll2_rename_user (NULL, user, new_user, &error);
+--------------------------------------
+
+== RETURN VALUE
+
+Returns 0 on success, -ENOMEM or -1 on other failure.
+_error_ contains an error string if the return value is -1.
+_error_ is not guaranteed to contain an error string, could also be NULL.
+_error_ should be freed by the caller.
+
+== AUTHORS
+
+Thorsten Kukuk (kukuk@suse.de)
+
+== SEE ALSO
+
+*lastlog2*(3),
+*ll2_new_context(3),
+*ll2_unref_context(3),
+*ll2_read_all*(3),
+*ll2_write_entry*(3),
+*ll2_read_entry*(3),
+*ll2_remove_entry*(3),
+*ll2_update_login_time*(3),
+*ll2_import_lastlog*(3)
+
+include::man-common/bugreports.adoc[]
+
+include::man-common/footer-lib.adoc[]
+
+ifdef::translation[]
+include::man-common/translation.adoc[]
+endif::[]
diff --git a/liblastlog2/man/ll2_unref_context.3.adoc b/liblastlog2/man/ll2_unref_context.3.adoc
new file mode 100644 (file)
index 0000000..5e52ca6
--- /dev/null
@@ -0,0 +1,57 @@
+//po4a: entry man manual
+= ll2_unref_context(3)
+:doctype: manpage
+:man manual: Programmer's Manual
+:man source: util-linux {release-version}
+:page-layout: base
+:lib: liblastlog2
+:firstversion: 2.40
+
+== NAME
+
+ll2_unref_context - Freeing lastlog2 context.
+
+== SYNOPSIS
+
+*#include <lastlog2.h>*
+
+*context * ll2_unref_context(ll2_context *context);
+
+== DESCRIPTION
+
+Freeing lastlog2 context, which has been generated by
+_ll2_new_context_.
+
+--------------------------------------
+
+const   char *db_path = "/var/lib/lastlog/lastlog2.db";
+ll2_context *context = ll2_new_context(db_path);
+
+/* other lastlog2 calls..... */
+
+ll2_unref_context(context);
+
+--------------------------------------
+
+== AUTHORS
+
+Thorsten Kukuk (kukuk@suse.de)
+
+== SEE ALSO
+
+*lastlog2*(3),
+*ll2_new_context(3),
+*ll2_read_all*(3),
+*ll2_write_entry*(3),
+*ll2_read_entry*(3),
+*ll2_remove_entry*(3),
+*ll2_update_login_time*(3),
+*ll2_import_lastlog*(3)
+
+include::man-common/bugreports.adoc[]
+
+include::man-common/footer-lib.adoc[]
+
+ifdef::translation[]
+include::man-common/translation.adoc[]
+endif::[]
diff --git a/liblastlog2/man/ll2_update_login_time.3.adoc b/liblastlog2/man/ll2_update_login_time.3.adoc
new file mode 100644 (file)
index 0000000..9cc43e1
--- /dev/null
@@ -0,0 +1,67 @@
+//po4a: entry man manual
+= ll2_update_login_time(3)
+:doctype: manpage
+:man manual: Programmer's Manual
+:man source: util-linux {release-version}
+:page-layout: base
+:lib: liblastlog2
+:firstversion: 2.40
+
+== NAME
+
+ll2_update_login_time - Writes an *new* entry with updated login time.
+
+== SYNOPSIS
+
+*#include <lastlog2.h>*
+
+*int ll2_update_login_time (struct ll2_context *__context__,
+                           const char *__user__, int64_t __ll_time__,
+                           char **__error__);*
+
+== DESCRIPTION
+
+Writes an *new* entry to database, defined in _context_, for user _user_.
+Time is set by _ll_time_ whereas the other values are taken from
+an already existing entry.
+If _context_ is NULL, the default database, defined in _LL2_DEFAULT_DATABASE_,
+will be taken.
+
+--------------------------------------
+char    *error = NULL;
+const   char *user = "root";
+int64_t login_time = time(0); // Get the system time;
+
+int ret = ll2_update_login_time (NULL, user, login_time, &error);
+--------------------------------------
+
+== RETURN VALUE
+
+Returns 0 on success, -ENOMEM or -1 on other failure.
+_error_ contains an error string if the return value is -1.
+_error_ is not guaranteed to contain an error string. It could also be NULL if the return value is -1.
+_error_ should be freed by the caller.
+
+== AUTHORS
+
+Thorsten Kukuk (kukuk@suse.de)
+
+== SEE ALSO
+
+*lastlog2*(3),
+*ll2_new_context(3),
+*ll2_unref_context(3),
+*ll2_read_all*(3),
+*ll2_write_entry*(3),
+*ll2_read_entry*(3),
+*ll2_remove_entry*(3),
+*ll2_rename_user*(3),
+*ll2_import_lastlog*(3)
+
+include::man-common/bugreports.adoc[]
+
+include::man-common/footer-lib.adoc[]
+
+ifdef::translation[]
+include::man-common/translation.adoc[]
+endif::[]
diff --git a/liblastlog2/man/ll2_write_entry.3.adoc b/liblastlog2/man/ll2_write_entry.3.adoc
new file mode 100644 (file)
index 0000000..6dca1b5
--- /dev/null
@@ -0,0 +1,70 @@
+//po4a: entry man manual
+= ll2_write_entry(3)
+:doctype: manpage
+:man manual: Programmer's Manual
+:man source: util-linux {release-version}
+:page-layout: base
+:lib: liblastlog2
+:firstversion: 2.40
+
+== NAME
+
+ll2_write_entry -  Write a new entry into the database.
+
+== SYNOPSIS
+
+*#include <lastlog2.h>*
+
+*int ll2_write_entry (struct ll2_context *__context__, const char *__user__,
+                     int64_t __ll_time__, const char *__tty__,
+                     const char *__rhost__, const char *__pam_service__,
+                     char **__error__);*
+
+== DESCRIPTION
+
+Writes an new entry into database, which is defined in _context_.
+If _context_ is NULL, the default database, defined in _LL2_DEFAULT_DATABASE_,
+will be taken.
+
+
+--------------------------------------
+time_t login_time = time(0); // Get the system time
+char   *error = NULL;
+const  char *user = "root";
+
+int ret = ll2_write_entry (NULL, user, login_time, "pts/0",
+                          "192.168.122.1", NULL, &error);
+--------------------------------------
+
+_pam_service_ is the service or instance name which has generated the entry (optional).
+
+== RETURN VALUE
+
+Returns 0 on success, -ENOMEM or -1 on other failure.
+_error_ contains an error string if the return value is -1.
+_error_ is not guaranteed to contain an error string, could also be NULL.
+_error_ should be freed by the caller.
+
+== AUTHORS
+
+Thorsten Kukuk (kukuk@suse.de)
+
+== SEE ALSO
+
+*lastlog2*(3),
+*ll2_new_context(3),
+*ll2_unref_context(3),
+*ll2_read_all*(3),
+*ll2_read_entry*(3),
+*ll2_update_login_time*(3),
+*ll2_remove_entry*(3),
+*ll2_rename_user*(3),
+*ll2_import_lastlog*(3)
+
+include::man-common/bugreports.adoc[]
+
+include::man-common/footer-lib.adoc[]
+
+ifdef::translation[]
+include::man-common/translation.adoc[]
+endif::[]
diff --git a/liblastlog2/meson.build b/liblastlog2/meson.build
new file mode 100644 (file)
index 0000000..c478939
--- /dev/null
@@ -0,0 +1,63 @@
+cc = meson.get_compiler('c')
+pkg = import('pkgconfig')
+dir_liblastlog2 = include_directories('src')
+lib_lastlog2_sources = '''
+  src/lastlog2.h
+  src/lastlog2P.h
+  src/lastlog2.c
+'''.split()  
+
+libsqlite3 = cc.find_library('sqlite3')
+
+liblastlog2_sym = 'src/liblastlog2.sym'
+liblastlog2_sym_path = '@0@/@1@'.format(meson.current_source_dir(), liblastlog2_sym)
+
+lib_lastlog2 = both_libraries(
+  'lastlog2',
+  lib_lastlog2_sources,
+  include_directories : [dir_include, dir_liblastlog2],
+  link_args : ['-Wl,--version-script=@0@'.format(liblastlog2_sym_path)],
+  link_depends : liblastlog2_sym,
+  dependencies : [libsqlite3],
+  install : build_liblastlog2,
+  version : liblastlog2_version,
+)
+
+lastlog2_dep = declare_dependency(link_with: lib_lastlog2, include_directories: dir_liblastlog2)
+
+lastlog2_tests = [
+  'dlopen',
+  'pam_lastlog2_output',
+  'remove_entry',
+  'rename_user',
+  'write_read_user',
+  'y2038_ll2_read_all',
+  'y2038_sqlite3_time',
+]
+libdl = cc.find_library('dl')
+
+if build_liblastlog2
+  pkg.generate(
+    lib_lastlog2,
+    description : 'library to manage last login data',
+    subdirs : 'lastlog2',
+    version : pc_version
+  )
+  meson.override_dependency('lastlog2', lastlog2_dep)
+
+  foreach lastlog2_test: lastlog2_tests
+    test_name = 'test_lastlog2_' + lastlog2_test
+    exe = executable(
+      test_name,
+      'src/tests/tst_' + lastlog2_test + '.c',
+      include_directories : [dir_include, dir_liblastlog2],
+      link_with : [lib_common, lib_lastlog2],
+      dependencies : libdl
+    )
+    # the test-setup expects the helpers in the toplevel build-directory
+    link = meson.project_build_root() / test_name
+    run_command('ln', '-srf', exe.full_path(), link,
+      check : true)
+  endforeach
+
+endif  
diff --git a/liblastlog2/src/Makemodule.am b/liblastlog2/src/Makemodule.am
new file mode 100644 (file)
index 0000000..712f086
--- /dev/null
@@ -0,0 +1,99 @@
+# includes
+lastlog2incdir = $(includedir)
+lastlog2inc_HEADERS = liblastlog2/src/lastlog2.h
+
+usrlib_exec_LTLIBRARIES += liblastlog2.la
+
+liblastlog2_la_SOURCES = \
+       liblastlog2/src/lastlog2.c \
+       liblastlog2/src/lastlog2P.h
+
+EXTRA_liblastlog2_la_DEPENDENCIES = \
+       liblastlog2/src/liblastlog2.sym
+
+liblastlog2_la_CFLAGS = \
+       $(AM_CFLAGS) \
+        $(SOLIB_CFLAGS) \
+        -I$(ul_liblastlog2_incdir) \
+        -Iliblastlog2/src
+
+liblastlog2_la_LDFLAGS = $(SOLIB_LDFLAGS)
+if HAVE_VSCRIPT
+liblastlog2_la_LDFLAGS += liblastlog2_la_LDFLAGS += $(VSCRIPT_LDFLAGS),$(top_srcdir)/liblastlog2/src/liblastlog2.sym
+endif
+liblastlog2_la_LDFLAGS += -lsqlite3
+liblastlog2_la_LDFLAGS += -version-info $(LIBLASTLOG2_VERSION_INFO)
+
+if BUILD_LIBLASTLOG2_TESTS
+check_PROGRAMS += \
+       test_lastlog2_dlopen \
+       test_lastlog2_pam_lastlog2_output \
+       test_lastlog2_remove_entry \
+       test_lastlog2_rename_user \
+       test_lastlog2_write_read_user \
+       test_lastlog2_y2038_ll2_read_all \
+       test_lastlog2_y2038_sqlite3_time
+
+lastlog2_tests_cflags  = -DTEST_PROGRAM $(liblastlog2_la_CFLAGS)
+lastlog2_tests_ldflags =
+lastlog2_tests_ldadd   = $(LDADD) liblastlog2.la
+lastlog2_tests_ldflags += -static -lsqlite3
+
+test_lastlog2_dlopen_SOURCES = liblastlog2/src/tests/tst_dlopen.c
+test_lastlog2_dlopen_CFLAGS = $(lastlog2_tests_cflags)
+test_lastlog2_dlopen_LDFLAGS = $(lastlog2_tests_ldflags) -ldl
+test_lastlog2_dlopen_LDADD = $(lastlog2_tests_ldadd)
+
+test_lastlog2_pam_lastlog2_output_SOURCES = liblastlog2/src/tests/tst_pam_lastlog2_output.c
+test_lastlog2_pam_lastlog2_output_CFLAGS = $(lastlog2_tests_cflags)
+test_lastlog2_pam_lastlog2_output_LDFLAGS = $(lastlog2_tests_ldflags)
+test_lastlog2_pam_lastlog2_output_LDADD = $(lastlog2_tests_ldadd)
+
+test_lastlog2_remove_entry_SOURCES = liblastlog2/src/tests/tst_remove_entry.c
+test_lastlog2_remove_entry_CFLAGS = $(lastlog2_tests_cflags)
+test_lastlog2_remove_entry_LDFLAGS = $(lastlog2_tests_ldflags)
+test_lastlog2_remove_entry_LDADD = $(lastlog2_tests_ldadd)
+
+test_lastlog2_rename_user_SOURCES = liblastlog2/src/tests/tst_rename_user.c
+test_lastlog2_rename_user_CFLAGS = $(lastlog2_tests_cflags)
+test_lastlog2_rename_user_LDFLAGS = $(lastlog2_tests_ldflags)
+test_lastlog2_rename_user_LDADD = $(lastlog2_tests_ldadd)
+
+test_lastlog2_write_read_user_SOURCES = liblastlog2/src/tests/tst_write_read_user.c
+test_lastlog2_write_read_user_CFLAGS = $(lastlog2_tests_cflags)
+test_lastlog2_write_read_user_LDFLAGS = $(lastlog2_tests_ldflags)
+test_lastlog2_write_read_user_LDADD = $(lastlog2_tests_ldadd)
+
+test_lastlog2_y2038_ll2_read_all_SOURCES = liblastlog2/src/tests/tst_y2038_ll2_read_all.c
+test_lastlog2_y2038_ll2_read_all_CFLAGS = $(lastlog2_tests_cflags)
+test_lastlog2_y2038_ll2_read_all_LDFLAGS = $(lastlog2_tests_ldflags)
+test_lastlog2_y2038_ll2_read_all_LDADD = $(lastlog2_tests_ldadd)
+
+test_lastlog2_y2038_sqlite3_time_SOURCES = liblastlog2/src/tests/tst_y2038_sqlite3_time.c
+test_lastlog2_y2038_sqlite3_time_CFLAGS = $(lastlog2_tests_cflags)
+test_lastlog2_y2038_sqlite3_time_LDFLAGS = $(lastlog2_tests_ldflags)
+test_lastlog2_y2038_sqlite3_time_LDADD = $(lastlog2_tests_ldadd)
+
+endif #BUILD_LIBLIBLASTLOG2_TESTS
+
+
+
+EXTRA_DIST += liblastlog2/src/liblastlog2.sym
+
+# move lib from $(usrlib_execdir) to $(libdir) if needed
+install-exec-hook-liblastlog2:
+       if test "$(usrlib_execdir)" != "$(libdir)" -a -f "$(DESTDIR)$(usrlib_execdir)/liblastlog2.so"; then \
+               $(MKDIR_P) $(DESTDIR)$(libdir); \
+               mv $(DESTDIR)$(usrlib_execdir)/liblastlog2.so.* $(DESTDIR)$(libdir); \
+               so_img_name=$$(readlink $(DESTDIR)$(usrlib_execdir)/liblastlog2.so); \
+               so_img_rel_target=$$(echo $(usrlib_execdir) | sed 's,\(^/\|\)[^/][^/]*,..,g'); \
+               (cd $(DESTDIR)$(usrlib_execdir) && \
+                       rm -f liblastlog2.so && \
+                       $(LN_S) $$so_img_rel_target$(libdir)/$$so_img_name liblastlog2.so); \
+       fi
+
+uninstall-hook-liblastlog2:
+       rm -f $(DESTDIR)$(libdir)/liblastlog2.so*
+
+INSTALL_EXEC_HOOKS += install-exec-hook-liblastlog2
+UNINSTALL_HOOKS += uninstall-hook-liblastlog2
diff --git a/liblastlog2/src/lastlog2.c b/liblastlog2/src/lastlog2.c
new file mode 100644 (file)
index 0000000..827bfe8
--- /dev/null
@@ -0,0 +1,594 @@
+/* SPDX-License-Identifier: BSD-2-Clause
+
+  Copyright (c) 2023, Thorsten Kukuk <kukuk@suse.com>
+
+  Redistribution and use in source and binary forms, with or without
+  modification, are permitted provided that the following conditions are met:
+
+  1. Redistributions of source code must retain the above copyright notice,
+     this list of conditions and the following disclaimer.
+
+  2. Redistributions in binary form must reproduce the above copyright
+     notice, this list of conditions and the following disclaimer in the
+     documentation and/or other materials provided with the distribution.
+
+  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+  POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#include <pwd.h>
+#include <errno.h>
+#include <time.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <limits.h>
+#include <sys/stat.h>
+#include <sqlite3.h>
+#include <lastlog.h>
+
+#include "lastlog2P.h"
+#include "strutils.h"
+
+/* Set the ll2 context/environment */
+/* Returns the context or NULL if an error has happened. */
+extern struct ll2_context * ll2_new_context(const char *db_path)
+{
+       struct ll2_context *context = (struct ll2_context *)malloc(sizeof(struct ll2_context));
+
+       if (context) {
+               if (db_path) {
+                       if ((context->lastlog2_path = strdup(db_path)) == NULL) {
+                               free(context);
+                               context = NULL;
+                       }
+               } else {
+                       if ((context->lastlog2_path = strdup(LL2_DEFAULT_DATABASE)) == NULL) {
+                               free(context);
+                               context = NULL;
+                       }
+               }
+       }
+       return context;
+}
+
+/* Release ll2 context/environment */
+extern void ll2_unref_context(struct ll2_context *context)
+{
+       if (context)
+               free(context->lastlog2_path);
+       free(context);
+}
+
+/* Returns 0 on success, -ENOMEM or -1 on other failure. */
+static int
+open_database_ro(struct ll2_context *context, sqlite3 **db, char **error)
+{
+       int ret = 0;
+       char *path = LL2_DEFAULT_DATABASE;
+
+       if (context && context->lastlog2_path)
+               path = context->lastlog2_path;
+
+       if (sqlite3_open_v2(path, db, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK) {
+               ret = -1;
+               if (error)
+                       if (asprintf(error, "Cannot open database (%s): %s",
+                                    path, sqlite3_errmsg(*db)) < 0)
+                               ret = -ENOMEM;
+
+               sqlite3_close(*db);
+       }
+
+       return ret;
+}
+
+/* Returns 0 on success, -ENOMEM or -1 on other failure. */
+static int
+open_database_rw(struct ll2_context *context,  sqlite3 **db, char **error)
+{
+       int ret = 0;
+       char *path = LL2_DEFAULT_DATABASE;
+
+       if (context && context->lastlog2_path)
+               path = context->lastlog2_path;
+
+       if (sqlite3_open(path, db) != SQLITE_OK) {
+               ret = -1;
+               if (error)
+                       if (asprintf(error, "Cannot create/open database (%s): %s",
+                                    path, sqlite3_errmsg(*db)) < 0)
+                               ret = -ENOMEM;
+
+               sqlite3_close(*db);
+       }
+
+       return ret;
+}
+
+/* Reads one entry from database and returns that.
+   Returns 0 on success, -ENOMEM or -1 on other failure. */
+static int
+read_entry(sqlite3 *db, const char *user,
+          int64_t *ll_time, char **tty, char **rhost,
+          char **pam_service, char **error)
+{
+       int retval = 0;
+       sqlite3_stmt *res = NULL;
+       static const char *sql = "SELECT Name,Time,TTY,RemoteHost,Service FROM Lastlog2 WHERE Name = ?";
+
+       if (sqlite3_prepare_v2(db, sql, -1, &res, 0) != SQLITE_OK) {
+               retval = -1;
+               if (error)
+                       if (asprintf(error, "Failed to execute statement: %s",
+                                    sqlite3_errmsg(db)) < 0)
+                               retval = -ENOMEM;
+               goto out_read_entry;
+       }
+
+       if (sqlite3_bind_text(res, 1, user, -1, SQLITE_STATIC) != SQLITE_OK) {
+               retval = -1;
+               if (error)
+                       if (asprintf(error, "Failed to create search query: %s",
+                                    sqlite3_errmsg(db)) < 0)
+                               retval = -ENOMEM;
+               goto out_read_entry;
+       }
+
+       int step = sqlite3_step(res);
+
+       if (step == SQLITE_ROW) {
+               const unsigned char *luser = sqlite3_column_text(res, 0);
+               const unsigned char *uc;
+
+               if (strcmp((const char *)luser, user) != 0) {
+                       retval = -1;
+                       if (error)
+                               if (asprintf(error, "Returned data is for %s, not %s", luser, user) < 0)
+                               retval = -ENOMEM;
+                       goto out_read_entry;
+               }
+
+               if (ll_time)
+                       *ll_time = sqlite3_column_int64(res, 1);
+
+               if (tty) {
+                       uc = sqlite3_column_text(res, 2);
+                       if (uc != NULL && strlen((const char *)uc) > 0)
+                               if ((*tty = strdup((const char *)uc)) == NULL) {
+                                       retval = -ENOMEM;
+                                       goto out_read_entry;
+                               }
+               }
+               if (rhost) {
+                       uc = sqlite3_column_text(res, 3);
+                       if (uc != NULL && strlen((const char *)uc) > 0)
+                               if ((*rhost = strdup((const char *)uc)) == NULL) {
+                                       retval = -ENOMEM;
+                                       goto out_read_entry;
+                               }
+               }
+               if (pam_service) {
+                       uc = sqlite3_column_text(res, 4);
+                       if (uc != NULL && strlen((const char *)uc) > 0)
+                               if ((*pam_service = strdup((const char *)uc)) == NULL) {
+                                       retval = -ENOMEM;
+                                       goto out_read_entry;
+                               }
+               }
+       } else {
+               retval = -1;
+               if (error)
+                       if (asprintf(error, "User '%s' not found (%d)", user, step) < 0)
+                               retval = -ENOMEM;
+       }
+
+out_read_entry:
+       if (res)
+               sqlite3_finalize(res);
+
+       return retval;
+}
+
+/* reads 1 entry from database and returns that. Returns 0 on success, -ENOMEM or -1 on other failure. */
+int
+ll2_read_entry(struct ll2_context *context, const char *user,
+              int64_t *ll_time, char **tty, char **rhost,
+              char **pam_service, char **error)
+{
+       sqlite3 *db;
+       int retval;
+
+       if ((retval = open_database_ro(context, &db, error)) != 0)
+               return retval;
+
+       retval = read_entry(db, user, ll_time, tty, rhost, pam_service, error);
+
+       sqlite3_close(db);
+
+       return retval;
+}
+
+/* Write a new entry. Returns 0 on success, -ENOMEM or -1 on other failure. */
+static int
+write_entry(sqlite3 *db, const char *user,
+           int64_t ll_time, const char *tty, const char *rhost,
+           const char *pam_service, char **error)
+{
+       int retval = 0;
+       char *err_msg = NULL;
+       sqlite3_stmt *res = NULL;
+       static const char *sql_table = "CREATE TABLE IF NOT EXISTS Lastlog2(Name TEXT PRIMARY KEY, Time INTEGER, TTY TEXT, RemoteHost TEXT, Service TEXT);";
+       static const char *sql_replace = "REPLACE INTO Lastlog2 VALUES(?,?,?,?,?);";
+
+       if (sqlite3_exec(db, sql_table, 0, 0, &err_msg) != SQLITE_OK) {
+               retval = -1;
+               if (error)
+                       if (asprintf(error, "SQL error: %s", err_msg) < 0)
+                               retval = -ENOMEM;
+
+               sqlite3_free(err_msg);
+               goto out_ll2_read_entry;
+       }
+
+       if (sqlite3_prepare_v2(db, sql_replace, -1, &res, 0) != SQLITE_OK) {
+               retval = -1;
+               if (error)
+                       if (asprintf(error, "Failed to execute statement: %s",
+                                    sqlite3_errmsg(db)) < 0)
+                               retval = -ENOMEM;
+               goto out_ll2_read_entry;
+       }
+
+       if (sqlite3_bind_text(res, 1, user, -1, SQLITE_STATIC) != SQLITE_OK) {
+               retval = -1;
+               if (error)
+                       if (asprintf(error, "Failed to create replace statement for user: %s",
+                                    sqlite3_errmsg(db)) < 0)
+                               retval = -ENOMEM;
+               goto out_ll2_read_entry;
+       }
+
+       if (sqlite3_bind_int64(res, 2, ll_time) != SQLITE_OK) {
+               retval = -1;
+               if (error)
+                       if (asprintf(error, "Failed to create replace statement for ll_time: %s",
+                                    sqlite3_errmsg(db)) < 0)
+                               retval = -ENOMEM;
+               goto out_ll2_read_entry;
+       }
+
+       if (sqlite3_bind_text(res, 3, tty, -1, SQLITE_STATIC) != SQLITE_OK) {
+               retval = -1;
+               if (error)
+                       if (asprintf(error, "Failed to create replace statement for tty: %s",
+                                    sqlite3_errmsg(db)) < 0)
+                               retval = -ENOMEM;
+               goto out_ll2_read_entry;
+       }
+
+       if (sqlite3_bind_text(res, 4, rhost, -1, SQLITE_STATIC) != SQLITE_OK) {
+               retval = -1;
+               if (error)
+                       if (asprintf(error, "Failed to create replace statement for rhost: %s",
+                                    sqlite3_errmsg(db)) < 0)
+                               retval = -ENOMEM;
+               goto out_ll2_read_entry;
+       }
+
+       if (sqlite3_bind_text(res, 5, pam_service, -1, SQLITE_STATIC) != SQLITE_OK) {
+               retval = -1;
+               if (error)
+                       if (asprintf(error, "Failed to create replace statement for PAM service: %s",
+                                    sqlite3_errmsg(db)) < 0)
+                               retval = -ENOMEM;
+               goto out_ll2_read_entry;
+       }
+
+       int step = sqlite3_step(res);
+
+       if (step != SQLITE_DONE) {
+               retval = -1;
+               if (error)
+                       if (asprintf(error, "Delete statement did not return SQLITE_DONE: %d",
+                                    step) < 0)
+                               retval = -ENOMEM;
+               goto out_ll2_read_entry;
+       }
+out_ll2_read_entry:
+       if (res)
+               sqlite3_finalize(res);
+
+       return retval;
+}
+
+/* Write a new entry. Returns 0 on success, -ENOMEM or -1 on other failure. */
+int
+ll2_write_entry(struct ll2_context *context, const char *user,
+               int64_t ll_time, const char *tty, const char *rhost,
+               const char *pam_service, char **error)
+{
+       sqlite3 *db;
+       int retval;
+
+       if ((retval = open_database_rw(context, &db, error)) != 0)
+               return retval;
+
+       retval = write_entry(db, user, ll_time, tty, rhost, pam_service, error);
+
+       sqlite3_close(db);
+
+       return retval;
+}
+
+/* Write a new entry with updated login time.
+   Returns 0 on success, -ENOMEM or -1 on other failure. */
+int
+ll2_update_login_time(struct ll2_context *context, const char *user,
+                     int64_t ll_time, char **error)
+{
+       sqlite3 *db;
+       int retval;
+       char *tty;
+       char *rhost;
+       char *pam_service;
+
+       if ((retval = open_database_rw(context , &db, error)) != 0)
+               return retval;
+
+       if ((retval = read_entry(db, user, 0, &tty, &rhost, &pam_service, error)) != 0) {
+               sqlite3_close(db);
+               return retval;
+       }
+
+       retval = write_entry(db, user, ll_time, tty, rhost, pam_service, error);
+
+       sqlite3_close(db);
+
+       free(tty);
+       free(rhost);
+       free(pam_service);
+
+       return retval;
+}
+
+
+typedef int (*callback_f)(const char *user, int64_t ll_time,
+                         const char *tty, const char *rhost,
+                         const char *pam_service, const char *cb_error);
+
+static int
+callback(void *cb_func, __attribute__((unused)) int argc, char **argv, __attribute__((unused)) char **azColName)
+{
+       char *endptr;
+       callback_f print_entry = cb_func;
+
+       errno = 0;
+       char *cb_error = NULL;
+       int64_t ll_time = strtoll(argv[1], &endptr, 10);
+       if ((errno == ERANGE && (ll_time == INT64_MAX || ll_time == INT64_MIN))
+           || (endptr == argv[1]) || (*endptr != '\0'))
+               if (asprintf(&cb_error, "Invalid numeric time entry for '%s': '%s'\n", argv[0], argv[1]) < 0)
+                       return -1;
+
+       print_entry(argv[0], ll_time, argv[2], argv[3], argv[4], cb_error);
+       free(cb_error);
+
+       return 0;
+}
+
+/* Reads all entries from database and calls the callback function for each entry.
+   Returns 0 on success, -ENOMEM or -1 on other failure. */
+int
+ll2_read_all(struct ll2_context *context,
+            int (*cb_func)(const char *user, int64_t ll_time,
+                           const char *tty, const char *rhost,
+                           const char *pam_service, const char *cb_error),
+            char **error)
+{
+       sqlite3 *db;
+       char *err_msg = 0;
+       int retval = 0;
+
+       if ((retval = open_database_ro(context, &db, error)) != 0)
+               return retval;
+
+       static const char *sql = "SELECT Name,Time,TTY,RemoteHost,Service FROM Lastlog2 ORDER BY Name ASC";
+
+       if (sqlite3_exec(db, sql, callback, cb_func, &err_msg) != SQLITE_OK) {
+               retval = -1;
+               if (error)
+                       if (asprintf(error, "SQL error: %s", err_msg) < 0)
+                               retval = -ENOMEM;
+
+               sqlite3_free(err_msg);
+       }
+
+       sqlite3_close(db);
+
+       return retval;
+}
+
+/* Remove an user entry. Returns 0 on success, -ENOMEM or -1 on other failure. */
+static int
+remove_entry(sqlite3 *db, const char *user, char **error)
+{
+       int retval = 0;
+       sqlite3_stmt *res = NULL;
+       static const char *sql = "DELETE FROM Lastlog2 WHERE Name = ?";
+
+       if (sqlite3_prepare_v2(db, sql, -1, &res, 0) != SQLITE_OK) {
+               if (error)
+                       if (asprintf(error, "Failed to execute statement: %s",
+                                    sqlite3_errmsg(db)) < 0)
+                               return -ENOMEM;
+
+               return -1;
+       }
+
+       if (sqlite3_bind_text(res, 1, user, -1, SQLITE_STATIC) != SQLITE_OK) {
+               retval = -1;
+               if (error)
+                       if (asprintf(error, "Failed to create delete statement: %s",
+                                    sqlite3_errmsg(db)) < 0)
+                               retval = -ENOMEM;
+               goto out_remove_entry;
+       }
+
+       int step = sqlite3_step(res);
+
+       if (step != SQLITE_DONE) {
+               retval = -1;
+               if (error)
+                       if (asprintf(error, "Delete statement did not return SQLITE_DONE: %d",
+                                    step) < 0)
+                               retval = -ENOMEM;
+       }
+out_remove_entry:
+       if (res)
+               sqlite3_finalize(res);
+
+       return retval;
+}
+
+/* Remove an user entry. Returns 0 on success, -ENOMEM or -1 on other failure. */
+int
+ll2_remove_entry(struct ll2_context *context, const char *user,
+                char **error)
+{
+       sqlite3 *db;
+       int retval;
+
+       if ((retval = open_database_rw(context, &db, error)) != 0)
+               return retval;
+
+       retval = remove_entry(db, user, error);
+
+       sqlite3_close(db);
+
+       return retval;
+}
+
+/* Renames an user entry. Returns 0 on success, -ENOMEM or -1 on other failure. */
+int
+ll2_rename_user(struct ll2_context *context, const char *user,
+               const char *newname, char **error)
+{
+       sqlite3 *db;
+       int64_t ll_time;
+       char *tty;
+       char *rhost;
+       char *pam_service;
+       int retval;
+
+       if ((retval = open_database_rw(context, &db, error)) != 0)
+               return retval;
+
+       if ((retval = read_entry(db, user, &ll_time, &tty, &rhost, &pam_service, error) != 0)) {
+               sqlite3_close(db);
+               return retval;
+       }
+
+       if ((retval = write_entry(db, newname, ll_time, tty, rhost, pam_service, error) != 0)) {
+               sqlite3_close(db);
+               free(tty);
+               free(rhost);
+               return retval;
+       }
+
+       retval = remove_entry(db, user, error);
+
+       sqlite3_close(db);
+
+       free(tty);
+       free(rhost);
+       free(pam_service);
+
+       return retval;
+}
+
+/* Import old lastlog file.
+   Returns 0 on success, -ENOMEM or -1 on other failure. */
+int
+ll2_import_lastlog(struct ll2_context *context, const char *lastlog_file,
+                  char **error)
+{
+       const struct passwd *pw;
+       struct stat statll;
+       sqlite3 *db;
+       FILE *ll_fp;
+       int retval = 0;
+
+       if ((retval = open_database_rw(context, &db, error)) != 0)
+               return retval;
+
+       ll_fp = fopen(lastlog_file, "r");
+       if (ll_fp == NULL) {
+               if (error)
+                       if (asprintf(error, "Failed to open '%s': %s",
+                                    lastlog_file, strerror(errno)) < 0)
+                               return -ENOMEM;
+
+               return -1;
+       }
+
+
+       if (fstat (fileno(ll_fp), &statll) != 0) {
+               if (error)
+                       if (asprintf(error, "Cannot get size of '%s': %s",
+                                    lastlog_file, strerror(errno)) < 0)
+                               return -ENOMEM;
+
+               return -1;
+       }
+
+       setpwent();
+       while ((pw = getpwent()) != NULL ) {
+               off_t offset;
+               struct lastlog ll;
+
+               offset = (off_t) pw->pw_uid * sizeof (ll);
+
+               if ((offset + (off_t)sizeof(ll)) <= statll.st_size) {
+                       if (fseeko(ll_fp, offset, SEEK_SET) == -1)
+                               continue; /* Ignore seek error */
+
+                       if (fread(&ll, sizeof(ll), 1, ll_fp) != 1) {
+                               retval = -1;
+                               if (error)
+                                       if (asprintf(error, "Failed to get the entry for UID '%lu'",
+                                                    (unsigned long int)pw->pw_uid) < 0)
+                                               retval = -ENOMEM;
+                               goto out_import_lastlog;
+                       }
+
+                       if (ll.ll_time != 0) {
+                               int64_t ll_time;
+                               char tty[sizeof(ll.ll_line) + 1];
+                               char rhost[sizeof(ll.ll_host) + 1];
+
+                               ll_time = ll.ll_time;
+                               mem2strcpy(tty, ll.ll_line, sizeof(ll.ll_line), sizeof(tty));
+                               mem2strcpy(rhost, ll.ll_host, sizeof(ll.ll_host), sizeof(rhost));
+
+                               if ((retval = write_entry(db, pw->pw_name, ll_time, tty,
+                                                         rhost, NULL, error)) != 0)
+                                       goto out_import_lastlog;
+                       }
+               }
+       }
+out_import_lastlog:
+       endpwent();
+       sqlite3_close(db);
+
+       return retval;
+}
diff --git a/liblastlog2/src/lastlog2.h b/liblastlog2/src/lastlog2.h
new file mode 100644 (file)
index 0000000..28ed096
--- /dev/null
@@ -0,0 +1,91 @@
+/* SPDX-License-Identifier: BSD-2-Clause
+
+  Copyright (c) 2023, Thorsten Kukuk <kukuk@suse.com>
+
+  Redistribution and use in source and binary forms, with or without
+  modification, are permitted provided that the following conditions are met:
+
+  1. Redistributions of source code must retain the above copyright notice,
+     this list of conditions and the following disclaimer.
+
+  2. Redistributions in binary form must reproduce the above copyright
+     notice, this list of conditions and the following disclaimer in the
+     documentation and/or other materials provided with the distribution.
+
+  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+  POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#ifndef _LIBLASTLOG2_H
+#define _LIBLASTLOG2_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define LL2_DEFAULT_DATABASE "/var/lib/lastlog/lastlog2.db"
+
+#include <stdint.h>
+
+struct ll2_context;
+
+/* Set the ll2 context/environment */
+/* Returns the context or NULL if an error has happened. */
+extern struct ll2_context * ll2_new_context(const char *db_path);
+
+/* Release ll2 context/environment */
+extern void ll2_unref_context(struct ll2_context *context);
+
+/* Writes a new entry. Returns 0 on success, -ENOMEM or -1 on other failure. */
+extern int ll2_write_entry (struct ll2_context *context, const char *user,
+                           int64_t ll_time, const char *tty,
+                           const char *rhost, const char *pam_service,
+                           char **error);
+
+/* Calling a defined function for each entry. Returns 0 on success, -ENOMEM or -1 on other failure. */
+extern int ll2_read_all (struct ll2_context *context,
+                        int (*callback)(const char *user, int64_t ll_time,
+                                        const char *tty, const char *rhost,
+                                        const char *pam_service, const char *cb_error),
+                        char **error);
+
+/* Reads one entry from database and returns that.
+   Returns 0 on success, -ENOMEM or -1 on other failure. */
+extern int ll2_read_entry (struct ll2_context *context, const char *user,
+                          int64_t *ll_time, char **tty, char **rhost,
+                          char **pam_service, char **error);
+
+/* Write a new entry with updated login time.
+   Returns 0 on success, -ENOMEM or -1 on other failure. */
+extern int ll2_update_login_time (struct ll2_context *context,
+                                 const char *user, int64_t ll_time,
+                                 char **error);
+
+/* Remove an user entry. Returns 0 on success, -ENOMEM or -1 on other failure. */
+extern int ll2_remove_entry (struct ll2_context *context, const char *user,
+                            char **error);
+
+/* Renames an user entry. Returns 0 on success, -ENOMEM or -1 on other failure. */
+extern int ll2_rename_user (struct ll2_context *context, const char *user,
+                           const char *newname, char **error);
+
+
+/* Import old lastlog file.
+   Returns 0 on success, -ENOMEM or -1 on other failure. */
+extern int ll2_import_lastlog (struct ll2_context *context,
+                              const char *lastlog_file, char **error);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* _LIBLASTLOG2_H */
diff --git a/liblastlog2/src/lastlog2P.h b/liblastlog2/src/lastlog2P.h
new file mode 100644 (file)
index 0000000..b39f660
--- /dev/null
@@ -0,0 +1,37 @@
+/* SPDX-License-Identifier: BSD-2-Clause
+
+  Copyright (c) 2024, Thorsten Kukuk <kukuk@suse.com>
+
+  Redistribution and use in source and binary forms, with or without
+  modification, are permitted provided that the following conditions are met:
+
+  1. Redistributions of source code must retain the above copyright notice,
+     this list of conditions and the following disclaimer.
+
+  2. Redistributions in binary form must reproduce the above copyright
+     notice, this list of conditions and the following disclaimer in the
+     documentation and/or other materials provided with the distribution.
+
+  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+  POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#ifndef _LIBLASTLOG2_P_H
+#define _LIBLASTLOG2_P_H
+
+#include "lastlog2.h"
+
+struct ll2_context {
+    char *lastlog2_path;
+};
+
+#endif /* _LIBLASTLOG2_P_H */
diff --git a/liblastlog2/src/liblastlog2.sym b/liblastlog2/src/liblastlog2.sym
new file mode 100644 (file)
index 0000000..afaf3cf
--- /dev/null
@@ -0,0 +1,13 @@
+LIBLASTLOG2_1.0 {
+  global:
+        ll2_new_context;
+        ll2_unref_context;
+        ll2_read_all;
+        ll2_read_entry;
+        ll2_remove_entry;
+        ll2_rename_user;
+        ll2_write_entry;
+        ll2_update_login_time;
+        ll2_import_lastlog;
+  local: *;
+};
diff --git a/liblastlog2/src/tests/tst_dlopen.c b/liblastlog2/src/tests/tst_dlopen.c
new file mode 100644 (file)
index 0000000..112564b
--- /dev/null
@@ -0,0 +1,40 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later
+
+   Copyright (C) Nalin Dahyabhai <nalin@redhat.com> 2003
+
+   This program is free software; you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 2 of the License, or
+   (at your option) any later version.
+*/
+
+#include <dlfcn.h>
+#include <stdio.h>
+#include <limits.h>
+#include <sys/stat.h>
+
+/* Simple program to see if dlopen() would succeed. */
+int main(int argc, char **argv)
+{
+       int i;
+       struct stat st;
+       char buf[PATH_MAX];
+
+       for (i = 1; i < argc; i++) {
+               if (dlopen(argv[i], RTLD_NOW)) {
+                       fprintf(stdout, "dlopen() of \"%s\" succeeded.\n",
+                               argv[i]);
+               } else {
+                       snprintf(buf, sizeof(buf), "./%s", argv[i]);
+                       if ((stat(buf, &st) == 0) && dlopen(buf, RTLD_NOW)) {
+                               fprintf(stdout, "dlopen() of \"./%s\" "
+                                       "succeeded.\n", argv[i]);
+                       } else {
+                               fprintf(stdout, "dlopen() of \"%s\" failed: "
+                                       "%s\n", argv[i], dlerror());
+                               return 1;
+                       }
+               }
+       }
+       return 0;
+}
diff --git a/liblastlog2/src/tests/tst_pam_lastlog2_output.c b/liblastlog2/src/tests/tst_pam_lastlog2_output.c
new file mode 100644 (file)
index 0000000..1fd1eec
--- /dev/null
@@ -0,0 +1,115 @@
+/* SPDX-License-Identifier: BSD-2-Clause
+
+  Copyright (c) 2023, Thorsten Kukuk <kukuk@suse.com>
+
+  Redistribution and use in source and binary forms, with or without
+  modification, are permitted provided that the following conditions are met:
+
+  1. Redistributions of source code must retain the above copyright notice,
+     this list of conditions and the following disclaimer.
+
+  2. Redistributions in binary form must reproduce the above copyright
+     notice, this list of conditions and the following disclaimer in the
+     documentation and/or other materials provided with the distribution.
+
+  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+  POSSIBILITY OF SUCH DAMAGE.
+*/
+
+/* Test case:
+   Store defined data into the database, read it, create the time
+   string like pam_lastlog2 does, compare the result.
+*/
+
+#include <time.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+
+#include "lastlog2P.h"
+
+const char *expected = "Last login: Mon Mar 13 07:13:41 UTC 2023 from 192.168.122.1 on pts/0";
+const time_t login_time = 1678691621;
+int
+main(void)
+{
+       const char *user = "root";
+       struct ll2_context *context = ll2_new_context("pam_lastlog2-output.db");
+       int64_t ll_time = 0;
+       char *tty = NULL;
+       char *rhost = NULL;
+       char *date = NULL;
+       char the_time[256];
+       char *error = NULL;
+       char *output = NULL;
+
+       if (ll2_write_entry (context, user, login_time, "pts/0",
+                      "192.168.122.1", NULL, &error) != 0) {
+               if (error) {
+                       fprintf (stderr, "%s\n", error);
+                       free (error);
+               } else
+                       fprintf (stderr, "ll2_write_entry failed\n");
+               ll2_unref_context(context);
+               return 1;
+       }
+
+       if (ll2_read_entry (context, user, &ll_time, &tty, &rhost,
+                           NULL, &error) != 0) {
+               if (error) {
+                       fprintf (stderr, "%s\n", error);
+                       free (error);
+               } else
+                       fprintf (stderr, "Unknown error reading database %s", context ? context->lastlog2_path : "NULL");
+               ll2_unref_context(context);
+               return 1;
+       }
+
+       if (ll_time) {
+               struct tm *tm, tm_buf;
+               /* this is necessary if you compile this on architectures with
+                  a 32bit time_t type. */
+               time_t t_time = ll_time;
+
+               if ((tm = localtime_r (&t_time, &tm_buf)) != NULL) {
+                       strftime (the_time, sizeof (the_time),
+                                 " %a %b %e %H:%M:%S %Z %Y", tm);
+                       date = the_time;
+               }
+       }
+
+       if (asprintf (&output, "Last login:%s%s%s%s%s",
+                     date ? date : "",
+                     rhost ? " from " : "",
+                     rhost ? rhost : "",
+                     tty ? " on " : "",
+                     tty ? tty : "") < 0) {
+               fprintf (stderr, "Out of memory!\n");
+               ll2_unref_context(context);
+               return 1;
+       }
+
+       if (strcmp (output, expected) != 0) {
+               fprintf (stderr, "Output '%s'\n does not match '%s'\n",
+                        output, expected);
+               ll2_unref_context(context);
+               return 1;
+       }
+
+       ll2_unref_context(context);
+       free (output);
+       free (tty);
+       free (rhost);
+
+       return 0;
+}
diff --git a/liblastlog2/src/tests/tst_remove_entry.c b/liblastlog2/src/tests/tst_remove_entry.c
new file mode 100644 (file)
index 0000000..39079c8
--- /dev/null
@@ -0,0 +1,88 @@
+/* SPDX-License-Identifier: BSD-2-Clause
+
+  Copyright (c) 2023, Thorsten Kukuk <kukuk@suse.com>
+
+  Redistribution and use in source and binary forms, with or without
+  modification, are permitted provided that the following conditions are met:
+
+  1. Redistributions of source code must retain the above copyright notice,
+     this list of conditions and the following disclaimer.
+
+  2. Redistributions in binary form must reproduce the above copyright
+     notice, this list of conditions and the following disclaimer in the
+     documentation and/or other materials provided with the distribution.
+
+  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+  POSSIBILITY OF SUCH DAMAGE.
+*/
+
+/* Test case:
+   Create an entry, delete that entry, and try to read entry again.
+   Reading the entry should fail.
+*/
+
+#include <time.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "lastlog2.h"
+
+int
+main(void)
+{
+       const char *user = "user";
+       struct ll2_context *context = ll2_new_context("tst-delete-user.db");
+       int64_t ll_time = 0;
+       char *tty = NULL;
+       char *rhost = NULL;
+       char *service = NULL;
+       char *error = NULL;
+
+       if (ll2_write_entry (context, user, time (NULL), "test-tty",
+                            "localhost", "sshd", &error) != 0) {
+               if (error) {
+                       fprintf (stderr, "%s\n", error);
+                       free (error);
+               } else
+                       fprintf (stderr, "ll2_write_entry failed\n");
+               ll2_unref_context(context);
+               return 1;
+       }
+
+       if (ll2_remove_entry (context, user, &error) != 0) {
+               if (error) {
+                       fprintf (stderr, "%s\n", error);
+                       free (error);
+               } else
+                       fprintf (stderr, "ll2_remove_entry failed\n");
+               ll2_unref_context(context);
+               return 1;
+       }
+
+       /* this needs to fail, as the old entry shouldn't exist anymore. */
+       if (ll2_read_entry (context, user, &ll_time, &tty, &rhost, &service, &error) == 0) {
+               fprintf (stderr, "Reading old user from database did not fail!\n");
+               fprintf (stderr, "ll_time=%lld, tty='%s', rhost='%s', service='%s'\n",
+                        (long long int)ll_time, tty, rhost, service);
+               ll2_unref_context(context);
+               return 1;
+       }
+
+       ll2_unref_context(context);
+       free (error);
+       free (tty);
+       free (rhost);
+       free (service);
+
+       return 0;
+}
diff --git a/liblastlog2/src/tests/tst_rename_user.c b/liblastlog2/src/tests/tst_rename_user.c
new file mode 100644 (file)
index 0000000..a1788b5
--- /dev/null
@@ -0,0 +1,112 @@
+/* SPDX-License-Identifier: BSD-2-Clause
+
+  Copyright (c) 2023, Thorsten Kukuk <kukuk@suse.com>
+
+  Redistribution and use in source and binary forms, with or without
+  modification, are permitted provided that the following conditions are met:
+
+  1. Redistributions of source code must retain the above copyright notice,
+     this list of conditions and the following disclaimer.
+
+  2. Redistributions in binary form must reproduce the above copyright
+     notice, this list of conditions and the following disclaimer in the
+     documentation and/or other materials provided with the distribution.
+
+  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+  POSSIBILITY OF SUCH DAMAGE.
+*/
+
+/* Test case:
+   Create an entry, rename that entry, and try to read the old and
+   new entry again. Reading the old entry should fail.
+*/
+
+#include <time.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "lastlog2P.h"
+
+int
+main(void)
+{
+       const char *user = "user";
+       const char *newname = "new";
+       struct ll2_context *context = ll2_new_context("tst-rename-user.db");
+       int64_t ll_time = 0;
+       char *tty = NULL;
+       char *rhost = NULL;
+       char *service = NULL;
+       char *error = NULL;
+
+       if (ll2_write_entry (context, user, time (NULL), "test-tty",
+                            "localhost", "test-service", &error) != 0) {
+               if (error) {
+                       fprintf (stderr, "%s\n", error);
+                       free (error);
+               }
+               else
+                       fprintf (stderr, "ll2_write_entry failed\n");
+               ll2_unref_context(context);
+               return 1;
+       }
+
+       if (ll2_rename_user (context, user, newname, &error) != 0) {
+               if (error) {
+                       fprintf (stderr, "%s\n", error);
+                       free (error);
+               }
+               else
+                       fprintf (stderr, "ll2_rename_user failed\n");
+               ll2_unref_context(context);
+               return 1;
+       }
+
+       /* this needs to fail, as the old entry shouldn't exist anymore. */
+       if (ll2_read_entry (context, user, &ll_time, &tty, &rhost,
+                           &service, &error) == 0) {
+               fprintf (stderr, "Reading old user from database did not fail!\n");
+               fprintf (stderr, "ll_time=%lld, tty='%s', rhost='%s', service='%s'\n",
+                        (long long int)ll_time, tty, rhost, service);
+               ll2_unref_context(context);
+               return 1;
+       }
+
+       if (error) {
+               free (error);
+               error = NULL;
+       }
+
+       if (ll2_read_entry (context, newname, &ll_time, &tty, &rhost, &service,
+                           &error) != 0) {
+               if (error) {
+                       fprintf (stderr, "%s\n", error);
+                       free (error);
+               } else
+                       fprintf (stderr, "Unknown error reading database %s", context->lastlog2_path);
+               ll2_unref_context(context);
+               return 1;
+       }
+
+       if (strcmp (tty, "test-tty") != 0 || strcmp (rhost, "localhost") != 0 ||
+           strcmp (service, "test-service") != 0) {
+               fprintf (stderr, "New entry data does not match old entry data!\n");
+       }
+
+       ll2_unref_context(context);
+       free (tty);
+       free (rhost);
+       free (service);
+
+       return 0;
+}
diff --git a/liblastlog2/src/tests/tst_write_read_user.c b/liblastlog2/src/tests/tst_write_read_user.c
new file mode 100644 (file)
index 0000000..dbf1db7
--- /dev/null
@@ -0,0 +1,165 @@
+/* SPDX-License-Identifier: BSD-2-Clause
+
+  Copyright (c) 2023, Thorsten Kukuk <kukuk@suse.com>
+
+  Redistribution and use in source and binary forms, with or without
+  modification, are permitted provided that the following conditions are met:
+
+  1. Redistributions of source code must retain the above copyright notice,
+     this list of conditions and the following disclaimer.
+
+  2. Redistributions in binary form must reproduce the above copyright
+     notice, this list of conditions and the following disclaimer in the
+     documentation and/or other materials provided with the distribution.
+
+  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+  POSSIBILITY OF SUCH DAMAGE.
+*/
+
+/* Test case:
+   Create an entry, rename that entry, and try to read the old and
+   new entry again. Reading the old entry should fail.
+*/
+
+#include <time.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+
+#include "lastlog2P.h"
+
+static int
+test_args (struct ll2_context *context, const char *user, int64_t ll_time,
+          const char *tty, const char *rhost, const char *service)
+{
+       char *error = NULL;
+       int64_t res_time;
+       char *res_tty = NULL;
+       char *res_rhost = NULL;
+       char *res_service = NULL;
+
+       if (ll2_write_entry (context, user, ll_time, tty, rhost, service, &error) != 0) {
+               if (error) {
+                       fprintf (stderr, "%s\n", error);
+                       free (error);
+               } else
+                       fprintf (stderr, "ll2_write_entry failed\n");
+               return 1;
+       }
+
+       if (ll2_read_entry (context, user, &res_time, &res_tty, &res_rhost, &res_service, &error) != 0) {
+               if (error) {
+                       fprintf (stderr, "%s\n", error);
+                       free (error);
+               } else
+                       fprintf (stderr, "Unknown error reading database %s", context->lastlog2_path);
+               return 1;
+       }
+
+       if (ll_time != res_time) {
+               fprintf (stderr, "Wrong time: got %lld, expect %lld\n",
+                        (long long int)res_time, (long long int)ll_time);
+               return 1;
+       }
+
+       if ((tty == NULL && res_tty != NULL) ||
+           (tty != NULL && res_tty == NULL) ||
+           (tty != NULL && res_tty != NULL && strcmp (tty, res_tty) != 0)) {
+               fprintf (stderr, "Wrong tty: got %s, expect %s\n", tty, res_tty);
+               return 1;
+       }
+
+       if ((rhost == NULL && res_rhost != NULL) ||
+           (rhost != NULL && res_rhost == NULL) ||
+           (rhost != NULL && res_rhost != NULL && strcmp (rhost, res_rhost) != 0)) {
+               fprintf (stderr, "Wrong rhost: got %s, expect %s\n", rhost, res_rhost);
+               return 1;
+       }
+
+       if ((service == NULL && res_service != NULL) ||
+           (service != NULL && res_service == NULL) ||
+           (service != NULL && res_service != NULL && strcmp (service, res_service) != 0)) {
+               fprintf (stderr, "Wrong service: got %s, expect %s\n", service, res_service);
+               return 1;
+       }
+
+
+       free (res_tty);
+       free (res_rhost);
+       free (res_service);
+
+       return 0;
+}
+
+int
+main(void)
+{
+       struct ll2_context *context = ll2_new_context("tst-write-read-user.db");
+       char *error = NULL;
+       int64_t res_time;
+       char *res_tty = NULL;
+       char *res_rhost = NULL;
+       char *res_service = NULL;
+
+       if (test_args (context, "user1", time (NULL), "test-tty", "localhost", "test") != 0) {
+               ll2_unref_context(context);
+               return 1;
+       }
+       if (test_args (context, "user2", 0, NULL, NULL, NULL) != 0) {
+               ll2_unref_context(context);
+               return 1;
+       }
+       if (test_args (context, "user3", time (NULL), NULL, NULL, NULL) != 0) {
+               ll2_unref_context(context);
+               return 1;
+       }
+       if (test_args (context, "user4", time (NULL), "test-tty", NULL, NULL) != 0) {
+               ll2_unref_context(context);
+               return 1;
+       }
+       if (test_args (context, "user5", time (NULL), NULL, "localhost", NULL) != 0) {
+               ll2_unref_context(context);
+               return 1;
+       }
+
+       /* Checking errno if the db file does not exist */
+       struct ll2_context *context_not_found = ll2_new_context("no_file");
+       if (ll2_read_entry (context_not_found, "user", &res_time, &res_tty, &res_rhost, &res_service, &error) != 0) {
+               if (error) {
+                       fprintf (stderr, "%s\n", error);
+                       free (error);
+               } else
+                       fprintf (stderr, "Couldn't read entries for all users\n");
+
+               if(errno) {
+                       if (errno == ENOENT)
+                       {
+                               fprintf (stderr, "Returning the correct errno: %s\n",
+                                        strerror (errno));
+                               ll2_unref_context(context_not_found);
+                               return 0;
+                       }
+                       fprintf (stderr, "errno: %s\n",
+                                strerror (errno));
+               } else {
+                       fprintf (stderr, "errno: NULL\n");
+               }
+
+               ll2_unref_context(context_not_found);
+               ll2_unref_context(context);
+               return 1;
+       }
+
+       ll2_unref_context(context);
+       return 0;
+}
diff --git a/liblastlog2/src/tests/tst_y2038_ll2_read_all.c b/liblastlog2/src/tests/tst_y2038_ll2_read_all.c
new file mode 100644 (file)
index 0000000..014ae9d
--- /dev/null
@@ -0,0 +1,156 @@
+/* SPDX-License-Identifier: BSD-2-Clause
+
+  Copyright (c) 2023, Thorsten Kukuk <kukuk@suse.com>
+
+  Redistribution and use in source and binary forms, with or without
+  modification, are permitted provided that the following conditions are met:
+
+  1. Redistributions of source code must retain the above copyright notice,
+     this list of conditions and the following disclaimer.
+
+  2. Redistributions in binary form must reproduce the above copyright
+     notice, this list of conditions and the following disclaimer in the
+     documentation and/or other materials provided with the distribution.
+
+  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+  POSSIBILITY OF SUCH DAMAGE.
+*/
+
+/* Test case:
+   Create an entry with an 3*INT32_MAX timestamp, store that,
+   read that via ll2_read_all callback again and make sure the
+   timestamp is correct.
+*/
+
+#include <time.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+#include <errno.h>
+
+#include "lastlog2P.h"
+
+#define BIG_TIME_VALUE ((int64_t)3*INT32_MAX)
+
+const char *user = "y2038";
+const char *on_tty = "pts/test";
+const char *rhost = NULL;
+const char *service = "sshd";
+
+static int
+check_y2038 (const char *res_user, int64_t ll_time, const char *res_tty,
+            const char *res_rhost, const char *res_service, const char *error)
+{
+
+       if (strcmp (user, res_user) != 0) {
+               fprintf (stderr, "write/read entry user mismatch: written: %s, got: %s\n",
+                        user, res_user);
+               exit (1);
+       }
+
+       if (ll_time != BIG_TIME_VALUE) {
+               fprintf (stderr, "write/read entry time mismatch: written: %lld, got: %lld\n",
+                        (long long int)BIG_TIME_VALUE, (long long int)ll_time);
+               exit (1);
+       }
+
+       if (strcmp (on_tty, res_tty) != 0) {
+               fprintf (stderr, "write/read entry tty mismatch: written: %s, got: %s\n",
+                        on_tty, res_tty);
+               exit (1);
+       }
+
+       if (rhost != NULL) {
+               fprintf (stderr, "write/read entry rhost mismatch: written: NULL, got: %s\n",
+                        res_rhost);
+               exit (1);
+       }
+
+       if (strcmp (service, res_service) != 0) {
+               fprintf (stderr, "write/read entry service mismatch: written: %s, got: %s\n",
+                        service, res_service);
+               exit (1);
+       }
+
+       if (error != NULL) {
+               fprintf (stderr, "got error: %s\n",
+                        error);
+               exit (1);
+       }
+
+       return 0;
+}
+
+int
+main(void)
+{
+       struct ll2_context *context = ll2_new_context("y2038-ll2_read_all.db");
+       char *error = NULL;
+
+       remove (context->lastlog2_path);
+
+       printf ("Big time value is: %lld\n", (long long int)BIG_TIME_VALUE);
+
+       if (ll2_write_entry (context, user, BIG_TIME_VALUE, on_tty, rhost, service,
+                            &error) != 0) {
+               if (error) {
+                       fprintf (stderr, "%s\n", error);
+                       free (error);
+               }
+               else
+                       fprintf (stderr, "ll2_write_entry failed\n");
+               ll2_unref_context(context);
+               return 1;
+       }
+
+       if (ll2_read_all (context, check_y2038, &error) != 0) {
+               if (error) {
+                       fprintf (stderr, "%s\n", error);
+                       free (error);
+               } else
+                       fprintf (stderr, "Couldn't read entries for all users\n");
+               ll2_unref_context(context);
+               return 1;
+       }
+
+       /* Checking errno if the db file does not exist */
+       remove (context->lastlog2_path);
+
+       if (ll2_read_all (context, check_y2038, &error) != 0) {
+               if (error) {
+                       fprintf (stderr, "%s\n", error);
+                       free (error);
+               } else
+                       fprintf (stderr, "Couldn't read entries for all users\n");
+
+               if(errno) {
+                       if (errno == ENOENT)
+                       {
+                               fprintf (stderr, "Returning the correct errno: %s\n",
+                                        strerror (errno));
+                               ll2_unref_context(context);
+                               return 0;
+                       }
+                       fprintf (stderr, "errno: %s\n",
+                                strerror (errno));
+               } else {
+                       fprintf (stderr, "errno: NULL\n");
+               }
+
+               ll2_unref_context(context);
+               return 1;
+       }
+
+       ll2_unref_context(context);
+       return 0;
+}
diff --git a/liblastlog2/src/tests/tst_y2038_sqlite3_time.c b/liblastlog2/src/tests/tst_y2038_sqlite3_time.c
new file mode 100644 (file)
index 0000000..a296634
--- /dev/null
@@ -0,0 +1,83 @@
+/* SPDX-License-Identifier: BSD-2-Clause
+
+  Copyright (c) 2023, Thorsten Kukuk <kukuk@suse.com>
+
+  Redistribution and use in source and binary forms, with or without
+  modification, are permitted provided that the following conditions are met:
+
+  1. Redistributions of source code must retain the above copyright notice,
+     this list of conditions and the following disclaimer.
+
+  2. Redistributions in binary form must reproduce the above copyright
+     notice, this list of conditions and the following disclaimer in the
+     documentation and/or other materials provided with the distribution.
+
+  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+  POSSIBILITY OF SUCH DAMAGE.
+*/
+
+/* Test case:
+   Create an entry with an INT64_MAX-1000 timestamp, store that,
+   read that again and make sure the timestamp is correct.
+*/
+
+#include <time.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+
+#include "lastlog2P.h"
+
+#define BIG_TIME_VALUE (INT64_MAX - 1000)
+
+int
+main(void)
+{
+       const char *user = "y2038";
+       struct ll2_context *context = ll2_new_context("y2038-sqlite3-time.db");
+       int64_t ll_time = 0;
+       char *error = NULL;
+
+       printf ("Big time value is: %lld\n", (long long int)BIG_TIME_VALUE);
+
+       if (ll2_write_entry (context, user, BIG_TIME_VALUE, NULL, NULL,
+                            NULL, &error) != 0) {
+               if (error) {
+                       fprintf (stderr, "%s\n", error);
+                       free (error);
+               } else
+                       fprintf (stderr, "ll2_write_entry failed\n");
+               ll2_unref_context(context);
+               return 1;
+       }
+
+       if (ll2_read_entry (context, user, &ll_time, NULL, NULL, NULL,
+                           &error) != 0) {
+               if (error) {
+                       fprintf (stderr, "%s\n", error);
+                       free (error);
+               } else
+                       fprintf (stderr, "Unknown error reading database %s", context->lastlog2_path);
+               ll2_unref_context(context);
+               return 1;
+       }
+
+       if (ll_time != BIG_TIME_VALUE) {
+               fprintf (stderr, "write/read entry time mismatch: written: %lld, got: %lld\n",
+                        (long long int)BIG_TIME_VALUE, (long long int)ll_time);
+               ll2_unref_context(context);
+               return 1;
+       }
+
+       ll2_unref_context(context);
+       return 0;
+}
index 5007ceb2abeed61f23578bf43dd15ee4bd1158ab..a68a422ebc4803073edab55ada34be028c216e4d 100644 (file)
@@ -13,6 +13,7 @@ pkgconfig = import('pkgconfig')
 libblkid_version = '1.1.0'
 libblkid_date = '01-Jun-2021'
 libuuid_version = '1.3.0'
+liblastlog2_version = '1.3.0'
 libmount_version = '1.1.0'
 libsmartcols_version = '1.1.0'
 libfdisk_version = '1.1.0'
@@ -79,6 +80,10 @@ build_libuuid = not get_option('build-libuuid').disabled()
 conf.set('HAVE_LIBUUID', build_libuuid ? 1 : false)
 summary('libuuid', build_libuuid ? 'enabled' : 'disabled', section : 'components')
 
+build_liblastlog2 = not get_option('build-liblastlog2').disabled()
+conf.set('HAVE_LIBLASTLOG2', build_liblastlog2 ? 1 : false)
+summary('liblastlog2', build_liblastlog2 ? 'enabled' : 'disabled', section : 'components')
+
 have_mountfd_api = cc.sizeof('struct mount_attr', prefix : '#include <linux/mount.h>') > 0
 conf.set('HAVE_STRUCT_MOUNT_ATTR', have_mountfd_api ? 1 : false)
 conf.set('HAVE_MOUNTFD_API', have_mountfd_api ? 1 : false)
@@ -926,7 +931,9 @@ subdir('libblkid')
 subdir('libmount')
 subdir('libsmartcols')
 subdir('libuuid')
+subdir('liblastlog2')
 subdir('libfdisk')
+subdir('pam_lastlog2')
 subdir('login-utils')
 subdir('sys-utils')
 subdir('disk-utils')
@@ -941,6 +948,7 @@ includes = [dir_include,
             dir_libmount,
             dir_libfdisk,
             dir_libuuid,
+            dir_liblastlog2,
             dir_sys_utils]
 
 exes = []
@@ -2595,6 +2603,28 @@ exes += exe
 manadocs += ['misc-utils/mcookie.1.adoc']
 bashcompletions += ['mcookie']
 
+if build_liblastlog2
+  exe = executable(
+    'lastlog2',
+    lastlog2_sources,
+    include_directories : includes,
+    link_with : [lib_common, lib_lastlog2],
+    install_dir : usrbin_exec_dir,
+    install : true)
+  exes += exe
+  manadocs += ['misc-utils/lastlog2.8.adoc']
+  bashcompletions += ['lastlog2']
+  manadocs += ['liblastlog2/man/lastlog2.3.adoc',
+               'liblastlog2/man/ll2_write_entry.3.adoc',
+              'liblastlog2/man/ll2_read_entry.3.adoc',
+              'liblastlog2/man/ll2_import_lastlog.3.adoc',
+              'liblastlog2/man/ll2_read_all.3.adoc',
+              'liblastlog2/man/ll2_remove_entry.3.adoc',
+              'liblastlog2/man/ll2_rename_user.3.adoc',
+              'liblastlog2/man/ll2_update_login_time.3.adoc'
+              ]
+endif
+
 exe = executable(
   'namei',
   namei_sources,
index 5555f50d4b0a294f91ed297be651d7bc2aa4a5b8..b3cdc2dd3e9ba31bc2f49470163476184b5c1b4e 100644 (file)
@@ -37,6 +37,8 @@ option('build-libblkid', type : 'feature',
        description : 'build libblkid and many related utilities')
 option('build-libuuid', type : 'feature',
        description : 'build libuuid and uuid utilities')
+option('build-liblastlog2', type : 'feature',
+       description : 'build liblastlog2 and lastlog2 utilities')
 option('build-libmount', type : 'feature',
        description : 'build libmount')
 option('build-libsmartcols', type : 'feature',
@@ -214,3 +216,9 @@ option('fs-search-path-extra',
 option('vendordir',
        type: 'string',
        description : 'directory for distribution provided econf files')
+
+option('pamlibdir', type : 'string',
+       description : 'directory for PAM modules')
+option('lastlog-compat-symlink', type : 'boolean',
+       value : 'false',
+       description : 'create lastlog compat symlink')
index c0287917d918f0399a25ac39c4af54a4e7013d76..24b40acafa9056d9c3917a35d45062410154ff61 100644 (file)
@@ -2,4 +2,6 @@ getopt.1
 uuidd.8
 uuidd.rc
 uuidd.service
+lastlog2-import.service
 uuidd.socket
+lastlog2.conf
index 7fbd667b1b8ece33585bf99ba6b7291a806eb225..5f035f84dee734a4f5fca76d8429946e82e3d47a 100644 (file)
@@ -100,6 +100,20 @@ lsblk_LDADD += -ludev
 endif
 endif # BUILD_LSBLK
 
+if BUILD_LIBLASTLOG2
+usrbin_exec_PROGRAMS += lastlog2
+MANPAGES += misc-utils/lastlog2.8
+dist_noinst_DATA += misc-utils/lastlog2.8.adoc
+lastlog2_SOURCES = misc-utils/lastlog2.c lib/strutils.c
+lastlog2_LDADD = $(LDADD) liblastlog2.la -lsqlite3
+lastlog2_CFLAGS = $(AM_CFLAGS) -I$(ul_liblastlog2_incdir)
+systemdsystemunit_DATA += \
+       misc-utils/lastlog2-import.service
+tmpfiles_DATA += misc-utils/lastlog2.conf
+endif
+PATHFILES += misc-utils/lastlog2-import.service \
+             misc-utils/lastlog2.conf
+
 if BUILD_UUIDGEN
 usrbin_exec_PROGRAMS += uuidgen
 MANPAGES += misc-utils/uuidgen.1
diff --git a/misc-utils/lastlog2-import.service.in b/misc-utils/lastlog2-import.service.in
new file mode 100644 (file)
index 0000000..c57a897
--- /dev/null
@@ -0,0 +1,15 @@
+[Unit]
+Description=Import lastlog data into lastlog2 database
+Documentation=man:lastlog2(8)
+After=local-fs.target
+ConditionPathExists=/var/log/lastlog
+ConditionPathExists=!/var/lib/lastlog/lastlog2.db
+
+[Service]
+Type=oneshot
+ExecStart=@usrbin_execdir@/lastlog2 --import /var/log/lastlog
+ExecStartPost=/usr/bin/mv /var/log/lastlog /var/log/lastlog.migrated
+RemainAfterExit=true
+
+[Install]
+WantedBy=default.target
diff --git a/misc-utils/lastlog2.8.adoc b/misc-utils/lastlog2.8.adoc
new file mode 100644 (file)
index 0000000..b6be372
--- /dev/null
@@ -0,0 +1,96 @@
+//po4a: entry man manual
+////
+Copyright 2023 Thorsten Kukuk (kukuk@suse.de)
+This file may be copied under the terms of the GNU Public License.
+////
+= lastlog2(8)
+:doctype: manpage
+:man manual: User Commands
+:man source: util-linux {release-version}
+:page-layout: base
+:command: lastlog2
+
+== NAME
+
+lastlog2 - display date of last login for all users or a specific one
+
+== SYNOPSIS
+
+*lastlog2* [options]
+
+== DESCRIPTION
+
+
+*lastlog2* displays the content of the last login database. The _login-name_,
+_last-login-time_, _tty_ and _remote-host_ will be printed.
+The default (no flags) causes all last login entries to be printed, sorted
+by the order as written the first time into the database.
+
+Compared to *lastlog* this command is Y2038 safe and uses sqlite3 to store the
+information and not a sparse file.
+
+== OPTIONS
+
+*-b*, *--before* _DAYS_::
+Print only last login records older than _DAYS_.
+
+*-C*, *--clear*::
+Clear last login record of a user. This option can be used only together with
+*-u' (*--user*).
+
+*-d*, *--database _FILE_::
+Use _FILE_ as lastlog2 database.
+
+*-h*, *--help*::
+Display help message and exit.
+
+*-i*, *--import* _FILE_::
+Import data from old lastlog file _FILE_. Existing entries in the lastlog2
+database will be overwritten.
+
+*-r*, *--rename* _NEWNAME_::
+This option can only be used together with *-u* (*--user*).
+
+*-s*, *--servive* _num_::
+Display PAM service used to login in the last column.
+
+*-S*, *--set*::
+Set last login record of a user to the current time. This option can only be used
+together with *-u* (*--user*).
+
+*-t*, *--time* _DAYS_::
+Print only last login records more recent than _DAYS_.
+
+*-u*, *--users* _LOGINS_::
+Print only the last login record of the user _LOGIN_.
+
+*-v*, *--version*::
+Print version number and exit.
+
+If the user has never logged in the message **Never logged in** will be displayed
+in the latest login time row.
+
+Only the entries for the current users of the system will be displayed.
+Other entries may exist for users that were deleted previously.
+
+== FILES
+
+*/var/lib/lastlog/lastlog2.db*::
+Lastlog2 logging database file
+
+
+== AUTHORS
+
+lastlog2 was written by Thorsten Kukuk for *liblastlog2*(3).
+
+== SEE ALSO
+
+*liblastlog2*(3)
+
+include::man-common/bugreports.adoc[]
+
+include::man-common/footer.adoc[]
+
+ifdef::translation[]
+include::man-common/translation.adoc[]
+endif::[]
diff --git a/misc-utils/lastlog2.c b/misc-utils/lastlog2.c
new file mode 100644 (file)
index 0000000..010c7e6
--- /dev/null
@@ -0,0 +1,337 @@
+/* SPDX-License-Identifier: BSD-2-Clause
+
+  Copyright (c) 2023, Thorsten Kukuk <kukuk@suse.com>
+
+  Redistribution and use in source and binary forms, with or without
+  modification, are permitted provided that the following conditions are met:
+
+  1. Redistributions of source code must retain the above copyright notice,
+     this list of conditions and the following disclaimer.
+
+  2. Redistributions in binary form must reproduce the above copyright
+     notice, this list of conditions and the following disclaimer in the
+     documentation and/or other materials provided with the distribution.
+
+  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+  POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#include <pwd.h>
+#include <time.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <getopt.h>
+#include <limits.h>
+
+#include "nls.h"
+#include "c.h"
+#include "strutils.h"
+#include "lastlog2.h"
+
+static char *lastlog2_path = LL2_DEFAULT_DATABASE;
+
+static int bflg = 0;
+static time_t b_days = 0;
+static int tflg = 0;
+static time_t t_days = 0;
+static int sflg = 0;
+
+static int
+print_entry(const char *user, int64_t ll_time,
+           const char *tty, const char *rhost,
+           const char *pam_service, const char *error)
+{
+       static int once = 0;
+       char *datep;
+       struct tm *tm, tm_buf;
+       char datetime[80];
+       /* IPv6 address is at maximum 39 characters.
+          But for LL-addresses (fe80+only) the interface should be set,
+          so LL-address + % + IFNAMSIZ. */
+       const int maxIPv6Addrlen = 42;
+
+       /* Print only if older than b days */
+       if (bflg && ((time (NULL) - ll_time) < b_days))
+               return 0;
+
+       /* Print only if newer than t days */
+       if (tflg && ((time (NULL) - ll_time) > t_days))
+               return 0;
+        /* this is necessary if you compile this on architectures with
+           a 32bit time_t type. */
+        time_t t_time = ll_time;
+        tm = localtime_r (&t_time, &tm_buf);
+       if (tm == NULL)
+               datep = "(unknown)";
+       else {
+               strftime (datetime, sizeof(datetime), "%a %b %e %H:%M:%S %z %Y", tm);
+               datep = datetime;
+       }
+
+       if (ll_time == 0)
+               datep = "**Never logged in**";
+
+       if (!once) {
+               printf("Username         Port     From%*s Latest%*s%s\n",
+                      maxIPv6Addrlen-4, " ",
+                      sflg?(int)strlen(datep)-5:0, " ", sflg?"Service":"");
+               once = 1;
+       }
+       printf("%-16s %-8.8s %*s %s%*s%s\n", user, tty ? tty : "",
+              -maxIPv6Addrlen, rhost ? rhost : "", datep,
+              sflg?31-(int)strlen(datep):0, " ", sflg?(pam_service?pam_service:""):"");
+
+       if (error)
+               printf("\nError: %s\n", error);
+
+       return 0;
+}
+
+static void
+usage(void)
+{
+       FILE *output = stdout;
+
+       fputs(USAGE_HEADER, output);
+       fprintf(output, _(" %s [options]\n"), program_invocation_short_name);
+
+       fputs(USAGE_OPTIONS, output);
+       fputs(_(" -b, --before DAYS       Print only records older than DAYS\n"), output);
+       fputs(_(" -C, --clear             Clear record of a user (requires -u)\n"), output);
+       fputs(_(" -d, --database FILE     Use FILE as lastlog2 database\n"), output);
+       fputs(_(" -i, --import FILE       Import data from old lastlog file\n"), output);
+       fputs(_(" -r, --rename NEWNAME    Rename existing user to NEWNAME (requires -u)\n"), output);
+       fputs(_(" -s, --service           Display PAM service\n"), output);
+       fputs(_(" -S, --set               ySet lastlog record to current time (requires -u)\n"), output);
+       fputs(_(" -t, --time DAYS         Print only lastlog records more recent than DAYS\n"), output);
+       fputs(_(" -u, --user LOGIN        Print lastlog record of the specified LOGIN\n"), output);
+
+       fputs(USAGE_SEPARATOR, output);
+       fprintf(output, USAGE_HELP_OPTIONS(25));
+       fprintf(output, USAGE_MAN_TAIL("lastlog2(8)"));
+
+       exit(EXIT_SUCCESS);
+}
+
+/* Check if an user exists on the system.
+   If yes, return 0, else return -1. */
+static int
+check_user(const char *name)
+{
+       if (getpwnam(name) == NULL)
+               return -1;
+       return 0;
+}
+
+int
+main(int argc, char **argv)
+{
+       struct option const longopts[] = {
+               {"before",   required_argument, NULL, 'b'},
+               {"clear",    no_argument,       NULL, 'C'},
+               {"database", required_argument, NULL, 'd'},
+               {"help",     no_argument,       NULL, 'h'},
+               {"import",   required_argument, NULL, 'i'},
+               {"rename",   required_argument, NULL, 'r'},
+               {"service",  no_argument,       NULL, 's'},
+               {"set",      no_argument,       NULL, 'S'},
+               {"time",     required_argument, NULL, 't'},
+               {"user",     required_argument, NULL, 'u'},
+               {"version",  no_argument,       NULL, 'v'},
+               {NULL, 0, NULL, '\0'}
+       };
+       char *error = NULL;
+       int Cflg = 0;
+       int iflg = 0;
+       int rflg = 0;
+       int Sflg = 0;
+       int uflg = 0;
+       const char *user = NULL;
+       const char *newname = NULL;
+       const char *lastlog_file = NULL;
+       struct ll2_context *db_context = NULL;
+
+       int c;
+
+       while ((c = getopt_long(argc, argv, "b:Cd:hi:r:sSt:u:v", longopts, NULL)) != -1) {
+               switch (c) {
+               case 'b': /* before DAYS; Print only records older than DAYS */
+                       {
+                               unsigned long days;
+                               errno = 0;
+                               days = strtoul_or_err(optarg, _("Cannot parse days"));
+                               b_days = (time_t) days * (24L*3600L) /* seconds/DAY */;
+                               bflg = 1;
+                       }
+                       break;
+               case 'C': /* clear; Clear record of a user (requires -u) */
+                       Cflg = 1;
+                       break;
+               case 'd': /* database <FILE>;   Use FILE as lastlog2 database */
+                       lastlog2_path = optarg;
+                       break;
+               case 'h': /* help; Display this help message and exit */
+                       usage();
+                       break;
+               case 'i': /* import <FILE>; Import data from old lastlog file */
+                       lastlog_file = optarg;
+                       iflg = 1;
+                       break;
+               case 'r': /* rename <NEWNAME>; Rename existing user to NEWNAME (requires -u) */
+                       rflg = 1;
+                       newname = optarg;
+                       break;
+               case 's': /* service; Display PAM service */
+                       sflg = 1;
+                       break;
+               case 'S': /* set; Set lastlog record to current time (requires -u) */
+                       /* Set lastlog record of a user to the current time. */
+                       Sflg = 1;
+                       break;
+               case 't': /* time <DAYS>; Print only lastlog records more recent than DAYS */
+                       {
+                               unsigned long days;
+                               errno = 0;
+                               days = strtoul_or_err(optarg, _("Cannot parse days"));
+                               t_days = (time_t) days * (24L*3600L) /* seconds/DAY */;
+                               tflg = 1;
+                       }
+                       break;
+               case 'u': /* user <LOGIN>; Print lastlog record of the specified LOGIN */
+                       uflg = 1;
+                       user = optarg;
+                       break;
+               case 'v': /* version; Print version number and exit */
+                       print_version(EXIT_SUCCESS);
+                       break;
+               default:
+                       errtryhelp(EXIT_FAILURE);
+               }
+       }
+
+       if ((Cflg + Sflg + iflg) > 1) {
+               errx(EXIT_FAILURE, _("Option -C, -i and -S cannot be used together\n"));
+       }
+
+       db_context = ll2_new_context(lastlog2_path);
+       if (!db_context)
+               errx(EXIT_FAILURE, _("Couldn't initialize lastlog2 environment.\n"));
+
+       if (iflg) {
+               /* Importing entries */
+               if (ll2_import_lastlog(db_context, lastlog_file, &error) != 0) {
+                       ll2_unref_context(db_context);
+                       if (error) {
+                               errx(EXIT_FAILURE, "%s\n", error);
+                       }
+                       else
+                               errx(EXIT_FAILURE, _("Couldn't import entries from '%s'\n"), lastlog_file);
+               }
+               ll2_unref_context(db_context);
+               exit(EXIT_SUCCESS);
+       }
+
+       if (Cflg || Sflg || rflg) {
+               /* udpating, inserting and removing entries */
+               if (!uflg || strlen(user) == 0) {
+                       ll2_unref_context(db_context);
+                       errx(EXIT_FAILURE, _("Options -C, -r and -S require option -u to specify the user\n"));
+               }
+
+               if ((Cflg || Sflg) && check_user(user) != 0) {
+                       ll2_unref_context(db_context);
+                       errx(EXIT_FAILURE, _("User '%s' does not exist.\n"), user);
+               }
+
+               if (Cflg) {
+                       if (ll2_remove_entry(db_context, user, &error) != 0) {
+                               ll2_unref_context(db_context);
+                               if (error) {
+                                       errx(EXIT_FAILURE, "%s\n", error);
+                               }
+                               else
+                                       errx(EXIT_FAILURE, _("Couldn't remove entry for '%s'\n"), user);
+                       }
+               }
+
+               if (Sflg) {
+                       time_t ll_time = 0;
+
+                       if (time(&ll_time) == -1) {
+                               ll2_unref_context(db_context);
+                               errx(EXIT_FAILURE, _("Could not determine current time: %s"),
+                                    strerror(errno));
+                       }
+
+                       if (ll2_update_login_time(db_context, user, ll_time, &error) != 0) {
+                               ll2_unref_context(db_context);
+                               if (error) {
+                                       errx(EXIT_FAILURE, "%s\n", error);
+                               }
+                               else
+                                       errx(EXIT_FAILURE, _("Couldn't update login time for '%s'\n"), user);
+                       }
+
+               }
+
+               if (rflg) {
+                       if (ll2_rename_user(db_context, user, newname, &error) != 0) {
+                               ll2_unref_context(db_context);
+                               if (error) {
+                                       errx(EXIT_FAILURE, "%s\n", error);
+                               }
+                               else
+                                       errx(EXIT_FAILURE, _("Couldn't rename entry '%s' to '%s'\n"), user, newname);
+                       }
+               }
+
+               ll2_unref_context(db_context);
+               exit(EXIT_SUCCESS);
+       }
+
+       if (user) {
+               /* print user specific information */
+               int64_t ll_time = 0;
+               char *tty = NULL;
+               char *rhost = NULL;
+               char *service = NULL;
+
+               if (check_user(user) != 0) {
+                       ll2_unref_context(db_context);
+                       errx(EXIT_FAILURE, _("User '%s' does not exist.\n"), user);
+               }
+
+               /* We ignore errors, if the user is not in the database he did never login */
+               ll2_read_entry(db_context, user, &ll_time, &tty, &rhost,
+                              &service, NULL);
+
+               print_entry(user, ll_time, tty, rhost, service, NULL);
+
+               ll2_unref_context(db_context);
+               exit(EXIT_SUCCESS);
+       }
+
+       /* print all information */
+       if (ll2_read_all(db_context, print_entry, &error) != 0) {
+               ll2_unref_context(db_context);
+               if (error) {
+                       errx(EXIT_FAILURE, "%s\n", error);
+               }
+               else
+                       errx(EXIT_FAILURE, _("Couldn't read entries for all users\n"));
+       }
+
+       ll2_unref_context(db_context);
+       exit(EXIT_SUCCESS);
+}
diff --git a/misc-utils/lastlog2.conf.in b/misc-utils/lastlog2.conf.in
new file mode 100644 (file)
index 0000000..9d11957
--- /dev/null
@@ -0,0 +1,5 @@
+# This file is part of lastlog2.
+#
+# See tmpfiles.d(5) for details
+#
+d /var/lib/lastlog 0755 - - -
index f1a9346931f02397eb8a2b77cb7d834cb0486e78..6a7dd8ceef06674a665b7087895dc3296136f55c 100644 (file)
@@ -12,6 +12,11 @@ look_sources = files(
   'look.c',
 )
 
+lastlog2_sources = files(
+  'lastlog2.c',
+) + \
+  strutils_c
+
 mcookie_sources = files(
   'mcookie.c',
 ) + \
@@ -71,6 +76,26 @@ test_uuidd_sources = files(
   'test_uuidd.c',
 )
 
+if build_liblastlog2 and systemd.found()
+  lastlog2_service = configure_file(
+    input : 'lastlog2-import.service.in',
+    output : 'lastlog2-import.service',
+    configuration : conf)
+  install_data(
+    lastlog2_service,
+    install_dir : systemdsystemunitdir)
+endif
+
+if build_liblastlog2
+  lastlog2_tmpfiles = configure_file(
+    input : 'lastlog2.conf.in',
+    output : 'lastlog2.conf',
+    configuration : conf)
+  install_data(
+    lastlog2_tmpfiles,
+    install_dir : '/usr/lib/tmpfiles.d')
+endif
+
 if build_uuidd and systemd.found()
   uuidd_service = configure_file(
     input : 'uuidd.service.in',
diff --git a/pam_lastlog2/COPYING b/pam_lastlog2/COPYING
new file mode 100644 (file)
index 0000000..df71a98
--- /dev/null
@@ -0,0 +1,24 @@
+BSD 2-Clause License
+
+Copyright (c) 2023, Thorsten Kukuk
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/pam_lastlog2/Makemodule.am b/pam_lastlog2/Makemodule.am
new file mode 100644 (file)
index 0000000..6d0dc26
--- /dev/null
@@ -0,0 +1,8 @@
+if BUILD_LIBLASTLOG2
+
+include pam_lastlog2/man/Makemodule.am
+include pam_lastlog2/src/Makemodule.am
+
+EXTRA_DIST     += pam_lastlog2/COPYING
+
+endif # BUILD_LIBLASTLOG2
diff --git a/pam_lastlog2/man/Makemodule.am b/pam_lastlog2/man/Makemodule.am
new file mode 100644 (file)
index 0000000..3f19ea1
--- /dev/null
@@ -0,0 +1,6 @@
+
+MANPAGES += \
+       pam_lastlog2/man/pam_lastlog2.8
+
+dist_noinst_DATA += \
+       pam_lastlog2/man/pam_lastlog2.8.adoc
diff --git a/pam_lastlog2/man/pam_lastlog2.8.adoc b/pam_lastlog2/man/pam_lastlog2.8.adoc
new file mode 100644 (file)
index 0000000..1ed5ba6
--- /dev/null
@@ -0,0 +1,78 @@
+//po4a: entry man manual
+= pam_lastlog2(8)
+:doctype: manpage
+:man manual: System Administration
+:man source: util-linux {release-version}
+:lib: pam_lastlog2
+:firstversion: 2.40
+:page-layout: base
+
+== NAME
+
+pam_lastlog2 - PAM module to display date of last login
+
+== SYNOPSIS
+
+*pam_lastlog2.so* [debug] [silent] [silent_if=<services>] [database=<file>]
+
+== DESCRIPTION
+
+pam_lastlog2 is a PAM module to display a line of information about the last login of the user. The module uses the /var/lib/lastlog/lastlog2.db database file to store all informations.
+
+Compared to pam_lastlog this PAM module is Y2038 safe and uses sqlite3 to store the information.
+
+== OPTIONS
+
+*debug*::
+Print debug information.
+
+*silent*::
+Avoid all messages except errors and don't inform the user about any previous login, only update the /var/lib/lastlog/lastlog2.db database.
+
+*silent_if=<services>*::
+The argument *services* is a comma separated list of PAM services. If a service is listed here, the last login message will not be shown.
+
+*database=<file>*::
+Use *file* instead of /var/lib/lastlog/lastlog2.db.
+
+== MODULE TYPES PROVIDED
+
+The *session* module type is provided for displaying the information about the last login and updating the lastlog file.
+
+== RETURN VALUES
+
+*PAM_SUCCESS*::
+Everything was successful.
+
+*PAM_SERVICE_ERR*::
+Internal service module error. This includes error reading from or writing to the database.
+
+*PAM_USER_UNKNOWN*::
+User not known.
+
+*PAM_IGNORE*::
+Returned by service types which do nothing.
+
+== EXAMPLES
+Add the following line to e.g.  /etc/pam.d/login to display the last login time of a user:
+
+session  optional pam_lastlog2.so silent_if=gdm,gdm-password
+
+It is up to the administrator to decide if the user can login (optional/required) when
+pam_lastlog2 has returned an error.
+
+== AUTHOR
+
+pam_lastlog2 was written by Thorsten Kukuk <kukuk@suse.com>.
+
+== SEE ALSO
+
+*liblastlog2*(3), *pam.conf*(5), *pam.d*(5), *pam*(8)
+
+include::man-common/bugreports.adoc[]
+
+include::man-common/footer-lib.adoc[]
+
+ifdef::translation[]
+include::man-common/translation.adoc[]
+endif::[]
diff --git a/pam_lastlog2/meson.build b/pam_lastlog2/meson.build
new file mode 100644 (file)
index 0000000..e9864f3
--- /dev/null
@@ -0,0 +1,39 @@
+cc = meson.get_compiler('c')
+pkg = import('pkgconfig')
+lib_pam_lastlog2_sources = '''
+  src/pam_lastlog2.c
+'''.split()  
+
+pamlibdir = get_option('pamlibdir')
+if pamlibdir == ''
+        pamlibdir = get_option('libdir') / 'security'
+endif
+
+if build_liblastlog2
+  pam_lastlog2_sym = 'src/pam_lastlog2.sym'
+  pam_lastlog2_sym_path = '@0@/@1@'.format(meson.current_source_dir(), pam_lastlog2_sym)
+
+  libpam = cc.find_library('pam')
+
+  pam_lastlog2 = both_libraries(
+    'pam_lastlog2',
+    lib_pam_lastlog2_sources,
+    name_prefix : '',
+    include_directories : [dir_include, dir_liblastlog2],
+    link_args : ['-Wl,--version-script=@0@'.format(pam_lastlog2_sym_path)],
+    link_depends : pam_lastlog2_sym,
+    link_with : [lib_lastlog2],
+    dependencies : [libpam],
+    install : build_liblastlog2,
+    install_dir : pamlibdir,
+    version : liblastlog2_version,
+  )
+  manadocs += ['pam_lastlog2/man/pam_lastlog2.8.adoc']
+
+  pkg.generate(
+    pam_lastlog2,
+    description : 'pam library to manage last login data with lastlog2',
+    subdirs : 'lastlog2',
+    version : pc_version
+  )
+endif  
diff --git a/pam_lastlog2/src/Makemodule.am b/pam_lastlog2/src/Makemodule.am
new file mode 100644 (file)
index 0000000..9884416
--- /dev/null
@@ -0,0 +1,21 @@
+securelibdir = $(libdir)/security
+securelib_LTLIBRARIES = pam_lastlog2.la
+
+pam_lastlog2_la_SOURCES = \
+       pam_lastlog2/src/pam_lastlog2.c
+
+EXTRA_pam_lastlog2_la_DEPENDENCIES = \
+       pam_lastlog2/src/pam_lastlog2.sym
+
+pam_lastlog2_la_CFLAGS = \
+       $(AM_CFLAGS) \
+        $(SOLIB_CFLAGS) \
+        -I$(ul_liblastlog2_incdir) \
+        -Iliblastlog2/src
+
+pam_lastlog2_la_LDFLAGS = $(SOLIB_LDFLAGS) -module -avoid-version -shared
+if HAVE_VSCRIPT
+pam_lastlog2_la_LDFLAGS += pam_lastlog2_la_LDFLAGS += $(VSCRIPT_LDFLAGS),$(top_srcdir)/pam_lastlog2/src/pam_lastlog2.sym
+endif
+
+EXTRA_DIST += pam_lastlog2/src/pam_lastlog2.sym
diff --git a/pam_lastlog2/src/pam_lastlog2.c b/pam_lastlog2/src/pam_lastlog2.c
new file mode 100644 (file)
index 0000000..2802f09
--- /dev/null
@@ -0,0 +1,340 @@
+/* SPDX-License-Identifier: BSD-2-Clause
+
+  Copyright (c) 2023, Thorsten Kukuk <kukuk@suse.com>
+
+  Redistribution and use in source and binary forms, with or without
+  modification, are permitted provided that the following conditions are met:
+
+  1. Redistributions of source code must retain the above copyright notice,
+     this list of conditions and the following disclaimer.
+
+  2. Redistributions in binary form must reproduce the above copyright
+     notice, this list of conditions and the following disclaimer in the
+     documentation and/or other materials provided with the distribution.
+
+  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+  POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#include <time.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <syslog.h>
+#include <security/pam_modules.h>
+#include <security/pam_ext.h>
+#include <security/pam_modutil.h>
+#include <security/_pam_macros.h>
+
+#include "lastlog2.h"
+#include "strutils.h"
+
+#define LASTLOG2_DEBUG        01  /* send info to syslog(3) */
+#define LASTLOG2_QUIET        02  /* keep quiet about things */
+
+static const char *lastlog2_path = LL2_DEFAULT_DATABASE;
+
+/* check for list match. */
+static int
+check_in_list (const char *service, const char *arg)
+{
+       const char *item;
+       const char *remaining;
+
+       if (!service)
+               return 0;
+
+       remaining = arg;
+
+       for (;;) {
+               item = strstr (remaining, service);
+               if (item == NULL)
+                       break;
+
+               /* is it really the start of an item in the list? */
+               if (item == arg || *(item - 1) == ',') {
+                       item += strlen (service);
+                       /* is item really the service? */
+                       if (*item == '\0' || *item == ',')
+                               return 1;
+               }
+
+               remaining = strchr (item, ',');
+               if (remaining == NULL)
+                       break;
+
+               /* skip ',' */
+               ++remaining;
+       }
+
+       return 0;
+}
+
+
+static int
+_pam_parse_args (pam_handle_t *pamh,
+                int flags, int argc,
+                const char **argv)
+{
+       int ctrl = 0;
+       const char *str;
+
+       /* does the application require quiet? */
+       if (flags & PAM_SILENT)
+               ctrl |= LASTLOG2_QUIET;
+
+       /* step through arguments */
+       for (; argc-- > 0; ++argv) {
+               if (strcmp (*argv, "debug") == 0)
+                       ctrl |= LASTLOG2_DEBUG;
+               else if (strcmp (*argv, "silent") == 0)
+                       ctrl |= LASTLOG2_QUIET;
+               else if ((str = startswith (*argv, "database=")) != NULL)
+                       lastlog2_path = str;
+               else if ((str = startswith (*argv, "silent_if=")) != NULL) {
+                       const void *void_str = NULL;
+                       const char *service;
+                       if ((pam_get_item (pamh, PAM_SERVICE, &void_str) != PAM_SUCCESS) ||
+                           void_str == NULL)
+                               service = "";
+                       else
+                               service = void_str;
+
+                       if (check_in_list (service, str)) {
+                               if (ctrl & LASTLOG2_DEBUG)
+                                       pam_syslog (pamh, LOG_DEBUG, "silent_if='%s' contains '%s'", str, service);
+                               ctrl |= LASTLOG2_QUIET;
+                       }
+               } else
+                       pam_syslog (pamh, LOG_ERR, "Unknown option: %s", *argv);
+       }
+
+       return ctrl;
+}
+
+static int
+write_login_data (pam_handle_t *pamh, int ctrl, const char *user)
+{
+       const void *void_str;
+       const char *tty;
+       const char *rhost;
+       const char *pam_service;
+       const char *xdg_vtnr;
+       int xdg_vtnr_nr;
+       char tty_buf[8];
+       time_t ll_time;
+       char *error = NULL;
+       int retval;
+
+       void_str = NULL;
+       retval = pam_get_item (pamh, PAM_TTY, &void_str);
+       if (retval != PAM_SUCCESS || void_str == NULL)
+               tty = "";
+       else
+               tty = void_str;
+
+       /* strip leading "/dev/" from tty. */
+       const char *str = startswith(tty, "/dev/");
+       if (str != NULL)
+               tty = str;
+
+       if (ctrl & LASTLOG2_DEBUG)
+               pam_syslog (pamh, LOG_DEBUG, "tty=%s", tty);
+
+       /* if PAM_TTY is not set or an X11 $DISPLAY, try XDG_VTNR */
+       if ((tty[0] == '\0' || strchr(tty, ':') != NULL) && (xdg_vtnr = pam_getenv (pamh, "XDG_VTNR")) != NULL) {
+               xdg_vtnr_nr = atoi (xdg_vtnr);
+               if (xdg_vtnr_nr > 0 && snprintf (tty_buf, sizeof(tty_buf), "tty%d", xdg_vtnr_nr) < (int) sizeof(tty_buf)) {
+                       tty = tty_buf;
+                       if (ctrl & LASTLOG2_DEBUG)
+                               pam_syslog (pamh, LOG_DEBUG, "tty(XDG_VTNR)=%s", tty);
+               }
+       }
+
+       void_str = NULL;
+       retval = pam_get_item (pamh, PAM_RHOST, &void_str);
+       if (retval != PAM_SUCCESS || void_str == NULL) {
+               void_str = NULL;
+               retval = pam_get_item (pamh, PAM_XDISPLAY, &void_str);
+               if (retval != PAM_SUCCESS || void_str == NULL) {
+                       rhost = "";
+               } else {
+                       rhost = void_str;
+                       if (ctrl & LASTLOG2_DEBUG)
+                               pam_syslog (pamh, LOG_DEBUG, "rhost(PAM_XDISPLAY)=%s", rhost);
+               }
+       } else {
+               rhost = void_str;
+               if (ctrl & LASTLOG2_DEBUG)
+                       pam_syslog (pamh, LOG_DEBUG, "rhost(PAM_RHOST)=%s", rhost);
+       }
+
+       void_str = NULL;
+       if ((pam_get_item (pamh, PAM_SERVICE, &void_str) != PAM_SUCCESS) ||
+           void_str == NULL)
+               pam_service = "";
+       else
+               pam_service = void_str;
+
+       if (time (&ll_time) < 0)
+               return PAM_SYSTEM_ERR;
+
+       struct ll2_context *context = ll2_new_context(lastlog2_path);
+       if (ll2_write_entry (context, user, ll_time, tty, rhost,
+                            pam_service, &error) != 0) {
+               if (error) {
+                       pam_syslog (pamh, LOG_ERR, "%s", error);
+                       free (error);
+               } else
+                       pam_syslog (pamh, LOG_ERR, "Unknown error writing to database %s", lastlog2_path);
+               ll2_unref_context(context);
+               return PAM_SYSTEM_ERR;
+       }
+       ll2_unref_context(context);
+
+       return PAM_SUCCESS;
+}
+
+static int
+show_lastlogin (pam_handle_t *pamh, int ctrl, const char *user)
+{
+       int64_t ll_time = 0;
+       char *tty = NULL;
+       char *rhost = NULL;
+       char *service = NULL;
+       char *date = NULL;
+       char the_time[256];
+       char *error = NULL;
+       int retval = PAM_SUCCESS;
+
+       if (ctrl & LASTLOG2_QUIET)
+               return retval;
+
+       struct ll2_context *context = ll2_new_context(lastlog2_path);
+       if (ll2_read_entry (context, user, &ll_time, &tty, &rhost,
+                           &service, &error) != 0) {
+               if (errno == ENOENT)
+               {
+                       /* DB file not found --> it is OK */
+                       ll2_unref_context(context);
+                       return PAM_SUCCESS;
+               }
+               if (error) {
+                       pam_syslog (pamh, LOG_ERR, "%s", error);
+                       free (error);
+               } else
+                       pam_syslog (pamh, LOG_ERR, "Unknown error reading database %s", lastlog2_path);
+               ll2_unref_context(context);
+               return PAM_SYSTEM_ERR;
+       }
+       ll2_unref_context(context);
+
+       if (ll_time) {
+               struct tm *tm, tm_buf;
+               /* this is necessary if you compile this on architectures with
+                  a 32bit time_t type. */
+               time_t t_time = ll_time;                
+
+               if ((tm = localtime_r (&t_time, &tm_buf)) != NULL) {
+                       strftime (the_time, sizeof (the_time),
+                                 " %a %b %e %H:%M:%S %Z %Y", tm);
+                       date = the_time;
+               }
+       }
+
+       if (date != NULL || rhost != NULL || tty != NULL)
+               retval = pam_info(pamh, "Last login:%s%s%s%s%s",
+                                 date ? date : "",
+                                 rhost ? " from " : "",
+                                 rhost ? rhost : "",
+                                 tty ? " on " : "",
+                                 tty ? tty : "");
+
+       _pam_drop(service);
+       _pam_drop(rhost);
+       _pam_drop(tty);
+
+       return retval;
+}
+
+int
+pam_sm_authenticate (pam_handle_t *pamh __attribute__((__unused__)),
+                    int flags __attribute__((__unused__)),
+                    int argc __attribute__((__unused__)),
+                    const char **argv __attribute__((__unused__)))
+{
+       return PAM_IGNORE;
+}
+
+int
+pam_sm_setcred (pam_handle_t *pamh __attribute__((__unused__)),
+               int flags __attribute__((__unused__)),
+               int argc __attribute__((__unused__)),
+               const char **argv __attribute__((__unused__)))
+{
+       return PAM_IGNORE;
+}
+
+int
+pam_sm_acct_mgmt (pam_handle_t *pamh __attribute__((__unused__)),
+                 int flags __attribute__((__unused__)),
+                 int argc __attribute__((__unused__)),
+                 const char **argv __attribute__((__unused__)))
+{
+       return PAM_IGNORE;
+}
+
+int
+pam_sm_open_session (pam_handle_t *pamh, int flags,
+                    int argc, const char **argv)
+{
+       const struct passwd *pwd;
+       const void *void_str;
+       const char *user;
+       int ctrl;
+
+       ctrl = _pam_parse_args (pamh, flags, argc, argv);
+
+       void_str = NULL;
+       int retval = pam_get_item (pamh, PAM_USER, &void_str);
+       if (retval != PAM_SUCCESS || void_str == NULL || strlen (void_str) == 0) {
+               if (!(ctrl & LASTLOG2_QUIET))
+                       pam_syslog (pamh, LOG_NOTICE, "User unknown");
+               return PAM_USER_UNKNOWN;
+       }
+       user = void_str;
+
+       /* verify the user exists */
+       pwd = pam_modutil_getpwnam (pamh, user);
+       if (pwd == NULL) {
+               if (ctrl & LASTLOG2_DEBUG)
+                       pam_syslog (pamh, LOG_DEBUG, "Couldn't find user %s",
+                                   (const char *)user);
+               return PAM_USER_UNKNOWN;
+       }
+
+       if (ctrl & LASTLOG2_DEBUG)
+               pam_syslog (pamh, LOG_DEBUG, "user=%s", user);
+
+       show_lastlogin (pamh, ctrl, user);
+
+       return write_login_data (pamh, ctrl, user);
+}
+
+int
+pam_sm_close_session (pam_handle_t *pamh __attribute__((__unused__)),
+                     int flags __attribute__((__unused__)),
+                     int argc __attribute__((__unused__)),
+                     const char **argv __attribute__((__unused__)))
+{
+       return PAM_SUCCESS;
+}
diff --git a/pam_lastlog2/src/pam_lastlog2.sym b/pam_lastlog2/src/pam_lastlog2.sym
new file mode 100644 (file)
index 0000000..1633155
--- /dev/null
@@ -0,0 +1,11 @@
+
+{
+  global:
+    pam_sm_acct_mgmt;
+    pam_sm_authenticate;
+    pam_sm_chauthtok;
+    pam_sm_close_session;
+    pam_sm_open_session;
+    pam_sm_setcred;
+  local: *;
+};
\ No newline at end of file
index adf9544945410f4d63e2db98d84b02e4ace1b275..6699f3d252a5e41d6651f471a12cf20b5986d483 100644 (file)
@@ -14,6 +14,13 @@ TS_HELPER_LIBFDISK_MKPART="${ts_helpersdir}sample-fdisk-mkpart"
 TS_HELPER_LIBMOUNT_CONTEXT="${ts_helpersdir}test_mount_context"
 TS_HELPER_LIBFDISK_MKPART_FULLSPEC="${ts_helpersdir}sample-fdisk-mkpart-fullspec"
 TS_HELPER_LIBFDISK_SCRIPT_FUZZ="${ts_helpersdir}test_fdisk_script_fuzz"
+TS_HELPER_LIBLASTLOG2_DLOPEN="${ts_helpersdir}test_lastlog2_dlopen"
+TS_HELPER_LIBLASTLOG2_PAM_LASTLOG2_OUTPUT="${ts_helpersdir}test_lastlog2_pam_lastlog2_output"
+TS_HELPER_LIBLASTLOG2_REMOVE_ENTRY="${ts_helpersdir}test_lastlog2_remove_entry"
+TS_HELPER_LIBLASTLOG2_RENAME_USER="${ts_helpersdir}test_lastlog2_rename_user"
+TS_HELPER_LIBLASTLOG2_WRITE_READ_USER="${ts_helpersdir}test_lastlog2_write_read_user"
+TS_HELPER_LIBLASTLOG2_Y2038_LL2_READ_ALL="${ts_helpersdir}test_lastlog2_y2038_ll2_read_all"
+TS_HELPER_LIBLASTLOG2_Y2038_SQLITE2_TIME="${ts_helpersdir}test_lastlog2_y2038_sqlite3_time"
 TS_HELPER_LIBMOUNT_LOCK="${ts_helpersdir}test_mount_lock"
 TS_HELPER_LIBMOUNT_OPTSTR="${ts_helpersdir}test_mount_optstr"
 TS_HELPER_LIBMOUNT_OPTLIST="${ts_helpersdir}test_mount_optlist"
diff --git a/tests/ts/liblastlog2/dlopen b/tests/ts/liblastlog2/dlopen
new file mode 100755 (executable)
index 0000000..8ce4e17
--- /dev/null
@@ -0,0 +1,23 @@
+#!/bin/bash
+
+TS_TOPDIR="${0%/*}/../.."
+TS_DESC="dlopen"
+
+. "$TS_TOPDIR"/functions.sh
+ts_init "$*"
+
+# incorrect warning: top_builddir is referenced but not assigned.
+# shellcheck disable=SC2154
+if [ -e "$top_builddir/meson.conf" ]; then
+  #meson build
+  libpath=${top_builddir}/liblastlog2
+else
+  #automake/autoconf build
+  libpath=${top_builddir}/.libs
+fi
+
+ts_check_test_command $TS_HELPER_LIBLASTLOG2_DLOPEN
+
+$TS_HELPER_LIBLASTLOG2_DLOPEN ${libpath}/liblastlog2.so >/dev/null || ts_failed "returned an error"
+
+ts_finalize
diff --git a/tests/ts/liblastlog2/pam_lastlog2_output b/tests/ts/liblastlog2/pam_lastlog2_output
new file mode 100755 (executable)
index 0000000..c9961f1
--- /dev/null
@@ -0,0 +1,17 @@
+#!/bin/bash
+
+TS_TOPDIR="${0%/*}/../.."
+TS_DESC="pam_lastlog2_output"
+
+. "$TS_TOPDIR"/functions.sh
+ts_init "$*"
+
+ts_check_test_command $TS_HELPER_LIBLASTLOG2_PAM_LASTLOG2_OUTPUT
+
+export TZ=UTC
+
+$TS_HELPER_LIBLASTLOG2_PAM_LASTLOG2_OUTPUT || ts_failed "returned an error"
+
+rm pam_lastlog2-output.db
+
+ts_finalize
diff --git a/tests/ts/liblastlog2/remove_entry b/tests/ts/liblastlog2/remove_entry
new file mode 100755 (executable)
index 0000000..63503d9
--- /dev/null
@@ -0,0 +1,15 @@
+#!/bin/bash
+
+TS_TOPDIR="${0%/*}/../.."
+TS_DESC="remove_entry"
+
+. "$TS_TOPDIR"/functions.sh
+ts_init "$*"
+
+ts_check_test_command $TS_HELPER_LIBLASTLOG2_REMOVE_ENTRY
+
+$TS_HELPER_LIBLASTLOG2_REMOVE_ENTRY || ts_failed "returned an error"
+
+rm tst-delete-user.db
+
+ts_finalize
diff --git a/tests/ts/liblastlog2/rename_user b/tests/ts/liblastlog2/rename_user
new file mode 100755 (executable)
index 0000000..38f0763
--- /dev/null
@@ -0,0 +1,15 @@
+#!/bin/bash
+
+TS_TOPDIR="${0%/*}/../.."
+TS_DESC="rename_user"
+
+. "$TS_TOPDIR"/functions.sh
+ts_init "$*"
+
+ts_check_test_command  $TS_HELPER_LIBLASTLOG2_RENAME_USER
+
+$TS_HELPER_LIBLASTLOG2_RENAME_USER || ts_failed "returned an error"
+
+rm tst-rename-user.db
+
+ts_finalize
diff --git a/tests/ts/liblastlog2/sqlite3_time b/tests/ts/liblastlog2/sqlite3_time
new file mode 100755 (executable)
index 0000000..9ff27fe
--- /dev/null
@@ -0,0 +1,15 @@
+#!/bin/bash
+
+TS_TOPDIR="${0%/*}/../.."
+TS_DESC="sqlite3_time"
+
+. "$TS_TOPDIR"/functions.sh
+ts_init "$*"
+
+ts_check_test_command $TS_HELPER_LIBLASTLOG2_Y2038_SQLITE2_TIME
+
+$TS_HELPER_LIBLASTLOG2_Y2038_SQLITE2_TIME >/dev/null || ts_failed "returned an error"
+
+rm y2038-sqlite3-time.db
+
+ts_finalize
diff --git a/tests/ts/liblastlog2/write_read_user b/tests/ts/liblastlog2/write_read_user
new file mode 100755 (executable)
index 0000000..8a6028a
--- /dev/null
@@ -0,0 +1,15 @@
+#!/bin/bash
+
+TS_TOPDIR="${0%/*}/../.."
+TS_DESC="write_read_user"
+
+. "$TS_TOPDIR"/functions.sh
+ts_init "$*"
+
+ts_check_test_command $TS_HELPER_LIBLASTLOG2_WRITE_READ_USER
+
+$TS_HELPER_LIBLASTLOG2_WRITE_READ_USER || ts_failed "returned an error"
+
+rm tst-write-read-user.db
+
+ts_finalize
diff --git a/tests/ts/liblastlog2/y2038_ll2_read_all b/tests/ts/liblastlog2/y2038_ll2_read_all
new file mode 100755 (executable)
index 0000000..da779c2
--- /dev/null
@@ -0,0 +1,15 @@
+#!/bin/bash
+
+TS_TOPDIR="${0%/*}/../.."
+TS_DESC="y2038_ll2_read_all"
+
+. "$TS_TOPDIR"/functions.sh
+ts_init "$*"
+
+ts_check_test_command $TS_HELPER_LIBLASTLOG2_Y2038_LL2_READ_ALL
+
+$TS_HELPER_LIBLASTLOG2_Y2038_LL2_READ_ALL >/dev/null || ts_failed "returned an error"
+
+rm y2038-ll2_read_all.db
+
+ts_finalize