diff --git a/.gitignore b/.gitignore index 611f2f6..4211558 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,10 @@ dist/ +target/ external/ node_modules coverage .eslintcache .env .vscode +*.node + diff --git a/docker-compose.yml b/docker-compose.yml index d70691c..bbb8d5b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,23 @@ services: + kdc: + image: alpine:latest + container_name: kdc + ports: + - '8000:88/tcp' + - '8000:88/udp' + - '8001:749' + volumes: + - './docker/kerberos/krb5-kdc.conf:/etc/krb5.conf:ro' + - './docker/kerberos/kdc.conf:/var/lib/krb5kdc/kdc.conf:ro' + - './docker/kerberos/init.sh:/init.sh:ro' + - './tmp/kerberos:/data' + entrypoint: ['/bin/sh', '/init.sh'] + healthcheck: + test: ['CMD', 'kadmin.local', '-q', 'list_principals'] + interval: 10s + timeout: 5s + retries: 5 + broker-single: # Rule of thumb: Confluent Kafka Version = Apache Kafka Version + 4.0.0 image: &image confluentinc/cp-kafka:${KAFKA_VERSION:-7.9.0} @@ -74,6 +93,38 @@ services: KAFKA_SASL_OAUTHBEARER_EXPECTED_AUDIENCE: users KAFKA_SASL_OAUTHBEARER_EXPECTED_SCOPE: test + broker-sasl-kerberos: + image: *image + container_name: broker-sasl-kerberos + ports: + - "9003:9092" # SASL + - "19003:19092" # PLAIN TEXT - Used to create users + healthcheck: *health_check + volumes: + - "./docker/sasl/jaas-kerberos.conf:/etc/kafka/jaas.conf:ro" + - "./docker/kerberos/krb5-broker.conf:/etc/krb5.conf:ro" + - "./tmp/kerberos/broker.keytab:/etc/kafka/broker.keytab:ro" + depends_on: + kdc: + condition: service_healthy + environment: + <<: *common_config + # Broker specific general and port options + KAFKA_LISTENERS: "SASL://:9092,DOCKER://:19092,CONTROLLER://:29092" + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: "SASL:SASL_PLAINTEXT,DOCKER:PLAINTEXT,CONTROLLER:PLAINTEXT" + KAFKA_ADVERTISED_LISTENERS: "SASL://localhost:9003,DOCKER://broker-sasl-kerberos:19092" + KAFKA_CONTROLLER_QUORUM_VOTERS: "1@broker-sasl-kerberos:29092" + # SASL + KAFKA_OPTS: "-Djava.security.auth.login.config=/etc/kafka/jaas.conf -Djava.security.krb5.conf=/etc/krb5.conf" + KAFKA_LISTENER_NAME_SASL_GSSAPI_SASL_JAAS_CONFIG: 'com.sun.security.auth.module.Krb5LoginModule required useKeyTab=true storeKey=true keyTab="/etc/kafka/broker.keytab" principal="broker/broker-sasl-kerberos@EXAMPLE.COM";' + KAFKA_CONNECTIONS_MAX_REAUTH_MS: 5000 + KAFKA_ALLOW_EVERYONE_IF_NO_ACL_FOUND: "false" + KAFKA_SUPER_USERS: 'User:admin;User:broker/broker-sasl-kerberos@EXAMPLE.COM;User:admin-keytab/localhost@EXAMPLE.COM;User:admin-password/localhost@EXAMPLE.COM' + KAFKA_SASL_ENABLED_MECHANISMS: "GSSAPI" + KAFKA_SASL_MECHANISM_CONTROLLER_PROTOCOL: "PLAIN" + KAFKA_SASL_MECHANISM_INTER_BROKER_PROTOCOL: "PLAIN" + KAFKA_SASL_KERBEROS_SERVICE_NAME: 'kafka' + broker-cluster-1: image: *image container_name: broker-cluster-1 diff --git a/docker/kerberos/README.md b/docker/kerberos/README.md new file mode 100644 index 0000000..de84489 --- /dev/null +++ b/docker/kerberos/README.md @@ -0,0 +1,10 @@ +To create `kafka.keytab`: + +``` +ktutil +addent -password -p admin/localhost@example.com -k 1 -e aes256-cts-hmac-sha1-96 +write_kt kafka.keytab +quit +``` + +On Mac, use `ktutil` from `krb5`, installed via Homebrew diff --git a/docker/kerberos/init.sh b/docker/kerberos/init.sh new file mode 100644 index 0000000..0241222 --- /dev/null +++ b/docker/kerberos/init.sh @@ -0,0 +1,26 @@ +#!/bin/sh +set -e + +# Setup KDC if needed +if [ ! -f /var/lib/krb5kdc/principal ]; then + echo "Setting up KDC ..." + + apk add --no-cache krb5-server krb5 + kdb5_util create -s -P password + + # # ACL file + echo "*/admin@EXAMPLE.COM *" > /var/lib/krb5kdc/kadm5.acl + + # Create principals + kadmin.local -q "addprinc -pw admin admin@EXAMPLE.COM" # Main administrator + kadmin.local -q "addprinc -randkey broker/broker-sasl-kerberos@EXAMPLE.COM" # Kafka broker + kadmin.local -q "addprinc -randkey admin-keytab@EXAMPLE.COM" # Client with keytab + kadmin.local -q "addprinc -pw admin admin-password@EXAMPLE.COM" # Client with password + + # Genera keytab + kadmin.local -q "ktadd -k /data/broker.keytab broker/broker-sasl-kerberos@EXAMPLE.COM" + kadmin.local -q "ktadd -k /data/admin.keytab admin-keytab@EXAMPLE.COM" +fi + +krb5kdc +kadmind -nofork \ No newline at end of file diff --git a/docker/kerberos/kdc.conf b/docker/kerberos/kdc.conf new file mode 100644 index 0000000..b244fbe --- /dev/null +++ b/docker/kerberos/kdc.conf @@ -0,0 +1,11 @@ +[kdcdefaults] + kdc_ports = 88 + kdc_tcp_ports = 88 + +[realms] + EXAMPLE.COM = { + acl_file = /var/lib/krb5kdc/kadm5.acl + dict_file = /usr/share/dict/words + admin_keytab = /var/lib/krb5kdc/kadm5.keytab + supported_enctypes = aes256-cts:normal aes128-cts:normal + } \ No newline at end of file diff --git a/docker/kerberos/krb5-broker.conf b/docker/kerberos/krb5-broker.conf new file mode 100644 index 0000000..834571b --- /dev/null +++ b/docker/kerberos/krb5-broker.conf @@ -0,0 +1,14 @@ +[libdefaults] + default_realm = EXAMPLE.COM + dns_lookup_realm = false + dns_lookup_kdc = false + +[realms] + EXAMPLE.COM = { + kdc = kdc:88 + admin_server = kdc:749 + } + +[domain_realm] + .example.com = EXAMPLE.COM + example.com = EXAMPLE.COM \ No newline at end of file diff --git a/docker/kerberos/krb5-kdc.conf b/docker/kerberos/krb5-kdc.conf new file mode 100644 index 0000000..c84a63a --- /dev/null +++ b/docker/kerberos/krb5-kdc.conf @@ -0,0 +1,14 @@ +[libdefaults] + default_realm = EXAMPLE.COM + dns_lookup_realm = false + dns_lookup_kdc = false + +[realms] + EXAMPLE.COM = { + kdc = localhost:88 + admin_server = localhost:749 + } + +[domain_realm] + .example.com = EXAMPLE.COM + example.com = EXAMPLE.COM \ No newline at end of file diff --git a/docker/sasl/jaas-kerberos.conf b/docker/sasl/jaas-kerberos.conf new file mode 100644 index 0000000..c1a14e1 --- /dev/null +++ b/docker/sasl/jaas-kerberos.conf @@ -0,0 +1,9 @@ +KafkaServer { + com.sun.security.auth.module.Krb5LoginModule required + useKeyTab=true + storeKey=true + keyTab="/etc/kafka/broker.keytab" + principal="broker/broker-sasl-kerberos@EXAMPLE.COM" + serviceName="kafka" + useTicketCache=false; +}; \ No newline at end of file diff --git a/native/.cargo/config.toml b/native/.cargo/config.toml new file mode 100644 index 0000000..ac2b23f --- /dev/null +++ b/native/.cargo/config.toml @@ -0,0 +1,2 @@ +[target.x86_64-pc-windows-msvc] +rustflags = ["-C", "target-feature=+crt-static"] diff --git a/native/Cargo.lock b/native/Cargo.lock new file mode 100644 index 0000000..4e6a7c3 --- /dev/null +++ b/native/Cargo.lock @@ -0,0 +1,616 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 1.1.0", + "shlex", + "syn", + "which", +] + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "convert_case" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "ctor" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59c9b8bdf64ee849747c1b12eb861d21aa47fa161564f48332f1afe2373bf899" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + +[[package]] +name = "dtor" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e58a0764cddb55ab28955347b45be00ade43d4d6f3ba4bf3dc354e4ec9432934" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gssapi" +version = "0.1.0" +dependencies = [ + "bindgen", + "napi", + "napi-build", + "napi-derive", + "uuid", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "js-sys" +version = "0.3.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "napi" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3a1135cfe16ca43ac82ac05858554fc39c037d8e4592f2b4a83d7ef8e822f43" +dependencies = [ + "bitflags", + "ctor", + "napi-build", + "napi-sys", + "nohash-hasher", + "rustc-hash 2.1.1", +] + +[[package]] +name = "napi-build" +version = "2.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae82775d1b06f3f07efd0666e59bbc175da8383bc372051031d7a447e94fbea" + +[[package]] +name = "napi-derive" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78665d6bdf10e9a4e6b38123efb0f66962e6197c1aea2f07cff3f159a374696d" +dependencies = [ + "convert_case", + "ctor", + "napi-derive-backend", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "napi-derive-backend" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d55d01423e7264de3acc13b258fa48ca7cf38a4d25db848908ec3c1304a85a" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "semver", + "syn", +] + +[[package]] +name = "napi-sys" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ed8f0e23a62a3ce0fbb6527cdc056e9282ddd9916b068c46f8923e18eed5ee6" +dependencies = [ + "libloading", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "syn" +version = "2.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "uuid" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +dependencies = [ + "getrandom", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" diff --git a/native/Cargo.toml b/native/Cargo.toml new file mode 100644 index 0000000..69c7875 --- /dev/null +++ b/native/Cargo.toml @@ -0,0 +1,20 @@ +[package] + edition = "2021" + name = "gssapi" + version = "0.1.0" + +[lib] + crate-type = ["cdylib"] + +[dependencies] + uuid = { version = "1.18.1", features = ["v4"] } + napi = "3.0.0" + napi-derive = "3.0.0" + +[build-dependencies] + napi-build = "2" + bindgen = "0.69" + +[profile.release] + lto = true + strip = "symbols" diff --git a/native/README.md b/native/README.md new file mode 100644 index 0000000..92523e0 --- /dev/null +++ b/native/README.md @@ -0,0 +1,87 @@ +# `@napi-rs/package-template` + +![https://github.com/napi-rs/package-template/actions](https://github.com/napi-rs/package-template/workflows/CI/badge.svg) + +> Template project for writing node packages with napi-rs. + +# Usage + +1. Click **Use this template**. +2. **Clone** your project. +3. Run `yarn install` to install dependencies. +4. Run `yarn napi rename -n [@your-scope/package-name] -b [binary-name]` command under the project folder to rename your package. + +## Install this test package + +```bash +yarn add @napi-rs/package-template +``` + +## Ability + +### Build + +After `yarn build/npm run build` command, you can see `package-template.[darwin|win32|linux].node` file in project root. This is the native addon built from [lib.rs](./src/lib.rs). + +### Test + +With [ava](https://github.com/avajs/ava), run `yarn test/npm run test` to testing native addon. You can also switch to another testing framework if you want. + +### CI + +With GitHub Actions, each commit and pull request will be built and tested automatically in [`node@20`, `@node22`] x [`macOS`, `Linux`, `Windows`] matrix. You will never be afraid of the native addon broken in these platforms. + +### Release + +Release native package is very difficult in old days. Native packages may ask developers who use it to install `build toolchain` like `gcc/llvm`, `node-gyp` or something more. + +With `GitHub actions`, we can easily prebuild a `binary` for major platforms. And with `N-API`, we should never be afraid of **ABI Compatible**. + +The other problem is how to deliver prebuild `binary` to users. Downloading it in `postinstall` script is a common way that most packages do it right now. The problem with this solution is it introduced many other packages to download binary that has not been used by `runtime codes`. The other problem is some users may not easily download the binary from `GitHub/CDN` if they are behind a private network (But in most cases, they have a private NPM mirror). + +In this package, we choose a better way to solve this problem. We release different `npm packages` for different platforms. And add it to `optionalDependencies` before releasing the `Major` package to npm. + +`NPM` will choose which native package should download from `registry` automatically. You can see [npm](./npm) dir for details. And you can also run `yarn add @napi-rs/package-template` to see how it works. + +## Develop requirements + +- Install the latest `Rust` +- Install `Node.js@10+` which fully supported `Node-API` +- Install `yarn@1.x` + +## Test in local + +- yarn +- yarn build +- yarn test + +And you will see: + +```bash +$ ava --verbose + + ✔ sync function from native code + ✔ sleep function from native code (201ms) + ─ + + 2 tests passed +✨ Done in 1.12s. +``` + +## Release package + +Ensure you have set your **NPM_TOKEN** in the `GitHub` project setting. + +In `Settings -> Secrets`, add **NPM_TOKEN** into it. + +When you want to release the package: + +```bash +npm version [ | major | minor | patch | premajor | preminor | prepatch | prerelease [--preid=] | from-git] + +git push +``` + +GitHub actions will do the rest job for you. + +> WARN: Don't run `npm publish` manually. diff --git a/native/build.rs b/native/build.rs new file mode 100644 index 0000000..71ca5b4 --- /dev/null +++ b/native/build.rs @@ -0,0 +1,17 @@ +fn main() { + println!("cargo:rerun-if-changed=src/includes.h"); + + let bindings = bindgen::Builder::default() + .header("src/includes.h") + .allowlist_var("KRB5_.*") + .allowlist_var("GSS_.*") + .generate() + .expect("Unable to generate bindings"); + + let bindings_path = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap()).join("bindings.rs"); + bindings + .write_to_file(&bindings_path) + .expect("Couldn't write bindings!"); + + napi_build::setup(); +} diff --git a/native/index.d.ts b/native/index.d.ts new file mode 100644 index 0000000..21cacc6 --- /dev/null +++ b/native/index.d.ts @@ -0,0 +1,15 @@ +/* auto-generated by NAPI-RS */ +/* eslint-disable */ +export declare class GSSAPI { + constructor(kdc: string, realm: string) + authenticateWithPassword(username: string, password: string): boolean + authenticateWithKeytab(username: string, keytab: string): boolean + step(service: string, input?: Buffer | undefined | null): StepResult + wrap(data: Buffer): Buffer + unwrap(data: Buffer): Buffer +} + +export interface StepResult { + output: Buffer + completed: boolean +} diff --git a/native/index.js b/native/index.js new file mode 100644 index 0000000..f7416b4 --- /dev/null +++ b/native/index.js @@ -0,0 +1,575 @@ +// prettier-ignore +/* eslint-disable */ +// @ts-nocheck +/* auto-generated by NAPI-RS */ + +const { readFileSync } = require('node:fs') +let nativeBinding = null +const loadErrors = [] + +const isMusl = () => { + let musl = false + if (process.platform === 'linux') { + musl = isMuslFromFilesystem() + if (musl === null) { + musl = isMuslFromReport() + } + if (musl === null) { + musl = isMuslFromChildProcess() + } + } + return musl +} + +const isFileMusl = (f) => f.includes('libc.musl-') || f.includes('ld-musl-') + +const isMuslFromFilesystem = () => { + try { + return readFileSync('/usr/bin/ldd', 'utf-8').includes('musl') + } catch { + return null + } +} + +const isMuslFromReport = () => { + let report = null + if (typeof process.report?.getReport === 'function') { + process.report.excludeNetwork = true + report = process.report.getReport() + } + if (!report) { + return null + } + if (report.header && report.header.glibcVersionRuntime) { + return false + } + if (Array.isArray(report.sharedObjects)) { + if (report.sharedObjects.some(isFileMusl)) { + return true + } + } + return false +} + +const isMuslFromChildProcess = () => { + try { + return require('child_process').execSync('ldd --version', { encoding: 'utf8' }).includes('musl') + } catch (e) { + // If we reach this case, we don't know if the system is musl or not, so is better to just fallback to false + return false + } +} + +function requireNative() { + if (process.env.NAPI_RS_NATIVE_LIBRARY_PATH) { + try { + return require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH); + } catch (err) { + loadErrors.push(err) + } + } else if (process.platform === 'android') { + if (process.arch === 'arm64') { + try { + return require('./gssapi.android-arm64.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('gssapi-android-arm64') + const bindingPackageVersion = require('gssapi-android-arm64/package.json').version + if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else if (process.arch === 'arm') { + try { + return require('./gssapi.android-arm-eabi.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('gssapi-android-arm-eabi') + const bindingPackageVersion = require('gssapi-android-arm-eabi/package.json').version + if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + loadErrors.push(new Error(`Unsupported architecture on Android ${process.arch}`)) + } + } else if (process.platform === 'win32') { + if (process.arch === 'x64') { + if (process.config?.variables?.shlib_suffix === 'dll.a' || process.config?.variables?.node_target_type === 'shared_library') { + try { + return require('./gssapi.win32-x64-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('gssapi-win32-x64-gnu') + const bindingPackageVersion = require('gssapi-win32-x64-gnu/package.json').version + if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + try { + return require('./gssapi.win32-x64-msvc.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('gssapi-win32-x64-msvc') + const bindingPackageVersion = require('gssapi-win32-x64-msvc/package.json').version + if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } + } else if (process.arch === 'ia32') { + try { + return require('./gssapi.win32-ia32-msvc.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('gssapi-win32-ia32-msvc') + const bindingPackageVersion = require('gssapi-win32-ia32-msvc/package.json').version + if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else if (process.arch === 'arm64') { + try { + return require('./gssapi.win32-arm64-msvc.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('gssapi-win32-arm64-msvc') + const bindingPackageVersion = require('gssapi-win32-arm64-msvc/package.json').version + if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + loadErrors.push(new Error(`Unsupported architecture on Windows: ${process.arch}`)) + } + } else if (process.platform === 'darwin') { + try { + return require('./gssapi.darwin-universal.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('gssapi-darwin-universal') + const bindingPackageVersion = require('gssapi-darwin-universal/package.json').version + if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + if (process.arch === 'x64') { + try { + return require('./gssapi.darwin-x64.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('gssapi-darwin-x64') + const bindingPackageVersion = require('gssapi-darwin-x64/package.json').version + if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else if (process.arch === 'arm64') { + try { + return require('./gssapi.darwin-arm64.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('gssapi-darwin-arm64') + const bindingPackageVersion = require('gssapi-darwin-arm64/package.json').version + if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + loadErrors.push(new Error(`Unsupported architecture on macOS: ${process.arch}`)) + } + } else if (process.platform === 'freebsd') { + if (process.arch === 'x64') { + try { + return require('./gssapi.freebsd-x64.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('gssapi-freebsd-x64') + const bindingPackageVersion = require('gssapi-freebsd-x64/package.json').version + if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else if (process.arch === 'arm64') { + try { + return require('./gssapi.freebsd-arm64.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('gssapi-freebsd-arm64') + const bindingPackageVersion = require('gssapi-freebsd-arm64/package.json').version + if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + loadErrors.push(new Error(`Unsupported architecture on FreeBSD: ${process.arch}`)) + } + } else if (process.platform === 'linux') { + if (process.arch === 'x64') { + if (isMusl()) { + try { + return require('./gssapi.linux-x64-musl.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('gssapi-linux-x64-musl') + const bindingPackageVersion = require('gssapi-linux-x64-musl/package.json').version + if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + try { + return require('./gssapi.linux-x64-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('gssapi-linux-x64-gnu') + const bindingPackageVersion = require('gssapi-linux-x64-gnu/package.json').version + if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } + } else if (process.arch === 'arm64') { + if (isMusl()) { + try { + return require('./gssapi.linux-arm64-musl.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('gssapi-linux-arm64-musl') + const bindingPackageVersion = require('gssapi-linux-arm64-musl/package.json').version + if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + try { + return require('./gssapi.linux-arm64-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('gssapi-linux-arm64-gnu') + const bindingPackageVersion = require('gssapi-linux-arm64-gnu/package.json').version + if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } + } else if (process.arch === 'arm') { + if (isMusl()) { + try { + return require('./gssapi.linux-arm-musleabihf.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('gssapi-linux-arm-musleabihf') + const bindingPackageVersion = require('gssapi-linux-arm-musleabihf/package.json').version + if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + try { + return require('./gssapi.linux-arm-gnueabihf.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('gssapi-linux-arm-gnueabihf') + const bindingPackageVersion = require('gssapi-linux-arm-gnueabihf/package.json').version + if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } + } else if (process.arch === 'loong64') { + if (isMusl()) { + try { + return require('./gssapi.linux-loong64-musl.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('gssapi-linux-loong64-musl') + const bindingPackageVersion = require('gssapi-linux-loong64-musl/package.json').version + if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + try { + return require('./gssapi.linux-loong64-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('gssapi-linux-loong64-gnu') + const bindingPackageVersion = require('gssapi-linux-loong64-gnu/package.json').version + if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } + } else if (process.arch === 'riscv64') { + if (isMusl()) { + try { + return require('./gssapi.linux-riscv64-musl.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('gssapi-linux-riscv64-musl') + const bindingPackageVersion = require('gssapi-linux-riscv64-musl/package.json').version + if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + try { + return require('./gssapi.linux-riscv64-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('gssapi-linux-riscv64-gnu') + const bindingPackageVersion = require('gssapi-linux-riscv64-gnu/package.json').version + if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } + } else if (process.arch === 'ppc64') { + try { + return require('./gssapi.linux-ppc64-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('gssapi-linux-ppc64-gnu') + const bindingPackageVersion = require('gssapi-linux-ppc64-gnu/package.json').version + if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else if (process.arch === 's390x') { + try { + return require('./gssapi.linux-s390x-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('gssapi-linux-s390x-gnu') + const bindingPackageVersion = require('gssapi-linux-s390x-gnu/package.json').version + if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + loadErrors.push(new Error(`Unsupported architecture on Linux: ${process.arch}`)) + } + } else if (process.platform === 'openharmony') { + if (process.arch === 'arm64') { + try { + return require('./gssapi.openharmony-arm64.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('gssapi-openharmony-arm64') + const bindingPackageVersion = require('gssapi-openharmony-arm64/package.json').version + if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else if (process.arch === 'x64') { + try { + return require('./gssapi.openharmony-x64.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('gssapi-openharmony-x64') + const bindingPackageVersion = require('gssapi-openharmony-x64/package.json').version + if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else if (process.arch === 'arm') { + try { + return require('./gssapi.openharmony-arm.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('gssapi-openharmony-arm') + const bindingPackageVersion = require('gssapi-openharmony-arm/package.json').version + if (bindingPackageVersion !== '1.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 1.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + loadErrors.push(new Error(`Unsupported architecture on OpenHarmony: ${process.arch}`)) + } + } else { + loadErrors.push(new Error(`Unsupported OS: ${process.platform}, architecture: ${process.arch}`)) + } +} + +nativeBinding = requireNative() + +if (!nativeBinding || process.env.NAPI_RS_FORCE_WASI) { + let wasiBinding = null + let wasiBindingError = null + try { + wasiBinding = require('./gssapi.wasi.cjs') + nativeBinding = wasiBinding + } catch (err) { + if (process.env.NAPI_RS_FORCE_WASI) { + wasiBindingError = err + } + } + if (!nativeBinding) { + try { + wasiBinding = require('gssapi-wasm32-wasi') + nativeBinding = wasiBinding + } catch (err) { + if (process.env.NAPI_RS_FORCE_WASI) { + wasiBindingError.cause = err + loadErrors.push(err) + } + } + } + if (process.env.NAPI_RS_FORCE_WASI === 'error' && !wasiBinding) { + const error = new Error('WASI binding not found and NAPI_RS_FORCE_WASI is set to error') + error.cause = wasiBindingError + throw error + } +} + +if (!nativeBinding) { + if (loadErrors.length > 0) { + throw new Error( + `Cannot find native binding. ` + + `npm has a bug related to optional dependencies (https://github.com/npm/cli/issues/4828). ` + + 'Please try `npm i` again after removing both package-lock.json and node_modules directory.', + { + cause: loadErrors.reduce((err, cur) => { + cur.cause = err + return cur + }), + }, + ) + } + throw new Error(`Failed to load native binding`) +} + +module.exports = nativeBinding +module.exports.GSSAPI = nativeBinding.GSSAPI diff --git a/native/package.json b/native/package.json new file mode 100644 index 0000000..8f3f419 --- /dev/null +++ b/native/package.json @@ -0,0 +1,108 @@ +{ + "name": "gssapi", + "version": "1.0.0", + "description": "Template project for writing node package with napi-rs", + "main": "index.js", + "repository": { + "type": "git", + "url": "git+ssh://git@github.com/napi-rs/package-template.git" + }, + "license": "Apache-2.0", + "browser": "browser.js", + "keywords": [ + "napi-rs", + "NAPI", + "N-API", + "Rust", + "node-addon", + "node-addon-api" + ], + "files": [ + "index.d.ts", + "index.js", + "browser.js" + ], + "napi": { + "binaryName": "gssapi", + "targets": [ + "x86_64-pc-windows-msvc", + "x86_64-apple-darwin", + "x86_64-unknown-linux-gnu", + "aarch64-unknown-linux-gnu", + "aarch64-apple-darwin" + ] + }, + "engines": { + "node": ">= 12.22.0 < 13 || >= 14.17.0 < 15 || >= 15.12.0 < 16 || >= 16.0.0" + }, + "publishConfig": { + "registry": "https://registry.npmjs.org/", + "access": "public" + }, + "scripts": { + "artifacts": "napi artifacts", + "bench": "node --import @oxc-node/core/register benchmark/bench.ts", + "build": "napi build --platform --release", + "build:debug": "napi build --platform", + "format": "run-p format:prettier format:rs format:toml", + "format:prettier": "prettier . -w", + "format:toml": "taplo format", + "format:rs": "cargo fmt", + "lint": "oxlint .", + "prepublishOnly": "napi prepublish -t npm", + "test": "ava", + "preversion": "napi build --platform && git add .", + "version": "napi version", + "prepare": "husky" + }, + "devDependencies": { + "@emnapi/core": "^1.5.0", + "@emnapi/runtime": "^1.5.0", + "@napi-rs/cli": "^3.2.0", + "@oxc-node/core": "^0.0.32", + "@taplo/cli": "^0.7.0", + "@tybys/wasm-util": "^0.10.0", + "ava": "^6.4.1", + "chalk": "^5.6.2", + "husky": "^9.1.7", + "lint-staged": "^16.1.6", + "npm-run-all2": "^8.0.4", + "oxlint": "^1.14.0", + "prettier": "^3.6.2", + "tinybench": "^5.0.1", + "typescript": "^5.9.2" + }, + "lint-staged": { + "*.@(js|ts|tsx)": [ + "oxlint --fix" + ], + "*.@(js|ts|tsx|yml|yaml|md|json)": [ + "prettier --write" + ], + "*.toml": [ + "taplo format" + ] + }, + "ava": { + "extensions": { + "ts": "module" + }, + "timeout": "2m", + "workerThreads": false, + "environmentVariables": { + "OXC_TSCONFIG_PATH": "./__test__/tsconfig.json" + }, + "nodeArguments": [ + "--import", + "@oxc-node/core/register" + ] + }, + "prettier": { + "printWidth": 120, + "semi": false, + "trailingComma": "all", + "singleQuote": true, + "arrowParens": "always" + }, + "packageManager": "yarn@4.10.3" +} diff --git a/native/rustfmt.toml b/native/rustfmt.toml new file mode 100644 index 0000000..6db34ff --- /dev/null +++ b/native/rustfmt.toml @@ -0,0 +1,10 @@ +combine_control_expr = false +fn_single_line = true +force_multiline_blocks = true +format_strings = true +group_imports = "StdExternalCrate" +max_width = 120 +normalize_comments = true +reorder_imports = true +tab_spaces = 2 +wrap_comments = true \ No newline at end of file diff --git a/native/src/includes.h b/native/src/includes.h new file mode 100644 index 0000000..c2a7f65 --- /dev/null +++ b/native/src/includes.h @@ -0,0 +1,3 @@ +#include +#include +#include \ No newline at end of file diff --git a/native/src/lib.rs b/native/src/lib.rs new file mode 100644 index 0000000..7f9505d --- /dev/null +++ b/native/src/lib.rs @@ -0,0 +1,730 @@ +#![deny(clippy::all)] + +use napi::bindgen_prelude::*; +use napi_derive::napi; +use std::env; +use std::ffi::{c_char, c_int, c_uchar, c_uint, c_void, CStr, CString}; +use std::path::Path; + +#[allow(warnings)] +mod bindings { + include!(concat!(env!("OUT_DIR"), "/bindings.rs")); +} + +use bindings::*; + +#[link(name = "krb5")] +extern "C" { + fn krb5_get_error_message(context: *mut c_void, code: c_int) -> *const c_char; + fn krb5_free_error_message(context: *mut c_void, message: *const c_char); + + fn krb5_init_context(context: *mut *mut c_void) -> i32; + fn krb5_free_context(context: *mut c_void); + + fn krb5_cc_default(context: *mut c_void, ccache: *mut *mut c_void) -> c_int; + fn krb5_cc_initialize(context: *mut c_void, cache: *mut c_void, principal: *mut c_void) -> c_int; + fn krb5_cc_close(context: *mut c_void, cache: *mut c_void) -> c_int; + fn krb5_cc_store_cred(context: *mut c_void, cache: *mut c_void, creds: *mut c_void) -> c_int; + + fn krb5_parse_name(context: *mut c_void, name: *const c_char, principal: *mut *mut c_void) -> c_int; + fn krb5_free_principal(context: *mut c_void, principal: *mut c_void); + + fn krb5_kt_resolve(context: *mut c_void, name: *const c_char, keytab: *mut *mut c_void) -> c_int; + fn krb5_kt_close(context: *mut c_void, keytab: *mut c_void) -> c_int; + + fn krb5_get_init_creds_password( + context: *mut c_void, + creds: *mut c_void, + principal: *mut c_void, + password: *const c_char, + prompter: *const c_void, + data: *const c_void, + start_time: c_uint, + in_tkt_service: *const c_char, + options: *const c_void, + ) -> c_int; + + fn krb5_get_init_creds_keytab( + context: *mut c_void, + creds: *mut c_void, + principal: *mut c_void, + keytab: *mut c_void, + start_time: c_uint, + in_tkt_service: *const c_char, + options: *const c_void, + ) -> c_int; + + fn krb5_free_cred_contents(context: *mut c_void, creds: *mut c_void); + + fn gss_display_status( + minor_status: *mut c_uint, + status_value: c_uint, + status_type: c_uint, + mech_type: *const c_void, + message_context: *mut c_uint, + status_string: *mut GssBufferDesc, + ) -> c_uint; + + fn gss_init_sec_context( + minor_status: *mut c_uint, + initiator_cred_handle: *const c_void, + context_handle: *mut *mut c_void, + target_name: *const c_void, + mech_type: *const c_void, + req_flags: c_uint, + time_req: c_uint, + input_chan_bindings: *const c_void, + input_token: *const GssBufferDesc, + actual_mech_type: *mut *const c_void, + output_token: *mut GssBufferDesc, + ret_flags: *mut c_uint, + time_rec: *mut c_uint, + ) -> c_uint; + + fn gss_delete_sec_context( + minor_status: *mut c_uint, + context_handle: *mut *mut c_void, + output_token: *mut GssBufferDesc, + ) -> c_uint; + + fn gss_import_name( + minor_status: *mut c_uint, + input_name_buffer: *const GssBufferDesc, + input_name_type: *const c_void, + output_name: *mut *mut c_void, + ) -> c_uint; + fn gss_release_name(minor_status: *mut c_uint, name: *mut *mut c_void) -> c_uint; + + fn gss_wrap( + minor_status: *mut c_uint, + context_handle: *const c_void, + conf_req_flag: c_int, + qop_req: c_uint, + input_message_buffer: *const GssBufferDesc, + conf_state: *mut c_int, + output_message_buffer: *mut GssBufferDesc, + ) -> c_uint; + + fn gss_unwrap( + minor_status: *mut c_uint, + context_handle: *const c_void, + input_message_buffer: *const GssBufferDesc, + output_message_buffer: *mut GssBufferDesc, + conf_state: *mut c_int, + qop_state: *mut c_uint, + ) -> c_uint; + + fn gss_release_buffer(minor_status: *mut c_uint, buffer: *mut GssBufferDesc) -> c_uint; +} + +// OID 1.2.840.113554.1.2.2 +#[cfg(target_os = "macos")] +const GSS_MECH_KRB5_BYTES: [u8; 9] = [0x2a, 0x86, 0x48, 0x86, 0xf7, 0x12, 0x01, 0x02, 0x02]; + +#[cfg(target_os = "macos")] +const GSS_MECH_KRB5: GssOidDesc = GssOidDesc { + length: 9, + elements: GSS_MECH_KRB5_BYTES.as_ptr(), +}; + +// OID 1.2.840.113554.1.2.1.4 +#[cfg(target_os = "macos")] +const GSS_C_NT_HOSTBASED_SERVICE_ELEMENTS: [u8; 10] = [0x2a, 0x86, 0x48, 0x86, 0xf7, 0x12, 0x01, 0x02, 0x01, 0x04]; + +#[cfg(target_os = "macos")] +const GSS_C_NT_HOSTBASED_SERVICE: GssOidDesc = GssOidDesc { + length: 10, + elements: GSS_C_NT_HOSTBASED_SERVICE_ELEMENTS.as_ptr(), +}; + +#[repr(C)] +#[derive(Debug)] +pub struct Krb5Data { + pub magic: c_int, + pub length: c_uint, + pub data: *mut c_char, +} + +#[repr(C)] +#[derive(Debug)] +pub struct Krb5Principal { + pub magic: c_int, + pub realm: Krb5Data, + pub data: *mut Krb5Data, + pub length: c_int, + pub type_: c_int, +} + +#[repr(C)] +#[derive(Debug)] +pub struct Krb5Keyblock { + pub magic: c_int, + pub enctype: c_int, + pub length: c_uint, + pub contents: *mut u8, +} + +#[repr(C)] +#[derive(Debug)] +pub struct Krb5TicketTimes { + pub authtime: c_int, + pub starttime: c_int, + pub endtime: c_int, + pub renew_till: c_int, +} + +#[repr(C)] +#[derive(Debug)] +pub struct Krb5Address { + pub magic: c_int, + pub addrtype: c_int, + pub length: c_uint, + pub contents: *mut u8, +} + +#[repr(C)] +#[derive(Debug)] +pub struct Krb5Authdata { + pub magic: c_int, + pub ad_type: c_int, + pub length: c_uint, + pub contents: *mut u8, +} + +#[repr(C)] +#[derive(Debug)] +pub struct Krb5Creds { + pub magic: c_int, + pub client: *const Krb5Principal, + pub server: *const Krb5Principal, + pub keyblock: Krb5Keyblock, + pub times: Krb5TicketTimes, + pub is_skey: c_int, + pub ticket_flags: c_uint, + pub addresses: *const *const Krb5Address, + pub ticket: Krb5Data, + pub second_ticket: Krb5Data, + pub authdata: *const *const Krb5Authdata, +} + +#[repr(C)] +#[derive(Debug)] +struct GssOidDesc { + length: c_uint, + elements: *const c_uchar, +} + +#[repr(C)] +#[derive(Debug)] +struct GssBufferDesc { + length: usize, + value: *mut c_void, +} + +#[napi(js_name = "GSSAPI")] +pub struct GSSAPI { + config_path: String, + cache_path: String, + context: *mut c_void, + cache: *mut c_void, + gss: *mut c_void, +} + +use std::sync::{Mutex, MutexGuard, OnceLock}; + +static GSSAPI_LOCK: OnceLock> = OnceLock::new(); + +fn gssapi_lock() -> &'static Mutex<()> { + GSSAPI_LOCK.get_or_init(|| Mutex::new(())) +} + +#[napi(object)] +pub struct StepResult { + pub output: Buffer, + pub completed: bool, +} + +struct EnvManager<'a> { + _guard: MutexGuard<'a, ()>, + prev_krbconfig: Option, + prev_krbcache: Option, +} + +impl<'a> EnvManager<'a> { + pub fn new(config_path: &str, cache_path: &str) -> Self { + let _guard = gssapi_lock().lock().unwrap(); + let prev_krbconfig = env::var("KRB5_CONFIG").ok(); + let prev_krbcache = env::var("KRB5CCNAME").ok(); + + env::set_var("KRB5_CONFIG", config_path); + env::set_var("KRB5CCNAME", cache_path); + + Self { + _guard, + prev_krbconfig, + prev_krbcache, + } + } + + pub fn new_from_api(api: &GSSAPI) -> Self { + EnvManager::new(&api.config_path, &api.cache_path) + } +} + +impl<'a> Drop for EnvManager<'a> { + fn drop(&mut self) { + if let Some(prev) = &self.prev_krbconfig { + env::set_var("KRB5_CONFIG", prev); + } else { + env::remove_var("KRB5_CONFIG"); + } + + if let Some(prev) = &self.prev_krbcache { + env::set_var("KRB5CCNAME", prev); + } else { + env::remove_var("KRB5CCNAME"); + } + } +} + +// TODO: Use RAII wrappers for everything +// TODO: Make all function async +#[napi] +impl GSSAPI { + #[napi(constructor)] + pub unsafe fn new(kdc: String, realm: String) -> Result { + let uuid = uuid::Uuid::new_v4().to_string(); + let temp_dir = env::temp_dir(); + + let config_path = temp_dir + .join(format!("plt-kafka-krb5-{}.conf", uuid)) + .to_string_lossy() + .to_string(); + + let cache_path = temp_dir + .join(format!("plt-kafka-krb5-{}.cache", uuid)) + .to_string_lossy() + .to_string(); + + let _env = EnvManager::new(&config_path, &cache_path); + + // Write the config file + { + let config = format!( + r#" +[libdefaults] + default_realm = {0} + default_ccache_name = FILE:{2} + +[realms] + {0} = {{ + kdc = {1} + }} +"#, + realm, kdc, cache_path + ); + + if let Err(e) = std::fs::write(&config_path, config) { + return Err(Error::from_reason(format!("Failed to write Kerberos config: {}", e))); + } + } + + // Create the Kerberos context + let mut context = std::ptr::null_mut(); + let ret = krb5_init_context(&mut context); + + if ret != 0 { + let _ = std::fs::remove_file(&config_path); + let _ = std::fs::remove_file(&cache_path); + + return Err(Error::from_reason(format!( + "krb5_init_context failed with error code {}.", + ret + ))); + } + + // Get the default cache + let mut cache = std::ptr::null_mut(); + let ret = krb5_cc_default(context, &mut cache); + + if ret != 0 { + krb5_free_context(context); + + let _ = std::fs::remove_file(&config_path); + + return Err(Error::from_reason(format!( + "krb5_cc_default failed with error code {}.", + ret + ))); + } + + Ok(Self { + config_path, + cache_path, + context, + cache, + gss: std::ptr::null_mut(), + }) + } + + #[napi] + pub unsafe fn authenticate_with_password(&self, username: String, password: String) -> Result<()> { + let _env = EnvManager::new_from_api(&self); + + // Parse the username to create a principal + let username_c = CString::new(username).unwrap(); + let mut principal = std::ptr::null_mut(); + let ret = krb5_parse_name(self.context, username_c.as_ptr(), &mut principal); + + if ret != 0 { + return Err(self.format_kerberos_error("krb5_parse_name failed", ret)); + } + + let password_c = CString::new(password).unwrap(); + let mut creds: Krb5Creds = std::mem::zeroed(); + + let ret = krb5_get_init_creds_password( + self.context, + &mut creds as *mut _ as *mut c_void, + principal, + password_c.as_ptr(), + std::ptr::null(), + std::ptr::null(), + 0, + std::ptr::null(), + std::ptr::null(), + ); + + if ret != 0 { + krb5_free_principal(self.context, principal); + + if ret == KRB5_KDC_UNREACH { + return Err(Error::from_reason(format!("Unable to reach the KDC."))); + } else if ret == KRB5_REALM_CANT_RESOLVE { + return Err(Error::from_reason(format!("Cannot resolve the realm."))); + } + + return Err(self.format_kerberos_error("krb5_get_init_creds_password failed", ret)); + } + + let ret = krb5_cc_initialize(self.context, self.cache, principal); + + if ret != 0 { + krb5_free_principal(self.context, principal); + return Err(self.format_kerberos_error("krb5_cc_initialize failed", ret)); + } + + let ret = krb5_cc_store_cred(self.context, self.cache, &mut creds as *mut _ as *mut c_void); + + krb5_free_principal(self.context, principal); + krb5_free_cred_contents(self.context, &mut creds as *mut _ as *mut c_void); + + if ret != 0 { + return Err(self.format_kerberos_error("krb5_cc_store_cred failed", ret)); + } + + Ok(()) + } + + #[napi] + pub unsafe fn authenticate_with_keytab(&self, username: String, keytab: String) -> Result<()> { + let _env = EnvManager::new_from_api(&self); + + if !Path::new(&keytab).exists() { + return Err(Error::from_reason(format!("Keytab file not found: {}", keytab))); + } + + // Parse the username to create a principal + let username_c = CString::new(username).unwrap(); + let mut principal = std::ptr::null_mut(); + let ret = krb5_parse_name(self.context, username_c.as_ptr(), &mut principal); + + if ret != 0 { + return Err(self.format_kerberos_error("krb5_parse_name failed", ret)); + } + + // Resolve keytab + let keytab_str = format!("FILE:{}", keytab); + let keytab_c = CString::new(keytab_str).unwrap(); + let mut keytab = std::ptr::null_mut(); + let ret = krb5_kt_resolve(self.context, keytab_c.as_ptr(), &mut keytab); + + if ret != 0 { + krb5_free_principal(self.context, principal); + return Err(self.format_kerberos_error("krb5_kt_resolve failed", ret)); + } + + // Get credentials from keytab + let mut creds: Krb5Creds = std::mem::zeroed(); + let ret = krb5_get_init_creds_keytab( + self.context, + &mut creds as *mut _ as *mut c_void, + principal, + keytab, + 0, + std::ptr::null(), + std::ptr::null(), + ); + krb5_kt_close(self.context, keytab); + + if ret != 0 { + krb5_free_principal(self.context, principal); + + if ret == KRB5_KDC_UNREACH { + return Err(Error::from_reason(format!("Unable to reach the KDC."))); + } else if ret == KRB5_REALM_CANT_RESOLVE { + return Err(Error::from_reason(format!("Cannot resolve the realm."))); + } + + return Err(self.format_kerberos_error("krb5_get_init_creds_keytab failed", ret)); + } + + let ret = krb5_cc_initialize(self.context, self.cache, principal); + + if ret != 0 { + krb5_free_principal(self.context, principal); + return Err(self.format_kerberos_error("krb5_cc_initialize failed", ret)); + } + + let ret = krb5_cc_store_cred(self.context, self.cache, &mut creds as *mut _ as *mut c_void); + + krb5_free_principal(self.context, principal); + krb5_free_cred_contents(self.context, &mut creds as *mut _ as *mut c_void); + + if ret != 0 { + return Err(self.format_kerberos_error("krb5_cc_store_cred failed", ret)); + } + + Ok(()) + } + + #[napi] + pub unsafe fn step(&mut self, service: String, input: Option) -> Result { + let _env = EnvManager::new_from_api(&self); + let mut minor: c_uint = 0; + + let service_buf = GssBufferDesc { + length: service.len(), + value: service.as_ptr() as *mut c_void, + }; + + let mut target_name = std::ptr::null_mut(); + let ret = gss_import_name( + &mut minor, + &service_buf, + &GSS_C_NT_HOSTBASED_SERVICE as *const _ as *const c_void, + &mut target_name, + ); + + if ret != 0 { + return Err(self.format_gss_error("gss_import_name failed", ret, minor)); + } + + let mut output = GssBufferDesc { + length: 0, + value: std::ptr::null_mut(), + }; + + let input_buf = input.map(|buf| GssBufferDesc { + length: buf.len(), + value: buf.as_ptr() as *mut c_void, + }); + + let ret = gss_init_sec_context( + &mut minor, + std::ptr::null(), + &mut self.gss, + target_name, + &GSS_MECH_KRB5 as *const _ as *const c_void, + 0, + 0, + std::ptr::null(), + input_buf.as_ref().map_or(std::ptr::null(), |b| b as *const _), + std::ptr::null_mut(), + &mut output, + std::ptr::null_mut(), + std::ptr::null_mut(), + ); + let init_minor = minor; + + const GSS_S_COMPLETE: c_uint = 0; + const GSS_S_CONTINUE_NEEDED: c_uint = 1; + + gss_release_name(&mut minor, &mut target_name); + + if ret != GSS_S_COMPLETE && ret != GSS_S_CONTINUE_NEEDED { + return Err(self.format_gss_error("gss_init_sec_context failed", ret, init_minor)); + } + + let result = Buffer::from(std::slice::from_raw_parts( + output.value as *const c_uchar, + output.length, + )); + gss_release_buffer(&mut minor, &mut output); + + Ok(StepResult { + output: result, + completed: ret == GSS_S_COMPLETE, + }) + } + + #[napi] + pub unsafe fn wrap(&self, data: Buffer) -> Result { + let mut minor: c_uint = 0; + + let input = GssBufferDesc { + length: data.len(), + value: data.as_ptr() as *mut c_void, + }; + + let mut output = GssBufferDesc { + length: 0, + value: std::ptr::null_mut(), + }; + + let ret = gss_wrap(&mut minor, self.gss, 0, 0, &input, std::ptr::null_mut(), &mut output); + + if ret != 0 { + return Err(self.format_gss_error("gss_wrap failed", ret, minor)); + } + + let result = Buffer::from(std::slice::from_raw_parts( + output.value as *const c_uchar, + output.length, + )); + + gss_release_buffer(&mut minor, &mut output); + Ok(result) + } + + #[napi] + pub unsafe fn unwrap(&self, data: Buffer) -> Result { + let mut minor: c_uint = 0; + + let input = GssBufferDesc { + length: data.len(), + value: data.as_ptr() as *mut c_void, + }; + + let mut output = GssBufferDesc { + length: 0, + value: std::ptr::null_mut(), + }; + + let ret = gss_unwrap( + &mut minor, + self.gss, + &input, + &mut output, + std::ptr::null_mut(), + std::ptr::null_mut(), + ); + + if ret != 0 { + return Err(self.format_gss_error("gss_unwrap failed", ret, minor)); + } + + let result = Buffer::from(std::slice::from_raw_parts( + output.value as *const c_uchar, + output.length, + )); + + gss_release_buffer(&mut minor, &mut output); + Ok(result) + } + + unsafe fn format_kerberos_error(&self, prefix: &str, code: c_int) -> Error { + let message = krb5_get_error_message(self.context, code); + let error = Error::from_reason(format!( + "{}: {}. (error code {})", + prefix, + CStr::from_ptr(message).to_string_lossy(), + code + )); + krb5_free_error_message(self.context, message); + + return error; + } + + unsafe fn format_gss_error(&self, prefix: &str, major: c_uint, minor: c_uint) -> Error { + let mut msg_ctx: c_uint = 0; + let mut status_string = GssBufferDesc { + length: 0, + value: std::ptr::null_mut(), + }; + let mut min: c_uint = 0; + + let ret = gss_display_status( + &mut min, + major, + GSS_C_GSS_CODE, + std::ptr::null(), + &mut msg_ctx, + &mut status_string, + ); + + if ret != 0 { + return Error::from_reason(format!("{}: unknown error. (error code {})", prefix, major)); + } + + let mut error_message = std::str::from_utf8(std::slice::from_raw_parts( + status_string.value as *const c_uchar, + status_string.length, + )) + .unwrap() + .to_string(); + + gss_release_buffer(&mut min, &mut status_string); + if minor != 0 { + let ret = gss_display_status( + &mut min, + minor, + GSS_C_MECH_CODE, + std::ptr::null(), + &mut msg_ctx, + &mut status_string, + ); + + if ret == 0 { + error_message.push_str( + std::str::from_utf8(std::slice::from_raw_parts( + status_string.value as *const c_uchar, + status_string.length, + )) + .unwrap(), + ); + + gss_release_buffer(&mut min, &mut status_string); + } + } + + let error = Error::from_reason(format!( + "{}: {}. (error code {} - {})", + prefix, error_message, major, minor + )); + + error + } +} + +impl Drop for GSSAPI { + fn drop(&mut self) { + unsafe { + let _ = std::fs::remove_file(self.config_path.clone()); + let _ = std::fs::remove_file(self.cache_path.clone()); + + if !self.gss.is_null() { + let mut minor: c_uint = 0; + gss_delete_sec_context(&mut minor, &mut self.gss, std::ptr::null_mut()); + } + + if !self.cache.is_null() { + krb5_cc_close(self.context, self.cache); + } + + if !self.context.is_null() { + krb5_free_context(self.context); + } + } + } +} diff --git a/package.json b/package.json index f466b0f..d17ee0a 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "ajv": "^8.17.1", "debug": "^4.4.3", "fastq": "^1.19.1", + "kerberos": "^2.2.2", "mnemonist": "^0.40.3", "scule": "^1.3.0" }, @@ -68,8 +69,8 @@ "eslint": "^9.35.0", "fast-jwt": "^6.0.2", "hwp": "^0.4.1", - "kafkajs": "^2.2.4", "json5": "^2.2.3", + "kafkajs": "^2.2.4", "neostandard": "^0.12.2", "parse5": "^7.3.0", "prettier": "^3.6.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 51627f1..d597e9a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: fastq: specifier: ^1.19.1 version: 1.19.1 + kerberos: + specifier: ^2.2.2 + version: 2.2.2 mnemonist: specifier: ^0.40.3 version: 0.40.3 @@ -829,6 +832,9 @@ packages: bintrees@1.0.2: resolution: {integrity: sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==} + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + bn.js@4.12.2: resolution: {integrity: sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==} @@ -842,6 +848,9 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + c8@10.1.3: resolution: {integrity: sha512-LvcyrOAaOnrrlMpW22n690PUvxiq4Uf9WMhQwNJ9vgagkL/ph1+D4uvjvDA5XCbykrc0sx+ay6pVi9YZ1GnhyA==} engines: {node: '>=18'} @@ -872,6 +881,9 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + chownr@2.0.0: resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} engines: {node: '>=10'} @@ -937,6 +949,14 @@ packages: supports-color: optional: true + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -975,6 +995,9 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + enhanced-resolve@5.18.3: resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} engines: {node: '>=10.13.0'} @@ -1130,6 +1153,10 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1192,6 +1219,9 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + fs-minipass@2.1.0: resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} engines: {node: '>= 8'} @@ -1237,6 +1267,9 @@ packages: get-tsconfig@4.11.0: resolution: {integrity: sha512-sNsqf7XKQ38IawiVGPOoAlqZo1DMrO7TU+ZcZwi7yLl7/7S0JwmoBMKz/IkUPhSoXM0Ng3vT0yB1iCe5XavDeQ==} + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -1322,6 +1355,9 @@ packages: hwp@0.4.1: resolution: {integrity: sha512-aN+FxnVyrNmIx6ULuAyefgQffE0NF9GRzW+lB5cAp5kxN5PBdjf8Ws+IvDHHJM1a7pyXTP9t0gwz7E/Z5ikj8w==} + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1345,6 +1381,9 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + internal-slot@1.1.0: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} @@ -1513,6 +1552,10 @@ packages: resolution: {integrity: sha512-j/YeapB1vfPT2iOIUn/vxdyKEuhuY2PxMBvf5JWux6iSaukAccrMtXEY/Lb7OvavDhOWME589bpLrEdnVHjfjA==} engines: {node: '>=14.0.0'} + kerberos@2.2.2: + resolution: {integrity: sha512-42O7+/1Zatsc3MkxaMPpXcIl/ukIrbQaGoArZEAr6GcEi2qhfprOBYOPhj+YvSMJkEkdpTjApUx+2DuWaKwRhg==} + engines: {node: '>=12.9.0'} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -1561,6 +1604,10 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + minimalistic-assert@1.0.1: resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} @@ -1575,6 +1622,9 @@ packages: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minipass@3.3.6: resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} engines: {node: '>=8'} @@ -1591,6 +1641,9 @@ packages: resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} engines: {node: '>= 8'} + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + mkdirp@1.0.4: resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} engines: {node: '>=10'} @@ -1605,6 +1658,9 @@ packages: nan@2.23.0: resolution: {integrity: sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==} + napi-build-utils@2.0.0: + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + napi-postinstall@0.3.4: resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -1620,6 +1676,13 @@ packages: peerDependencies: eslint: ^9.0.0 + node-abi@3.80.0: + resolution: {integrity: sha512-LyPuZJcI9HVwzXK1GPxWNzrr+vr8Hp/3UqlmWxxh8p54U1ZbclOqbSog9lWHaCX+dBaiGi6n/hIX+mKu74GmPA==} + engines: {node: '>=10'} + + node-addon-api@6.1.0: + resolution: {integrity: sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==} + node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -1736,6 +1799,11 @@ packages: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} + prebuild-install@7.1.3: + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} + engines: {node: '>=10'} + hasBin: true + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -1757,6 +1825,9 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + pump@3.0.3: + resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -1764,6 +1835,10 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -1886,6 +1961,12 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + slice-ansi@4.0.0: resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} engines: {node: '>=10'} @@ -1943,6 +2024,10 @@ packages: resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} engines: {node: '>=12'} + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -1963,6 +2048,13 @@ packages: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} + tar-fs@2.1.4: + resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + tar@6.2.1: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} @@ -1999,6 +2091,9 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -2773,6 +2868,12 @@ snapshots: bintrees@1.0.2: {} + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + bn.js@4.12.2: {} brace-expansion@1.1.12: @@ -2788,6 +2889,11 @@ snapshots: dependencies: fill-range: 7.1.1 + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + c8@10.1.3: dependencies: '@bcoe/v8-coverage': 1.0.2 @@ -2826,6 +2932,8 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + chownr@1.1.4: {} + chownr@2.0.0: {} cleaner-spec-reporter@0.5.0: {} @@ -2886,6 +2994,12 @@ snapshots: dependencies: ms: 2.1.3 + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + deep-extend@0.6.0: {} + deep-is@0.1.4: {} define-data-property@1.1.4: @@ -2924,6 +3038,10 @@ snapshots: emoji-regex@9.2.2: {} + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + enhanced-resolve@5.18.3: dependencies: graceful-fs: 4.2.11 @@ -3196,6 +3314,8 @@ snapshots: esutils@2.0.3: {} + expand-template@2.0.3: {} + fast-deep-equal@3.1.3: {} fast-glob@3.3.3: @@ -3258,6 +3378,8 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + fs-constants@1.0.0: {} + fs-minipass@2.1.0: dependencies: minipass: 3.3.6 @@ -3321,6 +3443,8 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + github-from-package@0.0.0: {} + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -3405,6 +3529,8 @@ snapshots: hwp@0.4.1: {} + ieee754@1.2.1: {} + ignore@5.3.2: {} ignore@7.0.5: {} @@ -3423,6 +3549,8 @@ snapshots: inherits@2.0.4: {} + ini@1.3.8: {} + internal-slot@1.1.0: dependencies: es-errors: 1.3.0 @@ -3600,6 +3728,11 @@ snapshots: kafkajs@2.2.4: {} + kerberos@2.2.2: + dependencies: + node-addon-api: 6.1.0 + prebuild-install: 7.1.3 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -3659,6 +3792,8 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mimic-response@3.1.0: {} + minimalistic-assert@1.0.1: {} minimatch@10.0.3: @@ -3673,6 +3808,8 @@ snapshots: dependencies: brace-expansion: 2.0.2 + minimist@1.2.8: {} + minipass@3.3.6: dependencies: yallist: 4.0.0 @@ -3686,6 +3823,8 @@ snapshots: minipass: 3.3.6 yallist: 4.0.0 + mkdirp-classic@0.5.3: {} + mkdirp@1.0.4: {} mnemonist@0.40.3: @@ -3696,6 +3835,8 @@ snapshots: nan@2.23.0: {} + napi-build-utils@2.0.0: {} + napi-postinstall@0.3.4: {} natural-compare@1.4.0: {} @@ -3721,6 +3862,12 @@ snapshots: - supports-color - typescript + node-abi@3.80.0: + dependencies: + semver: 7.7.2 + + node-addon-api@6.1.0: {} + node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 @@ -3834,6 +3981,21 @@ snapshots: possible-typed-array-names@1.1.0: {} + prebuild-install@7.1.3: + dependencies: + detect-libc: 2.1.2 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 2.0.0 + node-abi: 3.80.0 + pump: 3.0.3 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.4 + tunnel-agent: 0.6.0 + prelude-ls@1.2.1: {} prettier-plugin-space-before-function-paren@0.0.8(prettier@3.6.2): @@ -3853,10 +4015,22 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + pump@3.0.3: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + punycode@2.3.1: {} queue-microtask@1.2.3: {} + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + react-is@16.13.1: {} readable-stream@3.6.2: @@ -4000,6 +4174,14 @@ snapshots: signal-exit@4.1.0: {} + simple-concat@1.0.1: {} + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + slice-ansi@4.0.0: dependencies: ansi-styles: 4.3.0 @@ -4105,6 +4287,8 @@ snapshots: dependencies: ansi-regex: 6.2.2 + strip-json-comments@2.0.1: {} + strip-json-comments@3.1.1: {} supports-color@7.2.0: @@ -4123,6 +4307,21 @@ snapshots: tapable@2.3.0: {} + tar-fs@2.1.4: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.3 + tar-stream: 2.2.0 + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + tar@6.2.1: dependencies: chownr: 2.0.0 @@ -4165,6 +4364,10 @@ snapshots: tslib@2.8.1: optional: true + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..419c01b --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +onlyBuiltDependencies: + - kerberos diff --git a/src/apis/enumerations.ts b/src/apis/enumerations.ts index fb31983..09fddb4 100644 --- a/src/apis/enumerations.ts +++ b/src/apis/enumerations.ts @@ -3,7 +3,8 @@ export const SASLMechanisms = { PLAIN: 'PLAIN', SCRAM_SHA_256: 'SCRAM-SHA-256', SCRAM_SHA_512: 'SCRAM-SHA-512', - OAUTHBEARER: 'OAUTHBEARER' + OAUTHBEARER: 'OAUTHBEARER', + GSSAPI: 'GSSAPI' } as const export const allowedSASLMechanisms = Object.values(SASLMechanisms) as SASLMechanismValue[] diff --git a/src/clients/base/options.ts b/src/clients/base/options.ts index c6665be..76b121b 100644 --- a/src/clients/base/options.ts +++ b/src/clients/base/options.ts @@ -46,6 +46,7 @@ export const baseOptionsSchema = { username: { oneOf: [{ type: 'string' }, { function: true }] }, password: { oneOf: [{ type: 'string' }, { function: true }] }, token: { oneOf: [{ type: 'string' }, { function: true }] }, + keytab: { type: 'string' }, authBytesValidator: { function: true } }, required: ['mechanism'], diff --git a/src/network/connection.ts b/src/network/connection.ts index 780bdb8..b00d2f8 100644 --- a/src/network/connection.ts +++ b/src/network/connection.ts @@ -26,7 +26,7 @@ import { import { protocolAPIsById } from '../protocol/apis.ts' import { EMPTY_OR_SINGLE_COMPACT_LENGTH_SIZE, INT32_SIZE } from '../protocol/definitions.ts' import { DynamicBuffer } from '../protocol/dynamic-buffer.ts' -import { saslOAuthBearer, saslPlain, saslScramSha } from '../protocol/index.ts' +import { saslGssApi, saslOAuthBearer, saslPlain, saslScramSha } from '../protocol/index.ts' import { Reader } from '../protocol/reader.ts' import { defaultCrypto, type ScramAlgorithm } from '../protocol/sasl/scram-sha.ts' import { Writer } from '../protocol/writer.ts' @@ -43,6 +43,7 @@ export interface SASLOptions { username?: string | SASLCredentialProvider password?: string | SASLCredentialProvider token?: string | SASLCredentialProvider + keytab?: string | SASLCredentialProvider authBytesValidator?: (authBytes: Buffer, callback: CallbackWithPromise) => void } @@ -384,7 +385,7 @@ export class Connection extends EventEmitter { this.#status = ConnectionStatuses.AUTHENTICATING } - const { mechanism, username, password, token } = this.#options.sasl! + const { mechanism, username, password, token, keytab } = this.#options.sasl! if (!allowedSASLMechanisms.includes(mechanism)) { this.#onConnectionError( @@ -415,6 +416,8 @@ export class Connection extends EventEmitter { saslPlain.authenticate(saslAuthenticateV2.api, this, username!, password!, callback) } else if (mechanism === SASLMechanisms.OAUTHBEARER) { saslOAuthBearer.authenticate(saslAuthenticateV2.api, this, token!, callback) + } else if (mechanism === SASLMechanisms.GSSAPI) { + saslGssApi.authenticate(saslAuthenticateV2.api, this, username!, password!, keytab!, callback) } else { saslScramSha.authenticate( saslAuthenticateV2.api, diff --git a/src/protocol/index.ts b/src/protocol/index.ts index 1754d1e..e49c724 100644 --- a/src/protocol/index.ts +++ b/src/protocol/index.ts @@ -8,6 +8,7 @@ export * from './index.ts' export * from './murmur2.ts' export * from './reader.ts' export * from './records.ts' +export * as saslGssApi from './sasl/gssapi.ts' export * as saslOAuthBearer from './sasl/oauth-bearer.ts' export * as saslPlain from './sasl/plain.ts' export * as saslScramSha from './sasl/scram-sha.ts' diff --git a/src/protocol/sasl/gssapi.ts b/src/protocol/sasl/gssapi.ts new file mode 100644 index 0000000..5ba152c --- /dev/null +++ b/src/protocol/sasl/gssapi.ts @@ -0,0 +1,152 @@ +import { createRequire } from 'node:module' +import { createPromisifiedCallback, kCallbackPromise, type CallbackWithPromise } from '../../apis/callbacks.ts' +import { type SASLAuthenticationAPI, type SaslAuthenticateResponse } from '../../apis/security/sasl-authenticate-v2.ts' +import { AuthenticationError } from '../../errors.ts' +import { type Connection, type SASLCredentialProvider } from '../../network/connection.ts' +import { EMPTY_BUFFER } from '../definitions.ts' +import { getCredential } from './credential-provider.ts' + +function performChallenge ( + connection: Connection, + authenticateAPI: SASLAuthenticationAPI, + client: any, + target: string, + input: Buffer, + callback: CallbackWithPromise +): void { + try { + const { completed, output } = client.step(target, input) + + authenticateAPI(connection, output, (error, response) => { + /* c8 ignore next 7 - Hard to test */ + if (error) { + callback( + new AuthenticationError('SASL authentication failed.', { cause: error }), + undefined as unknown as SaslAuthenticateResponse + ) + return + } + + /* c8 ignore next 4 - Hard to test */ + if (!completed) { + performChallenge(connection, authenticateAPI, client, target, response.authBytes, callback) + return + } + + try { + client.unwrap(response.authBytes) + /* c8 ignore next 6 - Hard to test */ + } catch (e) { + callback( + new AuthenticationError('Cannot unwrap Kerberos response.', { kerberosError: e }), + undefined as unknown as SaslAuthenticateResponse + ) + } + + try { + // Byte 0: No security layer; Byte 1-3: max message size - 0=none + const wrapped = client.wrap(Buffer.from([1, 0, 0, 0])) + + authenticateAPI(connection, Buffer.from(wrapped, 'base64'), (error, response) => { + /* c8 ignore next 7 - Hard to test */ + if (error) { + callback( + new AuthenticationError('SASL authentication failed.', { cause: error }), + undefined as unknown as SaslAuthenticateResponse + ) + return + } + + callback(null, response) + }) + /* c8 ignore next 6 - Hard to test */ + } catch (e) { + callback( + new AuthenticationError('Cannot wrap Kerberos response.', { kerberosError: e }), + undefined as unknown as SaslAuthenticateResponse + ) + } + }) + /* c8 ignore next 6 - Hard to test */ + } catch (e) { + callback!( + new AuthenticationError('Cannot perform Kerberos step challenge.', { kerberosError: e }), + undefined as unknown as SaslAuthenticateResponse + ) + } +} + +export function authenticate ( + authenticateAPI: SASLAuthenticationAPI, + connection: Connection, + usernameProvider: string | SASLCredentialProvider, + passwordProvider: string | SASLCredentialProvider, + keytabProvider: string | SASLCredentialProvider, + callback: CallbackWithPromise +): void +export function authenticate ( + authenticateAPI: SASLAuthenticationAPI, + connection: Connection, + usernameProvider: string | SASLCredentialProvider, + passwordProvider: string | SASLCredentialProvider, + keytabProvider: string | SASLCredentialProvider +): Promise +export function authenticate ( + authenticateAPI: SASLAuthenticationAPI, + connection: Connection, + usernameProvider: string | SASLCredentialProvider, + passwordProvider: string | SASLCredentialProvider, + keytabProvider: string | SASLCredentialProvider, + callback?: CallbackWithPromise +): void | Promise { + /* c8 ignore next 3 - Hard to test */ + if (!callback) { + callback = createPromisifiedCallback() + } + + const require = createRequire(import.meta.url) + const { GSSAPI } = require('../../../native/gssapi.darwin-arm64.node') + + getCredential('SASL/GSSAPI username', usernameProvider, (error, username) => { + if (error) { + callback!(error, undefined as unknown as SaslAuthenticateResponse) + return + } + + let credentialProvider: string | SASLCredentialProvider + let credentialType: 'password' | 'keytab' + + if (passwordProvider) { + credentialType = 'password' + credentialProvider = passwordProvider + } else { + credentialType = 'keytab' + credentialProvider = keytabProvider + } + + getCredential(`SASL/GSSAPI ${credentialType}`, credentialProvider, (error, credential) => { + if (error) { + callback!(error, undefined as unknown as SaslAuthenticateResponse) + return + } + + const client = new GSSAPI('localhost:8000', 'EXAMPLE.COM') + try { + credentialType === 'password' + ? client.authenticateWithPassword(username, credential) + : client.authenticateWithKeytab(username, credential) + } catch (e) { + callback!( + new AuthenticationError('SASL authentication failed.', { kerberosError: e }), + undefined as unknown as SaslAuthenticateResponse + ) + + return + } + + performChallenge(connection, authenticateAPI, client, 'broker@broker-sasl-kerberos', EMPTY_BUFFER, callback!) + }) + }) + + return callback[kCallbackPromise] +} diff --git a/src/protocol/sasl/oauth-bearer.ts b/src/protocol/sasl/oauth-bearer.ts index 02e252f..a4be279 100644 --- a/src/protocol/sasl/oauth-bearer.ts +++ b/src/protocol/sasl/oauth-bearer.ts @@ -52,7 +52,8 @@ export function authenticate ( getCredential('SASL/OAUTHBEARER token', tokenOrProvider, (error, token) => { if (error) { - return callback!(error, undefined as unknown as SaslAuthenticateResponse) + callback!(error, undefined as unknown as SaslAuthenticateResponse) + return } authenticateAPI(connection, Buffer.from(`n,,\x01auth=Bearer ${token}\x01\x01`), callback!) diff --git a/src/protocol/sasl/plain.ts b/src/protocol/sasl/plain.ts index 0ada47e..f499e49 100644 --- a/src/protocol/sasl/plain.ts +++ b/src/protocol/sasl/plain.ts @@ -29,12 +29,14 @@ export function authenticate ( getCredential('SASL/PLAIN username', usernameProvider, (error, username) => { if (error) { - return callback!(error, undefined as unknown as SaslAuthenticateResponse) + callback!(error, undefined as unknown as SaslAuthenticateResponse) + return } getCredential('SASL/PLAIN password', passwordProvider, (error, password) => { if (error) { - return callback!(error, undefined as unknown as SaslAuthenticateResponse) + callback!(error, undefined as unknown as SaslAuthenticateResponse) + return } authenticateAPI(connection, Buffer.from(['', username, password].join('\0')), callback) diff --git a/src/protocol/sasl/scram-sha.ts b/src/protocol/sasl/scram-sha.ts index 842d515..af13b41 100644 --- a/src/protocol/sasl/scram-sha.ts +++ b/src/protocol/sasl/scram-sha.ts @@ -225,12 +225,14 @@ export function authenticate ( getCredential(`SASL/SCRAM-${algorithm} username`, usernameProvider, (error, username) => { if (error) { - return callback!(error, undefined as unknown as SaslAuthenticateResponse) + callback!(error, undefined as unknown as SaslAuthenticateResponse) + return } getCredential(`SASL/SCRAM-${algorithm} password`, passwordProvider, (error, password) => { if (error) { - return callback!(error, undefined as unknown as SaslAuthenticateResponse) + callback!(error, undefined as unknown as SaslAuthenticateResponse) + return } performAuthentication(connection, algorithm, definition, authenticateAPI, crypto, username, password, callback) diff --git a/test/clients/base/sasl-gssapi.test.ts b/test/clients/base/sasl-gssapi.test.ts new file mode 100644 index 0000000..4bf8e7f --- /dev/null +++ b/test/clients/base/sasl-gssapi.test.ts @@ -0,0 +1,216 @@ +import { deepStrictEqual, ok, rejects } from 'node:assert' +import { once } from 'node:events' +import { resolve } from 'node:path' +import { test } from 'node:test' +import { AuthenticationError, Base, MultipleErrors, NetworkError, parseBroker, sleep } from '../../../src/index.ts' +import { kafkaSaslKerberosBootstrapServers } from '../../helpers.ts' + +const saslBroker = parseBroker(kafkaSaslKerberosBootstrapServers[0]) + +test('should not connect to SASL protected broker by default', async t => { + const base = new Base({ + clientId: 'clientId', + bootstrapBrokers: kafkaSaslKerberosBootstrapServers, + strict: true, + retries: false + }) + t.after(() => base.close()) + + await rejects(() => base.metadata({ topics: [] })) +}) + +test('should connect to SASL protected broker using SASL/GSSAPI using username and password', async t => { + const base = new Base({ + clientId: 'clientId', + bootstrapBrokers: kafkaSaslKerberosBootstrapServers, + strict: true, + retries: 0, + sasl: { + mechanism: 'GSSAPI', + username: 'admin-password@EXAMPLE.COM', + password: 'admin' + } + }) + + t.after(() => base.close()) + + const metadata = await base.metadata({ topics: [] }) + + deepStrictEqual(metadata.brokers.get(1), { host: 'localhost', port: 9003 }) +}) + +test('should connect to SASL protected broker using SASL/GSSAPI using keytab', async t => { + const base = new Base({ + clientId: 'clientId', + bootstrapBrokers: kafkaSaslKerberosBootstrapServers, + strict: true, + retries: 0, + sasl: { + mechanism: 'GSSAPI', + username: 'admin-keytab@EXAMPLE.COM', + keytab: resolve(import.meta.dirname, '../../../tmp/kerberos/admin.keytab') + } + }) + + t.after(() => base.close()) + + const metadata = await base.metadata({ topics: [] }) + + deepStrictEqual(metadata.brokers.get(1), { host: 'localhost', port: 9003 }) +}) + +test('should handle authentication errors', async t => { + const base = new Base({ + clientId: 'clientId', + bootstrapBrokers: kafkaSaslKerberosBootstrapServers, + retries: 0, + sasl: { + mechanism: 'GSSAPI', + username: 'admin-password@EXAMPLE.COM', + password: 'admin123' + } + }) + + t.after(() => base.close()) + + try { + await base.metadata({ topics: [] }) + throw new Error('Expected error not thrown') + } catch (error) { + ok(error instanceof MultipleErrors) + deepStrictEqual(error.errors[0].cause.message, 'SASL authentication failed.') + } +}) + +test('should accept a function as credential provider', async t => { + const base = new Base({ + clientId: 'clientId', + bootstrapBrokers: kafkaSaslKerberosBootstrapServers, + retries: 0, + sasl: { + mechanism: 'GSSAPI', + username () { + return 'admin-password@EXAMPLE.COM' + }, + password: 'admin' + } + }) + + t.after(() => base.close()) + + const metadata = await base.metadata({ topics: [] }) + + deepStrictEqual(metadata.brokers.get(1), saslBroker) +}) + +test('should accept an async function as credential provider', async t => { + const base = new Base({ + clientId: 'clientId', + bootstrapBrokers: kafkaSaslKerberosBootstrapServers, + retries: 0, + sasl: { + mechanism: 'GSSAPI', + username: 'admin-password@EXAMPLE.COM', + async password () { + await sleep(1000) + return 'admin' + } + } + }) + + t.after(() => base.close()) + + const metadata = await base.metadata({ topics: [] }) + + deepStrictEqual(metadata.brokers.get(1), saslBroker) +}) + +test('should handle sync credential provider errors', async t => { + const base = new Base({ + clientId: 'clientId', + bootstrapBrokers: kafkaSaslKerberosBootstrapServers, + retries: 0, + sasl: { + mechanism: 'GSSAPI', + username () { + throw new Error('Kaboom!') + }, + password: 'admin' + } + }) + + t.after(() => base.close()) + + try { + await base.metadata({ topics: [] }) + throw new Error('Expected error not thrown') + } catch (error) { + deepStrictEqual(error.message, 'Cannot connect to any broker.') + + const networkError = error.errors[0] + deepStrictEqual(networkError instanceof NetworkError, true) + deepStrictEqual(networkError.message, `Connection to ${kafkaSaslKerberosBootstrapServers[0]} failed.`) + + const authenticationError = networkError.cause + deepStrictEqual(authenticationError instanceof AuthenticationError, true) + deepStrictEqual(authenticationError.message, 'The SASL/GSSAPI username provider threw an error.') + deepStrictEqual(authenticationError.cause.message, 'Kaboom!') + } +}) + +test('should handle async credential provider errors', async t => { + const base = new Base({ + clientId: 'clientId', + bootstrapBrokers: kafkaSaslKerberosBootstrapServers, + strict: true, + retries: 0, + sasl: { + mechanism: 'GSSAPI', + username: 'admin-password@EXAMPLE.COM', + async password () { + throw new Error('Kaboom!') + } + } + }) + + t.after(() => base.close()) + + try { + await base.metadata({ topics: [] }) + throw new Error('Expected error not thrown') + } catch (error) { + deepStrictEqual(error.message, 'Cannot connect to any broker.') + + const networkError = error.errors[0] + deepStrictEqual(networkError instanceof NetworkError, true) + deepStrictEqual(networkError.message, `Connection to ${kafkaSaslKerberosBootstrapServers[0]} failed.`) + + const authenticationError = networkError.cause + deepStrictEqual(authenticationError instanceof AuthenticationError, true) + deepStrictEqual(authenticationError.message, 'The SASL/GSSAPI password provider threw an error.') + deepStrictEqual(authenticationError.cause.message, 'Kaboom!') + } +}) + +test('should automatically refresh expired tokens when the server provides a session_lifetime', async t => { + const base = new Base({ + clientId: 'clientId', + bootstrapBrokers: kafkaSaslKerberosBootstrapServers, + strict: true, + retries: 0, + sasl: { + mechanism: 'GSSAPI', + username: 'admin-password@EXAMPLE.COM', + password: 'admin' + } + }) + + t.after(() => base.close()) + + await base.metadata({ topics: [] }) + + // Wait for the token to expire, and for the re-authentication to happen + await Promise.all([sleep(6000), once(base, 'client:broker:sasl:authentication:extended')]) + + await base.metadata({ topics: [], forceUpdate: true }) +}) diff --git a/test/clients/base/sasl.test.ts b/test/clients/base/sasl.test.ts index 266b384..0d68f29 100644 --- a/test/clients/base/sasl.test.ts +++ b/test/clients/base/sasl.test.ts @@ -30,9 +30,7 @@ test('UNAUTHENTICATED - should not connect to SASL protected broker by default', }) for (const mechanism of allowedSASLMechanisms) { - if (mechanism === 'OAUTHBEARER') { - // GSSAPI requires a properly configured Kerberos environment - // which is out of scope for these tests + if (mechanism === 'OAUTHBEARER' || mechanism === 'GSSAPI') { continue } diff --git a/test/helpers.ts b/test/helpers.ts index 2556ce8..a0dcf96 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -36,6 +36,7 @@ import { export const kafkaBootstrapServers = ['localhost:9011'] export const kafkaSaslBootstrapServers = ['localhost:9002'] +export const kafkaSaslKerberosBootstrapServers = ['localhost:9003'] export const mockedErrorMessage = 'Cannot connect to any broker.' export const mockedOperationId = -1n let kafkaVersion = process.env.KAFKA_VERSION diff --git a/test/network/connection.test.ts b/test/network/connection.test.ts index a3c2427..d89c35f 100644 --- a/test/network/connection.test.ts +++ b/test/network/connection.test.ts @@ -28,6 +28,7 @@ import { createCreationChannelVerifier, createTracingChannelVerifier, kafkaSaslBootstrapServers, + kafkaSaslKerberosBootstrapServers, mockConnectionAPI, mockedErrorMessage, mockedOperationId @@ -943,8 +944,20 @@ test('Connection.connect should not connect to SASL protected broker by default' }) for (const mechanism of allowedSASLMechanisms) { - const sasl: SASLOptions = - mechanism === 'OAUTHBEARER' ? { mechanism, token: 'token' } : { mechanism, username: 'admin', password: 'admin' } + let sasl: SASLOptions + let saslBroker = parseBroker(kafkaSaslBootstrapServers[0]) + + switch (mechanism) { + case SASLMechanisms.OAUTHBEARER: + sasl = { mechanism, token: 'token' } + break + case SASLMechanisms.GSSAPI: + saslBroker = parseBroker(kafkaSaslKerberosBootstrapServers[0]) + sasl = { mechanism, username: 'admin-password@EXAMPLE.COM', password: 'admin' } + break + default: + sasl = { mechanism, username: 'admin', password: 'admin' } + } test(`Connection.connect should connect to SASL protected broker using SASL/${mechanism}`, async t => { const connection = new Connection('clientId', { sasl })