Skip to content
Snippets Groups Projects
test_privatestorage.py 9.52 KiB
Newer Older
import hmac
from shlex import quote
from time import time

def runOnNode(node, argvs):
    """
    Run a shell command on one of the nodes.  The first argument is the name
    of the node.  The second is a list of the argv to run.

    The program's output is piped to systemd-cat and the python fragment
    evaluates to success if the command exits with a success status.
    """
    for argv in argvs:
        try:
            node.succeed('set -eo pipefail; {} | systemd-cat'.format(" ".join(map(quote, argv))))
        except Exception as e:
            code, output = node.execute('cat /tmp/stdout /tmp/stderr')
            node.log(output)
            raise
Florian Sesser's avatar
Florian Sesser committed
def ssh(username, sshPrivateKeyFile, hostname):
    """
    Generate a command which can be used with runOnNode to ssh to the given
    host.
    """
    return [
        ["cp", sshPrivateKeyFile, "/tmp/ssh_key"],
        ["chmod", "0400", "/tmp/ssh_key"],
        ["ssh", "-oStrictHostKeyChecking=no", "-i", "/tmp/ssh_key",
         "{username}@{hostname}".format(username=username, hostname=hostname), ":"],
    ]

def checkout_session_completed(voucher: str) -> str:
    """
    Return a request body string which represents the payment completed event
    for the given voucher.
    """
    return """\
{
  "id": "evt_1LxcsdBHXBAMm9bPSq6UWAZe",
  "object": "event",
  "api_version": "2019-11-05",
  "created": 1666903247,
  "data": {
    "object": {
      "id": "cs_test_a1kWLWGoXZPa6ywyVnuib8DPA3BqXCWZX5UEjLfKh7gLjdZy2LD3F5mEp3",
      "object": "checkout.session",
      "after_expiration": null,
      "allow_promotion_codes": null,
      "amount_subtotal": 3000,
      "amount_total": 3000,
      "automatic_tax": {
        "enabled": false,
        "status": null
      },
      "billing_address_collection": null,
      "cancel_url": "https://httpbin.org/post",
      "client_reference_id": "%(voucher)s",
      "consent": null,
      "consent_collection": null,
      "created": 1666903243,
      "currency": "usd",
      "customer": "cus_Mh0u62xtelUehD",
      "customer_creation": "always",
      "customer_details": {
        "address": {
          "city": null,
          "country": null,
          "line1": null,
          "line2": null,
          "postal_code": null,
          "state": null
        },
        "email": "stripe@example.com",
        "name": null,
        "phone": null,
        "tax_exempt": "none",
        "tax_ids": [

        ]
      },
      "customer_email": null,
      "display_items": [
        {
          "amount": 1500,
          "currency": "usd",
          "custom": {
            "description": "comfortable cotton t-shirt",
            "images": null,
            "name": "t-shirt"
          },
          "quantity": 2,
          "type": "custom"
        }
      ],
      "expires_at": 1666989643,
      "livemode": false,
      "locale": null,
      "metadata": {
      },
      "mode": "payment",
      "payment_intent": "pi_3LxcsZBHXBAMm9bP1daBGoPV",
      "payment_link": null,
      "payment_method_collection": "always",
      "payment_method_options": {
      },
      "payment_method_types": [
        "card"
      ],
      "payment_status": "paid",
      "phone_number_collection": {
        "enabled": false
      },
      "recovered_from": null,
      "setup_intent": null,
      "shipping": null,
      "shipping_address_collection": null,
      "shipping_options": [

      ],
      "shipping_rate": null,
      "status": "complete",
      "submit_type": null,
      "subscription": null,
      "success_url": "https://httpbin.org/post",
      "total_details": {
        "amount_discount": 0,
        "amount_shipping": 0,
        "amount_tax": 0
      },
      "url": null
    }
  },
  "livemode": false,
  "pending_webhooks": 2,
  "request": {
    "id": null,
    "idempotency_key": null
  },
  "type": "checkout.session.completed"
}
""" % dict(voucher=voucher)

def stripe_signature(key: str, body: str) -> str:
    """
    Construct a valid value for the ``Stripe-Signature`` header item.
    """
    timestamp = int(time())
    v1 = hmac.new(key.encode("utf-8"), f"{timestamp}.{body}".encode("utf-8"), "sha256").hexdigest()
    return f"t={timestamp},v1={v1}"

