diff --git a/Gemfile b/Gemfile index 4468198a..89f93c12 100644 --- a/Gemfile +++ b/Gemfile @@ -14,6 +14,12 @@ gem 'rack_csrf' gem 'dry-schema' gem 'dry-validation' gem 'yaml' +gem 'sequel' +gem 'pg' +gem 'bcrypt' +gem 'thor' +gem 'readline' +gem 'readline-ext' gem "rack-reverse-proxy", require: "rack/reverse_proxy" group :development, :test do gem "rerun" diff --git a/cli/cli.rb b/cli/cli.rb new file mode 100644 index 00000000..86a1d131 --- /dev/null +++ b/cli/cli.rb @@ -0,0 +1,108 @@ +require 'thor' +require 'colorize' +require 'sequel' +require 'bcrypt' +require 'readline' +require 'yaml' +require_relative '../ruby/db.rb' + +settings = YAML.load_file(File.join(File.dirname(__FILE__), '../config/settings.yml')) +$host = ENV['DB_HOST'] || settings['database']['host'].to_s +$user = ENV['DB_USERNAME'] || settings['database']['username'].to_s +$password = ENV['DB_PASSWORD'] || settings['database']['password'].to_s +$database = ENV['DB_DATABASE'] || settings['database']['dbname'].to_s + +class RubyCLI < Thor + desc "create", "Create a new user" + def create + puts "Creating a new user...".red + username = Readline.readline("Username: ", true) + username = username.gsub(/[^0-9A-Za-z]/, '') + username = username.downcase + while true + password = Readline.readline("Password: ", true) + passwordConfirm = Readline.readline("Retype password: ", true) + if password != passwordConfirm + puts "\e[H\e[2J" + puts "Passwords do not match! Try again.".red + else + break + end + end + + db = connectDB($host, $user, $password, $database) + hashedPassword = BCrypt::Password.create(password) + if db[:users].where(username: username).count > 0 + puts "User already exists!".red + db.disconnect + exit + else + db[:users].insert(username: username, password: hashedPassword, admin: false) + puts "User created!".blue + db.disconnect + end + + end + desc "delete", "Delete a user" + def delete + puts "Deleting a user...".red + username = Readline.readline("Username: ", true) + username = username.gsub(/[^0-9A-Za-z]/, '') + username = username.downcase + db = connectDB($host, $user, $password, $database) + while true + usernameConfirm = Readline.readline("Are you sure you want to delete #{username}? (y/n): ", true).downcase + if usernameConfirm == "y" || usernameConfirm == "yes" + if db[:users].where(username: username).count > 0 + db[:users].where(username: username).delete + puts "User deleted!".blue + db.disconnect + else + puts "User does not exist! (use the list command to see all users)".red + db.disconnect + end + exit + elsif usernameConfirm == "n" || usernameConfirm == "no" + puts "Ok, exiting...".blue + db.disconnect + exit + end + end + end + desc "list", "List all users" + def list + db = connectDB($host, $user, $password, $database) + puts "Listing all users...".red + users = db[:users] + users.each{|user| puts user[:username]} + end + desc "reset", "Reset a user's password" + def reset + puts "Resetting a user's password...".red + username = Readline.readline("Username: ", true) + username = username.gsub(/[^0-9A-Za-z]/, '') + username = username.downcase + db = connectDB($host, $user, $password, $database) + if db[:users].where(username: username).count > 0 + while true + password = Readline.readline("New Password: ", true) + passwordConfirm = Readline.readline("Retype new password: ", true) + if password != passwordConfirm + puts "\e[H\e[2J" + puts "Passwords do not match! Try again.".red + else + break + end + end + hashedPassword = BCrypt::Password.create(password) + db[:users].where(username: username).update(password: hashedPassword) + puts "Password reset!".blue + db.disconnect + else + puts "User does not exist! (use the list command to see all users)".red + db.disconnect + end + end +end + +RubyCLI.start(ARGV) diff --git a/config/settings.example.yml b/config/settings.example.yml index d879c4f4..475a0207 100644 --- a/config/settings.example.yml +++ b/config/settings.example.yml @@ -1,6 +1,18 @@ -port: 9293 -verboseLogging: "false" -private: "false" -username: "ruby" -password: "ruby" -mainURL: "http://localhost:3000/browser/" +port: 9293 #currently does nothing, but will be used in the future +verboseLogging: "false" #change this to "true" to enable verbose logging +private: "false" #change this to "true" to enable private mode +username: "ruby" #change this to your username (when using private mode) +password: "ruby" #change this to your password (when using private mode) + +# Everything below this line is optional is some form or another +mainURL: "https://localhost:9293/" # set to a URL to redirect to when the user visits the root of the server (e.g. http://example.com/) WILL be ignored when private mode is enabled +multiuser: "true" # set to true to enable multiuser mode when using private mode (if not using private mode, this will be ignored) + +# Database Settings Only Needed When Using Multiuser Mode is Enabled +# These are ignored when multiuser mode is not enabled +# NOTE: when using docker these values should not be changed +database: + username: "ruby" # change this to your database username + password: "ruby" # change this to your database password + host: "db" # change this to your database host + dbname: "ruby" # change this to your database name diff --git a/docker/docker-compose.build.yml b/docker/docker-compose.build.yml index 0451ae21..561e3f3a 100644 --- a/docker/docker-compose.build.yml +++ b/docker/docker-compose.build.yml @@ -10,7 +10,21 @@ services: - your port here:9293 volumes: - ./config.yml:/usr/src/app/config/settings.yml -#networks: -# default: -# external: -# name: default_net + # + # Uncomment the following lines if you want to use a database (mutliuser mode) + #db: + # image: postgres + # restart: unless-stopped + # environment: + # POSTGRES_PASSWORD: ruby + # POSTGRES_USER: ruby + # POSTGRES_DB: ruby + # volumes: + # - ./db:/var/lib/postgresql/data + + # Uncomment the following lines if you want to use adminer (database management) + #adminer: + # image: adminer + # restart: unless-stopped + # ports: + # - 8099:8080 diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 35171840..5b7571fc 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,14 +1,29 @@ -version: '2' +version: '3' services: ruby: restart: unless-stopped - image: 'ghcr.io/ruby-network/ruby:latest' + image: 'motortruck1221/ruby:latest' ports: #DO NOT CHANGE 9293! - - 9293:9293 + - your port here:9293 volumes: - ./config.yml:/usr/src/app/config/settings.yml -#networks: - # default: - # external: - # name: default_net + + # + # Uncomment the following lines if you want to use a database (mutliuser mode) + #db: + # image: postgres + # restart: unless-stopped + # environment: + # POSTGRES_PASSWORD: ruby + # POSTGRES_USER: ruby + # POSTGRES_DB: ruby + # volumes: + # - ./db:/var/lib/postgresql/data + + # Uncomment the following lines if you want to use adminer (database management) + #adminer: + # image: adminer + # restart: unless-stopped + # ports: + # - 8099:8080 diff --git a/docs/README.md b/docs/README.md index e69f6d23..fcf444a2 100644 --- a/docs/README.md +++ b/docs/README.md @@ -11,7 +11,8 @@ ## Getting Started - Local setup (no docker) [here](./terminal.md) - Local setup (with docker)(*recommended*) [here](./docker.md) -- Private instance setup [here](./private.md) (both docker and non-docker) +- Private instance setup [here](./private.md) (both docker and non-docker, including multiuser) +- CLI commands for multiuser mode [here](./multiuser.md) - Ruby and Bundler installation [here](./install-ruby.md) - Docker Installation [here](./docker-install.md) diff --git a/docs/advanced-config.md b/docs/advanced-config.md index e31d3dcc..fef90e56 100644 --- a/docs/advanced-config.md +++ b/docs/advanced-config.md @@ -3,20 +3,28 @@ ##### This provides a list of all the configuration options available to you. ##### The config file can be found [here](../config/settings.example.yml) -`port` - The port the Ruby server will run on. Default is `9293` +- `port` - The port the Ruby server will run on. Default is `9293` (does not currently work) -`verboseLogging` - Whether or not to log all requests to the console. Default is `false` +- `verboseLogging` - Whether or not to log all requests to the console. Default is `false` -`private` - Whether or not to enable private mode. Default is `false` +- `private` - Whether or not to enable private mode. Default is `false` -`username` - The username for use in either, private instances. If it is a normal instance, username will always be `ruby` +- `username` - The username for use in either, private instances. If it is a normal instance, username will always be `ruby` -`password` - The password for use in either, private instances. If it is a normal instance, password will always be `ruby` +- `password` - The password for use in either, private instances. If it is a normal instance, password will always be `ruby` -`mainUrl` - The main URL for use in a normal instance. If you are trying to make a private instance set this value to `NA` +- `mainUrl` - The main URL for use in a normal instance. If you are trying to make a private instance set this value to anything or delete it. + +- `multiuser` - Whether or not to enable multiuser mode. Default is `false` **ONLY WORKS IN PRIVATE MODE** + +- `database` - A set of options for the database connection. **ONLY WORKS IN PRIVATE MODE AND MULTIUSER IS ENABLED, CURRENTLY ONLY SUPPORTS POSTGRESQL** + - `host` - The host of the database. Default is `localhost` + - `dbName` - The name of the database. Default is `ruby` + - `username` - The username for the database. Default is `ruby` + - `password` - The password for the database. Default is `ruby` --- #### Options coming soon: -`port` - Will be switched to `rubyPort` and will be the port the Ruby server will run on. Default is `9292` +- `port` - Will be switched to `rubyPort` and will be the port the Ruby server will run on. Default is `9292` -`nodePort` - Will be the port the Node server will run on. Default is `9293` +- `nodePort` - Will be the port the Node server will run on. Default is `9293` diff --git a/docs/docker.md b/docs/docker.md index c3a00931..d48fa49e 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -31,20 +31,35 @@ Or just copy this config: ```yml - version: '2' + version: "3" services: - ruby: - restart: unless-stopped - image: 'ghcr.io/ruby-network/ruby' - ports: - #DO NOT CHANGE 9293! - - your port here:9293 - volumes: - - ./config.yml:/usr/src/app/config/settings.yml - #networks: - # default: - # external: - # name: default_net + ruby: + image: 'motortruck1221/ruby:latest' + container_name: ruby + restart: unless-stopped + ports: + # DO NOT CHANGE 9293 + - "your port here:9293" + volumes: + - ./config.yml:/usr/src/app/config/settings.yml + + # Uncomment the following lines if you want to use a database (multiuser mode) + #db: + # image: postgres + # restart: unless-stopped + # environment: + # POSTGRES_PASSWORD: ruby + # POSTGRES_USER: ruby + # POSTGRES_DB: ruby + # volumes: + # - ./db:/var/lib/postgresql/data + + # Uncomment the following lines if you want to use adminer (database management) + #adminer: + # image: adminer + # restart: unless-stopped + # ports: + # - 8099:8080 ``` 2. Download our settings.example.yml file [here](https://github.com/ruby-network/ruby/tree/main/config/settings.example.yml) diff --git a/docs/multiuser.md b/docs/multiuser.md new file mode 100644 index 00000000..21b862f7 --- /dev/null +++ b/docs/multiuser.md @@ -0,0 +1,25 @@ +# Multiuser mode + +## Prerequisites +- A setup private instance of Ruby (using Docker Compose, or standalone) with multiuser mode enabled (see [here](./private.md#docker-multiuser) for more info) + +--- + +## How to execute commands + +There are two ways to execute the CLI, either using `yarn cli` or `bundler exec ruby ./cli/cli.rb` + +This tutorial will use `yarn cli` as it is easier to type + +## Commands + +- `yarn cli` is the command to execute the CLI +- `yarn cli help [command]` to get help with a command +- `yarn cli create` - Create a new user +- `yarn cli delete` - Delete a user +- `yarn cli list` - List all users +- `yarn cli reset` - Reset a users password + +## How to use in Docker Compose + +- `docker-compose exec ruby yarn cli [command]` - where `[command]` is one of the commands listed above diff --git a/docs/private.md b/docs/private.md index e94290b7..6440c17b 100644 --- a/docs/private.md +++ b/docs/private.md @@ -3,7 +3,7 @@ ## Notes - I am expecting you to be using Linux. If you are not the commands should be easily translatable to the shell you are using -## No Docker +## No Docker (one user) #### Prerequisites - Ruby needs to be installed - Don't know how? Follow our [guide](./install-ruby.md) @@ -34,8 +34,7 @@ private: "true" username: "your username" password: "your password" - # DO NOT CHANGE THE VALUE BELOW - mainURL: "NA" + multiuser: "false" ``` 6. Start the server ```bash @@ -43,7 +42,7 @@ ``` 7. You should now be able to access your instance at `http://localhost:9293` -## Docker +## Docker (one user) #### Prerequisites - Docker needs to be installed - Don't know how? Follow our [guide](./docker-install.md) @@ -63,12 +62,59 @@ private: "true" username: "your username" password: "your password" - # DO NOT CHANGE THE VALUE BELOW - mainURL: "NA" + multiuser: "false" ``` 3. Follow the commands in the [docker instance](./docker.md) guide depending on what docker method you used - Docker Compose [here](./docker.md#docker-compose) - Docker Compose (build) [here](./docker.md#docker-compose-build) - - Standalone Docker (not recommended) [here](./docker.md#standalone) + - Standalone Docker (not recommended) [here](./docker.md#standalone) **NOT RECOMMENDED** 3a. Simply omit the step where it tells you to make a config.yml file (as you have already done that) + +## No Docker (multiuser) - **NOT SUPPORTED** +This is not supported due to the fact that everyones setup is different. +Here is a list of things you will need to do: +- Setup a database (Postgresql) +- Setup ruby to use multiuser mode + +**Use docker if you want multiuser mode [here](./private.md#docker-multiuser)** + +## Docker (multiuser) + +#### Prerequisites +- Docker needs to be installed - Don't know how? Follow our [guide](./docker-install.md) + +--- + +##### Most of the commands are the same as the ones in a public [docker instance](./docker.md) + +1. Make a config.yml file + ```bash + nano config.yml + ``` + +2. To make a private instance with multiuser the file should look something like this: + ```yml + port: 9293 #currently does nothing, but will be used in the future + verboseLogging: "false" #change this to "true" to enable verbose logging + private: "true" #change this to "true" to enable private mode + username: "yourUsername" #change this to your username (when using private mode) + password: "yourPassword" #change this to your password (when using private mode) + + multiuser: "true" # set to true to enable multiuser mode when using private mode (if not using private mode, this will be ignored) + + database: + #The db defaults should not be changed when using docker (unless you know what you are doing) + username: "ruby" # change this to your database username + password: "ruby" # change this to your database password + host: "db" # change this to your database host + dbname: "ruby" # change this to your database name + ``` +3. Follow the commands in the [docker instance](./docker.md) guide depending on what docker method you used. **Make sure you uncomment the database section of the file** + - Docker Compose [here](./docker.md#docker-compose) + - Docker Compose (build) [here](./docker.md#docker-compose-build) + - Standalone Docker (not recommended) [here](./docker.md#standalone) **NOT SUPPORTED** + +3a. Simply omit the step where it tells you to make a config.yml file (as you have already done that) + +3b. For multiuser mode, we provide a set of CLI commands to manage users. You can find them [here](./multiuser.md) diff --git a/main.rb b/main.rb index 04ced05e..8d03a5fd 100644 --- a/main.rb +++ b/main.rb @@ -4,7 +4,17 @@ set :public_folder, File.join(settings.root, 'src', 'public') set :views, File.join(settings.root, 'src', 'views') set :template, File.join(settings.root, 'src', 'templates') + +Config.setup do |config| + config.use_env = true + config.env_prefix = 'SETTINGS' + config.env_separator = '_' + config.env_converter = :downcase + config.env_parse_values = true +end + register Config + logging = Settings.verboseLogging if logging == "true" puts "Verbose logging is enabled".green @@ -28,6 +38,10 @@ validateYML() #Validate the ENV variables validateEnv() +#Setup DB when Private is true +if Settings.private == "true" && Settings.multiuser == "true" + dbSetup() +end #Encrypted cookies use Rack::Session::EncryptedCookie, cookie_options #csrf @@ -40,8 +54,12 @@ if request.path_info == '/auth' return #any route on the main domain - elsif request.url.include? ENV['DOMAIN'] || Settings.mainURL - return + elsif Settings.private == "false" + if request.url.include? ENV['DOMAIN'] || Settings.mainURL + return + else + auth() + end else auth() end @@ -61,11 +79,22 @@ #Auth to login to the site post '/auth' do - if params[:password] == Settings.password && params[:username] == Settings.username - session[:auth] = true - session[:uid] = SecureRandom.alphanumeric(2048) - redirect '/' + if Settings.private == "false" || Settings.multiuser == "false" + if params[:password] == Settings.password && params[:username].downcase == Settings.username.downcase + session[:auth] = true + session[:uid] = SecureRandom.alphanumeric(2048) + redirect '/' + else + redirect '/' + end else - redirect '/' + loggedIn = login(params[:username], params[:password]) + if loggedIn == true + session[:auth] = true + session[:uid] = SecureRandom.alphanumeric(2048) + redirect '/' + else + redirect '/' + end end end diff --git a/node-server/server.js b/node-server/server.js index eeb9e53b..0ae553ff 100644 --- a/node-server/server.js +++ b/node-server/server.js @@ -53,7 +53,7 @@ const proxyHandler = (handler, opts) => { }; const app = Fastify({ logger: false, serverFactory: proxyHandler }) -await app +await app .register(fastifyHttpProxy, { upstream: 'http://localhost:9292', prefix: '/', @@ -82,7 +82,7 @@ app.get('/search=:query', async (req, res) => { reply.code(500).send({ error: "Internal Server Error" }); } }); -app.get('/version', async (req, res) => { +app.get('/version', (req, res) => { res.send({ version: latestRelease }); }); app.get('/health', async (req, res) => { diff --git a/package.json b/package.json index 730aca4e..c42d1fb7 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "dev": "bundle exec rerun --ignore 'src/public/css/*' --signal 'TERM' -c -w 5 --no-notify -- puma", "start": "bundle exec puma -e production", "install": "bundle install && npm run build", - "build": "node ./build.js" + "build": "node ./build.js", + "cli": "bundle exec ruby ./cli/cli.rb" }, "engines": { "node": ">=18.0.0" diff --git a/postgres.yml b/postgres.yml new file mode 100644 index 00000000..af5df55b --- /dev/null +++ b/postgres.yml @@ -0,0 +1,19 @@ +version: '3.1' + +services: + + db: + image: postgres + restart: always + environment: + POSTGRES_PASSWORD: example + POSTGRES_USER: example + POSTGRES_DB: example + ports: + - 5432:5432 + + adminer: + image: adminer + restart: always + ports: + - 8099:8080 diff --git a/rammerhead/README.md b/rammerhead/README.md index 4a53ecbe..759e11f3 100644 --- a/rammerhead/README.md +++ b/rammerhead/README.md @@ -1,2 +1,2 @@ -# Rammerhead source files for the browser. Compiled for older browsers via [Esbuild](https://esbuild.github.io/). +# Rammerhead source files for the browser. Compiled for older browsers via [ESBuild](https://esbuild.github.io/). diff --git a/require.rb b/require.rb index 529b1496..8311ecc8 100644 --- a/require.rb +++ b/require.rb @@ -10,7 +10,10 @@ require 'dry/schema' require 'dry/validation' require 'yaml' +require 'sequel' +require 'bcrypt' require './ruby/utils.rb' require './ruby/uv.rb' require './ruby/auth.rb' require './ruby/validator.rb' +require './ruby/db.rb' diff --git a/ruby/db.rb b/ruby/db.rb new file mode 100644 index 00000000..a742feb9 --- /dev/null +++ b/ruby/db.rb @@ -0,0 +1,59 @@ +def connectDB(host, user, password, database) + db = Sequel.postgres(host: host, user: user, password: password, database: database) + return db +end + +def defineDBVars() + $host = ENV['DB_HOST'] || Settings.database.host + $user = ENV['DB_USERNAME'] || Settings.database.username + $password = ENV['DB_PASSWORD'] || Settings.database.password + $database = ENV['DB_DATABASE'] || Settings.database.dbname +end + +def dbSetup() + puts "Setting up database...".green + defineDBVars() + db = connectDB($host, $user, $password, $database) + puts "Creating table 'users' (if it does not exist)...".green + if db.table_exists?(:users) + puts "Table 'users' already exists.".yellow + else + db.create_table :users do + primary_key :id + String :username + String :password + Boolean :admin + end + end + + puts "Adding user admin user: #{Settings.username.downcase} (if it does not exist)".green + if db[:users].where(username: Settings.username.downcase).count >= 1 + puts "User #{Settings.username.downcase} already exists.".yellow + else + password = BCrypt::Password.create(Settings.password) + db[:users].insert(username: Settings.username.downcase, password: password, admin: true) + end + db.disconnect + puts "Database setup complete.".green + return +end + +def login(username, password) + username = username.gsub(/[^0-9A-Za-z]/, '') + username = username.downcase + password = password.gsub(/[^0-9A-Za-z]/, '') + db = connectDB($host, $user, $password, $database) + hashedPassword = db[:users].where(username: username).get(:password) + begin + if BCrypt::Password.new(hashedPassword) == password + db.disconnect + return true + else + db.disconnect + return false + end + rescue BCrypt::Errors::InvalidHash + db.disconnect + return false + end +end diff --git a/ruby/validator.rb b/ruby/validator.rb index cb5406e8..26872403 100644 --- a/ruby/validator.rb +++ b/ruby/validator.rb @@ -5,7 +5,14 @@ class YamlValidator < Dry::Validation::Contract required(:private).filled(:string) required(:username).filled(:string) required(:password).filled(:string) - required(:mainURL).filled(:string) + optional(:multiuser).filled(:string) + optional(:mainURL).filled(:string) + optional(:database).filled(:hash).schema do + required(:host).filled(:string) + required(:username).filled(:string) + required(:password).filled(:string) + required(:dbname).filled(:string) + end end rule(:port) do key.failure('must be greater than 0') if value <= 0 @@ -20,8 +27,6 @@ class YamlValidator < Dry::Validation::Contract if (Settings.private == "false") key.failure('must have a url') if value !~ /\A#{URI::regexp(['http', 'https'])}\z/ key.failure('must have a / at the end') if value !~ /\/\z/ - else - key.failure('must NOT have a url') if value =~ /\A#{URI::regexp(['http', 'https'])}\z/ end end rule(:username) do @@ -40,7 +45,13 @@ class YamlValidator < Dry::Validation::Contract key.failure('the password must NOT be "ruby"') if value == "ruby" end end -end + rule(:multiuser) do + if (Settings.private == "true") + key.failure('multiuser is required to be used when private mode is enabled') if value == nil + key.failure('MUST BE TRUE OR FALSE') if value != "true" && value != "false" + end + end + end def validateYML config = YAML.load_file(File.join(settings.root, '/config/settings.yml'))