From: "J.P." <jp@neverwas.me>
To: 29108@debbugs.gnu.org
Cc: emacs-erc@gnu.org, bandali@gnu.org
Subject: bug#29108: 25.3; ERC SASL support
Date: Sun, 13 Nov 2022 07:36:46 -0800 [thread overview]
Message-ID: <87o7taoohd.fsf__5476.25507913239$1668353859$gmane$org@neverwas.me> (raw)
In-Reply-To: <87o7thlepf.fsf@neverwas.me> (J. P.'s message of "Tue, 08 Nov 2022 06:10:20 -0800")
[-- Attachment #1: Type: text/plain, Size: 470 bytes --]
"J.P." <jp@neverwas.me> writes:
> In the interest of keeping things from stagnating further, I'd like to
> move development on this to HEAD so people can try it and provide
> feedback prior to Emacs 29 being cut. If there are any objections to
> that, please raise them before Saturday the 12th of November, 2022.
> Thanks.
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.
[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0000-v5-v6.diff --]
[-- Type: text/x-patch, Size: 35458 bytes --]
From 21f3196c0b55d8e7c27c4918f741cbbecfaf2136 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Sun, 13 Nov 2022 07:14:27 -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 | 137 +++++-
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 | 424 ++++++++++++++++++
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 ++
17 files changed, 1586 insertions(+), 80 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 80b4171cdb..79f8c92719 100644
--- a/doc/misc/erc.texi
+++ b/doc/misc/erc.texi
@@ -938,13 +938,12 @@ SASL
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. This means invoking
-@code{erc-sasl-mode} manually or calling @code{erc-update-modules}
-won't do any good. Instead, simply add @code{sasl} to
-@code{erc-modules} (or @code{let}-bind it while calling
-@code{erc-tls}), and SASL will be enabled for the current connection.
-But before that, please explore all custom options pertaining to your
-chosen mechanism.
+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.
diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index fee29e7d05..37a3da8b66 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -1529,7 +1529,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)
@@ -1539,6 +1539,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))))))
@@ -2165,6 +2168,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-common.el b/lisp/erc/erc-common.el
index 90ea56108d..a300cfc4fa 100644
--- a/lisp/erc/erc-common.el
+++ b/lisp/erc/erc-common.el
@@ -28,7 +28,6 @@
(defvar erc--casemapping-rfc1459)
(defvar erc--casemapping-rfc1459-strict)
-(defvar erc--module-name-migrations)
(defvar erc-channel-users)
(defvar erc-dbuf)
(defvar erc-log-p)
@@ -86,9 +85,40 @@ erc--target
(contents "" :type string)
(tags '() :type list))
-(defun erc--normalize-module-symbol (module)
- "Canonicalize symbol MODULE for `erc-modules'."
- (or (cdr (assq module erc--module-name-migrations)) module))
+;; 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)
@@ -118,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))))
@@ -141,21 +172,20 @@ define-erc-module
,(format "Enable ERC %S mode."
name)
(interactive)
- (unless ,local-p
- (cl-pushnew (erc--normalize-module-symbol ',name) erc-modules))
- (when (or ,(not local-p) (eq major-mode 'erc-mode))
- (setq ,mode t)
- ,@enable-body))
+ ,@(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)))
(defun ,disable ()
,(format "Disable ERC %S mode."
name)
(interactive)
- (unless ,local-p
- (setq erc-modules (delq (erc--normalize-module-symbol ',name)
- erc-modules)))
- (when (or ,(not local-p) ,mode)
- (setq ,mode nil)
- ,@disable-body))
+ ,@(unless local-p `((setq erc-modules (delq ',mod erc-modules))))
+ ,@(macroexp-unprogn
+ `(,@(if local-p `(when ,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 bc3e1dcfc6..6d4ef21383 100644
--- a/lisp/erc/erc-compat.el
+++ b/lisp/erc/erc-compat.el
@@ -194,9 +194,9 @@ erc-compat--sasl-scram-client-first-message
;; > The use of base64 in SCRAM is restricted to the canonical form
;; > with no whitespace.
;;
-;; Unfortunately, advising `base64-encode-string' won't work
-;; because the byte compiler precomputes the result when all inputs
-;; are constants, as they are in the unpatched version.
+;; 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
@@ -272,6 +272,18 @@ erc-compat--with-memoization
`(cl--generic-with-memoization ,table ,@forms))
(t `(progn ,@forms))))
+(defun erc-compat--local-minor-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-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-sasl.el b/lisp/erc/erc-sasl.el
index f36a305247..ac2646051c 100644
--- a/lisp/erc/erc-sasl.el
+++ b/lisp/erc/erc-sasl.el
@@ -19,8 +19,8 @@
;;; Commentary:
-;; This "non-IRCv3" implementation resembles many others that have
-;; surfaced over the years, the first possibly being:
+;; 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
;;
@@ -30,29 +30,35 @@
;;
;; - Find a way to obfuscate the password in memory (via something
;; like `auth-source--obfuscate'); it's currently visible in
-;; backtraces.
+;; 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)
+(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")) ; FIXME increment on next release
+ :package-version '(ERC . "5.4.1")) ; FIXME increment on next release
-(defcustom erc-sasl-mechanism nil
+(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 nil)
- (const plain)
+ :type '(choice (const plain)
(const external)
(const scram-sha-1)
(const scram-sha-256)
@@ -68,17 +74,18 @@ erc-sasl-user
(defcustom erc-sasl-password nil
"Optional account password to send when authenticating.
-When the value is a string, ERC uses it unconditionally for most
-mechanisms (see below). As a special case, when the value is a
-non-nil symbol, ERC uses it as the value of the `:host' field in
-an auth-source query, provided `erc-sasl-auth-source-function' is
-set to a function. When nil, ERC will try a non-nil \"session
-password\", likely one given as the `:password' argument to
-`erc-tls'. As a last resort, ERC will prompt the user for input.
+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, which is typically in PEM format."
+of the key."
:type '(choice (const nil) string symbol))
(defcustom erc-sasl-auth-source-function nil
@@ -91,7 +98,7 @@ erc-sasl-auth-source-function
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 (const erc-auth-source-search)
+ :type '(choice (function-item erc-auth-source-search)
(const nil)
function))
@@ -103,6 +110,13 @@ erc-sasl-authzid
;; 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)
@@ -263,13 +277,26 @@ erc-sasl--create-client
(sasl-client-set-property client 'ecdsa-keyfile keyfile)
client)))
-;; This stands alone because it's also used by bug#49860
+;; This stands alone because it's also used by bug#49860.
(defun erc-sasl--init ()
- (setq erc-sasl--state (make-erc-sasl--state)
- erc-sasl--options `((user . ,erc-sasl-user)
- (password . ,erc-sasl-password)
- (mechanism . ,erc-sasl-mechanism)
- (authzid . ,erc-sasl-authzid))))
+ ;; 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))
(defun erc-sasl--mechanism-offered-p (offered)
"Return non-nil when OFFERED appears among a list of mechanisms."
@@ -359,6 +386,7 @@ erc-sasl--destroy
(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)
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 2869383960..a703f903ec 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -1780,31 +1780,6 @@ erc-default-nicks
(defvar-local erc-nick-change-attempt-count 0
"Used to keep track of how many times an attempt at changing nick is made.")
-(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.")
-
-(defconst erc--modules-to-features
- (cl-loop for (feature . names) in erc--features-to-modules
- append (mapcar (lambda (name) (cons name feature)) names))
- "Migration alist mapping a module's name to 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-migrate-modules (mods)
"Migrate old names of ERC modules to new ones."
;; modify `transforms' to specify what needs to be changed
@@ -1888,23 +1863,25 @@ erc-modules
(repeat :tag "Others" :inline t symbol))
:group 'erc)
-(defun erc-update-modules ()
+(defun erc-update-modules (&optional defer-locals)
"Enable global minor mode for all global modules in `erc-modules'.
-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.6."
- (let (local-modules)
- (dolist (mod erc-modules)
- (require (or (alist-get mod erc--modules-to-features)
- (intern (concat "erc-" (symbol-name mod))))
+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-minor-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 ((sym (intern-soft (concat "erc-" (symbol-name mod) "-mode"))))
- (unless (and sym (fboundp sym))
- (error "`%s' is not a known ERC module" mod))
- (if (custom-variable-p sym)
- (funcall sym 1)
- (push sym local-modules))))
- local-modules))
+ (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'."
@@ -1966,15 +1943,17 @@ erc-open
(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))
(set-buffer buffer)
(setq old-point (point))
- (setq delayed-modules (erc-update-modules))
+ (setq delayed-modules (erc-update-modules 'defer-locals))
(delay-mode-hooks (erc-mode))
- (setq erc-server-reconnect-count old-recon-count)
+ (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
@@ -2030,10 +2009,11 @@ 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 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
@@ -3197,7 +3177,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
diff --git a/test/lisp/erc/erc-sasl-tests.el b/test/lisp/erc/erc-sasl-tests.el
index 112303baf5..81db9ad948 100644
--- a/test/lisp/erc/erc-sasl-tests.el
+++ b/test/lisp/erc/erc-sasl-tests.el
@@ -1,6 +1,6 @@
;;; erc-sasl-tests.el --- Tests for erc-sasl. -*- lexical-binding:t -*-
-;; Copyright (C) 2020-2022 Free Software Foundation, Inc.
+;; Copyright (C) 2022 Free Software Foundation, Inc.
;;
;; This file is part of GNU Emacs.
;;
@@ -276,6 +276,10 @@ erc-sasl-tests-ecdsa-key-file
")
(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
@@ -295,8 +299,21 @@ erc-sasl-create-client-ecdsa
"\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))
- ;; FIXME this is dumb
- (should (<= 68 (length (sasl-step-data step)) 72))))
+ (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
index 3ff7cc805d..7970e65ec2 100644
--- a/test/lisp/erc/erc-scenarios-sasl.el
+++ b/test/lisp/erc/erc-scenarios-sasl.el
@@ -41,6 +41,7 @@ erc-scenarios-sasl--plain
(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)))
@@ -60,6 +61,49 @@ erc-scenarios-sasl--plain
;; 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
@@ -70,6 +114,7 @@ erc-scenarios-sasl--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)))
@@ -99,6 +144,7 @@ erc-scenarios-sasl--plain-fail
(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))
@@ -128,6 +174,7 @@ erc-scenarios--common--sasl
(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)
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index 4646c35e25..91815b8fae 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -960,44 +960,60 @@ erc-migrate-modules
(should (equal (erc-migrate-modules erc-modules) erc-modules)))
(ert-deftest erc-update-modules ()
- (let* (calls
- (erc-modules '(fake-foo fake-bar)))
+ (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)))
- ((symbol-function 'erc-fake-foo-mode)
- (lambda (n) (push (cons 'fake-foo n) calls)))
- ;; Here, foo is a global module (minor mode)
- ((get 'erc-fake-foo-mode 'standard-value) #'ignore)
+
+ ;; 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)
+ ((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 ("Locals")
- (should (equal (erc-update-modules)
- '(erc-fake-bar-mode)))
- ;; Bar still required
- (should (equal (nreverse calls) '(erc-fake-foo
- (fake-foo . 1)
- erc-fake-bar)))
+ ((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)) ; no locals
- (should (equal (nreverse calls)
- '(erc-pcomplete
- (completion . 1)
- erc-join
- (autojoin . 1)
- erc-networks
- (networks . 1))))
- (setq calls nil)))))
+ (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))
+ (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)
+ erc-networks (networks . 1)))))))))
;;; erc-tests.el ends here
diff --git a/test/lisp/erc/resources/sasl/plain.eld b/test/lisp/erc/resources/sasl/plain.eld
index 9c6ce3feeb..1341cd78e5 100644
--- a/test/lisp/erc/resources/sasl/plain.eld
+++ b/test/lisp/erc/resources/sasl/plain.eld
@@ -33,3 +33,7 @@
((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))
--
2.38.1
[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: 0001-Add-GS2-authorization-to-sasl-scram-rfc.patch --]
[-- Type: text/x-patch, Size: 3030 bytes --]
From a7177b08ef8a0fe055d1e09045aaa95a8ba66ceb Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
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
[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #4: 0002-Don-t-set-erc-networks-id-until-network-is-known.patch --]
[-- Type: text/x-patch, Size: 7501 bytes --]
From 665eb8627e3b2ba1befeb64cbff0caf217a28089 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
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
[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #5: 0003-Support-local-ERC-modules-in-erc-mode-buffers.patch --]
[-- Type: text/x-patch, Size: 14134 bytes --]
From 6210a98556063dd22b0ddc36ec75cebab5cb9cd6 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Mon, 12 Jul 2021 03:44:28 -0700
Subject: [PATCH 3/5] Support local ERC modules in erc-mode buffers
* 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.)
---
lisp/erc/erc-common.el | 56 +++++++++++++++++++++++----
lisp/erc/erc-goodies.el | 1 +
lisp/erc/erc.el | 78 ++++++++++++++++++++------------------
test/lisp/erc/erc-tests.el | 58 ++++++++++++++++++++++++++++
4 files changed, 150 insertions(+), 43 deletions(-)
diff --git a/lisp/erc/erc-common.el b/lisp/erc/erc-common.el
index d8aac36eab..a300cfc4fa 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)))
+ ,@(macroexp-unprogn
+ `(,@(if local-p '(when (eq major-mode 'erc-mode)) '(progn))
+ (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 ,mode) '(progn))
+ (setq ,mode nil)
+ ,@disable-body)))
,(when (and alias (not (eq name alias)))
`(defalias
',(intern
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..6c9d4de2ba 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,28 @@ 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))
+ (delq nil (mapcar
+ (lambda (m)
+ (and (string-prefix-p "erc-" (symbol-name m)) m))
+ local-minor-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 +1939,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 +2013,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 +2037,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
[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #6: 0004-Call-erc-login-indirectly-via-new-generic-wrapper.patch --]
[-- Type: text/x-patch, Size: 1981 bytes --]
From e47b40618b6481e1fa2b751dd79a5a4e2f3da2a7 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
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
[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #7: 0005-Add-non-IRCv3-SASL-module-to-ERC.patch --]
[-- Type: text/x-patch, Size: 73241 bytes --]
From 21f3196c0b55d8e7c27c4918f741cbbecfaf2136 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
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.
(erc-compat--local-minor-modes): Add helper for finding local modules
active in an ERC buffer.
* 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 +++++-
lisp/erc/erc-compat.el | 116 +++++
lisp/erc/erc-sasl.el | 424 ++++++++++++++++++
lisp/erc/erc.el | 6 +-
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 ++
12 files changed, 1394 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 3db83197f9..79f8c92719 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.
@@ -478,6 +479,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
@@ -525,6 +530,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
@@ -842,6 +848,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
@@ -854,7 +861,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
@@ -915,6 +923,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/lisp/erc/erc-compat.el b/lisp/erc/erc-compat.el
index 03bd8f1352..6d4ef21383 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)
@@ -168,6 +272,18 @@ erc-compat--with-memoization
`(cl--generic-with-memoization ,table ,@forms))
(t `(progn ,@forms))))
+(defun erc-compat--local-minor-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-sasl.el b/lisp/erc/erc-sasl.el
new file mode 100644
index 0000000000..ac2646051c
--- /dev/null
+++ b/lisp/erc/erc-sasl.el
@@ -0,0 +1,424 @@
+;;; 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 <https://www.gnu.org/licenses/>.
+
+;;; 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 (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))
+
+(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))
+
+(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 mechanism: %s" mech))
+ (erc-error "Unknown mechanism: %s" mech))
+ (setf (erc-sasl--state-client erc-sasl--state) client))))
+ ((remove-hook 'erc-server-AUTHENTICATE-functions
+ #'erc-sasl--authenticate-handler t)
+ (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."
+ (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))))
+
+(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 6c9d4de2ba..a703f903ec 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"
@@ -1870,10 +1871,7 @@ erc-update-modules
introduced in ERC 5.5."
(let ((local-modes
(when (and defer-locals (derived-mode-p 'erc-mode))
- (delq nil (mapcar
- (lambda (m)
- (and (string-prefix-p "erc-" (symbol-name m)) m))
- local-minor-modes)))))
+ (erc-compat--local-minor-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))))
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 <https://www.gnu.org/licenses/>.
+
+;;; 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
+;; <https://www.gnu.org/licenses/>.
+
+;;; 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
next prev parent reply other threads:[~2022-11-13 15:36 UTC|newest]
Thread overview: 54+ messages / expand[flat|nested] mbox.gz Atom feed top
2017-11-01 20:07 bug#29108: 25.3; ERC SASL support Alex Branham
2017-11-10 2:24 ` Noam Postavsky
2019-10-23 9:24 ` Lars Ingebrigtsen
2019-10-23 10:34 ` Alex Branham
2019-10-23 11:19 ` Lars Ingebrigtsen
2019-10-23 12:19 ` Stefan Kangas
2019-10-23 12:57 ` Noam Postavsky
2019-10-23 13:32 ` Stefan Kangas
2019-11-02 14:10 ` Stefan Kangas
2020-08-03 9:39 ` Lars Ingebrigtsen
2021-07-28 16:59 ` Ulrich Mueller
2021-07-28 17:21 ` Eli Zaretskii
2021-07-28 22:42 ` J.P.
2021-08-09 9:59 ` J.P.
2021-08-09 10:22 ` Ulrich Mueller
2021-08-09 10:56 ` J.P.
2021-08-09 12:39 ` J.P.
2021-08-23 13:47 ` J.P.
[not found] ` <87o89oi87g.fsf@neverwas.me>
2021-08-23 14:01 ` Lars Ingebrigtsen
[not found] ` <87zgt8s1jt.fsf@gnus.org>
2021-08-24 13:42 ` J.P.
2022-09-18 18:32 ` bug#29108: [J.P.] Add "non-IRCv3" SASL to ERC J.P.
2022-09-20 6:07 ` bug#29108: 25.3; ERC SASL support J.P.
[not found] ` <875yhifujk.fsf_-_@neverwas.me>
2022-09-21 13:13 ` J.P.
2022-10-14 3:05 ` J.P.
[not found] ` <878rljxfxs.fsf@neverwas.me>
2022-10-26 13:14 ` J.P.
[not found] ` <87k04m4th8.fsf@neverwas.me>
2022-11-08 14:10 ` J.P.
[not found] ` <87o7thlepf.fsf@neverwas.me>
2022-11-09 4:08 ` Akib Azmain Turja via Bug reports for GNU Emacs, the Swiss army knife of text editors
2022-11-09 13:49 ` J.P.
[not found] ` <874jv81bn2.fsf@neverwas.me>
2022-11-09 17:50 ` Akib Azmain Turja via Bug reports for GNU Emacs, the Swiss army knife of text editors
[not found] ` <87iljoqaor.fsf@disroot.org>
2022-11-10 5:28 ` J.P.
[not found] ` <87sfirml89.fsf@neverwas.me>
2022-11-10 18:04 ` Adam Porter
2022-11-10 21:50 ` J.P.
2022-11-11 5:51 ` Akib Azmain Turja via Bug reports for GNU Emacs, the Swiss army knife of text editors
2022-11-14 22:28 ` Adam Porter
[not found] ` <87sfiq7a3j.fsf@neverwas.me>
2022-11-11 1:25 ` Adam Porter
2022-11-11 5:56 ` Akib Azmain Turja via Bug reports for GNU Emacs, the Swiss army knife of text editors
[not found] ` <878rkighkn.fsf@disroot.org>
2022-11-14 22:29 ` Adam Porter
2022-11-13 15:36 ` J.P. [this message]
[not found] ` <87o7taoohd.fsf@neverwas.me>
2022-11-14 6:45 ` J.P.
2022-11-14 15:20 ` J.P.
[not found] ` <87y1sdk1fg.fsf@neverwas.me>
2022-11-16 14:51 ` J.P.
[not found] ` <875yfflzps.fsf@neverwas.me>
2022-11-17 6:30 ` J.P.
[not found] ` <877czuks8k.fsf@neverwas.me>
2022-11-17 15:28 ` J.P.
2022-11-18 2:26 ` J.P.
[not found] ` <878rk9576b.fsf@neverwas.me>
2022-11-18 14:06 ` J.P.
[not found] ` <87leo8z79j.fsf@neverwas.me>
2022-11-19 14:48 ` J.P.
[not found] ` <87tu2vroeh.fsf@neverwas.me>
2022-11-20 14:29 ` J.P.
[not found] ` <87wn7pog1l.fsf@neverwas.me>
2022-11-21 15:09 ` J.P.
[not found] ` <87y1s4mjj6.fsf@neverwas.me>
2022-11-22 14:01 ` J.P.
[not found] ` <87r0xvks03.fsf@neverwas.me>
2022-11-24 2:49 ` Amin Bandali
[not found] ` <87r0xtnk24.fsf@gnu.org>
2022-11-25 14:43 ` J.P.
[not found] ` <87wn7jgkne.fsf@neverwas.me>
2022-11-28 0:08 ` J.P.
2022-11-29 5:19 ` Amin Bandali
[not found] ` <87iliyz6at.fsf@gnu.org>
2022-11-29 15:05 ` J.P.
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to='87o7taoohd.fsf__5476.25507913239$1668353859$gmane$org@neverwas.me' \
--to=jp@neverwas.me \
--cc=29108@debbugs.gnu.org \
--cc=bandali@gnu.org \
--cc=emacs-erc@gnu.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
Code repositories for project(s) associated with this external index
https://git.savannah.gnu.org/cgit/emacs.git
https://git.savannah.gnu.org/cgit/emacs/org-mode.git
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.