I'm a Python, Linux, Nix/NixOS, JavaScript, Rust, ... basically everything open-source enthusiast.
Too many Nix pills, I am addicted

In this blog post you are going to see how to make simple determenistic application with the power of Nix.

First of all, no offence to Luca Bruno aka Lethalman - the inventor of Nix pills

Second of all, do not worry, Nix pills are not actual pills.

There was a joke a month back … about an app that shows system usage of remote computer in your system tray or something like that… well I made it. Khm… apperantly all my apps start with a joke.

Lets start with usage

Get the code from github:

git clone git://github.com/matejc/cgisysinfo.git
cd cgisysinfo

This will build the app to Nix store:

nix-build --argstr prefix `pwd` \
    --argstr listenAddress "0.0.0.0" \
    --argstr listenPort "8080" \
    --argstr user "matejc" \
    --argstr password "mypassword"

To run the app, just execute in current folder:

./result/bin/cgisysinfo-run

To use it on remote machine you have to forward

1
8080/tcp
port (from example).

To test if it is working on remote machine use following command:

notify-send "`curl --user matejc:<your-password> -k https://<yourip>:8080/`"

Or just open in browser the following page:

1
https://<yourip>:8080/
, the browser should complain about unsecure connection but this is just because the cert is self-signed. After that you will have to enter username and password.

You could try also other scripts .. like

1
https://<yourip>:8080/hello.pl
or
1
https://<yourip>:8080/hello.py
.

Code

The program is in one short file, lets go through it:

At the start we declare parameters and its default values:

  1. pkgs: the root of all packages
  2. prefix: where the logs, certs, unix socket, pid files lives
  3. listenAddress: address, where the Nginx will listen
  4. listenPort: port on which Nginx will listen
  5. user: username for simpleauth, used when accessing cgi scripts with curl or something
  6. password: password for simpleauth
  7. templatesFile: the file where you will have templates.nix
  8. extraNginxConf: if you want to add extra Nginx configuration in the
    1
    
    server
    
    block
1
2
3
4
5
6
7
8
9
{ pkgs ? import <nixpkgs> {}
, prefix ? "/var/lib/cgisysinfo"
, listenAddress ? "localhost"
, listenPort ? "9999"
, user ? "user"
, password ? "password"
, templatesFile ? "${prefix}/templates.nix"
, extraNginxConf ? "" }:
let

Now we have to create a

1
nginx.conf
- the web server configuration with wich we are going serve cgi scripts.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
  nginxconf = pkgs.writeText "nginx.conf" ''
  pid ${prefix}/nginx.pid;
  worker_processes 1;
  events {
    worker_connections 128;
  }
  http {
    server {
      access_log ${prefix}/cgi.access.log;
      error_log ${prefix}/cgi.error.log;

      ssl on;
      ssl_certificate     ${prefix}/ssl/selfsigned.crt;
      ssl_certificate_key ${prefix}/ssl/selfsigned.key;

      root ${scripts}/www;
      index index.sh index.pl index.py;
      listen ${listenAddress}:${listenPort};

      location ~ .(py|pl|sh)$ {
        expires -1;

        auth_basic "closed site";
        auth_basic_user_file ${htpasswd};

        gzip           off;
        fastcgi_pass   unix:${prefix}/fcgiwrap.socket;

        # include      fastcgi_params;
        fastcgi_param  QUERY_STRING       $query_string;
        fastcgi_param  REQUEST_METHOD     $request_method;
        fastcgi_param  CONTENT_TYPE       $content_type;
        fastcgi_param  CONTENT_LENGTH     $content_length;

        fastcgi_param  SCRIPT_FILENAME    $document_root$fastcgi_script_name;
        fastcgi_param  SCRIPT_NAME        $fastcgi_script_name;
        fastcgi_param  REQUEST_URI        $request_uri;
        fastcgi_param  DOCUMENT_URI       $document_uri;
        fastcgi_param  DOCUMENT_ROOT      $document_root;
        fastcgi_param  SERVER_PROTOCOL    $server_protocol;

        fastcgi_param  GATEWAY_INTERFACE  CGI/1.1;
        fastcgi_param  SERVER_SOFTWARE    nginx/$nginx_version;

        fastcgi_param  REMOTE_ADDR        $remote_addr;
        fastcgi_param  REMOTE_PORT        $remote_port;
        fastcgi_param  SERVER_ADDR        $server_addr;
        fastcgi_param  SERVER_PORT        $server_port;
        fastcgi_param  SERVER_NAME        $server_name;
      }

      ${extraNginxConf}
    }
  }
  '';

Set variable