def pay_for_voucher(url: str, webhook_secret, voucher: str) -> list[str]:
    """
    Return a command to run to report to the issuer that payment for the given
    voucher has been received.
    """
    body = checkout_session_completed(voucher)
    return [
        "curl",
        "-X", "POST",
        "--header", "content-type: application/json; charset=utf-8",
        "--header", f"stripe-signature: {stripe_signature(webhook_secret, body)}",
        "--data-binary", body,
        url + "v1/stripe/webhook",
Florian Sesser's avatar
Florian Sesser committed
        sshPrivateKeyFile,
        pemFile,
        run_introducer,
        run_client,
        get_passes,
        exercise_storage,
        introducerPort,
        introducerFURL,
        issuerURL,
        ristrettoPublicKey,
):
    """
    """
    # Boot the VMs.  We used to do them all in parallel but the boot
    # sequence got flaky at some point for some reason I don't
    # understand. :/ It might be related to this:
    #
    # https://discourse.nixos.org/t/nixos-ppc64le-vm-does-not-have-dev-vda-device/11548/9
    #
    # See <nixpkgs/nixos/modules/virtualisation/qemu-vm.nix> for the Nix
    # that constructs the QEMU command that gets run.
    #
    # Boot them one at a time for now.
    issuer.connect()
    introducer.connect()
    storage.connect()
    client.connect()
    api_stripe_com.connect()

    # The issuer and the storage server should accept SSH connections.  This
    # doesn't prove it is so but if it fails it's a pretty good indication
    # it isn't so.
    storage.wait_for_open_port(22)
Florian Sesser's avatar
Florian Sesser committed
    runOnNode(issuer, ssh("probeuser", sshPrivateKeyFile, "storage"))
    runOnNode(issuer, ssh("root", sshPrivateKeyFile, "storage"))
Florian Sesser's avatar
Florian Sesser committed
    runOnNode(storage, ssh("probeuser", sshPrivateKeyFile, "issuer"))
    runOnNode(storage, ssh("root", sshPrivateKeyFile, "issuer"))

    # Set up a Tahoe-LAFS introducer.
    introducer.copy_from_host(pemFile, '/tmp/node.pem')
    runOnNode(introducer, [[run_introducer, "/tmp/node.pem", str(introducerPort), introducerFURL]])

    #
    # Get a Tahoe-LAFS storage server up.
    #
    code, version = storage.execute('tahoe --version')
    storage.log(version)

    # The systemd unit should reach the running state.
    storage.wait_for_unit('tahoe.storage.service')

    # Some while after that the Tahoe-LAFS node should listen on the web API
    # port. The port number here has to agree with the port number set in
    # the private-storage.nix module.
    storage.wait_for_open_port(3456)

    # Once the web API is listening it should be possible to scrape some
    # status from the node if it is really working.
    storage.succeed('tahoe -d /var/db/tahoe-lafs/storage status')

    # It should have Eliot logging turned on as well.
    storage.succeed('[ -e /var/db/tahoe-lafs/storage/logs/eliot.json ]')

    # Make sure the issuer is ready to accept connections.
    issuer.wait_for_open_port(80)

    # Pretend to be Stripe and report that our voucher has been paid for.
    runOnNode(issuer, [pay_for_voucher("http://localhost/", stripeWebhookSecretKey, voucher)])

    #
    # Storage appears to be working so try to get a client to speak with it.
    #
    runOnNode(client, [[run_client, "/tmp/client", introducerFURL, issuerURL, ristrettoPublicKey, str(tokenCount)]])
    client.wait_for_open_port(3456)

    # Make sure the fake Stripe API server is ready for requests.
    try:
        api_stripe_com.wait_for_open_port(80)
    except:
        code, output = api_stripe_com.execute('journalctl -u api.stripe.com')
        api_stripe_com.log(output)
        raise

    # Get some ZKAPs from the issuer.
    try:
            get_passes,
            "http://127.0.0.1:3456",
            "/tmp/client/private/api_auth_token",
            voucher,
    except:
        # Dump the fake Stripe API server logs, too, since the error may arise
        # from a PaymentServer/Stripe interaction.
        for node, unit in [(api_stripe_com, "api.stripe.com"), (issuer, "zkapissuer")]:
            code, output = node.execute(f'journalctl -u {unit}')
            node.log(output)

        raise

    # The client should be prepped now.  Make it try to use some storage.
    runOnNode(client, [[exercise_storage, "/tmp/client"]])

    # It should be possible to restart the storage service without the
    # storage node fURL changing.
    furlfile = '/var/db/tahoe-lafs/storage/private/storage-plugin.privatestorageio-zkapauthz-v2.furl'
    before = storage.execute('cat ' + furlfile)
    runOnNode(storage, [["systemctl", "restart", "tahoe.storage"]])
    after = storage.execute('cat ' + furlfile)
    if (before != after):
        raise Exception('fURL changes after storage node restart')

    # The client should actually still work, too.
    runOnNode(client, [[exercise_storage, "/tmp/client"]])

    # The issuer metrics should be accessible from the monitoring network.
    issuer.execute('ifconfig lo:fauxvpn 172.23.23.2/24')
    issuer.wait_until_succeeds("nc -z 172.23.23.2 80")
    issuer.succeed('curl --silent --insecure --fail --output /dev/null http://172.23.23.2/metrics')
    # The issuer metrics should NOT be accessible from any other network.
    issuer.fail('curl --silent --insecure --fail --output /dev/null http://localhost/metrics')
    client.fail('curl --silent --insecure --fail --output /dev/null http://issuer/metrics')
    issuer.execute('ifconfig lo:fauxvpn down')