How to publish a static site over NNCP
Published by Beto Dealmeida on
A tutorial on how to use NNCP to publish a static site to a server over an unreliable connection
Let's say you're running a static site generator on your laptop, and you want to push the built website to a machine running a web server. A good tool to do that would be rsync
, a command-line utility that synchronizes files and directories efficiently, sending only files that were modified. You can use rsync
to synchronize a directory between two computers, sending the updates through an encrypted channel via ssh
.
But what if your connection to the web server is unreliable? Maybe the server is down from time to time, maybe the network link is unstable. You could just run rsync
, and keep retrying until you succeed, but there are better ways. One better solution for this problem is Node to Node copy, a small collection of utilities written in Go as an evolution of UUCP.
Last week I set up NNCP as a way of publishing my blog (https://blog.taoetc.org/) to a Raspberry Pi that will run under solar power and a 4G connection. Since the documentation for NNCP is very sparse I decided to write down the process, hoping it can be useful for other people.
Set up
Let's assume you have two machines, client
and server
. The client machine has a directory that is updated frequently, and you want to send those files to the server using NNCP. Let's also assume that your static site generator is smart enough to know which files were modified since the last build.
Installation
The first step is to install NNCP in both machines. I couldn't find packages for Debian stable, so I installed Go and compiled NNCP. If you're going to compile NNCP make sure you have Go version 1.13 or higher; you can verify which version you have by running go version
. The compilation requires a command called redo
, which I didn't have installed. In that case, you can use contrib/do
from the package:
contrib/do all
On the client I installed NNCP in the ~/.local
directory, since I wanted to run the commands as my user:
PREFIX=~/.local contrib/do install
On the server I created an nncp
system account, and installed NNCP to /usr/local
:
PREFIX=/usr/local contrib/do install
Configuration
The next step is to create configuration file, using the command nncp-cfgnew
. For the client:
nncp-cfgnew > ~/.local/etc/nncp.hjson
And for the server:
nncp-cfgnew > /usr/local/etc/nncp.hjson
These configuration files contain public and private keys, so make sure that no one else can read them:
chmod 600 ~/.local/etc/nncp.hjson
chown nncp:nncp /usr/local/etc/nncp.hjson
chmod 600 /usr/local/etc/nncp.hjson
By default the configuration file will use a spool directory and store a log file in /var/spool/nncp
. On the server I made sure to create the directory and give ownership to the system account:
mkdir -p /var/spool/nncp
chown nncp:nncp /var/spool/nncp
On the client I changed the configuration to have the spool and the log file pointing to ~/.local
:
{
# Path to encrypted packets spool directory
spool: /home/user/.local/var/spool/nncp
# Path to log file
log: /home/user/.local/var/spool/nncp/log
And then created the directory:
mkdir -p ~/.local/var/spool/nncp
The next step is modifying the configuration files so that the nodes (computers) are aware of each other.
Creating a friend-to-friend network
NNCP allows us to build a small network of trusted nodes; unlike UUCP there are no anonymous peers in this network, which is why it's called friend-to-friend.
To create the network you need to add the public keys of all neighboring nodes to the configuration file you created in the previous step, under the neigh
section. Initially this section could contain only an entry for a node called self
, representing the node itself. All you need to do is copy these keys to the other nodes. For example, for the client:
neigh: {
# client
self: {
id: WWW
exchpub: XXX
signpub: YYY
noisepub: ZZZ
}
# "self" from server
server: {
id: AAA
exchpub: BBB
signpub: CCC
noisepub: DDD
}
}
And for the server:
neigh: {
# server
self: {
id: AAA
exchpub: BBB
signpub: CCC
noisepub: DDD
}
# "self" from client
client: {
id: WWW
exchpub: XXX
signpub: YYY
noisepub: ZZZ
}
}
With this configuration the nodes can identify each other. But how do they communicate? In addition to the public keys you can also put the address where the node can be found. In this case the client will push files to the server, so you only need to configure the address of the server on the client configuration:
neigh: {
# client
self: {
id: WWW
exchpub: XXX
signpub: YYY
noisepub: ZZZ
}
# "self" from server
server: {
id: AAA
exchpub: BBB
signpub: CCC
noisepub: DDD
addrs: {
internet: server.example.com:5400
}
}
}
Now, client
knows who server
is, and how to talk to it. And server
knows who client
is when receiving a message from it.
Sending a file
OK, now that you have a network, how do you send a file? From the client
machine you simply run:
nncp-file -cfg ~/.local/etc/nncp.hjson file.txt server:
If you run the command above you'll notice that it completes very quickly! The file was not actually sent. Instead, it was queued in the spool at ~/.local/var/spool/nncp
. Inside the spool there should be a directory with the ID of server
("AAA", in this example), and a symlink called "server" pointing to it. Inside this directory there should be a directory called "tx", and inside an encrypted and compressed version of our file.
To actually send the file you need to call the server:
nncp-call -cfg ~/.local/etc/nncp.hjson server
This command will try to connect to server and send all the packets that are in the spool. It will fail, because the server is not listening to any incoming connections yet.
Configuring the server
On the server you need to run a daemon listening for incoming connections:
nncp-daemon -autotoss -bind server.example.com:5400
You also need to change the server configuration, so that the it will allow incoming file requests from the client:
neigh: {
# server
self: {
id: AAA
exchpub: BBB
signpub: CCC
noisepub: DDD
}
# "self" from client
client: {
id: WWW
exchpub: XXX
signpub: YYY
noisepub: ZZZ
incoming: "/path/to/files/from/client"
}
}
Now the client can send file packets to the server. The autotoss
option in the daemon means that these packets will be automatically processed, and moved to the incoming directory.
If you go back to the client and run:
nncp-file -cfg ~/.local/etc/nncp.hjson file.txt server:
nncp-call -cfg ~/.local/etc/nncp.hjson server
The file file.txt
will show up in /path/to/files/from/client/file.txt
... success!
Synchronizing files
With this setup, how do you periodically sync the files from a static site generator to the server? You could simply have the generator call the command on files that were modified, pushing them to a directory that the server would then serve. For example:
nncp-file -cfg ~/.local/etc/nncp.hjson index.html server:my_blog/index.html
nncp-file -cfg ~/.local/etc/nncp.hjson posts/hello_world/index.html server:my_blog/hello_world/index.html
nncp-file -cfg ~/.local/etc/nncp.hjson css/theme.css server:my_blog/css/theme.css
nncp-call -cfg ~/.local/etc/nncp.hjson server
This works, but it has a problem: whenever a file changes and you run the command again, the file will not be overwritten. For example, if index.html
changes, the next time you send it to the server it will be called index.html.0
, to prevent the original one from being overwritten. And there's no configuration flag to allow overwriting files.
Fortunately NNCP also allows you to execute remote commands! On the server we can define a command called "sync", that will use rsync
to copy the pushed files to the website directory:
neigh: {
# server
self: {
id: AAA
exchpub: BBB
signpub: CCC
noisepub: DDD
}
# "self" from client
client: {
id: WWW
exchpub: XXX
signpub: YYY
noisepub: ZZZ
incoming: "/path/to/files/from/client"
exec: {
sync: [
"/usr/bin/rsync",
"-av",
"--remove-source-files",
"/path/to/files/from/client/",
"/var/www/example.com"
]
}
}
}
The rsync command will move any new files that show up in the incoming directory to the root of the website. To execute the command the client needs to run:
echo | nncp-exec -cfg ~/.local/etc/nncp.hjson server sync
The nncp-exec
command reads from stdin, which is why you need to have the echo
command piping into it. With this, your static site generator can now run:
nncp-file -cfg ~/.local/etc/nncp.hjson index.html server:my_blog/index.html
nncp-file -cfg ~/.local/etc/nncp.hjson posts/hello_world/index.html server:my_blog/hello_world/index.html
nncp-file -cfg ~/.local/etc/nncp.hjson css/theme.css server:my_blog/css/theme.css
echo | nncp-exec -cfg ~/.local/etc/nncp.hjson server sync
nncp-call -cfg ~/.local/etc/nncp.hjson server
The commands above will push the files to the server, and invoke rsync
remotely to move the files to the website directory.
Order of execution
There's just one problem with the solution above. NNCP does not guarantee that packets are processed in order. When I tested it on my blog the rsync
call was running before all the files were transferred. There's a clever way to solve the problem, though: NNCP packets can have different priorities. You can send the file packets with a higher priority than the execution (sync) packet, and call for the higher packets to be processed first:
nncp-file -nice PRIORITY -cfg ~/.local/etc/nncp.hjson index.html server:my_blog/index.html
nncp-file -nice PRIORITY -cfg ~/.local/etc/nncp.hjson posts/hello_world/index.html server:my_blog/hello_world/index.html
nncp-file -nice PRIORITY -cfg ~/.local/etc/nncp.hjson css/theme.css server:my_blog/css/theme.css
echo | nncp-exec -nice NORMAL -cfg ~/.local/etc/nncp.hjson server sync
nncp-call -nice PRIORITY -cfg ~/.local/etc/nncp.hjson server
nncp-call -nice NORMAL -cfg ~/.local/etc/nncp.hjson server
The first nncp-call
will process only the file packets, while the second one will process the sync call.
Calling in the background
The nice thing about nncp-file
and nncp-exec
is that they queue commands and terminate quickly. Ideally the static site generator will call these commands to queue the operations, and the client will run nncp-call
in the background periodically, to transfer the files and run the sync whenever the server is up. To do this, you can specify in the client configuration that the server should be called periodically:
neigh: {
# client
self: {
id: WWW
exchpub: XXX
signpub: YYY
noisepub: ZZZ
}
# "self" from server
server: {
id: AAA
exchpub: BBB
signpub: CCC
noisepub: DDD
addrs: {
internet: server.example.com:5400
}
calls: [
{
cron: "0 * * * *"
nice: PRIORITY
}
{
cron: "10 * * * *"
nice: NORMAL
}
]
}
}
The configuration above will make the client call the server every hour to process priority packets, and 10 minutes later call it again to process normal packets. This way there's no need to manually call nncp-call
.