diff --git a/.gitignore b/.gitignore index ff6e7ae3a3..8a0215bd9e 100644 --- a/.gitignore +++ b/.gitignore @@ -69,6 +69,7 @@ /cups/testjson /cups/testjwt /cups/testlang +/cups/testoauth /cups/testoptions /cups/testppd /cups/testpwg diff --git a/config-scripts/cups-common.m4 b/config-scripts/cups-common.m4 index 78ed916d28..e445230844 100644 --- a/config-scripts/cups-common.m4 +++ b/config-scripts/cups-common.m4 @@ -344,8 +344,8 @@ INSTALLXPC="" AS_CASE([$host_os_name], [darwin*], [ BACKLIBS="$BACKLIBS -framework IOKit" SERVERLIBS="$SERVERLIBS -framework IOKit -weak_framework ApplicationServices" - LIBS="-framework CoreFoundation -framework Security $LIBS" - PKGCONFIG_LIBS_STATIC="$PKGCONFIG_LIBS_STATIC -framework CoreFoundation -framework Security" + LIBS="-framework CoreFoundation -framework CoreServices -framework Security $LIBS" + PKGCONFIG_LIBS_STATIC="$PKGCONFIG_LIBS_STATIC -framework CoreFoundation -framework CoreServices -framework Security" dnl Check for framework headers... AC_CHECK_HEADER([ApplicationServices/ApplicationServices.h], [ diff --git a/configure b/configure index e77b85467a..19304377ac 100755 --- a/configure +++ b/configure @@ -4726,11 +4726,11 @@ if test x$ac_prog_cxx_stdcxx = xno then : { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $CXX option to enable C++11 features" >&5 printf %s "checking for $CXX option to enable C++11 features... " >&6; } -if test ${ac_cv_prog_cxx_cxx11+y} +if test ${ac_cv_prog_cxx_11+y} then : printf %s "(cached) " >&6 else $as_nop - ac_cv_prog_cxx_cxx11=no + ac_cv_prog_cxx_11=no ac_save_CXX=$CXX cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ @@ -4772,11 +4772,11 @@ if test x$ac_prog_cxx_stdcxx = xno then : { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $CXX option to enable C++98 features" >&5 printf %s "checking for $CXX option to enable C++98 features... " >&6; } -if test ${ac_cv_prog_cxx_cxx98+y} +if test ${ac_cv_prog_cxx_98+y} then : printf %s "(cached) " >&6 else $as_nop - ac_cv_prog_cxx_cxx98=no + ac_cv_prog_cxx_98=no ac_save_CXX=$CXX cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ @@ -6764,8 +6764,8 @@ case $host_os_name in #( BACKLIBS="$BACKLIBS -framework IOKit" SERVERLIBS="$SERVERLIBS -framework IOKit -weak_framework ApplicationServices" - LIBS="-framework CoreFoundation -framework Security $LIBS" - PKGCONFIG_LIBS_STATIC="$PKGCONFIG_LIBS_STATIC -framework CoreFoundation -framework Security" + LIBS="-framework CoreFoundation -framework CoreServices -framework Security $LIBS" + PKGCONFIG_LIBS_STATIC="$PKGCONFIG_LIBS_STATIC -framework CoreFoundation -framework CoreServices -framework Security" ac_fn_c_check_header_compile "$LINENO" "ApplicationServices/ApplicationServices.h" "ac_cv_header_ApplicationServices_ApplicationServices_h" "$ac_includes_default" if test "x$ac_cv_header_ApplicationServices_ApplicationServices_h" = xyes diff --git a/cups/Dependencies b/cups/Dependencies index 16fdddd93c..95b7f9b752 100644 --- a/cups/Dependencies +++ b/cups/Dependencies @@ -24,7 +24,6 @@ auth.o: auth.c cups-private.h string-private.h ../config.h ../cups/base.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h debug.o: debug.c cups-private.h string-private.h ../config.h \ @@ -51,7 +50,6 @@ debug.o: debug.c cups-private.h string-private.h ../config.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h dest.o: dest.c cups-private.h string-private.h ../config.h ../cups/base.h \ @@ -77,7 +75,6 @@ dest.o: dest.c cups-private.h string-private.h ../config.h ../cups/base.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h dnssd.h dest-job.o: dest-job.c cups-private.h string-private.h ../config.h \ @@ -104,7 +101,6 @@ dest-job.o: dest-job.c cups-private.h string-private.h ../config.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h dest-localization.o: dest-localization.c cups-private.h string-private.h \ @@ -131,7 +127,6 @@ dest-localization.o: dest-localization.c cups-private.h string-private.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h dest-options.o: dest-options.c cups-private.h string-private.h \ @@ -158,12 +153,33 @@ dest-options.o: dest-options.c cups-private.h string-private.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h -dir.o: dir.c cups.h file.h base.h ipp.h http.h array.h language.h pwg.h \ - string-private.h ../config.h ../cups/base.h debug-internal.h \ - debug-private.h dir.h +dir.o: dir.c cups-private.h string-private.h ../config.h ../cups/base.h \ + debug-internal.h debug-private.h ipp-private.h ../cups/cups.h file.h \ + base.h ipp.h http.h array.h language.h pwg.h http-private.h \ + ../cups/language.h ../cups/http.h \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ + thread.h cups.h dir.h dnssd.o: dnssd.c cups-private.h string-private.h ../config.h \ ../cups/base.h debug-internal.h debug-private.h ipp-private.h \ ../cups/cups.h file.h base.h ipp.h http.h array.h language.h pwg.h \ @@ -188,7 +204,6 @@ dnssd.o: dnssd.c cups-private.h string-private.h ../config.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h dnssd.h encode.o: encode.c cups-private.h string-private.h ../config.h \ @@ -215,7 +230,6 @@ encode.o: encode.c cups-private.h string-private.h ../config.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h file.o: file.c file-private.h cups-private.h string-private.h ../config.h \ @@ -242,7 +256,6 @@ file.o: file.c file-private.h cups-private.h string-private.h ../config.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h form.o: form.c form.h cups.h file.h base.h ipp.h http.h array.h \ @@ -269,7 +282,6 @@ form.o: form.c form.h cups.h file.h base.h ipp.h http.h array.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h getputfile.o: getputfile.c cups-private.h string-private.h ../config.h \ @@ -296,7 +308,6 @@ getputfile.o: getputfile.c cups-private.h string-private.h ../config.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h globals.o: globals.c cups-private.h string-private.h ../config.h \ @@ -323,7 +334,6 @@ globals.o: globals.c cups-private.h string-private.h ../config.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h hash.o: hash.c cups-private.h string-private.h ../config.h ../cups/base.h \ @@ -349,7 +359,6 @@ hash.o: hash.c cups-private.h string-private.h ../config.h ../cups/base.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h md5-internal.h http.o: http.c cups-private.h string-private.h ../config.h ../cups/base.h \ @@ -375,7 +384,6 @@ http.o: http.c cups-private.h string-private.h ../config.h ../cups/base.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h http-addr.o: http-addr.c cups-private.h string-private.h ../config.h \ @@ -402,7 +410,6 @@ http-addr.o: http-addr.c cups-private.h string-private.h ../config.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h http-addrlist.o: http-addrlist.c cups-private.h string-private.h \ @@ -429,7 +436,6 @@ http-addrlist.o: http-addrlist.c cups-private.h string-private.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h http-support.o: http-support.c cups-private.h string-private.h \ @@ -456,7 +462,6 @@ http-support.o: http-support.c cups-private.h string-private.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h dnssd.h ipp.o: ipp.c cups-private.h string-private.h ../config.h ../cups/base.h \ @@ -482,7 +487,6 @@ ipp.o: ipp.c cups-private.h string-private.h ../config.h ../cups/base.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h ipp-file.o: ipp-file.c cups-private.h string-private.h ../config.h \ @@ -509,7 +513,6 @@ ipp-file.o: ipp-file.c cups-private.h string-private.h ../config.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h ipp-support.o: ipp-support.c cups-private.h string-private.h ../config.h \ @@ -536,7 +539,6 @@ ipp-support.o: ipp-support.c cups-private.h string-private.h ../config.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h json.o: json.c cups-private.h string-private.h ../config.h ../cups/base.h \ @@ -562,7 +564,6 @@ json.o: json.c cups-private.h string-private.h ../config.h ../cups/base.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h json-private.h json.h jwt.o: jwt.c cups-private.h string-private.h ../config.h ../cups/base.h \ @@ -588,9 +589,9 @@ jwt.o: jwt.c cups-private.h string-private.h ../config.h ../cups/base.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ - thread.h cups.h jwt.h json.h json-private.h + thread.h cups.h jwt.h json.h json-private.h \ + langprintf.o: langprintf.c cups-private.h string-private.h ../config.h \ ../cups/base.h debug-internal.h debug-private.h ipp-private.h \ ../cups/cups.h file.h base.h ipp.h http.h array.h language.h pwg.h \ @@ -615,7 +616,6 @@ langprintf.o: langprintf.c cups-private.h string-private.h ../config.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h language.o: language.c cups-private.h string-private.h ../config.h \ @@ -642,7 +642,6 @@ language.o: language.c cups-private.h string-private.h ../config.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h md5.o: md5.c md5-internal.h base.h string-private.h ../config.h \ @@ -669,7 +668,6 @@ md5passwd.o: md5passwd.c ../cups/cups.h file.h base.h ipp.h http.h \ \ \ \ - \ string-private.h ../cups/base.h notify.o: notify.c cups-private.h string-private.h ../config.h \ ../cups/base.h debug-internal.h debug-private.h ipp-private.h \ @@ -695,10 +693,9 @@ notify.o: notify.c cups-private.h string-private.h ../config.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h -options.o: options.c cups-private.h string-private.h ../config.h \ +oauth.o: oauth.c cups-private.h string-private.h ../config.h \ ../cups/base.h debug-internal.h debug-private.h ipp-private.h \ ../cups/cups.h file.h base.h ipp.h http.h array.h language.h pwg.h \ http-private.h ../cups/language.h ../cups/http.h \ @@ -722,6 +719,31 @@ options.o: options.c cups-private.h string-private.h ../config.h \ \ \ \ + language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ + thread.h cups.h oauth.h jwt.h json.h form.h +options.o: options.c cups-private.h string-private.h ../config.h \ + ../cups/base.h debug-internal.h debug-private.h ipp-private.h \ + ../cups/cups.h file.h base.h ipp.h http.h array.h language.h pwg.h \ + http-private.h ../cups/language.h ../cups/http.h \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h @@ -749,7 +771,6 @@ pwg-media.o: pwg-media.c cups-private.h string-private.h ../config.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h rand.o: rand.c cups.h file.h base.h ipp.h http.h array.h language.h pwg.h @@ -777,7 +798,6 @@ raster-error.o: raster-error.c cups-private.h string-private.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h raster-private.h raster.h ../cups/debug-private.h \ ../cups/string-private.h @@ -813,7 +833,6 @@ request.o: request.c cups-private.h string-private.h ../config.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h string.o: string.c cups-private.h string-private.h ../config.h \ @@ -840,7 +859,6 @@ string.o: string.c cups-private.h string-private.h ../config.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h tempfile.o: tempfile.c cups-private.h string-private.h ../config.h \ @@ -867,7 +885,6 @@ tempfile.o: tempfile.c cups-private.h string-private.h ../config.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h thread.o: thread.c cups-private.h string-private.h ../config.h \ @@ -894,7 +911,6 @@ thread.o: thread.c cups-private.h string-private.h ../config.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h tls.o: tls.c cups-private.h string-private.h ../config.h ../cups/base.h \ @@ -920,9 +936,8 @@ tls.o: tls.c cups-private.h string-private.h ../config.h ../cups/base.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ - thread.h cups.h tls-openssl.c \ + thread.h cups.h dir.h tls-openssl.c \ transcode.o: transcode.c cups-private.h string-private.h ../config.h \ ../cups/base.h debug-internal.h debug-private.h ipp-private.h \ @@ -948,7 +963,6 @@ transcode.o: transcode.c cups-private.h string-private.h ../config.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h usersys.o: usersys.c cups-private.h string-private.h ../config.h \ @@ -975,7 +989,6 @@ usersys.o: usersys.c cups-private.h string-private.h ../config.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h util.o: util.c cups-private.h string-private.h ../config.h ../cups/base.h \ @@ -1001,7 +1014,6 @@ util.o: util.c cups-private.h string-private.h ../config.h ../cups/base.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h adminutil.o: adminutil.c cups-private.h string-private.h ../config.h \ @@ -1028,7 +1040,6 @@ adminutil.o: adminutil.c cups-private.h string-private.h ../config.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h ppd.h raster.h adminutil.h backchannel.o: backchannel.c cups.h file.h base.h ipp.h http.h array.h \ @@ -1057,7 +1068,6 @@ backend.o: backend.c cups-private.h string-private.h ../config.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h backend.h ppd.h raster.h getdevices.o: getdevices.c cups-private.h string-private.h ../config.h \ @@ -1084,7 +1094,6 @@ getdevices.o: getdevices.c cups-private.h string-private.h ../config.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h adminutil.h getifaddrs.o: getifaddrs.c getifaddrs-internal.h ../config.h base.h @@ -1111,7 +1120,6 @@ ppd.o: ppd.c cups-private.h string-private.h ../config.h ../cups/base.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h ppd-private.h ../cups/ppd.h raster.h ppd-attr.o: ppd-attr.c cups-private.h string-private.h ../config.h \ @@ -1138,7 +1146,6 @@ ppd-attr.o: ppd-attr.c cups-private.h string-private.h ../config.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h ppd-private.h ../cups/ppd.h raster.h ppd-cache.o: ppd-cache.c cups-private.h string-private.h ../config.h \ @@ -1165,7 +1172,6 @@ ppd-cache.o: ppd-cache.c cups-private.h string-private.h ../config.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h ppd-private.h ../cups/ppd.h raster.h ppd-conflicts.o: ppd-conflicts.c cups-private.h string-private.h \ @@ -1192,7 +1198,6 @@ ppd-conflicts.o: ppd-conflicts.c cups-private.h string-private.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h ppd-private.h ../cups/ppd.h raster.h ppd-custom.o: ppd-custom.c cups-private.h string-private.h ../config.h \ @@ -1219,7 +1224,6 @@ ppd-custom.o: ppd-custom.c cups-private.h string-private.h ../config.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h ppd-private.h ../cups/ppd.h raster.h ppd-emit.o: ppd-emit.c cups-private.h string-private.h ../config.h \ @@ -1246,7 +1250,6 @@ ppd-emit.o: ppd-emit.c cups-private.h string-private.h ../config.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h ppd.h raster.h ppd-localize.o: ppd-localize.c cups-private.h string-private.h \ @@ -1273,7 +1276,6 @@ ppd-localize.o: ppd-localize.c cups-private.h string-private.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h ppd-private.h ../cups/ppd.h raster.h ppd-mark.o: ppd-mark.c cups-private.h string-private.h ../config.h \ @@ -1300,7 +1302,6 @@ ppd-mark.o: ppd-mark.c cups-private.h string-private.h ../config.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h ppd-private.h ../cups/ppd.h raster.h ppd-page.o: ppd-page.c string-private.h ../config.h ../cups/base.h \ @@ -1330,7 +1331,6 @@ ppd-util.o: ppd-util.c cups-private.h string-private.h ../config.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h ppd-private.h ../cups/ppd.h raster.h raster-interpret.o: raster-interpret.c ../cups/raster-private.h raster.h \ @@ -1366,7 +1366,6 @@ sidechannel.o: sidechannel.c sidechannel.h base.h cups-private.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h snmp.o: snmp.c cups-private.h string-private.h ../config.h ../cups/base.h \ @@ -1392,7 +1391,6 @@ snmp.o: snmp.c cups-private.h string-private.h ../config.h ../cups/base.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h snmp-private.h raster-interstub.o: raster-interstub.c ../cups/ppd-private.h \ @@ -1402,6 +1400,32 @@ raster-stubs.o: raster-stubs.c raster-private.h raster.h cups.h file.h \ base.h ipp.h http.h array.h language.h pwg.h ../cups/cups.h \ ../cups/debug-private.h ../cups/base.h ../cups/string-private.h \ ../config.h +cachebench.o: cachebench.c ppd-private.h ../cups/cups.h file.h base.h \ + ipp.h http.h array.h language.h pwg.h ../cups/ppd.h cups.h raster.h \ + pwg-private.h ../cups/pwg.h file-private.h cups-private.h \ + string-private.h ../config.h ../cups/base.h debug-internal.h \ + debug-private.h ipp-private.h http-private.h ../cups/language.h \ + ../cups/http.h \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + language-private.h ../cups/transcode.h thread.h fuzzipp.o: fuzzipp.c file.h base.h string-private.h ../config.h \ ../cups/base.h ipp-private.h ../cups/cups.h ipp.h http.h array.h \ language.h pwg.h @@ -1437,7 +1461,6 @@ testcache.o: testcache.c ppd-private.h ../cups/cups.h file.h base.h ipp.h \ \ \ \ - \ language-private.h ../cups/transcode.h thread.h testclient.o: testclient.c ../config.h ../cups/cups.h file.h base.h ipp.h \ http.h array.h language.h pwg.h ../cups/raster.h cups.h \ @@ -1469,7 +1492,6 @@ testcreds.o: testcreds.c cups-private.h string-private.h ../config.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h test-internal.h testcups.o: testcups.c cups-private.h string-private.h ../config.h \ @@ -1496,7 +1518,6 @@ testcups.o: testcups.c cups-private.h string-private.h ../config.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h ppd.h raster.h testdest.o: testdest.c cups.h file.h base.h ipp.h http.h array.h \ @@ -1534,7 +1555,6 @@ testhttp.o: testhttp.c cups-private.h string-private.h ../config.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h test-internal.h testi18n.o: testi18n.c cups.h file.h base.h ipp.h http.h array.h \ @@ -1571,9 +1591,10 @@ testlang.o: testlang.c cups-private.h string-private.h ../config.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h ppd-private.h ../cups/ppd.h raster.h ../cups/dir.h +testoauth.o: testoauth.c cups.h file.h base.h ipp.h http.h array.h \ + language.h pwg.h oauth.h jwt.h json.h test-internal.h testoptions.o: testoptions.c cups-private.h string-private.h ../config.h \ ../cups/base.h debug-internal.h debug-private.h ipp-private.h \ ../cups/cups.h file.h base.h ipp.h http.h array.h language.h pwg.h \ @@ -1598,7 +1619,6 @@ testoptions.o: testoptions.c cups-private.h string-private.h ../config.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h testppd.o: testppd.c cups-private.h string-private.h ../config.h \ @@ -1625,7 +1645,6 @@ testppd.o: testppd.c cups-private.h string-private.h ../config.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h ppd-private.h ../cups/ppd.h raster.h raster-private.h \ ../cups/debug-private.h ../cups/string-private.h @@ -1654,7 +1673,6 @@ testpwg.o: testpwg.c ppd-private.h ../cups/cups.h file.h base.h ipp.h \ \ \ \ - \ language-private.h ../cups/transcode.h thread.h testraster.o: testraster.c ../cups/raster-private.h raster.h cups.h \ file.h base.h ipp.h http.h array.h language.h pwg.h ../cups/cups.h \ @@ -1684,7 +1702,6 @@ testsnmp.o: testsnmp.c cups-private.h string-private.h ../config.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h snmp-private.h testtestpage.o: testtestpage.c raster-testpage.h raster-private.h \ @@ -1717,6 +1734,5 @@ tlscheck.o: tlscheck.c cups-private.h string-private.h ../config.h \ \ \ \ - \ language-private.h ../cups/transcode.h pwg-private.h ../cups/pwg.h \ thread.h cups.h diff --git a/cups/Makefile b/cups/Makefile index 2807053e0e..fadd00624a 100644 --- a/cups/Makefile +++ b/cups/Makefile @@ -46,6 +46,7 @@ COREOBJS = \ md5.o \ md5passwd.o \ notify.o \ + oauth.o \ options.o \ pwg-media.o \ rand.o \ @@ -111,6 +112,7 @@ TESTOBJS = \ testjson.o \ testjwt.o \ testlang.o \ + testoauth.o \ testoptions.o \ testppd.o \ testpwg.o \ @@ -142,6 +144,7 @@ COREHEADERS = \ json.h \ jwt.h \ language.h \ + oauth.h \ pwg.h \ raster.h \ thread.h \ @@ -207,6 +210,7 @@ UNITTARGETS = \ testjson \ testjwt \ testlang \ + testoauth \ testoptions \ testppd \ testpwg \ @@ -740,6 +744,16 @@ testlang: testlang.o $(LIBCUPSSTATIC) $(CODE_SIGN) -s "$(CODE_SIGN_IDENTITY)" $@ +# +# testoauth (dependency on static libraries is intentional) +# + +testoauth: testoauth.o $(LIBCUPS_STATIC) + echo Linking $@... + $(LD_CC) $(ARCHFLAGS) $(ALL_LDFLAGS) -o $@ testoauth.o $(LINKCUPSSTATIC) + $(CODE_SIGN) -s "$(CODE_SIGN_IDENTITY)" $@ + + # # testoptions (dependency on static CUPS library is intentional) # diff --git a/cups/http.c b/cups/http.c index c0716f1dbf..b7e37048b6 100644 --- a/cups/http.c +++ b/cups/http.c @@ -97,7 +97,8 @@ static const char * const http_fields[] = "Optional-WWW-Authenticate", "Origin", "OSCORE", - "Strict-Transport-Security" + "Strict-Transport-Security", + "Accept" }; diff --git a/cups/http.h b/cups/http.h index 6cc06ce0ff..68c1c95853 100644 --- a/cups/http.h +++ b/cups/http.h @@ -162,6 +162,7 @@ typedef enum http_field_e // HTTP field names HTTP_FIELD_ORIGIN, // RFC 6454 Origin field @since CUPS 2.4@ HTTP_FIELD_OSCORE, // RFC 8613 OSCORE field @since CUPS 2.4@ HTTP_FIELD_STRICT_TRANSPORT_SECURITY, // HSTS Strict-Transport-Security field @since CUPS 2.4@ + HTTP_FIELD_ACCEPT, // Accept field @since CUPS 2.5@ HTTP_FIELD_MAX // Maximum field index } http_field_t; diff --git a/cups/ipp-support.c b/cups/ipp-support.c index 29fc0b11e3..bf844e8ee6 100644 --- a/cups/ipp-support.c +++ b/cups/ipp-support.c @@ -128,7 +128,8 @@ static const char * const ipp_status_oks[] = // "OK" status codes { "cups-authentication-canceled", "cups-pki-error", - "cups-upgrade-required" + "cups-upgrade-required", + "cups-oauth" }; static const char * const ipp_std_ops[] = { @@ -2257,9 +2258,8 @@ ippErrorString(ipp_status_t error) // I - Error status error <= IPP_STATUS_ERROR_TOO_MANY_DOCUMENTS) return (ipp_status_500s[error - IPP_STATUS_ERROR_INTERNAL]); else if (error >= IPP_STATUS_ERROR_CUPS_AUTHENTICATION_CANCELED && - error <= IPP_STATUS_ERROR_CUPS_UPGRADE_REQUIRED) - return (ipp_status_1000s[error - - IPP_STATUS_ERROR_CUPS_AUTHENTICATION_CANCELED]); + error <= IPP_STATUS_ERROR_CUPS_OAUTH) + return (ipp_status_1000s[error - IPP_STATUS_ERROR_CUPS_AUTHENTICATION_CANCELED]); /* * No, build an "0xxxxx" error string... diff --git a/cups/ipp.h b/cups/ipp.h index 891b1a4ca0..a2e21c3dbf 100644 --- a/cups/ipp.h +++ b/cups/ipp.h @@ -448,7 +448,9 @@ typedef enum ipp_status_e // IPP status code values IPP_STATUS_ERROR_CUPS_AUTHENTICATION_CANCELED = 0x1000, // cups-authentication-canceled - Authentication canceled by user @since CUPS 1.5/macOS 10.7@ IPP_STATUS_ERROR_CUPS_PKI, // cups-pki-error - Error negotiating a secure connection @since CUPS 1.5/macOS 10.7@ - IPP_STATUS_ERROR_CUPS_UPGRADE_REQUIRED// cups-upgrade-required - TLS upgrade required @since CUPS 1.5/macOS 10.7@ + IPP_STATUS_ERROR_CUPS_UPGRADE_REQUIRED, + // cups-upgrade-required - TLS upgrade required @since CUPS 1.5/macOS 10.7@ + IPP_STATUS_ERROR_CUPS_OAUTH // cups-oauth - OAuth error } ipp_status_t; typedef enum ipp_tag_e // Value and group tag values for attributes diff --git a/cups/oauth.c b/cups/oauth.c new file mode 100644 index 0000000000..5ce264c674 --- /dev/null +++ b/cups/oauth.c @@ -0,0 +1,1814 @@ +// +// OAuth API implementation for CUPS. +// +// Copyright © 2024 by OpenPrinting. +// Copyright © 2017-2024 by Michael R Sweet +// +// Licensed under Apache License v2.0. See the file "LICENSE" for more +// information. +// + +#include "cups-private.h" +#include "oauth.h" +#include "form.h" +#include +#ifdef _WIN32 +# include +# define O_NOFOLLOW 0 // Windows doesn't support this... +#else +# include +# ifdef __APPLE__ +# include +# include +# else +# include +# include +extern char **environ; // @private@ +# endif // __APPLE__ +#endif // _WIN32 + + +// +// Overview +// ======== +// +// The CUPS OAuth implementation follows the IEEE-ISTO Printer Working Group's +// IPP OAuth Extensions v1.0 (OAUTH) specification (pending publication), which +// in turn depends on a boatload of IETF RFCs and the OpenID Connect +// specifications. In short, the IPP specification handles how to combine IPP +// (which is layered on top of HTTP) with OAuth and works to "consolidate" the +// different requirements of IETF OAuth 2.x and OpenID Connect so that we are as +// widely interoperable as possible. +// +// +// Compatibility +// ------------- +// +// The intent is for CUPS to support using common OAuth implementations, +// including (but not limited to): +// +// - Amazon Cognito () +// - Github () +// - Google () +// - Microsoft Account/Azure Active Directory/Entra ID () +// - mOAuth () +// - Okta Auth0 () +// +// +// Security +// -------- +// +// Security on the wire is as good as OAuth and TLS provides. +// +// The current OAuth cache implementation uses unencrypted files in your home +// directory with restricted permissions. Ideally they should be encrypted +// "at rest" but Unix doesn't have a universal solution for this and the +// available options don't generally protect against malicious code running as +// the target user. The code is setup to facilitate replacement with another +// storage "backend" (like the Keychain API on macOS), and adding conditional +// platform support code for this is planned. This sort of issue is generally +// mitigated by access tokens having a limited life... +// +// +// Notes +// ----- +// +// - Amazon and Microsoft require you to setup an Authorization Server for your +// domain before you can play/test. There is no public sandbox service. +// - Github support currently depends on hardcoded metadata +// () and has a few +// authorization extensions that might require some special-handling. +// - Google implements OpenID Connect but not RFC 8414 +// () and seems to only allow a redirect URI of +// "http://localhost" without a specified path. +// - Okta Auth0 provides a sample OpenID Connect Authorization Server +// () that also supports Device Connect and a few +// other extensions that might be handy in the future. +// + + +// +// Local types... +// + +typedef enum _cups_otype_e // OAuth data type +{ + _CUPS_OTYPE_ACCESS, // Access token + _CUPS_OTYPE_CLIENT_ID, // Client ID + _CUPS_OTYPE_CLIENT_SECRET, // Client secret + _CUPS_OTYPE_CODE_VERIFIER, // Client code_verifier + _CUPS_OTYPE_USER_ID, // (User) ID token + _CUPS_OTYPE_JWKS, // Server key store + _CUPS_OTYPE_METADATA, // Server metadata + _CUPS_OTYPE_NONCE, // Client nonce + _CUPS_OTYPE_REDIRECT_URI, // Redirect URI used + _CUPS_OTYPE_REFRESH // Refresh token +} _cups_otype_t; + + +// +// Local constants... +// + +#define _CUPS_OAUTH_REDIRECT_FORMAT "http://127.0.0.1:%d/" + // Redirect URI with port +#define _CUPS_OAUTH_REDIRECT_PATH "/?" + // Redirect URI request path prefix +#define _CUPS_OAUTH_REDIRECT_PATHLEN 2 + // Redirect URI request path length + +#ifdef DEBUG +static const char * const cups_otypes[] = +{ // OAuth data types... + "_CUPS_OTYPE_ACCESS", // Access token + "_CUPS_OTYPE_CLIENT_ID", // Client ID + "_CUPS_OTYPE_CLIENT_SECRET", // Client secret + "_CUPS_OTYPE_CODE_VERIFIER", // Client code_verifier + "_CUPS_OTYPE_USER_ID", // (User) ID token + "_CUPS_OTYPE_JWKS", // Server key store + "_CUPS_OTYPE_METADATA", // Server metadata + "_CUPS_OTYPE_NONCE", // Client nonce + "_CUPS_OTYPE_REDIRECT_URI", // Redirect URI used + "_CUPS_OTYPE_REFRESH" // Refresh token +}; +#endif // DEBUG + +static const char *github_metadata = // Github.com OAuth metadata +"{\ +\"issuer\":\"https://github.com\",\ +\"authorization_endpoint\":\"https://github.com/login/oauth/authorize\",\ +\"token_endpoint\":\"https://github.com/login/oauth/access_token\",\ +\"token_endpoint_auth_methods_supported\":[\"client_secret_basic\"],\ +\"scopes_supported\":[\"repo\",\"repo:status\",\"repo_deployment\",\"public_repo\",\"repo:invite\",\"security_events\",\"admin:repo_hook\",\"write:repo_hook\",\"read:repo_hook\",\"admin:org\",\"write:org\",\"read:org\",\"admin:public_key\",\"write:public_key\",\"read:public_key\",\"admin:org_hook\",\"gist\",\"notifications\",\"user\",\"read:user\",\"user:email\",\"user:follow\",\"project\",\"read:project\",\"delete_repo\",\"write:packages\",\"read:packages\",\"delete:packages\",\"admin.gpg_key\",\"write:gpg_key\",\"read:gpg_key\",\"codespace\",\"workflow\"],\ +\"response_types_supported\":[\"code\"],\ +\"grant_types_supported\":[\"authorization_code\",\"refresh_token\",\"\",\"urn:ietf:params:oauth:grant-type:device_code\"],\ +\"device_authorization_endpoint\":\"https://github.com/login/device/code\",\ +}"; + + +// +// Local functions... +// + +static char *oauth_copy_response(http_t *http); +static cups_json_t *oauth_do_post(const char *ep, const char *content_type, const char *data); +static cups_json_t *oauth_get_jwks(const char *auth_uri, cups_json_t *metadata); +static char *oauth_load_value(const char *auth_uri, const char *secondary_uri, _cups_otype_t otype); +static char *oauth_make_path(char *buffer, size_t bufsize, const char *auth_uri, const char *secondary_uri, _cups_otype_t otype); +static char *oauth_make_software_id(char *buffer, size_t bufsize); +static bool oauth_metadata_contains(cups_json_t *metadata, const char *parameter, const char *value); +static void oauth_save_value(const char *auth_uri, const char *secondary_uri, _cups_otype_t otype, const char *value); +static bool oauth_set_error(cups_json_t *json, size_t num_form, cups_option_t *form); + + +// +// 'cupsOAuthClearTokens()' - Clear any cached authorization information. +// +// This function clears cached authorization information for the given +// Authorization Server "auth_uri" and Resource "resource_uri" combination. + +void +cupsOAuthClearTokens( + const char *auth_uri, // I - Authorization server URI + const char *resource_uri) // I - Resource server URI +{ + oauth_save_value(auth_uri, resource_uri, _CUPS_OTYPE_ACCESS, /*value*/NULL); + oauth_save_value(auth_uri, resource_uri, _CUPS_OTYPE_CODE_VERIFIER, NULL); + oauth_save_value(auth_uri, resource_uri, _CUPS_OTYPE_USER_ID, /*value*/NULL); + oauth_save_value(auth_uri, resource_uri, _CUPS_OTYPE_NONCE, NULL); + oauth_save_value(auth_uri, resource_uri, _CUPS_OTYPE_REFRESH, /*value*/NULL); +} + + +// +// 'cupsOAuthCopyAccessToken()' - Get a cached access token. +// +// This function makes a copy of a cached access token and any +// associated expiration time for the given Authorization Server "auth_uri" and +// Resource "resource_uri" combination. The returned access token must be freed +// using the `free` function. +// +// `NULL` is returned if no token is cached. +// + +char * // O - Access token +cupsOAuthCopyAccessToken( + const char *auth_uri, // I - Authorization Server URI + const char *resource_uri, // I - Resource URI + time_t *access_expires) // O - Access expiration time or `NULL` for don't care +{ + char *token, // Access token + *tokptr; // Pointer into token + + + // See if we have a token... + if (access_expires) + *access_expires = 0; + + if ((token = oauth_load_value(auth_uri, resource_uri, _CUPS_OTYPE_ACCESS)) != NULL) + { + if ((tokptr = strchr(token, '\n')) != NULL) + { + *tokptr++ = '\0'; + + if (access_expires) + *access_expires = strtol(tokptr, NULL, 10); + } + } + + return (token); +} + + +// +// 'cupsOAuthCopyClientId()' - Get the cached `client_id` value. +// +// This function makes a copy of the cached `client_id` value for a given +// Authorization Server "auth_uri" and Redirection URI "resource_uri". The +// returned value must be freed using the `free` function. +// +// `NULL` is returned if no `client_id` is cached. +// + +char * // O - `client_id` value +cupsOAuthCopyClientId( + const char *auth_uri, // I - Authorization Server URI + const char *redirect_uri) // I - Redirection URI +{ + return (oauth_load_value(auth_uri, redirect_uri, _CUPS_OTYPE_CLIENT_ID)); +} + + +// +// 'cupsOAuthCopyRefreshToken()' - Get a cached refresh token. +// +// This function makes a copy of a cached refresh token for the given +// given Authorization Server "auth_uri" and Resource "resource_uri" +// combination. The returned refresh token must be freed using the `free` +// function. +// +// `NULL` is returned if no refresh token is cached. +// + +char * // O - Refresh token +cupsOAuthCopyRefreshToken( + const char *auth_uri, // I - Authorization Server URI + const char *resource_uri) // I - Resource URI +{ + return (oauth_load_value(auth_uri, resource_uri, _CUPS_OTYPE_REFRESH)); +} + + +// +// 'cupsOAuthCopyUserId()' - Get cached user identification information. +// +// This function makes a copy of cached user identification information for the +// given Authorization Server "auth_uri" and Resource "resource_uri" +// combination. The returned user information must be freed using the +// @link cupsJWTDelete@ function. +// +// `NULL` is returned if no identification information is cached. +// + +cups_jwt_t * // O - Identification information +cupsOAuthCopyUserId( + const char *auth_uri, // I - Authorization Server URI + const char *resource_uri) // I - Resource URI +{ + char *value; // ID token value + cups_jwt_t *jwt; // JWT value + + + value = oauth_load_value(auth_uri, resource_uri, _CUPS_OTYPE_USER_ID); + jwt = cupsJWTImportString(value, CUPS_JWS_FORMAT_COMPACT); + + free(value); + return (jwt); +} + + +// +// 'cupsOAuthGetAuthorizationCode()' - Authorize access using a web browser. +// +// This function performs a local/"native" OAuth authorization flow to obtain an +// authorization code for use with the @link cupsOAuthGetTokens@ function. +// +// The "auth_uri" parameter specifies the URI for the OAuth Authorization +// Server. The "metadata" parameter specifies the Authorization Server metadata +// as obtained using @link cupsOAuthCopyMetadata@ and/or +// @link cupsOAuthGetMetadata@. +// +// The "resource_uri" parameter specifies the URI for a resource (printer, web +// file, etc.) that you which to access. +// +// The "scopes" parameter specifies zero or more whitespace-delimited scope +// names to request during authorization. The list of supported scope names are +// available from the Authorization Server metadata, for example: +// +// The "redirect_uri" parameter specifies a 'http:' URL with a listen address, +// port, and path to use. If `NULL`, 127.0.0.1 on a random port is used with a +// path of "/". +// +// ``` +// cups_json_t *metadata = cupsOAuthGetMetadata(auth_uri); +// cups_json_t *scopes_supported = cupsJSONFind(metadata, "scopes_supported"); +// ``` +// +// The returned authorization code must be freed using the `free` function. +// + +char * // O - Authorization code or `NULL` on error +cupsOAuthGetAuthorizationCode( + const char *auth_uri, // I - Authorization Server URI + cups_json_t *metadata, // I - Authorization Server metadata + const char *resource_uri, // I - Resource URI + const char *scopes, // I - Space-delimited scopes + const char *redirect_uri) // I - Redirect URI or `NULL` for default +{ + char *client_id = NULL, // `client_id` value + *code_verifier = NULL, // Code verifier string + *nonce = NULL, // Nonce string + *state = NULL, // State string + *url = NULL, // URL for authorization page + *scopes_supported = NULL; + // Supported scopes + char resource[256], // Resource path + final_uri[1024]; // Final redirect URI + size_t resourcelen; // Length of resource path + http_addr_t addr; // Loopback listen address + int port; // Port number + int fd = -1; // Listen file descriptor + fd_set input; // Input file descriptors for select() + struct timeval timeout; // Timeout for select() + time_t endtime; // End time + http_t *http; // HTTP client + char *auth_code = NULL; // Authorization code + + + // Range check input... + DEBUG_printf("cupsOAuthGetAuthorizationCode(auth_uri=\"%s\", metadata=%p, resource_uri=\"%s\", scopes=\"%s\", redirect_uri=\"%s\")", auth_uri, (void *)metadata, resource_uri, scopes, redirect_uri); + + if (!auth_uri || !metadata || cupsJSONGetString(cupsJSONFind(metadata, "authorization_endpoint")) == NULL) + return (NULL); + + // Get the client_id value... + if ((client_id = cupsOAuthCopyClientId(auth_uri, redirect_uri ? redirect_uri : CUPS_OAUTH_REDIRECT_URI)) == NULL) + client_id = cupsOAuthGetClientId(auth_uri, metadata, redirect_uri ? redirect_uri : CUPS_OAUTH_REDIRECT_URI, /*logo_uri*/NULL, /*tos_uri*/NULL); + + if (!client_id) + return (NULL); + + // Listen on a local port... + if (redirect_uri) + { + // Use the host/port/resource from the URI + char scheme[32], // URL scheme + userpass[256], // Username:password (ignored) + host[256]; // Hostname + + if (httpSeparateURI(HTTP_URI_CODING_ALL, redirect_uri, scheme, sizeof(scheme), userpass, sizeof(userpass), host, sizeof(host), &port, resource, sizeof(resource)) < HTTP_URI_STATUS_OK || strcmp(scheme, "http")) + { + DEBUG_printf("1cupsOAuthGetAuthorizationCode: Bad redirect_uri '%s'.", redirect_uri); + _cupsSetError(IPP_STATUS_ERROR_INTERNAL, strerror(EINVAL), false); + goto done; + } + + memset(&addr, 0, sizeof(addr)); +#ifdef __APPLE__ + addr.ipv4.sin_len = sizeof(struct sockaddr_in); +#endif // __APPLE__ + addr.ipv4.sin_family = AF_INET; + + if (!strcmp(host, "localhost") || !strcmp(host, "127.0.0.1")) + addr.ipv4.sin_addr.s_addr = htonl(0x7f000001); + + fd = httpAddrListen(&addr, port); + + cupsConcatString(resource, "?", sizeof(resource)); + } + else + { + // Find the next available port on 127.0.0.1... + memset(&addr, 0, sizeof(addr)); +#ifdef __APPLE__ + addr.ipv4.sin_len = sizeof(struct sockaddr_in); +#endif // __APPLE__ + addr.ipv4.sin_family = AF_INET; + addr.ipv4.sin_addr.s_addr = htonl(0x7f000001); + + for (port = 10000; port < 11000; port ++) + { + if ((fd = httpAddrListen(&addr, port)) >= 0) + break; + } + + // Save the redirect URI and resource... + cupsCopyString(resource, _CUPS_OAUTH_REDIRECT_PATH, sizeof(resource)); + snprintf(final_uri, sizeof(final_uri), _CUPS_OAUTH_REDIRECT_FORMAT, port); + redirect_uri = final_uri; + } + + DEBUG_printf("1cupsOAuthGetAuthorizationCode: Listen socket for port %d is %d (%s)", port, fd, strerror(errno)); + + if (fd < 0) + goto done; + + resourcelen = strlen(resource); + + // Point redirection to the local port... + oauth_save_value(auth_uri, resource_uri, _CUPS_OTYPE_REDIRECT_URI, redirect_uri); + + // Make state and code verification strings... + if (oauth_metadata_contains(metadata, "code_challenge_methods_supported", "S256")) + code_verifier = cupsOAuthMakeBase64Random(128); + else + code_verifier = NULL; + + if (oauth_metadata_contains(metadata, "scopes_supported", "openid")) + nonce = cupsOAuthMakeBase64Random(16); + else + nonce = NULL; + + state = cupsOAuthMakeBase64Random(16); + + if (!state) + goto done; + + if (!scopes) + { + cups_json_t *values; // Parameter values + + if ((values = cupsJSONFind(metadata, "scopes_supported")) != NULL) + { + // Convert scopes_supported to a string... + size_t i, // Looping var + count, // Number of values + length = 0; // Length of string + cups_json_t *current; // Current value + + for (i = 0, count = cupsJSONGetCount(values); i < count; i ++) + { + current = cupsJSONGetChild(values, i); + + if (cupsJSONGetType(current) == CUPS_JTYPE_STRING) + length += strlen(cupsJSONGetString(current)) + 1; + } + + if (length > 0 && (scopes_supported = malloc(length)) != NULL) + { + // Copy the scopes to a string with spaces between them... + char *ptr; // Pointer into value + + for (i = 0, ptr = scopes_supported; i < count; i ++) + { + current = cupsJSONGetChild(values, i); + + if (cupsJSONGetType(current) == CUPS_JTYPE_STRING) + { + if (i) + *ptr++ = ' '; + + cupsCopyString(ptr, cupsJSONGetString(current), length - (size_t)(ptr - scopes_supported)); + ptr += strlen(ptr); + } + } + + // Use the supported scopes in the request... + scopes = scopes_supported; + } + } + } + + // Get the authorization URL... + if ((url = cupsOAuthMakeAuthorizationURL(auth_uri, metadata, resource_uri, scopes, client_id, code_verifier, nonce, redirect_uri, state)) == NULL) + goto done; + + // Open a web browser with the authorization page... +#ifdef __APPLE__ + CFURLRef cfurl; // CoreFoundation URL + int error = 1; // Open status + + if ((cfurl = CFURLCreateWithBytes(kCFAllocatorDefault, (const UInt8 *)url, (CFIndex)strlen(url), kCFStringEncodingASCII, NULL)) != NULL) + { + error = LSOpenCFURLRef(cfurl, NULL); + + CFRelease(cfurl); + } + + if (error != noErr) + goto done; + +#elif defined(_WIN32) + if (_spawnl(_P_WAIT, "start", "", url, NULL)) + goto done; + +#else + pid_t pid = 0; // Process ID + int estatus; // Exit status + const char *xdg_open_argv[3]; // xdg-open arguments + + xdg_open_argv[0] = "xdg-open"; + xdg_open_argv[1] = url; + xdg_open_argv[2] = NULL; + + if (posix_spawnp(&pid, "xdg-open", NULL, NULL, (char * const *)xdg_open_argv, environ)) + goto done; // Couldn't run xdg-open + else if (waitpid(pid, &estatus, 0)) + goto done; // Couldn't get exit status + else if (estatus) + goto done; // Non-zero exit status +#endif // __APPLE__ + + // Listen for connections up to 60 seconds... + endtime = time(NULL) + 60; + + while (auth_code == NULL && time(NULL) < endtime) + { + timeout.tv_sec = 1; + timeout.tv_usec = 0; + + FD_ZERO(&input); + FD_SET(fd, &input); + + if (select(fd + 1, &input, /*writefds*/NULL, /*errorfds*/NULL, &timeout) > 0 && FD_ISSET(fd, &input)) + { + // Try accepting a connection... + if ((http = httpAcceptConnection(fd, true)) != NULL) + { + // Respond to HTTP requests... + while (auth_code == NULL && time(NULL) < endtime && httpWait(http, 1000)) + { + char reqres[4096], // Resource path + message[2048]; // Response message + http_state_t hstate; // HTTP request state + http_status_t hstatus; // HTTP request status + const char *htype = NULL, // HTTP response content type + *hbody = NULL; // HTTP response body + + // Get the request header... + if ((hstate = httpReadRequest(http, reqres, sizeof(reqres))) == HTTP_STATE_WAITING) + continue; + else if (hstate == HTTP_STATE_ERROR || hstate == HTTP_STATE_UNKNOWN_METHOD || hstate == HTTP_STATE_UNKNOWN_VERSION) + break; + + // Read incoming headers until the status changes... + do + { + hstatus = httpUpdate(http); + } + while (hstatus == HTTP_STATUS_CONTINUE && time(NULL) < endtime); + + // Stop on error... + if (hstatus != HTTP_STATUS_OK) + break; + + // Process the request... + switch (hstate) + { + default : + hstatus = HTTP_STATUS_METHOD_NOT_ALLOWED; + break; + + case HTTP_STATE_HEAD : + if (!strncmp(reqres, resource, resourcelen)) + { + // Respond that the content will be HTML... + htype = "text/html"; + } + else + { + // Resource doesn't exist... + hstatus = HTTP_STATUS_NOT_FOUND; + } + break; + + case HTTP_STATE_GET : + if (!strncmp(reqres, resource, resourcelen)) + { + // Collect form parameters from resource... + const char *code_value, // Authoziation code value + *error_code, // Error code + *error_desc, // Error description + *state_value; // State value + size_t num_form; // Number of form variables + cups_option_t *form = NULL; // Form variables + + num_form = cupsFormDecode(reqres + resourcelen, &form); + code_value = cupsGetOption("code", num_form, form); + error_code = cupsGetOption("error", num_form, form); + error_desc = cupsGetOption("error_description", num_form, form); + state_value = cupsGetOption("state", num_form, form); + + if (code_value && state_value && !strcmp(state, state_value)) + { + // Got a code and the correct state value, copy the code and + // save out code_verifier and nonce values... + auth_code = strdup(code_value); + + oauth_save_value(auth_uri, resource_uri, _CUPS_OTYPE_CODE_VERIFIER, code_verifier); + oauth_save_value(auth_uri, resource_uri, _CUPS_OTYPE_NONCE, nonce); + + hbody = "\n" + "\n" + " Authorization Complete\n" + " \n" + "

Authorization Complete

\n" + "

You may now close this window/tab.

\n" + " \n" + " \n" + "\n"; + } + else + { + // Got an error... + hbody = message; + snprintf(message, sizeof(message), + "\n" + "\n" + " Authorization Failed\n" + " \n" + "

Authorization Failed

\n" + "

%s: %s

\n" + " \n" + "\n", error_code ? error_code : "bad_response", error_desc ? error_desc : "The authorization server's response was not understood."); + } + cupsFreeOptions(num_form, form); + + // Respond accordingly... + htype = "text/html"; + } + else + { + // Resource doesn't exist... + hstatus = HTTP_STATUS_NOT_FOUND; + htype = "text/plain"; + hbody = "This is not the resource you are looking for.\n"; + } + break; + } + + // Send response... + httpClearFields(http); + if (hstatus >= HTTP_STATUS_BAD_REQUEST) + httpSetField(http, HTTP_FIELD_CONNECTION, "close"); + if (htype) + httpSetField(http, HTTP_FIELD_CONTENT_TYPE, htype); + if (hbody) + httpSetLength(http, strlen(hbody)); + httpWriteResponse(http, hstatus); + + if (hbody) + httpWrite(http, hbody, strlen(hbody)); + + // Stop on error... + if (hstatus != HTTP_STATUS_OK) + break; + } + + // Close the client connection... + httpClose(http); + } + } + } + + done: + + // Free strings, close the listen socket, and return... + if (fd >= 0) + httpAddrClose(&addr, fd); + + free(client_id); + free(code_verifier); + free(nonce); + free(scopes_supported); + free(state); + free(url); + + return (auth_code); +} + + +// +// 'cupsOAuthGetClientId()' - Register a client application and get its ID. +// +// This function registers a client application with the specified OAuth +// Authorization Server. +// +// The "auth_uri" parameter specifies the URI for the OAuth Authorization +// Server. The "metadata" parameter specifies the Authorization Server metadata +// as obtained using @link cupsOAuthCopyMetadata@ and/or +// @link cupsOAuthGetMetadata@. +// +// The "redirect_uri" argument specifies the URL to use for providing +// authorization results to a WWW application. +// +// The "logo_uri" argument specifies a public URL for the logo of your +// application, while the "tos_uri" specifies a public URL for the terms of +// service for your application. +// +// The returned "client_id" string must be freed using the `free` function. +// +// *Note*: This function should only be used to register WWW applications. The +// @link cupsOAuthGetAuthorizationCode@ function handles registration of +// local/"native" applications for you. +// + +char * // O - `client_id` string or `NULL` on error +cupsOAuthGetClientId( + const char *auth_uri, // I - Authorization Server URI + cups_json_t *metadata, // I - Authorization Server metadata + const char *redirect_uri, // I - Redirection URL + const char *logo_uri, // I - Logo URL or `NULL` for none + const char *tos_uri) // I - Terms-of-service URL or `NULL` for none +{ + const char *registration_ep; // Registration endpoint + char software_id[37]; // `software_id` string + char *client_id = NULL; // `client_id` string + char *req_data = NULL; // JSON request data + cups_json_t *request, // JSON request variables + *response, // JSON response + *jarray; // JSON array + const char *value; // JSON value + + + // Range check input... + if (!auth_uri || !metadata || (registration_ep = cupsJSONGetString(cupsJSONFind(metadata, "registration_endpoint"))) == NULL || !redirect_uri) + return (NULL); + + // Prepare JSON data to register the client application... + request = cupsJSONNew(/*parent*/NULL, /*after*/NULL, CUPS_JTYPE_OBJECT); + cupsJSONNewString(request, cupsJSONNewKey(request, /*after*/NULL, "client_name"), "CUPS"); + cupsJSONNewString(request, cupsJSONNewKey(request, /*after*/NULL, "client_uri"), "https://openprinting.github.io/cups/"); + if (logo_uri) + cupsJSONNewString(request, cupsJSONNewKey(request, /*after*/NULL, "logo_uri"), logo_uri); + cupsJSONNewString(request, cupsJSONNewKey(request, /*after*/NULL, "software_id"), oauth_make_software_id(software_id, sizeof(software_id))); + cupsJSONNewString(request, cupsJSONNewKey(request, /*after*/NULL, "software_version"), CUPS_SVERSION); + jarray = cupsJSONNew(request, cupsJSONNewKey(request, /*after*/NULL, "redirect_uris"), CUPS_JTYPE_ARRAY); + cupsJSONNewString(jarray, /*after*/NULL, redirect_uri); + if (tos_uri) + cupsJSONNewString(request, cupsJSONNewKey(request, /*after*/NULL, "tos_uri"), tos_uri); + + req_data = cupsJSONExportString(request); + cupsJSONDelete(request); + + if (!req_data) + goto done; + + if ((response = oauth_do_post(registration_ep, "application/json", req_data)) == NULL) + goto done; + + if ((value = cupsJSONGetString(cupsJSONFind(response, "client_id"))) != NULL) + { + if ((client_id = strdup(value)) != NULL) + { + // Save client_id and optional client_secret... + oauth_save_value(auth_uri, redirect_uri, _CUPS_OTYPE_CLIENT_ID, value); + oauth_save_value(auth_uri, redirect_uri, _CUPS_OTYPE_CLIENT_SECRET, cupsJSONGetString(cupsJSONFind(response, "client_secret"))); + } + } + + cupsJSONDelete(response); + + // Return whatever we got... + done: + + free(req_data); + + return (client_id); +} + + +// +// 'cupsOAuthGetMetadata()' - Get the metadata for an Authorization Server. +// +// This function gets the metadata for the specified Authorization Server URI +// "auth_uri". Metadata is cached per-user for better performance. +// +// The returned metadata must be freed using the @link cupsJSONDelete@ function. +// + +cups_json_t * // O - JSON metadata or `NULL` on error +cupsOAuthGetMetadata( + const char *auth_uri) // I - Authorization Server URI +{ + char filename[1024]; // Local metadata filename + struct stat fileinfo; // Local metadata file info + char filedate[256], // Local metadata modification date + host[256], // Hostname + resource[256]; // Resource path + int port; // Port to use + http_t *http; // Connection to server + http_status_t status = HTTP_STATUS_NOT_FOUND; + // Request status + size_t i; // Looping var + static const char * const paths[] = // Metadata paths + { + "/.well-known/oauth-authorization-server", + "/.well-known/openid-configuration" + }; + + + DEBUG_printf("cupsOAuthGetMetadata(auth_uri=\"%s\")", auth_uri); + + // Special-cases... + if (!strcmp(auth_uri, "https://github.com")) + return (cupsJSONImportString(github_metadata)); + + // Get existing metadata... + if (!oauth_make_path(filename, sizeof(filename), auth_uri, /*secondary_uri*/NULL, _CUPS_OTYPE_METADATA)) + return (NULL); + + if (stat(filename, &fileinfo)) + memset(&fileinfo, 0, sizeof(fileinfo)); + + if (fileinfo.st_mtime) + httpGetDateString2(fileinfo.st_mtime, filedate, sizeof(filedate)); + else + filedate[0] = '\0'; + + // Don't bother connecting if the metadata was updated recently... + if ((time(NULL) - fileinfo.st_mtime) <= 60) + goto load_metadata; + + // Try getting the metadata... + if ((http = httpConnectURI(auth_uri, host, sizeof(host), &port, resource, sizeof(resource), /*blocking*/true, /*msec*/30000, /*cancel*/NULL, /*require_ca*/true)) == NULL) + return (NULL); + + for (i = 0; i < (sizeof(paths) / sizeof(paths[0])); i ++) + { + cupsCopyString(resource, paths[i], sizeof(resource)); + + do + { + if (!_cups_strcasecmp(httpGetField(http, HTTP_FIELD_CONNECTION), "close")) + { + httpClearFields(http); + if (!httpConnectAgain(http, /*msec*/30000, /*cancel*/NULL)) + { + status = HTTP_STATUS_ERROR; + break; + } + } + + httpClearFields(http); + + httpSetField(http, HTTP_FIELD_IF_MODIFIED_SINCE, filedate); + if (!httpWriteRequest(http, "GET", resource)) + { + if (!httpConnectAgain(http, 30000, NULL) || !httpWriteRequest(http, "GET", resource)) + { + status = HTTP_STATUS_ERROR; + break; + } + } + + while ((status = httpUpdate(http)) == HTTP_STATUS_CONTINUE) + ; + + if (status >= HTTP_STATUS_MULTIPLE_CHOICES && status <= HTTP_STATUS_SEE_OTHER) + { + // Redirect + char lscheme[32], // Location scheme + luserpass[256], // Location user:password (not used) + lhost[256], // Location hostname + lresource[256]; // Location resource path + int lport; // Location port + + if (httpSeparateURI(HTTP_URI_CODING_ALL, httpGetField(http, HTTP_FIELD_LOCATION), lscheme, sizeof(lscheme), luserpass, sizeof(luserpass), lhost, sizeof(lhost), &lport, lresource, sizeof(lresource)) < HTTP_URI_STATUS_OK) + break; // Don't redirect to an invalid URI + + if (_cups_strcasecmp(host, lhost) || port != lport) + break; // Don't redirect off this host + + // Redirect to a local resource... + cupsCopyString(resource, lresource, sizeof(resource)); + } + } + while (status >= HTTP_STATUS_MULTIPLE_CHOICES && status <= HTTP_STATUS_SEE_OTHER); + + if (status == HTTP_STATUS_NOT_MODIFIED) + { + // Metadata isn't changed, stop now... + break; + } + else if (status == HTTP_STATUS_OK) + { + // Copy the metadata to the file... + int fd; // Local metadata file + char buffer[8192]; // Copy buffer + ssize_t bytes; // Bytes read + + if ((fd = open(filename, O_CREAT | O_TRUNC | O_WRONLY | O_NOFOLLOW, 0600)) < 0) + { + httpFlush(http); + break; + } + + while ((bytes = httpRead(http, buffer, sizeof(buffer))) > 0) + write(fd, buffer, (size_t)bytes); + + close(fd); + break; + } + } + + if (status != HTTP_STATUS_OK && status != HTTP_STATUS_NOT_MODIFIED) + { + // Remove old cached data... + unlink(filename); + } + + httpClose(http); + + // Return the cached metadata, if any... + load_metadata: + + return (cupsJSONImportFile(filename)); +} + + +// +// 'cupsOAuthGetTokens()' - Obtain access and refresh tokens. +// +// This function obtains a access and refresh tokens from an OAuth Authorization +// Server. OpenID Authorization Servers also provide user identification +// information. +// +// The "auth_uri" parameter specifies the URI for the OAuth Authorization +// Server. The "metadata" parameter specifies the Authorization Server metadata +// as obtained using @link cupsOAuthCopyMetadata@ and/or +// @link cupsOAuthGetMetadata@. +// +// The "resource_uri" parameter specifies the URI for a resource (printer, web +// file, etc.) that you which to access. +// +// The "grant_code" parameter specifies the code or token to use while the +// "grant_type" parameter specifies the type of code: +// +// - `CUPS_OGRANT_AUTHORIZATION_CODE`: A user authorization grant code. +// - `CUPS_OGRANT_DEVICE_CODE`: A device authorization grant code. +// - `CUPS_OGRANT_REFRESH_TOKEN`: A refresh token. +// +// The "redirect_uri" specifies the redirection URI used to obtain the code. The +// constant `CUPS_OAUTH_REDIRECT_URI` should be used for codes obtained using +// the @link cupsOAuthGetAuthorizationCode@ function. +// +// When successful, the access token and expiration time are returned. The +// access token must be freed using the `free` function. The new refresh token +// and any user ID information can be obtained using the +// @link cupsOAuthCopyRefreshToken@ and @link cupsOAuthCopyUserId@ functions +// respectively. +// + +char * // O - Access token or `NULL` on error +cupsOAuthGetTokens( + const char *auth_uri, // I - Authorization Server URI + cups_json_t *metadata, // I - Authorization Server metadata + const char *resource_uri, // I - Resource URI + const char *grant_code, // I - Authorization code or refresh token + cups_ogrant_t grant_type, // I - Grant code type + const char *redirect_uri, // I - Redirect URI + time_t *access_expires) // O - Expiration time for access token +{ + const char *token_ep; // Token endpoint + char *value, // Prior value + *nonce = NULL; // Prior nonce value + size_t num_form = 0; // Number of form variables + cups_option_t *form = NULL; // Form variables + char *request = NULL; // Form request data + cups_json_t *response = NULL; // JSON response variables + const char *access_value = NULL, // access_token + *id_value = NULL, // id_token + *refresh_value = NULL; // refresh_token + double expires_in; // expires_in value + time_t access_expvalue; // Expiration time for access_token + cups_jwt_t *jwt = NULL; // JWT of the id_token + const char *jnonce; // Nonce value from the JWT + char *access_token = NULL; // Access token + static const char * const grant_types[] = + { // Grant type strings + "authorization_code", + "urn:ietf:params:oauth:grant-type:device_code", + "refresh_token" + }; + + + DEBUG_printf("cupsOAuthGetTokens(auth_uri=\"%s\", metadata=%p, resource_uri=\"%s\", grant_code=\"%s\", grant_type=%d, redirect_uri=\"%s\", access_expires=%p)", auth_uri, (void *)metadata, resource_uri, grant_code, grant_type, redirect_uri, (void *)access_expires); + + // Range check input... + if (access_expires) + *access_expires = 0; + + if (!auth_uri || !metadata || (token_ep = cupsJSONGetString(cupsJSONFind(metadata, "token_endpoint"))) == NULL || !grant_code || !redirect_uri) + return (NULL); + + // Prepare form data to get an access token... + num_form = cupsAddOption("grant_type", grant_types[grant_type], num_form, &form); + num_form = cupsAddOption("code", grant_code, num_form, &form); + + if (!strcmp(redirect_uri, CUPS_OAUTH_REDIRECT_URI) && (value = oauth_load_value(auth_uri, resource_uri, _CUPS_OTYPE_REDIRECT_URI)) != NULL) + { + DEBUG_printf("1cupsOAuthGetTokens: redirect_uri=\"%s\"", value); + num_form = cupsAddOption("redirect_uri", value, num_form, &form); + free(value); + } + else + { + num_form = cupsAddOption("redirect_uri", redirect_uri, num_form, &form); + } + + if ((value = oauth_load_value(auth_uri, redirect_uri, _CUPS_OTYPE_CLIENT_ID)) != NULL) + { + DEBUG_printf("1cupsOAuthGetTokens: client_id=\"%s\"", value); + num_form = cupsAddOption("client_id", value, num_form, &form); + free(value); + } + + if ((value = oauth_load_value(auth_uri, redirect_uri, _CUPS_OTYPE_CLIENT_SECRET)) != NULL) + { + DEBUG_printf("1cupsOAuthGetTokens: client_secret=\"%s\"", value); + num_form = cupsAddOption("client_secret", value, num_form, &form); + free(value); + } + + if ((value = oauth_load_value(auth_uri, resource_uri, _CUPS_OTYPE_CODE_VERIFIER)) != NULL) + { + DEBUG_printf("1cupsOAuthGetTokens: code_verifier=\"%s\"", value); + num_form = cupsAddOption("code_verifier", value, num_form, &form); + free(value); + } + + request = cupsFormEncode(/*url*/NULL, num_form, form); + cupsFreeOptions(num_form, form); + + if (!request) + goto done; + + if ((response = oauth_do_post(token_ep, "application/x-www-form-urlencoded", request)) == NULL) + goto done; + + access_value = cupsJSONGetString(cupsJSONFind(response, "access_token")); + expires_in = cupsJSONGetNumber(cupsJSONFind(response, "expires_in")); + id_value = cupsJSONGetString(cupsJSONFind(response, "id_token")); + refresh_value = cupsJSONGetString(cupsJSONFind(response, "refresh_token")); + + if (id_value) + { + // Validate the JWT + cups_json_t *jwks; // JWT key set + bool valid; // Valid id_token? + const char *at_hash; // at_hash claim value + + jwt = cupsJWTImportString(id_value, CUPS_JWS_FORMAT_COMPACT); + jnonce = cupsJWTGetClaimString(jwt, "nonce"); + nonce = oauth_load_value(auth_uri, resource_uri, _CUPS_OTYPE_NONCE); + + // Check nonce + if (!jwt || (jnonce && nonce && strcmp(jnonce, nonce))) + goto done; + + // Validate id_token against the Authorization Server's JWKS + if ((jwks = oauth_get_jwks(auth_uri, metadata)) == NULL) + goto done; + + valid = cupsJWTHasValidSignature(jwt, jwks); + DEBUG_printf("1cupsOAuthGetTokens: valid=%s", valid ? "true" : "false"); + cupsJSONDelete(jwks); + if (!valid) + goto done; + + // Validate the at_hash claim string against access_token value + if (access_value && (at_hash = cupsJWTGetClaimString(jwt, "at_hash")) != NULL) + { + unsigned char sha256[32], // Hash of the access_token value + at_hash_buffer[32]; // at_hash bytes + size_t at_hash_bytes = sizeof(at_hash_buffer); + // Number of at_hash bytes + + cupsHashData("sha2-256", access_value, strlen(access_value), sha256, sizeof(sha256)); + httpDecode64_3((char *)at_hash_buffer, &at_hash_bytes, at_hash, /*end*/NULL); + if (at_hash_bytes != 16 || memcmp(sha256, at_hash_buffer, 16)) + { + DEBUG_puts("1cupsOAuthGetTokens: at_hash doesn't match SHA-256 of access_token."); + goto done; + } + } + } + + if (expires_in > 0.0) + access_expvalue = time(NULL) + (long)expires_in; + else + access_expvalue = 0; + + cupsOAuthSaveTokens(auth_uri, resource_uri, access_value, access_expvalue, id_value, refresh_value); + + if (access_value) + access_token = strdup(access_value); + + if (access_expires) + *access_expires = access_expvalue; + + // Return whatever we got... + done: + + cupsJSONDelete(response); + cupsJWTDelete(jwt); + free(nonce); + free(request); + + return (access_token); +} + + +// +// 'cupsOAuthMakeAuthorizationURL()' - Make an authorization URL. +// +// This function makes an authorization URL for the specified authorization +// server and resource. +// +// The "auth_uri" parameter specifies the URI for the OAuth Authorization +// Server. The "metadata" parameter specifies the Authorization Server metadata +// as obtained using @link cupsOAuthCopyMetadata@ and/or +// @link cupsOAuthGetMetadata@. +// +// The "resource_uri" parameter specifies the URI for a resource (printer, web +// file, etc.) that you which to access. +// +// The "scopes" parameter specifies zero or more whitespace-delimited scope +// names to request during authorization. The list of supported scope names are +// available from the Authorization Server metadata, for example: +// +// ``` +// cups_json_t *metadata = cupsOAuthGetMetadata(auth_uri); +// cups_json_t *scopes_supported = cupsJSONFind(metadata, "scopes_supported"); +// ``` +// +// The "client_id" parameter specifies the client identifier obtained using +// @link cupsOAuthCopyClientId@ and/or @link cupsOAuthGetClientId@. +// +// The "client_id" parameter is the string returned by +// @link cupsOAuthCopyClientId@ or @link cupsOAuthGetClientId@. +// +// The "code_verifier" parameter specifies a random Base64URL-encoded string +// that is used by the Proof Key for Code Exchange [RFC7636] extension to help +// secure the authorization flow. The @link cupsOAuthMakeBase64Random@ function +// can be used to generate this string. +// +// The "nonce" parameter specifies a random Base64URL-encoded string that is +// used by OpenID to validate the ID token. The @link cupsOAuthMakeBase64Random@ +// function can be used to generate this string. +// +// The "redirect_uri" parameter specifies the URI that will receive the +// authorization grant code. +// +// The "state" parameter is a unique (random) identifier for the authorization +// request. It is provided to the redirection URI as a form parameter. +// + +char * // O - Authorization URL +cupsOAuthMakeAuthorizationURL( + const char *auth_uri, // I - Authorization Server URI + cups_json_t *metadata, // I - Authorization Server metadata + const char *resource_uri, // I - Resource URI + const char *scopes, // I - Space-delimited scope(s) + const char *client_id, // I - Client ID + const char *code_verifier, // I - Code verifier string + const char *nonce, // I - Nonce + const char *redirect_uri, // I - Redirection URI + const char *state) // I - State +{ + const char *authorization_ep; // Authorization endpoint + unsigned char sha256[32]; // SHA-256 hash of code verifier + char code_challenge[64]; // Hashed code verifier string + size_t num_vars = 0; // Number of form variables + cups_option_t *vars = NULL; // Form variables + char *url; // URL for authorization page + + + // Range check input... + if (!auth_uri || !metadata || (authorization_ep = cupsJSONGetString(cupsJSONFind(metadata, "authorization_endpoint"))) == NULL || !redirect_uri || !client_id) + return (NULL); + + // Make the authorization URL using the information supplied... + if (oauth_metadata_contains(metadata, "response_type_supported", "code id_token")) + num_vars = cupsAddOption("response_type", "code id_token", num_vars, &vars); + else + num_vars = cupsAddOption("response_type", "code", num_vars, &vars); + + num_vars = cupsAddOption("client_id", client_id, num_vars, &vars); + num_vars = cupsAddOption("redirect_uri", redirect_uri, num_vars, &vars); + + if (code_verifier && oauth_metadata_contains(metadata, "code_challenge_methods_supported", "S256")) + { + oauth_save_value(auth_uri, resource_uri, _CUPS_OTYPE_CODE_VERIFIER, /*value*/NULL); + + cupsHashData("sha2-256", code_verifier, strlen(code_verifier), sha256, sizeof(sha256)); + httpEncode64_3(code_challenge, sizeof(code_challenge), (char *)sha256, sizeof(sha256), true); + num_vars = cupsAddOption("code_challenge", code_challenge, num_vars, &vars); + num_vars = cupsAddOption("code_challenge_method", "S256", num_vars, &vars); + } + + if (nonce && oauth_metadata_contains(metadata, "scopes_supported", "openid")) + { + oauth_save_value(auth_uri, resource_uri, _CUPS_OTYPE_NONCE, /*value*/NULL); + + num_vars = cupsAddOption("nonce", nonce, num_vars, &vars); + } + + if (resource_uri) + num_vars = cupsAddOption("resource", resource_uri, num_vars, &vars); + + if (scopes) + num_vars = cupsAddOption("scope", scopes, num_vars, &vars); + + if (state) + num_vars = cupsAddOption("state", state, num_vars, &vars); + + url = cupsFormEncode(authorization_ep, num_vars, vars); + + cupsFreeOptions(num_vars, vars); + + return (url); +} + + +// +// 'cupsOAuthMakeBase64Random()' - Make a random data string. +// +// This function creates a string containing random data that has been Base64URL +// encoded. "len" specifies the number of random bytes to include in the string. +// The returned string must be freed using the `free` function. +// + +char * // O - Random string +cupsOAuthMakeBase64Random(size_t len) // I - Number of bytes +{ + size_t i; // Looping var + char bytes[768], // Random bytes + base64url[1025]; // Random string + + + // Range check input... + len = len * 3 / 4; + + if (len < 1) + len = 1; + else if (len > sizeof(bytes)) + len = sizeof(bytes); + + // Fill out random bytes and convert it to Base64URL... + for (i = 0; i < len; i ++) + bytes[i] = (char)cupsGetRand(); + + httpEncode64_3(base64url, sizeof(base64url), bytes, len, /*url*/true); + + // Copy and return the random string... + return (strdup(base64url)); +} + + +// +// 'cupsOAuthSaveClientData()' - Save client_id and client_secret values. +// +// This function saves the "client_id" and "client_secret" values for the given +// Authorization Server "auth_uri" and redirection URI "redirect_uri". If the +// "client_id" is `NULL` then any saved values are deleted from the per-user +// store. +// + +void +cupsOAuthSaveClientData( + const char *auth_uri, // I - Authorization Server URI + const char *redirect_uri, // I - Redirection URI + const char *client_id, // I - client_id or `NULL` to delete + const char *client_secret) // I - client_secret value or `NULL` for none +{ + oauth_save_value(auth_uri, redirect_uri, _CUPS_OTYPE_CLIENT_ID, client_id); + oauth_save_value(auth_uri, redirect_uri, _CUPS_OTYPE_CLIENT_SECRET, client_secret); +} + + +// +// 'cupsOAuthSaveTokens()' - Save authorization and refresh tokens. +// +// This function saves the access token "access_token", user ID "user_id", and +// refresh token "refresh_token" values for the given Authorization Server +// "auth_uri" and resource "resource_uri". Specifying `NULL` for any of the +// values will delete the corresponding saved values from the per-user store. +// + +void +cupsOAuthSaveTokens( + const char *auth_uri, // I - Authorization Server URI + const char *resource_uri, // I - Resource URI + const char *access_token, // I - Access token or `NULL` to delete + time_t access_expires, // I - Access expiration time + const char *user_id, // I - User ID or `NULL` to delete + const char *refresh_token) // I - Refresh token or `NULL` to delete +{ + char temp[16384]; // Temporary string + + + // Access token... + if (access_token) + { + // Save access token... + snprintf(temp, sizeof(temp), "%s\n%ld\n", access_token, (long)access_expires); + oauth_save_value(auth_uri, resource_uri, _CUPS_OTYPE_ACCESS, temp); + } + else + { + // Remove access token + oauth_save_value(auth_uri, resource_uri, _CUPS_OTYPE_ACCESS, NULL); + } + + // User ID... + oauth_save_value(auth_uri, resource_uri, _CUPS_OTYPE_USER_ID, user_id); + + // Refresh token... + oauth_save_value(auth_uri, resource_uri, _CUPS_OTYPE_REFRESH, refresh_token); +} + + +// +// 'oauth_copy_response()' - Copy the response from a HTTP response. +// + +static char * // O - Response as a string +oauth_copy_response(http_t *http) // I - HTTP connection +{ + char *body, // Message body data string + *end, // End of data + *ptr; // Pointer into string + size_t bodylen; // Allocated length of string + ssize_t bytes; // Bytes read + http_state_t initial_state; // Initial HTTP state + + + // Allocate memory for string... + initial_state = httpGetState(http); + + if ((bodylen = (size_t)httpGetLength(http)) == 0 || bodylen > 65536) + bodylen = 65536; // Accept up to 64k for GETs/POSTs + + if ((body = calloc(1, bodylen + 1)) != NULL) + { + for (ptr = body, end = body + bodylen; ptr < end; ptr += bytes) + { + if ((bytes = httpRead(http, ptr, (size_t)(end - ptr))) <= 0) + break; + } + } + + if (httpGetState(http) == initial_state) + httpFlush(http); + + return (body); +} + + +// +// 'oauth_do_post()' - Send a POST request with the specified data and do error +// handling, returning JSON when available. +// + +static cups_json_t * // O - JSON response +oauth_do_post(const char *ep, // I - Endpoint URI + const char *content_type, // I - Content type + const char *request) // I - Request message body +{ + http_t *http; // Connection to endpoint + char host[256], // Endpoint hostname + resource[1024]; // Endpoint resource + int port; // Endpoint port + size_t req_length; // Length of request message body + http_status_t status; // POST status + char *response; // Response message body + cups_json_t *resp_json = NULL; // Response JSON + bool resp_error; // Is the response an error? + + + DEBUG_printf("3oauth_do_post(ep=\"%s\", content_type=\"%s\", request=\"%s\")", ep, content_type, request); + + // Connect to the endpoint... + if ((http = httpConnectURI(ep, host, sizeof(host), &port, resource, sizeof(resource), /*blocking*/true, /*msec*/30000, /*cancel*/NULL, /*require_ca*/true)) == NULL) + return (NULL); + + // Send a POST request with the request data... + req_length = strlen(request); + + httpClearFields(http); + httpSetField(http, HTTP_FIELD_ACCEPT, "application/json,text/json"); + httpSetField(http, HTTP_FIELD_CONTENT_TYPE, content_type); + httpSetLength(http, req_length); + + if (!httpWriteRequest(http, "POST", resource)) + { + if (!httpConnectAgain(http, 30000, NULL)) + goto done; + + if (!httpWriteRequest(http, "POST", resource)) + goto done; + } + + if (httpWrite(http, request, req_length) < (ssize_t)req_length) + goto done; + + // Get the response... + while ((status = httpUpdate(http)) == HTTP_STATUS_CONTINUE); + + response = oauth_copy_response(http); + resp_json = cupsJSONImportString(response); + + free(response); + + // Check for errors... + resp_error = oauth_set_error(resp_json, /*num_form*/0, /*form*/NULL); + if (!resp_error && status != HTTP_STATUS_OK) + { + _cupsSetError(IPP_STATUS_ERROR_INTERNAL, httpStatusString(status), false); + resp_error = true; + } + + if (resp_error) + { + cupsJSONDelete(resp_json); + resp_json = NULL; + } + + // Close the HTTP connection and return any JSON we have... + done: + + httpClose(http); + + return (resp_json); +} + + +// +// 'oauth_get_jwks()' - Get the JWT key set for an Authorization Server. +// + +static cups_json_t * // O - JWKS or `NULL` on error +oauth_get_jwks(const char *auth_uri, // I - Authorization server URI + cups_json_t *metadata) // I - Server metadata +{ + const char *jwks_uri; // URI of key set + cups_json_t *jwks; // JWT key set + char filename[1024]; // Local metadata filename + struct stat fileinfo; // Local metadata file info + + + DEBUG_printf("oauth_get_jwks(auth_uri=\"%s\", metadata=%p)", auth_uri, (void *)metadata); + + // Get existing key set... + if (!oauth_make_path(filename, sizeof(filename), auth_uri, /*secondary_uri*/NULL, _CUPS_OTYPE_JWKS)) + return (NULL); + + if (stat(filename, &fileinfo)) + memset(&fileinfo, 0, sizeof(fileinfo)); + + // Don't bother connecting if the key set was updated recently... + if ((time(NULL) - fileinfo.st_mtime) <= 60) + return (cupsJSONImportFile(filename)); + + // Try getting the key set... + if ((jwks_uri = cupsJSONGetString(cupsJSONFind(metadata, "jwks_uri"))) == NULL) + return (NULL); + + if ((jwks = cupsJSONImportURL(jwks_uri, &fileinfo.st_mtime)) != NULL) + { + // Save the key set... + char *s = cupsJSONExportString(jwks); + // JSON string + + oauth_save_value(auth_uri, /*secondary_uri*/NULL, _CUPS_OTYPE_JWKS, s); + free(s); + } + + // Return what we got... + return (jwks); +} + + +// +// 'oauth_load_value()' - Load the contents of the specified value file. +// + +static char * +oauth_load_value( + const char *auth_uri, // I - Authorization Server URI + const char *secondary_uri, // I - Resource or redirect URI + _cups_otype_t otype) // I - Type (_CUPS_OTYPE_xxx) +{ + char filename[1024]; // Filename + struct stat fileinfo; // File information + int fd; // File descriptor + char *value = NULL; // Value + + + DEBUG_printf("3oauth_load_value(auth_uri=\"%s\", secondary_uri=\"%s\", otype=%s)", auth_uri, secondary_uri, cups_otypes[otype]); + + // Try to make the corresponding file path... + if (!oauth_make_path(filename, sizeof(filename), auth_uri, secondary_uri, otype)) + return (NULL); + + // Open the file... + if ((fd = open(filename, O_RDONLY)) >= 0) + { + // Opened, read up to 64k of data... + if (!fstat(fd, &fileinfo) && fileinfo.st_size <= 65536 && (value = calloc(1, (size_t)fileinfo.st_size + 1)) != NULL) + read(fd, value, (size_t)fileinfo.st_size); + else + _cupsSetError(IPP_STATUS_ERROR_INTERNAL, strerror(errno), false); + + close(fd); + } + else + { + _cupsSetError(IPP_STATUS_ERROR_INTERNAL, strerror(errno), false); + } + + // Return whatever we have... + DEBUG_printf("4oauth_load_value: Returning \"%s\".", value); + + return (value); +} + + +// +// 'oauth_make_path()' - Make an OAuth store filename. +// + +static char * // O - Filename +oauth_make_path( + char *buffer, // I - Filename buffer + size_t bufsize, // I - Size of filename buffer + const char *auth_uri, // I - Authorization server URI + const char *secondary_uri, // I - Resource/redirect URI + _cups_otype_t otype) // I - Type (_CUPS_OTYPE_xxx) +{ + char auth_temp[1024], // Temporary copy of auth_uri + secondary_temp[1024], // Temporary copy of secondary_uri + *ptr; // Pointer into temporary strings + unsigned char auth_hash[32], // SHA-256 hash of base auth_uri + secondary_hash[32]; // SHA-256 hash of base secondary_uri + _cups_globals_t *cg = _cupsGlobals(); // Global data + static const char * const otypes[] = // Filename extensions for each type + { + "accs", // Access token + "clid", // Client ID + "csec", // Client secret + "cver", // Code verifier + "idtk", // ID token + "jwks", // Key store + "meta", // Metadata + "nonc", // Nonce + "ruri", // Redirect URI + "rfsh" // Refresh token + }; + + + DEBUG_printf("3oauth_make_path(buffer=%p, bufsize=%lu, auth_uri=\"%s\", secondary_uri=\"%s\", otype=%s)", (void *)buffer, (unsigned long)bufsize, auth_uri, secondary_uri, cups_otypes[otype]); + + // Range check input... + if (!auth_uri || strncmp(auth_uri, "https://", 8) || auth_uri[8] == '[' || isdigit(auth_uri[8] & 255) || (secondary_uri && strncmp(secondary_uri, "http://", 7) && strncmp(secondary_uri, "https://", 8) && strncmp(secondary_uri, "ipps://", 7))) + { + _cupsSetError(IPP_STATUS_ERROR_INTERNAL, strerror(EINVAL), false); + *buffer = '\0'; + return (NULL); + } + + // First make sure the "oauth" directory exists... + snprintf(buffer, bufsize, "%s/oauth", cg->userconfig); + if (!_cupsDirCreate(buffer, 0700)) + { + _cupsSetError(IPP_STATUS_ERROR_INTERNAL, strerror(errno), false); + *buffer = '\0'; + return (NULL); + } + + // Build the hashed versions of the auth and resource URIs... + cupsCopyString(auth_temp, auth_uri + 8, sizeof(auth_temp)); + if ((ptr = strchr(auth_temp, '/')) != NULL) + *ptr = '\0'; // Strip resource path + if (!strchr(auth_temp, ':')) // Add :443 if no port is present + cupsConcatString(auth_temp, ":443", sizeof(auth_temp)); + + cupsHashData("sha2-256", auth_temp, strlen(auth_temp), auth_hash, sizeof(auth_hash)); + cupsHashString(auth_hash, sizeof(auth_hash), auth_temp, sizeof(auth_temp)); + + if (secondary_uri) + { + if (!strncmp(secondary_uri, "http://", 7)) + { + // HTTP URI + cupsCopyString(secondary_temp, secondary_uri + 7, sizeof(secondary_temp)); + if ((ptr = strchr(secondary_temp, '/')) != NULL) + *ptr = '\0'; // Strip resource path + if (!strchr(secondary_temp, ':')) // Add :80 if no port is present + cupsConcatString(secondary_temp, ":80", sizeof(secondary_temp)); + } + else if (!strncmp(secondary_uri, "https://", 8)) + { + // HTTPS URI + cupsCopyString(secondary_temp, secondary_uri + 8, sizeof(secondary_temp)); + if ((ptr = strchr(secondary_temp, '/')) != NULL) + *ptr = '\0'; // Strip resource path + if (!strchr(secondary_temp, ':')) // Add :443 if no port is present + cupsConcatString(secondary_temp, ":443", sizeof(secondary_temp)); + } + else + { + // IPPS URI + cupsCopyString(secondary_temp, secondary_uri + 7, sizeof(secondary_temp)); + if ((ptr = strchr(secondary_temp, '/')) != NULL) + *ptr = '\0'; // Strip resource path + if (!strchr(secondary_temp, ':')) // Add :631 if no port is present + cupsConcatString(secondary_temp, ":631", sizeof(secondary_temp)); + } + + cupsHashData("sha2-256", secondary_temp, strlen(secondary_temp), secondary_hash, sizeof(secondary_hash)); + cupsHashString(secondary_hash, sizeof(secondary_hash), secondary_temp, sizeof(secondary_temp)); + } + else + { + // Leave an empty string for the resource portion + secondary_temp[0] = '\0'; + } + + // Build the filename for the corresponding data... + if (secondary_temp[0]) + snprintf(buffer, bufsize, "%s/oauth/%s+%s.%s", cg->userconfig, auth_temp, secondary_temp, otypes[otype]); + else + snprintf(buffer, bufsize, "%s/oauth/%s.%s", cg->userconfig, auth_temp, otypes[otype]); + + DEBUG_printf("4oauth_make_path: Returning \"%s\".", buffer); + + return (buffer); +} + + +// +// 'oauth_make_software_id()' - Make the software_id UUID. +// +// The CUPS OAuth software_id is a format 8 (custom) UUID as defined in RFC 9562 +// (replaces RFC 4122). A certain amount of the UUID is "vanity" (RFC 8010 and +// RFC 8011 define the core IPP standard) with "CUPS" and "OAuth" in the UUID +// bytes as well, but this will be as unique as a regular random UUID will be. +// +// (Has the advantage of being easily identified, too...) +// +// For CUPS 3.0.x: +// +// 43555053-0300-8010-8011-4F4175746820 +// + +static char * // O - UUID string +oauth_make_software_id(char *buffer, // I - UUID buffer + size_t bufsize) // I - Size of UUID buffer +{ + unsigned char uuid[16]; // UUID bytes + + + uuid[ 0] = 'C'; // "CUPS" + uuid[ 1] = 'U'; + uuid[ 2] = 'P'; + uuid[ 3] = 'S'; + uuid[ 4] = CUPS_VERSION_MAJOR; // CUPS major.minor + uuid[ 5] = CUPS_VERSION_MINOR; + uuid[ 6] = 0x80; // Custom UUID format 8 + uuid[ 7] = 0x10; // "8010" for RFC 8010 + uuid[ 8] = 0x80; // Variant 8 + uuid[ 9] = 0x11; // "8011" for RFC 8011 + uuid[10] = 'O'; // "OAuth" + uuid[11] = 'A'; + uuid[12] = 'u'; + uuid[13] = 't'; + uuid[14] = 'h'; + uuid[15] = 0x20; // 2.0 + + snprintf(buffer, bufsize, "%02X%02X%02X%02X-%02X%02X-%02X%02X-%02X%02X-%02X%02X%02X%02X%02X%02X", uuid[0], uuid[1], uuid[2], uuid[3], uuid[4], uuid[5], uuid[6], uuid[7], uuid[8], uuid[9], uuid[10], uuid[11], uuid[12], uuid[13], uuid[14], uuid[15]); + + return (buffer); +} + + +// +// 'oauth_metadata_contains()' - Determine whether a metadata parameter contains the specified value. +// + +static bool // O - `true` if present, `false` otherwise +oauth_metadata_contains( + cups_json_t *metadata, // I - Authorization server metadata + const char *parameter, // I - Metadata parameter + const char *value) // I - Parameter value +{ + size_t i, // Looping var + count; // Number of values + cups_json_t *values, // Parameter values + *current; // Current value + + + DEBUG_printf("3oauth_metadata_contains(metadata=%p, parameter=\"%s\", value=\"%s\")", (void *)metadata, parameter, value); + + if ((values = cupsJSONFind(metadata, parameter)) == NULL) + { + DEBUG_puts("4oauth_metadata: Returning false."); + return (false); + } + + for (i = 0, count = cupsJSONGetCount(values); i < count; i ++) + { + current = cupsJSONGetChild(values, i); + + if (cupsJSONGetType(current) == CUPS_JTYPE_STRING && !strcmp(value, cupsJSONGetString(current))) + return (true); + } + + return (false); +} + + +// +// 'oauth_save_value()' - Save a value string to the OAuth store. +// + +static void +oauth_save_value( + const char *auth_uri, // I - Authorization Server URI + const char *secondary_uri, // I - Resource or redirect URI + _cups_otype_t otype, // I - Type (_CUPS_OTYPE_xxx) + const char *value) // I - Value string or `NULL` to remove +{ + char filename[1024]; // Filename + int fd; // File descriptor + + + DEBUG_printf("3oauth_save_value(auth_uri=\"%s\", secondary_uri=\"%s\", otype=%s, value=\"%s\")", auth_uri, secondary_uri, cups_otypes[otype], value); + + // Try making the filename... + if (!oauth_make_path(filename, sizeof(filename), auth_uri, secondary_uri, otype)) + return; + + if (value) + { + // Create the file... + if ((fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC | O_NOFOLLOW, 0600)) >= 0) + { + // Write the value and close... + write(fd, value, strlen(value)); + close(fd); + } + } + else + { + // Remove the file... + unlink(filename); + } +} + + +// +// 'oauth_set_error()' - Set the OAuth error message from a JSON response. +// + +static bool // O - `true` if there was an error, `false` otherwise +oauth_set_error(cups_json_t *json, // I - JSON response + size_t num_form, // I - Number of form variables + cups_option_t *form) // I - Form variables +{ + const char *error, // error value + *error_desc; // error_description value + + + if (json) + { + error = cupsJSONGetString(cupsJSONFind(json, "error")); + error_desc = cupsJSONGetString(cupsJSONFind(json, "error_description")); + } + else + { + error = cupsGetOption("error", num_form, form); + error_desc = cupsGetOption("error_description", num_form, form); + } + + if (error) + { + if (error_desc) + { + char message[1024]; // Message string + + snprintf(message, sizeof(message), "%s: %s", error, error_desc); + _cupsSetError(IPP_STATUS_ERROR_CUPS_OAUTH, message, false); + } + else + { + _cupsSetError(IPP_STATUS_ERROR_CUPS_OAUTH, error, false); + } + + return (true); + } + + return (false); +} diff --git a/cups/oauth.h b/cups/oauth.h new file mode 100644 index 0000000000..a1625feda8 --- /dev/null +++ b/cups/oauth.h @@ -0,0 +1,63 @@ +// +// OAuth API definitions for CUPS. +// +// Copyright © 2024 by OpenPrinting. +// +// Licensed under Apache License v2.0. See the file "LICENSE" for more +// information. +// + +#ifndef _CUPS_OAUTH_H_ +# define _CUPS_OAUTH_H_ +# include "jwt.h" +# ifdef __cplusplus +extern "C" { +# endif /* __cplusplus */ + + +// +// Constants... +// + +# define CUPS_OAUTH_REDIRECT_URI "http://127.0.0.1/" + // Redirect URI for local authorization + + +// +// Types... +// + +typedef enum cups_ogrant_e // OAuth Grant Types +{ + CUPS_OGRANT_AUTHORIZATION_CODE, // Authorization code + CUPS_OGRANT_DEVICE_CODE, // Device code + CUPS_OGRANT_REFRESH_TOKEN // Refresh token +} cups_ogrant_t; + + +// +// Functions... +// + +extern void cupsOAuthClearTokens(const char *auth_uri, const char *resource_uri) _CUPS_PUBLIC; +extern char *cupsOAuthCopyAccessToken(const char *auth_uri, const char *resource_uri, time_t *access_expires) _CUPS_PUBLIC; +extern char *cupsOAuthCopyClientId(const char *auth_uri, const char *redirect_uri) _CUPS_PUBLIC; +extern char *cupsOAuthCopyRefreshToken(const char *auth_uri, const char *resource_uri) _CUPS_PUBLIC; +extern cups_jwt_t *cupsOAuthCopyUserId(const char *auth_uri, const char *resource_uri) _CUPS_PUBLIC; + +extern char *cupsOAuthGetAuthorizationCode(const char *auth_uri, cups_json_t *metadata, const char *resource_uri, const char *scopes, const char *redirect_uri) _CUPS_PUBLIC; +extern char *cupsOAuthGetClientId(const char *auth_uri, cups_json_t *metadata, const char *redirect_uri, const char *logo_uri, const char *tos_uri) _CUPS_PUBLIC; +extern cups_json_t *cupsOAuthGetMetadata(const char *auth_uri) _CUPS_PUBLIC; +extern char *cupsOAuthGetTokens(const char *auth_uri, cups_json_t *metadata, const char *resource_uri, const char *grant_code, cups_ogrant_t grant_type, const char *redirect_uri, time_t *access_expires) _CUPS_PUBLIC; + +extern char *cupsOAuthMakeAuthorizationURL(const char *auth_uri, cups_json_t *metadata, const char *resource_uri, const char *scopes, const char *client_id, const char *code_verifier, const char *nonce, const char *redirect_uri, const char *state) _CUPS_PUBLIC; +extern char *cupsOAuthMakeBase64Random(size_t len) _CUPS_PUBLIC; + +extern void cupsOAuthSaveClientData(const char *auth_uri, const char *redirect_uri, const char *client_id, const char *client_secret) _CUPS_PUBLIC; +extern void cupsOAuthSaveTokens(const char *auth_uri, const char *resource_uri, const char *access_token, time_t access_expires, const char *user_id, const char *refresh_token) _CUPS_PUBLIC; + + +# ifdef __cplusplus +} +# endif /* __cplusplus */ +#endif // !_CUPS_OAUTH_H_ diff --git a/cups/testoauth.c b/cups/testoauth.c new file mode 100644 index 0000000000..789a6525e7 --- /dev/null +++ b/cups/testoauth.c @@ -0,0 +1,501 @@ +// +// OAuth API unit tests for CUPS. +// +// Copyright © 2024 by OpenPrinting. +// +// Licensed under Apache License v2.0. See the file "LICENSE" for more +// information. +// +// Usage: testoauth [-a OAUTH-URI] [-r REDIRECT-URI] [-s SCOPE(S)] [COMMAND [ARGUMENT(S)]] +// +// Commands: +// +// authorize RESOURCE-URI +// clear RESOURCE-URI +// get-access-token RESOURCE-URI +// get-client-id +// get-metadata +// get-refresh-token RESOURCE-URI +// get-user-id RESOURCE-URI +// set-client-data CLIENT-ID CLIENT-SECRET +// test +// + +#include "cups.h" +#include "oauth.h" +#include "test-internal.h" + + +// +// Local constants... +// + +#define TEST_OAUTH_URI "https://samples.auth0.com" + + +// +// Local functions... +// + +static int authorize(const char *oauth_uri, const char *scopes, const char *resource_uri, const char *redirect_uri); +static int clear(const char *oauth_uri, const char *resource_uri); +static int get_access_token(const char *oauth_uri, const char *resource_uri); +static int get_client_id(const char *oauth_uri, const char *redirect_uri); +static int get_metadata(const char *oauth_uri); +static int get_refresh_token(const char *oauth_uri, const char *resource_uri); +static int get_user_id(const char *oauth_uri, const char *resource_uri); +static int set_client_data(const char *oauth_uri, const char *redirect_uri, const char *client_id, const char *client_secret); +static int unit_tests(const char *oauth_uri, const char *redirect_uri); +static int usage(FILE *out); + + +// +// 'main()' - Main entry. +// + +int // O - Exit status +main(int argc, // I - Number of command-line arguments + char *argv[]) // I - Command-line arguments +{ + int i; // Looping var + const char *opt, // Current option + *oauth_uri = NULL, // OAuth authorization server URI + *command = NULL, // Command + *redirect_uri = NULL, // Redirection URI + *scopes = NULL; // Scopes + + + // Parse the command-line... + for (i = 1; i < argc; i ++) + { + if (!strcmp(argv[i], "--help")) + { + return (usage(stdout)); + } + else if (argv[i][0] == '-' && argv[i][1] != '-') + { + for (opt = argv[i] + 1; *opt; opt ++) + { + switch (*opt) + { + case 'a' : // -a AUTH-URI + i ++; + if (i >= argc) + { + fputs("testoauth: Missing Authorization Server URI after '-a'.\n", stderr); + return (usage(stderr)); + } + + oauth_uri = argv[i]; + break; + + case 'r' : // -r REDIRECT-URI + i ++; + if (i >= argc) + { + fputs("testoauth: Missing redirect URI after '-r'.\n", stderr); + return (usage(stderr)); + } + + redirect_uri = argv[i]; + break; + + case 's' : // -s SCOPE(S) + i ++; + if (i >= argc) + { + fputs("testoauth: Missing scope(s) after '-s'.\n", stderr); + return (usage(stderr)); + } + + scopes = argv[i]; + break; + + default : + fprintf(stderr, "testoauth: Unknown option '-%c'.\n", *opt); + return (usage(stderr)); + } + } + } + else if (strncmp(argv[i], "--", 2) && !command) + { + command = argv[i]; + i ++; + break; + } + else + { + fprintf(stderr, "testoauth: Unknown option '%s'.\n", argv[i]); + return (usage(stderr)); + } + } + + // Apply defaults... + if (!command) + command = "test"; + + if (!oauth_uri) + oauth_uri = TEST_OAUTH_URI; + + // Do commands... + if (!strcmp(command, "authorize")) + { + if (i >= argc) + { + fputs("testoauth: Missing resource URI.\n", stderr); + return (usage(stderr)); + } + + return (authorize(oauth_uri, scopes, argv[i], redirect_uri)); + } + else if (!strcmp(command, "clear")) + { + if (i >= argc) + { + fputs("testoauth: Missing resource URI.\n", stderr); + return (usage(stderr)); + } + + return (clear(oauth_uri, argv[i])); + } + else if (!strcmp(command, "get-access-token")) + { + if (i >= argc) + { + fputs("testoauth: Missing resource URI.\n", stderr); + return (usage(stderr)); + } + + return (get_access_token(oauth_uri, argv[i])); + } + else if (!strcmp(command, "get-client-id")) + { + return (get_client_id(oauth_uri, redirect_uri)); + } + else if (!strcmp(command, "get-metadata")) + { + return (get_metadata(oauth_uri)); + } + else if (!strcmp(command, "get-refresh-token")) + { + if (i >= argc) + { + fputs("testoauth: Missing resource URI.\n", stderr); + return (usage(stderr)); + } + + return (get_refresh_token(oauth_uri, argv[i])); + } + else if (!strcmp(command, "get-user-id")) + { + return (get_user_id(oauth_uri, argv[i])); + } + else if (!strcmp(command, "set-client-data")) + { + if ((i + 1) >= argc) + { + fputs("testoauth: Missing client_id and/or client_secret.\n", stderr); + return (usage(stderr)); + } + + return (set_client_data(oauth_uri, redirect_uri, argv[i], argv[i + 1])); + } + else if (!strcmp(command, "test")) + { + return (unit_tests(oauth_uri, redirect_uri)); + } + else + { + fprintf(stderr, "testoauth: Unknown command '%s'.\n", command); + return (usage(stderr)); + } +} + + +// +// 'authorize()' - Authorize access. +// + +static int // O - Exit status +authorize(const char *oauth_uri, // I - Authorization Server URI + const char *scopes, // I - Scope(s) + const char *resource_uri, // I - Resource URI + const char *redirect_uri) // I - Redirect URI +{ + (void)oauth_uri; + (void)scopes; + (void)resource_uri; + (void)redirect_uri; + + return (1); +} + + +// +// 'clear()' - Clear authorization information. +// + +static int // O - Exit status +clear(const char *oauth_uri, // I - Authorization Server URI + const char *resource_uri) // I - Resource URI +{ + (void)oauth_uri; + (void)resource_uri; + + return (1); +} + + +// +// 'get_access_token()' - Get an access token. +// + +static int // O - Exit status +get_access_token( + const char *oauth_uri, // I - Authorization Server URI + const char *resource_uri) // I - Resource URI +{ + (void)oauth_uri; + (void)resource_uri; + + return (1); +} + + +// +// 'get_client_id()' - Get the client ID value. +// + +static int // O - Exit status +get_client_id(const char *oauth_uri, // I - Authorization Server URI + const char *redirect_uri) // I - Redirection URI +{ + (void)oauth_uri; + (void)redirect_uri; + + return (1); +} + + +// +// 'get_metadata()' - Get authorization server metadata. +// + +static int // O - Exit status +get_metadata(const char *oauth_uri) // I - Authorization Server URI +{ + (void)oauth_uri; + return (1); +} + + +// +// 'get_refresh_token()' - Get the resource token. +// + +static int // O - Exit status +get_refresh_token( + const char *oauth_uri, // I - Authorization Server URI + const char *resource_uri) // I - Resource URI +{ + (void)oauth_uri; + (void)resource_uri; + + return (1); +} + + +// +// 'get_user_id()' - Get user identification. +// + +static int // O - Exit status +get_user_id(const char *oauth_uri, // I - Authorization Server URI + const char *resource_uri) // I - Resource URI +{ + cups_jwt_t *user_id; // User ID information + + + if ((user_id = cupsOAuthCopyUserId(oauth_uri, resource_uri)) != NULL) + { + const char *aud = cupsJWTGetClaimString(user_id, CUPS_JWT_AUD); + // Audience + const char *iss = cupsJWTGetClaimString(user_id, CUPS_JWT_ISS); + // Issuer + const char *jti = cupsJWTGetClaimString(user_id, CUPS_JWT_JTI); + // JWT ID + const char *name = cupsJWTGetClaimString(user_id, CUPS_JWT_NAME); + // Display name + const char *sub = cupsJWTGetClaimString(user_id, CUPS_JWT_SUB); + // Subject (username/ID) + double iat = cupsJWTGetClaimNumber(user_id, CUPS_JWT_IAT); + // Issue time + double exp = cupsJWTGetClaimNumber(user_id, CUPS_JWT_EXP); + // Expiration time + double nbf = cupsJWTGetClaimNumber(user_id, CUPS_JWT_NBF); + // Not before time + char date[256]; // Date + + if (iss) + printf("Issuer: %s\n", iss); + if (name) + printf("Display Name: %s\n", name); + if (sub) + printf("Subject: %s\n", sub); + if (aud) + printf("Audience: %s\n", aud); + if (jti) + printf("JWT ID: %s\n", jti); + if (iat > 0.0) + printf("Issued On: %s\n", httpGetDateString2((time_t)iat, date, sizeof(date))); + if (exp > 0.0) + printf("Expires On: %s\n", httpGetDateString2((time_t)exp, date, sizeof(date))); + if (nbf > 0.0) + printf("Not Before: %s\n", httpGetDateString2((time_t)nbf, date, sizeof(date))); + + return (0); + } + else + { + return (1); + } +} + + +// +// 'set_client_data()' - Save client_id and client_secret values. +// + +static int // O - Exit status +set_client_data( + const char *oauth_uri, // I - Authorization Server URI + const char *redirect_uri, // I - Redirect URI + const char *client_id, // I - Client ID + const char *client_secret) // I - Client secret +{ + cupsOAuthSaveClientData(oauth_uri, redirect_uri ? redirect_uri : CUPS_OAUTH_REDIRECT_URI, client_id, client_secret); + + return (0); +} + + +// +// 'unit_tests()' - Run unit tests. +// + +static int // O - Exit status +unit_tests(const char *oauth_uri, // I - Authorization Server URI + const char *redirect_uri) // I - Redirection URI +{ + cups_json_t *metadata; // Server metadata + char *auth_code = NULL, // Authorization code + *access_token = NULL, // Access token + *refresh_token = NULL; // Refresh token + cups_jwt_t *user_id = NULL; // User identification + time_t access_expires; // Expiration data of access token + + + // Get metadata... + testBegin("cupsOAuthGetMetadata(%s)", oauth_uri); + if ((metadata = cupsOAuthGetMetadata(oauth_uri)) != NULL) + { + testEnd(true); + } + else + { + testEndMessage(false, "%s", cupsGetErrorString()); + goto done; + } + + // Authorize... + testBegin("cupsOAuthGetAuthorizationCode(%s)", oauth_uri); + if ((auth_code = cupsOAuthGetAuthorizationCode(oauth_uri, metadata, /*resource_uri*/NULL, "openid email profile", redirect_uri)) != NULL) + { + testEndMessage(true, "%s", auth_code); + } + else + { + testEndMessage(false, "%s", cupsGetErrorString()); + goto done; + } + + // Get the access token... + testBegin("cupsOAuthGetTokens(%s)", oauth_uri); + if ((access_token = cupsOAuthGetTokens(oauth_uri, metadata, /*resource_uri*/NULL, auth_code, CUPS_OGRANT_AUTHORIZATION_CODE, CUPS_OAUTH_REDIRECT_URI, &access_expires)) != NULL) + { + testEndMessage(true, "%s, expires in %ld seconds", access_token, (long)(access_expires - time(NULL))); + } + else + { + testEndMessage(false, "%s", cupsGetErrorString()); + goto done; + } + + // Get the refresh token... + testBegin("cupsOAuthCopyRefreshToken(%s)", oauth_uri); + if ((refresh_token = cupsOAuthCopyRefreshToken(oauth_uri, /*resource_uri*/NULL)) != NULL) + { + testEndMessage(true, "%s", refresh_token); + } + else + { + testEndMessage(false, "%s", cupsGetErrorString()); + goto done; + } + + // Get the user identifications... + testBegin("cupsOAuthCopyUserId(%s)", oauth_uri); + if ((user_id = cupsOAuthCopyUserId(oauth_uri, /*resource_uri*/NULL)) != NULL) + { + const char *iss = cupsJWTGetClaimString(user_id, CUPS_JWT_ISS); + // Issuer + const char *name = cupsJWTGetClaimString(user_id, CUPS_JWT_NAME); + // Display name + const char *sub = cupsJWTGetClaimString(user_id, CUPS_JWT_SUB); + // Subject (username/ID) + double exp = cupsJWTGetClaimNumber(user_id, CUPS_JWT_EXP); + // Expiration time + char expdate[256]; // Expiration date + + testEndMessage(true, "iss=\"%s\", name=\"%s\", sub=\"%s\", exp=%s", iss, name, sub, httpGetDateString2((time_t)exp, expdate, sizeof(expdate))); + } + else + { + testEndMessage(false, "%s", cupsGetErrorString()); + goto done; + } + + // Free memory and return... + done: + + cupsJSONDelete(metadata); + free(auth_code); + free(access_token); + free(refresh_token); + cupsJWTDelete(user_id); + + return (testsPassed ? 0 : 1); +} + + +// +// 'usage()' - Show usage. +// + +static int // O - Exit status +usage(FILE *out) // I - Output file +{ + fputs("Usage: testoauth [-a OAUTH-URI] [-r REDIRECT-URI] [-s SCOPE(S)] [COMMAND [ARGUMENT(S)]]\n", out); + fputs("Commands:\n", out); + fputs(" authorize RESOURCE-URI\n", out); + fputs(" clear RESOURCE-URI\n", out); + fputs(" get-access-token RESOURCE-URI\n", out); + fputs(" get-client-id\n", out); + fputs(" get-metadata\n", out); + fputs(" get-refresh-token RESOURCE-URI\n", out); + fputs(" get-user-id RESOURCE-URI\n", out); + fputs(" set-client-data CLIENT-ID CLIENT-SECRET\n", out); + fputs(" test\n", out); + + return (out == stdout ? 0 : 1); +}