From mboxrd@z Thu Jan 1 00:00:00 1970 Path: news.gmane.io!.POSTED.blaine.gmane.org!not-for-mail From: "J.P." Newsgroups: gmane.emacs.bugs,gmane.emacs.erc.general Subject: bug#29108: 25.3; ERC SASL support Date: Sun, 13 Nov 2022 22:45:03 -0800 Message-ID: <87a64unifk.fsf@neverwas.me> References: <87h8ud92zl.fsf@gmail.com> <874jx4h6sk.fsf@neverwas.me> <875yhifujk.fsf_-_@neverwas.me> <87edw4swdk.fsf@neverwas.me> <878rljxfxs.fsf@neverwas.me> <87k04m4th8.fsf@neverwas.me> <87o7thlepf.fsf@neverwas.me> <87o7taoohd.fsf@neverwas.me> Mime-Version: 1.0 Content-Type: multipart/mixed; boundary="=-=-=" Injection-Info: ciao.gmane.io; posting-host="blaine.gmane.org:116.202.254.214"; logging-data="3687"; mail-complaints-to="usenet@ciao.gmane.io" User-Agent: Gnus/5.13 (Gnus v5.13) Cc: emacs-erc@gnu.org, bandali@gnu.org To: 29108@debbugs.gnu.org Original-X-From: bug-gnu-emacs-bounces+geb-bug-gnu-emacs=m.gmane-mx.org@gnu.org Tue Nov 15 00:32:32 2022 Return-path: Envelope-to: geb-bug-gnu-emacs@m.gmane-mx.org Original-Received: from lists.gnu.org ([209.51.188.17]) by ciao.gmane.io with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.92) (envelope-from ) id 1ouiwJ-0000gF-H8 for geb-bug-gnu-emacs@m.gmane-mx.org; Tue, 15 Nov 2022 00:32:32 +0100 Original-Received: from localhost ([::1] helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1ouiq0-0002tM-Jz; Mon, 14 Nov 2022 18:26:00 -0500 Original-Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1ouigx-0001m0-W8 for bug-gnu-emacs@gnu.org; Mon, 14 Nov 2022 18:16:40 -0500 Original-Received: from debbugs.gnu.org ([209.51.188.43]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_128_GCM_SHA256:128) (Exim 4.90_1) (envelope-from ) id 1ouTEI-0000nE-Sh for bug-gnu-emacs@gnu.org; Mon, 14 Nov 2022 01:46:03 -0500 Original-Received: from Debian-debbugs by debbugs.gnu.org with local (Exim 4.84_2) (envelope-from ) id 1ouTEI-0008L7-GO for bug-gnu-emacs@gnu.org; Mon, 14 Nov 2022 01:46:02 -0500 X-Loop: help-debbugs@gnu.org Resent-From: "J.P." Original-Sender: "Debbugs-submit" Resent-CC: bug-gnu-emacs@gnu.org Resent-Date: Mon, 14 Nov 2022 06:46:02 +0000 Resent-Message-ID: Resent-Sender: help-debbugs@gnu.org X-GNU-PR-Message: followup 29108 X-GNU-PR-Package: emacs X-GNU-PR-Keywords: patch Original-Received: via spool by 29108-submit@debbugs.gnu.org id=B29108.166840831831991 (code B ref 29108); Mon, 14 Nov 2022 06:46:02 +0000 Original-Received: (at 29108) by debbugs.gnu.org; 14 Nov 2022 06:45:18 +0000 Original-Received: from localhost ([127.0.0.1]:48822 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1ouTDa-0008Jv-2H for submit@debbugs.gnu.org; Mon, 14 Nov 2022 01:45:18 -0500 Original-Received: from mail-108-mta69.mxroute.com ([136.175.108.69]:32771) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1ouTDY-0008Jf-Ix for 29108@debbugs.gnu.org; Mon, 14 Nov 2022 01:45:16 -0500 Original-Received: from mail-111-mta2.mxroute.com ([136.175.111.2] filter006.mxroute.com) (Authenticated sender: mN4UYu2MZsgR) by mail-108-mta69.mxroute.com (ZoneMTA) with ESMTPSA id 18474e26d230006e99.001 for <29108@debbugs.gnu.org> (version=TLSv1/SSLv3 cipher=ECDHE-RSA-AES128-GCM-SHA256); Mon, 14 Nov 2022 06:45:06 +0000 X-Zone-Loop: 3558e46c7260884dcef61f1060a0e597c9ba6858026f X-Originating-IP: [136.175.111.2] DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=neverwas.me ; s=x; h=Content-Type:MIME-Version:Message-ID:Date:References:In-Reply-To: Subject:Cc:To:From:Sender:Reply-To:Content-Transfer-Encoding:Content-ID: Content-Description:Resent-Date:Resent-From:Resent-Sender:Resent-To:Resent-Cc :Resent-Message-ID:List-Id:List-Help:List-Unsubscribe:List-Subscribe: List-Post:List-Owner:List-Archive; bh=VIedutqHq7SwOwKVzas9mW930qMGXkZ1BEDF6XHvyNI=; b=Tr+k74y4CyXnqCSicDXrB8GSHZ k7Glrn3MRsiUGDjVr7QN+sdqi9X+N/ZgLiUuQoMOLWikr69HPoOVnLUyXn+qWYTRXV7usZJf/iR0a V8j4E2iRBLa3xpFssbay0jhbFgU3ibc0UnxS3N3KMTgiTqWhmcpV/rrdxf/YrulxYvyGR9dKU09M5 4n3EmA9xlXf0AsLzg3oajoZ9U067rgOZEOJzEymPeWZHb27CPy8+gAkAUggWQiYKiEKzazd8JBP95 57jgLqQf004H0UjC1GgaZuH5hZM7ZiG7YxYiUzmxLH8OWZDR+ZWQCRjmCvjfZHBPlo7TKOXtiEPJm KzXGfMuw==; In-Reply-To: <87o7taoohd.fsf@neverwas.me> (J. P.'s message of "Sun, 13 Nov 2022 07:36:46 -0800") X-Authenticated-Id: masked@neverwas.me X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list X-BeenThere: bug-gnu-emacs@gnu.org List-Id: "Bug reports for GNU Emacs, the Swiss army knife of text editors" List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: bug-gnu-emacs-bounces+geb-bug-gnu-emacs=m.gmane-mx.org@gnu.org Original-Sender: bug-gnu-emacs-bounces+geb-bug-gnu-emacs=m.gmane-mx.org@gnu.org Xref: news.gmane.io gmane.emacs.bugs:247831 gmane.emacs.erc.general:1986 Archived-At: --=-=-= Content-Type: text/plain "J.P." writes: > v6. Added some sweeping changes that are still pretty raw, which > probably means a delay of a couple days, at least. Apologies for the > hold up. v7. Fixed some sloppiness involving mode activation. Restored misplaced compat hunk to rightful patch. --=-=-= Content-Type: text/x-patch Content-Disposition: attachment; filename=0000-v6-v7.diff >From ba6fae5c2851e2926e20e21c8dc962977c94987a Mon Sep 17 00:00:00 2001 From: "F. Jason Park" Date: Sun, 13 Nov 2022 22:38:13 -0800 Subject: [PATCH 0/5] *** NOT A PATCH *** *** BLURB HERE *** F. Jason Park (5): Add GS2 authorization to sasl-scram-rfc Don't set erc-networks--id until network is known Support local ERC modules in erc-mode buffers Call erc-login indirectly via new generic wrapper Add non-IRCv3 SASL module to ERC doc/misc/erc.texi | 148 +++++- etc/ERC-NEWS | 21 +- lisp/erc/erc-backend.el | 15 +- lisp/erc/erc-common.el | 56 ++- lisp/erc/erc-compat.el | 116 +++++ lisp/erc/erc-goodies.el | 1 + lisp/erc/erc-networks.el | 39 +- lisp/erc/erc-sasl.el | 433 ++++++++++++++++++ lisp/erc/erc.el | 85 ++-- lisp/net/sasl-scram-rfc.el | 21 +- test/lisp/erc/erc-sasl-tests.el | 319 +++++++++++++ test/lisp/erc/erc-scenarios-sasl.el | 208 +++++++++ test/lisp/erc/erc-tests.el | 63 +++ test/lisp/erc/resources/sasl/external.eld | 33 ++ test/lisp/erc/resources/sasl/plain-failed.eld | 16 + test/lisp/erc/resources/sasl/plain.eld | 39 ++ test/lisp/erc/resources/sasl/scram-sha-1.eld | 47 ++ .../lisp/erc/resources/sasl/scram-sha-256.eld | 47 ++ 18 files changed, 1621 insertions(+), 86 deletions(-) create mode 100644 lisp/erc/erc-sasl.el create mode 100644 test/lisp/erc/erc-sasl-tests.el create mode 100644 test/lisp/erc/erc-scenarios-sasl.el create mode 100644 test/lisp/erc/resources/sasl/external.eld create mode 100644 test/lisp/erc/resources/sasl/plain-failed.eld create mode 100644 test/lisp/erc/resources/sasl/plain.eld create mode 100644 test/lisp/erc/resources/sasl/scram-sha-1.eld create mode 100644 test/lisp/erc/resources/sasl/scram-sha-256.eld Interdiff: diff --git a/doc/misc/erc.texi b/doc/misc/erc.texi index 79f8c92719..8eb33c8e80 100644 --- a/doc/misc/erc.texi +++ b/doc/misc/erc.texi @@ -390,8 +390,15 @@ Modules There is a spiffy customize interface, which may be reached by typing @kbd{M-x customize-option @key{RET} erc-modules @key{RET}}. -Alternatively, set @code{erc-modules} manually and then call -@code{erc-update-modules}. +Alternatively, set @code{erc-modules} manually, and ERC will load them +and run their setup code during buffer initialization. Third-party +code may need to call the function @code{erc-update-modules} +explicitly, although this is typically unnecessary. + +All modules operate as minor modes under the hood, and some newer ones +are defined as buffer-local. For everyday use, the only practical +difference is that local modules can only be enabled in ERC buffers, +and their toggle commands never mutate @code{erc-modules}. The following is a list of available modules. @@ -1026,7 +1033,7 @@ SASL Otherwise, if you set this option to @code{nil} (or the empty string) or if an auth-source lookup has failed, ERC will try a non-@code{nil} -``server password'', likely whatever you gave as the @var{password} +``server password,'' likely whatever you gave as the @var{password} argument to @code{erc-tls}. This fallback behavior may change, however, so please don't rely on it. As a last resort, ERC will prompt you for input. diff --git a/etc/ERC-NEWS b/etc/ERC-NEWS index 5cabb9b015..f5b14376ad 100644 --- a/etc/ERC-NEWS +++ b/etc/ERC-NEWS @@ -48,10 +48,9 @@ hell. For some, auth-source may provide a workaround in the form of nonstandard server passwords. See the "Connection" node in the manual under the subheading "Password". -If you require SASL immediately, please participate in ERC development -by volunteering to try (and give feedback on) edge features, one of -which is SASL. All known external offerings, past and present, are -valiant efforts whose use is nevertheless discouraged. +** Rudimentary SASL support has arrived. +A new module, 'erc-sasl', now ships with ERC 5.5. See the SASL +section in the manual for details. ** Username argument for entry-point commands. Commands 'erc' and 'erc-tls' now accept a ':user' keyword argument, @@ -97,6 +96,20 @@ messages during periods of heavy traffic no longer disappear. Although rare, server passwords containing white space are now handled correctly. +** Local modules and ERC-mode hooks are more useful. +The 'local-p' parameter of 'define-erc-module' now affects more than +the scope of a module's minor-mode. This currently has little direct +impact on the user experience, but third-party packages may wish to +take note. + +More importantly, the function 'erc-update-modules' now supports an +optional argument to defer the enabling of local modules and instead +return their mode-activation commands. 'erc-open' leverages this new +functionality to delay their activation, as well as that of all +'erc-mode-hook' members, until most of ERC's mode-related variables +have been initialized. This does not include connection-specific +variables defined in erc-backend, however. + ** Miscellaneous behavioral changes in the library API. A number of core macros and other definitions have been moved to a new file called erc-common.el. This was done to further lessen the diff --git a/lisp/erc/erc-common.el b/lisp/erc/erc-common.el index a300cfc4fa..e5fabdc67f 100644 --- a/lisp/erc/erc-common.el +++ b/lisp/erc/erc-common.el @@ -173,17 +173,17 @@ define-erc-module name) (interactive) ,@(unless local-p `((cl-pushnew ',mod erc-modules))) - ,@(macroexp-unprogn - `(,@(if local-p '(when (eq major-mode 'erc-mode)) '(progn)) - (setq ,mode t) - ,@enable-body))) + ,@(if local-p + `((when (setq ,mode (and (derived-mode-p 'erc-mode) t)) + ,@enable-body)) + `((setq ,mode t) ,@enable-body))) (defun ,disable () ,(format "Disable ERC %S mode." name) (interactive) ,@(unless local-p `((setq erc-modules (delq ',mod erc-modules)))) ,@(macroexp-unprogn - `(,@(if local-p `(when ,mode) '(progn)) + `(,@(if local-p '(when (derived-mode-p 'erc-mode)) '(progn)) (setq ,mode nil) ,@disable-body))) ,(when (and alias (not (eq name alias))) diff --git a/lisp/erc/erc-compat.el b/lisp/erc/erc-compat.el index 6d4ef21383..d4a2e312be 100644 --- a/lisp/erc/erc-compat.el +++ b/lisp/erc/erc-compat.el @@ -272,7 +272,7 @@ erc-compat--with-memoization `(cl--generic-with-memoization ,table ,@forms)) (t `(progn ,@forms)))) -(defun erc-compat--local-minor-modes () +(defun erc-compat--local-module-modes () (delq nil (if (boundp 'local-minor-modes) (mapcar (lambda (m) diff --git a/lisp/erc/erc-sasl.el b/lisp/erc/erc-sasl.el index ac2646051c..a9d7ed235d 100644 --- a/lisp/erc/erc-sasl.el +++ b/lisp/erc/erc-sasl.el @@ -223,17 +223,18 @@ erc-sasl--create-client (let ((sasl-mechanism-alist (copy-sequence sasl-mechanism-alist)) (sasl-mechanisms sasl-mechanisms) (name (upcase (symbol-name mechanism))) - (feature (intern (concat "erc-sasl-" (symbol-name mechanism)))) + (feature (intern-soft (concat "erc-sasl-" (symbol-name mechanism)))) client) - (setf (alist-get name sasl-mechanism-alist nil nil #'equal) `(,feature)) - (cl-pushnew name sasl-mechanisms :test #'equal) - (setq client (sasl-make-client (sasl-find-mechanism `(,name)) - (or (alist-get 'user erc-sasl--options) - (erc-downcase (erc-current-nick))) - "N/A" "N/A")) - (sasl-client-set-property client 'authenticator-name - (alist-get 'authzid erc-sasl--options)) - client)) + (when feature + (setf (alist-get name sasl-mechanism-alist nil nil #'equal) `(,feature)) + (cl-pushnew name sasl-mechanisms :test #'equal) + (setq client (sasl-make-client (sasl-find-mechanism `(,name)) + (or (alist-get 'user erc-sasl--options) + (erc-downcase (erc-current-nick))) + "N/A" "N/A")) + (sasl-client-set-property client 'authenticator-name + (alist-get 'authzid erc-sasl--options)) + client))) (cl-defmethod erc-sasl--create-client ((_m (eql plain))) "Create and return a new PLAIN client object." @@ -296,7 +297,9 @@ erc-sasl--init (defun erc-sasl--on-connection-established (&rest _) (setf (alist-get erc-networks--id erc-sasl--session-options nil nil #'erc-networks--id-equal-p) - erc-sasl--options)) + erc-sasl--options + ;; + erc-sasl--options nil)) (defun erc-sasl--mechanism-offered-p (offered) "Return non-nil when OFFERED appears among a list of mechanisms." @@ -318,7 +321,8 @@ erc-sasl--authenticate-handler (when (string= "+" response) (setq response "")) (setf response (base64-decode-string - (concat (erc-sasl--state-pending erc-sasl--state) response)) + (concat (erc-sasl--state-pending erc-sasl--state) + response)) (erc-sasl--state-pending erc-sasl--state) nil) ;; The server is done sending, so our turn (let ((client (erc-sasl--state-client erc-sasl--state)) @@ -357,11 +361,14 @@ sasl (let* ((mech (alist-get 'mechanism erc-sasl--options)) (client (erc-sasl--create-client mech))) (unless client - (erc-display-error-notice nil (format "Unknown mechanism: %s" mech)) - (erc-error "Unknown mechanism: %s" mech)) + (erc-display-error-notice + nil (format "Unknown SASL mechanism: %s" mech)) + (erc-error "Unknown SASL mechanism: %s" mech)) (setf (erc-sasl--state-client erc-sasl--state) client)))) ((remove-hook 'erc-server-AUTHENTICATE-functions #'erc-sasl--authenticate-handler t) + (setf (alist-get erc-networks--id erc-sasl--session-options nil t) nil) + (kill-local-variable 'erc-sasl--state) (kill-local-variable 'erc-sasl--options)) 'local) @@ -410,11 +417,13 @@ erc-sasl--destroy (cl-defmethod erc--register-connection (&context (erc-sasl-mode (eql t))) "Send speculative/pipelined CAP and AUTHENTICATE and hope for the best." - (erc-server-send "CAP REQ :sasl") - (erc-login) - (let* ((c (erc-sasl--state-client erc-sasl--state)) - (m (sasl-mechanism-name (sasl-client-mechanism c)))) - (erc-server-send (format "AUTHENTICATE %s" m)))) + (if-let* ((c (erc-sasl--state-client erc-sasl--state)) + (m (sasl-mechanism-name (sasl-client-mechanism c)))) + (progn + (erc-server-send "CAP REQ :sasl") + (erc-login) + (erc-server-send (format "AUTHENTICATE %s" m))) + (erc-sasl--destroy erc-server-process))) (provide 'erc-sasl) ;;; erc-sasl.el ends here diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el index a703f903ec..c5989dbc7e 100644 --- a/lisp/erc/erc.el +++ b/lisp/erc/erc.el @@ -1871,7 +1871,7 @@ erc-update-modules introduced in ERC 5.5." (let ((local-modes (when (and defer-locals (derived-mode-p 'erc-mode)) - (erc-compat--local-minor-modes)))) + (erc-compat--local-module-modes)))) (dolist (module erc-modules (and defer-locals local-modes)) (require (or (alist-get module erc--modules-to-features) (intern (concat "erc-" (symbol-name module)))) -- 2.38.1 --=-=-= Content-Type: text/x-patch Content-Disposition: attachment; filename=0001-Add-GS2-authorization-to-sasl-scram-rfc.patch >From a7177b08ef8a0fe055d1e09045aaa95a8ba66ceb Mon Sep 17 00:00:00 2001 From: "F. Jason Park" Date: Mon, 19 Sep 2022 21:28:52 -0700 Subject: [PATCH 1/5] Add GS2 authorization to sasl-scram-rfc * lisp/net/sasl-scram-rfc.el (sasl-scram-gs2-header-function, sasl-scram-construct-gs2-header): Add new variable and default function for determining a SCRAM GSS-API message header. This is mainly intended for other libraries rather than end users. (sasl-scram-client-first-message): Use gs2-header function. (sasl-scram--client-final-message): Use dedicated gs2-header function. Also remove whitespace when base64-encoding, as per RFC 5802. (Bug#57956.) --- lisp/net/sasl-scram-rfc.el | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/lisp/net/sasl-scram-rfc.el b/lisp/net/sasl-scram-rfc.el index ee52ed6e07..f7a2e42541 100644 --- a/lisp/net/sasl-scram-rfc.el +++ b/lisp/net/sasl-scram-rfc.el @@ -45,14 +45,21 @@ ;;; Generic for SCRAM-* +(defvar sasl-scram-gs2-header-function 'sasl-scram-construct-gs2-header + "Function to create GS2 header. +See https://www.rfc-editor.org/rfc/rfc5801#section-4.") + +(defun sasl-scram-construct-gs2-header (client) + ;; The "n," means the client doesn't support channel binding, and + ;; the trailing comma is included as per RFC 5801. + (let ((authzid (sasl-client-property client 'authenticator-name))) + (concat "n," (and authzid "a=") authzid ","))) + (defun sasl-scram-client-first-message (client _step) (let ((c-nonce (sasl-unique-id))) (sasl-client-set-property client 'c-nonce c-nonce)) (concat - ;; n = client doesn't support channel binding - "n," - ;; TODO: where would we get authorization id from? - "," + (funcall sasl-scram-gs2-header-function client) (sasl-scram--client-first-message-bare client))) (defun sasl-scram--client-first-message-bare (client) @@ -77,11 +84,11 @@ sasl-scram--client-final-message (c-nonce (sasl-client-property client 'c-nonce)) ;; no channel binding, no authorization id - (cbind-input "n,,")) + (cbind-input (funcall sasl-scram-gs2-header-function client))) (unless (string-prefix-p c-nonce nonce) (sasl-error "Invalid nonce from server")) (let* ((client-final-message-without-proof - (concat "c=" (base64-encode-string cbind-input) "," + (concat "c=" (base64-encode-string cbind-input t) "," "r=" nonce)) (password ;; TODO: either apply saslprep or disallow non-ASCII characters @@ -113,7 +120,7 @@ sasl-scram--client-final-message (client-proof (funcall string-xor client-key client-signature)) (client-final-message (concat client-final-message-without-proof "," - "p=" (base64-encode-string client-proof)))) + "p=" (base64-encode-string client-proof t)))) (sasl-client-set-property client 'auth-message auth-message) (sasl-client-set-property client 'salted-password salted-password) client-final-message))) -- 2.38.1 --=-=-= Content-Type: text/x-patch Content-Disposition: attachment; filename=0002-Don-t-set-erc-networks-id-until-network-is-known.patch >From 665eb8627e3b2ba1befeb64cbff0caf217a28089 Mon Sep 17 00:00:00 2001 From: "F. Jason Park" Date: Sun, 13 Nov 2022 01:52:48 -0800 Subject: [PATCH 2/5] Don't set erc-networks--id until network is known * lisp/erc/erc-networks.el (erc-networks--id-given): Accept a null argument. (erc-networks--id-on-connect): Remove unused function. (erc-networks--id-equal-p): Add method for comparing initialized and unset IDs. (erc-networks--update-server-identity): Ensure `erc-networks--id' is set before continuing search. (erc-networks--init-identity): Don't assume `erc-networks--id' is non-nil. * lisp/erc/erc.el (erc-open): For continued sessions, try copying over the last network ID. (erc--auth-source-determine-params-default): Don't expect a network ID to have been initialized. * lisp/erc/erc-backend.el (erc-server-NICK, erc-server-433): Unless already connected, clear network ID when server rejects or mandates a nick change. --- lisp/erc/erc-backend.el | 7 ++++++- lisp/erc/erc-networks.el | 39 ++++++++++++++++----------------------- lisp/erc/erc.el | 13 ++++++++----- 3 files changed, 30 insertions(+), 29 deletions(-) diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el index 026b34849a..2c8c4dcb28 100644 --- a/lisp/erc/erc-backend.el +++ b/lisp/erc/erc-backend.el @@ -1525,7 +1525,7 @@ define-erc-response-handler (cl-pushnew (erc-server-buffer) bufs) (erc-set-current-nick nn) ;; Rename session, possibly rename server buf and all targets - (when (erc-network) + (when erc-server-connected (erc-networks--id-reload erc-networks--id proc parsed)) (erc-update-mode-line) (setq erc-nick-change-attempt-count 0) @@ -1535,6 +1535,9 @@ define-erc-response-handler 'NICK-you ?n nick ?N nn) (run-hook-with-args 'erc-nick-changed-functions nn nick)) (t + (unless (or erc-server-connected + (erc-networks--id-given erc-networks--id)) + (setq erc-networks--id nil)) (erc-handle-user-status-change 'nick (list nick login host) (list nn)) (erc-display-message parsed 'notice bufs 'NICK ?n nick ?u login ?h host ?N nn)))))) @@ -2161,6 +2164,8 @@ erc-server-322-message (define-erc-response-handler (433) "Login-time \"nick in use\"." nil + (unless (or erc-server-connected (erc-networks--id-given erc-networks--id)) + (setq erc-networks--id nil)) (erc-nickname-in-use (cadr (erc-response.command-args parsed)) "already in use")) diff --git a/lisp/erc/erc-networks.el b/lisp/erc/erc-networks.el index dba6ead073..aa90bb8479 100644 --- a/lisp/erc/erc-networks.el +++ b/lisp/erc/erc-networks.el @@ -826,12 +826,11 @@ erc-networks--id ;; For now, please use this instead of `erc-networks--id-fixed-p'. (cl-defgeneric erc-networks--id-given (net-id) - "Return the preassigned identifier for a network presence, if any. -This may have originated from an `:id' arg to entry-point commands -`erc-tls' or `erc'.") + "Return the preassigned identifier for a network context, if any. +When non-nil, assume NET-ID originated from an `:id' argument to +entry-point commands `erc-tls' or `erc'.") -(cl-defmethod erc-networks--id-given ((_ erc-networks--id)) - nil) +(cl-defmethod erc-networks--id-given (_) nil) ; _ may be nil (cl-defmethod erc-networks--id-given ((nid erc-networks--id-fixed)) (erc-networks--id-symbol nid)) @@ -866,22 +865,15 @@ erc-networks--id-create ((_ symbol) &context (erc-obsolete-var erc-reuse-buffers null)) (erc-networks--id-fixed-create (intern (buffer-name)))) -(cl-defgeneric erc-networks--id-on-connect (net-id) - "Update NET-ID `erc-networks--id' after connection params known. -This is typically during or just after MOTD.") - -(cl-defmethod erc-networks--id-on-connect ((_ erc-networks--id)) - nil) - -(cl-defmethod erc-networks--id-on-connect ((id erc-networks--id-qualifying)) - (erc-networks--id-qualifying-update id (erc-networks--id-qualifying-create))) - (cl-defgeneric erc-networks--id-equal-p (self other) - "Return non-nil when two network identities exhibit underlying equality. -SELF and OTHER are `erc-networks--id' struct instances. This -should normally be used only for ID recovery or merging, after -which no two identities should be `equal' (timestamps aside) that -aren't also `eq'.") + "Return non-nil when two network IDs exhibit underlying equality. +Expect SELF and OTHER to be `erc-networks--id' struct instances +and that this will only be called for ID recovery or merging, +after which no two identities should be `equal' (timestamps +aside) that aren't also `eq'.") + +(cl-defmethod erc-networks--id-equal-p ((_ null) (_ erc-networks--id)) nil) +(cl-defmethod erc-networks--id-equal-p ((_ erc-networks--id) (_ null)) nil) (cl-defmethod erc-networks--id-equal-p ((self erc-networks--id) (other erc-networks--id)) @@ -1381,7 +1373,8 @@ erc-networks--update-server-identity (let* ((identity erc-networks--id) (buffer (current-buffer)) (f (lambda () - (unless (or (eq (current-buffer) buffer) + (unless (or (not erc-networks--id) + (eq (current-buffer) buffer) (eq erc-networks--id identity)) (if (erc-networks--id-equal-p identity erc-networks--id) (throw 'buffer erc-networks--id) @@ -1400,8 +1393,8 @@ erc-networks--init-identity "Update identity with real network name." ;; Initialize identity for real now that we know the network (cl-assert erc-network) - (unless (erc-networks--id-symbol erc-networks--id) ; unless just reconnected - (erc-networks--id-on-connect erc-networks--id)) + (unless erc-networks--id + (setq erc-networks--id (erc-networks--id-create nil))) ;; Find duplicate identities or other conflicting ones and act ;; accordingly. (erc-networks--update-server-identity) diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el index 6b14cf87e2..63379af141 100644 --- a/lisp/erc/erc.el +++ b/lisp/erc/erc.el @@ -2008,10 +2008,12 @@ erc-open (setq erc-default-nicks (if (consp erc-nick) erc-nick (list erc-nick))) ;; client certificate (only useful if connecting over TLS) (setq erc-session-client-certificate client-certificate) - (setq erc-networks--id (if connect - (erc-networks--id-create id) - (buffer-local-value 'erc-networks--id - old-buffer))) + (setq erc-networks--id + (if connect + (or (and continued-session + (buffer-local-value 'erc-networks--id old-buffer)) + (and id (erc-networks--id-create id))) + (buffer-local-value 'erc-networks--id old-buffer))) ;; debug output buffer (setq erc-dbuf (when erc-log-p @@ -3171,7 +3173,8 @@ erc-auth-source-join-function function)) (defun erc--auth-source-determine-params-defaults () - (let* ((net (and-let* ((esid (erc-networks--id-symbol erc-networks--id)) + (let* ((net (and-let* ((erc-networks--id) + (esid (erc-networks--id-symbol erc-networks--id)) ((symbol-name esid))))) (localp (and erc--target (erc--target-channel-local-p erc--target))) (hosts (if localp -- 2.38.1 --=-=-= Content-Type: text/x-patch Content-Disposition: attachment; filename=0003-Support-local-ERC-modules-in-erc-mode-buffers.patch >From 21145f307c90c0231b8564e7f6517d2782a8cf17 Mon Sep 17 00:00:00 2001 From: "F. Jason Park" Date: Mon, 12 Jul 2021 03:44:28 -0700 Subject: [PATCH 3/5] Support local ERC modules in erc-mode buffers * doc/misc/erc.texi: Mention local modules in Modules Chapter. * lisp/erc/erc-compat.el (erc-compat--local-module-modes): Add helper for finding local modules active in an ERC buffer. * lisp/erc/erc.el (erc-migrate-modules): Add some missing mappings. (erc-update-modules): Change return value from nil to a list of minor-mode commands for local modules. Use `custom-variable-p' to detect flavor. Currently, all modules are global and so are their accompanying minor modes. (erc-open): Defer enabling of local modules via `erc-update-modules' until after buffer is initialized with other local vars. Also defer major mode hooks so they can detect things like whether the buffer is a server or target buffer. Also ensure local module setup code can detect when `erc-open' was called with a non-nil `erc--server-reconnecting'. It's reset to nil by `erc-server-connect'. * lisp/erc/erc-common.el (erc--module-name-migrations, erc--features-to-modules, erc--modules-to-features): Add alists of old-to-new module names to support module-name migrations. (define-erc-modules): Don't enable local modules (minor modes) unless `erc-mode' is the major mode. And don't disable them unless the minor mode is actually active. Also, don't mutate `erc-modules' when dealing with a local module. (erc--normalize-module-symbol): Add helper for `erc-migrate-modules'. * lisp/erc/erc-goodies.el: Require cl-lib. * test/lisp/erc/erc-tests.el (erc-migrate-modules, erc-update-modules): Add rudimentary unit tests. (Bug#57955.) --- doc/misc/erc.texi | 11 +++++- etc/ERC-NEWS | 14 +++++++ lisp/erc/erc-common.el | 56 ++++++++++++++++++++++++---- lisp/erc/erc-compat.el | 12 ++++++ lisp/erc/erc-goodies.el | 1 + lisp/erc/erc.el | 75 ++++++++++++++++++++------------------ test/lisp/erc/erc-tests.el | 58 +++++++++++++++++++++++++++++ 7 files changed, 182 insertions(+), 45 deletions(-) diff --git a/doc/misc/erc.texi b/doc/misc/erc.texi index 3db83197f9..5049710b32 100644 --- a/doc/misc/erc.texi +++ b/doc/misc/erc.texi @@ -389,8 +389,15 @@ Modules There is a spiffy customize interface, which may be reached by typing @kbd{M-x customize-option @key{RET} erc-modules @key{RET}}. -Alternatively, set @code{erc-modules} manually and then call -@code{erc-update-modules}. +Alternatively, set @code{erc-modules} manually, and ERC will load them +and run their setup code during buffer initialization. Third-party +code may need to call the function @code{erc-update-modules} +explicitly, although this is typically unnecessary. + +All modules operate as minor modes under the hood, and some newer ones +are defined as buffer-local. For everyday use, the only practical +difference is that local modules can only be enabled in ERC buffers, +and their toggle commands never mutate @code{erc-modules}. The following is a list of available modules. diff --git a/etc/ERC-NEWS b/etc/ERC-NEWS index 5cabb9b015..e14cd3492a 100644 --- a/etc/ERC-NEWS +++ b/etc/ERC-NEWS @@ -97,6 +97,20 @@ messages during periods of heavy traffic no longer disappear. Although rare, server passwords containing white space are now handled correctly. +** Local modules and ERC-mode hooks are more useful. +The 'local-p' parameter of 'define-erc-module' now affects more than +the scope of a module's minor-mode. This currently has little direct +impact on the user experience, but third-party packages may wish to +take note. + +More importantly, the function 'erc-update-modules' now supports an +optional argument to defer the enabling of local modules and instead +return their mode-activation commands. 'erc-open' leverages this new +functionality to delay their activation, as well as that of all +'erc-mode-hook' members, until most of ERC's mode-related variables +have been initialized. This does not include connection-specific +variables defined in erc-backend, however. + ** Miscellaneous behavioral changes in the library API. A number of core macros and other definitions have been moved to a new file called erc-common.el. This was done to further lessen the diff --git a/lisp/erc/erc-common.el b/lisp/erc/erc-common.el index d8aac36eab..e5fabdc67f 100644 --- a/lisp/erc/erc-common.el +++ b/lisp/erc/erc-common.el @@ -85,6 +85,41 @@ erc--target (contents "" :type string) (tags '() :type list)) +;; TODO move goodies modules here after 29 is released. +(defconst erc--features-to-modules + '((erc-pcomplete completion pcomplete) + (erc-capab capab-identify) + (erc-join autojoin) + (erc-page page ctcp-page) + (erc-sound sound ctcp-sound) + (erc-stamp stamp timestamp) + (erc-services services nickserv)) + "Migration alist mapping a library feature to module names. +Keys need not be unique: a library may define more than one +module. Sometimes a module's downcased alias will be its +canonical name.") + +(defconst erc--modules-to-features + (let (pairs) + (pcase-dolist (`(,feature . ,names) erc--features-to-modules) + (dolist (name names) + (push (cons name feature) pairs))) + (nreverse pairs)) + "Migration alist mapping a module's name to its home library feature.") + +(defconst erc--module-name-migrations + (let (pairs) + (pcase-dolist (`(,_ ,canonical . ,rest) erc--features-to-modules) + (dolist (obsolete rest) + (push (cons obsolete canonical) pairs))) + pairs) + "Association list of obsolete module names to canonical names.") + +(defun erc--normalize-module-symbol (symbol) + "Return preferred SYMBOL for `erc-modules'." + (setq symbol (intern (downcase (symbol-name symbol)))) + (or (cdr (assq symbol erc--module-name-migrations)) symbol)) + (defmacro define-erc-module (name alias doc enable-body disable-body &optional local-p) "Define a new minor mode using ERC conventions. @@ -98,7 +133,9 @@ define-erc-module This will define a minor mode called erc-NAME-mode, possibly an alias erc-ALIAS-mode, as well as the helper functions -erc-NAME-enable, and erc-NAME-disable. +erc-NAME-enable, and erc-NAME-disable. Beware that for global +modules, these helpers, as well as the minor-mode toggle, all mutate +the user option `erc-modules'. Example: @@ -111,6 +148,7 @@ define-erc-module #\\='erc-replace-insert)))" (declare (doc-string 3) (indent defun)) (let* ((sn (symbol-name name)) + (mod (erc--normalize-module-symbol name)) (mode (intern (format "erc-%s-mode" (downcase sn)))) (group (intern (format "erc-%s" (downcase sn)))) (enable (intern (format "erc-%s-enable" (downcase sn)))) @@ -134,16 +172,20 @@ define-erc-module ,(format "Enable ERC %S mode." name) (interactive) - (add-to-list 'erc-modules (quote ,name)) - (setq ,mode t) - ,@enable-body) + ,@(unless local-p `((cl-pushnew ',mod erc-modules))) + ,@(if local-p + `((when (setq ,mode (and (derived-mode-p 'erc-mode) t)) + ,@enable-body)) + `((setq ,mode t) ,@enable-body))) (defun ,disable () ,(format "Disable ERC %S mode." name) (interactive) - (setq erc-modules (delq (quote ,name) erc-modules)) - (setq ,mode nil) - ,@disable-body) + ,@(unless local-p `((setq erc-modules (delq ',mod erc-modules)))) + ,@(macroexp-unprogn + `(,@(if local-p '(when (derived-mode-p 'erc-mode)) '(progn)) + (setq ,mode nil) + ,@disable-body))) ,(when (and alias (not (eq name alias))) `(defalias ',(intern diff --git a/lisp/erc/erc-compat.el b/lisp/erc/erc-compat.el index 03bd8f1352..96b862c8c5 100644 --- a/lisp/erc/erc-compat.el +++ b/lisp/erc/erc-compat.el @@ -168,6 +168,18 @@ erc-compat--with-memoization `(cl--generic-with-memoization ,table ,@forms)) (t `(progn ,@forms)))) +(defun erc-compat--local-module-modes () + (delq nil + (if (boundp 'local-minor-modes) + (mapcar (lambda (m) + (and (string-prefix-p "erc-" (symbol-name m)) m)) + local-minor-modes) + (mapcar (pcase-lambda (`(,k . _)) + (and (string-prefix-p "erc-" (symbol-name k)) + (string-suffix-p "-mode" (symbol-name k)) + k)) + (buffer-local-variables))))) + (provide 'erc-compat) ;;; erc-compat.el ends here diff --git a/lisp/erc/erc-goodies.el b/lisp/erc/erc-goodies.el index 59b5f01f23..1af83b58ba 100644 --- a/lisp/erc/erc-goodies.el +++ b/lisp/erc/erc-goodies.el @@ -31,6 +31,7 @@ ;;; Imenu support +(eval-when-compile (require 'cl-lib)) (require 'erc-common) (defvar erc-controls-highlight-regexp) diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el index 63379af141..3d8afe8df6 100644 --- a/lisp/erc/erc.el +++ b/lisp/erc/erc.el @@ -1784,10 +1784,7 @@ erc-migrate-modules "Migrate old names of ERC modules to new ones." ;; modify `transforms' to specify what needs to be changed ;; each item is in the format '(old . new) - (let ((transforms '((pcomplete . completion)))) - (delete-dups - (mapcar (lambda (m) (or (cdr (assoc m transforms)) m)) - mods)))) + (delete-dups (mapcar #'erc--normalize-module-symbol mods))) (defcustom erc-modules '(netsplit fill button match track completion readonly networks ring autojoin noncommands irccontrols @@ -1865,28 +1862,25 @@ erc-modules (repeat :tag "Others" :inline t symbol)) :group 'erc) -(defun erc-update-modules () - "Run this to enable erc-foo-mode for all modules in `erc-modules'." - (let (req) - (dolist (mod erc-modules) - (setq req (concat "erc-" (symbol-name mod))) - (cond - ;; yuck. perhaps we should bring the filenames into sync? - ((string= req "erc-capab-identify") - (setq req "erc-capab")) - ((string= req "erc-completion") - (setq req "erc-pcomplete")) - ((string= req "erc-pcomplete") - (setq mod 'completion)) - ((string= req "erc-autojoin") - (setq req "erc-join"))) - (condition-case nil - (require (intern req)) - (error nil)) - (let ((sym (intern-soft (concat "erc-" (symbol-name mod) "-mode")))) - (if (fboundp sym) - (funcall sym 1) - (error "`%s' is not a known ERC module" mod)))))) +(defun erc-update-modules (&optional defer-locals) + "Enable global minor mode for all global modules in `erc-modules'. +With DEFER-LOCALS, return minor-mode commands for all local +modules, possibly for deferred invocation, as done by `erc-open' +whenever a new ERC buffer is created. Local modules were +introduced in ERC 5.5." + (let ((local-modes + (when (and defer-locals (derived-mode-p 'erc-mode)) + (erc-compat--local-module-modes)))) + (dolist (module erc-modules (and defer-locals local-modes)) + (require (or (alist-get module erc--modules-to-features) + (intern (concat "erc-" (symbol-name module)))) + nil 'noerror) ; some modules don't have a corresponding feature + (let ((mode (intern-soft (concat "erc-" (symbol-name module) "-mode")))) + (unless (and mode (fboundp mode)) + (error "`%s' is not a known ERC module" module)) + (if (and defer-locals (not (custom-variable-p mode))) + (cl-pushnew mode local-modes) + (funcall mode 1)))))) (defun erc-setup-buffer (buffer) "Consults `erc-join-buffer' to find out how to display `BUFFER'." @@ -1942,18 +1936,24 @@ erc-open (let* ((target (and channel (erc--target-from-string channel))) (buffer (erc-get-buffer-create server port nil target id)) (old-buffer (current-buffer)) - old-point + (old-recon-count erc-server-reconnect-count) + (old-point nil) + (delayed-modules nil) (continued-session (and erc--server-reconnecting (with-suppressed-warnings ((obsolete erc-reuse-buffers)) - erc-reuse-buffers)))) + erc-reuse-buffers) + erc-networks--id))) (when connect (run-hook-with-args 'erc-before-connect server port nick)) - (erc-update-modules) (set-buffer buffer) (setq old-point (point)) - (let ((old-recon-count erc-server-reconnect-count)) - (erc-mode) - (setq erc-server-reconnect-count old-recon-count)) + (setq delayed-modules (erc-update-modules 'defer-locals)) + + (delay-mode-hooks (erc-mode)) + + (setq erc-server-reconnect-count old-recon-count + erc--server-reconnecting continued-session) + (when (setq erc-server-connected (not connect)) (setq erc-server-announced-name (buffer-local-value 'erc-server-announced-name old-buffer))) @@ -2010,14 +2010,19 @@ erc-open (setq erc-session-client-certificate client-certificate) (setq erc-networks--id (if connect - (or (and continued-session - (buffer-local-value 'erc-networks--id old-buffer)) + (or erc--server-reconnecting (and id (erc-networks--id-create id))) (buffer-local-value 'erc-networks--id old-buffer))) ;; debug output buffer (setq erc-dbuf (when erc-log-p (get-buffer-create (concat "*ERC-DEBUG: " server "*")))) + + (erc-determine-parameters server port nick full-name user passwd) + + (save-excursion (run-mode-hooks)) + (dolist (mod delayed-modules) (funcall mod +1)) + ;; set up prompt (unless continued-session (goto-char (point-max)) @@ -2029,8 +2034,6 @@ erc-open (erc-display-prompt) (goto-char (point-max))) - (erc-determine-parameters server port nick full-name user passwd) - ;; Saving log file on exit (run-hook-with-args 'erc-connect-pre-hook buffer) diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el index c88dd9888d..d074b36c8b 100644 --- a/test/lisp/erc/erc-tests.el +++ b/test/lisp/erc/erc-tests.el @@ -953,4 +953,62 @@ erc-message (kill-buffer "ExampleNet") (kill-buffer "#chan"))) +(ert-deftest erc-migrate-modules () + (should (equal (erc-migrate-modules '(autojoin timestamp button)) + '(autojoin stamp button))) + ;; Default unchanged + (should (equal (erc-migrate-modules erc-modules) erc-modules))) + +(ert-deftest erc-update-modules () + (let (calls + erc-modules + erc-kill-channel-hook erc-kill-server-hook erc-kill-buffer-hook) + (cl-letf (((symbol-function 'require) + (lambda (s &rest _) (push s calls))) + + ;; Local modules + ((symbol-function 'erc-fake-bar-mode) + (lambda (n) (push (cons 'fake-bar n) calls))) + + ;; Global modules + ((symbol-function 'erc-fake-foo-mode) + (lambda (n) (push (cons 'fake-foo n) calls))) + ((get 'erc-fake-foo-mode 'standard-value) 'ignore) + ((symbol-function 'erc-autojoin-mode) + (lambda (n) (push (cons 'autojoin n) calls))) + ((get 'erc-autojoin-mode 'standard-value) 'ignore) + ((symbol-function 'erc-networks-mode) + (lambda (n) (push (cons 'networks n) calls))) + ((get 'erc-networks-mode 'standard-value) 'ignore) + ((symbol-function 'erc-completion-mode) + (lambda (n) (push (cons 'completion n) calls))) + ((get 'erc-completion-mode 'standard-value) 'ignore)) + + (ert-info ("Local modules") + (setq erc-modules '(fake-foo fake-bar)) + (should (equal (erc-update-modules t) '(erc-fake-bar-mode))) + ;; Bar the feature is still required but the mode is not activated + (should (equal (nreverse calls) + '(erc-fake-foo (fake-foo . 1) erc-fake-bar))) + (setq calls nil)) + + (ert-info ("Module name overrides") + (setq erc-modules '(completion autojoin networks)) + (should-not (erc-update-modules t)) ; no locals + (should (equal (nreverse calls) '( erc-pcomplete (completion . 1) + erc-join (autojoin . 1) + erc-networks (networks . 1)))) + (setq calls nil)) + + (ert-info ("Reenabling of local minor modes by `erc-open'") + (with-temp-buffer + (erc-mode) + (setq erc-modules '(completion autojoin networks)) + (let ((local-minor-modes '(font-lock-mode erc-fake-bar-mode))) + (should (equal (erc-update-modules t) '(erc-fake-bar-mode)))) + (should (equal (nreverse calls) + '( erc-pcomplete (completion . 1) + erc-join (autojoin . 1) + erc-networks (networks . 1))))))))) + ;;; erc-tests.el ends here -- 2.38.1 --=-=-= Content-Type: text/x-patch Content-Disposition: attachment; filename=0004-Call-erc-login-indirectly-via-new-generic-wrapper.patch >From d7a7309214089aee49ce547816ba39c1ae0672ce Mon Sep 17 00:00:00 2001 From: "F. Jason Park" Date: Sun, 18 Sep 2022 01:49:23 -0700 Subject: [PATCH 4/5] Call erc-login indirectly via new generic wrapper * lisp/erc/erc-backend (erc--register-connection): Add new generic function that defers to `erc-login' by default. (erc-process-sentinel, erc-server-connect): Call `erc--register-connection' instead of `erc-login'. --- lisp/erc/erc-backend.el | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el index 2c8c4dcb28..37a3da8b66 100644 --- a/lisp/erc/erc-backend.el +++ b/lisp/erc/erc-backend.el @@ -625,6 +625,10 @@ erc-open-network-stream (let ((p (plist-put parameters :nowait t))) (apply #'open-network-stream name buffer host service p))) +(cl-defmethod erc--register-connection () + "Perform opening IRC protocol exchange with server." + (erc-login)) + (defun erc-server-connect (server port buffer &optional client-certificate) "Perform the connection and login using the specified SERVER and PORT. We will store server variables in the buffer given by BUFFER. @@ -673,7 +677,7 @@ erc-server-connect ;; waiting for a non-blocking connect - keep the user informed (erc-display-message nil nil buffer "Opening connection..\n") (message "%s...done" msg) - (erc-login)) )) + (erc--register-connection)))) (defun erc-server-reconnect () "Reestablish the current IRC connection. @@ -851,7 +855,7 @@ erc-process-sentinel cproc (process-status cproc) event erc-server-quitting)) (if (string-match "^open" event) ;; newly opened connection (no wait) - (erc-login) + (erc--register-connection) ;; assume event is 'failed (erc-with-all-buffers-of-server cproc nil (setq erc-server-connected nil)) -- 2.38.1 --=-=-= Content-Type: text/x-patch Content-Disposition: attachment; filename=0005-Add-non-IRCv3-SASL-module-to-ERC.patch >From ba6fae5c2851e2926e20e21c8dc962977c94987a Mon Sep 17 00:00:00 2001 From: "F. Jason Park" Date: Mon, 12 Jul 2021 03:44:28 -0700 Subject: [PATCH 5/5] Add non-IRCv3 SASL module to ERC * lisp/erc/erc-compat.el (erc-compat--sasl-scram-construct-gs2-header, erc-compat--sasl-scram-client-first-message, erc-compat--sasl-scram--client-final-message): Add minimal authorization support via own variant of `sasl-scram--client-final-message' and supporting sasl-scram-rfc functions introduced in Emacs 29. * lisp/erc/erc.el (erc-modules): Add `sasl'. * lisp/erc/erc-sasl.el: New file (bug#29108). * test/lisp/erc/erc-sasl-tests.el: New file. * test/lisp/erc/erc-scenarios-sasl.el: New file. * test/lisp/erc/resources/sasl/plain-failed.eld: New file. * test/lisp/erc/resources/sasl/plain.eld: New file. * test/lisp/erc/resources/sasl/scram-sha-1.eld: New file. * test/lisp/erc/resources/sasl/scram-sha-256.eld: New file. * test/lisp/erc/resources/sasl/external.eld: New file. --- doc/misc/erc.texi | 137 +++++- etc/ERC-NEWS | 7 +- lisp/erc/erc-compat.el | 104 +++++ lisp/erc/erc-sasl.el | 433 ++++++++++++++++++ lisp/erc/erc.el | 1 + test/lisp/erc/erc-sasl-tests.el | 319 +++++++++++++ test/lisp/erc/erc-scenarios-sasl.el | 208 +++++++++ test/lisp/erc/erc-tests.el | 9 +- test/lisp/erc/resources/sasl/external.eld | 33 ++ test/lisp/erc/resources/sasl/plain-failed.eld | 16 + test/lisp/erc/resources/sasl/plain.eld | 39 ++ test/lisp/erc/resources/sasl/scram-sha-1.eld | 47 ++ .../lisp/erc/resources/sasl/scram-sha-256.eld | 47 ++ 13 files changed, 1393 insertions(+), 7 deletions(-) create mode 100644 lisp/erc/erc-sasl.el create mode 100644 test/lisp/erc/erc-sasl-tests.el create mode 100644 test/lisp/erc/erc-scenarios-sasl.el create mode 100644 test/lisp/erc/resources/sasl/external.eld create mode 100644 test/lisp/erc/resources/sasl/plain-failed.eld create mode 100644 test/lisp/erc/resources/sasl/plain.eld create mode 100644 test/lisp/erc/resources/sasl/scram-sha-1.eld create mode 100644 test/lisp/erc/resources/sasl/scram-sha-256.eld diff --git a/doc/misc/erc.texi b/doc/misc/erc.texi index 5049710b32..8eb33c8e80 100644 --- a/doc/misc/erc.texi +++ b/doc/misc/erc.texi @@ -78,6 +78,7 @@ Top Advanced Usage * Connecting:: Ways of connecting to an IRC server. +* SASL:: Authenticating via SASL. * Sample Configuration:: An example configuration file. * Options:: Options that are available for ERC. @@ -485,6 +486,10 @@ Modules @item ring Enable an input history +@cindex modules, sasl +@item sasl +Enable SASL authentication + @cindex modules, scrolltobottom @item scrolltobottom Scroll to the bottom of the buffer @@ -532,6 +537,7 @@ Advanced Usage @menu * Connecting:: Ways of connecting to an IRC server. +* SASL:: Authenticating via SASL * Sample Configuration:: An example configuration file. * Options:: Options that are available for ERC. @end menu @@ -849,6 +855,7 @@ Connecting @noindent For details, @pxref{Top,,auth-source, auth, Emacs auth-source Library}. +@anchor{ERC auth-source functions} @defopt erc-auth-source-server-function @end defopt @defopt erc-auth-source-services-function @@ -861,7 +868,8 @@ Connecting @code{:user} is the ``desired'' nickname rather than the current one. Generalized names, like @code{:user} and @code{:host}, are always used over back-end specific ones, like @code{:login} or @code{:machine}. -ERC expects a string to use as the secret or nil, if the search fails. +ERC expects a string to use as the secret or @code{nil}, if the search +fails. @findex erc-auth-source-search The default value for all three options is the function @@ -922,6 +930,133 @@ Connecting make the most sense, but any reasonably printable object is acceptable. +@node SASL +@section Authenticating via SASL +@cindex SASL + +@strong{Warning:} ERC's SASL offering is currently limited by a lack +of support for proper IRCv3 capability negotiation. In most cases, +this shouldn't affect your ability to authenticate. If you run into +trouble, please contact us (@pxref{Getting Help and Reporting Bugs}). + +Regardless of the mechanism or the network, you'll likely have to be +registered before first use. Please refer to the network's own +instructions for details. If you're new to IRC and using a bouncer, +know that you almost certainly won't be needing SASL for the +@samp{client -> bouncer} connection. + +Note that @code{sasl} is a ``local'' ERC module, which various library +functions, like @code{erc-update-modules}, may treat differently than +global modules in user code. However, this should not affect everyday +client use. To get started, just add @code{sasl} to +@code{erc-modules} like any other module. But before that, please +explore all custom options pertaining to your chosen mechanism. + +@defopt erc-sasl-mechanism +The name of an SASL subprotocol type as a @emph{lowercase} symbol. + +@var{plain} and @var{scram} (``password-based''): + +@indentedblock +Here, ``password'' refers to your account password, which is usually +your @samp{NickServ} password. This often differs from any connection +(server) password given to @code{erc-tls} via its @code{:password} +parameter. To make this work, customize both @code{erc-sasl-user} and +@code{erc-sasl-password} or bind them when invoking @code{erc-tls}. +@end indentedblock + +@var{external} (via Client TLS Certificate): + +@indentedblock +You'll want to specify the @code{:client-certificate} param when +opening a new connection, which is typically done by calling +@code{emacs-tls}. But before that, ensure you've registered your +fingerprint with the network. The fingerprint is usually a SHA1 or +SHA256 digest in either "normalized" or "openssl" forms. The first is +lowercase without delims (@samp{deadbeef}) and the second uppercase +with colon seps (@samp{DE:AD:BE:EF}). + +Additional considerations: +@enumerate +@item +There's no reason to send your password after registering. +@item +Most IRCds will allow you to authenticate with a client cert but +without the hassle of SASL (meaning you may not need this module). +@item +Technically, @var{EXTERNAL} merely indicates that an out-of-band mode +of authentication is in effect (being deferred to), so depending on +the specific application or service, there's an off chance client +certs aren't involved. +@end enumerate +@end indentedblock + +@var{ecdsa-nist256p-challenge}: + +@indentedblock +This mechanism is quite complicated and currently requires the +external @samp{openssl} executable, so please use something else if at +all possible. Ignoring that, specify your key file (e.g., +@samp{~/pki/mykey.pem}) as the value of @code{erc-sasl-password}, and +then configure your network settings. On servers running Atheme +services, you can add your public key with @samp{NickServ} like so: + +@example +ERC> /msg NickServ set property \ + pubkey AgGZmlYTUjJlea/BVz7yrjJ6gysiAPaQxzeUzTH4hd5j + +@end example +(You may be able to omit the @samp{property} subcommand.) +@end indentedblock + +@end defopt + +@defopt erc-sasl-user +This should be your network account name, typically the same one +registered with nickname services. Specify this when your +@samp{NickServ} account name differs from the nick you're connecting +with. +@end defopt + +@defopt erc-sasl-password +For ``password-based'' mechanisms, ERC sends any nonempty string as +the authentication password. + +If you instead give a non-@code{nil} symbol, like @samp{Libera.Chat}, +ERC will use it for the @code{:host} field in an auth-source query. +Actually, the same goes for when this option is @code{nil} but an +explicit session ID is already on file (@pxref{Network Identifier}). +For all such queries, ERC specifies the value of @code{erc-sasl-user} +for the @code{:user} (@code{:login}) param. Keep in mind that none of +this matters unless @code{erc-sasl-auth-source-function} holds a +function (it's @code{nil} by default). + +Otherwise, if you set this option to @code{nil} (or the empty string) +or if an auth-source lookup has failed, ERC will try a non-@code{nil} +``server password,'' likely whatever you gave as the @var{password} +argument to @code{erc-tls}. This fallback behavior may change, +however, so please don't rely on it. As a last resort, ERC will +prompt you for input. + +Also, if your mechanism is @code{ecdsa-nist256p-challenge}, this +option should instead hold the file name of your key. +@end defopt + +@defopt erc-sasl-auth-source-function +This is nearly identical to the other ERC @samp{auth-source} function +options (@pxref{ERC auth-source functions}) except that the default +value here is @code{nil}, meaning you have to set it to something like +@code{erc-auth-source-search} for queries to be performed. +@end defopt + +@defopt erc-sasl-authzid +In the rarest of circumstances, a network may want you to specify a +specific role or assume an alternate identity. In most cases, this +happens because the server is buggy or misconfigured. If you suspect +such a thing, please contact your network operator. Otherwise, just +leave this set to @code{nil}. +@end defopt + @node Sample Configuration @section Sample Configuration diff --git a/etc/ERC-NEWS b/etc/ERC-NEWS index e14cd3492a..f5b14376ad 100644 --- a/etc/ERC-NEWS +++ b/etc/ERC-NEWS @@ -48,10 +48,9 @@ hell. For some, auth-source may provide a workaround in the form of nonstandard server passwords. See the "Connection" node in the manual under the subheading "Password". -If you require SASL immediately, please participate in ERC development -by volunteering to try (and give feedback on) edge features, one of -which is SASL. All known external offerings, past and present, are -valiant efforts whose use is nevertheless discouraged. +** Rudimentary SASL support has arrived. +A new module, 'erc-sasl', now ships with ERC 5.5. See the SASL +section in the manual for details. ** Username argument for entry-point commands. Commands 'erc' and 'erc-tls' now accept a ':user' keyword argument, diff --git a/lisp/erc/erc-compat.el b/lisp/erc/erc-compat.el index 96b862c8c5..d4a2e312be 100644 --- a/lisp/erc/erc-compat.el +++ b/lisp/erc/erc-compat.el @@ -157,6 +157,110 @@ erc-subseq res)))))) +;;;; SASL + +(declare-function sasl-step-data "sasl" (step)) +(declare-function sasl-error "sasl" (datum)) +(declare-function sasl-client-property "sasl" (client property)) +(declare-function sasl-client-set-property "sasl" (client property value)) +(declare-function sasl-mechanism-name "sasl" (mechanism)) +(declare-function sasl-client-name "sasl" (client)) +(declare-function sasl-client-mechanism "sasl" (client)) +(declare-function sasl-read-passphrase "sasl" (prompt)) +(declare-function sasl-unique-id "sasl" nil) +(declare-function decode-hex-string "hex-util" (string)) +(declare-function rfc2104-hash "rfc2104" (hash block-length hash-length + key text)) +(declare-function sasl-scram--client-first-message-bare "sasl-scram-rfc" + (client)) +(declare-function cl-mapcar "cl-lib" (cl-func cl-x &rest cl-rest)) + +(defun erc-compat--sasl-scram-construct-gs2-header (client) + ;; The "n," means the client doesn't support channel binding, and + ;; the trailing comma is included as per RFC 5801. + (let ((authzid (sasl-client-property client 'authenticator-name))) + (concat "n," (and authzid "a=") authzid ","))) + +(defun erc-compat--sasl-scram-client-first-message (client _step) + (let ((c-nonce (sasl-unique-id))) + (sasl-client-set-property client 'c-nonce c-nonce)) + (concat (erc-compat--sasl-scram-construct-gs2-header client) + (sasl-scram--client-first-message-bare client))) + +;; This is `sasl-scram--client-final-message' from sasl-scram-rfc, +;; with the NO-LINE-BREAK argument of `base64-encode-string' set to t +;; because https://www.rfc-editor.org/rfc/rfc5802#section-2.1 says: +;; +;; > The use of base64 in SCRAM is restricted to the canonical form +;; > with no whitespace. +;; +;; Unfortunately, simply advising `base64-encode-string' won't work +;; since the byte compiler precomputes the result when all inputs are +;; constants, as they are in the original version. +;; +;; The only other substantial change is the addition of authz support. +;; This can be dropped if adopted by Emacs 29 and `compat'. Changes +;; proposed for 29 are marked with a "; *n", comment below. See older +;; versions of lisp/erc/erc-v3-sasl.el (bug#49860) if needing a true +;; side-by-side diff. This also inlines the internal function +;; `sasl-scram--client-first-message-bare' and takes various liberties +;; with formatting. + +(defun erc-compat--sasl-scram--client-final-message + (hash-fun block-length hash-length client step) + (unless (string-match + "^r=\\([^,]+\\),s=\\([^,]+\\),i=\\([0-9]+\\)\\(?:$\\|,\\)" + (sasl-step-data step)) + (sasl-error "Unexpected server response")) + (let* ((hmac-fun + (lambda (text key) + (decode-hex-string + (rfc2104-hash hash-fun block-length hash-length key text)))) + (step-data (sasl-step-data step)) + (nonce (match-string 1 step-data)) + (salt-base64 (match-string 2 step-data)) + (iteration-count (string-to-number (match-string 3 step-data))) + (c-nonce (sasl-client-property client 'c-nonce)) + (cbind-input + (if (string-prefix-p c-nonce nonce) + (erc-compat--sasl-scram-construct-gs2-header client) ; *1 + (sasl-error "Invalid nonce from server"))) + (client-final-message-without-proof + (concat "c=" (base64-encode-string cbind-input t) "," ; *2 + "r=" nonce)) + (password + (sasl-read-passphrase + (format "%s passphrase for %s: " + (sasl-mechanism-name (sasl-client-mechanism client)) + (sasl-client-name client)))) + (salt (base64-decode-string salt-base64)) + (string-xor (lambda (a b) + (apply #'unibyte-string (cl-mapcar #'logxor a b)))) + (salted-password (let ((digest (concat salt (string 0 0 0 1))) + (xored nil)) + (dotimes (_i iteration-count xored) + (setq digest (funcall hmac-fun digest password)) + (setq xored (if (null xored) + digest + (funcall string-xor xored + digest)))))) + (client-key (funcall hmac-fun "Client Key" salted-password)) + (stored-key (decode-hex-string (funcall hash-fun client-key))) + (auth-message (concat "n=" (sasl-client-name client) + ",r=" c-nonce "," step-data + "," client-final-message-without-proof)) + (client-signature (funcall hmac-fun + (encode-coding-string auth-message 'utf-8) + stored-key)) + (client-proof (funcall string-xor client-key client-signature)) + (client-final-message + (concat client-final-message-without-proof "," + "p=" (base64-encode-string client-proof t)))) ; *3 + (sasl-client-set-property client 'auth-message auth-message) + (sasl-client-set-property client 'salted-password salted-password) + client-final-message)) + + ;;;; Misc 29.1 (defmacro erc-compat--with-memoization (table &rest forms) diff --git a/lisp/erc/erc-sasl.el b/lisp/erc/erc-sasl.el new file mode 100644 index 0000000000..a9d7ed235d --- /dev/null +++ b/lisp/erc/erc-sasl.el @@ -0,0 +1,433 @@ +;;; erc-sasl.el --- SASL for ERC -*- lexical-binding: t -*- + +;; Copyright (C) 2022 Free Software Foundation, Inc. +;; +;; This file is part of GNU Emacs. +;; +;; GNU Emacs is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published +;; by the Free Software Foundation, either version 3 of the License, +;; or (at your option) any later version. +;; +;; GNU Emacs is distributed in the hope that it will be useful, but +;; WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +;; General Public License for more details. +;; +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see . + +;;; Commentary: + +;; This "non-IRCv3" implementation resembles others that have surfaced +;; over the years, the first possibly being from Joseph Gay: +;; +;; https://lists.gnu.org/archive/html/erc-discuss/2012-02/msg00001.html +;; +;; See options and Info manual for usage. +;; +;; TODO: +;; +;; - Find a way to obfuscate the password in memory (via something +;; like `auth-source--obfuscate'); it's currently visible in +;; backtraces and bug reports. +;; +;; - Implement a proxy mechanism that chooses the strongest available +;; mechanism for you. Requires CAP 3.2 (see bug#49860). +;; +;; - Integrate with whatever solution ERC eventually settles on to +;; handle user options for different network contexts. At the +;; moment, this does its own thing for stashing and restoring +;; session options, but ERC should make abstractions available for +;; all local modules to use, possibly based on connection-local +;; variables. + +;;; Code: +(require 'erc) +(require 'rx) +(require 'sasl) +(require 'sasl-scram-rfc) +(require 'sasl-scram-sha256 nil t) ; not present in Emacs 27 + +(defgroup erc-sasl nil + "SASL for ERC." + :group 'erc + :package-version '(ERC . "5.4.1")) ; FIXME increment on next release + +(defcustom erc-sasl-mechanism 'plain + "SASL mechanism to connect with. +Note that any value other than nil or `external' likely requires +`erc-sasl-user' and `erc-sasl-password'." + :type '(choice (const plain) + (const external) + (const scram-sha-1) + (const scram-sha-256) + (const scram-sha-512) + (const ecdsa-nist256p-challenge))) + +(defcustom erc-sasl-user nil + "Optional account username to send when authenticating. +This is also referred to as the authentication identity, or +\"authcid\". When nil, applicable mechanisms will use the +session's current nick." + :type '(choice string (const nil))) + +(defcustom erc-sasl-password nil + "Optional account password to send when authenticating. +When the value is a string, ERC will use it unconditionally for +most mechanisms. Otherwise, when `erc-sasl-auth-source-function' +is a function, ERC will attempt an auth-source query, possibly +using a non-nil symbol for the suggested `:host' parameter if set +as this option's value or passed as an `:id' to `erc-tls'. +Failing that, ERC will try a non-nil \"session password\" if one +is on file, typically from a `:password' argument supplied to +`erc-tls'. As a last resort, ERC will prompt for input. + +Note that when `erc-sasl-mechanism' is set to +`ecdsa-nist256p-challenge', this option should hold the file name +of the key." + :type '(choice (const nil) string symbol)) + +(defcustom erc-sasl-auth-source-function nil + "Function to query auth-source for an SASL password. +Called with keyword params known to `auth-source-search', which +may include a non-nil `erc-sasl-user' for the `:user' field +and a non-nil `erc-sasl-password' for the `:host' field, when +the latter option is a symbol instead of a string. In return, +ERC expects a string to send as the SASL password, or nil, to +move on to the next approach, as described in the doc string for +the option `erc-sasl-password'. See info node `(erc) +Connecting' for details on ERC's auth-source integration." + :type '(choice (function-item erc-auth-source-search) + (const nil) + function)) + +(defcustom erc-sasl-authzid nil + "SASL authorization identity, likely unneeded for everyday use." + :type '(choice (const nil) string)) + + +;; Analogous to what erc-backend does to persist opening params. +(defvar-local erc-sasl--options nil) + +;; In the future, ERC will hopefully use connection-local variables to +;; handle such bookkeeping transparently. +(defvar erc-sasl--session-options nil + "An alist associating network-IDs to `erc-sasl--options'. +This is for persisting user options captured at entry-point +invocation throughout an Emacs session.") + +;; Session-local (server buffer) SASL subproto state +(defvar-local erc-sasl--state nil) + +(cl-defstruct erc-sasl--state + "Holder for client object and subproto state." + (client nil :type vector) + (step nil :type vector) + (pending nil :type string)) + +(defun erc-sasl--read-password (prompt) + "Return configured option or server password. +PROMPT is passed to `read-passwd' if necessary." + (let* ((pass (alist-get 'password erc-sasl--options)) + (found + (or (and (stringp pass) (not (string-empty-p pass)) pass) + (and erc-sasl-auth-source-function + (let ((user (alist-get 'user erc-sasl--options)) + (host (or pass + (erc-networks--id-given erc-networks--id)))) + (apply erc-sasl-auth-source-function + `(,@(and user (list :user user)) + ,@(and host (list :host (symbol-name host))))))) + erc-session-password))) + (if found + (copy-sequence found) + (read-passwd prompt)))) + +(defun erc-sasl--plain-response (client steps) + (let ((sasl-read-passphrase #'erc-sasl--read-password)) + (sasl-plain-response client steps))) + +(declare-function erc-compat--sasl-scram--client-final-message "erc-compat" + (hash-fun block-length hash-length client step)) + +(defun erc-sasl--scram-sha-hack-client-final-message (&rest args) + ;; In the future (29+), we'll hopefully be able to call + ;; `sasl-scram--client-final-message' directly + (require 'erc-compat) + (let ((sasl-read-passphrase #'erc-sasl--read-password)) + (apply #'erc-compat--sasl-scram--client-final-message args))) + +(defun erc-sasl--scram-sha-1-client-final-message (client step) + (erc-sasl--scram-sha-hack-client-final-message 'sha1 64 20 client step)) + +(defun erc-sasl--scram-sha-256-client-final-message (client step) + (erc-sasl--scram-sha-hack-client-final-message 'sasl-scram-sha256 64 32 + client step)) + +(defun erc-sasl--scram-sha512 (object &optional start end binary) + (secure-hash 'sha512 object start end binary)) + +(defun erc-sasl--scram-sha-512-client-final-message (client step) + (erc-sasl--scram-sha-hack-client-final-message #'erc-sasl--scram-sha512 + 128 64 client step)) + +(defun erc-sasl--scram-sha-512-authenticate-server (client step) + (sasl-scram--authenticate-server #'erc-sasl--scram-sha512 + 128 64 client step)) + +(defun erc-sasl--ecdsa-first (client _step) + "Return CLIENT name." + (sasl-client-name client)) + +;; FIXME do this with gnutls somehow +(defun erc-sasl--ecdsa-sign (client step) + "Return signed challenge for CLIENT and current STEP." + (let ((challenge (sasl-step-data step))) + (with-temp-buffer + (set-buffer-multibyte nil) + (insert challenge) + (call-process-region (point-min) (point-max) + "openssl" 'delete t nil "pkeyutl" "-inkey" + (sasl-client-property client 'ecdsa-keyfile) + "-sign") + (buffer-string)))) + +(pcase-dolist + (`(,name . ,steps) + '(("PLAIN" + erc-sasl--plain-response) + ("EXTERNAL" + ignore) + ("SCRAM-SHA-1" + erc-compat--sasl-scram-client-first-message + erc-sasl--scram-sha-1-client-final-message + sasl-scram-sha-1-authenticate-server) + ("SCRAM-SHA-256" + erc-compat--sasl-scram-client-first-message + erc-sasl--scram-sha-256-client-final-message + sasl-scram-sha-256-authenticate-server) + ("SCRAM-SHA-512" + erc-compat--sasl-scram-client-first-message + erc-sasl--scram-sha-512-client-final-message + erc-sasl--scram-sha-512-authenticate-server) + ("ECDSA-NIST256P-CHALLENGE" + erc-sasl--ecdsa-first + erc-sasl--ecdsa-sign))) + (let ((feature (intern (concat "erc-sasl-" (downcase name))))) + (put feature 'sasl-mechanism (sasl-make-mechanism name steps)) + (provide feature))) + +(cl-defgeneric erc-sasl--create-client (mechanism) + "Create and return a new SASL client object for MECHANISM." + (let ((sasl-mechanism-alist (copy-sequence sasl-mechanism-alist)) + (sasl-mechanisms sasl-mechanisms) + (name (upcase (symbol-name mechanism))) + (feature (intern-soft (concat "erc-sasl-" (symbol-name mechanism)))) + client) + (when feature + (setf (alist-get name sasl-mechanism-alist nil nil #'equal) `(,feature)) + (cl-pushnew name sasl-mechanisms :test #'equal) + (setq client (sasl-make-client (sasl-find-mechanism `(,name)) + (or (alist-get 'user erc-sasl--options) + (erc-downcase (erc-current-nick))) + "N/A" "N/A")) + (sasl-client-set-property client 'authenticator-name + (alist-get 'authzid erc-sasl--options)) + client))) + +(cl-defmethod erc-sasl--create-client ((_m (eql plain))) + "Create and return a new PLAIN client object." + ;; https://tools.ietf.org/html/rfc4616#section-2. + (let* ((sans (remq (assoc "PLAIN" sasl-mechanism-alist) + sasl-mechanism-alist)) + (sasl-mechanism-alist (cons '("PLAIN" erc-sasl-plain) sans)) + (authc (or (alist-get 'user erc-sasl--options) + (erc-downcase (erc-current-nick)))) + (port (if (numberp erc-session-port) + (number-to-string erc-session-port) + "0")) + ;; In most cases, `erc-server-announced-name' won't be known. + (host (or erc-server-announced-name erc-session-server)) + (mech (sasl-find-mechanism '("PLAIN"))) + (client (sasl-make-client mech authc port host))) + (sasl-client-set-property client 'authenticator-name + (alist-get 'authzid erc-sasl--options)) + client)) + +(cl-defmethod erc-sasl--create-client ((m (eql scram-sha-256))) + "Create and return a new SCRAM-SHA-256 client." + (unless (featurep 'sasl-scram-sha256) + (user-error "SASL mechanism %s unsupported" m)) + (cl-call-next-method)) + +(cl-defmethod erc-sasl--create-client ((m (eql scram-sha-512))) + "Create and return a new SCRAM-SHA-512 client." + (unless (featurep 'sasl-scram-sha256) + (user-error "SASL mechanism %s unsupported" m)) + (cl-call-next-method)) + +(cl-defmethod erc-sasl--create-client ((_ (eql ecdsa-nist256p-challenge))) + "Create and return a new ECDSA-NIST256P-CHALLENGE client." + (unless (executable-find "openssl") + (user-error "Could not find openssl command-line utility")) + (let ((keyfile (cdr (assq 'password erc-sasl--options)))) + (unless (and keyfile (file-exists-p keyfile)) + (user-error "`erc-sasl-password' does not point to ECDSA keyfile")) + (let ((client (cl-call-next-method))) + (sasl-client-set-property client 'ecdsa-keyfile keyfile) + client))) + +;; This stands alone because it's also used by bug#49860. +(defun erc-sasl--init () + ;; When reconnecting, try to recover stashed parameters. + (let ((existing (assoc erc-networks--id erc-sasl--session-options + #'erc-networks--id-equal-p))) + ;; This likely only runs when `erc' was called with an :id keyword. + (when (and existing (not erc--server-reconnecting)) + (setq erc-sasl--session-options (delq existing erc-sasl--session-options) + existing nil)) + (setq erc-sasl--state (make-erc-sasl--state) + erc-sasl--options (or (cdr existing) + `((user . ,erc-sasl-user) + (password . ,erc-sasl-password) + (mechanism . ,erc-sasl-mechanism) + (authzid . ,erc-sasl-authzid)))))) + +(defun erc-sasl--on-connection-established (&rest _) + (setf (alist-get erc-networks--id erc-sasl--session-options nil nil + #'erc-networks--id-equal-p) + erc-sasl--options + ;; + erc-sasl--options nil)) + +(defun erc-sasl--mechanism-offered-p (offered) + "Return non-nil when OFFERED appears among a list of mechanisms." + (string-match-p (rx-to-string + `(: (| bot ",") + ,(symbol-name + (alist-get 'mechanism erc-sasl--options)) + (| eot ","))) + (downcase offered))) + +(defun erc-sasl--authenticate-handler (_proc parsed) + "Handle PARSED `erc-response' from server. +Maybe transition to next state." + (if-let* ((response (car (erc-response.command-args parsed))) + ((= 400 (length response)))) + (cl-callf (lambda (s) (concat s response)) + (erc-sasl--state-pending erc-sasl--state)) + (cl-assert response t) + (when (string= "+" response) + (setq response "")) + (setf response (base64-decode-string + (concat (erc-sasl--state-pending erc-sasl--state) + response)) + (erc-sasl--state-pending erc-sasl--state) nil) + ;; The server is done sending, so our turn + (let ((client (erc-sasl--state-client erc-sasl--state)) + (step (erc-sasl--state-step erc-sasl--state)) + data) + (when step + (sasl-step-set-data step response)) + (setq step (setf (erc-sasl--state-step erc-sasl--state) + (sasl-next-step client step)) + data (sasl-step-data step)) + (when (string= data "") + (setq data nil)) + (when data + (setq data (base64-encode-string data t))) + ;; No need for : because no spaces (right?) + (erc-server-send (concat "AUTHENTICATE " (or data "+")))))) + +(erc-define-catalog + 'english + '((s902 . "ERR_NICKLOCKED nick %n unavailable: %s") + (s904 . "ERR_SASLFAIL (authentication failed) %s") + (s905 . "ERR SASLTOOLONG (credentials too long) %s") + (s906 . "ERR_SASLABORTED (authentication aborted) %s") + (s907 . "ERR_SASLALREADY (already authenticated) %s") + (s908 . "RPL_SASLMECHS (unsupported mechanism %m) %s"))) + +(define-erc-module sasl nil + "Non-IRCv3 SASL support for ERC. +This doesn't solicit or validate a suite of supported mechanisms." + ;; See bug#49860 for a full, CAP 3.2-aware implementation, currently + ;; a WIP as of ERC 5.5. + ((unless erc--target + (add-hook 'erc-server-AUTHENTICATE-functions + #'erc-sasl--authenticate-handler 0 t) + (erc-sasl--init) + (let* ((mech (alist-get 'mechanism erc-sasl--options)) + (client (erc-sasl--create-client mech))) + (unless client + (erc-display-error-notice + nil (format "Unknown SASL mechanism: %s" mech)) + (erc-error "Unknown SASL mechanism: %s" mech)) + (setf (erc-sasl--state-client erc-sasl--state) client)))) + ((remove-hook 'erc-server-AUTHENTICATE-functions + #'erc-sasl--authenticate-handler t) + (setf (alist-get erc-networks--id erc-sasl--session-options nil t) nil) + (kill-local-variable 'erc-sasl--state) + (kill-local-variable 'erc-sasl--options)) + 'local) + +;; FIXME use generic mechanism instead of hooks after bug#49860. +(define-erc-response-handler (AUTHENTICATE) + "Maybe authenticate to server." nil) + +(defun erc-sasl--destroy (proc) + (run-hook-with-args 'erc-quit-hook proc) + (delete-process proc) + (erc-error "Disconnected from %s; please review SASL settings" proc)) + +(define-erc-response-handler (902) + "Handle a ERR_NICKLOCKED response." nil + (erc-display-message parsed '(notice error) 'active 's902 + ?n (car (erc-response.command-args parsed)) + ?s (erc-response.contents parsed)) + (erc-sasl--destroy proc)) + +(define-erc-response-handler (903) + "Handle a RPL_SASLSUCCESS response." nil + (when erc-sasl-mode + (unless erc-server-connected + (erc-server-send "CAP END"))) + (add-hook 'erc-after-connect #'erc-sasl--on-connection-established 0 t) + (erc-handle-unknown-server-response proc parsed)) + +(define-erc-response-handler (907) + "Handle a RPL_SASLALREADY response." nil + (erc-display-message parsed '(notice error) 'active 's907 + ?s (erc-response.contents parsed))) + +(define-erc-response-handler (904 905 906) + "Handle various SASL-related error responses." nil + (erc-display-message parsed '(notice error) 'active + (intern (format "s%s" (erc-response.command parsed))) + ?s (erc-response.contents parsed)) + (erc-sasl--destroy proc)) + +(define-erc-response-handler (908) + "Handle a RPL_SASLALREADY response." nil + (erc-display-message parsed '(notice error) 'active 's908 + '?m (alist-get 'mechanism erc-sasl--options) + '?s (erc-response.contents parsed)) + (erc-sasl--destroy proc)) + +(cl-defmethod erc--register-connection (&context (erc-sasl-mode (eql t))) + "Send speculative/pipelined CAP and AUTHENTICATE and hope for the best." + (if-let* ((c (erc-sasl--state-client erc-sasl--state)) + (m (sasl-mechanism-name (sasl-client-mechanism c)))) + (progn + (erc-server-send "CAP REQ :sasl") + (erc-login) + (erc-server-send (format "AUTHENTICATE %s" m))) + (erc-sasl--destroy erc-server-process))) + +(provide 'erc-sasl) +;;; erc-sasl.el ends here +;; +;; Local Variables: +;; generated-autoload-file: "erc-loaddefs.el" +;; End: diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el index 3d8afe8df6..c5989dbc7e 100644 --- a/lisp/erc/erc.el +++ b/lisp/erc/erc.el @@ -1846,6 +1846,7 @@ erc-modules (const :tag "readonly: Make displayed lines read-only" readonly) (const :tag "replace: Replace text in messages" replace) (const :tag "ring: Enable an input history" ring) + (const :tag "sasl: Enable SASL authentication" sasl) (const :tag "scrolltobottom: Scroll to the bottom of the buffer" scrolltobottom) (const :tag "services: Identify to Nickserv (IRC Services) automatically" diff --git a/test/lisp/erc/erc-sasl-tests.el b/test/lisp/erc/erc-sasl-tests.el new file mode 100644 index 0000000000..81db9ad948 --- /dev/null +++ b/test/lisp/erc/erc-sasl-tests.el @@ -0,0 +1,319 @@ +;;; erc-sasl-tests.el --- Tests for erc-sasl. -*- lexical-binding:t -*- + +;; Copyright (C) 2022 Free Software Foundation, Inc. +;; +;; This file is part of GNU Emacs. +;; +;; GNU Emacs is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. +;; +;; GNU Emacs is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. +;; +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see . + +;;; Commentary: + +;;; Code: + +(require 'ert-x) +(require 'erc-sasl) + +(ert-deftest erc-sasl--mechanism-offered-p () + (let ((erc-sasl--options '((mechanism . external)))) + (should (erc-sasl--mechanism-offered-p "foo,external")) + (should (erc-sasl--mechanism-offered-p "external,bar")) + (should (erc-sasl--mechanism-offered-p "foo,external,bar")) + (should-not (erc-sasl--mechanism-offered-p "fooexternal")) + (should-not (erc-sasl--mechanism-offered-p "externalbar")))) + +(ert-deftest erc-sasl--read-password () + (ert-info ("Explicit erc-sasl-password") + (let ((erc-sasl--options '((password . "foo")))) + (should (string= (erc-sasl--read-password nil) "foo")))) + + (ert-info ("Fallback to erc-session-password") + (let ((erc-session-password "bar") + (erc-networks--id (erc-networks--id-create nil))) + (should (string= (erc-sasl--read-password nil) "bar"))) + (let ((erc-session-password "bar") + (erc-sasl--options '((user . "tester") (password))) + (erc-networks--id (erc-networks--id-create nil))) + (should (string= (erc-sasl--read-password nil) "bar")))) + + (let* ((entries (list + "machine FSF.chat port 6697 user bob password sesame" + ;; This must come *after* ^, else *1 (below) always passes + "machine GNU/chat port 6697 user bob password spam" + "machine MyHost port irc password 123")) + (netrc-file (make-temp-file "auth-source-test" nil nil + (mapconcat 'identity entries "\n"))) + (auth-sources (list netrc-file)) + (erc-session-server "irc.gnu.org") + (erc-session-port 6697) + (erc-networks--id (erc-networks--id-create nil)) + ;; + (erc-sasl-auth-source-function #'erc--auth-source-search) + erc-server-announced-name ; too early + auth-source-do-cache) + + (unwind-protect + (ert-info ("Auth source") + + (ert-info ("Symbol as password specifies machine") + (let ((erc-sasl--options '((user . "bob") + (password . FSF.chat))) + (erc-networks--id (make-erc-networks--id))) + (should (string= (erc-sasl--read-password nil) "sesame")))) + + (ert-info ("Use session ID when password empty") ; *1 + (let ((erc-sasl--options '((user . "bob") (password))) + (erc-networks--id (erc-networks--id-create 'GNU/chat))) + (should (string= (erc-sasl--read-password nil) "spam"))))) + + (delete-file netrc-file)) + + (ert-info ("Prompt when search fails and server password null") + (let ((erc-sasl-auth-source-function #'ignore)) + (should (string= (ert-simulate-keys "baz\r" + (erc-sasl--read-password "pwd:")) + "baz")))))) + +(ert-deftest erc-sasl-create-client--plain () + (let* ((erc-session-password "password123") + (erc-server-current-nick "tester") + (erc-session-port 1667) + (erc-session-server "localhost") + (client (erc-sasl--create-client 'plain)) + (result (sasl-next-step client nil))) + (should (equal (format "%S" [erc-sasl--plain-response + "\0tester\0password123"]) + (format "%S" result))) + (should (string= (sasl-step-data result) "\0tester\0password123")) + (should-not (sasl-next-step client result))) + (should (equal (assoc-default "PLAIN" sasl-mechanism-alist) '(sasl-plain)))) + +(ert-deftest erc-sasl-create-client--external () + (let* ((erc-server-current-nick "tester") + (client (erc-sasl--create-client 'external)) + (result (sasl-next-step client nil))) + (should (equal (format "%S" [ignore nil]) (format "%S" result))) + (should-not (sasl-step-data result)) + (should-not (sasl-next-step client result))) + (should-not (member "EXTERNAL" sasl-mechanisms)) + (should-not (assoc-default "EXTERNAL" sasl-mechanism-alist))) + +(ert-deftest erc-sasl-create-client--scram-sha-1 () + (let* ((erc-server-current-nick "jilles") + (erc-session-password "sesame") + (erc-sasl--options '((authzid . "jilles"))) + (mock-rvs (list "c5RqLCZy0L4fGkKAZ0hujFBs" "")) + (sasl-unique-id-function (lambda () (pop mock-rvs))) + (client (erc-sasl--create-client 'scram-sha-1)) + (step (sasl-next-step client nil))) + (ert-info ("Client's initial request") + (let ((req "n,a=jilles,n=jilles,r=c5RqLCZy0L4fGkKAZ0hujFBs")) + (should (equal (format "%S" + `[erc-compat--sasl-scram-client-first-message + ,req]) + (format "%S" step))) + (should (string= (sasl-step-data step) req)))) + (ert-info ("Server's initial response") + (let ((resp (concat "r=c5RqLCZy0L4fGkKAZ0hujFBsXQoKcivqCw9iDZPSpb," + "s=5mJO6d4rjCnsBU1X," + "i=4096")) + (req (concat "c=bixhPWppbGxlcyw=," + "r=c5RqLCZy0L4fGkKAZ0hujFBsXQoKcivqCw9iDZPSpb," + "p=OVUhgPu8wEm2cDoVLfaHzVUYPWU="))) + (sasl-step-set-data step resp) + (setq step (sasl-next-step client step)) + (should (equal (format "%S" + `[erc-sasl--scram-sha-1-client-final-message + ,req]) + (format "%S" step))) + (should (string= (sasl-step-data step) req)))) + (ert-info ("Server's final message") + (let ((resp "v=ZWR23c9MJir0ZgfGf5jEtLOn6Ng=")) + (sasl-step-set-data step resp) + (setq step (sasl-next-step client step)) + (should-not (sasl-step-data step))))) + (should (eq sasl-unique-id-function #'sasl-unique-id-function))) + +(ert-deftest erc-sasl-create-client--scram-sha-256 () + (unless (featurep 'sasl-scram-sha256) + (ert-skip "Emacs lacks sasl-scram-sha256")) + (let* ((erc-server-current-nick "jilles") + (erc-session-password "sesame") + (erc-sasl--options '((authzid . "jilles"))) + (mock-rvs (list "c5RqLCZy0L4fGkKAZ0hujFBs" "")) + (sasl-unique-id-function (lambda () (pop mock-rvs))) + (client (erc-sasl--create-client 'scram-sha-256)) + (step (sasl-next-step client nil))) + (ert-info ("Client's initial request") + (let ((req "n,a=jilles,n=jilles,r=c5RqLCZy0L4fGkKAZ0hujFBs")) + (should (equal (format "%S" + `[erc-compat--sasl-scram-client-first-message + ,req]) + (format "%S" step))) + (should (string= (sasl-step-data step) req)))) + (ert-info ("Server's initial response") + (let ((resp (concat + "r=c5RqLCZy0L4fGkKAZ0hujFBse697140729d8445fb95ec94ceacb14b3," + "s=MTk2M2VkMzM5ZmU0NDRiYmI0MzIyOGVhN2YwNzYwNmI=," + "i=4096")) + (req (concat + "c=bixhPWppbGxlcyw=," + "r=c5RqLCZy0L4fGkKAZ0hujFBse697140729d8445fb95ec94ceacb14b3," + "p=1vDesVBzJmv0lX0Ae1kHFtdVHkC6j4gISKVqaR45HFg="))) + (sasl-step-set-data step resp) + (setq step (sasl-next-step client step)) + (should (equal (format "%S" + `[erc-sasl--scram-sha-256-client-final-message + ,req]) + (format "%S" step))) + (should (string= (sasl-step-data step) req)))) + (ert-info ("Server's final message") + (let ((resp "v=gUePTYSZN9xgcE06KSyKO9fUmSwH26qifoapXyEs75s=")) + (sasl-step-set-data step resp) + (setq step (sasl-next-step client step)) + (should-not (sasl-step-data step))))) + (should (eq sasl-unique-id-function #'sasl-unique-id-function))) + +(ert-deftest erc-sasl-create-client--scram-sha-256--no-authzid () + (unless (featurep 'sasl-scram-sha256) + (ert-skip "Emacs lacks sasl-scram-sha256")) + (let* ((erc-server-current-nick "jilles") + (erc-session-password "sesame") + (mock-rvs (list "c5RqLCZy0L4fGkKAZ0hujFBs" "")) + (sasl-unique-id-function (lambda () (pop mock-rvs))) + (client (erc-sasl--create-client 'scram-sha-256)) + (step (sasl-next-step client nil))) + (ert-info ("Client's initial request") + (let ((req "n,,n=jilles,r=c5RqLCZy0L4fGkKAZ0hujFBs")) + (should (equal (format "%S" + `[erc-compat--sasl-scram-client-first-message + ,req]) + (format "%S" step))) + (should (string= (sasl-step-data step) req)))) + (ert-info ("Server's initial response") + (let ((resp (concat + "r=c5RqLCZy0L4fGkKAZ0hujFBsd4067f0afdb54c3dbd4fe645b84cae37," + "s=ZTg1MmE1YmFhZGI1NDcyMjk3NzYwZmRjZDM3Y2I1OTM=," + "i=4096")) + (req (concat + "c=biws," + "r=c5RqLCZy0L4fGkKAZ0hujFBsd4067f0afdb54c3dbd4fe645b84cae37," + "p=LP4sjJrjJKp5qTsARyZCppXpKLu4FMM284hNESPvGhI="))) + (sasl-step-set-data step resp) + (setq step (sasl-next-step client step)) + (should (equal (format "%S" + `[erc-sasl--scram-sha-256-client-final-message + ,req]) + (format "%S" step))) + (should (string= (sasl-step-data step) req)))) + (ert-info ("Server's final message") + (let ((resp "v=847WXfnmReGyE1qlq1And6R4bPBNROTZ7EMS/QrJtUM=")) + (sasl-step-set-data step resp) + (setq step (sasl-next-step client step)) + (should-not (sasl-step-data step))))) + (should (eq sasl-unique-id-function #'sasl-unique-id-function))) + +(ert-deftest erc-sasl-create-client--scram-sha-512--no-authzid () + (unless (featurep 'sasl-scram-sha256) + (ert-skip "Emacs lacks sasl-scram-sha512")) + (let* ((erc-server-current-nick "jilles") + (erc-session-password "sesame") + (mock-rvs (list "c5RqLCZy0L4fGkKAZ0hujFBs" "")) + (sasl-unique-id-function (lambda () (pop mock-rvs))) + (client (erc-sasl--create-client 'scram-sha-512)) + (step (sasl-next-step client nil))) + (ert-info ("Client's initial request") + (let ((req "n,,n=jilles,r=c5RqLCZy0L4fGkKAZ0hujFBs")) + (should (equal (format "%S" + `[erc-compat--sasl-scram-client-first-message + ,req]) + (format "%S" step))) + (should (string= (sasl-step-data step) req)))) + (ert-info ("Server's initial response") + (let ((resp (concat + "r=c5RqLCZy0L4fGkKAZ0hujFBs54c592745ce14e559fcc3f27b15464f6," + "s=YzMzOWZiY2U0YzcwNDA0M2I4ZGE2M2ZjOTBjODExZTM=," + "i=4096")) + (req (concat + "c=biws," + "r=c5RqLCZy0L4fGkKAZ0hujFBs54c592745ce14e559fcc3f27b15464f6," + "p=vMBb9tKxFAfBtel087/GLbo4objAIYr1wM+mFv/jYLKXE" + "NUF0vynm81qQbywQE5ScqFFdAfwYMZq/lj4s0V1OA=="))) + (sasl-step-set-data step resp) + (setq step (sasl-next-step client step)) + (should (equal (format + "%S" `[erc-sasl--scram-sha-512-client-final-message + ,req]) + (format "%S" step))) + (should (string= (sasl-step-data step) req)))) + (ert-info ("Server's final message") + (let ((resp (concat "v=Va7NIvt8wCdhvxnv+bZriSxGoto6On5EVnRHO/ece8zs0" + "qpQassdqir1Zlwh3e3EmBq+kcSy+ClNCsbzBpXe/w=="))) + (sasl-step-set-data step resp) + (setq step (sasl-next-step client step)) + (should-not (sasl-step-data step))))) + (should (eq sasl-unique-id-function #'sasl-unique-id-function))) + +(defconst erc-sasl-tests-ecdsa-key-file " +-----BEGIN EC PARAMETERS----- +BggqhkjOPQMBBw== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIIJueQ3W2IrGbe9wKdOI75yGS7PYZSj6W4tg854hlsvmoAoGCCqGSM49 +AwEHoUQDQgAEAZmaVhNSMmV5r8FXPvKuMnqDKyIA9pDHN5TNMfiF3mMeikGgK10W +IRX9cyi2wdYg9mUUYyh9GKdBCYHGUJAiCA== +-----END EC PRIVATE KEY----- +") + +(ert-deftest erc-sasl-create-client-ecdsa () + :tags '(:unstable) + ;; This is currently useless because it just roundtrips shelling out + ;; to pkeyutl. + (ert-skip "Placeholder") + (unless (executable-find "openssl") + (ert-skip "System lacks openssl")) + (ert-with-temp-file keyfile + :prefix "ecdsa_key" + :suffix ".pem" + :text erc-sasl-tests-ecdsa-key-file + (let* ((erc-server-current-nick "jilles") + (erc-sasl--options `((password . ,keyfile))) + (client (erc-sasl--create-client 'ecdsa-nist256p-challenge)) + (step (sasl-next-step client nil))) + (ert-info ("Client's initial request") + (should (equal (format "%S" [erc-sasl--ecdsa-first "jilles"]) + (format "%S" step))) + (should (string= (sasl-step-data step) "jilles"))) + (ert-info ("Server's initial response") + (let ((resp (concat "\0\1\2\3\4\5\6\7\10\11\12\13\14\15\16\17\20" + "\21\22\23\24\25\26\27\30\31\32\33\34\35\36\37"))) + (sasl-step-set-data step resp) + (setq step (sasl-next-step client step)) + (ert-with-temp-file sigfile + :prefix "ecdsa_sig" + :suffix ".sig" + :text (sasl-step-data step) + (with-temp-buffer + (set-buffer-multibyte nil) + (insert resp) + (let ((ec (call-process-region + (point-min) (point-max) + "openssl" 'delete t nil "pkeyutl" + "-inkey" keyfile "-sigfile" sigfile + "-verify"))) + (unless (zerop ec) + (message "%s" (buffer-string))) + (should (zerop ec))))))) + (should-not (sasl-next-step client step))))) + +;;; erc-sasl-tests.el ends here diff --git a/test/lisp/erc/erc-scenarios-sasl.el b/test/lisp/erc/erc-scenarios-sasl.el new file mode 100644 index 0000000000..7970e65ec2 --- /dev/null +++ b/test/lisp/erc/erc-scenarios-sasl.el @@ -0,0 +1,208 @@ +;;; erc-scenarios-sasl.el --- SASL tests for ERC -*- lexical-binding: t -*- + +;; Copyright (C) 2022 Free Software Foundation, Inc. +;; +;; This file is part of GNU Emacs. +;; +;; This program is free software: you can redistribute it and/or +;; modify it under the terms of the GNU General Public License as +;; published by the Free Software Foundation, either version 3 of the +;; License, or (at your option) any later version. +;; +;; This program is distributed in the hope that it will be useful, but +;; WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +;; General Public License for more details. +;; +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see +;; . + +;;; Code: + +(require 'ert-x) +(eval-and-compile + (let ((load-path (cons (ert-resource-directory) load-path))) + (require 'erc-scenarios-common))) + +(declare-function sasl-client-name "sasl" (client)) + +(require 'erc-scenarios-common) +(require 'erc-sasl) + +(ert-deftest erc-scenarios-sasl--plain () + :tags '(:expensive-test) + (erc-scenarios-common-with-cleanup + ((erc-scenarios-common-dialog "sasl") + (erc-d-linger-secs 0.5) + (erc-server-flood-penalty 0.1) + (dumb-server (erc-d-run "localhost" t 'plain)) + (port (process-contact dumb-server :service)) + (erc-modules (cons 'sasl erc-modules)) + (erc-sasl-mechanism 'plain) + (erc-sasl-password "password123") + (erc-sasl--session-options nil) + (inhibit-message noninteractive) + (expect (erc-d-t-make-expecter))) + + (ert-info ("Connect") + (with-current-buffer (erc :server "127.0.0.1" + :port port + :nick "tester" + :user "tester" + :full-name "tester") + (should (string= (buffer-name) (format "127.0.0.1:%d" port))))) + + (erc-d-t-wait-for 10 "server buffer ready" (get-buffer "ExampleOrg")) + + (ert-info ("Notices received") + (with-current-buffer "ExampleOrg" + (funcall expect 10 "This server is in debug mode") + ;; Regression "\0\0\0\0 ..." caused by (fillarray passphrase 0) + (should (string= erc-sasl-password "password123")))))) + +;; This is meant to assert `erc-update-modules' and local-module +;; behavior generally. It only exists here for convenience because as +;; of ERC 5.5, `sasl' is the only local module. +(ert-deftest erc-scenarios-sasl--local-modules-reconnect () + :tags '(:expensive-test) + (erc-scenarios-common-with-cleanup + ((erc-scenarios-common-dialog "sasl") + (erc-server-flood-penalty 0.1) + (dumb-server (erc-d-run "localhost" t 'plain 'plain)) + (port (process-contact dumb-server :service)) + (erc-sasl--session-options nil) + (inhibit-message noninteractive) + (expect (erc-d-t-make-expecter))) + + (ert-info ("Connect with options let-bound") + (with-current-buffer + ;; This won't work unless the library is already loaded + (let ((erc-modules (cons 'sasl erc-modules)) + (erc-sasl-mechanism 'plain) + (erc-sasl-password "password123")) + (erc :server "127.0.0.1" + :port port + :nick "tester" + :user "tester" + :full-name "tester")) + (should (string= (buffer-name) (format "127.0.0.1:%d" port))))) + + (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "ExampleOrg")) + (ert-info ("First connection succeeds") + (funcall expect 10 "This server is in debug mode") + (erc-cmd-QUIT "") + (funcall expect 10 "finished")) + + (should-not erc-sasl-password) ; obviously + (should-not (memq 'sasl erc-modules)) + + (erc-d-t-wait-for 10 (not (erc-server-process-alive))) + (erc-cmd-RECONNECT) + (ert-info ("Second connection succeeds") + (funcall expect 10 "This server is in debug mode") + (erc-cmd-QUIT "") + (funcall expect 10 "finished"))))) + +(ert-deftest erc-scenarios-sasl--external () + :tags '(:expensive-test) + (erc-scenarios-common-with-cleanup + ((erc-scenarios-common-dialog "sasl") + (erc-d-linger-secs 0.5) + (erc-server-flood-penalty 0.1) + (dumb-server (erc-d-run "localhost" t 'external)) + (port (process-contact dumb-server :service)) + (erc-modules (cons 'sasl erc-modules)) + (erc-sasl-mechanism 'external) + (erc-sasl--session-options nil) + (inhibit-message noninteractive) + (expect (erc-d-t-make-expecter))) + + (ert-info ("Connect") + (with-current-buffer (erc :server "127.0.0.1" + :port port + :nick "tester" + :user "tester" + :full-name "tester") + (should (string= (buffer-name) (format "127.0.0.1:%d" port))))) + + (erc-d-t-wait-for 10 "server buffer ready" (get-buffer "ExampleOrg")) + + (ert-info ("Notices received") + (with-current-buffer "ExampleOrg" + (funcall expect 10 "903 * Authentication successful") + (funcall expect 10 "This server is in debug mode"))))) + +(ert-deftest erc-scenarios-sasl--plain-fail () + :tags '(:expensive-test) + (erc-scenarios-common-with-cleanup + ((erc-scenarios-common-dialog "sasl") + (erc-d-linger-secs 0.5) + (erc-server-flood-penalty 0.1) + (dumb-server (erc-d-run "localhost" t 'plain-failed)) + (port (process-contact dumb-server :service)) + (erc-modules (cons 'sasl erc-modules)) + (erc-sasl-password "wrong") + (erc-sasl-mechanism 'plain) + (erc-sasl--session-options nil) + (inhibit-message noninteractive) + (expect (erc-d-t-make-expecter)) + (buf nil)) + + (ert-info ("Connect") + (setq buf (erc :server "127.0.0.1" + :port port + :nick "tester" + :user "tester" + :full-name "tester")) + (let ((err (should-error + (with-current-buffer buf + (funcall expect 20 "Connection failed!"))))) + (should (string-search "please review" (cadr err))) + (with-current-buffer buf + (funcall expect 10 "Opening connection") + (funcall expect 20 "SASL authentication failed") + (should-not (erc-server-process-alive))))))) + +(defun erc-scenarios--common--sasl (mech) + (erc-scenarios-common-with-cleanup + ((erc-scenarios-common-dialog "sasl") + (erc-d-linger-secs 0.5) + (erc-server-flood-penalty 0.1) + (dumb-server (erc-d-run "localhost" t mech)) + (port (process-contact dumb-server :service)) + (erc-modules (cons 'sasl erc-modules)) + (erc-sasl-password "sesame") + (erc-sasl-mechanism mech) + (erc-sasl--session-options nil) + (mock-rvs (list "c5RqLCZy0L4fGkKAZ0hujFBs" "")) + (sasl-unique-id-function (lambda () (pop mock-rvs))) + (inhibit-message noninteractive) + (expect (erc-d-t-make-expecter))) + + (ert-info ("Connect") + (with-current-buffer (erc :server "127.0.0.1" + :port port + :nick "jilles" + :full-name "jilles") + (should (string= (buffer-name) (format "127.0.0.1:%d" port))))) + + (erc-d-t-wait-for 10 "server buffer ready" (get-buffer "jaguar")) + + (ert-info ("Notices received") + (with-current-buffer "jaguar" + (funcall expect 10 "Found your hostname") + (funcall expect 20 "marked as being away"))))) + +(ert-deftest erc-scenarios-sasl--scram-sha-1 () + :tags '(:expensive-test) + (let ((erc-sasl-authzid "jilles")) + (erc-scenarios--common--sasl 'scram-sha-1))) + +(ert-deftest erc-scenarios-sasl--scram-sha-256 () + :tags '(:expensive-test) + (unless (featurep 'sasl-scram-sha256) + (ert-skip "Emacs lacks sasl-scram-sha256")) + (erc-scenarios--common--sasl 'scram-sha-256)) + +;;; erc-scenarios-sasl.el ends here diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el index d074b36c8b..91815b8fae 100644 --- a/test/lisp/erc/erc-tests.el +++ b/test/lisp/erc/erc-tests.el @@ -1004,8 +1004,13 @@ erc-update-modules (with-temp-buffer (erc-mode) (setq erc-modules '(completion autojoin networks)) - (let ((local-minor-modes '(font-lock-mode erc-fake-bar-mode))) - (should (equal (erc-update-modules t) '(erc-fake-bar-mode)))) + (if (< 27 emacs-major-version) + (let ((local-minor-modes '(font-lock-mode erc-fake-bar-mode))) + (should (equal (erc-update-modules t) '(erc-fake-bar-mode)))) + (cl-letf (((symbol-function 'buffer-local-variables) + (lambda (&rest _) '((font-lock-mode) + (erc-fake-bar-mode))))) + (should (equal (erc-update-modules t) '(erc-fake-bar-mode))))) (should (equal (nreverse calls) '( erc-pcomplete (completion . 1) erc-join (autojoin . 1) diff --git a/test/lisp/erc/resources/sasl/external.eld b/test/lisp/erc/resources/sasl/external.eld new file mode 100644 index 0000000000..2cd237ec4d --- /dev/null +++ b/test/lisp/erc/resources/sasl/external.eld @@ -0,0 +1,33 @@ +;; -*- mode: lisp-data; -*- +((cap-req 10 "CAP REQ :sasl")) +((nick 1 "NICK tester")) +((user 1 "USER tester 0 * :tester")) + +((auth-req 3.2 "AUTHENTICATE EXTERNAL") + (0.0 ":irc.example.org CAP * ACK :sasl") + (0.0 "AUTHENTICATE +")) + +((auth-noop 3.2 "AUTHENTICATE +") + (0.0 ":irc.example.org 900 * * tester :You are now logged in as tester") + (0.0 ":irc.example.org 903 * :Authentication successful")) + +((cap-end 3.2 "CAP END") + (0.0 ":irc.example.org 001 tester :Welcome to the ExampleOrg IRC Network tester") + (0.01 ":irc.example.org 002 tester :Your host is irc.example.org, running version oragono-2.6.1") + (0.01 ":irc.example.org 003 tester :This server was created Sat, 17 Jul 2021 09:06:42 UTC") + (0.01 ":irc.example.org 004 tester irc.example.org oragono-2.6.1 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv") + (0.0 ":irc.example.org 005 tester AWAYLEN=200 BOT=B CASEMAPPING=ascii CHANLIMIT=#:100 CHANMODES=Ibe,k,fl,CEMRUimnstu CHANNELLEN=64 CHANTYPES=# ELIST=U EXCEPTS EXTBAN=,m FORWARD=f INVEX KICKLEN=390 :are supported by this server") + (0.01 ":irc.example.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=ExampleOrg NICKLEN=32 PREFIX=(qaohv)~&@%+ STATUSMSG=~&@%+ TARGMAX=NAMES:1,LIST:1,KICK:1,WHOIS:1,USERHOST:10,PRIVMSG:4,TAGMSG:4,NOTICE:4,MONITOR:100 TOPICLEN=390 UTF8MAPPING=rfc8265 UTF8ONLY :are supported by this server") + (0.01 ":irc.example.org 005 tester draft/CHATHISTORY=100 :are supported by this server") + (0.0 ":irc.example.org 251 tester :There are 1 users and 0 invisible on 1 server(s)") + (0.0 ":irc.example.org 252 tester 0 :IRC Operators online") + (0.0 ":irc.example.org 253 tester 0 :unregistered connections") + (0.0 ":irc.example.org 254 tester 0 :channels formed") + (0.0 ":irc.example.org 255 tester :I have 1 clients and 0 servers") + (0.0 ":irc.example.org 265 tester 1 1 :Current local users 1, max 1") + (0.21 ":irc.example.org 266 tester 1 1 :Current global users 1, max 1") + (0.0 ":irc.example.org 422 tester :MOTD File is missing")) + +((mode-user 1.2 "MODE tester +i") + (0.0 ":irc.example.org 221 tester +Zi") + (0.0 ":irc.example.org NOTICE tester :This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.")) diff --git a/test/lisp/erc/resources/sasl/plain-failed.eld b/test/lisp/erc/resources/sasl/plain-failed.eld new file mode 100644 index 0000000000..336700290c --- /dev/null +++ b/test/lisp/erc/resources/sasl/plain-failed.eld @@ -0,0 +1,16 @@ +;; -*- mode: lisp-data; -*- +((cap-req 10 "CAP REQ :sasl")) +((nick 1 "NICK tester")) +((user 1 "USER tester 0 * :tester") + (0.0 ":irc.foonet.org NOTICE * :*** Looking up your hostname...") + (0.0 ":irc.foonet.org NOTICE * :*** Found your hostname") + (0.0 ":irc.foonet.org CAP * ACK :cap-notify sasl")) + +((authenticate-plain 3.2 "AUTHENTICATE PLAIN") + (0.0 ":irc.foonet.org AUTHENTICATE +")) + +((authenticate-gimme 3.2 "AUTHENTICATE AHRlc3RlcgB3cm9uZw==") + (0.0 ":irc.foonet.org 900 * * tester :You are now logged in as tester") + (0.0 ":irc.foonet.org 904 * :SASL authentication failed: Invalid account credentials")) + +((cap-end 3.2 "CAP END")) diff --git a/test/lisp/erc/resources/sasl/plain.eld b/test/lisp/erc/resources/sasl/plain.eld new file mode 100644 index 0000000000..1341cd78e5 --- /dev/null +++ b/test/lisp/erc/resources/sasl/plain.eld @@ -0,0 +1,39 @@ +;; -*- mode: lisp-data; -*- +((cap-req 10 "CAP REQ :sasl")) +((nick 1 "NICK tester")) +((user 1 "USER tester 0 * :tester") + (0.0 ":irc.example.org NOTICE * :*** Looking up your hostname...") + (0.0 ":irc.example.org NOTICE * :*** Found your hostname") + (0.0 ":irc.example.org CAP * ACK :sasl")) + +((authenticate-plain 3.2 "AUTHENTICATE PLAIN") + (0.0 ":irc.example.org AUTHENTICATE +")) + +((authenticate-gimme 3.2 "AUTHENTICATE AHRlc3RlcgBwYXNzd29yZDEyMw==") + (0.0 ":irc.example.org 900 * * tester :You are now logged in as tester") + (0.0 ":irc.example.org 903 * :Authentication successful")) + +((cap-end 3.2 "CAP END") + (0.0 ":irc.example.org 001 tester :Welcome to the ExampleOrg IRC Network tester") + (0.01 ":irc.example.org 002 tester :Your host is irc.example.org, running version oragono-2.6.1") + (0.01 ":irc.example.org 003 tester :This server was created Sat, 17 Jul 2021 09:06:42 UTC") + (0.01 ":irc.example.org 004 tester irc.example.org oragono-2.6.1 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv") + (0.0 ":irc.example.org 005 tester AWAYLEN=200 BOT=B CASEMAPPING=ascii CHANLIMIT=#:100 CHANMODES=Ibe,k,fl,CEMRUimnstu CHANNELLEN=64 CHANTYPES=# ELIST=U EXCEPTS EXTBAN=,m FORWARD=f INVEX KICKLEN=390 :are supported by this server") + (0.01 ":irc.example.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=ExampleOrg NICKLEN=32 PREFIX=(qaohv)~&@%+ STATUSMSG=~&@%+ TARGMAX=NAMES:1,LIST:1,KICK:1,WHOIS:1,USERHOST:10,PRIVMSG:4,TAGMSG:4,NOTICE:4,MONITOR:100 TOPICLEN=390 UTF8MAPPING=rfc8265 UTF8ONLY :are supported by this server") + (0.01 ":irc.example.org 005 tester draft/CHATHISTORY=100 :are supported by this server") + (0.0 ":irc.example.org 251 tester :There are 1 users and 0 invisible on 1 server(s)") + (0.0 ":irc.example.org 252 tester 0 :IRC Operators online") + (0.0 ":irc.example.org 253 tester 0 :unregistered connections") + (0.0 ":irc.example.org 254 tester 0 :channels formed") + (0.0 ":irc.example.org 255 tester :I have 1 clients and 0 servers") + (0.0 ":irc.example.org 265 tester 1 1 :Current local users 1, max 1") + (0.21 ":irc.example.org 266 tester 1 1 :Current global users 1, max 1") + (0.0 ":irc.example.org 422 tester :MOTD File is missing")) + +((mode-user 1.2 "MODE tester +i") + (0.0 ":irc.example.org 221 tester +Zi") + (0.0 ":irc.example.org NOTICE tester :This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.")) + +((quit 5 "QUIT :\2ERC\2") + (0 ":tester!~u@yuvqisyu7m7qs.irc QUIT :Quit")) +((drop 1 DROP)) diff --git a/test/lisp/erc/resources/sasl/scram-sha-1.eld b/test/lisp/erc/resources/sasl/scram-sha-1.eld new file mode 100644 index 0000000000..49980e9e12 --- /dev/null +++ b/test/lisp/erc/resources/sasl/scram-sha-1.eld @@ -0,0 +1,47 @@ +;;; -*- mode: lisp-data -*- +((cap-req 5.2 "CAP REQ :sasl")) +((nick 10 "NICK jilles")) +((user 10 "USER user 0 * :jilles") + (0 "NOTICE AUTH :*** Processing connection to jaguar.test") + (0 "NOTICE AUTH :*** Looking up your hostname...") + (0 "NOTICE AUTH :*** Checking Ident") + (0 "NOTICE AUTH :*** No Ident response") + (0 "NOTICE AUTH :*** Found your hostname") + (0 ":jaguar.test CAP jilles ACK :sasl")) + +((auth-init 10 "AUTHENTICATE SCRAM-SHA-1") + (0 "AUTHENTICATE +")) + +((auth-challenge 10 "AUTHENTICATE bixhPWppbGxlcyxuPWppbGxlcyxyPWM1UnFMQ1p5MEw0ZkdrS0FaMGh1akZCcw==") + (0 "AUTHENTICATE cj1jNVJxTENaeTBMNGZHa0tBWjBodWpGQnNYUW9LY2l2cUN3OWlEWlBTcGIscz01bUpPNmQ0cmpDbnNCVTFYLGk9NDA5Ng==")) + +((auth-final 10 "AUTHENTICATE Yz1iaXhoUFdwcGJHeGxjeXc9LHI9YzVScUxDWnkwTDRmR2tLQVowaHVqRkJzWFFvS2NpdnFDdzlpRFpQU3BiLHA9T1ZVaGdQdTh3RW0yY0RvVkxmYUh6VlVZUFdVPQ==") + (0 "AUTHENTICATE dj1aV1IyM2M5TUppcjBaZ2ZHZjVqRXRMT242Tmc9")) + +((auth-done 10 "AUTHENTICATE +") + (0 ":jaguar.test 900 jilles jilles!jilles@localhost.stack.nl jilles :You are now logged in as jilles") + (0 ":jaguar.test 903 jilles :SASL authentication successful")) + +((cap-end 10.2 "CAP END") + (0 ":jaguar.test 001 jilles :Welcome to the jaguar IRC Network jilles!~jilles@127.0.0.1") + (0 ":jaguar.test 002 jilles :Your host is jaguar.test, running version InspIRCd-3") + (0 ":jaguar.test 003 jilles :This server was created 09:44:05 Dec 24 2020") + (0 ":jaguar.test 004 jilles jaguar.test InspIRCd-3 BILRSWcghiorswz ABEFHIJLMNOQRSTXYabcefghijklmnopqrstuvz :BEFHIJLXYabefghjkloqv") + (0 ":jaguar.test 005 jilles ACCEPT=30 AWAYLEN=200 BOT=B CALLERID=g CASEMAPPING=rfc1459 CHANLIMIT=#:120 CHANMODES=IXbeg,k,BEFHJLfjl,AMNOQRSTcimnprstuz CHANNELLEN=64 CHANTYPES=# ELIST=CMNTU ESILENCE=CcdiNnPpTtx EXCEPTS=e :are supported by this server") + (0 ":jaguar.test 005 jilles EXTBAN=,ANOQRSTUacmnprz HOSTLEN=64 INVEX=I KEYLEN=32 KICKLEN=255 LINELEN=512 MAXLIST=I:100,X:100,b:100,e:100,g:100 MAXTARGETS=20 MODES=20 MONITOR=30 NAMELEN=128 NAMESX NETWORK=jaguar :are supported by this server") + (0 ":jaguar.test 005 jilles NICKLEN=31 PREFIX=(Yqaohv)!~&@%+ REMOVE SAFELIST SECURELIST=60 SILENCE=32 STATUSMSG=!~&@%+ TOPICLEN=307 UHNAMES USERIP USERLEN=11 USERMODES=,,s,BILRSWcghiorwz WATCH=30 :are supported by this server") + (0 ":jaguar.test 005 jilles :are supported by this server") + (0 ":jaguar.test 251 jilles :There are 740 users and 108 invisible on 11 servers") + (0 ":jaguar.test 252 jilles 10 :operator(s) online") + (0 ":jaguar.test 254 jilles 373 :channels formed") + (0 ":jaguar.test 255 jilles :I have 28 clients and 1 servers") + (0 ":jaguar.test 265 jilles :Current local users: 28 Max: 29") + (0 ":jaguar.test 266 jilles :Current global users: 848 Max: 879") + (0 ":jaguar.test 375 jilles :jaguar.test message of the day") + (0 ":jaguar.test 372 jilles : ~~ some message of the day ~~") + (0 ":jaguar.test 372 jilles : ~~ or rkpryyrag gb rnpu bgure ~~") + (0 ":jaguar.test 376 jilles :End of message of the day.")) + +((mode-user 1.2 "MODE jilles +i") + (0 ":jilles!~jilles@127.0.0.1 MODE jilles :+ri") + (0 ":jaguar.test 306 jilles :You have been marked as being away")) diff --git a/test/lisp/erc/resources/sasl/scram-sha-256.eld b/test/lisp/erc/resources/sasl/scram-sha-256.eld new file mode 100644 index 0000000000..74de9a23ec --- /dev/null +++ b/test/lisp/erc/resources/sasl/scram-sha-256.eld @@ -0,0 +1,47 @@ +;;; -*- mode: lisp-data -*- +((cap-req 5.2 "CAP REQ :sasl")) +((nick 10 "NICK jilles")) +((user 10 "USER user 0 * :jilles") + (0 "NOTICE AUTH :*** Processing connection to jaguar.test") + (0 "NOTICE AUTH :*** Looking up your hostname...") + (0 "NOTICE AUTH :*** Checking Ident") + (0 "NOTICE AUTH :*** No Ident response") + (0 "NOTICE AUTH :*** Found your hostname") + (0 ":jaguar.test CAP jilles ACK :sasl")) + +((auth-init 10 "AUTHENTICATE SCRAM-SHA-256") + (0 "AUTHENTICATE +")) + +((auth-challenge 10 "AUTHENTICATE biwsbj1qaWxsZXMscj1jNVJxTENaeTBMNGZHa0tBWjBodWpGQnM=") + (0 "AUTHENTICATE cj1jNVJxTENaeTBMNGZHa0tBWjBodWpGQnNkNDA2N2YwYWZkYjU0YzNkYmQ0ZmU2NDViODRjYWUzNyxzPVpUZzFNbUUxWW1GaFpHSTFORGN5TWprM056WXdabVJqWkRNM1kySTFPVE09LGk9NDA5Ng==")) + +((auth-final 10 "AUTHENTICATE Yz1iaXdzLHI9YzVScUxDWnkwTDRmR2tLQVowaHVqRkJzZDQwNjdmMGFmZGI1NGMzZGJkNGZlNjQ1Yjg0Y2FlMzcscD1MUDRzakpyakpLcDVxVHNBUnlaQ3BwWHBLTHU0Rk1NMjg0aE5FU1B2R2hJPQ==") + (0 "AUTHENTICATE dj04NDdXWGZubVJlR3lFMXFscTFBbmQ2UjRiUEJOUk9UWjdFTVMvUXJKdFVNPQ==")) + +((auth-done 10 "AUTHENTICATE +") + (0 ":jaguar.test 900 jilles jilles!jilles@localhost.stack.nl jilles :You are now logged in as jilles") + (0 ":jaguar.test 903 jilles :SASL authentication successful")) + +((cap-end 10.2 "CAP END") + (0 ":jaguar.test 001 jilles :Welcome to the jaguar IRC Network jilles!~jilles@127.0.0.1") + (0 ":jaguar.test 002 jilles :Your host is jaguar.test, running version InspIRCd-3") + (0 ":jaguar.test 003 jilles :This server was created 09:44:05 Dec 24 2020") + (0 ":jaguar.test 004 jilles jaguar.test InspIRCd-3 BILRSWcghiorswz ABEFHIJLMNOQRSTXYabcefghijklmnopqrstuvz :BEFHIJLXYabefghjkloqv") + (0 ":jaguar.test 005 jilles ACCEPT=30 AWAYLEN=200 BOT=B CALLERID=g CASEMAPPING=rfc1459 CHANLIMIT=#:120 CHANMODES=IXbeg,k,BEFHJLfjl,AMNOQRSTcimnprstuz CHANNELLEN=64 CHANTYPES=# ELIST=CMNTU ESILENCE=CcdiNnPpTtx EXCEPTS=e :are supported by this server") + (0 ":jaguar.test 005 jilles EXTBAN=,ANOQRSTUacmnprz HOSTLEN=64 INVEX=I KEYLEN=32 KICKLEN=255 LINELEN=512 MAXLIST=I:100,X:100,b:100,e:100,g:100 MAXTARGETS=20 MODES=20 MONITOR=30 NAMELEN=128 NAMESX NETWORK=jaguar :are supported by this server") + (0 ":jaguar.test 005 jilles NICKLEN=31 PREFIX=(Yqaohv)!~&@%+ REMOVE SAFELIST SECURELIST=60 SILENCE=32 STATUSMSG=!~&@%+ TOPICLEN=307 UHNAMES USERIP USERLEN=11 USERMODES=,,s,BILRSWcghiorwz WATCH=30 :are supported by this server") + (0 ":jaguar.test 005 jilles :are supported by this server") + (0 ":jaguar.test 251 jilles :There are 740 users and 108 invisible on 11 servers") + (0 ":jaguar.test 252 jilles 10 :operator(s) online") + (0 ":jaguar.test 254 jilles 373 :channels formed") + (0 ":jaguar.test 255 jilles :I have 28 clients and 1 servers") + (0 ":jaguar.test 265 jilles :Current local users: 28 Max: 29") + (0 ":jaguar.test 266 jilles :Current global users: 848 Max: 879") + (0 ":jaguar.test 375 jilles :jaguar.test message of the day") + (0 ":jaguar.test 372 jilles : ~~ some message of the day ~~") + (0 ":jaguar.test 372 jilles : ~~ or rkpryyrag gb rnpu bgure ~~") + (0 ":jaguar.test 376 jilles :End of message of the day.")) + +((mode-user 1.2 "MODE jilles +i") + (0 ":jilles!~jilles@127.0.0.1 MODE jilles :+ri") + (0 ":jaguar.test 306 jilles :You have been marked as being away")) -- 2.38.1 --=-=-=--