Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add JoinMarket's Jam web interface #700

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ NixOS modules ([src](modules/modules.nix))
* [liquid](https://github.com/elementsproject/elements): federated sidechain
* [JoinMarket](https://github.com/joinmarket-org/joinmarket-clientserver)
* [JoinMarket Orderbook Watcher](https://github.com/JoinMarket-Org/joinmarket-clientserver/blob/master/docs/orderbook.md)
* [Jam](https://github.com/joinmarket-webui/jam): simplified user-friendly JoinMarket web interface
* [bitcoin-core-hwi](https://github.com/bitcoin-core/HWI)
* Helper
* [netns-isolation](modules/netns-isolation.nix): isolates applications on the network-level via network namespaces
Expand Down
10 changes: 10 additions & 0 deletions docs/services.md
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,16 @@ See [here](https://github.com/JoinMarket-Org/joinmarket-clientserver/blob/master

3. Profit

## Run the Jam, the JoinMarket web interface

The [Jam](https://github.com/joinmarket-webui/jam) is a static web interface
which is served by nginx. It connects to the jmwalled daemon which gets started
in the background.

TODO
- How to access the web interface using e.g. ssh proxy: `ssh -L 61851:localhost:61851 root@<nix-bitcoin-node-host>`
- how to access interface over onion?

# clightning

## Plugins
Expand Down
7 changes: 7 additions & 0 deletions examples/configuration.nix
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,13 @@
#
# Set this to enable the JoinMarket order book watcher.
# services.joinmarket-ob-watcher.enable = true;
#
# Set this to enable Jam, the JoinMarket web interface.
# services.joinmarket-jam.enable = true;
#
# Set this to create an onion service to make the Jam web interface
# available via Tor:
# nix-bitcoin.onionServices.joinmarket-jam.enable = true;

### Nodeinfo
# Set this to add command `nodeinfo` to the system environment.
Expand Down
180 changes: 180 additions & 0 deletions modules/joinmarket-jam.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
{ config, lib, pkgs, ... }:

with lib;
let
options.services.joinmarket-jam = {
enable = mkEnableOption "Enable the JoinMarket Jam web interface.";
address = mkOption {
type = types.str;
default = "127.0.0.1";
description = mdDoc "HTTP server address.";
};
port = mkOption {
type = types.port;
default = 61851;
description = mdDoc "HTTP server port.";
};
staticContentRoot = mkOption {
type = types.path;
default = nbPkgs.joinmarket-jam;
defaultText = "config.nix-bitcoin.pkgs.joinmarket-jam";
description = mdDoc "Path of the static content root.";
};
nginxConfig = mkOption {
readOnly = true;
default = nginxConfig;
defaultText = "(See source)";
description = mdDoc ''
An attrset of nginx config snippets for assembling a custom
joinmarket's jam nginx config.
'';
};
package = mkOption {
type = types.package;
default = nbPkgs.joinmarket-jam;
defaultText = "config.nix-bitcoin.pkgs.joinmarket-jam";
description = mdDoc "The package providing joinmarket's jam files.";
};
user = mkOption {
type = types.str;
default = "joinmarket-jam";
description = mdDoc "The user as which to run Jam.";
};
group = mkOption {
type = types.str;
default = cfg.user;
description = mdDoc "The group as which to run Jam.";
};
tor.enforce = nbLib.tor.enforce;

#settings = mkOption {
#};
};

cfg = config.services.joinmarket-jam;
nbLib = config.nix-bitcoin.lib;
nbPkgs = config.nix-bitcoin.pkgs;

inherit (config.services) joinmarket-ob-watcher joinmarket-jmwalletd;

# Nginx configuration is highgly inspired by official jam-docker ui-only container.
# https://github.com/joinmarket-webui/jam-docker/tree/master/ui-only/nginx
nginxConfig = {
staticContent = ''
index index.html;

add_header Cache-Control "public, no-transform";
add_header Vary Accept-Language;
add_header Vary Cookie;
'';

proxyApi = let
jmwalletd_api_backend = "https://${nbLib.addressWithPort joinmarket-jmwalletd.address joinmarket-jmwalletd.port}";
jmwalletd_wss_backend = "https://${nbLib.addressWithPort joinmarket-jmwalletd.address joinmarket-jmwalletd.wssPort}/";
ob_watcher_backend = "http://${nbLib.addressWithPort joinmarket-ob-watcher.address joinmarket-ob-watcher.port}";
in ''
location / {
#include /etc/nginx/snippets/proxy-params.conf;

try_files $uri $uri/ /index.html;
add_header Cache-Control no-cache;
}
location /api/ {
proxy_pass ${jmwalletd_api_backend};

#include /etc/nginx/snippets/proxy-params.conf;

proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Authorization $http_x_jm_authorization;
proxy_set_header x-jm-authorization "";
proxy_read_timeout 300s;
proxy_connect_timeout 300s;
}
location = /jmws {
proxy_pass ${jmwalletd_wss_backend};

#include /etc/nginx/snippets/proxy-params.conf;

proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Authorization "";

# allow 10m without socket activity (default is 60 sec)
proxy_read_timeout 600s;
proxy_send_timeout 600s;
}
location /obwatch/ {
proxy_pass ${ob_watcher_backend};

#include /etc/nginx/snippets/proxy-params.conf;

proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_read_timeout 300s;
proxy_connect_timeout 300s;
}
location = /jam/internal/auth {
internal;
proxy_pass http://$server_addr:$server_port/api/v1/session;

#if ($jm_auth_present != 1) {
# return 401;
#}

#include /etc/nginx/snippets/proxy-params.conf;

proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_pass_request_body off;
proxy_set_header Content-Length "";
}
location = /jam/api/v0/features {
auth_request /jam/internal/auth;
default_type application/json;
return 200 '{ "features": { "logs": false } }';
}
location /jam/api/v0/log/ {
auth_request /jam/internal/auth;
return 501; # Not Implemented
}
'';
};

in {
inherit options;

config = mkIf cfg.enable {
services = {
joinmarket-ob-watcher.enable = true;
joinmarket-jmwalletd.enable = true;

nginx = {
enable = true;
enableReload = true;
recommendedBrotliSettings = true;
recommendedGzipSettings = true;
recommendedOptimisation = true;
recommendedProxySettings = true;
recommendedTlsSettings = true;
# TODO: Use this to define "map"? See: https://github.com/joinmarket-webui/jam-docker/blob/master/ui-only/nginx/templates/default.conf.template#L20
#commonHttpConfig = nginxConfig.httpConfig;
virtualHosts."joinmarket-jam" = {
serverName = "_";
listen = [ { addr = cfg.address; port = cfg.port; } ];
root = cfg.staticContentRoot;
extraConfig = nginxConfig.staticContent + nginxConfig.proxyApi;
};
};
};

users = {
users.${cfg.user} = {
isSystemUser = true;
group = cfg.group;
};
groups.${cfg.group} = {};
};
};
}
163 changes: 163 additions & 0 deletions modules/joinmarket-jmwalletd.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
{ config, lib, pkgs, ... }:

with lib;
let
options.services.joinmarket-jmwalletd = {
enable = mkEnableOption "JoinMarket jmwalletd";

# Unfortunately it's not possible to set the listening address for
# jmwalletd. It's used only internally.
address = mkOption {
type = types.str;
readOnly = true;
internal = true;
default = "127.0.0.1";
description = mdDoc ''
The address where the jmwalletd listens to.
'';
};
port = mkOption {
type = types.port;
default = 28183;
description = mdDoc "The port over which to serve RPC.";
};
wssPort = mkOption {
type = types.port;
default = 28283;
description = mdDoc "The port over which to serve websocket subscriptions.";
};
extraArgs = mkOption {
type = types.separatedString " ";
default = "";
description = mdDoc "Extra coomand line arguments passed to jmwalletd.";
};
user = mkOption {
type = types.str;
default = "joinmarket-jmwalletd";
description = mdDoc "The user as which to run JoinMarket jmwalletd.";
};
group = mkOption {
type = types.str;
default = cfg.user;
description = mdDoc "The group as which to run JoinMarket jmwalletd.";
};
dataDir = mkOption {
readOnly = true;
type = types.path;
default = config.services.joinmarket.dataDir;
description = mdDoc "The JoinMarket data directory.";
};
sslDir = mkOption {
readOnly = true;
type = types.path;
default = "${cfg.dataDir}/ssl";
description = mdDoc "The SSL directory for jmwalled.";
};
certPath = mkOption {
readOnly = true;
default = "${secretsDir}/joinmarket-jmwalletd";
description = mdDoc "JoinMarket jmwalletd TLS certificate path.";
};
recoverSync = mkOption {
type = types.bool;
default = false;
description = mdDoc ''
Choose to do detailed wallet sync, used for recovering on new Core
instance.
'';
};
certificate = {
extraIPs = mkOption {
type = with types; listOf str;
default = [];
example = [ "60.100.0.1" ];
description = mdDoc ''
Extra `subjectAltName` IPs added to the certificate.
'';
};
extraDomains = mkOption {
type = with types; listOf str;
default = [];
example = [ "example.com" ];
description = mdDoc ''
Extra `subjectAltName` domain names added to the certificate.
'';
};
};
};

cfg = config.services.joinmarket-jmwalletd;
nbLib = config.nix-bitcoin.lib;
nbPkgs = config.nix-bitcoin.pkgs;
secretsDir = config.nix-bitcoin.secretsDir;
in {
inherit options;

config = mkIf cfg.enable (mkMerge [{
services.joinmarket.enable = true;

users = {
users.${cfg.user} = {
isSystemUser = true;
group = cfg.group;
home = cfg.dataDir;
# Allow access to the joinmarket dataDir.
extraGroups = [ config.services.joinmarket.group ];
};
groups.${cfg.group} = {};
};

nix-bitcoin = {
secrets.joinmarket-jmwalletd-password.user = cfg.user;
generateSecretsCmds.joinmarket-jmwalletd-password = ''
makePasswordSecret joinmarket-jmwalletd-password
'';
};
}

(mkIf cfg.enable {
nix-bitcoin = {
secrets = {
joinmarket-jmwalletd-cert.user = cfg.user;
joinmarket-jmwalletd-key.user = cfg.user;
};
generateSecretsCmds.joinmarket-jmwalletd = ''
makeCert joinmarket-jmwalletd '${nbLib.mkCertExtraAltNames cfg.certificate}'
'';
};

systemd.tmpfiles.rules = [
"d '${cfg.sslDir}' 0770 ${cfg.user} ${cfg.group} - -"
];

systemd.services.joinmarket-jmwalletd = {
wantedBy = [ "joinmarket.service" ];
requires = [ "joinmarket.service" ];
after = [ "joinmarket.service" "nix-bitcoin-secrets.target" ];
preStart = ''
# Copy the certificates into a data directory under the `ssl` dir
mkdir -p '${cfg.sslDir}'
install -m600 '${cfg.certPath}-cert' '${cfg.sslDir}/cert.pem'
install -m600 '${cfg.certPath}-key' '${cfg.sslDir}/key.pem'
'';
serviceConfig = nbLib.defaultHardening // {
WorkingDirectory = cfg.dataDir;
User = cfg.user;
ExecStart = ''
${config.nix-bitcoin.pkgs.joinmarket}/bin/jm-jmwalletd \
--port='${toString cfg.port}' \
--wss-port='${toString cfg.wssPort}' \
--datadir='${cfg.dataDir}' \
${optionalString (cfg.recoverSync) "--recoversync \\"}
${cfg.extraArgs}
'';
SyslogIdentifier = "joinmarket-jmwalletd";
ReadWritePaths = [ cfg.dataDir ];
Restart = "on-failure";
RestartSec = "10s";
MemoryDenyWriteExecute = false;
} // nbLib.allowTor;
};
})
]);
}
2 changes: 2 additions & 0 deletions modules/modules.nix
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
./btcpayserver.nix
./joinmarket.nix
./joinmarket-ob-watcher.nix
./joinmarket-jmwalletd.nix
./joinmarket-jam.nix
./hardware-wallets.nix

# Support features
Expand Down
Loading