New article: Editing remote files with Emacs, comfortably.

This commit is contained in:
tastytea 2019-05-08 15:47:30 +02:00
parent ff69b060f2
commit 63b05f18ee
Signed by: tastytea
GPG Key ID: CFC39497F1B26E07
1 changed files with 215 additions and 0 deletions

View File

@ -0,0 +1,215 @@
---
title: "Editing remote files with Emacs, comfortably"
description: "Edit remote files with Emacs using SSH and a wrapper for emacsclient."
date: "2019-05-05T23:22:04+02:00"
draft: false
tags: ["emacs", "ssh"]
---
It took me a long time to collect all the bits and pieces I needed to make
editing remote files with Emacs work the way I want. I hope I can save you some
time by stitching it here together into a tutorial. I assume you use
https://github.com/jwiegley/use-package[use-package] in my examples.
== Emacs server & TRAMP
We start with Emacs's good old inbuilt
https://www.gnu.org/software/emacs/manual/html_node/emacs/Emacs-Server.html[server]. The
default is to use an UNIX domain socket; We have to change that to TCP to be able to
receive input from our remote hosts. The server will bind to 127.0.0.1. Pick a
strong password that is exactly 64 characters long and a port
https://www.w3.org/Daemon/User/Installation/PrivilegedPorts.html[above 1023]. I
chose 51313 because if we substitute the digits for letters in the Latin
alphabet, we get E M A C. The server will create the file
`~/.emacs.d/server/server` with the IP, port and password in it. This file needs
to be distributed to every host that should be able to access the server.
----
{{< highlight elisp >}}
;; Run server if:
;; - Our EUID is not 0,
;; - We are not logged in via SSH,
;; - It is not already running.
(unless (equal (user-real-uid) 0)
(unless (getenv "SSH_CONNECTION")
(use-package server
:init
(setq server-use-tcp t
server-port 51313
server-auth-key ; 64 chars, saved in ~/.emacs.d/server/server.
"looph8oow3Aph5ahje1eek1aish3Ohthu4Paengae0iketohGhaemi2iek5ae4ee")
:config
(unless (eq (server-running-p) t) ; Run server if not t.
(server-start)))))
{{< / highlight >}}
----
The server expects filenames as input, we can't just feed it the file. The
package https://www.gnu.org/software/tramp/[TRAMP] allow us to use remote file
paths with Emacs with the help of SSH. I have modified
`tramp-password-prompt-regexp` to look for verification code prompts from the
https://github.com/google/google-authenticator-libpam[Google Authenticator PAM
module].
[NOTE]
My modification overwrites the original value of `tramp-password-prompt-regexp`,
which has a bunch of localized variants of “password” in it. You can view the
original value with `C-h v tramp-password-prompt-regexp`.
----
{{< highlight elisp >}}
(use-package tramp
:custom
(tramp-use-ssh-controlmaster-options nil) ; Don't override SSH config.
(tramp-default-method "ssh") ; ssh is faster than scp and supports ports.
(tramp-password-prompt-regexp ; Add verification code support.
(concat
"^.*"
(regexp-opt
'("passphrase" "Passphrase"
"password" "Password"
"Verification code")
t)
".*:\0? *")))
{{< / highlight >}}
----
== SSH
In order to avoid having to enter our password again and again, we can edit our
https://linux.die.net/man/5/ssh_config[SSH configuration] to reuse existing
connections. The following configuration will create an UNIX domain socket per
host and re-use that for all further connections to this host. It will also
forward the Emacs server port, that we picked earlier, to every host we
connect to. We will have to create `~/.ssh/sockets/` before we use the new
configuration.
[WARNING]
These sockets allow for unauthenticated access to every host you are
connected to. While this is very convenient, it is also a *security
risk*. The sockets are only usable by your user and root (file mode 0600).
[WARNING]
Everyone on the remote host can connect to the port you forward. They will still
need the password, but you might not want to do this if you don't trust the
other users.
----
{{< highlight cfg >}}
Host fc??:* fd??:* 192.168.* server1.example.com server2.example.com
# Reuse connections.
ControlMaster auto
# Close socket 600s after after last connection closes.
ControlPersist 600
# Set path for sockets.
ControlPath ~/.ssh/sockets/%r@%h-%p
# Forward Emacs-server port.
RemoteForward 127.0.0.1:51313 127.0.0.1:51313
{{< / highlight >}}
----
== Wrapper for emacsclient
Using file paths in TRAMP notation gets annoying really quick. Thankfully Andy
Skelton created a
https://andy.wordpress.com/2013/01/03/automatic-emacsclient/[wrapper script]; I
extended it with the ability to https://stackoverflow.com/a/16408592[become root
using sudo] and an option to use it with local servers. This file needs to be
distributed to every host that should be able to access the server.
.https://dotfiles.tastytea.de/bin/emacsremote[emacsremote]
----
{{< highlight bash >}}
#!/bin/bash
# Open file on a remote Emacs server.
# https://andy.wordpress.com/2013/01/03/automatic-emacsclient/ with added sudo.
params=()
sudo=0
local=0
for p in "${@}"; do
if [[ "${p}" == "-n" ]]; then
params+=( "${p}" )
elif [[ "${p:0:1}" == "+" ]]; then
params+=( "${p}" )
elif [[ "${p}" == "--sudo" ]]; then
# This only works if the user can access the file.
sudo=1
elif [[ "${p}" == "--local" ]]; then
# Use local server, for use with --sudo.
local=1
else
if [[ $(id -u) -eq 0 || ${sudo} -eq 1 ]]; then
if [[ ${local} -eq 0 ]]; then
params+=( "/ssh:$(hostname -f)|sudo:$(hostname -f):"$(readlink -f $p) )
else
params+=( "/sudo:localhost:"$(readlink -f $p) )
fi
else
params+=( "/ssh:$(hostname -f):"$(readlink -f $p) )
fi
fi
done
emacsclient "${params[@]}"
{{< / highlight >}}
----
== Shell configuration
Now we should set `VISUAL` and `EDITOR` to the wrapper and set up some nice,
short aliases. In my examples I assume we called our wrapper `emacsremote`.
NOTE: I wrote the following code for Zsh, but it should also work for Bash.
----
{{< highlight zsh >}}
# Set preferred editor.
if which emacsclient > /dev/null; then
VISUAL="$(which emacsclient) -a emacs"
if [[ -n "${SSH_CONNECTION}" ]]; then # Logged in via SSH.
if which emacsremote > /dev/null; then
VISUAL="$(which emacsremote)"
fi
elif [[ $(id -u) -eq 0 ]] && which emacsremote > /dev/null; then
# Edit files as root in the Emacs instance run by the current user.
VISUAL="$(which emacsremote) --sudo --local"
fi
elif which emacs > /dev/null; then
VISUAL="$(which emacs)"
elif which vim > /dev/null; then
VISUAL="$(which vim)"
elif which nano > /dev/null; then
VISUAL="$(which nano)"
fi
export VISUAL
export EDITOR="${VISUAL}"
{{< / highlight >}}
----
----
{{< highlight zsh >}}
# If ${VISUAL} contains emacs{client,remote}, return immediately to terminal.
if [[ "${VISUAL}" =~ "emacs(client|remote)" ]]; then
alias e="${VISUAL} -n"
if [[ "${VISUAL}" =~ "emacsremote$" ]]; then
alias se="${VISUAL} -n --sudo"
elif which emacsremote >/dev/null && [[ -z "${SSH_CONNECTION}" ]]; then
# Edit files as root in the Emacs instance run by the current user.
alias se="$(which emacsremote) -n --sudo --local"
fi
else
alias e="${VISUAL}"
alias se="sudo ${VISUAL}"
fi
{{< / highlight >}}
----
To detect SSH connections after using `sudo -i`, we have to tell sudo to
preserve the environment variable `SSH_CONNECTION`.
----
{{< highlight zsh >}}
echo 'Defaults env_keep += "SSH_CONNECTION"' >> /etc/sudoers.d/ssh_vars
{{< / highlight >}}
----