1
socketPath
for
1
fcgiwrap.socket
and create file
1
htpasswd
for simple authentication for Nginx (when built, password will be hashed - there will be no plain text password in store).

1
2
3
4
5
6
7
8
9
10
  socketPath = "${prefix}/fcgiwrap.socket";

  htpasswd = pkgs.stdenv.mkDerivation {
    name = "${user}-htpasswd";
    phases = "installPhase";
    installPhase = ''
      export PATH="${pkgs.openssl}/bin:$PATH"
      printf "${user}:$(openssl passwd -crypt ${password})\n" >> $out
    '';
  };

Here comes a bit of magic, we take

1
templatesFile
which we are going to see later and make python/perl/bash scripts from it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  scripts =
    let
      templates = import templatesFile {inherit pkgs prefix;};
      paths = map (template:
        pkgs.writeTextFile rec {
          name = "cgisysinfo-${template.name}";
          text = template.text;
          executable = true;
          destination = "/www/${template.name}";
        }) templates;
    in
      pkgs.buildEnv {
        name = "cgisysinfo-scripts";
        inherit paths;
        pathsToLink = [ "/www" ];
      };

Main run script takes care of starting

1
fcgiwrap
and
1
Nginx
.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
  run = pkgs.writeScriptBin "cgisysinfo-run" ''
  #!${pkgs.bash}/bin/bash

  # we send QUIT signal to Nginx when you press Ctrl+C
  function stopall() {
    kill -QUIT $( cat ${prefix}/nginx.pid )
    exit
  }
  trap "stopall" INT

  export PATH="${pkgs.fcgiwrap}/sbin:${pkgs.nginx}/bin:${pkgs.openssl}/bin:$PATH"

  # generate self-signed cert for nginx (if folder 'ssl' does not exist yet)
  test -d ${prefix}/ssl || \
    { mkdir -p ${prefix}/ssl && \
    openssl req -new -x509 -nodes -keyout ${prefix}/ssl/selfsigned.key -out ${prefix}/ssl/selfsigned.crt -subj "/C=GB/ST=London/L=London/O=Global Security/OU=IT Department/CN=example.xyz"; }

  test -S ${socketPath} && unlink ${socketPath}

  echo -e "\nExample usage:"
  echo "$ notify-send \"\`curl --user ${user}:<your-password> -k https://${listenAddress}:${listenPort}/\`\""
  echo -e "\nPress Ctrl+C to stop ..."

  # start nginx as daemon
  mkdir -p ${prefix}/var/logs
  nginx -c ${nginxconf} -p ${prefix}/var

  # start fcgiwrap (this one blocks)
  fcgiwrap -c 1 -s unix:${socketPath}
  '';

At the end we install

1
cgisysinfo-run
command to Nix store. I also added a
1
shellHook
to run app with
1
nix-shell
.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  cgisysinfo = pkgs.stdenv.mkDerivation rec {
    name = "cgisysinfo-${version}";
    version = "0.2";
    unpackPhase = "true";
    installPhase = ''
      mkdir -p $out/bin
      ln -s ${run}/bin/* $out/bin
    '';
    shellHook = ''
      ${run}/bin/cgisysinfo-run
    '';
  };

in cgisysinfo

1
templatesFile

This is an example of

1
templates.nix
which is also located on github as example. Currently only
1
.py
,
1
.pl
and
1
.sh
files are supported.

Ok lets make one thing clear at this point, for security reasons, be very carefull what information and how you expose on the internet, this is literally remote code execution app.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
{ pkgs, prefix }:
[

  {
    name = "index.sh";
    text = ''
      #!${pkgs.bash}/bin/bash

      export PATH="$PATH:${pkgs.procps}/bin:${pkgs.sysstat}${pkgs.sysstat}/bin"
      echo -e "Content-type: text/plain\n\n"

      echo RAM: `free -mh | awk 'NR==2{ print $3"/"$2 }'`
      echo Swap: `free -mh | awk 'NR==3{ print $3"/"$2 }'`
      ps -eo pcpu,pmem,user,args | sort -k 1 -r | awk 'NR>1 && NR<5{n=split($4,a,"/"); print a[n]": cpu:"$1"%, mem:"$2"%, u:"$3}'
      echo
    '';
  }

  {
    name = "hello.pl";
    text = ''
      #!${pkgs.perl}/bin/perl

      print "Content-type: text/html\n\n";
      print "<html><body>Hello, Perl world.</body></html>";
    '';
  }

  {
    name = "hello.py";
    text = ''
      #!${pkgs.python27}/bin/python

      print "Content-type: text/html\n\n";
      print "<html><body>Hello, Python world.</body></html>";
    '';
  }

]

You have seen how easy is to make a Nix application. This is it. Happy hacking!