diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e91d1ce --- /dev/null +++ b/.env.example @@ -0,0 +1,24 @@ +# Linux user account information for running the bot +UID=1000 +USER_NAME=bot + +# Username for HTTP access +HTTP_AUTH_USER=arbb0t +# Password for HTTP access +HTTP_AUTH_PASSWORD=arbb0tpass +# Port on which the HTTP server should listen on +HTTP_PORT=80 + +# MySQL related config +MYSQL_ROOT_PASSWORD=root +MYSQL_DATABASE=arbitrage +MYSQL_USER=arbitrage +MYSQL_PASSWORD=MySqlPassword + +# TLS Deployment using Let's Encrypt (requires a domain name pointing to your server/cluster's IP address) +VIRTUAL_HOST=arbbot.example.com # Your Domain Name +LETSENCRYPT_HOST=arbbot.example.com # Your Domain Name +LETSENCRYPT_EMAIL=arbbot@example.com # Email Address Let's Encrypt Uses For Sending Expiration Notices + # (highly suggested to fill in a valid email address.) +HTTPS_PORT=443 # Port on which the HTTPS server should listen on +IP=0.0.0.0 # External IP to which the server should bind to diff --git a/.gitignore b/.gitignore index f379d9d..457751e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ bittrex-fullOrders.csv config.ini config.ini.[0-9]* poloniex-tradeHistory.csv -web/config.inc.php +\.idea/ +\.env +/docker/db-data/ diff --git a/README.md b/README.md index 0976509..b0fb5b6 100644 --- a/README.md +++ b/README.md @@ -12,137 +12,63 @@ You can see the bot running on a cheap [linode](https://www.linode.com) here. C * Bleutrade (all BTC markets) * Poloniex (all BTC markets) -## Installation on Debian 9.0+ / Ubuntu 16.04+ +## Running the bot using Docker -Install required packages: +The only supported way to run this bot currently is to use Docker. Advanced users are welcome to study the docker files to see how the configuration works if they want to setup their own advanced setups, but all such configurations are unsupported and you're on your own if things go wrong! -``` -sudo apt-get install php-cli php-curl php-mysqlnd mysql-server nginx-full php-fpm unzip apache2-utils ntpdate -sudo ntpdate ntp.ubuntu.com -``` - -Clone the repository on the server. Note that installation from ZIP archives isn't supported any more. - -``` -cd /var/www -git clone --recursive https://github.com/cryptoeax/arbbot.git -``` - -cd into the directory: - -``` -cd arbbot -``` - -Prepare the MySQL database: - -``` - mysql -u root -p - mysql> CREATE DATABASE arbitrage; - mysql> GRANT ALL ON arbitrage.* TO arbitrage@localhost IDENTIFIED BY 'YOUR_PASSWORD'; - mysql> use arbitrage; - mysql> source database.sql; - mysql> quit - ``` - -Configure the database connection: - -``` -cp web/config.inc.php.example web/config.inc.php -nano web/config.inc.php -``` - - -Configure the bot: - -``` -cp config.ini.example config.ini -nano config.ini -``` +### Pre-requisites -Edit all options to fit your needs and enter your API keys! You can change settings even while the bot is running. The changes will be automatically applied. +Install the latest versions of docker and docker-compose -## Configuring the Webinterface +Clone the repository somewhere using `git clone --recursive`. -First, you need to configure a username and password: +### Preparation -``` -htpasswd -b /var/www/conf/arbitrage.passwd username password -``` - -Then you need to edit the web server configuration. +Customize your environment settings: ``` -rm /etc/nginx/sites-enabled/default -nano /etc/nginx/sites-enabled/default +cp .env.example .env +vi .env # edit the file to customize the variables, NEVER use the default passwords +mkdir -p docker/config +cp config.ini.example docker/config/config.ini +cd docker/config +vi config.ini # edit the database settings to make them match .env +cd ../.. ``` -The NGINX configuration file should look like this: +In case you have been running the bot from the pre-dockerized versions, you probably want to import the data that the bot currently has saved in its database into the database that the new containerized bot launches. You can do so by planting a special `data.sql` file in the right place, like this: ``` -server { - - listen 80; - root /var/www/arbbot/web; - index index.html; - server_name localhost; - - location / { - - auth_basic "Restricted area"; - auth_basic_user_file /var/www/conf/arbitrage.passwd; - - } - - location ~ \.php$ { - - include snippets/fastcgi-php.conf; - fastcgi_pass unix:/var/run/php/php7.0-fpm.sock; - - } - -} +mysqldump -h host -u username -p --no-create-info > docker/db/seed/data.sql ``` -If your web server is exposed to the public Internet, the UI will by default refuse to load unless you enable HTTPS. You can get a free certificate using Let's Encrypt, see [this tutorial](https://www.nginx.com/blog/free-certificates-lets-encrypt-and-nginx/) on how to do that. Assuming you have a certificate, you would need to add a `ssl_certificate` and `ssl_certificate_key` directive to the configration file, if the Let's Encrypt client doesn't do that for you (which it should.) If you are going to access your bot on http://localhost then you don't need to perform this step. - -Now, restart the webserver: +Once you have finished the above steps, you are ready to build your containers. ``` -/etc/init.d/nginx restart +docker-compose build ``` -You should now be able to access the webinterface with your browser. - -## Running the bot +### Running the bot Now you are ready to give the bot a test by running it: ``` -php main.php +docker-compose up -d ``` -(Note: It is recommended to run `main.php` with [`hhvm`](https://docs.hhvm.com/hhvm/installation/linux) instead of `php` in order to speed up the bot a bit.) - -You should see output like this: - -``` -19:13:34: ARBITRATOR V2.0 launching - -``` -To actually allow the bot to buy coins automatically, you need to reserve some autobuy funds. You can do that either by running the following commands against the database manually: -``` - mysql -u root -p - mysql> use arbitrage; - mysql> UPDATE stats SET value = "0.2" WHERE keyy = "autobuy_funds"; - mysql> quit -``` +To actually allow the bot to buy coins automatically, you need to reserve some autobuy funds. You can do that by turning on the admin UI by enabling the `general.admin-ui` setting, the web UI shows you the Admin interface which allows you to change the autobuy funds amount. It is not recommended to enable this if your web UI isn't secure using password authentication and HTTPS in case it's exposed to the Internet. This example assigns 0.2 BTC to the "autobuy_funds". The higher the amount, the more coins can be bought and the more arbitrage-opportunities can be taken. Be careful to keep at least 0.1 - 0.2 BTC at the exchange to give the bot enough room to trade. -Alternatively by enabling the `general.admin-ui` setting, the web UI shows you the Admin interface which allows you to change the autobuy funds amount. It is not recommended to enable this if your web UI isn't secure using password authentication and HTTPS in case it's exposed to the Internet. +## Updating the bot +When you update the bot, you need to rebuild the docker containers in case they require rebuilding before rerunning the bot, you can do that with `docker-compose build`. + +## Backing up the bot's data +The bot's mysql server saves its configuration in `docker/config` and its data in `docker/db-data`. You are encouraged to back up the contents of these directories occasionally. If you delete the contents of these directories, the next time you run the bot the bot will reinitialize its database from scratch (and will import an initial seed data from `data.sql` if that file exists.) Please note that the bot currently requires a config file to start. + +When you use the Admin UI to modify the bot's settings, the bot saves backups of the config file before modifying `config.ini`. These backup copies are also stored in the `docker/config` directory. ## How does the bot make profit? diff --git a/bot/Config.php b/bot/Config.php index e56a5f2..aef6bec 100644 --- a/bot/Config.php +++ b/bot/Config.php @@ -2,11 +2,7 @@ class Config { - // DATABASE - const DB_USER = 'db.user'; - const DB_PASS = 'db.pass'; - const DB_HOST = 'db.host'; - const DB_NAME = 'db.name'; + const CONFIG_DIR = "/var/arbbot/"; // // MAIL const MAIL_RECIPIENT = 'mail.address'; @@ -157,6 +153,12 @@ private function __construct() { } + public static function getConfigDir() { + + return self::CONFIG_DIR; + + } + public static function isCurrency( $coin ) { return $coin == 'BTC'; @@ -231,14 +233,9 @@ public static function get( $key, $default = null ) { public static function refresh() { - $config = @parse_ini_file( "config.ini", true ); + $config = @parse_ini_file( self::getConfigDir() . "/config.ini", true ); if ( !$config ) { - // The web UI accesses the Config object from ../bot, so config.ini will - // be placed in the parent directory. - $config = @parse_ini_file( "../config.ini", true ); - if ( !$config ) { - throw new Exception( "Configuration not found or invalid!" ); - } + throw new Exception( "Configuration not found or invalid!" ); } self::$config = $config; @@ -283,7 +280,7 @@ public static function getEditableKeys() { ); } - $file = file_get_contents( __DIR__ . '/../config.ini' ); + $file = file_get_contents( self::getConfigDir() . '/config.ini' ); $lines = array( ); if (strstr( $file, "\r\n" )) { $lines = explode( "\r\n", $file ); @@ -375,8 +372,8 @@ public static function setEditableKeys($input) { $inputs[ $item[ 'name' ] ] = $item[ 'value' ]; } - $file = file_get_contents( __DIR__ . '/../config.ini' ); - file_put_contents( __DIR__ . '/../config.ini.' . time(), $file ); + $file = file_get_contents( self::getConfigDir() . '/config.ini' ); + file_put_contents( self::getConfigDir() . '/config.ini.' . time(), $file ); $lines = array( ); $output = ''; @@ -434,7 +431,7 @@ public static function setEditableKeys($input) { } } - $bytes = file_put_contents( __DIR__ . '/../config.ini', $output ); + $bytes = file_put_contents( self::getConfigDir() . '/config.ini', $output ); return $bytes !== false; diff --git a/bot/Database.php b/bot/Database.php index 8c538d5..2bd2cea 100644 --- a/bot/Database.php +++ b/bot/Database.php @@ -1,5 +1,8 @@ &1 | grep Date: | cut -d' ' -f5-8)Z" +service ntp start +update-rc.d ntp enable +sleep 5 +echo "Checking for Peers" +ntpq -c lpeer + diff --git a/docker/bot/php.ini b/docker/bot/php.ini new file mode 100644 index 0000000..061bb56 --- /dev/null +++ b/docker/bot/php.ini @@ -0,0 +1,10 @@ +log_errors = On +error_log = /dev/stderr +file_uploads = On +memory_limit = 512M +upload_max_filesize = 256M +post_max_size = 256M +max_execution_time = 60 +error_reporting = E_ALL +date.timezone = "UTC" +include_path = .:/usr/share/php:/usr/share/pear \ No newline at end of file diff --git a/docker/bot/wait-for-db.sh b/docker/bot/wait-for-db.sh new file mode 100644 index 0000000..c9feb81 --- /dev/null +++ b/docker/bot/wait-for-db.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +set -e +sleep 10 + +waitForTable() { + table=$1; + db=$2 + count=$(mysql -N -s -u root '-p$3' -h db -e \ + "select count(*) from information_schema.tables where \ + table_schema='${db}' and table_name='${table}';" 2>/dev/null) + while [ "$count" -eq "0" ]; do + sleep 1 + done +} + +waitForTable $@ +sleep 1 + +shift 3; +exec $@ diff --git a/docker/db/config/my.cnf b/docker/db/config/my.cnf new file mode 100644 index 0000000..884a59a --- /dev/null +++ b/docker/db/config/my.cnf @@ -0,0 +1,14 @@ +[mysqld] +max_allowed_packet = 32M +query_cache_limit = 1M +query_cache_size = 16M +sql_mode = "ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION" +character_set_server=utf8 +collation_server=utf8_general_ci +innodb_use_native_aio=0 + +[client] +default-character-set = utf8 + +[mysql] +default-character-set = utf8 diff --git a/docker/db/seed/.gitignore b/docker/db/seed/.gitignore new file mode 100644 index 0000000..f291df2 --- /dev/null +++ b/docker/db/seed/.gitignore @@ -0,0 +1 @@ +data.sql \ No newline at end of file diff --git a/database.sql b/docker/db/seed/01-database.sql similarity index 100% rename from database.sql rename to docker/db/seed/01-database.sql diff --git a/docker/db/seed/data.sql b/docker/db/seed/data.sql new file mode 100644 index 0000000..e69de29 diff --git a/docker/nginx-gen/Dockerfile b/docker/nginx-gen/Dockerfile new file mode 100644 index 0000000..f5ef602 --- /dev/null +++ b/docker/nginx-gen/Dockerfile @@ -0,0 +1,5 @@ +FROM jwilder/docker-gen + +# Create nginx.tmpl as a file to ensure it won't get mapped as a directory +RUN mkdir -p /etc/docker-gen/templates +ADD nginx.tmpl /etc/docker-gen/templates/nginx.tmpl diff --git a/docker/nginx-gen/nginx.tmpl b/docker/nginx-gen/nginx.tmpl new file mode 100644 index 0000000..9eb9520 --- /dev/null +++ b/docker/nginx-gen/nginx.tmpl @@ -0,0 +1,237 @@ +{{ $CurrentContainer := where $ "ID" .Docker.CurrentContainerID | first }} + +{{ define "upstream" }} + {{ if .Address }} + {{/* If we got the containers from swarm and this container's port is published to host, use host IP:PORT */}} + {{ if and .Container.Node.ID .Address.HostPort }} + # {{ .Container.Node.Name }}/{{ .Container.Name }} + server {{ .Container.Node.Address.IP }}:{{ .Address.HostPort }}; + {{/* If there is no swarm node or the port is not published on host, use container's IP:PORT */}} + {{ else if .Network }} + # {{ .Container.Name }} + server {{ .Network.IP }}:{{ .Address.Port }}; + {{ end }} + {{ else if .Network }} + # {{ .Container.Name }} + server {{ .Network.IP }} down; + {{ end }} +{{ end }} + +# If we receive X-Forwarded-Proto, pass it through; otherwise, pass along the +# scheme used to connect to this server +map $http_x_forwarded_proto $proxy_x_forwarded_proto { + default $http_x_forwarded_proto; + '' $scheme; +} + +# If we receive Upgrade, set Connection to "upgrade"; otherwise, delete any +# Connection header that may have been passed to this server +map $http_upgrade $proxy_connection { + default upgrade; + '' close; +} + +gzip_types text/plain text/css application/javascript application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript; + +log_format vhost '$host $remote_addr - $remote_user [$time_local] ' + '"$request" $status $body_bytes_sent ' + '"$http_referer" "$http_user_agent"'; + +access_log off; + +{{ if (exists "/etc/nginx/proxy.conf") }} +include /etc/nginx/proxy.conf; +{{ else }} +# HTTP 1.1 support +proxy_http_version 1.1; +proxy_buffering off; +proxy_set_header Host $http_host; +proxy_set_header Upgrade $http_upgrade; +proxy_set_header Connection $proxy_connection; +proxy_set_header X-Real-IP $remote_addr; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; + +# Mitigate httpoxy attack (see README for details) +proxy_set_header Proxy ""; +{{ end }} + +server { + server_name _; # This is just an invalid value which will never trigger on a real hostname. + listen 80; + access_log /var/log/nginx/access.log vhost; + return 503; +} + +{{ if (and (exists "/etc/nginx/certs/default.crt") (exists "/etc/nginx/certs/default.key")) }} +server { + server_name _; # This is just an invalid value which will never trigger on a real hostname. + listen 443 ssl http2; + access_log /var/log/nginx/access.log vhost; + return 503; + + ssl_session_tickets off; + ssl_certificate /etc/nginx/certs/default.crt; + ssl_certificate_key /etc/nginx/certs/default.key; +} +{{ end }} + +{{ range $host, $containers := groupByMulti $ "Env.VIRTUAL_HOST" "," }} + +upstream {{ $host }} { +{{ range $container := $containers }} + {{ $addrLen := len $container.Addresses }} + + {{ range $knownNetwork := $CurrentContainer.Networks }} + {{ range $containerNetwork := $container.Networks }} + {{ if eq $knownNetwork.Name $containerNetwork.Name }} + ## Can be connect with "{{ $containerNetwork.Name }}" network + + {{/* If only 1 port exposed, use that */}} + {{ if eq $addrLen 1 }} + {{ $address := index $container.Addresses 0 }} + {{ template "upstream" (dict "Container" $container "Address" $address "Network" $containerNetwork) }} + {{/* If more than one port exposed, use the one matching VIRTUAL_PORT env var, falling back to standard web port 80 */}} + {{ else }} + {{ $port := coalesce $container.Env.VIRTUAL_PORT "80" }} + {{ $address := where $container.Addresses "Port" $port | first }} + {{ template "upstream" (dict "Container" $container "Address" $address "Network" $containerNetwork) }} + {{ end }} + {{ end }} + {{ end }} + {{ end }} +{{ end }} +} + +{{ $default_host := or ($.Env.DEFAULT_HOST) "" }} +{{ $default_server := index (dict $host "" $default_host "default_server") $host }} + +{{/* Get the VIRTUAL_PROTO defined by containers w/ the same vhost, falling back to "http" */}} +{{ $proto := or (first (groupByKeys $containers "Env.VIRTUAL_PROTO")) "http" }} + +{{/* Get the HTTPS_METHOD defined by containers w/ the same vhost, falling back to "redirect" */}} +{{ $https_method := or (first (groupByKeys $containers "Env.HTTPS_METHOD")) "redirect" }} + +{{/* Get the first cert name defined by containers w/ the same vhost */}} +{{ $certName := (first (groupByKeys $containers "Env.CERT_NAME")) }} + +{{/* Get the best matching cert by name for the vhost. */}} +{{ $vhostCert := (closest (dir "/etc/nginx/certs") (printf "%s.crt" $host))}} + +{{/* vhostCert is actually a filename so remove any suffixes since they are added later */}} +{{ $vhostCert := trimSuffix ".crt" $vhostCert }} +{{ $vhostCert := trimSuffix ".key" $vhostCert }} + +{{/* Use the cert specified on the container or fallback to the best vhost match */}} +{{ $cert := (coalesce $certName $vhostCert) }} + +{{ $is_https := (and (ne $cert "") (exists (printf "/etc/nginx/certs/%s.crt" $cert)) (exists (printf "/etc/nginx/certs/%s.key" $cert))) }} + +{{ if $is_https }} + +{{ if eq $https_method "redirect" }} +server { + server_name {{ $host }}; + listen 80 {{ $default_server }}; + access_log /var/log/nginx/access.log vhost; + return 301 https://$host$request_uri; +} +{{ end }} + +server { + server_name {{ $host }}; + listen 443 ssl http2 {{ $default_server }}; + access_log /var/log/nginx/access.log vhost; + + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS'; + + ssl_prefer_server_ciphers on; + ssl_session_timeout 5m; + ssl_session_cache shared:SSL:50m; + ssl_session_tickets off; + + ssl_certificate /etc/nginx/certs/{{ (printf "%s.crt" $cert) }}; + ssl_certificate_key /etc/nginx/certs/{{ (printf "%s.key" $cert) }}; + + {{ if (exists (printf "/etc/nginx/certs/%s.dhparam.pem" $cert)) }} + ssl_dhparam {{ printf "/etc/nginx/certs/%s.dhparam.pem" $cert }}; + {{ end }} + + {{ if (ne $https_method "noredirect") }} + add_header Strict-Transport-Security "max-age=31536000"; + {{ end }} + + {{ if (exists (printf "/etc/nginx/vhost.d/%s" $host)) }} + include {{ printf "/etc/nginx/vhost.d/%s" $host }}; + {{ else if (exists "/etc/nginx/vhost.d/default") }} + include /etc/nginx/vhost.d/default; + {{ end }} + + location / { + {{ if eq $proto "uwsgi" }} + include uwsgi_params; + uwsgi_pass {{ trim $proto }}://{{ trim $host }}; + {{ else }} + proxy_pass {{ trim $proto }}://{{ trim $host }}; + {{ end }} + {{ if (exists (printf "/etc/nginx/htpasswd/%s" $host)) }} + auth_basic "Restricted {{ $host }}"; + auth_basic_user_file {{ (printf "/etc/nginx/htpasswd/%s" $host) }}; + {{ end }} + {{ if (exists (printf "/etc/nginx/vhost.d/%s_location" $host)) }} + include {{ printf "/etc/nginx/vhost.d/%s_location" $host}}; + {{ else if (exists "/etc/nginx/vhost.d/default_location") }} + include /etc/nginx/vhost.d/default_location; + {{ end }} + } +} + +{{ end }} + +{{ if or (not $is_https) (eq $https_method "noredirect") }} + +server { + server_name {{ $host }}; + listen 80 {{ $default_server }}; + access_log /var/log/nginx/access.log vhost; + + {{ if (exists (printf "/etc/nginx/vhost.d/%s" $host)) }} + include {{ printf "/etc/nginx/vhost.d/%s" $host }}; + {{ else if (exists "/etc/nginx/vhost.d/default") }} + include /etc/nginx/vhost.d/default; + {{ end }} + + location / { + {{ if eq $proto "uwsgi" }} + include uwsgi_params; + uwsgi_pass {{ trim $proto }}://{{ trim $host }}; + {{ else }} + proxy_pass {{ trim $proto }}://{{ trim $host }}; + {{ end }} + {{ if (exists (printf "/etc/nginx/htpasswd/%s" $host)) }} + auth_basic "Restricted {{ $host }}"; + auth_basic_user_file {{ (printf "/etc/nginx/htpasswd/%s" $host) }}; + {{ end }} + {{ if (exists (printf "/etc/nginx/vhost.d/%s_location" $host)) }} + include {{ printf "/etc/nginx/vhost.d/%s_location" $host}}; + {{ else if (exists "/etc/nginx/vhost.d/default_location") }} + include /etc/nginx/vhost.d/default_location; + {{ end }} + } +} + +{{ if (and (not $is_https) (exists "/etc/nginx/certs/default.crt") (exists "/etc/nginx/certs/default.key")) }} +server { + server_name {{ $host }}; + listen 443 ssl http2 {{ $default_server }}; + access_log /var/log/nginx/access.log vhost; + return 500; + + ssl_certificate /etc/nginx/certs/default.crt; + ssl_certificate_key /etc/nginx/certs/default.key; +} +{{ end }} + +{{ end }} +{{ end }} diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile new file mode 100644 index 0000000..f2658d1 --- /dev/null +++ b/docker/php/Dockerfile @@ -0,0 +1,26 @@ +FROM php:7.0-fpm + +RUN apt-get update && apt-get install -y apt-utils + + +RUN apt-get update && apt-get install -y \ + libicu-dev unzip ntpdate libmcrypt-dev zlib1g-dev g++ ntp \ + && rm -r /var/lib/apt/lists/* + + +RUN docker-php-ext-configure intl \ +&& docker-php-ext-install -j$(nproc) mbstring zip mcrypt bcmath intl mysqli + +COPY php.ini /usr/local/etc/php/php.ini + +ARG uid +ARG username + +RUN (users | grep -q ${username}) || \ + (useradd -s /bin/bash -m -u ${uid} ${username} \ + && chown -R ${username}:${username} /home/${username} \ + && echo "Using user ${username} with UID ${uid}.") + +RUN mkdir -p /var/arbbot && chown -R ${username}:${username} /var/arbbot + +CMD ["php-fpm"] diff --git a/docker/php/php.ini b/docker/php/php.ini new file mode 100644 index 0000000..061bb56 --- /dev/null +++ b/docker/php/php.ini @@ -0,0 +1,10 @@ +log_errors = On +error_log = /dev/stderr +file_uploads = On +memory_limit = 512M +upload_max_filesize = 256M +post_max_size = 256M +max_execution_time = 60 +error_reporting = E_ALL +date.timezone = "UTC" +include_path = .:/usr/share/php:/usr/share/pear \ No newline at end of file diff --git a/docker/web/Dockerfile b/docker/web/Dockerfile new file mode 100644 index 0000000..3793072 --- /dev/null +++ b/docker/web/Dockerfile @@ -0,0 +1,26 @@ +FROM nginx:1.13 + +RUN apt-get update && apt-get install -y apt-utils + +RUN apt-get install -y openssl + +# Remove the default Nginx coniguration file +RUN rm -v /etc/nginx/nginx.conf && rm -v /etc/nginx/conf.d/* + +# Copy a configuration file from the current directory +ADD ./nginx.conf /etc/nginx/nginx.conf +ADD ./config/* /etc/nginx/conf.d/ + +ARG http_auth_user +ARG http_auth_password + +RUN echo -n ${http_auth_user}:$(openssl passwd -apr1 ${http_auth_password}) > /etc/nginx/arbitrage.passwd + +ARG uid +ARG username + +RUN (users | grep -q ${username}) || \ + (useradd -s /bin/bash -m -u ${uid} ${username} \ + && chown -R ${username}:${username} /home/${username} \ + && echo "Using user ${username} with UID ${uid}.") + diff --git a/docker/web/config/default.conf b/docker/web/config/default.conf new file mode 100644 index 0000000..6d4a562 --- /dev/null +++ b/docker/web/config/default.conf @@ -0,0 +1,38 @@ +server { + + listen 80; + root /var/www/html/web; + autoindex off; + index index.html; + charset utf-8; + + server_name localhost; + error_log /dev/stdout error; + access_log /dev/stdout; + + location / { + auth_basic "Restricted area"; + auth_basic_user_file /etc/nginx/arbitrage.passwd; + } + + location = /favicon.ico { access_log off; log_not_found off; } + location = /robots.txt { access_log off; log_not_found off; } + + location ~ \.php$ { + try_files $uri =404; + include fastcgi_params; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_param SCRIPT_FILENAME $document_root/$fastcgi_script_name; + fastcgi_pass php:9000; + } + + location ~ /\. { + deny all; + } + + location ~* \.(js|css|jpg|jpeg|gif|png|svg|ico|pdf|html|htm|woff|woff2|eof)$ { + access_log off; + log_not_found off; + expires 30d; + } +} diff --git a/docker/web/nginx.conf b/docker/web/nginx.conf new file mode 100644 index 0000000..d427861 --- /dev/null +++ b/docker/web/nginx.conf @@ -0,0 +1,43 @@ +user nginx; +worker_processes 1; + +error_log /dev/stdout warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + + +http { + server_tokens off; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /dev/stdout; + error_log /dev/stdout; + + sendfile on; + #tcp_nopush on; + + keepalive_timeout 65; + + gzip on; + gzip_http_version 1.0; + gzip_proxied any; + gzip_min_length 500; + gzip_disable "MSIE [1-6]\."; + gzip_types text/plain text/xml text/css + text/comma-separated-values + text/javascript + application/x-javascript + application/atom+xml; + + + include /etc/nginx/conf.d/*.conf; +} diff --git a/import-profit-loss-prompt.txt b/import-profit-loss-prompt.txt deleted file mode 100644 index 3b306fc..0000000 --- a/import-profit-loss-prompt.txt +++ /dev/null @@ -1,9 +0,0 @@ -The bot has detected that you have just upgraded to a new version which -supports storing the profit&loss information in the database which enables -faster retrieval times. - -We now need to do a one time conversion of your current data to store it -in the database, and from now on your new trade's data will be stored in -the database automatically. - -Press enter when you are ready to start. This operation may take a while. diff --git a/import-profit-loss.php b/import-profit-loss.php deleted file mode 100644 index d0de8b5..0000000 --- a/import-profit-loss.php +++ /dev/null @@ -1,106 +0,0 @@ -refreshWallets(); - $x2->refreshWallets(); - $x1->refreshExchangeData(); - $x2->refreshExchangeData(); - $exchanges = array( - '1' => $x2, - '3' => $x1, - ); - - $hist1 = $x1->queryTradeHistory(); - $hist2 = $x2->queryTradeHistory(array( - 'start' => strtotime( '1/1/1970 1:1:1' ), - 'end' => time(), - )); - $histories = array( - '1' => &$hist2, - '3' => &$hist1, - ); - - foreach ( $pl as $row ) { - $coin = $row[ 'coin' ]; - $currency = $row[ 'currency' ]; - $time = $row[ 'time' ]; - $sourceExchange = $row[ 'source_exchange' ]; - $targetExchange = $row[ 'target_exchange' ]; - $rawTradeIDsBuy = array( ); - $tradeIDsBuy = array( ); - $rawTradeIDsSell = array( ); - $tradeIDsSell = array( ); - $rateTimesAmountBuy = 0; - $rateTimesAmountSell = 0; - $tradeableBought = 0; - $tradeableSold = 0; - $currencyBought = 0; - $currencySold = 0; - $currencyRevenue = 0; - $currencyProfitLoss = 0; - $tradeableTransferFee = 0; - $currencyTransferFee = 0; - $buyFee = 0; - $sellFee = 0; - - $ex = $exchanges[ $row[ 'source_exchange' ] ]; - $rawTradeIDsBuy = ''; - $tradeIDsBuy = ''; - $rateTimesAmountBuy = $row[ 'currency_bought' ]; - $tradeableBought = $ex->deductFeeFromAmountSell( $row[ 'tradeable_bought' ], $coin, $currency ); - $currencyBought = $ex->deductFeeFromAmountSell( $row[ 'currency_bought' ], $coin, $currency ); - $boughtAmount = $ex->deductFeeFromAmountSell( $row[ 'amount_bought_tradeable' ], $coin, $currency ); - $buyFee = $currencyBought - $ex->addFeeToPrice( $currencyBought, $coin, $currency ); - $rateBuy = ($tradeableBought > 0.0e-9) ? ($rateTimesAmountBuy / $tradeableBought) : 0; - - $ex = $exchanges[ $row[ 'target_exchange' ] ]; - $rawTradeIDsSell = ''; - $tradeIDsSell = ''; - $rateTimesAmountSell = $row[ 'currency_sold' ]; - $tradeableSold = $row[ 'tradeable_sold' ]; - $currencySold = $row[ 'currency_sold' ]; - $soldAmount = $row[ 'amount_sold_tradeable' ]; - $sellFee = $row[ 'currency_sold' ] - $ex->addFeeToPrice( $row[ 'currency_sold' ], $coin, $currency ); - $rateSell = ($tradeableSold > 0.0e-9) ? ($rateTimesAmountSell / $tradeableSold) : 0; - - $tradeableTransferFee = $row[ 'tx_fee_tradeable' ]; - $currencyTransferFee = $tradeableTransferFee * $rateSell; - $currencyRevenue = $row[ 'currency_revenue' ]; - $currencyProfitLoss = $currencyRevenue - $currencyTransferFee; - - Database::saveProfitLoss( $coin, $currency, $time, $sourceExchange, $targetExchange, - $rawTradeIDsBuy, $tradeIDsBuy, $rawTradeIDsSell, $tradeIDsSell, - $rateBuy, $rateSell, $tradeableBought, $tradeableSold, $currencyBought, - $currencySold, $currencyRevenue, $currencyProfitLoss, $tradeableTransferFee, - $currencyTransferFee, $buyFee, $sellFee ); - - } -} - diff --git a/main.php b/main.php index 4f9dab2..15af9ed 100644 --- a/main.php +++ b/main.php @@ -66,11 +66,6 @@ Database::importProfits(); } -if ( !Database::profitLossTableExists() ) { - require_once __DIR__ . '/import-profit-loss.php'; - importProfitLoss(); -} - // Configure exchanges... $exchanges = [ ]; $msg = ''; @@ -121,7 +116,7 @@ $name = $exchange->getTradeHistoryCSVName(); if ( $name ) { - $csvPath = __DIR__ . '/' . $name; + $csvPath = Config::getConfigDir() . '/' . $name; if ( ! is_readable( $csvPath ) ) { $prompt = file_get_contents( __DIR__ . '/bot/xchange/' . $exchange->getName() . '-csv-missing.txt' ); logg( $prompt ); diff --git a/web/WebDB.php b/web/WebDB.php index c5e49f3..9d86793 100644 --- a/web/WebDB.php +++ b/web/WebDB.php @@ -1,7 +1,6 @@