diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..277f336 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,38 @@ +FROM starefossen/ruby-node:2-8 + +RUN mkdir /matreon +WORKDIR /matreon + +RUN apt-get update -qq && apt-get install -y build-essential libpq-dev --no-install-recommends && rm -rf /var/lib/apt/lists/* + +ENV RAILS_ENV production +ENV RAILS_SERVE_STATIC_FILES true +ENV RAILS_LOG_TO_STDOUT true + +COPY Gemfile /matreon/ +COPY Gemfile.lock /matreon/ +RUN bundle config --global frozen 1 +RUN bundle install --without development test + +ENV NODE_ENV production + +COPY package.json /matreon/package.json +COPY yarn.lock /matreon/yarn.lock +RUN yarn install + +COPY Procfile Rakefile config.ru .babelrc .postcssrc.yml /matreon/ +COPY config /matreon/config +COPY db /matreon/db +COPY vendor /matreon/vendor +COPY app /matreon/app +COPY bin /matreon/bin +COPY lib /matreon/lib + +COPY public /matreon/public +RUN rm -rf public/assets public/packs + +ENV DEVISE_SECRET_KEY ${DEVISE_SECRET_KEY} +RUN bundle exec rake assets:precompile + +EXPOSE 3000 +CMD bundle exec puma -C config/puma.rb diff --git a/Gemfile b/Gemfile index b338ffd..cd71dd3 100644 --- a/Gemfile +++ b/Gemfile @@ -15,7 +15,9 @@ gem 'react_on_rails', '10.1.3' # Use Puma as the app server gem 'puma', '~> 3.11' -# Use SCSS for stylesheets + +# Proxy Lightning Charge: +gem 'rack-proxy' gem 'secure_headers', '~> 6.0.0.alpha02' diff --git a/Gemfile.lock b/Gemfile.lock index 0d17a68..b166ae1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -372,6 +372,7 @@ DEPENDENCIES mini_racer pg puma (~> 3.11) + rack-proxy rack-timeout rails (~> 5.1.6) rails-controller-testing diff --git a/Matreon.Template b/Matreon.Template new file mode 100644 index 0000000..d48432d --- /dev/null +++ b/Matreon.Template @@ -0,0 +1,414 @@ +{ + "AWSTemplateFormatVersion" : "2010-09-09", + + "Description" : "Creates a single EC2 instance with a pruned Bitcoin Core node, C-Lightning, Lightning Charge, Postgres, Ruby on Rails and Matreon.", + + "Metadata" : { + "AWS::CloudFormation::Interface" : { + "ParameterGroups" : [ + { + "Label" : { "default" : "Server" }, + "Parameters" : [ "InstanceType", "KeyName", "SSHLocation" , "BugsEmail" ] + }, + { + "Label" : { "default":"Bitcoin" }, + "Parameters" : [ "Network" ] + }, + { + "Label" : { "default":"Matreon" }, + "Parameters" : [ "HostName", "FromEmail", "SmtpHost", "SmtpUser", "SmtpPassword", "SmtpPort" ] + }, + { + "Label" : { "default":"Podcast" }, + "Parameters" : [ "Podcast", "PodcastTitle", "PodcastImage", "PodcastUrl" ] + } + ] + }, + }, + + "Parameters" : { + + "KeyName": { + "Description" : "Name of an existing EC2 KeyPair to enable SSH access to the instances", + "Type": "AWS::EC2::KeyPair::KeyName", + "ConstraintDescription" : "must be the name of an existing EC2 KeyPair." + }, + + "Network" : { + "Description" : "Real Bitcoin or testnet", + "Type" : "String", + "Default" : "testnet", + "AllowedValues" : [ "testnet", "bitcoin"] + }, + + "InstanceType" : { + "Description" : "WebServer EC2 instance type. Unless you are famous t2.small should suffice", + "Type" : "String", + "Default" : "t2.small", + "AllowedValues" : [ "t2.small", "t2.medium", "t2.large", "t2.large", "t2.xlarge", "t2.2xlarge"], + "ConstraintDescription" : "must be a valid EC2 instance type." + }, + + "SSHLocation" : { + "Description" : "The IP address range that can be used to SSH to the EC2 instances", + "Type": "String", + "MinLength": "9", + "MaxLength": "18", + "Default": "0.0.0.0/0", + "AllowedPattern": "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})", + "ConstraintDescription": "must be a valid IP CIDR range of the form x.x.x.x/x." + }, + + "HostName": { + "Default": "http://example.com", + "Description" : "Hostname, no trailing slash", + "Type": "String", + "MinLength": "5", + "MaxLength": "100" + }, + + "FromEmail": { + "Default": "you@example.com", + "Description" : "From email address", + "Type": "String", + "MinLength": "5", + "MaxLength": "100" + }, + + "BugsEmail": { + "Default": "bugs@example.com", + "Description" : "Bug report email address", + "Type": "String", + "MinLength": "5", + "MaxLength": "100" + }, + + "SmtpHost": { + "Default": "smtp.fastmail.com", + "Description" : "SMTP server", + "Type": "String", + "MinLength": "5", + "MaxLength": "100" + }, + + "SmtpPort": { + "Default": "587", + "Description" : "SMTP port", + "Type": "String", + "MinLength": "1", + "MaxLength": "5" + }, + + "SmtpUser": { + "Default": "you@example.com", + "Description" : "SMTP username", + "Type": "String", + "MinLength": "5", + "MaxLength": "100" + }, + + "SmtpPassword": { + "Default": "", + "Description" : "SMTP password", + "Type": "String", + "NoEcho" : "true", + "MinLength": "0", + "MaxLength": "100" + }, + + "Podcast" : { + "Description" : "Enable podcast feature. Currently requires an existing RSS source.", + "Type" : "String", + "Default" : "0", + "AllowedValues" : [ "0", "1"] + }, + + "PodcastTitle" : { + "Description" : "Podcast title", + "Type": "String", + "MinLength": "0", + "MaxLength": "100" + }, + + "PodcastImage" : { + "Description" : "Podcast image URL", + "Type": "String", + "MinLength": "0", + "MaxLength": "255" + }, + + "PodcastUrl" : { + "Description" : "Existing podcast RSS feed", + "Type": "String", + "MinLength": "0", + "MaxLength": "255" + } + + }, + + "Resources" : { + + "WebServer": { + "Type": "AWS::EC2::Instance", + "Metadata" : { + "AWS::CloudFormation::Init" : { + "configSets" : { + "full_install" : [ + "install_cfn", + "install_docker", + "prepare_datadir", + "install_container_bitcoin", + "install_container_lightning", + "install_container_charge", + "install_docker_compose_with_pg_and_rails", + "initial_blockchain_download", + "launch_matreon" + ] + }, + + "install_cfn" : { + "files" : { + "/etc/cfn/cfn-hup.conf" : { + "content" : { "Fn::Join" : ["", [ + "[main]\n", + "stack=", { "Ref" : "AWS::StackId" }, "\n", + "region=", { "Ref" : "AWS::Region" }, "\n" + ]]}, + "mode" : "000400", + "owner" : "root", + "group" : "root" + }, + + "/etc/cfn/hooks.d/cfn-auto-reloader.conf" : { + "content": { "Fn::Join" : ["", [ + "[cfn-auto-reloader-hook]\n", + "triggers=post.update\n", + "path=Resources.WebServer.Metadata.AWS::CloudFormation::Init\n", + "action=/opt/aws/bin/cfn-init -v ", + " --stack ", { "Ref" : "AWS::StackName" }, + " --resource WebServer ", + " --configsets full_install ", + " --region ", { "Ref" : "AWS::Region" }, "\n", + "runas=root\n" + ]]}, + "mode" : "000400", + "owner" : "root", + "group" : "root" + } + }, + + "services" : { + "sysvinit" : { + "cfn-hup" : { "enabled" : "true", "ensureRunning" : "true", + "files" : ["/etc/cfn/cfn-hup.conf", "/etc/cfn/hooks.d/cfn-auto-reloader.conf"]} + } + } + }, + + "install_docker": { + "commands": { + "01_install_docker": { + "command": "yum install -y docker git" + }, + "02_install_docker_compose": { + "command": {"Fn::Join" : ["", [ + "curl -L https://github.com/docker/compose/releases/download/1.21.2/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose &&", + "chmod +x /usr/local/bin/docker-compose &&", + "ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose &&", + "chmod +x /usr/bin/docker-compose" + ]]} + }, + "03_start_service": { + "command": "service docker start" + } + } + }, + + "prepare_datadir": { + "commands": { + "01_create_datadir": { + "command": "mkdir /matreon_data" + }, + "02_set_env": { + "command": { "Fn::Join" : ["", [ + "echo \"SECRET_KEY_BASE=`hexdump -n 64 -e '16/4 \"%08x\" 1 \"\n\"' /dev/random`\" >> /matreon_data/.env \n", + "echo \"DEVISE_SECRET_KEY=`hexdump -n 64 -e '16/4 \"%08x\" 1 \"\n\"' /dev/random`\" >> /matreon_data/.env \n", + "echo \"LIGHTNING_CHARGE_API_TOKEN=`hexdump -n 64 -e '16/4 \"%08x\" 1 \"\n\"' /dev/random`\" >> /matreon_data/.env \n", + "echo \"HOSTNAME=", { "Ref" : "HostName" }, "\" >> /matreon_data/.env \n", + "echo \"FROM_EMAIL=", { "Ref" : "FromEmail" }, "\" >> /matreon_data/.env \n", + "echo \"BUGS_TO=", { "Ref" : "BugsEmail" }, "\" >> /matreon_data/.env \n", + "echo \"SMTP_HOST=", { "Ref" : "SmtpHost" }, "\" >> /matreon_data/.env \n", + "echo \"SMTP_PORT=", { "Ref" : "SmtpPort" }, "\" >> /matreon_data/.env \n", + "echo \"SMTP_USERNAME=", { "Ref" : "SmtpUser" }, "\" >> /matreon_data/.env \n", + "echo \"SMTP_PASSWORD=", { "Ref" : "SmtpPassword" }, "\" >> /matreon_data/.env \n", + "echo \"NETWORK=", { "Ref" : "Network" }, "\" >> /matreon_data/.env \n", + "echo \"PODCAST=", { "Ref" : "Podcast" }, "\" >> /matreon_data/.env \n", + "echo \"PODCAST_TITLE=", { "Ref" : "PodcastTitle" }, "\" >> /matreon_data/.env \n", + "echo \"PODCAST_URL=", { "Ref" : "PodcastUrl" }, "\" >> /matreon_data/.env \n", + "echo \"PODCAST_IMAGE=", { "Ref" : "PodcastImage" }, "\" >> /matreon_data/.env \n", + "echo \"DATADIR=/matreon_data", "\" >> /matreon_data/.env \n", + "echo \"IP_ADDRESS=", { "Ref" : "IPAddress" }, "\" >> /matreon_data/.env\n", + "source /matreon_data/.env" + ]]} + } + } + }, + + "install_container_bitcoin": { + "commands": { + "01_configure_datadir": { + "command": "mkdir /matreon_data/bitcoin" + }, + "02_clone_repo": { + "command": "git clone https://github.com/NicolasDorier/docker-bitcoin /usr/local/src/docker-bitcoin" + }, + "03_docker_build": { + "command": "docker build /usr/local/src/docker-bitcoin/core/0.16.0 -t bitcoind:0.16.0" + } + } + }, + + "install_container_lightning": { + "commands": { + "01_configure_datadir": { + "command": "mkdir /matreon_data/lightning" + }, + "02_clone_repo": { + "command": "git clone https://github.com/cdecker/dockerfiles /usr/local/src/docker-lightning" + }, + "03_docker_build": { + "command": "docker build /usr/local/src/docker-lightning/lightning/node -f /usr/local/src/docker-lightning/lightning/node/Dockerfile.master -t lightningd:latest" + } + } + }, + + "install_container_charge": { + "commands": { + "01_configure_datadir": { + "command": "mkdir /matreon_data/charge" + }, + "02_clone_repo": { + "command": "git clone https://github.com/Sjors/lightning-charge /usr/local/src/lightning-charge && cd /usr/local/src/lightning-charge && git checkout 2018/05/node-uri" + }, + "03_docker_build": { + "command": "docker build /usr/local/src/lightning-charge -t charge:latest" + } + } + }, + + "install_docker_compose_with_pg_and_rails": { + "commands": { + "01_make_db_dir": { + "command": "mkdir /matreon_data/pg" + }, + "02_clone_repo": { + "command": "git clone https://github.com/Sjors/matreon.git /usr/local/src/matreon && cd /usr/local/src/matreon && git checkout 2018/05/docker-compose-aws-cloud" + }, + "03_build_rails_container": { + "command": "cd /usr/local/src/matreon && docker-compose build" + } + } + }, + + "initial_blockchain_download": { + "commands": { + "01_start_bitcoind": { + "command": "cd /usr/local/src/matreon && set -a && source /matreon_data/.env && docker-compose up -d bitcoind" + }, + "02_wait_for_sync": { + "command": "cd /usr/local/src/matreon && set -a && source /matreon_data/.env && ./wait_for_ibd.sh > /var/log/wait_for_ibd.log" + } + } + }, + + "launch_matreon": { + "01_launch": { + "command": "cd /usr/local/src/matreon && set -a && source /matreon_data/.env && docker-compose up -d" + }, + "02_migrate_db": { + "command": "cd /usr/local/src/matreon && docker-compose run web rake db:migrate" + }, + "03_install_crontab": { + "command": { "Fn::Join" : ["", [ + "echo '0 * * * * root cd /usr/local/src/matreon && set -a && source /matreon_data/.env && docker-compose run web rake invoices:process' >> /tmp/crontab \n", + "echo '0 * * * * root cd /usr/local/src/matreon && set -a && source /matreon_data/.env && docker-compose run web rake podcast:fetch' >> /tmp/crontab \n", + "echo '' >> /tmp/crontab", + "crontab /tmp/crontab" + ]]} + } + } + } + }, + "Properties": { + "ImageId" : "ami-5652ce39", + "InstanceType" : { "Ref" : "InstanceType" }, + "BlockDeviceMappings" : [ + { + "DeviceName" : "/dev/xvda", + "Ebs" : { + "VolumeSize" : "25" + } + } + ], + "SecurityGroups" : [ {"Ref" : "WebServerSecurityGroup"} ], + "KeyName" : { "Ref" : "KeyName" }, + "Tags" : [ + {"Key" : "Name", "Value" : "Matreon"} + ], + "UserData" : { "Fn::Base64" : { "Fn::Join" : ["", [ + "#!/bin/bash -xe\n", + "yum update -y aws-cfn-bootstrap\n", + + "/opt/aws/bin/cfn-init -v ", + " --stack ", { "Ref" : "AWS::StackId" }, + " --resource WebServer ", + " --configsets full_install ", + " --region ", { "Ref" : "AWS::Region" }, "\n", + + "/opt/aws/bin/cfn-signal -e $? ", + " --stack ", { "Ref" : "AWS::StackId" }, + " --resource WebServer ", + " --region ", { "Ref" : "AWS::Region" }, "\n" + ]]}} + }, + "CreationPolicy" : { + "ResourceSignal" : { + "Timeout" : "PT14400M" + } + } + }, + + "IPAddress" : { + "Type" : "AWS::EC2::EIP" + }, + + "IPAssoc" : { + "Type" : "AWS::EC2::EIPAssociation", + "Properties" : { + "InstanceId" : { "Ref" : "WebServer" }, + "EIP" : { "Ref" : "IPAddress" } + } + }, + + "WebServerSecurityGroup" : { + "Type" : "AWS::EC2::SecurityGroup", + "Properties" : { + "GroupDescription" : "Enable SSH, Bitcoin P2P, Lightning P2P and Charge access", + "SecurityGroupIngress" : [ + {"IpProtocol" : "tcp", "FromPort" : "8883", "ToPort" : "8883", "CidrIp" : "0.0.0.0/0"}, + {"IpProtocol" : "tcp", "FromPort" : "18883", "ToPort" : "18883", "CidrIp" : "0.0.0.0/0"}, + {"IpProtocol" : "tcp", "FromPort" : "9735", "ToPort" : "9735", "CidrIp" : "0.0.0.0/0"}, + {"IpProtocol" : "tcp", "FromPort" : "80", "ToPort" : "80", "CidrIp" : "0.0.0.0/0"}, + {"IpProtocol" : "tcp", "FromPort" : "22", "ToPort" : "22", "CidrIp" : { "Ref" : "SSHLocation"}} + ] + } + } + }, + + "Outputs" : { + "WebsiteURL" : { + "Value" : { "Fn::Join" : ["", ["http://", { "Fn::GetAtt" : [ "WebServer", "PublicDnsName" ]}, "/" ]] }, + "Description" : "URL for your Matreon" + } + } +} diff --git a/README.md b/README.md index 5507ee7..fbae18a 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,144 @@ Current status: extremely experimental! Live instance: [matreon.sprovoost.nl](https://matreon.sprovoost.nl/) -## Prerequisites +## Deploy to AWS using Docker -You need to run [c-lightning](https://github.com/ElementsProject/lightning) and [Lightning Charge](https://github.com/ElementsProject/lightning-charge) somewhere. +This is currently quite brittle and not very secure. + +Install the Amazon CloudFormation template by downloading [Matreon.Template](https://raw.githubusercontent.com/Sjors/matreon/master/Matreon.Template) and then uploading it on the [CloudFormation stack creation page](https://eu-central-1.console.aws.amazon.com/cloudformation/home?region=eu-central-1&stackName=Matreon#/stacks/new). + +Fill out the form, click next a few times and then wait... This [blog post](https://medium.com/provoost-on-crypto/bitcoin-core-lightning-rails-on-aws-ad3bd45b11e0) explains the steps in more detail. + +## Deploy elsewhere using Docker + +Install [Docker](https://docs.docker.com/install/). + +Create a directory to store the blockchain, wallet info, etc: + +```sh +mkdir matreon-vol +mkdir matreon-vol/bitcoin +mkdir matreon-vol/lightning +mkdir matreon-vol/charge +mkdir matreon-vol/pg +``` + +We use Docker Compose to combine a number of containers. Hardcoding a [Docker image checksum](https://docs.docker.com/engine/security/trust/content_trust/#content-trust-operations-and-keys) doesn't prove how the image was built, so to minimize trust, we have to build the containers locally. + +### Container 1 - Bitcoin Core + +```sh +git clone https://github.com/NicolasDorier/docker-bitcoin +docker build docker-bitcoin/core/0.16.0 -t bitcoind:0.16.0 +``` + +Once the container is running (see below), you can view bitcoind logs (e.g. to + see how syncing the blockchain is going): + +``` +docker logs -f --since 1m matreon_bitcoind_1 +``` + + +### Container 2 - C-Lightning + +```sh +git clone https://github.com/cdecker/dockerfiles docker-lightning +docker build docker-lightning/lightning/node -f docker-lightning/lightning/node/Dockerfile.master -t lightningd:latest +``` + +Once the container is running (see below), you can monitor the logs: + +```sh +docker logs -f --since 1m matreon_lightningd_1 +``` + +Or interact: + +```sh +docker-compose exec lightningd lightning-cli help +``` + +### Container 3 - Lightning Charge + +```sh +git clone https://github.com/ElementsProject/lightning-charge +docker build lightning-charge -t charge:latest +``` + +### Container 4 - Postgres + +We're trusting the upstream image for now. + +### Container 5 - Rails & Matreon + +```sh +git clone https://github.com/Sjors/matreon.git +``` + +Once the container is running (see below), you can open a Rails console: + +```sh +docker-compose run web rails console +``` + +Or view the server logs: + +```sh +docker logs -f matreon_web_1 +``` + +### Docker Compose + +From the Matreon project directory: + +```sh +export NETWORK=testnet # or "bitcoin" for mainnet +export DATADIR=~/matreon-vol/bitcoin +export LIGHTNING_CHARGE_API_TOKEN=1234 +export FROM_EMAIL="you@example.com" +export BUGS_TO="bugs@example.com" +export SECRET_KEY_BASE=`hexdump -n 64 -e '16/4 "%08x" 1 "\n"' /dev/random` +export DEVISE_SECRET_KEY=`hexdump -n 64 -e '16/4 "%08x" 1 "\n"' /dev/random` +export HOSTNAME=http://localhost +export SMTP_HOST=... +export SMTP_USERNAME=... +export SMTP_PASSWORD=... +docker-compose build +docker-compose up -d +``` + +Migrate the database: + +```sh +docker-compose run web rake db:migrate +``` + +Visit [localhost](http://localhost/). + +To shut everything down + +``` +docker-compose down +``` + +### Cron jobs + +Add the following cron jobs (`crontab -e`): + +```sh +0 * * * * cd /usr/local/src/matreon && /usr/local/bin/docker-compose run web rake invoices:process +0 * * * * cd /usr/local/src/matreon && /usr/local/bin/docker-compose run web rake podcast:fetch +``` ## Deploy to Heroku +### Prerequisites + +You need to run [c-lightning](https://github.com/ElementsProject/lightning) and [Lightning Charge](https://github.com/ElementsProject/lightning-charge) somewhere. + +### Heroku + Create a new Heroku app `app-name` and add the Sendgrid Add-On. Clone this repo and: diff --git a/app/javascript/bundles/Matreon/components/Invoices/Invoices.jsx b/app/javascript/bundles/Matreon/components/Invoices/Invoices.jsx index 81b737d..7a26152 100644 --- a/app/javascript/bundles/Matreon/components/Invoices/Invoices.jsx +++ b/app/javascript/bundles/Matreon/components/Invoices/Invoices.jsx @@ -55,7 +55,7 @@ class Invoice extends React.Component {