@@ -1,7 +0,0 @@
|
|||||||
# Used by "mix format"
|
|
||||||
[
|
|
||||||
inputs: [
|
|
||||||
".formatter.exs",
|
|
||||||
"account.exs"
|
|
||||||
]
|
|
||||||
]
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
FROM elixir:1.14-slim
|
|
||||||
|
|
||||||
RUN mix local.hex --force && \
|
|
||||||
mix local.rebar --force
|
|
||||||
|
|
||||||
COPY account.exs /account.exs
|
|
||||||
COPY srp.exs /srp.exs
|
|
||||||
RUN chmod +x /account.exs
|
|
||||||
|
|
||||||
CMD /account.exs
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
# Account.exs
|
|
||||||
|
|
||||||
Simple script to create an account for AzerothCore
|
|
||||||
|
|
||||||
This script allows a server admin to create a user automatically when after the `dbimport` tool runs, without needed to open up the `worldserver` console.
|
|
||||||
|
|
||||||
## How To Use
|
|
||||||
|
|
||||||
### Pre-requisites
|
|
||||||
|
|
||||||
- MySQL is running
|
|
||||||
- The authserver database (`acore_auth`, typically) has a table named `account`
|
|
||||||
|
|
||||||
### Running
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ elixir account.exs
|
|
||||||
```
|
|
||||||
|
|
||||||
### Configuration
|
|
||||||
|
|
||||||
This script reads from environment variables in order to control which account it creates and the MySQL server it's communicating with
|
|
||||||
|
|
||||||
|
|
||||||
- `ACORE_USERNAME` Username for account, default "admin"
|
|
||||||
- `ACORE_PASSWORD` Password for account, default "admin"
|
|
||||||
- `ACORE_GM_LEVEL` GM Level for account, default 3
|
|
||||||
- `MYSQL_DATABASE` Database name, default "acore_auth"
|
|
||||||
- `MYSQL_USERNAME` MySQL username, default "root"
|
|
||||||
- `MYSQL_PASSWORD` MySQL password, default "password"
|
|
||||||
- `MYSQL_PORT` MySQL Port, default 3306
|
|
||||||
- `MYSQL_HOST` MySQL Host, default "localhost"
|
|
||||||
|
|
||||||
To use these environment variables, execute the script like so:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ MYSQL_HOST=mysql \
|
|
||||||
MYSQL_PASSWORD="fourthehoard" \
|
|
||||||
ACORE_USERNAME=drekthar \
|
|
||||||
ACORE_PASSWORD=securepass22 \
|
|
||||||
elixir account.exs
|
|
||||||
```
|
|
||||||
|
|
||||||
This can also be used in a loop. Consider this csv file:
|
|
||||||
|
|
||||||
```csv
|
|
||||||
user,pass,gm_level
|
|
||||||
admin,adminpass,2
|
|
||||||
soapuser,soappass,3
|
|
||||||
mainuser,userpass,0
|
|
||||||
```
|
|
||||||
|
|
||||||
You can then loop over this csv file, and manage users like so:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ while IFS=, read -r user pass gm; do
|
|
||||||
ACORE_USERNAME=$user \
|
|
||||||
ACORE_PASSWORD=$pass \
|
|
||||||
GM_LEVEL=$gm \
|
|
||||||
elixir account.exs
|
|
||||||
done <<< $(tail -n '+2' users.csv)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker
|
|
||||||
|
|
||||||
Running and building with docker is simple:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ docker build -t acore/account-create .
|
|
||||||
$ docker run \
|
|
||||||
-e MYSQL_HOST=mysql \
|
|
||||||
-v mix_cache:/root/.cache/mix/installs \
|
|
||||||
acore/account-create
|
|
||||||
```
|
|
||||||
|
|
||||||
Note that the `MYSQL_HOST` is required to be set with the docker container, as the default setting targets `localhost`.
|
|
||||||
|
|
||||||
### docker-compose
|
|
||||||
|
|
||||||
A simple way to integrate this into a docker-compose file.
|
|
||||||
|
|
||||||
This is why I wrote this script - an automatic way to have an admin account idempotently created on startup of the server.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
services:
|
|
||||||
account-create:
|
|
||||||
image: acore/account-create:${DOCKER_IMAGE_TAG:-master}
|
|
||||||
build:
|
|
||||||
context: apps/account-create/
|
|
||||||
dockerfile: apps/account-create/Dockerfile
|
|
||||||
environment:
|
|
||||||
MYSQL_HOST: ac-database
|
|
||||||
MYSQL_PASSWORD: ${DOCKER_DB_ROOT_PASSWORD:-password}
|
|
||||||
ACORE_USERNAME: ${ACORE_ROOT_ADMIN_ACCOUNT:-admin}
|
|
||||||
ACORE_PASSWORD: ${ACORE_ROOT_ADMIN_PASSWORD:-password}
|
|
||||||
volumes:
|
|
||||||
- mix_cache:/root/.cache/mix/installs
|
|
||||||
profiles: [local, app, db-import-local]
|
|
||||||
depends_on:
|
|
||||||
ac-db-import:
|
|
||||||
condition: service_completed_successfully
|
|
||||||
```
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
#!/usr/bin/env elixir
|
|
||||||
# Execute this Elixir script with the below command
|
|
||||||
#
|
|
||||||
# $ ACORE_USERNAME=foo ACORE_PASSWORD=barbaz123 elixir account.exs
|
|
||||||
#
|
|
||||||
# Set environment variables for basic configuration
|
|
||||||
#
|
|
||||||
# ACORE_USERNAME - Username for account, default "admin"
|
|
||||||
# ACORE_PASSWORD - Password for account, default "admin"
|
|
||||||
# ACORE_GM_LEVEL - GM level for account
|
|
||||||
# MYSQL_DATABASE - Database name, default "acore_auth"
|
|
||||||
# MYSQL_USERNAME - MySQL username, default "root"
|
|
||||||
# MYSQL_PASSWORD - MySQL password, default "password"
|
|
||||||
# MYSQL_PORT - MySQL Port, default 3306
|
|
||||||
# MYSQL_HOST - MySQL Host, default "localhost"
|
|
||||||
|
|
||||||
# Install remote dependencies
|
|
||||||
[
|
|
||||||
{:myxql, "~> 0.6.0"}
|
|
||||||
]
|
|
||||||
|> Mix.install()
|
|
||||||
|
|
||||||
# Start the logger
|
|
||||||
Application.start(:logger)
|
|
||||||
require Logger
|
|
||||||
|
|
||||||
# Constants
|
|
||||||
default_credential = "admin"
|
|
||||||
default_gm_level = "3"
|
|
||||||
account_access_comment = "Managed via account-create script"
|
|
||||||
|
|
||||||
# Import srp functions
|
|
||||||
Code.require_file("srp.exs", Path.absname(__DIR__))
|
|
||||||
|
|
||||||
# Assume operator provided a "human-readable" name.
|
|
||||||
# The database stores usernames in all caps
|
|
||||||
username_lower =
|
|
||||||
System.get_env("ACORE_USERNAME", default_credential)
|
|
||||||
|> tap(&Logger.info("Account to create: #{&1}"))
|
|
||||||
|
|
||||||
username = String.upcase(username_lower)
|
|
||||||
|
|
||||||
password = System.get_env("ACORE_PASSWORD", default_credential)
|
|
||||||
|
|
||||||
gm_level = System.get_env("ACORE_GM_LEVEL", default_gm_level) |> String.to_integer()
|
|
||||||
|
|
||||||
if Range.new(0, 3) |> Enum.member?(gm_level) |> Kernel.not do
|
|
||||||
Logger.info("Valid ACORE_GM_LEVEL values are 0, 1, 2, and 3. The given value was: #{gm_level}.")
|
|
||||||
end
|
|
||||||
|
|
||||||
{:ok, pid} =
|
|
||||||
MyXQL.start_link(
|
|
||||||
protocol: :tcp,
|
|
||||||
database: System.get_env("MYSQL_DATABASE", "acore_auth"),
|
|
||||||
username: System.get_env("MYSQL_USERNAME", "root"),
|
|
||||||
password: System.get_env("MYSQL_PASSWORD", "password"),
|
|
||||||
port: System.get_env("MYSQL_PORT", "3306") |> String.to_integer(),
|
|
||||||
hostname: System.get_env("MYSQL_HOST", "localhost")
|
|
||||||
)
|
|
||||||
|
|
||||||
Logger.info("MySQL connection created")
|
|
||||||
|
|
||||||
Logger.info("Checking database for user #{username_lower}")
|
|
||||||
|
|
||||||
# Check if user already exists in database
|
|
||||||
{:ok, result} = MyXQL.query(pid, "SELECT salt FROM account WHERE username=?", [username])
|
|
||||||
|
|
||||||
%{salt: salt, verifier: verifier} =
|
|
||||||
case result do
|
|
||||||
%{rows: [[salt | _] | _]} ->
|
|
||||||
Logger.info("Salt for #{username_lower} found in database")
|
|
||||||
# re-use the salt if the user exists in database
|
|
||||||
Srp.generate_stored_values(username, password, salt)
|
|
||||||
_ ->
|
|
||||||
Logger.info("Salt not found in database for #{username_lower}. Generating a new one")
|
|
||||||
Srp.generate_stored_values(username, password)
|
|
||||||
end
|
|
||||||
|
|
||||||
Logger.info("New salt and verifier generated")
|
|
||||||
|
|
||||||
# Insert values into DB, replacing the verifier if the user already exists
|
|
||||||
result =
|
|
||||||
MyXQL.query(
|
|
||||||
pid,
|
|
||||||
"""
|
|
||||||
INSERT INTO account
|
|
||||||
(`username`, `salt`, `verifier`)
|
|
||||||
VALUES
|
|
||||||
(?, ?, ?)
|
|
||||||
ON DUPLICATE KEY UPDATE verifier=?
|
|
||||||
""",
|
|
||||||
[username, salt, verifier, verifier]
|
|
||||||
)
|
|
||||||
|
|
||||||
case result do
|
|
||||||
{:error, %{message: message}} ->
|
|
||||||
File.write("fail.log", message)
|
|
||||||
|
|
||||||
Logger.info(
|
|
||||||
"Account #{username_lower} failed to create. You can check the error message at fail.log."
|
|
||||||
)
|
|
||||||
|
|
||||||
exit({:shutdown, 1})
|
|
||||||
|
|
||||||
# if num_rows changed and last_insert_id == 0, it means the verifier matched. No change necessary
|
|
||||||
{:ok, %{num_rows: 1, last_insert_id: 0}} ->
|
|
||||||
Logger.info(
|
|
||||||
"Account #{username_lower} doesn't need to have its' password changed. You should be able to log in with that account"
|
|
||||||
)
|
|
||||||
|
|
||||||
{:ok, %{num_rows: 1}} ->
|
|
||||||
Logger.info(
|
|
||||||
"Account #{username_lower} has been created. You should now be able to login with that account"
|
|
||||||
)
|
|
||||||
|
|
||||||
{:ok, %{num_rows: 2}} ->
|
|
||||||
Logger.info(
|
|
||||||
"Account #{username_lower} has had its' password reset. You should now be able to login with that account"
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Set GM level to configured value
|
|
||||||
{:ok, _} =
|
|
||||||
MyXQL.query(
|
|
||||||
pid,
|
|
||||||
"""
|
|
||||||
INSERT INTO account_access
|
|
||||||
(`id`, `gmlevel`, `comment`)
|
|
||||||
VALUES
|
|
||||||
((SELECT id FROM account WHERE username=?), ?, ?)
|
|
||||||
ON DUPLICATE KEY UPDATE gmlevel=?, comment=?
|
|
||||||
""", [username, gm_level, account_access_comment, gm_level, account_access_comment])
|
|
||||||
|
|
||||||
Logger.info("GM Level for #{username_lower} set to #{gm_level}")
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
defmodule Srp do
|
|
||||||
# Constants required in WoW's SRP6 implementation
|
|
||||||
@n <<137, 75, 100, 94, 137, 225, 83, 91, 189, 173, 91, 139, 41, 6, 80, 83, 8, 1, 177, 142, 191,
|
|
||||||
191, 94, 143, 171, 60, 130, 135, 42, 62, 155, 183>>
|
|
||||||
@g <<7>>
|
|
||||||
@field_length 32
|
|
||||||
|
|
||||||
# Wrapper function
|
|
||||||
def generate_stored_values(username, password, salt \\ "") do
|
|
||||||
default_state()
|
|
||||||
|> generate_salt(salt)
|
|
||||||
|> calculate_x(username, password)
|
|
||||||
|> calculate_v()
|
|
||||||
end
|
|
||||||
|
|
||||||
def default_state() do
|
|
||||||
%{n: @n, g: @g}
|
|
||||||
end
|
|
||||||
|
|
||||||
# Generate salt if it's not defined
|
|
||||||
def generate_salt(state, "") do
|
|
||||||
salt = :crypto.strong_rand_bytes(32)
|
|
||||||
Map.merge(state, %{salt: salt})
|
|
||||||
end
|
|
||||||
|
|
||||||
# Don't generate salt if it's already defined
|
|
||||||
def generate_salt(state, salt) do
|
|
||||||
padded_salt = pad_binary(salt)
|
|
||||||
Map.merge(state, %{salt: padded_salt})
|
|
||||||
end
|
|
||||||
|
|
||||||
# Get h1
|
|
||||||
def calculate_x(state, username, password) do
|
|
||||||
hash = :crypto.hash(:sha, String.upcase(username) <> ":" <> String.upcase(password))
|
|
||||||
x = reverse(:crypto.hash(:sha, state.salt <> hash))
|
|
||||||
Map.merge(state, %{x: x, username: username})
|
|
||||||
end
|
|
||||||
|
|
||||||
# Get h2
|
|
||||||
def calculate_v(state) do
|
|
||||||
verifier =
|
|
||||||
:crypto.mod_pow(state.g, state.x, state.n)
|
|
||||||
|> reverse()
|
|
||||||
|> pad_binary()
|
|
||||||
|
|
||||||
Map.merge(state, %{verifier: verifier})
|
|
||||||
end
|
|
||||||
|
|
||||||
defp pad_binary(blob) do
|
|
||||||
pad = @field_length - byte_size(blob)
|
|
||||||
<<blob::binary, 0::pad*8>>
|
|
||||||
end
|
|
||||||
|
|
||||||
defp reverse(binary) do
|
|
||||||
binary
|
|
||||||
|> :binary.decode_unsigned(:big)
|
|
||||||
|> :binary.encode_unsigned(:little)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
Reference in New Issue
Block a user